/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; this.EXPORTED_SYMBOLS = ["ESEDBReader"]; /* exported ESEDBReader */ const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; Cu.import("resource://gre/modules/ctypes.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyGetter(this, "log", () => { let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI; let consoleOptions = { maxLogLevelPref: "browser.esedbreader.loglevel", prefix: "ESEDBReader", }; return new ConsoleAPI(consoleOptions); }); XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); // We have a globally unique identifier for ESE instances. A new one // is used for each different database opened. let gESEInstanceCounter = 0; // We limit the length of strings that we read from databases. const MAX_STR_LENGTH = 64 * 1024; // Kernel-related types: const KERNEL = {}; KERNEL.FILETIME = new ctypes.StructType("FILETIME", [ {dwLowDateTime: ctypes.uint32_t}, {dwHighDateTime: ctypes.uint32_t} ]); KERNEL.SYSTEMTIME = new ctypes.StructType("SYSTEMTIME", [ {wYear: ctypes.uint16_t}, {wMonth: ctypes.uint16_t}, {wDayOfWeek: ctypes.uint16_t}, {wDay: ctypes.uint16_t}, {wHour: ctypes.uint16_t}, {wMinute: ctypes.uint16_t}, {wSecond: ctypes.uint16_t}, {wMilliseconds: ctypes.uint16_t} ]); // DB column types, cribbed from the ESE header var COLUMN_TYPES = { JET_coltypBit: 1, /* True, False, or NULL */ JET_coltypUnsignedByte: 2, /* 1-byte integer, unsigned */ JET_coltypShort: 3, /* 2-byte integer, signed */ JET_coltypLong: 4, /* 4-byte integer, signed */ JET_coltypCurrency: 5, /* 8 byte integer, signed */ JET_coltypIEEESingle: 6, /* 4-byte IEEE single precision */ JET_coltypIEEEDouble: 7, /* 8-byte IEEE double precision */ JET_coltypDateTime: 8, /* Integral date, fractional time */ JET_coltypBinary: 9, /* Binary data, < 255 bytes */ JET_coltypText: 10, /* ANSI text, case insensitive, < 255 bytes */ JET_coltypLongBinary: 11, /* Binary data, long value */ JET_coltypLongText: 12, /* ANSI text, long value */ JET_coltypUnsignedLong: 14, /* 4-byte unsigned integer */ JET_coltypLongLong: 15, /* 8-byte signed integer */ JET_coltypGUID: 16, /* 16-byte globally unique identifier */ }; // Not very efficient, but only used for error messages function getColTypeName(numericValue) { return Object.keys(COLUMN_TYPES).find(t => COLUMN_TYPES[t] == numericValue) || "unknown"; } // All type constants and method wrappers go on this object: const ESE = {}; ESE.JET_ERR = ctypes.long; ESE.JET_PCWSTR = ctypes.char16_t.ptr; // The ESE header calls this JET_API_PTR, but because it isn't ever used as a // pointer and because OS.File code implies that the name you give a type // matters, I opted for a different name. // Note that this is defined differently on 32 vs. 64-bit in the header. ESE.JET_API_ITEM = ctypes.voidptr_t.size == 4 ? ctypes.unsigned_long : ctypes.uint64_t; ESE.JET_INSTANCE = ESE.JET_API_ITEM; ESE.JET_SESID = ESE.JET_API_ITEM; ESE.JET_TABLEID = ESE.JET_API_ITEM; ESE.JET_COLUMNID = ctypes.unsigned_long; ESE.JET_GRBIT = ctypes.unsigned_long; ESE.JET_COLTYP = ctypes.unsigned_long; ESE.JET_DBID = ctypes.unsigned_long; ESE.JET_COLUMNDEF = new ctypes.StructType("JET_COLUMNDEF", [ {"cbStruct": ctypes.unsigned_long}, {"columnid": ESE.JET_COLUMNID }, {"coltyp": ESE.JET_COLTYP }, {"wCountry": ctypes.unsigned_short }, // sepcifies the country/region for the column definition {"langid": ctypes.unsigned_short }, {"cp": ctypes.unsigned_short }, {"wCollate": ctypes.unsigned_short }, /* Must be 0 */ {"cbMax": ctypes.unsigned_long }, {"grbit": ESE.JET_GRBIT } ]); // Track open databases let gOpenDBs = new Map(); // Track open libraries let gLibs = {}; this.ESE = ESE; // Required for tests. this.KERNEL = KERNEL; // ditto this.gLibs = gLibs; // ditto function convertESEError(errorCode) { switch (errorCode) { case -1213 /* JET_errPageSizeMismatch */: case -1002 /* JET_errInvalidName*/: case -1507 /* JET_errColumnNotFound */: // The DB format has changed and we haven't updated this migration code: return "The database format has changed, error code: " + errorCode; case -1032 /* JET_errFileAccessDenied */: case -1207 /* JET_errDatabaseLocked */: case -1302 /* JET_errTableLocked */: return "The database or table is locked, error code: " + errorCode; case -1809 /* JET_errPermissionDenied*/: case -1907 /* JET_errAccessDenied */: return "Access or permission denied, error code: " + errorCode; case -1044 /* JET_errInvalidFilename */: return "Invalid file name"; case -1811 /* JET_errFileNotFound */: return "File not found"; case -550 /* JET_errDatabaseDirtyShutdown */: return "Database in dirty shutdown state (without the requisite logs?)"; case -514 /* JET_errBadLogVersion */: return "Database log version does not match the version of ESE in use."; default: return "Unknown error: " + errorCode; } } function handleESEError(method, methodName, shouldThrow = true, errorLog = true) { return function() { let rv; try { rv = method.apply(null, arguments); } catch (ex) { log.error("Error calling into ctypes method", methodName, ex); throw ex; } let resultCode = parseInt(rv.toString(10), 10); if (resultCode < 0) { if (errorLog) { log.error("Got error " + resultCode + " calling " + methodName); } if (shouldThrow) { throw new Error(convertESEError(rv)); } } else if (resultCode > 0 && errorLog) { log.warn("Got warning " + resultCode + " calling " + methodName); } return resultCode; }; } function declareESEFunction(methodName, ...args) { let declaration = ["Jet" + methodName, ctypes.winapi_abi, ESE.JET_ERR].concat(args); let ctypeMethod = gLibs.ese.declare.apply(gLibs.ese, declaration); ESE[methodName] = handleESEError(ctypeMethod, methodName); ESE["FailSafe" + methodName] = handleESEError(ctypeMethod, methodName, false); ESE["Manual" + methodName] = handleESEError(ctypeMethod, methodName, false, false); } function declareESEFunctions() { declareESEFunction("GetDatabaseFileInfoW", ESE.JET_PCWSTR, ctypes.voidptr_t, ctypes.unsigned_long, ctypes.unsigned_long); declareESEFunction("GetSystemParameterW", ESE.JET_INSTANCE, ESE.JET_SESID, ctypes.unsigned_long, ESE.JET_API_ITEM.ptr, ESE.JET_PCWSTR, ctypes.unsigned_long); declareESEFunction("SetSystemParameterW", ESE.JET_INSTANCE.ptr, ESE.JET_SESID, ctypes.unsigned_long, ESE.JET_API_ITEM, ESE.JET_PCWSTR); declareESEFunction("CreateInstanceW", ESE.JET_INSTANCE.ptr, ESE.JET_PCWSTR); declareESEFunction("Init", ESE.JET_INSTANCE.ptr); declareESEFunction("BeginSessionW", ESE.JET_INSTANCE, ESE.JET_SESID.ptr, ESE.JET_PCWSTR, ESE.JET_PCWSTR); declareESEFunction("AttachDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR, ESE.JET_GRBIT); declareESEFunction("DetachDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR); declareESEFunction("OpenDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR, ESE.JET_PCWSTR, ESE.JET_DBID.ptr, ESE.JET_GRBIT); declareESEFunction("OpenTableW", ESE.JET_SESID, ESE.JET_DBID, ESE.JET_PCWSTR, ctypes.voidptr_t, ctypes.unsigned_long, ESE.JET_GRBIT, ESE.JET_TABLEID.ptr); declareESEFunction("GetColumnInfoW", ESE.JET_SESID, ESE.JET_DBID, ESE.JET_PCWSTR, ESE.JET_PCWSTR, ctypes.voidptr_t, ctypes.unsigned_long, ctypes.unsigned_long); declareESEFunction("Move", ESE.JET_SESID, ESE.JET_TABLEID, ctypes.long, ESE.JET_GRBIT); declareESEFunction("RetrieveColumn", ESE.JET_SESID, ESE.JET_TABLEID, ESE.JET_COLUMNID, ctypes.voidptr_t, ctypes.unsigned_long, ctypes.unsigned_long.ptr, ESE.JET_GRBIT, ctypes.voidptr_t); declareESEFunction("CloseTable", ESE.JET_SESID, ESE.JET_TABLEID); declareESEFunction("CloseDatabase", ESE.JET_SESID, ESE.JET_DBID, ESE.JET_GRBIT); declareESEFunction("EndSession", ESE.JET_SESID, ESE.JET_GRBIT); declareESEFunction("Term", ESE.JET_INSTANCE); } function unloadLibraries() { log.debug("Unloading"); if (gOpenDBs.size) { log.error("Shouldn't unload libraries before DBs are closed!"); for (let db of gOpenDBs.values()) { db._close(); } } for (let k of Object.keys(ESE)) { delete ESE[k]; } gLibs.ese.close(); gLibs.kernel.close(); delete gLibs.ese; delete gLibs.kernel; } function loadLibraries() { Services.obs.addObserver(unloadLibraries, "xpcom-shutdown"); gLibs.ese = ctypes.open("esent.dll"); gLibs.kernel = ctypes.open("kernel32.dll"); KERNEL.FileTimeToSystemTime = gLibs.kernel.declare("FileTimeToSystemTime", ctypes.winapi_abi, ctypes.int, KERNEL.FILETIME.ptr, KERNEL.SYSTEMTIME.ptr); declareESEFunctions(); } function ESEDB(rootPath, dbPath, logPath) { log.info("Created db"); this.rootPath = rootPath; this.dbPath = dbPath; this.logPath = logPath; this._references = 0; this._init(); } ESEDB.prototype = { rootPath: null, dbPath: null, logPath: null, _opened: false, _attached: false, _sessionCreated: false, _instanceCreated: false, _dbId: null, _sessionId: null, _instanceId: null, _init() { if (!gLibs.ese) { loadLibraries(); } this.incrementReferenceCounter(); this._internalOpen(); }, _internalOpen() { try { let dbinfo = new ctypes.unsigned_long(); ESE.GetDatabaseFileInfoW(this.dbPath, dbinfo.address(), ctypes.unsigned_long.size, 17); let pageSize = ctypes.UInt64.lo(dbinfo.value); ESE.SetSystemParameterW(null, 0, 64 /* JET_paramDatabasePageSize*/, pageSize, null); this._instanceId = new ESE.JET_INSTANCE(); ESE.CreateInstanceW(this._instanceId.address(), "firefox-dbreader-" + (gESEInstanceCounter++)); this._instanceCreated = true; ESE.SetSystemParameterW(this._instanceId.address(), 0, 0 /* JET_paramSystemPath*/, 0, this.rootPath); ESE.SetSystemParameterW(this._instanceId.address(), 0, 1 /* JET_paramTempPath */, 0, this.rootPath); ESE.SetSystemParameterW(this._instanceId.address(), 0, 2 /* JET_paramLogFilePath*/, 0, this.logPath); // Shouldn't try to call JetTerm if the following call fails. this._instanceCreated = false; ESE.Init(this._instanceId.address()); this._instanceCreated = true; this._sessionId = new ESE.JET_SESID(); ESE.BeginSessionW(this._instanceId, this._sessionId.address(), null, null); this._sessionCreated = true; const JET_bitDbReadOnly = 1; ESE.AttachDatabaseW(this._sessionId, this.dbPath, JET_bitDbReadOnly); this._attached = true; this._dbId = new ESE.JET_DBID(); ESE.OpenDatabaseW(this._sessionId, this.dbPath, null, this._dbId.address(), JET_bitDbReadOnly); this._opened = true; } catch (ex) { try { this._close(); } catch (innerException) { Cu.reportError(innerException); } // Make sure caller knows we failed. throw ex; } gOpenDBs.set(this.dbPath, this); }, checkForColumn(tableName, columnName) { if (!this._opened) { throw new Error("The database was closed!"); } let columnInfo; try { columnInfo = this._getColumnInfo(tableName, [{name: columnName}]); } catch (ex) { return null; } return columnInfo[0]; }, tableExists(tableName) { if (!this._opened) { throw new Error("The database was closed!"); } let tableId = new ESE.JET_TABLEID(); let rv = ESE.ManualOpenTableW(this._sessionId, this._dbId, tableName, null, 0, 4 /* JET_bitTableReadOnly */, tableId.address()); if (rv == -1305 /* JET_errObjectNotFound */) { return false; } if (rv < 0) { log.error("Got error " + rv + " calling OpenTableW"); throw new Error(convertESEError(rv)); } if (rv > 0) { log.error("Got warning " + rv + " calling OpenTableW"); } ESE.FailSafeCloseTable(this._sessionId, tableId); return true; }, *tableItems(tableName, columns) { if (!this._opened) { throw new Error("The database was closed!"); } let tableOpened = false; let tableId; try { tableId = this._openTable(tableName); tableOpened = true; let columnInfo = this._getColumnInfo(tableName, columns); let rv = ESE.ManualMove(this._sessionId, tableId, -2147483648 /* JET_MoveFirst */, 0); if (rv == -1603 /* JET_errNoCurrentRecord */) { // There are no rows in the table. this._closeTable(tableId); return; } if (rv != 0) { throw new Error(convertESEError(rv)); } do { let rowContents = {}; for (let column of columnInfo) { let [buffer, bufferSize] = this._getBufferForColumn(column); // We handle errors manually so we accurately deal with NULL values. let err = ESE.ManualRetrieveColumn(this._sessionId, tableId, column.id, buffer.address(), bufferSize, null, 0, null); rowContents[column.name] = this._convertResult(column, buffer, err); } yield rowContents; } while (ESE.ManualMove(this._sessionId, tableId, 1 /* JET_MoveNext */, 0) === 0); } catch (ex) { if (tableOpened) { this._closeTable(tableId); } throw ex; } this._closeTable(tableId); }, _openTable(tableName) { let tableId = new ESE.JET_TABLEID(); ESE.OpenTableW(this._sessionId, this._dbId, tableName, null, 0, 4 /* JET_bitTableReadOnly */, tableId.address()); return tableId; }, _getBufferForColumn(column) { let buffer; if (column.type == "string") { let wchar_tArray = ctypes.ArrayType(ctypes.char16_t); // size on the column is in bytes, 2 bytes to a wchar, so: let charCount = column.dbSize >> 1; buffer = new wchar_tArray(charCount); } else if (column.type == "boolean") { buffer = new ctypes.uint8_t(); } else if (column.type == "date") { buffer = new KERNEL.FILETIME(); } else if (column.type == "guid") { let byteArray = ctypes.ArrayType(ctypes.uint8_t); buffer = new byteArray(column.dbSize); } else { throw new Error("Unknown type " + column.type); } return [buffer, buffer.constructor.size]; }, _convertResult(column, buffer, err) { if (err != 0) { if (err == 1004) { // Deal with null values: buffer = null; } else { Cu.reportError("Unexpected JET error: " + err + "; retrieving value for column " + column.name); throw new Error(convertESEError(err)); } } if (column.type == "string") { return buffer ? buffer.readString() : ""; } if (column.type == "boolean") { return buffer ? (buffer.value == 255) : false; } if (column.type == "guid") { if (buffer.length != 16) { Cu.reportError("Buffer size for guid field " + column.id + " should have been 16!"); return ""; } let rv = "{"; for (let i = 0; i < 16; i++) { if (i == 4 || i == 6 || i == 8 || i == 10) { rv += "-"; } let byteValue = buffer.addressOfElement(i).contents; // Ensure there's a leading 0 rv += ("0" + byteValue.toString(16)).substr(-2); } return rv + "}"; } if (column.type == "date") { if (!buffer) { return null; } let systemTime = new KERNEL.SYSTEMTIME(); let result = KERNEL.FileTimeToSystemTime(buffer.address(), systemTime.address()); if (result == 0) { throw new Error(ctypes.winLastError); } // System time is in UTC, so we use Date.UTC to get milliseconds from epoch, // then divide by 1000 to get seconds, and round down: return new Date(Date.UTC(systemTime.wYear, systemTime.wMonth - 1, systemTime.wDay, systemTime.wHour, systemTime.wMinute, systemTime.wSecond, systemTime.wMilliseconds)); } return undefined; }, _getColumnInfo(tableName, columns) { let rv = []; for (let column of columns) { let columnInfoFromDB = new ESE.JET_COLUMNDEF(); ESE.GetColumnInfoW(this._sessionId, this._dbId, tableName, column.name, columnInfoFromDB.address(), ESE.JET_COLUMNDEF.size, 0 /* JET_ColInfo */); let dbType = parseInt(columnInfoFromDB.coltyp.toString(10), 10); let dbSize = parseInt(columnInfoFromDB.cbMax.toString(10), 10); if (column.type == "string") { if (dbType != COLUMN_TYPES.JET_coltypLongText && dbType != COLUMN_TYPES.JET_coltypText) { throw new Error("Invalid column type for column " + column.name + "; expected text type, got type " + getColTypeName(dbType)); } if (dbSize > MAX_STR_LENGTH) { throw new Error("Column " + column.name + " has more than 64k data in it. This API is not designed to handle data that large."); } } else if (column.type == "boolean") { if (dbType != COLUMN_TYPES.JET_coltypBit) { throw new Error("Invalid column type for column " + column.name + "; expected bit type, got type " + getColTypeName(dbType)); } } else if (column.type == "date") { if (dbType != COLUMN_TYPES.JET_coltypLongLong) { throw new Error("Invalid column type for column " + column.name + "; expected long long type, got type " + getColTypeName(dbType)); } } else if (column.type == "guid") { if (dbType != COLUMN_TYPES.JET_coltypGUID) { throw new Error("Invalid column type for column " + column.name + "; expected guid type, got type " + getColTypeName(dbType)); } } else if (column.type) { throw new Error("Unknown column type " + column.type + " requested for column " + column.name + ", don't know what to do."); } rv.push({name: column.name, id: columnInfoFromDB.columnid, type: column.type, dbSize, dbType}); } return rv; }, _closeTable(tableId) { ESE.FailSafeCloseTable(this._sessionId, tableId); }, _close() { this._internalClose(); gOpenDBs.delete(this.dbPath); }, _internalClose() { if (this._opened) { log.debug("close db"); ESE.FailSafeCloseDatabase(this._sessionId, this._dbId, 0); log.debug("finished close db"); this._opened = false; } if (this._attached) { log.debug("detach db"); ESE.FailSafeDetachDatabaseW(this._sessionId, this.dbPath); this._attached = false; } if (this._sessionCreated) { log.debug("end session"); ESE.FailSafeEndSession(this._sessionId, 0); this._sessionCreated = false; } if (this._instanceCreated) { log.debug("term"); ESE.FailSafeTerm(this._instanceId); this._instanceCreated = false; } }, incrementReferenceCounter() { this._references++; }, decrementReferenceCounter() { this._references--; if (this._references <= 0) { this._close(); } }, }; let ESEDBReader = { openDB(rootDir, dbFile, logDir) { let dbFilePath = dbFile.path; if (gOpenDBs.has(dbFilePath)) { let db = gOpenDBs.get(dbFilePath); db.incrementReferenceCounter(); return db; } // ESE is really picky about the trailing slashes according to the docs, // so we do as we're told and ensure those are there: return new ESEDB(rootDir.path + "\\", dbFilePath, logDir.path + "\\"); }, async dbLocked(dbFile) { let options = {winShare: OS.Constants.Win.FILE_SHARE_READ}; let locked = true; await OS.File.open(dbFile.path, {read: true}, options).then(fileHandle => { locked = false; // Return the close promise so we wait for the file to be closed again. // Otherwise the file might still be kept open by this handle by the time // that we try to use the ESE APIs to access it. return fileHandle.close(); }, () => { Cu.reportError("ESE DB at " + dbFile.path + " is locked."); }); return locked; }, closeDB(db) { db.decrementReferenceCounter(); }, COLUMN_TYPES, };