/* 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/. */ import { ctypes } from "resource://gre/modules/ctypes.sys.mjs"; const lazy = {}; ChromeUtils.defineLazyGetter(lazy, "log", () => { let { ConsoleAPI } = ChromeUtils.importESModule( "resource://gre/modules/Console.sys.mjs" ); let consoleOptions = { maxLogLevelPref: "browser.esedbreader.loglevel", prefix: "ESEDBReader", }; return new ConsoleAPI(consoleOptions); }); // 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: export 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 export 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: export 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, 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 export let gLibs = {}; 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 -1305 /* JET_errObjectNotFound */: return "The table/object was not found."; 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) { lazy.log.error("Error calling into ctypes method", methodName, ex); throw ex; } let resultCode = parseInt(rv.toString(10), 10); if (resultCode < 0) { if (errorLog) { lazy.log.error("Got error " + resultCode + " calling " + methodName); } if (shouldThrow) { throw new Error(convertESEError(rv)); } } else if (resultCode > 0 && errorLog) { lazy.log.warn("Got warning " + resultCode + " calling " + methodName); } return resultCode; }; } export 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() { lazy.log.debug("Unloading"); if (gOpenDBs.size) { lazy.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; } export 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) { lazy.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) { console.error(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) { lazy.log.error("Got error " + rv + " calling OpenTableW"); throw new Error(convertESEError(rv)); } if (rv > 0) { lazy.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 { console.error( "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) { console.error( "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) { lazy.log.debug("close db"); ESE.FailSafeCloseDatabase(this._sessionId, this._dbId, 0); lazy.log.debug("finished close db"); this._opened = false; } if (this._attached) { lazy.log.debug("detach db"); ESE.FailSafeDetachDatabaseW(this._sessionId, this.dbPath); this._attached = false; } if (this._sessionCreated) { lazy.log.debug("end session"); ESE.FailSafeEndSession(this._sessionId, 0); this._sessionCreated = false; } if (this._instanceCreated) { lazy.log.debug("term"); ESE.FailSafeTerm(this._instanceId); this._instanceCreated = false; } }, incrementReferenceCounter() { this._references++; }, decrementReferenceCounter() { this._references--; if (this._references <= 0) { this._close(); } }, }; export 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) { const utils = Cc[ "@mozilla.org/profile/migrator/edgemigrationutils;1" ].createInstance(Ci.nsIEdgeMigrationUtils); const locked = await utils.isDbLocked(dbFile); if (locked) { console.error(`ESE DB at ${dbFile.path} is locked.`); } return locked; }, closeDB(db) { db.decrementReferenceCounter(); }, COLUMN_TYPES, };