mdb-reader
Version:
JavaScript library to read data from Access databases
119 lines (118 loc) • 5.15 kB
JavaScript
import { createCodecHandler } from "./codec-handler/index.js";
import { decryptRC4 } from "./crypto/index.js";
import { readDateTime } from "./data/datetime.js";
import { getJetFormat } from "./JetFormat/index.js";
import { PageType, assertPageType } from "./PageType.js";
import { uncompressText } from "./unicodeCompression.js";
import { isEmptyBuffer, xor } from "./util.js";
const PASSWORD_OFFSET = 0x42;
export class Database {
#buffer;
#format;
#codecHandler;
#databaseDefinitionPage;
constructor(buffer, password) {
this.#buffer = buffer;
assertPageType(this.#buffer, PageType.DatabaseDefinitionPage);
this.#format = getJetFormat(this.#buffer);
this.#databaseDefinitionPage = Buffer.alloc(this.#format.pageSize);
this.#buffer.copy(this.#databaseDefinitionPage, 0, 0, this.#format.pageSize);
decryptHeader(this.#databaseDefinitionPage, this.#format);
this.#codecHandler = createCodecHandler(this.#databaseDefinitionPage, password);
if (!this.#codecHandler.verifyPassword()) {
throw new Error("Wrong password");
}
}
get format() {
return this.#format;
}
getPassword() {
let passwordBuffer = this.#databaseDefinitionPage.slice(PASSWORD_OFFSET, PASSWORD_OFFSET + this.#format.databaseDefinitionPage.passwordSize);
const mask = this.#getPasswordMask();
if (mask !== null) {
passwordBuffer = xor(passwordBuffer, mask);
}
if (isEmptyBuffer(passwordBuffer)) {
return null;
}
let password = uncompressText(passwordBuffer, this.#format);
const nullCharIndex = password.indexOf("\0");
if (nullCharIndex >= 0) {
password = password.slice(0, nullCharIndex);
}
return password;
}
#getPasswordMask() {
if (this.#format.databaseDefinitionPage.creationDateOffset === null) {
return null;
}
const mask = Buffer.alloc(this.#format.databaseDefinitionPage.passwordSize);
const dateValue = this.#databaseDefinitionPage.readDoubleLE(this.#format.databaseDefinitionPage.creationDateOffset);
mask.writeInt32LE(Math.floor(dateValue));
for (let i = 0; i < mask.length; ++i) {
mask[i] = mask[i % 4];
}
return mask;
}
getCreationDate() {
if (this.#format.databaseDefinitionPage.creationDateOffset === null) {
return null;
}
const creationDateBuffer = this.#databaseDefinitionPage.slice(this.#format.databaseDefinitionPage.creationDateOffset, this.#format.databaseDefinitionPage.creationDateOffset + 8);
return readDateTime(creationDateBuffer);
}
getDefaultSortOrder() {
const value = this.#databaseDefinitionPage.readUInt16LE(this.#format.databaseDefinitionPage.defaultSortOrder.offset + 3);
if (value === 0) {
return this.#format.defaultSortOrder;
}
let version = this.#format.defaultSortOrder.version;
if (this.#format.databaseDefinitionPage.defaultSortOrder.size == 4) {
version = this.#databaseDefinitionPage.readUInt8(this.#format.databaseDefinitionPage.defaultSortOrder.offset + 3);
}
return Object.freeze({ value, version });
}
getPage(page) {
if (page === 0) {
// already decrypted
return this.#databaseDefinitionPage;
}
const offset = page * this.#format.pageSize;
if (this.#buffer.length < offset) {
throw new Error(`Page ${page} does not exist`);
}
const pageBuffer = this.#buffer.slice(offset, offset + this.#format.pageSize);
return this.#codecHandler.decryptPage(pageBuffer, page);
}
/**
* @param pageRow Lower byte contains the row number, the upper three contain page
*
* @see https://github.com/brianb/mdbtools/blob/d6f5745d949f37db969d5f424e69b54f0da60b9b/src/libmdb/data.c#L102-L124
*/
findPageRow(pageRow) {
const page = pageRow >> 8;
const row = pageRow & 0xff;
const pageBuffer = this.getPage(page);
return this.findRow(pageBuffer, row);
}
/**
* @param pageBuffer Buffer of a data page
*
* @see https://github.com/brianb/mdbtools/blob/d6f5745d949f37db969d5f424e69b54f0da60b9b/src/libmdb/data.c#L126-L138
*/
findRow(pageBuffer, row) {
const rco = this.#format.dataPage.recordCountOffset;
if (row > 1000) {
throw new Error("Cannot read rows > 1000"); // TODO: why?
}
const start = pageBuffer.readUInt16LE(rco + 2 + row * 2);
const nextStart = row === 0 ? this.#format.pageSize : pageBuffer.readUInt16LE(rco + row * 2);
return pageBuffer.slice(start, nextStart);
}
}
const ENCRYPTION_START = 0x18;
const ENCRYPTION_KEY = [0xc7, 0xda, 0x39, 0x6b];
function decryptHeader(buffer, format) {
const decryptedBuffer = decryptRC4(Buffer.from(ENCRYPTION_KEY), buffer.slice(ENCRYPTION_START, ENCRYPTION_START + format.databaseDefinitionPage.encryptedSize));
decryptedBuffer.copy(buffer, ENCRYPTION_START);
}