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