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
342size_t mLibraryCount(struct mLibrary* library, const struct mLibraryEntry* constraints) {
343 sqlite3_clear_bindings(library->count);
344 sqlite3_reset(library->count);
345 _bindConstraints(library->count, constraints);
346 if (sqlite3_step(library->count) != SQLITE_ROW) {
347 return 0;
348 }
349 return sqlite3_column_int64(library->count, 0);
350}
351
352size_t mLibraryGetEntries(struct mLibrary* library, struct mLibraryListing* out, size_t numEntries, size_t offset, const struct mLibraryEntry* constraints) {
353 mLibraryListingClear(out); // TODO: Free memory
354 sqlite3_clear_bindings(library->select);
355 sqlite3_reset(library->select);
356 _bindConstraints(library->select, constraints);
357
358 int countIndex = sqlite3_bind_parameter_index(library->select, ":count");
359 int offsetIndex = sqlite3_bind_parameter_index(library->select, ":offset");
360 sqlite3_bind_int64(library->select, countIndex, numEntries ? numEntries : -1);
361 sqlite3_bind_int64(library->select, offsetIndex, offset);
362
363 size_t entryIndex;
364 for (entryIndex = 0; (!numEntries || entryIndex < numEntries) && sqlite3_step(library->select) == SQLITE_ROW; ++entryIndex) {
365 struct mLibraryEntry* entry = mLibraryListingAppend(out);
366 memset(entry, 0, sizeof(*entry));
367 int nCols = sqlite3_column_count(library->select);
368 int i;
369 for (i = 0; i < nCols; ++i) {
370 const char* colName = sqlite3_column_name(library->select, i);
371 if (strcmp(colName, "crc32") == 0) {
372 entry->crc32 = sqlite3_column_int(library->select, i);
373 struct NoIntroGame game;
374 if (NoIntroDBLookupGameByCRC(library->gameDB, entry->crc32, &game)) {
375 entry->title = strdup(game.name);
376 }
377 } else if (strcmp(colName, "platform") == 0) {
378 entry->platform = sqlite3_column_int(library->select, i);
379 } else if (strcmp(colName, "size") == 0) {
380 entry->filesize = sqlite3_column_int64(library->select, i);
381 } else if (strcmp(colName, "internalCode") == 0 && sqlite3_column_type(library->select, i) == SQLITE_TEXT) {
382 strncpy(entry->internalCode, (const char*) sqlite3_column_text(library->select, i), sizeof(entry->internalCode) - 1);
383 } else if (strcmp(colName, "internalTitle") == 0 && sqlite3_column_type(library->select, i) == SQLITE_TEXT) {
384 strncpy(entry->internalTitle, (const char*) sqlite3_column_text(library->select, i), sizeof(entry->internalTitle) - 1);
385 } else if (strcmp(colName, "filename") == 0) {
386 entry->filename = strdup((const char*) sqlite3_column_text(library->select, i));
387 } else if (strcmp(colName, "base") == 0) {
388 entry->base = strdup((const char*) sqlite3_column_text(library->select, i));
389 }
390 }
391 }
392 return mLibraryListingSize(out);
393}
394
395void mLibraryEntryFree(struct mLibraryEntry* entry) {
396 free((void*) entry->title);
397 free((void*) entry->filename);
398 free((void*) entry->base);
399}
400
401struct VFile* mLibraryOpenVFile(struct mLibrary* library, const struct mLibraryEntry* entry) {
402 struct mLibraryListing entries;
403 mLibraryListingInit(&entries, 0);
404 if (!mLibraryGetEntries(library, &entries, 0, 0, entry)) {
405 mLibraryListingDeinit(&entries);
406 return NULL;
407 }
408 struct VFile* vf = NULL;
409 size_t i;
410 for (i = 0; i < mLibraryListingSize(&entries); ++i) {
411 struct mLibraryEntry* e = mLibraryListingGetPointer(&entries, i);
412 struct VDir* dir = VDirOpenArchive(e->base);
413 bool isArchive = true;
414 if (!dir) {
415 dir = VDirOpen(e->base);
416 isArchive = false;
417 }
418 if (!dir) {
419 continue;
420 }
421 vf = dir->openFile(dir, e->filename, O_RDONLY);
422 if (vf && isArchive) {
423 struct VFile* vfclone = VFileMemChunk(NULL, vf->size(vf));
424 uint8_t buffer[2048];
425 ssize_t read;
426 while ((read = vf->read(vf, buffer, sizeof(buffer))) > 0) {
427 vfclone->write(vfclone, buffer, read);
428 }
429 vf->close(vf);
430 vf = vfclone;
431 }
432 dir->close(dir);
433 if (vf) {
434 break;
435 }
436 }
437 return vf;
438}
439
440void mLibraryAttachGameDB(struct mLibrary* library, const struct NoIntroDB* db) {
441 library->gameDB = db;
442}
443
444#endif