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