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 void _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 != PLATFORM_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) {
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 struct VFile* vf = dir->openFile(dir, dirent->name(dirent), O_RDONLY);
252 if (!vf) {
253 dirent = dir->listNext(dir);
254 continue;
255 }
256 _mLibraryAddEntry(library, dirent->name(dirent), base, vf);
257 dirent = dir->listNext(dir);
258 }
259 dir->close(dir);
260 sqlite3_exec(library->db, "COMMIT;", NULL, NULL, NULL);
261}
262
263void _mLibraryAddEntry(struct mLibrary* library, const char* filename, const char* base, struct VFile* vf) {
264 struct mCore* core;
265 if (!vf) {
266 return;
267 }
268 core = mCoreFindVF(vf);
269 if (core) {
270 struct mLibraryEntry entry;
271 memset(&entry, 0, sizeof(entry));
272 core->init(core);
273 core->loadROM(core, vf);
274
275 core->getGameTitle(core, entry.internalTitle);
276 core->getGameCode(core, entry.internalCode);
277 core->checksum(core, &entry.crc32, CHECKSUM_CRC32);
278 entry.platform = core->platform(core);
279 entry.title = NULL;
280 entry.base = base;
281 entry.filename = filename;
282 entry.filesize = vf->size(vf);
283 _mLibraryInsertEntry(library, &entry);
284 // Note: this destroys the VFile
285 core->deinit(core);
286 } else {
287 vf->close(vf);
288 }
289}
290
291static void _mLibraryInsertEntry(struct mLibrary* library, struct mLibraryEntry* entry) {
292 sqlite3_clear_bindings(library->selectRom);
293 sqlite3_reset(library->selectRom);
294 struct mLibraryEntry constraints = *entry;
295 constraints.filename = NULL;
296 constraints.base = NULL;
297 _bindConstraints(library->selectRom, &constraints);
298 sqlite3_int64 romId;
299 if (sqlite3_step(library->selectRom) == SQLITE_DONE) {
300 sqlite3_clear_bindings(library->insertRom);
301 sqlite3_reset(library->insertRom);
302 _bindConstraints(library->insertRom, entry);
303 sqlite3_step(library->insertRom);
304 romId = sqlite3_last_insert_rowid(library->db);
305 } else {
306 romId = sqlite3_column_int64(library->selectRom, 0);
307 }
308
309 sqlite3_int64 rootId = 0;
310 if (entry->base) {
311 sqlite3_clear_bindings(library->selectRoot);
312 sqlite3_reset(library->selectRoot);
313 sqlite3_bind_text(library->selectRoot, 1, entry->base, -1, SQLITE_TRANSIENT);
314 if (sqlite3_step(library->selectRoot) == SQLITE_DONE) {
315 sqlite3_clear_bindings(library->insertRoot);
316 sqlite3_reset(library->insertRoot);
317 sqlite3_bind_text(library->insertRoot, 1, entry->base, -1, SQLITE_TRANSIENT);
318 sqlite3_step(library->insertRoot);
319 rootId = sqlite3_last_insert_rowid(library->db);
320 } else {
321 rootId = sqlite3_column_int64(library->selectRoot, 0);
322 }
323 }
324
325 sqlite3_clear_bindings(library->insertPath);
326 sqlite3_reset(library->insertPath);
327 sqlite3_bind_int64(library->insertPath, 1, romId);
328 sqlite3_bind_text(library->insertPath, 2, entry->filename, -1, SQLITE_TRANSIENT);
329 sqlite3_bind_text(library->insertPath, 3, entry->title, -1, SQLITE_TRANSIENT);
330 if (rootId > 0) {
331 sqlite3_bind_int64(library->insertPath, 4, rootId);
332 }
333 sqlite3_step(library->insertPath);
334}
335
336static void _mLibraryDeleteEntry(struct mLibrary* library, struct mLibraryEntry* entry) {
337 sqlite3_clear_bindings(library->deletePath);
338 sqlite3_reset(library->deletePath);
339 sqlite3_bind_text(library->deletePath, 1, entry->filename, -1, SQLITE_TRANSIENT);
340 sqlite3_step(library->insertPath);
341}
342
343void mLibraryClear(struct mLibrary* library) {
344 int result = sqlite3_exec(library->db,
345 " BEGIN TRANSACTION;"
346 "\n DELETE FROM roots;"
347 "\n DELETE FROM roms;"
348 "\n DELETE FROM paths;"
349 "\n COMMIT;"
350 "\n VACUUM;", NULL, NULL, NULL);
351}
352
353size_t mLibraryCount(struct mLibrary* library, const struct mLibraryEntry* constraints) {
354 sqlite3_clear_bindings(library->count);
355 sqlite3_reset(library->count);
356 _bindConstraints(library->count, constraints);
357 if (sqlite3_step(library->count) != SQLITE_ROW) {
358 return 0;
359 }
360 return sqlite3_column_int64(library->count, 0);
361}
362
363size_t mLibraryGetEntries(struct mLibrary* library, struct mLibraryListing* out, size_t numEntries, size_t offset, const struct mLibraryEntry* constraints) {
364 mLibraryListingClear(out); // TODO: Free memory
365 sqlite3_clear_bindings(library->select);
366 sqlite3_reset(library->select);
367 _bindConstraints(library->select, constraints);
368
369 int countIndex = sqlite3_bind_parameter_index(library->select, ":count");
370 int offsetIndex = sqlite3_bind_parameter_index(library->select, ":offset");
371 sqlite3_bind_int64(library->select, countIndex, numEntries ? numEntries : -1);
372 sqlite3_bind_int64(library->select, offsetIndex, offset);
373
374 size_t entryIndex;
375 for (entryIndex = 0; (!numEntries || entryIndex < numEntries) && sqlite3_step(library->select) == SQLITE_ROW; ++entryIndex) {
376 struct mLibraryEntry* entry = mLibraryListingAppend(out);
377 memset(entry, 0, sizeof(*entry));
378 int nCols = sqlite3_column_count(library->select);
379 int i;
380 for (i = 0; i < nCols; ++i) {
381 const char* colName = sqlite3_column_name(library->select, i);
382 if (strcmp(colName, "crc32") == 0) {
383 entry->crc32 = sqlite3_column_int(library->select, i);
384 struct NoIntroGame game;
385 if (NoIntroDBLookupGameByCRC(library->gameDB, entry->crc32, &game)) {
386 entry->title = strdup(game.name);
387 }
388 } else if (strcmp(colName, "platform") == 0) {
389 entry->platform = sqlite3_column_int(library->select, i);
390 } else if (strcmp(colName, "size") == 0) {
391 entry->filesize = sqlite3_column_int64(library->select, i);
392 } else if (strcmp(colName, "internalCode") == 0 && sqlite3_column_type(library->select, i) == SQLITE_TEXT) {
393 strncpy(entry->internalCode, (const char*) sqlite3_column_text(library->select, i), sizeof(entry->internalCode) - 1);
394 } else if (strcmp(colName, "internalTitle") == 0 && sqlite3_column_type(library->select, i) == SQLITE_TEXT) {
395 strncpy(entry->internalTitle, (const char*) sqlite3_column_text(library->select, i), sizeof(entry->internalTitle) - 1);
396 } else if (strcmp(colName, "filename") == 0) {
397 entry->filename = strdup((const char*) sqlite3_column_text(library->select, i));
398 } else if (strcmp(colName, "base") == 0) {
399 entry->base = strdup((const char*) sqlite3_column_text(library->select, i));
400 }
401 }
402 }
403 return mLibraryListingSize(out);
404}
405
406void mLibraryEntryFree(struct mLibraryEntry* entry) {
407 free((void*) entry->title);
408 free((void*) entry->filename);
409 free((void*) entry->base);
410}
411
412struct VFile* mLibraryOpenVFile(struct mLibrary* library, const struct mLibraryEntry* entry) {
413 struct mLibraryListing entries;
414 mLibraryListingInit(&entries, 0);
415 if (!mLibraryGetEntries(library, &entries, 0, 0, entry)) {
416 mLibraryListingDeinit(&entries);
417 return NULL;
418 }
419 struct VFile* vf = NULL;
420 size_t i;
421 for (i = 0; i < mLibraryListingSize(&entries); ++i) {
422 struct mLibraryEntry* e = mLibraryListingGetPointer(&entries, i);
423 struct VDir* dir = VDirOpenArchive(e->base);
424 bool isArchive = true;
425 if (!dir) {
426 dir = VDirOpen(e->base);
427 isArchive = false;
428 }
429 if (!dir) {
430 continue;
431 }
432 vf = dir->openFile(dir, e->filename, O_RDONLY);
433 if (vf && isArchive) {
434 struct VFile* vfclone = VFileMemChunk(NULL, vf->size(vf));
435 uint8_t buffer[2048];
436 ssize_t read;
437 while ((read = vf->read(vf, buffer, sizeof(buffer))) > 0) {
438 vfclone->write(vfclone, buffer, read);
439 }
440 vf->close(vf);
441 vf = vfclone;
442 }
443 dir->close(dir);
444 if (vf) {
445 break;
446 }
447 }
448 return vf;
449}
450
451void mLibraryAttachGameDB(struct mLibrary* library, const struct NoIntroDB* db) {
452 library->gameDB = db;
453}
454
455#endif