all repos — mgba @ a760c7bb4a85c8abea17ff9db466e96ebe20a8de

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 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 != 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) {
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, mCHECKSUM_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	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