src/core/library.c (view raw)
1/* Copyright (c) 2013-2017 Jeffrey Pfau
2 *
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6#include <mgba/core/library.h>
7
8#include <mgba/core/core.h>
9#include <mgba-util/vfs.h>
10
11#ifdef USE_SQLITE3
12
13#include <sqlite3.h>
14
15DEFINE_VECTOR(mLibraryListing, struct mLibraryEntry);
16
17struct mLibrary {
18 sqlite3* db;
19 sqlite3_stmt* insertPath;
20 sqlite3_stmt* insertRom;
21 sqlite3_stmt* insertRoot;
22 sqlite3_stmt* selectRom;
23 sqlite3_stmt* selectRoot;
24 sqlite3_stmt* deletePath;
25 sqlite3_stmt* deleteRoot;
26 sqlite3_stmt* count;
27 sqlite3_stmt* select;
28};
29
30#define CONSTRAINTS_ROMONLY \
31 "CASE WHEN :useSize THEN roms.size = :size ELSE 1 END AND " \
32 "CASE WHEN :usePlatform THEN roms.platform = :platform ELSE 1 END AND " \
33 "CASE WHEN :useCrc32 THEN roms.crc32 = :crc32 ELSE 1 END AND " \
34 "CASE WHEN :useInternalCode THEN roms.internalCode = :internalCode ELSE 1 END"
35
36#define CONSTRAINTS \
37 CONSTRAINTS_ROMONLY " AND " \
38 "CASE WHEN :useFilename THEN paths.path = :path ELSE 1 END AND " \
39 "CASE WHEN :useRoot THEN roots.path = :root ELSE 1 END"
40
41static void _mLibraryDeleteEntry(struct mLibrary* library, struct mLibraryEntry* entry);
42static void _mLibraryInsertEntry(struct mLibrary* library, struct mLibraryEntry* entry);
43static void _mLibraryAddEntry(struct mLibrary* library, const char* filename, const char* base, struct VFile* vf);
44
45static void _bindConstraints(sqlite3_stmt* statement, const struct mLibraryEntry* constraints) {
46 if (!constraints) {
47 return;
48 }
49
50 int useIndex, index;
51 if (constraints->crc32) {
52 useIndex = sqlite3_bind_parameter_index(statement, ":useCrc32");
53 index = sqlite3_bind_parameter_index(statement, ":crc32");
54 sqlite3_bind_int(statement, useIndex, 1);
55 sqlite3_bind_int(statement, index, constraints->crc32);
56 }
57
58 if (constraints->filesize) {
59 useIndex = sqlite3_bind_parameter_index(statement, ":useSize");
60 index = sqlite3_bind_parameter_index(statement, ":size");
61 sqlite3_bind_int(statement, useIndex, 1);
62 sqlite3_bind_int64(statement, index, constraints->filesize);
63 }
64
65 if (constraints->filename) {
66 useIndex = sqlite3_bind_parameter_index(statement, ":useFilename");
67 index = sqlite3_bind_parameter_index(statement, ":path");
68 sqlite3_bind_int(statement, useIndex, 1);
69 sqlite3_bind_text(statement, index, constraints->filename, -1, SQLITE_TRANSIENT);
70 }
71
72 if (constraints->base) {
73 useIndex = sqlite3_bind_parameter_index(statement, ":useRoot");
74 index = sqlite3_bind_parameter_index(statement, ":root");
75 sqlite3_bind_int(statement, useIndex, 1);
76 sqlite3_bind_text(statement, index, constraints->base, -1, SQLITE_TRANSIENT);
77 }
78
79 if (constraints->internalCode[0]) {
80 useIndex = sqlite3_bind_parameter_index(statement, ":useInternalCode");
81 index = sqlite3_bind_parameter_index(statement, ":internalCode");
82 sqlite3_bind_int(statement, useIndex, 1);
83 sqlite3_bind_text(statement, index, constraints->internalCode, -1, SQLITE_TRANSIENT);
84 }
85
86 if (constraints->platform != PLATFORM_NONE) {
87 useIndex = sqlite3_bind_parameter_index(statement, ":usePlatform");
88 index = sqlite3_bind_parameter_index(statement, ":platform");
89 sqlite3_bind_int(statement, useIndex, 1);
90 sqlite3_bind_int(statement, index, constraints->platform);
91 }
92}
93
94struct mLibrary* mLibraryCreateEmpty(void) {
95 return mLibraryLoad(":memory:");
96}
97
98struct mLibrary* mLibraryLoad(const char* path) {
99 struct mLibrary* library = malloc(sizeof(*library));
100 memset(library, 0, sizeof(*library));
101
102 if (sqlite3_open_v2(path, &library->db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL)) {
103 goto error;
104 }
105
106 static const char createTables[] =
107 " PRAGMA foreign_keys = ON;"
108 "\n CREATE TABLE IF NOT EXISTS version ("
109 "\n tname TEXT NOT NULL PRIMARY KEY,"
110 "\n version INTEGER NOT NULL DEFAULT 1"
111 "\n );"
112 "\n CREATE TABLE IF NOT EXISTS roots ("
113 "\n rootid INTEGER NOT NULL PRIMARY KEY ASC,"
114 "\n path TEXT NOT NULL UNIQUE,"
115 "\n mtime INTEGER NOT NULL DEFAULT 0"
116 "\n );"
117 "\n CREATE TABLE IF NOT EXISTS roms ("
118 "\n romid INTEGER NOT NULL PRIMARY KEY ASC,"
119 "\n internalTitle TEXT,"
120 "\n internalCode TEXT,"
121 "\n platform INTEGER NOT NULL DEFAULT -1,"
122 "\n size INTEGER,"
123 "\n crc32 INTEGER,"
124 "\n md5 BLOB,"
125 "\n sha1 BLOB"
126 "\n );"
127 "\n CREATE TABLE IF NOT EXISTS paths ("
128 "\n pathid INTEGER NOT NULL PRIMARY KEY ASC,"
129 "\n romid INTEGER NOT NULL REFERENCES roms(romid) ON DELETE CASCADE,"
130 "\n path TEXT NOT NULL,"
131 "\n mtime INTEGER NOT NULL DEFAULT 0,"
132 "\n rootid INTEGER REFERENCES roots(rootid) ON DELETE CASCADE,"
133 "\n customTitle TEXT,"
134 "\n CONSTRAINT location UNIQUE (path, rootid)"
135 "\n );"
136 "\n CREATE INDEX IF NOT EXISTS crc32 ON roms (crc32);"
137 "\n INSERT OR IGNORE INTO version (tname, version) VALUES ('version', 1);"
138 "\n INSERT OR IGNORE INTO version (tname, version) VALUES ('roots', 1);"
139 "\n INSERT OR IGNORE INTO version (tname, version) VALUES ('roms', 1);"
140 "\n INSERT OR IGNORE INTO version (tname, version) VALUES ('paths', 1);";
141 if (sqlite3_exec(library->db, createTables, NULL, NULL, NULL)) {
142 goto error;
143 }
144
145 static const char insertPath[] = "INSERT INTO paths (romid, path, customTitle, rootid) VALUES (?, ?, ?, ?);";
146 if (sqlite3_prepare_v2(library->db, insertPath, -1, &library->insertPath, NULL)) {
147 goto error;
148 }
149
150 static const char insertRom[] = "INSERT INTO roms (crc32, size, internalCode, platform) VALUES (:crc32, :size, :internalCode, :platform);";
151 if (sqlite3_prepare_v2(library->db, insertRom, -1, &library->insertRom, NULL)) {
152 goto error;
153 }
154
155 static const char insertRoot[] = "INSERT INTO roots (path) VALUES (?);";
156 if (sqlite3_prepare_v2(library->db, insertRoot, -1, &library->insertRoot, NULL)) {
157 goto error;
158 }
159
160 static const char deleteRoot[] = "DELETE FROM roots WHERE path = ?;";
161 if (sqlite3_prepare_v2(library->db, deleteRoot, -1, &library->deleteRoot, NULL)) {
162 goto error;
163 }
164
165 static const char deletePath[] = "DELETE FROM paths WHERE path = ?;";
166 if (sqlite3_prepare_v2(library->db, deletePath, -1, &library->deletePath, NULL)) {
167 goto error;
168 }
169
170 static const char selectRom[] = "SELECT romid FROM roms WHERE " CONSTRAINTS_ROMONLY ";";
171 if (sqlite3_prepare_v2(library->db, selectRom, -1, &library->selectRom, NULL)) {
172 goto error;
173 }
174
175 static const char selectRoot[] = "SELECT rootid FROM roots WHERE path = ? AND CASE WHEN :useMtime THEN mtime <= :mtime ELSE 1 END;";
176 if (sqlite3_prepare_v2(library->db, selectRoot, -1, &library->selectRoot, NULL)) {
177 goto error;
178 }
179
180 static const char count[] = "SELECT count(pathid) FROM paths JOIN roots USING (rootid) JOIN roms USING (romid) WHERE " CONSTRAINTS ";";
181 if (sqlite3_prepare_v2(library->db, count, -1, &library->count, NULL)) {
182 goto error;
183 }
184
185 static const char select[] = "SELECT *, paths.path AS filename, roots.path AS base FROM paths JOIN roots USING (rootid) JOIN roms USING (romid) WHERE " CONSTRAINTS " LIMIT :count OFFSET :offset;";
186 if (sqlite3_prepare_v2(library->db, select, -1, &library->select, NULL)) {
187 goto error;
188 }
189
190 return library;
191
192error:
193 mLibraryDestroy(library);
194 return NULL;
195}
196
197void mLibraryDestroy(struct mLibrary* library) {
198 sqlite3_finalize(library->insertPath);
199 sqlite3_finalize(library->insertRom);
200 sqlite3_finalize(library->insertRoot);
201 sqlite3_finalize(library->deletePath);
202 sqlite3_finalize(library->deleteRoot);
203 sqlite3_finalize(library->selectRom);
204 sqlite3_finalize(library->selectRoot);
205 sqlite3_finalize(library->select);
206 sqlite3_finalize(library->count);
207 sqlite3_close(library->db);
208}
209
210void mLibraryLoadDirectory(struct mLibrary* library, const char* base) {
211 struct VDir* dir = VDirOpenArchive(base);
212 if (!dir) {
213 dir = VDirOpen(base);
214 }
215 sqlite3_exec(library->db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
216 if (!dir) {
217 sqlite3_clear_bindings(library->deleteRoot);
218 sqlite3_reset(library->deleteRoot);
219 sqlite3_bind_text(library->deleteRoot, 1, base, -1, SQLITE_TRANSIENT);
220 sqlite3_step(library->deleteRoot);
221 sqlite3_exec(library->db, "COMMIT;", NULL, NULL, NULL);
222 return;
223 }
224
225 struct mLibraryEntry entry;
226 memset(&entry, 0, sizeof(entry));
227 entry.base = base;
228 struct mLibraryListing entries;
229 mLibraryListingInit(&entries, 0);
230 mLibraryGetEntries(library, &entries, 0, 0, &entry);
231 size_t i;
232 for (i = 0; i < mLibraryListingSize(&entries); ++i) {
233 struct mLibraryEntry* current = mLibraryListingGetPointer(&entries, i);
234 struct VFile* vf = dir->openFile(dir, current->filename, O_RDONLY);
235 _mLibraryDeleteEntry(library, current);
236 if (!vf) {
237 continue;
238 }
239 _mLibraryAddEntry(library, current->filename, base, vf);
240 }
241 mLibraryListingDeinit(&entries);
242
243 dir->rewind(dir);
244 struct VDirEntry* dirent = dir->listNext(dir);
245 while (dirent) {
246 struct VFile* vf = dir->openFile(dir, dirent->name(dirent), O_RDONLY);
247 if (!vf) {
248 dirent = dir->listNext(dir);
249 continue;
250 }
251 _mLibraryAddEntry(library, dirent->name(dirent), base, vf);
252 dirent = dir->listNext(dir);
253 }
254 dir->close(dir);
255 sqlite3_exec(library->db, "COMMIT;", NULL, NULL, NULL);
256}
257
258void _mLibraryAddEntry(struct mLibrary* library, const char* filename, const char* base, struct VFile* vf) {
259 struct mCore* core;
260 if (!vf) {
261 return;
262 }
263 core = mCoreFindVF(vf);
264 if (core) {
265 struct mLibraryEntry entry;
266 memset(&entry, 0, sizeof(entry));
267 core->init(core);
268 core->loadROM(core, vf);
269
270 core->getGameTitle(core, entry.internalTitle);
271 core->getGameCode(core, entry.internalCode);
272 core->checksum(core, &entry.crc32, CHECKSUM_CRC32);
273 entry.platform = core->platform(core);
274 entry.title = NULL;
275 entry.base = base;
276 entry.filename = filename;
277 entry.filesize = vf->size(vf);
278 _mLibraryInsertEntry(library, &entry);
279 // Note: this destroys the VFile
280 core->deinit(core);
281 } else {
282 vf->close(vf);
283 }
284}
285
286static void _mLibraryInsertEntry(struct mLibrary* library, struct mLibraryEntry* entry) {
287 sqlite3_clear_bindings(library->selectRom);
288 sqlite3_reset(library->selectRom);
289 struct mLibraryEntry constraints = *entry;
290 constraints.filename = NULL;
291 constraints.base = NULL;
292 _bindConstraints(library->selectRom, &constraints);
293 sqlite3_int64 romId;
294 if (sqlite3_step(library->selectRom) == SQLITE_DONE) {
295 sqlite3_clear_bindings(library->insertRom);
296 sqlite3_reset(library->insertRom);
297 _bindConstraints(library->insertRom, entry);
298 sqlite3_step(library->insertRom);
299 romId = sqlite3_last_insert_rowid(library->db);
300 } else {
301 romId = sqlite3_column_int64(library->selectRom, 0);
302 }
303
304 sqlite3_int64 rootId = 0;
305 if (entry->base) {
306 sqlite3_clear_bindings(library->selectRoot);
307 sqlite3_reset(library->selectRoot);
308 sqlite3_bind_text(library->selectRoot, 1, entry->base, -1, SQLITE_TRANSIENT);
309 if (sqlite3_step(library->selectRoot) == SQLITE_DONE) {
310 sqlite3_clear_bindings(library->insertRoot);
311 sqlite3_reset(library->insertRoot);
312 sqlite3_bind_text(library->insertRoot, 1, entry->base, -1, SQLITE_TRANSIENT);
313 sqlite3_step(library->insertRoot);
314 rootId = sqlite3_last_insert_rowid(library->db);
315 } else {
316 rootId = sqlite3_column_int64(library->selectRoot, 0);
317 }
318 }
319
320 sqlite3_clear_bindings(library->insertPath);
321 sqlite3_reset(library->insertPath);
322 sqlite3_bind_int64(library->insertPath, 1, romId);
323 sqlite3_bind_text(library->insertPath, 2, entry->filename, -1, SQLITE_TRANSIENT);
324 sqlite3_bind_text(library->insertPath, 3, entry->title, -1, SQLITE_TRANSIENT);
325 if (rootId > 0) {
326 sqlite3_bind_int64(library->insertPath, 4, rootId);
327 }
328 sqlite3_step(library->insertPath);
329}
330
331static void _mLibraryDeleteEntry(struct mLibrary* library, struct mLibraryEntry* entry) {
332 sqlite3_clear_bindings(library->deletePath);
333 sqlite3_reset(library->deletePath);
334 sqlite3_bind_text(library->deletePath, 1, entry->filename, -1, SQLITE_TRANSIENT);
335 sqlite3_step(library->insertPath);
336}
337
338size_t mLibraryCount(struct mLibrary* library, const struct mLibraryEntry* constraints) {
339 sqlite3_clear_bindings(library->count);
340 sqlite3_reset(library->count);
341 _bindConstraints(library->count, constraints);
342 if (sqlite3_step(library->count) != SQLITE_ROW) {
343 return 0;
344 }
345 return sqlite3_column_int64(library->count, 0);
346}
347
348size_t mLibraryGetEntries(struct mLibrary* library, struct mLibraryListing* out, size_t numEntries, size_t offset, const struct mLibraryEntry* constraints) {
349 mLibraryListingClear(out); // TODO: Free memory
350 sqlite3_clear_bindings(library->select);
351 sqlite3_reset(library->select);
352 _bindConstraints(library->select, constraints);
353
354 int countIndex = sqlite3_bind_parameter_index(library->select, ":count");
355 int offsetIndex = sqlite3_bind_parameter_index(library->select, ":offset");
356 sqlite3_bind_int64(library->select, countIndex, numEntries ? numEntries : -1);
357 sqlite3_bind_int64(library->select, offsetIndex, offset);
358
359 size_t entryIndex;
360 for (entryIndex = 0; (!numEntries || entryIndex < numEntries) && sqlite3_step(library->select) == SQLITE_ROW; ++entryIndex) {
361 struct mLibraryEntry* entry = mLibraryListingAppend(out);
362 memset(entry, 0, sizeof(*entry));
363 int nCols = sqlite3_column_count(library->select);
364 int i;
365 for (i = 0; i < nCols; ++i) {
366 const char* colName = sqlite3_column_name(library->select, i);
367 if (strcmp(colName, "crc32") == 0) {
368 entry->crc32 = sqlite3_column_int(library->select, i);
369 } else if (strcmp(colName, "platform") == 0) {
370 entry->platform = sqlite3_column_int(library->select, i);
371 } else if (strcmp(colName, "size") == 0) {
372 entry->filesize = sqlite3_column_int64(library->select, i);
373 } else if (strcmp(colName, "internalCode") == 0 && sqlite3_column_type(library->select, i) == SQLITE_TEXT) {
374 strncpy(entry->internalCode, (const char*) sqlite3_column_text(library->select, i), sizeof(entry->internalCode) - 1);
375 } else if (strcmp(colName, "internalTitle") == 0 && sqlite3_column_type(library->select, i) == SQLITE_TEXT) {
376 strncpy(entry->internalTitle, (const char*) sqlite3_column_text(library->select, i), sizeof(entry->internalTitle) - 1);
377 } else if (strcmp(colName, "filename") == 0) {
378 entry->filename = strdup((const char*) sqlite3_column_text(library->select, i));
379 } else if (strcmp(colName, "base") == 0) {
380 entry->base = strdup((const char*) sqlite3_column_text(library->select, i));
381 }
382 }
383 }
384 return mLibraryListingSize(out);
385}
386
387struct VFile* mLibraryOpenVFile(struct mLibrary* library, const struct mLibraryEntry* entry) {
388 struct mLibraryListing entries;
389 mLibraryListingInit(&entries, 0);
390 if (!mLibraryGetEntries(library, &entries, 0, 0, entry)) {
391 mLibraryListingDeinit(&entries);
392 return NULL;
393 }
394 struct VFile* vf = NULL;
395 size_t i;
396 for (i = 0; i < mLibraryListingSize(&entries); ++i) {
397 struct mLibraryEntry* e = mLibraryListingGetPointer(&entries, i);
398 struct VDir* dir = VDirOpenArchive(e->base);
399 bool isArchive = true;
400 if (!dir) {
401 dir = VDirOpen(e->base);
402 isArchive = false;
403 }
404 if (!dir) {
405 continue;
406 }
407 vf = dir->openFile(dir, e->filename, O_RDONLY);
408 if (vf && isArchive) {
409 struct VFile* vfclone = VFileMemChunk(NULL, vf->size(vf));
410 uint8_t buffer[2048];
411 ssize_t read;
412 while ((read = vf->read(vf, buffer, sizeof(buffer))) > 0) {
413 vfclone->write(vfclone, buffer, read);
414 }
415 vf->close(vf);
416 vf = vfclone;
417 }
418 dir->close(dir);
419 if (vf) {
420 break;
421 }
422 }
423 return vf;
424}
425
426#endif