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
15DEFINE_VECTOR(mLibraryListing, struct mLibraryEntry);
16
17struct mLibrary {
18 sqlite3* db;
19 sqlite3_stmt* insertPath;
20 sqlite3_stmt* insertRom;
21 sqlite3_stmt* insertRoot;
22 sqlite3_stmt* selectRom;
23 sqlite3_stmt* selectRoot;
24 sqlite3_stmt* count;
25 sqlite3_stmt* select;
26};
27
28#define CONSTRAINTS_ROMONLY \
29 "CASE WHEN :useSize THEN roms.size = :size ELSE 1 END AND " \
30 "CASE WHEN :usePlatform THEN roms.platform = :platform ELSE 1 END AND " \
31 "CASE WHEN :useCrc32 THEN roms.crc32 = :crc32 ELSE 1 END AND " \
32 "CASE WHEN :useInternalCode THEN roms.internalCode = :internalCode ELSE 1 END"
33
34#define CONSTRAINTS \
35 CONSTRAINTS_ROMONLY " AND " \
36 "CASE WHEN :useFilename THEN paths.path = :path ELSE 1 END AND " \
37 "CASE WHEN :useRoot THEN roots.path = :root ELSE 1 END"
38
39static void _mLibraryInsertEntry(struct mLibrary* library, struct mLibraryEntry* entry);
40static void _mLibraryAddEntry(struct mLibrary* library, const char* filename, const char* base, struct VFile* vf);
41
42static void _bindConstraints(sqlite3_stmt* statement, const struct mLibraryEntry* constraints) {
43 if (!constraints) {
44 return;
45 }
46
47 int useIndex, index;
48 if (constraints->crc32) {
49 useIndex = sqlite3_bind_parameter_index(statement, ":useCrc32");
50 index = sqlite3_bind_parameter_index(statement, ":crc32");
51 sqlite3_bind_int(statement, useIndex, 1);
52 sqlite3_bind_int(statement, index, constraints->crc32);
53 }
54
55 if (constraints->filesize) {
56 useIndex = sqlite3_bind_parameter_index(statement, ":useSize");
57 index = sqlite3_bind_parameter_index(statement, ":size");
58 sqlite3_bind_int(statement, useIndex, 1);
59 sqlite3_bind_int64(statement, index, constraints->filesize);
60 }
61
62 if (constraints->filename) {
63 useIndex = sqlite3_bind_parameter_index(statement, ":useFilename");
64 index = sqlite3_bind_parameter_index(statement, ":path");
65 sqlite3_bind_int(statement, useIndex, 1);
66 sqlite3_bind_text(statement, index, constraints->filename, -1, SQLITE_TRANSIENT);
67 }
68
69 if (constraints->base) {
70 useIndex = sqlite3_bind_parameter_index(statement, ":useRoot");
71 index = sqlite3_bind_parameter_index(statement, ":root");
72 sqlite3_bind_int(statement, useIndex, 1);
73 sqlite3_bind_text(statement, index, constraints->base, -1, SQLITE_TRANSIENT);
74 }
75
76 if (constraints->internalCode[0]) {
77 useIndex = sqlite3_bind_parameter_index(statement, ":useInternalCode");
78 index = sqlite3_bind_parameter_index(statement, ":internalCode");
79 sqlite3_bind_int(statement, useIndex, 1);
80 sqlite3_bind_text(statement, index, constraints->internalCode, -1, SQLITE_TRANSIENT);
81 }
82
83 if (constraints->platform != PLATFORM_NONE) {
84 useIndex = sqlite3_bind_parameter_index(statement, ":usePlatform");
85 index = sqlite3_bind_parameter_index(statement, ":platform");
86 sqlite3_bind_int(statement, useIndex, 1);
87 sqlite3_bind_int(statement, index, constraints->platform);
88 }
89}
90
91struct mLibrary* mLibraryCreateEmpty(void) {
92 return mLibraryLoad(":memory:");
93}
94
95struct mLibrary* mLibraryLoad(const char* path) {
96 struct mLibrary* library = malloc(sizeof(*library));
97 memset(library, 0, sizeof(*library));
98
99 if (sqlite3_open_v2(path, &library->db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL)) {
100 goto error;
101 }
102
103 static const char createTables[] =
104 " PRAGMA foreign_keys = ON;"
105 "\n CREATE TABLE IF NOT EXISTS version ("
106 "\n tname TEXT NOT NULL PRIMARY KEY,"
107 "\n version INTEGER NOT NULL DEFAULT 1"
108 "\n );"
109 "\n CREATE TABLE IF NOT EXISTS roots ("
110 "\n rootid INTEGER NOT NULL PRIMARY KEY ASC,"
111 "\n path TEXT NOT NULL UNIQUE,"
112 "\n mtime INTEGER NOT NULL DEFAULT 0"
113 "\n );"
114 "\n CREATE TABLE IF NOT EXISTS roms ("
115 "\n romid INTEGER NOT NULL PRIMARY KEY ASC,"
116 "\n internalTitle TEXT,"
117 "\n internalCode TEXT,"
118 "\n platform INTEGER NOT NULL DEFAULT -1,"
119 "\n size INTEGER,"
120 "\n crc32 INTEGER,"
121 "\n md5 BLOB,"
122 "\n sha1 BLOB"
123 "\n );"
124 "\n CREATE TABLE IF NOT EXISTS paths ("
125 "\n pathid INTEGER NOT NULL PRIMARY KEY ASC,"
126 "\n romid INTEGER NOT NULL REFERENCES roms(romid) ON DELETE CASCADE,"
127 "\n path TEXT NOT NULL,"
128 "\n mtime INTEGER NOT NULL DEFAULT 0,"
129 "\n rootid INTEGER REFERENCES roots(rootid) ON DELETE CASCADE,"
130 "\n customTitle TEXT,"
131 "\n CONSTRAINT location UNIQUE (path, rootid)"
132 "\n );"
133 "\n CREATE INDEX IF NOT EXISTS crc32 ON roms (crc32);"
134 "\n INSERT OR IGNORE INTO version (tname, version) VALUES ('version', 1);"
135 "\n INSERT OR IGNORE INTO version (tname, version) VALUES ('roots', 1);"
136 "\n INSERT OR IGNORE INTO version (tname, version) VALUES ('roms', 1);"
137 "\n INSERT OR IGNORE INTO version (tname, version) VALUES ('paths', 1);";
138 if (sqlite3_exec(library->db, createTables, NULL, NULL, NULL)) {
139 goto error;
140 }
141
142 static const char insertPath[] = "INSERT INTO paths (romid, path, customTitle, rootid) VALUES (?, ?, ?, ?);";
143 if (sqlite3_prepare_v2(library->db, insertPath, -1, &library->insertPath, NULL)) {
144 goto error;
145 }
146
147 static const char insertRom[] = "INSERT INTO roms (crc32, size, internalCode, platform) VALUES (:crc32, :size, :internalCode, :platform);";
148 if (sqlite3_prepare_v2(library->db, insertRom, -1, &library->insertRom, NULL)) {
149 goto error;
150 }
151
152 static const char insertRoot[] = "INSERT INTO roots (path) VALUES (?);";
153 if (sqlite3_prepare_v2(library->db, insertRoot, -1, &library->insertRoot, NULL)) {
154 goto error;
155 }
156
157 static const char selectRom[] = "SELECT romid FROM roms WHERE " CONSTRAINTS_ROMONLY ";";
158 if (sqlite3_prepare_v2(library->db, selectRom, -1, &library->selectRom, NULL)) {
159 goto error;
160 }
161
162 static const char selectRoot[] = "SELECT rootid FROM roots WHERE path = ?;";
163 if (sqlite3_prepare_v2(library->db, selectRoot, -1, &library->selectRoot, NULL)) {
164 goto error;
165 }
166
167 static const char count[] = "SELECT count(pathid) FROM paths JOIN roots USING (rootid) JOIN roms USING (romid) WHERE " CONSTRAINTS ";";
168 if (sqlite3_prepare_v2(library->db, count, -1, &library->count, NULL)) {
169 goto error;
170 }
171
172 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;";
173 if (sqlite3_prepare_v2(library->db, select, -1, &library->select, NULL)) {
174 goto error;
175 }
176
177 return library;
178
179error:
180 mLibraryDestroy(library);
181 return NULL;
182}
183
184void mLibraryDestroy(struct mLibrary* library) {
185 sqlite3_finalize(library->insertPath);
186 sqlite3_finalize(library->insertRom);
187 sqlite3_finalize(library->insertRoot);
188 sqlite3_finalize(library->selectRom);
189 sqlite3_finalize(library->selectRoot);
190 sqlite3_finalize(library->select);
191 sqlite3_finalize(library->count);
192 sqlite3_close(library->db);
193}
194
195void mLibraryLoadDirectory(struct mLibrary* library, const char* base) {
196 struct VDir* dir = VDirOpenArchive(base);
197 if (!dir) {
198 dir = VDirOpen(base);
199 }
200 if (!dir) {
201 return;
202 }
203 struct VDirEntry* dirent = dir->listNext(dir);
204 while (dirent) {
205 struct VFile* vf = dir->openFile(dir, dirent->name(dirent), O_RDONLY);
206 if (!vf) {
207 dirent = dir->listNext(dir);
208 continue;
209 }
210 _mLibraryAddEntry(library, dirent->name(dirent), base, vf);
211 dirent = dir->listNext(dir);
212 }
213 dir->close(dir);
214}
215
216void _mLibraryAddEntry(struct mLibrary* library, const char* filename, const char* base, struct VFile* vf) {
217 struct mCore* core;
218 if (!vf) {
219 return;
220 }
221 core = mCoreFindVF(vf);
222 if (core) {
223 struct mLibraryEntry entry;
224 memset(&entry, 0, sizeof(entry));
225 core->init(core);
226 core->loadROM(core, vf);
227
228 core->getGameTitle(core, entry.internalTitle);
229 core->getGameCode(core, entry.internalCode);
230 core->checksum(core, &entry.crc32, CHECKSUM_CRC32);
231 entry.platform = core->platform(core);
232 entry.title = NULL;
233 entry.base = base;
234 entry.filename = filename;
235 entry.filesize = vf->size(vf);
236 _mLibraryInsertEntry(library, &entry);
237 // Note: this destroys the VFile
238 core->deinit(core);
239 } else {
240 vf->close(vf);
241 }
242}
243
244static void _mLibraryInsertEntry(struct mLibrary* library, struct mLibraryEntry* entry) {
245 sqlite3_exec(library->db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
246
247 sqlite3_clear_bindings(library->selectRom);
248 sqlite3_reset(library->selectRom);
249 struct mLibraryEntry constraints = *entry;
250 constraints.filename = NULL;
251 constraints.base = NULL;
252 _bindConstraints(library->selectRom, &constraints);
253 sqlite3_int64 romId;
254 if (sqlite3_step(library->selectRom) == SQLITE_DONE) {
255 sqlite3_clear_bindings(library->insertRom);
256 sqlite3_reset(library->insertRom);
257 _bindConstraints(library->insertRom, entry);
258 sqlite3_step(library->insertRom);
259 romId = sqlite3_last_insert_rowid(library->db);
260 } else {
261 romId = sqlite3_column_int64(library->selectRom, 0);
262 }
263
264 sqlite3_int64 rootId = 0;
265 if (entry->base) {
266 sqlite3_clear_bindings(library->selectRoot);
267 sqlite3_reset(library->selectRoot);
268 sqlite3_bind_text(library->selectRoot, 1, entry->base, -1, SQLITE_TRANSIENT);
269 if (sqlite3_step(library->selectRoot) == SQLITE_DONE) {
270 sqlite3_clear_bindings(library->insertRoot);
271 sqlite3_reset(library->insertRoot);
272 sqlite3_bind_text(library->insertRoot, 1, entry->base, -1, SQLITE_TRANSIENT);
273 sqlite3_step(library->insertRoot);
274 rootId = sqlite3_last_insert_rowid(library->db);
275 } else {
276 rootId = sqlite3_column_int64(library->selectRoot, 0);
277 }
278 }
279
280 sqlite3_clear_bindings(library->insertPath);
281 sqlite3_reset(library->insertPath);
282 sqlite3_bind_int64(library->insertPath, 1, romId);
283 sqlite3_bind_text(library->insertPath, 2, entry->filename, -1, SQLITE_TRANSIENT);
284 sqlite3_bind_text(library->insertPath, 3, entry->title, -1, SQLITE_TRANSIENT);
285 if (rootId > 0) {
286 sqlite3_bind_int64(library->insertPath, 4, rootId);
287 }
288 sqlite3_step(library->insertPath);
289 sqlite3_exec(library->db, "COMMIT;", NULL, NULL, NULL);
290}
291
292size_t mLibraryCount(struct mLibrary* library, const struct mLibraryEntry* constraints) {
293 sqlite3_clear_bindings(library->count);
294 sqlite3_reset(library->count);
295 _bindConstraints(library->count, constraints);
296 if (sqlite3_step(library->count) != SQLITE_ROW) {
297 return 0;
298 }
299 return sqlite3_column_int64(library->count, 0);
300}
301
302size_t mLibraryGetEntries(struct mLibrary* library, struct mLibraryListing* out, size_t numEntries, size_t offset, const struct mLibraryEntry* constraints) {
303 mLibraryListingClear(out); // TODO: Free memory
304 sqlite3_clear_bindings(library->select);
305 sqlite3_reset(library->select);
306 _bindConstraints(library->select, constraints);
307
308 int countIndex = sqlite3_bind_parameter_index(library->select, ":count");
309 int offsetIndex = sqlite3_bind_parameter_index(library->select, ":offset");
310 sqlite3_bind_int64(library->select, countIndex, numEntries ? numEntries : -1);
311 sqlite3_bind_int64(library->select, offsetIndex, offset);
312
313 size_t entryIndex;
314 for (entryIndex = 0; (!numEntries || entryIndex < numEntries) && sqlite3_step(library->select) == SQLITE_ROW; ++entryIndex) {
315 struct mLibraryEntry* entry = mLibraryListingAppend(out);
316 memset(entry, 0, sizeof(*entry));
317 int nCols = sqlite3_column_count(library->select);
318 int i;
319 for (i = 0; i < nCols; ++i) {
320 const char* colName = sqlite3_column_name(library->select, i);
321 if (strcmp(colName, "crc32") == 0) {
322 entry->crc32 = sqlite3_column_int(library->select, i);
323 } else if (strcmp(colName, "platform") == 0) {
324 entry->platform = sqlite3_column_int(library->select, i);
325 } else if (strcmp(colName, "size") == 0) {
326 entry->filesize = sqlite3_column_int64(library->select, i);
327 } else if (strcmp(colName, "internalCode") == 0 && sqlite3_column_type(library->select, i) == SQLITE_TEXT) {
328 strncpy(entry->internalCode, (const char*) sqlite3_column_text(library->select, i), sizeof(entry->internalCode) - 1);
329 } else if (strcmp(colName, "internalTitle") == 0 && sqlite3_column_type(library->select, i) == SQLITE_TEXT) {
330 strncpy(entry->internalTitle, (const char*) sqlite3_column_text(library->select, i), sizeof(entry->internalTitle) - 1);
331 } else if (strcmp(colName, "filename") == 0) {
332 entry->filename = strdup((const char*) sqlite3_column_text(library->select, i));
333 } else if (strcmp(colName, "base") == 0) {
334 entry->base = strdup((const char*) sqlite3_column_text(library->select, i));
335 }
336 }
337 }
338 return mLibraryListingSize(out);
339}
340
341struct VFile* mLibraryOpenVFile(struct mLibrary* library, const struct mLibraryEntry* entry) {
342 struct mLibraryListing entries;
343 mLibraryListingInit(&entries, 0);
344 if (!mLibraryGetEntries(library, &entries, 0, 0, entry)) {
345 mLibraryListingDeinit(&entries);
346 return NULL;
347 }
348 struct VFile* vf = NULL;
349 size_t i;
350 for (i = 0; i < mLibraryListingSize(&entries); ++i) {
351 struct mLibraryEntry* e = mLibraryListingGetPointer(&entries, i);
352 struct VDir* dir = VDirOpenArchive(e->base);
353 bool isArchive = true;
354 if (!dir) {
355 dir = VDirOpen(e->base);
356 isArchive = false;
357 }
358 if (!dir) {
359 continue;
360 }
361 vf = dir->openFile(dir, e->filename, O_RDONLY);
362 if (vf && isArchive) {
363 struct VFile* vfclone = VFileMemChunk(NULL, vf->size(vf));
364 uint8_t buffer[2048];
365 ssize_t read;
366 while ((read = vf->read(vf, buffer, sizeof(buffer))) > 0) {
367 vfclone->write(vfclone, buffer, read);
368 }
369 vf->close(vf);
370 vf = vfclone;
371 }
372 dir->close(dir);
373 if (vf) {
374 break;
375 }
376 }
377 return vf;
378}
379
380#endif