all repos — mgba @ 70204e410c18785a27a39af538642cd3f54e2528

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 != 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