1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5"use strict";
6
7var EXPORTED_SYMBOLS = ["ESEDBReader"]; /* exported ESEDBReader */
8
9const { ctypes } = ChromeUtils.import("resource://gre/modules/ctypes.jsm");
10const { XPCOMUtils } = ChromeUtils.import(
11  "resource://gre/modules/XPCOMUtils.jsm"
12);
13const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
14XPCOMUtils.defineLazyGetter(this, "log", () => {
15  let ConsoleAPI = ChromeUtils.import("resource://gre/modules/Console.jsm", {})
16    .ConsoleAPI;
17  let consoleOptions = {
18    maxLogLevelPref: "browser.esedbreader.loglevel",
19    prefix: "ESEDBReader",
20  };
21  return new ConsoleAPI(consoleOptions);
22});
23
24ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
25
26// We have a globally unique identifier for ESE instances. A new one
27// is used for each different database opened.
28let gESEInstanceCounter = 0;
29
30// We limit the length of strings that we read from databases.
31const MAX_STR_LENGTH = 64 * 1024;
32
33// Kernel-related types:
34const KERNEL = {};
35KERNEL.FILETIME = new ctypes.StructType("FILETIME", [
36  { dwLowDateTime: ctypes.uint32_t },
37  { dwHighDateTime: ctypes.uint32_t },
38]);
39KERNEL.SYSTEMTIME = new ctypes.StructType("SYSTEMTIME", [
40  { wYear: ctypes.uint16_t },
41  { wMonth: ctypes.uint16_t },
42  { wDayOfWeek: ctypes.uint16_t },
43  { wDay: ctypes.uint16_t },
44  { wHour: ctypes.uint16_t },
45  { wMinute: ctypes.uint16_t },
46  { wSecond: ctypes.uint16_t },
47  { wMilliseconds: ctypes.uint16_t },
48]);
49
50// DB column types, cribbed from the ESE header
51var COLUMN_TYPES = {
52  JET_coltypBit: 1 /* True, False, or NULL */,
53  JET_coltypUnsignedByte: 2 /* 1-byte integer, unsigned */,
54  JET_coltypShort: 3 /* 2-byte integer, signed */,
55  JET_coltypLong: 4 /* 4-byte integer, signed */,
56  JET_coltypCurrency: 5 /* 8 byte integer, signed */,
57  JET_coltypIEEESingle: 6 /* 4-byte IEEE single precision */,
58  JET_coltypIEEEDouble: 7 /* 8-byte IEEE double precision */,
59  JET_coltypDateTime: 8 /* Integral date, fractional time */,
60  JET_coltypBinary: 9 /* Binary data, < 255 bytes */,
61  JET_coltypText: 10 /* ANSI text, case insensitive, < 255 bytes */,
62  JET_coltypLongBinary: 11 /* Binary data, long value */,
63  JET_coltypLongText: 12 /* ANSI text, long value */,
64
65  JET_coltypUnsignedLong: 14 /* 4-byte unsigned integer */,
66  JET_coltypLongLong: 15 /* 8-byte signed integer */,
67  JET_coltypGUID: 16 /* 16-byte globally unique identifier */,
68};
69
70// Not very efficient, but only used for error messages
71function getColTypeName(numericValue) {
72  return (
73    Object.keys(COLUMN_TYPES).find(t => COLUMN_TYPES[t] == numericValue) ||
74    "unknown"
75  );
76}
77
78// All type constants and method wrappers go on this object:
79const ESE = {};
80ESE.JET_ERR = ctypes.long;
81ESE.JET_PCWSTR = ctypes.char16_t.ptr;
82// The ESE header calls this JET_API_PTR, but because it isn't ever used as a
83// pointer and because OS.File code implies that the name you give a type
84// matters, I opted for a different name.
85// Note that this is defined differently on 32 vs. 64-bit in the header.
86ESE.JET_API_ITEM =
87  ctypes.voidptr_t.size == 4 ? ctypes.unsigned_long : ctypes.uint64_t;
88ESE.JET_INSTANCE = ESE.JET_API_ITEM;
89ESE.JET_SESID = ESE.JET_API_ITEM;
90ESE.JET_TABLEID = ESE.JET_API_ITEM;
91ESE.JET_COLUMNID = ctypes.unsigned_long;
92ESE.JET_GRBIT = ctypes.unsigned_long;
93ESE.JET_COLTYP = ctypes.unsigned_long;
94ESE.JET_DBID = ctypes.unsigned_long;
95
96ESE.JET_COLUMNDEF = new ctypes.StructType("JET_COLUMNDEF", [
97  { cbStruct: ctypes.unsigned_long },
98  { columnid: ESE.JET_COLUMNID },
99  { coltyp: ESE.JET_COLTYP },
100  { wCountry: ctypes.unsigned_short }, // sepcifies the country/region for the column definition
101  { langid: ctypes.unsigned_short },
102  { cp: ctypes.unsigned_short },
103  { wCollate: ctypes.unsigned_short } /* Must be 0 */,
104  { cbMax: ctypes.unsigned_long },
105  { grbit: ESE.JET_GRBIT },
106]);
107
108// Track open databases
109let gOpenDBs = new Map();
110
111// Track open libraries
112let gLibs = {};
113this.ESE = ESE; // Required for tests.
114this.KERNEL = KERNEL; // ditto
115this.gLibs = gLibs; // ditto
116
117function convertESEError(errorCode) {
118  switch (errorCode) {
119    case -1213 /* JET_errPageSizeMismatch */:
120    case -1002 /* JET_errInvalidName*/:
121    case -1507 /* JET_errColumnNotFound */:
122      // The DB format has changed and we haven't updated this migration code:
123      return "The database format has changed, error code: " + errorCode;
124    case -1032 /* JET_errFileAccessDenied */:
125    case -1207 /* JET_errDatabaseLocked */:
126    case -1302 /* JET_errTableLocked */:
127      return "The database or table is locked, error code: " + errorCode;
128    case -1305 /* JET_errObjectNotFound */:
129      return "The table/object was not found.";
130    case -1809 /* JET_errPermissionDenied*/:
131    case -1907 /* JET_errAccessDenied */:
132      return "Access or permission denied, error code: " + errorCode;
133    case -1044 /* JET_errInvalidFilename */:
134      return "Invalid file name";
135    case -1811 /* JET_errFileNotFound */:
136      return "File not found";
137    case -550 /* JET_errDatabaseDirtyShutdown */:
138      return "Database in dirty shutdown state (without the requisite logs?)";
139    case -514 /* JET_errBadLogVersion */:
140      return "Database log version does not match the version of ESE in use.";
141    default:
142      return "Unknown error: " + errorCode;
143  }
144}
145
146function handleESEError(
147  method,
148  methodName,
149  shouldThrow = true,
150  errorLog = true
151) {
152  return function() {
153    let rv;
154    try {
155      rv = method.apply(null, arguments);
156    } catch (ex) {
157      log.error("Error calling into ctypes method", methodName, ex);
158      throw ex;
159    }
160    let resultCode = parseInt(rv.toString(10), 10);
161    if (resultCode < 0) {
162      if (errorLog) {
163        log.error("Got error " + resultCode + " calling " + methodName);
164      }
165      if (shouldThrow) {
166        throw new Error(convertESEError(rv));
167      }
168    } else if (resultCode > 0 && errorLog) {
169      log.warn("Got warning " + resultCode + " calling " + methodName);
170    }
171    return resultCode;
172  };
173}
174
175function declareESEFunction(methodName, ...args) {
176  let declaration = ["Jet" + methodName, ctypes.winapi_abi, ESE.JET_ERR].concat(
177    args
178  );
179  let ctypeMethod = gLibs.ese.declare.apply(gLibs.ese, declaration);
180  ESE[methodName] = handleESEError(ctypeMethod, methodName);
181  ESE["FailSafe" + methodName] = handleESEError(ctypeMethod, methodName, false);
182  ESE["Manual" + methodName] = handleESEError(
183    ctypeMethod,
184    methodName,
185    false,
186    false
187  );
188}
189
190function declareESEFunctions() {
191  declareESEFunction(
192    "GetDatabaseFileInfoW",
193    ESE.JET_PCWSTR,
194    ctypes.voidptr_t,
195    ctypes.unsigned_long,
196    ctypes.unsigned_long
197  );
198
199  declareESEFunction(
200    "GetSystemParameterW",
201    ESE.JET_INSTANCE,
202    ESE.JET_SESID,
203    ctypes.unsigned_long,
204    ESE.JET_API_ITEM.ptr,
205    ESE.JET_PCWSTR,
206    ctypes.unsigned_long
207  );
208  declareESEFunction(
209    "SetSystemParameterW",
210    ESE.JET_INSTANCE.ptr,
211    ESE.JET_SESID,
212    ctypes.unsigned_long,
213    ESE.JET_API_ITEM,
214    ESE.JET_PCWSTR
215  );
216  declareESEFunction("CreateInstanceW", ESE.JET_INSTANCE.ptr, ESE.JET_PCWSTR);
217  declareESEFunction("Init", ESE.JET_INSTANCE.ptr);
218
219  declareESEFunction(
220    "BeginSessionW",
221    ESE.JET_INSTANCE,
222    ESE.JET_SESID.ptr,
223    ESE.JET_PCWSTR,
224    ESE.JET_PCWSTR
225  );
226  declareESEFunction(
227    "AttachDatabaseW",
228    ESE.JET_SESID,
229    ESE.JET_PCWSTR,
230    ESE.JET_GRBIT
231  );
232  declareESEFunction("DetachDatabaseW", ESE.JET_SESID, ESE.JET_PCWSTR);
233  declareESEFunction(
234    "OpenDatabaseW",
235    ESE.JET_SESID,
236    ESE.JET_PCWSTR,
237    ESE.JET_PCWSTR,
238    ESE.JET_DBID.ptr,
239    ESE.JET_GRBIT
240  );
241  declareESEFunction(
242    "OpenTableW",
243    ESE.JET_SESID,
244    ESE.JET_DBID,
245    ESE.JET_PCWSTR,
246    ctypes.voidptr_t,
247    ctypes.unsigned_long,
248    ESE.JET_GRBIT,
249    ESE.JET_TABLEID.ptr
250  );
251
252  declareESEFunction(
253    "GetColumnInfoW",
254    ESE.JET_SESID,
255    ESE.JET_DBID,
256    ESE.JET_PCWSTR,
257    ESE.JET_PCWSTR,
258    ctypes.voidptr_t,
259    ctypes.unsigned_long,
260    ctypes.unsigned_long
261  );
262
263  declareESEFunction(
264    "Move",
265    ESE.JET_SESID,
266    ESE.JET_TABLEID,
267    ctypes.long,
268    ESE.JET_GRBIT
269  );
270
271  declareESEFunction(
272    "RetrieveColumn",
273    ESE.JET_SESID,
274    ESE.JET_TABLEID,
275    ESE.JET_COLUMNID,
276    ctypes.voidptr_t,
277    ctypes.unsigned_long,
278    ctypes.unsigned_long.ptr,
279    ESE.JET_GRBIT,
280    ctypes.voidptr_t
281  );
282
283  declareESEFunction("CloseTable", ESE.JET_SESID, ESE.JET_TABLEID);
284  declareESEFunction(
285    "CloseDatabase",
286    ESE.JET_SESID,
287    ESE.JET_DBID,
288    ESE.JET_GRBIT
289  );
290
291  declareESEFunction("EndSession", ESE.JET_SESID, ESE.JET_GRBIT);
292
293  declareESEFunction("Term", ESE.JET_INSTANCE);
294}
295
296function unloadLibraries() {
297  log.debug("Unloading");
298  if (gOpenDBs.size) {
299    log.error("Shouldn't unload libraries before DBs are closed!");
300    for (let db of gOpenDBs.values()) {
301      db._close();
302    }
303  }
304  for (let k of Object.keys(ESE)) {
305    delete ESE[k];
306  }
307  gLibs.ese.close();
308  gLibs.kernel.close();
309  delete gLibs.ese;
310  delete gLibs.kernel;
311}
312
313function loadLibraries() {
314  Services.obs.addObserver(unloadLibraries, "xpcom-shutdown");
315  gLibs.ese = ctypes.open("esent.dll");
316  gLibs.kernel = ctypes.open("kernel32.dll");
317  KERNEL.FileTimeToSystemTime = gLibs.kernel.declare(
318    "FileTimeToSystemTime",
319    ctypes.winapi_abi,
320    ctypes.int,
321    KERNEL.FILETIME.ptr,
322    KERNEL.SYSTEMTIME.ptr
323  );
324
325  declareESEFunctions();
326}
327
328function ESEDB(rootPath, dbPath, logPath) {
329  log.info("Created db");
330  this.rootPath = rootPath;
331  this.dbPath = dbPath;
332  this.logPath = logPath;
333  this._references = 0;
334  this._init();
335}
336
337ESEDB.prototype = {
338  rootPath: null,
339  dbPath: null,
340  logPath: null,
341  _opened: false,
342  _attached: false,
343  _sessionCreated: false,
344  _instanceCreated: false,
345  _dbId: null,
346  _sessionId: null,
347  _instanceId: null,
348
349  _init() {
350    if (!gLibs.ese) {
351      loadLibraries();
352    }
353    this.incrementReferenceCounter();
354    this._internalOpen();
355  },
356
357  _internalOpen() {
358    try {
359      let dbinfo = new ctypes.unsigned_long();
360      ESE.GetDatabaseFileInfoW(
361        this.dbPath,
362        dbinfo.address(),
363        ctypes.unsigned_long.size,
364        17
365      );
366
367      let pageSize = ctypes.UInt64.lo(dbinfo.value);
368      ESE.SetSystemParameterW(
369        null,
370        0,
371        64 /* JET_paramDatabasePageSize*/,
372        pageSize,
373        null
374      );
375
376      this._instanceId = new ESE.JET_INSTANCE();
377      ESE.CreateInstanceW(
378        this._instanceId.address(),
379        "firefox-dbreader-" + gESEInstanceCounter++
380      );
381      this._instanceCreated = true;
382
383      ESE.SetSystemParameterW(
384        this._instanceId.address(),
385        0,
386        0 /* JET_paramSystemPath*/,
387        0,
388        this.rootPath
389      );
390      ESE.SetSystemParameterW(
391        this._instanceId.address(),
392        0,
393        1 /* JET_paramTempPath */,
394        0,
395        this.rootPath
396      );
397      ESE.SetSystemParameterW(
398        this._instanceId.address(),
399        0,
400        2 /* JET_paramLogFilePath*/,
401        0,
402        this.logPath
403      );
404
405      // Shouldn't try to call JetTerm if the following call fails.
406      this._instanceCreated = false;
407      ESE.Init(this._instanceId.address());
408      this._instanceCreated = true;
409      this._sessionId = new ESE.JET_SESID();
410      ESE.BeginSessionW(
411        this._instanceId,
412        this._sessionId.address(),
413        null,
414        null
415      );
416      this._sessionCreated = true;
417
418      const JET_bitDbReadOnly = 1;
419      ESE.AttachDatabaseW(this._sessionId, this.dbPath, JET_bitDbReadOnly);
420      this._attached = true;
421      this._dbId = new ESE.JET_DBID();
422      ESE.OpenDatabaseW(
423        this._sessionId,
424        this.dbPath,
425        null,
426        this._dbId.address(),
427        JET_bitDbReadOnly
428      );
429      this._opened = true;
430    } catch (ex) {
431      try {
432        this._close();
433      } catch (innerException) {
434        Cu.reportError(innerException);
435      }
436      // Make sure caller knows we failed.
437      throw ex;
438    }
439    gOpenDBs.set(this.dbPath, this);
440  },
441
442  checkForColumn(tableName, columnName) {
443    if (!this._opened) {
444      throw new Error("The database was closed!");
445    }
446
447    let columnInfo;
448    try {
449      columnInfo = this._getColumnInfo(tableName, [{ name: columnName }]);
450    } catch (ex) {
451      return null;
452    }
453    return columnInfo[0];
454  },
455
456  tableExists(tableName) {
457    if (!this._opened) {
458      throw new Error("The database was closed!");
459    }
460
461    let tableId = new ESE.JET_TABLEID();
462    let rv = ESE.ManualOpenTableW(
463      this._sessionId,
464      this._dbId,
465      tableName,
466      null,
467      0,
468      4 /* JET_bitTableReadOnly */,
469      tableId.address()
470    );
471    if (rv == -1305 /* JET_errObjectNotFound */) {
472      return false;
473    }
474    if (rv < 0) {
475      log.error("Got error " + rv + " calling OpenTableW");
476      throw new Error(convertESEError(rv));
477    }
478
479    if (rv > 0) {
480      log.error("Got warning " + rv + " calling OpenTableW");
481    }
482    ESE.FailSafeCloseTable(this._sessionId, tableId);
483    return true;
484  },
485
486  *tableItems(tableName, columns) {
487    if (!this._opened) {
488      throw new Error("The database was closed!");
489    }
490
491    let tableOpened = false;
492    let tableId;
493    try {
494      tableId = this._openTable(tableName);
495      tableOpened = true;
496
497      let columnInfo = this._getColumnInfo(tableName, columns);
498
499      let rv = ESE.ManualMove(
500        this._sessionId,
501        tableId,
502        -2147483648 /* JET_MoveFirst */,
503        0
504      );
505      if (rv == -1603 /* JET_errNoCurrentRecord */) {
506        // There are no rows in the table.
507        this._closeTable(tableId);
508        return;
509      }
510      if (rv != 0) {
511        throw new Error(convertESEError(rv));
512      }
513
514      do {
515        let rowContents = {};
516        for (let column of columnInfo) {
517          let [buffer, bufferSize] = this._getBufferForColumn(column);
518          // We handle errors manually so we accurately deal with NULL values.
519          let err = ESE.ManualRetrieveColumn(
520            this._sessionId,
521            tableId,
522            column.id,
523            buffer.address(),
524            bufferSize,
525            null,
526            0,
527            null
528          );
529          rowContents[column.name] = this._convertResult(column, buffer, err);
530        }
531        yield rowContents;
532      } while (
533        ESE.ManualMove(this._sessionId, tableId, 1 /* JET_MoveNext */, 0) === 0
534      );
535    } catch (ex) {
536      if (tableOpened) {
537        this._closeTable(tableId);
538      }
539      throw ex;
540    }
541    this._closeTable(tableId);
542  },
543
544  _openTable(tableName) {
545    let tableId = new ESE.JET_TABLEID();
546    ESE.OpenTableW(
547      this._sessionId,
548      this._dbId,
549      tableName,
550      null,
551      0,
552      4 /* JET_bitTableReadOnly */,
553      tableId.address()
554    );
555    return tableId;
556  },
557
558  _getBufferForColumn(column) {
559    let buffer;
560    if (column.type == "string") {
561      let wchar_tArray = ctypes.ArrayType(ctypes.char16_t);
562      // size on the column is in bytes, 2 bytes to a wchar, so:
563      let charCount = column.dbSize >> 1;
564      buffer = new wchar_tArray(charCount);
565    } else if (column.type == "boolean") {
566      buffer = new ctypes.uint8_t();
567    } else if (column.type == "date") {
568      buffer = new KERNEL.FILETIME();
569    } else if (column.type == "guid") {
570      let byteArray = ctypes.ArrayType(ctypes.uint8_t);
571      buffer = new byteArray(column.dbSize);
572    } else {
573      throw new Error("Unknown type " + column.type);
574    }
575    return [buffer, buffer.constructor.size];
576  },
577
578  _convertResult(column, buffer, err) {
579    if (err != 0) {
580      if (err == 1004) {
581        // Deal with null values:
582        buffer = null;
583      } else {
584        Cu.reportError(
585          "Unexpected JET error: " +
586            err +
587            "; retrieving value for column " +
588            column.name
589        );
590        throw new Error(convertESEError(err));
591      }
592    }
593    if (column.type == "string") {
594      return buffer ? buffer.readString() : "";
595    }
596    if (column.type == "boolean") {
597      return buffer ? buffer.value == 255 : false;
598    }
599    if (column.type == "guid") {
600      if (buffer.length != 16) {
601        Cu.reportError(
602          "Buffer size for guid field " + column.id + " should have been 16!"
603        );
604        return "";
605      }
606      let rv = "{";
607      for (let i = 0; i < 16; i++) {
608        if (i == 4 || i == 6 || i == 8 || i == 10) {
609          rv += "-";
610        }
611        let byteValue = buffer.addressOfElement(i).contents;
612        // Ensure there's a leading 0
613        rv += ("0" + byteValue.toString(16)).substr(-2);
614      }
615      return rv + "}";
616    }
617    if (column.type == "date") {
618      if (!buffer) {
619        return null;
620      }
621      let systemTime = new KERNEL.SYSTEMTIME();
622      let result = KERNEL.FileTimeToSystemTime(
623        buffer.address(),
624        systemTime.address()
625      );
626      if (result == 0) {
627        throw new Error(ctypes.winLastError);
628      }
629
630      // System time is in UTC, so we use Date.UTC to get milliseconds from epoch,
631      // then divide by 1000 to get seconds, and round down:
632      return new Date(
633        Date.UTC(
634          systemTime.wYear,
635          systemTime.wMonth - 1,
636          systemTime.wDay,
637          systemTime.wHour,
638          systemTime.wMinute,
639          systemTime.wSecond,
640          systemTime.wMilliseconds
641        )
642      );
643    }
644    return undefined;
645  },
646
647  _getColumnInfo(tableName, columns) {
648    let rv = [];
649    for (let column of columns) {
650      let columnInfoFromDB = new ESE.JET_COLUMNDEF();
651      ESE.GetColumnInfoW(
652        this._sessionId,
653        this._dbId,
654        tableName,
655        column.name,
656        columnInfoFromDB.address(),
657        ESE.JET_COLUMNDEF.size,
658        0 /* JET_ColInfo */
659      );
660      let dbType = parseInt(columnInfoFromDB.coltyp.toString(10), 10);
661      let dbSize = parseInt(columnInfoFromDB.cbMax.toString(10), 10);
662      if (column.type == "string") {
663        if (
664          dbType != COLUMN_TYPES.JET_coltypLongText &&
665          dbType != COLUMN_TYPES.JET_coltypText
666        ) {
667          throw new Error(
668            "Invalid column type for column " +
669              column.name +
670              "; expected text type, got type " +
671              getColTypeName(dbType)
672          );
673        }
674        if (dbSize > MAX_STR_LENGTH) {
675          throw new Error(
676            "Column " +
677              column.name +
678              " has more than 64k data in it. This API is not designed to handle data that large."
679          );
680        }
681      } else if (column.type == "boolean") {
682        if (dbType != COLUMN_TYPES.JET_coltypBit) {
683          throw new Error(
684            "Invalid column type for column " +
685              column.name +
686              "; expected bit type, got type " +
687              getColTypeName(dbType)
688          );
689        }
690      } else if (column.type == "date") {
691        if (dbType != COLUMN_TYPES.JET_coltypLongLong) {
692          throw new Error(
693            "Invalid column type for column " +
694              column.name +
695              "; expected long long type, got type " +
696              getColTypeName(dbType)
697          );
698        }
699      } else if (column.type == "guid") {
700        if (dbType != COLUMN_TYPES.JET_coltypGUID) {
701          throw new Error(
702            "Invalid column type for column " +
703              column.name +
704              "; expected guid type, got type " +
705              getColTypeName(dbType)
706          );
707        }
708      } else if (column.type) {
709        throw new Error(
710          "Unknown column type " +
711            column.type +
712            " requested for column " +
713            column.name +
714            ", don't know what to do."
715        );
716      }
717
718      rv.push({
719        name: column.name,
720        id: columnInfoFromDB.columnid,
721        type: column.type,
722        dbSize,
723        dbType,
724      });
725    }
726    return rv;
727  },
728
729  _closeTable(tableId) {
730    ESE.FailSafeCloseTable(this._sessionId, tableId);
731  },
732
733  _close() {
734    this._internalClose();
735    gOpenDBs.delete(this.dbPath);
736  },
737
738  _internalClose() {
739    if (this._opened) {
740      log.debug("close db");
741      ESE.FailSafeCloseDatabase(this._sessionId, this._dbId, 0);
742      log.debug("finished close db");
743      this._opened = false;
744    }
745    if (this._attached) {
746      log.debug("detach db");
747      ESE.FailSafeDetachDatabaseW(this._sessionId, this.dbPath);
748      this._attached = false;
749    }
750    if (this._sessionCreated) {
751      log.debug("end session");
752      ESE.FailSafeEndSession(this._sessionId, 0);
753      this._sessionCreated = false;
754    }
755    if (this._instanceCreated) {
756      log.debug("term");
757      ESE.FailSafeTerm(this._instanceId);
758      this._instanceCreated = false;
759    }
760  },
761
762  incrementReferenceCounter() {
763    this._references++;
764  },
765
766  decrementReferenceCounter() {
767    this._references--;
768    if (this._references <= 0) {
769      this._close();
770    }
771  },
772};
773
774let ESEDBReader = {
775  openDB(rootDir, dbFile, logDir) {
776    let dbFilePath = dbFile.path;
777    if (gOpenDBs.has(dbFilePath)) {
778      let db = gOpenDBs.get(dbFilePath);
779      db.incrementReferenceCounter();
780      return db;
781    }
782    // ESE is really picky about the trailing slashes according to the docs,
783    // so we do as we're told and ensure those are there:
784    return new ESEDB(rootDir.path + "\\", dbFilePath, logDir.path + "\\");
785  },
786
787  async dbLocked(dbFile) {
788    let options = { winShare: OS.Constants.Win.FILE_SHARE_READ };
789    let locked = true;
790    await OS.File.open(dbFile.path, { read: true }, options).then(
791      fileHandle => {
792        locked = false;
793        // Return the close promise so we wait for the file to be closed again.
794        // Otherwise the file might still be kept open by this handle by the time
795        // that we try to use the ESE APIs to access it.
796        return fileHandle.close();
797      },
798      () => {
799        Cu.reportError("ESE DB at " + dbFile.path + " is locked.");
800      }
801    );
802    return locked;
803  },
804
805  closeDB(db) {
806    db.decrementReferenceCounter();
807  },
808
809  COLUMN_TYPES,
810};
811