UNPKG

libretrodb

Version:
222 lines (221 loc) 8.12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Libretrodb = void 0; const file_handler_1 = require("./file-handler"); const constants_1 = require("./constants"); const is_record_1 = require("./is-record"); class Libretrodb { constructor(path, options = {}) { this.path = path; this.metadataOffset = 0; this.firstIndexOffset = 0; this.count = 0; this.entries = []; this.index = {}; this.file = new file_handler_1.FileHandler(path); this.options = { bufferToString: true, indexHashes: true, ...options }; } static async from(path, options) { const db = new Libretrodb(path, options); await db.load(); return db; } async load() { await this.file.load(); this.readMagicNumber(); this.readMetadataOffset(); this.firstIndexOffset = this.file.tell(); this.readMetadata(); this.file.seek(this.firstIndexOffset, file_handler_1.SEEK_MODE.SEEK_SET); this.readEntries(); this.file.free(); } getEntries() { return this.entries.slice(); } searchHash(hex) { const len = hex.length; if (len !== 8 && len !== 32 && len !== 40) { throw new Error(`hash length mismatch with crc (8), md5 (32) and sha1 (40) (got ${len})`); } if (this.options.indexHashes) { return this.index[hex]; } const search = Buffer.from(hex, 'hex'); const key = len === 8 ? 'crc' : len === 32 ? 'md5' : 'sha1'; return this.entries.find(entry => { const value = entry[key]; if (value) { if (this.options.bufferToString) { return value === hex; } else { return value.compare(search) === 0; } } }); } readMagicNumber() { const buffer = this.file.read(constants_1.MAGIC_NUMBER.length); const mismatch = Buffer.compare(buffer, Buffer.from(constants_1.MAGIC_NUMBER)) !== 0; if (mismatch) { throw new Error('not a libretro database (magic number mismatch)'); } // MAGIC NUMBER length is 7, // Due to the data structure alignment with the uint64 // The compiler pads it to 8, so add a byte this.file.seek(1); } readMetadataOffset() { this.metadataOffset = Number(this.file.readUInt64()); if (!this.metadataOffset) { this.metadataOffset = this.searchForMetadataOffset(); } if (!this.metadataOffset) { throw new Error('metadata_offset not found'); } } readMetadata() { this.file.seek(this.metadataOffset, file_handler_1.SEEK_MODE.SEEK_SET); const result = this.rmsgpackRead(); if (is_record_1.isRecord(result) && typeof result.count === 'number') { this.count = result.count; } else { throw new Error('failed to read metadata'); } } readEntries() { for (let i = 0; i < this.count; i++) { const pos = this.file.tell(); const value = this.rmsgpackRead(); if (is_record_1.isRecord(value)) { const entry = value; this.entries.push(entry); if (this.options.indexHashes) { this.addToIndex(entry); } } else { throw new Error(`unexpected data read at cursor 0x${pos.toString(16)}`); } } if (this.metadataOffset !== this.file.tell() + 1) { console.warn(`** WARNING **\n** Unexpected cursor position (0x${this.file.tell().toString(16)} instead of 0x${(this.metadataOffset - 1).toString(16)}). **\n** There are some unidentified data after the last entry. **`); } } addToIndex(entry) { const keys = ['crc', 'md5', 'sha1']; for (const key of keys) { const value = entry[key]; if (value) { if (Buffer.isBuffer(value)) { this.index[value.toString('hex')] = entry; } else { this.index[value] = entry; } } } } rmsgpackRead() { const type = this.file.readUInt(); if (type < constants_1.MPF.FIXMAP) { return type; } if (type < constants_1.MPF.FIXARRAY) { return this.readMap(type - constants_1.MPF.FIXMAP); } if (type < constants_1.MPF.FIXSTR) { return this.readArray(type - constants_1.MPF.FIXARRAY); } if (type < constants_1.MPF.NIL) { return this.file.readString(type - constants_1.MPF.FIXSTR); } if (type > constants_1.MPF.MAP32) { return type - 0xff - 1; } let tmpLen; switch (type) { case constants_1.MPF.NIL: return null; case constants_1.MPF.FALSE: return false; case constants_1.MPF.TRUE: return true; case constants_1.MPF.BIN8: case constants_1.MPF.BIN16: case constants_1.MPF.BIN32: tmpLen = this.file.readInt(1 << (type - constants_1.MPF.BIN8)); const buffer = this.file.read(tmpLen); if (this.options.bufferToString) { return buffer.toString('hex'); } return Buffer.from(buffer); case constants_1.MPF.UINT8: case constants_1.MPF.UINT16: case constants_1.MPF.UINT32: case constants_1.MPF.UINT64: return this.file.readUInt(1 << (type - constants_1.MPF.UINT8)); case constants_1.MPF.INT8: case constants_1.MPF.INT16: case constants_1.MPF.INT32: case constants_1.MPF.INT64: return this.file.readInt(1 << (type - constants_1.MPF.INT8)); case constants_1.MPF.STR8: case constants_1.MPF.STR16: case constants_1.MPF.STR32: tmpLen = this.file.readUInt(1 << (type - constants_1.MPF.STR8)); return this.file.readString(tmpLen); case constants_1.MPF.ARRAY16: case constants_1.MPF.ARRAY32: tmpLen = this.file.readUInt(2 << (type - constants_1.MPF.ARRAY16)); return this.readArray(tmpLen); case constants_1.MPF.MAP16: case constants_1.MPF.MAP32: tmpLen = this.file.readUInt(2 << (type - constants_1.MPF.MAP16)); return this.readMap(tmpLen); } return 0; } readMap(len) { const result = {}; for (let i = 0; i < len; i++) { const key = this.rmsgpackRead(); result[key] = this.rmsgpackRead(); } return result; } readArray(len) { const result = []; for (let i = 0; i < len; i++) { result.push(this.rmsgpackRead()); } return result; } /** * Some rdb files are missing the metadata_offset, so * we duck search the position of the map+count+len from the end * of the file. * https://github.com/libretro/libretro-database/issues/1163 */ searchForMetadataOffset() { const previousPos = this.file.tell(); const searched = Buffer.from('count'); for (let offset = 20; offset > searched.length; offset--) { this.file.seek(offset, file_handler_1.SEEK_MODE.SEEK_END); const buffer = this.file.read(searched.length); if (buffer.compare(searched) === 0) { const metadata_offset = this.file.tell() - searched.length - 2; this.file.seek(previousPos, file_handler_1.SEEK_MODE.SEEK_SET); return metadata_offset; } } return 0; } } exports.Libretrodb = Libretrodb;