tenfourfox/browser/components/migration/ESEDBReader.jsm
Cameron Kaiser c9b2922b70 hello FPR
2017-04-19 00:56:45 -07:00

584 lines
20 KiB
JavaScript

/* 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"];
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);
});
// 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 = {};
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 -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", false);
gLibs.ese = ctypes.open("esent.dll");
gLibs.kernel = ctypes.open("kernel32.dll");
KERNEL.FileTimeToSystemTime = gLibs.kernel.declare("FileTimeToSystemTime",
ctypes.default_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);
ESE.Init(this._instanceId.address());
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: function*(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 (0 === ESE.ManualMove(this._sessionId, tableId, 1 /* JET_MoveNext */, 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 ? (255 == buffer.value) : 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));
}
},
_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 + "\\");
},
closeDB(db) {
db.decrementReferenceCounter();
},
COLUMN_TYPES,
};