@regrapes/access-db-parser
Version:
A pure javascript Microsoft AccessDB files (.mdb, .accdb) parser
804 lines (796 loc) • 31.8 kB
JavaScript
import createDebug from 'debug';
import { TextDecoder } from 'util';
import { Parser } from 'binary-parser';
import UUID from 'uuid';
const originalArray = Parser.prototype.array;
Parser.prototype.array = function (varName, options) {
if (options.length === 0) {
return this.setNextParser('array', varName, options);
}
return originalArray.call(this, varName, options);
};
const ACCESSHEADER = new Parser()
.seek(4)
.string('jetString', {
zeroTerminated: true,
})
.uint32le('jetVersion')
.seek(126);
const MEMO = new Parser().uint32le('memoLength').uint32le('recordPointer').uint32le('memoUnknown').saveOffset('memoEnd');
const VERSION_3_FLAGS = new Parser()
.bit1('hyperlink')
.bit1('autoGUID')
.bit1('unk1')
.bit1('replication')
.bit1('unk2')
.bit1('autonumber')
.bit1('canBeNull')
.bit1('fixedLength');
const VERSION_4_FLAGS = new Parser()
.bit1('hyperlink')
.bit1('autoGUID')
.bit1('unk1')
.bit1('replication')
.bit1('unk2')
.bit1('autonumber')
.bit1('canBeNull')
.bit1('fixedLength')
.bit1('unk3')
.bit1('unk4')
.bit1('unk5')
.bit1('modernPackageType')
.bit1('unk6')
.bit1('unk7')
.bit1('unk8')
.bit1('compressedUnicode');
const TDEF_HEADER = new Parser()
.seek(2)
.uint16le('peekVersion')
.seek(-2)
.uint16le('tdefVer')
.uint32le('nextPagePtr')
.saveOffset('headerEnd');
const parseTableHead = (buffer, version = 3) => new Parser()
.nest('TDEF_header', { type: TDEF_HEADER })
.uint32le('tableDefinitionLength')
// Conditional
.uint32le('ver4Unknown')
.seek(version > 3 ? 0 : -4)
.uint32le('numberOfRows')
.uint32le('autonumber')
// Conditional
.uint32le('autonumberIncrement')
.seek(version > 3 ? 0 : -4)
// Conditional
.uint32le('complexAutonumber')
.seek(version > 3 ? 0 : -4)
// Conditional
.uint32le('ver4Unknown1')
.seek(version > 3 ? 0 : -4)
// Conditional
.uint32le('ver4Unknown2')
.seek(version > 3 ? 0 : -4)
.uint8('tableTypeFlags')
.uint16le('nextColumnID')
.uint16le('variableColumns')
.uint16le('columnCount')
.uint32le('indexCount')
.uint32le('realIndexCount')
.uint32le('rowPageMap')
.uint32le('freeSpacePageMap')
.saveOffset('tDefHeaderEnd')
.parse(buffer);
const parseTableData = (buffer, realIndexCount, columnCount, version = 3) => {
const REAL_INDEX = new Parser()
.uint32le('unk1')
.uint32le('indexRowCount')
.seek(version > 3 ? 0 : -4)
// Conditional
.uint32le('ver4AlwaysZero');
const VARIOUS_TEXT_V3 = new Parser().uint16le('LCID').uint16le('codePage').uint16le('variousText3Unknown');
const VARIOUS_TEXT_V4 = new Parser().uint16le('collation').uint8('variousText4Unknown').uint8('collationVersionNumber');
const VARIOUS_TEXT = version === 3 ? VARIOUS_TEXT_V3 : VARIOUS_TEXT_V4;
const VARIOUS_DEC_V3 = new Parser()
.uint16le('variousDec3Unknown')
.uint8('maxNumberOfDigits')
.uint8('numberOfDecimal')
.uint16le('variousDec3Unknown2');
const VARIOUS_DEC_V4 = new Parser().uint8('maxNumOfDigits').uint8('numOfDecimalDigits').uint16le('variousDec4Unknown');
const VARIOUS_DEC = version === 3 ? VARIOUS_DEC_V3 : VARIOUS_DEC_V4;
const COLUMN = new Parser()
.uint8('type')
// Conditional
.uint32le('ver4Unknown3')
.seek(version > 3 ? 0 : -4)
.uint16le('columnID')
.uint16le('variableColumnNumber')
.uint16le('columnIndex')
.choice('various', {
tag: 'type',
choices: {
1: VARIOUS_DEC,
2: VARIOUS_DEC,
3: VARIOUS_DEC,
4: VARIOUS_DEC,
5: VARIOUS_DEC,
6: VARIOUS_DEC,
7: VARIOUS_DEC,
8: VARIOUS_DEC,
9: VARIOUS_TEXT,
10: VARIOUS_TEXT,
11: VARIOUS_TEXT,
12: VARIOUS_TEXT,
},
defaultChoice: new Parser().seek(version === 3 ? 6 : 4),
})
.choice('columnFlags', {
// eslint-disable-next-line no-new-func
tag: new Function(`return ${version === 3 ? 1 : 0}`),
choices: {
1: VERSION_3_FLAGS,
0: VERSION_4_FLAGS,
},
})
// Conditional
.uint32le('ver4Unknown4')
.seek(version > 3 ? 0 : -4)
.uint16le('fixedOffset')
.uint16le('length');
const COLUMN_NAMES_V3 = new Parser().uint8('colNamesLen').string('colNameStr', {
length: 'colNamesLen',
encoding: 'utf8',
stripNull: true,
});
const COLUMN_NAMES_V4 = new Parser().uint16le('colNamesLen').buffer('colNameStr', {
length: 'colNamesLen',
});
// .string("colNameStr", {
// length: "colNamesLen",
// encoding: "utf16",
// stripNull: true,
// });
const COLUMN_NAMES = version === 3 ? COLUMN_NAMES_V3 : COLUMN_NAMES_V4;
const res = new Parser()
.array('readIndex', {
length: realIndexCount,
type: REAL_INDEX,
})
.array('column', {
length: columnCount,
type: COLUMN,
})
.array('columnNames', {
length: columnCount,
type: COLUMN_NAMES,
})
.parse(buffer);
if (version !== 3) {
for (const columnName of res.columnNames) {
const buffer = columnName.colNameStr;
columnName.colNameStr = new TextDecoder('utf-16le').decode(buffer);
}
}
return res;
};
const parseDataPageHeader = (buffer, version = 3) => new Parser()
.seek(2)
.uint16le('dataFreeSpace')
.uint32le('owner')
.seek(version > 3 ? 0 : -4)
// Conditional
.uint32le('ver4UnknownData')
.uint16le('recordCount')
.array('recordOffsets', {
length: 'recordCount',
type: 'uint16le',
})
.parse(buffer);
const parseRelativeObjectMetadataStruct = (buffer, variableJumpTablesCNT = 0, version = 3) => {
if (version === 3) {
return new Parser()
.uint8('variableLengthFieldCount')
.array('variableLengthJumpTable', {
length: variableJumpTablesCNT,
type: 'uint8',
})
.array('variableLengthFieldOffsets', {
length() {
return this.variableLengthFieldCount;
},
type: 'uint8',
})
.uint8('varLenCount')
.saveOffset('relativeMetadataEnd')
.parse(buffer);
}
else {
const part1 = new Parser()
.uint16le('variableLengthFieldCount')
.array('variableLengthJumpTable', {
length: variableJumpTablesCNT,
type: 'uint8',
})
.saveOffset('part2StartOffset')
.parse(buffer);
const part2 = new Parser()
.array('variableLengthFieldOffsets', {
length: (part1.variableLengthFieldCount & 0xff) >>> 0,
type: 'uint16le',
})
.uint16le('varLenCount')
.saveOffset('relativeMetadataEnd')
.parse(buffer.slice(part1.part2StartOffset));
const result = Object.assign(Object.assign({}, part1), part2);
return result;
}
};
var DataType;
(function (DataType) {
DataType[DataType["Boolean"] = 1] = "Boolean";
DataType[DataType["Int8"] = 2] = "Int8";
DataType[DataType["Int16"] = 3] = "Int16";
DataType[DataType["Int32"] = 4] = "Int32";
DataType[DataType["Money"] = 5] = "Money";
DataType[DataType["Float32"] = 6] = "Float32";
DataType[DataType["Float64"] = 7] = "Float64";
DataType[DataType["DateTime"] = 8] = "DateTime";
DataType[DataType["Binary"] = 9] = "Binary";
DataType[DataType["Text"] = 10] = "Text";
DataType[DataType["OLE"] = 11] = "OLE";
DataType[DataType["Memo"] = 12] = "Memo";
DataType[DataType["GUID"] = 15] = "GUID";
DataType[DataType["Bit96Bytes17"] = 16] = "Bit96Bytes17";
DataType[DataType["Complex"] = 18] = "Complex";
})(DataType || (DataType = {}));
const TABLE_PAGE_MAGIC = Buffer.from([0x02, 0x01]);
const DATA_PAGE_MAGIC = Buffer.from([0x01, 0x01]);
const BOMS = [Buffer.from([0xfe, 0xff]), Buffer.from([0xff, 0xfe])];
const parseType = (dataType, buffer, length, version = 3, textEncoding = 'utf8', sanitizeTextBuffer = (buffer) => buffer) => {
switch (dataType) {
case DataType.Int8: {
return buffer.readInt8(0);
}
case DataType.Int16: {
return buffer.readInt16LE(0);
}
case DataType.Int32:
case DataType.Complex: {
return buffer.readInt32LE(0);
}
case DataType.Float32: {
return buffer.readFloatLE(0);
}
case DataType.Float64: {
return buffer.readDoubleLE(0);
}
case DataType.Money: {
return buffer.readUInt32LE(0) + buffer.readUInt32LE(4) * Math.pow(0x10, 8);
}
case DataType.DateTime: {
const daysPassed = Math.floor(buffer.readDoubleLE(0));
// ms access expresses hours in decimals
const hoursPassedDecimal = buffer.readDoubleLE(0) % 1;
const hours = Math.floor(hoursPassedDecimal * 24);
const minutes = Math.floor(((hoursPassedDecimal * 24) % 1) * 60);
const seconds = Math.ceil(((((hoursPassedDecimal * 24) % 1) * 60) % 1) * 60);
const date = new Date(Date.UTC(1899, 11, 30));
date.setUTCHours(12, 0, 0, 0);
date.setUTCDate(date.getUTCDate() + daysPassed);
date.setUTCHours(hours, minutes, seconds);
return date;
}
case DataType.Binary: {
return buffer.slice(0, length).toString(textEncoding); // Maybe
}
case DataType.GUID: {
return UUID.stringify(buffer.slice(0, 16));
}
case DataType.Bit96Bytes17: {
return buffer.slice(0, 17).toString(textEncoding); // Maybe
}
case DataType.Text: {
if (version > 3) {
const sanitzedBuffer = sanitizeTextBuffer(buffer);
if (sanitzedBuffer.slice(0, 2).compare(BOMS[0]) === 0 || sanitzedBuffer.slice(0, 2).compare(BOMS[1]) === 0) {
return new TextDecoder(textEncoding, { ignoreBOM: true }).decode(sanitzedBuffer.slice(2));
}
return new TextDecoder('utf-16le').decode(buffer);
}
return buffer.toString(textEncoding);
}
default: {
return null;
}
}
};
const categorizePages = (dbData, pageSize) => {
if (dbData.length % pageSize)
throw new Error(`DB is not full or pageSize is wrong. pageSize: ${pageSize} dbData.length: ${dbData.length}`);
const pages = {};
for (let i = 0; i < dbData.length; i += pageSize)
pages[i] = dbData.slice(i, i + pageSize);
const dataPages = {};
const tableDefs = {};
for (const page of Object.keys(pages)) {
const comp1 = Buffer.compare(DATA_PAGE_MAGIC, pages[page].slice(0, DATA_PAGE_MAGIC.length)) === 0;
const comp2 = Buffer.compare(TABLE_PAGE_MAGIC, pages[page].slice(0, TABLE_PAGE_MAGIC.length)) === 0;
if (comp1)
dataPages[page] = pages[page];
else if (comp2)
tableDefs[page] = pages[page];
}
return [tableDefs, dataPages, pages];
};
const debug = createDebug('access-db-parser:AccessTable');
class AccessTable {
constructor(table, version, pageSize, dataPages, tableDefs, textEncoding, sanitizeTextBuffer) {
this.table = table;
this.version = version;
this.pageSize = pageSize;
this.dataPages = dataPages;
this.tableDefs = tableDefs;
this.textEncoding = textEncoding;
this.sanitizeTextBuffer = sanitizeTextBuffer;
this.parsedTable = {};
[this.columns, this.tableHeader] = this.getTableColumns();
}
getTableColumns() {
let tableHeader;
let colNames;
let columns;
try {
tableHeader = parseTableHead(this.table.value, this.version);
let mergedData = this.table.value.slice(tableHeader.tDefHeaderEnd);
if (tableHeader.TDEF_header.nextPagePtr) {
mergedData = Buffer.concat([mergedData, this.mergeTableData(tableHeader.TDEF_header.nextPagePtr)]);
}
const parsedData = parseTableData(mergedData, tableHeader.realIndexCount, tableHeader.columnCount, this.version);
columns = parsedData.column;
colNames = parsedData.columnNames;
// REMOVE FOR NOW
// (tableHeader as any).column = parsedData.column;
// (tableHeader as any).columnNames = parsedData.columnNames;
}
catch (err) {
throw new Error(`Failed to parse table header`);
}
// const colNames = tableHeader.columnNames;
// const columns = tableHeader.column;
columns.forEach((column, index) => {
column.colNameStr = colNames[index].colNameStr;
});
const offset = Math.min(...columns.map(c => c.columnIndex));
const columnDict = {};
for (const x of columns)
columnDict[x.columnIndex - offset] = x;
if (Object.keys(columnDict).length !== columns.length) {
for (const x of columns)
columnDict[x.columnID] = x;
}
if (Object.keys(columnDict).length !== tableHeader.columnCount)
throw new Error(`Expected ${tableHeader.columnCount} columns got ${Object.keys(columnDict).length}`);
return [columnDict, tableHeader];
}
mergeTableData(firstPage) {
let table = this.tableDefs[firstPage * this.pageSize];
let parsedHeader = TDEF_HEADER.parse(table);
let data = table.slice(parsedHeader.headerEnd);
while (parsedHeader.nextPagePtr) {
table = this.tableDefs[parsedHeader.nextPagePtr * this.pageSize];
parsedHeader = TDEF_HEADER.parse(table);
data = Buffer.concat([data, table.slice(parsedHeader.headerEnd)]);
}
return data;
}
createEmptyTable() {
const parsedTable = {};
const [columns] = this.getTableColumns();
for (const i of Object.keys(columns)) {
const column = columns[i];
parsedTable[column.colNameStr] = [];
}
return parsedTable;
}
getOverflowRecord(recordPointer) {
const recordOffset = (recordPointer & 0xff) >>> 0;
const pageNum = recordPointer >>> 8;
const recordPage = this.dataPages[pageNum * this.pageSize];
if (!recordPage)
return;
const parsedData = parseDataPageHeader(recordPage, this.version);
if (recordOffset > parsedData.recordOffsets.length)
return;
let start = parsedData.recordOffsets[recordOffset];
if ((start & 0x8000) >>> 0)
start = (start & 0xfff) >>> 0;
else
debug(`Overflow record flag is not present ${start}`);
let record;
if (recordOffset === 0) {
record = recordPage.slice(start);
}
else {
let end = parsedData.recordOffsets[recordOffset - 1];
if (end & 0x8000 && (end & 0xff) !== 0) {
end &= 0xfff;
}
record = recordPage.slice(start, end);
}
return record;
}
parseFixedLengthData(originalRecord, column, nullTable) {
const columnName = column.colNameStr;
let parsedType;
if (column.type === DataType.Boolean) {
if (column.columnID > nullTable.length)
throw new Error(`Failed to parse bool field, Column not found in nullTable column: ${columnName}, column id: ${column.columnID}, nullTable: ${nullTable}`);
parsedType = nullTable[column.columnID];
}
else {
if (column.fixedOffset > originalRecord.length)
throw new Error(`Column offset is bigger than the length of the record ${column.fixedOffset}`);
const record = originalRecord.slice(column.fixedOffset);
parsedType = parseType(column.type, record, this.version, undefined, this.textEncoding, this.sanitizeTextBuffer);
}
if (this.parsedTable[columnName] === undefined)
this.parsedTable[columnName] = [];
this.parsedTable[columnName].push(parsedType);
return parsedType;
}
parseDynamicLengthRecordsMetadata(reverseRecord, originalRecord, nullTableLength) {
if (this.version > 3) {
reverseRecord = reverseRecord.slice(nullTableLength + 1);
if (reverseRecord.length > 1 && reverseRecord[0] === 0)
reverseRecord = reverseRecord.slice(1);
return parseRelativeObjectMetadataStruct(reverseRecord, undefined, this.version);
}
const variableLengthJumpTableCNT = Math.floor((originalRecord.length - 1) / 256);
reverseRecord = reverseRecord.slice(nullTableLength);
let relativeRecordMetadata;
try {
relativeRecordMetadata = parseRelativeObjectMetadataStruct(reverseRecord, variableLengthJumpTableCNT, this.version);
relativeRecordMetadata.relativeMetadataEnd += nullTableLength;
}
catch (_a) {
throw new Error('Failed parsing record');
}
if (relativeRecordMetadata && relativeRecordMetadata.variableLengthFieldCount !== this.tableHeader.variableColumns) {
const tmpBuffer = Buffer.allocUnsafe(2);
tmpBuffer.writeUInt16LE(this.tableHeader.variableColumns);
const metadataStart = reverseRecord.indexOf(tmpBuffer);
if (metadataStart !== 1 && metadataStart < 10) {
reverseRecord = reverseRecord.slice(metadataStart);
try {
relativeRecordMetadata = parseRelativeObjectMetadataStruct(reverseRecord, variableLengthJumpTableCNT, this.version);
}
catch (_b) {
throw new Error(`Failed to parse record metadata: ${originalRecord}`);
}
relativeRecordMetadata.relativeMetadataEnd += metadataStart;
}
else {
debug(`Record did not parse correctly. Number of columns: ${this.tableHeader.variableColumns}. Number of parsed columns: ${relativeRecordMetadata.variableLengthFieldCount}`);
return;
}
}
return relativeRecordMetadata;
}
parseMemo(relativeObjData) {
debug(`Parsing memo field ${relativeObjData}`);
const parsedMemo = MEMO.parse(relativeObjData);
let memoData;
let memoType;
if (parsedMemo.memoLength & 0x80000000) {
debug('Memo data inline');
memoData = relativeObjData.slice(parsedMemo.memoEnd);
memoType = DataType.Text;
}
else if (parsedMemo.memoLength & 0x40000000) {
debug('LVAL type 1');
const tmp = this.getOverflowRecord(parsedMemo.recordPointer);
if (tmp === undefined) {
throw new Error('LVAL type 1 memoData is undefined');
}
memoData = tmp;
memoType = DataType.Text;
}
else {
debug('LVAL type 2');
const dataBlocks = [];
let { recordPointer } = parsedMemo;
while (recordPointer) {
const record = this.getOverflowRecord(recordPointer);
if (record === undefined) {
throw new Error('LVAL type 2 memoData is undefined');
}
dataBlocks.push(record.subarray(4));
recordPointer = record.readInt32LE();
}
memoData = Buffer.concat(dataBlocks);
memoType = DataType.Text;
}
return parseType(memoType, memoData, memoData.length, this.version, this.textEncoding, this.sanitizeTextBuffer);
}
parseDynamicLengthData(originalRecord, relativeRecordMetadata, relativeRecordsColumnMap) {
const relativeOffsets = relativeRecordMetadata.variableLengthFieldOffsets;
let jumpTableAddition = 0;
let i = -1;
for (const columnIndex of Object.keys(relativeRecordsColumnMap)) {
i += 1;
const column = relativeRecordsColumnMap[columnIndex];
const colName = column.colNameStr;
if (this.version === 3) {
if (relativeRecordMetadata.variableLengthJumpTable.includes(i))
jumpTableAddition = (jumpTableAddition + 0x100) >>> 0;
}
let relStart = relativeOffsets[i];
let relEnd;
if (i + 1 === relativeOffsets.length)
relEnd = relativeRecordMetadata.varLenCount;
else
relEnd = relativeOffsets[i + 1];
if (this.version > 3) {
if (relEnd > originalRecord.length)
relEnd = (relEnd & 0xff) >>> 0;
if (relStart > originalRecord.length)
relStart = (relStart & 0xff) >>> 0;
}
if (relStart === relEnd) {
if (this.parsedTable[colName] === undefined)
this.parsedTable[colName] = [];
this.parsedTable[colName].push('');
continue;
}
const relativeObjData = originalRecord.slice(relStart + jumpTableAddition, relEnd + jumpTableAddition);
let parsedType;
if (column.type === DataType.Memo) {
try {
parsedType = this.parseMemo(relativeObjData);
}
catch (_a) {
debug(`Failed to parse memo field. Using data as bytes`);
parsedType = relativeObjData.toString();
}
}
else {
parsedType = parseType(column.type, relativeObjData, relativeObjData.length, this.version, this.textEncoding, this.sanitizeTextBuffer);
}
if (this.parsedTable[colName] === undefined)
this.parsedTable[colName] = [];
this.parsedTable[colName].push(parsedType);
}
}
parseRow(record) {
const originalRecord = Buffer.allocUnsafe(record.length);
record.copy(originalRecord);
let reverseRecord = Buffer.allocUnsafe(record.length);
record.copy(reverseRecord);
reverseRecord = reverseRecord.reverse();
const nullTableLen = Math.floor((this.tableHeader.columnCount + 7) / 8);
const nullTable = [];
if (nullTableLen && nullTableLen < originalRecord.length) {
const nullTableBuffer = record.slice(nullTableLen === 0 ? 0 : record.length - nullTableLen);
debug(record.slice(record.length - nullTableLen));
for (let i = 0; i < nullTableBuffer.length; i++) {
const byte = nullTableBuffer[i];
for (let j = 0; j < 8; j++) {
if ((byte & (1 << j)) === 0)
nullTable.push(false);
else
nullTable.push(true);
}
}
}
else {
debug(`Failed to parse null table column count ${this.tableHeader.columnCount}`);
return;
}
if (this.version > 3)
record = record.slice(2);
else
record = record.slice(1);
const relativeRecordsColumnMap = {};
for (const i of Object.keys(this.columns)) {
const column = this.columns[i];
if (!column.columnFlags.fixedLength) {
relativeRecordsColumnMap[i] = column;
continue;
}
this.parseFixedLengthData(record, column, nullTable);
}
if (relativeRecordsColumnMap) {
const metadata = this.parseDynamicLengthRecordsMetadata(reverseRecord, originalRecord, nullTableLen);
if (metadata === undefined)
return;
this.parseDynamicLengthData(originalRecord, metadata, relativeRecordsColumnMap);
}
}
parse() {
if (!this.table.linkedPages)
return this.createEmptyTable();
for (const dataChunk of this.table.linkedPages) {
const originalData = dataChunk;
const parsedData = parseDataPageHeader(originalData, this.version);
let lastOffset;
for (const recOffset of parsedData.recordOffsets) {
if ((recOffset & 0x8000) >>> 0) {
lastOffset = (recOffset & 0xfff) >>> 0;
continue;
}
if ((recOffset & 0x4000) >>> 0) {
const recPtrOffset = (recOffset & 0xfff) >>> 0;
lastOffset = recPtrOffset;
const overflowRecPtrBuffer = originalData.slice(recPtrOffset, recPtrOffset + 4);
const overflowRecPtr = overflowRecPtrBuffer.readUInt32LE(0);
const record = this.getOverflowRecord(overflowRecPtr);
if (record !== undefined)
this.parseRow(record);
continue;
}
let record;
if (!lastOffset)
record = originalData.slice(recOffset);
else
record = originalData.slice(recOffset, lastOffset);
lastOffset = recOffset;
if (record)
this.parseRow(record);
}
}
return this.parsedTable;
}
}
class TableObject {
constructor(_offset, value) {
this.linkedPages = [];
this.value = value;
this.linkedPages = [];
}
}
var ALL_VERSIONS;
(function (ALL_VERSIONS) {
ALL_VERSIONS[ALL_VERSIONS["VERSION_3"] = 3] = "VERSION_3";
ALL_VERSIONS[ALL_VERSIONS["VERSION_4"] = 4] = "VERSION_4";
ALL_VERSIONS[ALL_VERSIONS["VERSION_5"] = 5] = "VERSION_5";
ALL_VERSIONS[ALL_VERSIONS["VERSION_2010"] = 2010] = "VERSION_2010";
})(ALL_VERSIONS || (ALL_VERSIONS = {}));
const PAGE_SIZE_V3 = 0x800;
const PAGE_SIZE_V4 = 0x1000;
// Versions
const VERSION_3 = 0x00;
const VERSION_4 = 0x01;
const VERSION_5 = 0x02;
const VERSION_2010 = 0x03;
const NEW_VERSIONS = [VERSION_4, VERSION_5, VERSION_2010];
const SYSTEM_TABLE_FLAGS = [-0x80000000, -0x00000002, 0x80000000, 0x00000002];
class AccessParser {
constructor(dbData, textEncoding = 'utf8', sanitizeTextBuffer) {
this.dbData = dbData;
this.textEncoding = textEncoding;
this.sanitizeTextBuffer = sanitizeTextBuffer;
this.version = ALL_VERSIONS.VERSION_3;
this.pageSize = PAGE_SIZE_V3;
this.parseFileHeader();
[this.tableDefs, this.dataPages /* this.allPages */] = categorizePages(this.dbData, this.pageSize);
this.tablesWithData = this.linkTablesToData();
this.catalog = this.parseCatalog();
}
parseFileHeader() {
let head;
try {
head = ACCESSHEADER.parse(this.dbData);
}
catch (_a) {
throw new Error('Failed to parse DB file header. Check it is a valid file header');
}
const version = head.jetVersion;
if (NEW_VERSIONS.includes(version)) {
if (version === VERSION_4)
this.version = ALL_VERSIONS.VERSION_4;
else if (version === VERSION_5)
this.version = ALL_VERSIONS.VERSION_5;
else if (version === VERSION_2010)
this.version = ALL_VERSIONS.VERSION_2010;
this.pageSize = PAGE_SIZE_V4;
}
else if (version !== VERSION_3) {
throw new Error(`Unknown database version ${version} Trying to parse database as version 3`);
}
}
linkTablesToData() {
const tablesWithData = {};
for (const i of Object.keys(this.dataPages)) {
const data = this.dataPages[i];
let parsedDP;
try {
parsedDP = parseDataPageHeader(data, this.version);
}
catch (_a) {
console.error(`Failed to parse data page ${data}`);
continue;
}
const pageOffset = parsedDP.owner * this.pageSize;
if (Object.keys(this.tableDefs)
.map(str => parseInt(str, 10))
.includes(pageOffset)) {
const tablePageValue = this.tableDefs[pageOffset];
if (!Object.keys(tablesWithData).includes(pageOffset.toString()))
tablesWithData[pageOffset] = new TableObject(pageOffset, tablePageValue);
tablesWithData[pageOffset].linkedPages.push(data);
}
}
return tablesWithData;
}
parseCatalog() {
const catalogPage = this.tablesWithData[2 * this.pageSize];
const accessTable = new AccessTable(catalogPage, this.version, this.pageSize, this.dataPages, this.tableDefs, this.textEncoding, this.sanitizeTextBuffer);
const catalog = accessTable.parse();
const tablesMapping = {};
let i = -1;
const names = catalog.Name;
const types = catalog.Type;
const flags = catalog.Flags;
const ids = catalog.Id;
if (names === undefined || types === undefined || flags === undefined || ids === undefined)
throw new Error('The catalog is missing required fields');
for (const tableName of names) {
if (typeof tableName !== 'string')
continue;
i += 1;
const tableType = 1;
if (types[i] === tableType) {
if (!SYSTEM_TABLE_FLAGS.includes(flags[i]) && flags[i] === 0) {
// TODO: CHECK IF 0 IS THE RIGHT FLAG TO SET
// console.log(tableName);
// console.log(flags[i]);
tablesMapping[tableName] = ids[i];
}
}
}
return tablesMapping;
}
parseTableUnformatted(tableName) {
let tableOffset = this.catalog[tableName];
if (tableOffset === undefined)
throw new Error(`Could not find table ${tableName} in Database`);
tableOffset *= this.pageSize;
const table = this.tablesWithData[tableOffset];
if (table === undefined) {
const tableDef = this.tableDefs[tableOffset];
if (tableDef === undefined) {
throw new Error(`Could not find table ${tableName} offset ${tableOffset}`);
}
else {
throw new Error('Empty table');
// table = new TableObject(tableOffset, tableDef);
}
}
const accessTable = new AccessTable(table, this.version, this.pageSize, this.dataPages, this.tableDefs, this.textEncoding, this.sanitizeTextBuffer);
return accessTable.parse();
}
parseTable(name) {
const table = this.parseTableUnformatted(name);
const fields = Object.keys(table);
if (fields.length === 0) {
return [];
}
const linesNumber = table[fields[0]].length;
const lines = [];
for (let i = 0; i < linesNumber; ++i) {
const line = {};
for (const field of fields) {
line[field] = table[field][i];
}
lines.push(line);
}
return lines;
}
getTables() {
return Object.keys(this.catalog);
}
getVersion() {
return this.version;
}
}
export { AccessParser };
//# sourceMappingURL=index.esm.js.map