all repos — mgba @ 88212fc2deb72b3ab85313852b0b091edc88403b

mGBA Game Boy Advance Emulator

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