libretrodb
Version:
A small reader for RetroArch databases
222 lines (221 loc) • 8.12 kB
JavaScript
"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;