accessdb-parser
Version:
A pure javascript Microsoft AccessDB files (.mdb, .accdb) parser
472 lines • 21.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AccessParser = void 0;
var utils_1 = require("./utils");
var parsing_primitives_1 = require("./parsing-primitives");
var PAGE_SIZE_V3 = 0x800;
var PAGE_SIZE_V4 = 0x1000;
// Versions
var VERSION_3 = 0x00;
var VERSION_4 = 0x01;
var VERSION_5 = 0x02;
var VERSION_2010 = 0x03;
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 = {}));
var NEW_VERSIONS = [VERSION_4, VERSION_5, VERSION_2010];
var SYSTEM_TABLE_FLAGS = [-0x80000000, -0x00000002, 0x80000000, 0x00000002];
var TableObject = /** @class */ (function () {
function TableObject(_offset, value) {
// private offset: number;
this.linkedPages = [];
this.value = value;
// this.offset = offset;
this.linkedPages = [];
}
return TableObject;
}());
var AccessParser = /** @class */ (function () {
function AccessParser(dbData) {
var _a;
this.version = ALL_VERSIONS.VERSION_3;
this.pageSize = PAGE_SIZE_V3;
this.dbData = dbData;
this.parseFileHeader();
_a = utils_1.categorizePages(this.dbData, this.pageSize), this.tableDefs = _a[0], this.dataPages = _a[1];
this.tablesWithData = this.linkTablesToData();
this.catalog = this.parseCatalog();
}
AccessParser.prototype.parseFileHeader = function () {
var head;
try {
head = parsing_primitives_1.ACCESSHEADER.parse(this.dbData);
}
catch (_a) {
throw new Error("Failed to parse DB file header. Check it is a valid file header");
}
var 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");
}
};
AccessParser.prototype.linkTablesToData = function () {
var tablesWithData = {};
for (var _i = 0, _a = Object.keys(this.dataPages); _i < _a.length; _i++) {
var i = _a[_i];
var data = this.dataPages[i];
var parsedDP = void 0;
try {
parsedDP = parsing_primitives_1.parseDataPageHeader(data, this.version);
}
catch (_b) {
console.error("Failed to parse data page " + data);
continue;
}
var pageOffset = parsedDP.owner * this.pageSize;
if (Object.keys(this.tableDefs).map(function (str) { return parseInt(str); }).includes(pageOffset)) {
var tablePageValue = this.tableDefs[pageOffset];
if (!Object.keys(tablesWithData).includes(pageOffset.toString()))
tablesWithData[pageOffset] = new TableObject(pageOffset, tablePageValue);
tablesWithData[pageOffset].linkedPages.push(data);
}
}
return tablesWithData;
};
AccessParser.prototype.parseCatalog = function () {
var catalogPage = this.tablesWithData[2 * this.pageSize];
var accessTable = new AccessTable(catalogPage, this.version, this.pageSize, this.dataPages, this.tableDefs);
var catalog = accessTable.parse();
var tablesMapping = {};
var i = -1;
var names = catalog["Name"];
var types = catalog["Type"];
var flags = catalog["Flags"];
var ids = catalog["Id"];
if (names === undefined || types === undefined || flags === undefined || ids === undefined)
throw new Error("The catalog is missing required fields");
for (var _i = 0, names_1 = names; _i < names_1.length; _i++) {
var tableName = names_1[_i];
if (typeof tableName !== "string")
continue;
i += 1;
var 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;
};
AccessParser.prototype.parseTableUnformatted = function (tableName) {
var tableOffset = this.catalog[tableName];
if (tableOffset === undefined)
throw new Error("Could not find table " + tableName + " in Database");
tableOffset *= this.pageSize;
var table = this.tablesWithData[tableOffset];
if (table === undefined) {
var 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);
}
}
var accessTable = new AccessTable(table, this.version, this.pageSize, this.dataPages, this.tableDefs);
return accessTable.parse();
};
AccessParser.prototype.parseTable = function (name) {
var table = this.parseTableUnformatted(name);
var fields = Object.keys(table);
if (fields.length === 0) {
return { fields: [], lines: [] };
}
var linesNumber = table[fields[0]].length;
var lines = [];
for (var i = 0; i < linesNumber; ++i) {
var line = [];
for (var _i = 0, fields_1 = fields; _i < fields_1.length; _i++) {
var field = fields_1[_i];
line.push(table[field][i].toString());
}
lines.push(line);
}
return { fields: fields, lines: lines };
};
AccessParser.prototype.getTables = function () {
return Object.keys(this.catalog);
};
AccessParser.prototype.getVersion = function () {
return this.version;
};
return AccessParser;
}());
exports.AccessParser = AccessParser;
var AccessTable = /** @class */ (function () {
function AccessTable(table, version, pageSize, dataPages, tableDefs) {
var _a;
this.version = version;
this.pageSize = pageSize;
this.dataPages = dataPages;
this.tableDefs = tableDefs;
this.table = table;
this.parsedTable = {};
_a = this.getTableColumns(), this.columns = _a[0], this.tableHeader = _a[1];
}
AccessTable.prototype.getTableColumns = function () {
var tableHeader;
var colNames;
var columns;
try {
tableHeader = parsing_primitives_1.parseTableHead(this.table.value, this.version);
var mergedData = this.table.value.slice(tableHeader.tDefHeaderEnd);
if (tableHeader.TDEF_header.nextPagePtr) {
mergedData = Buffer.concat([
mergedData,
this.mergeTableData(tableHeader.TDEF_header.nextPagePtr),
]);
}
var parsedData = parsing_primitives_1.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(function (c, i) {
c.colNameStr = colNames[i].colNameStr;
});
var offset = Math.min.apply(Math, columns.map(function (c) { return c.columnIndex; }));
var columnDict = {};
for (var _i = 0, columns_1 = columns; _i < columns_1.length; _i++) {
var x = columns_1[_i];
columnDict[x.columnIndex - offset] = x;
}
if (Object.keys(columnDict).length !== columns.length) {
for (var _a = 0, columns_2 = columns; _a < columns_2.length; _a++) {
var x = columns_2[_a];
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];
};
AccessTable.prototype.mergeTableData = function (firstPage) {
var table = this.tableDefs[firstPage * this.pageSize];
var parsedHeader = parsing_primitives_1.TDEF_HEADER.parse(table);
var data = table.slice(parsedHeader.headerEnd);
while (parsedHeader.nextPagePtr) {
table = this.tableDefs[parsedHeader.nextPagePtr * this.pageSize];
parsedHeader = parsing_primitives_1.TDEF_HEADER.parse(table);
data = Buffer.concat([data, table.slice(parsedHeader.headerEnd)]);
}
return data;
};
AccessTable.prototype.createEmptyTable = function () {
var parsedTable = {};
var columns = this.getTableColumns()[0];
for (var _i = 0, _a = Object.keys(columns); _i < _a.length; _i++) {
var i = _a[_i];
var column = columns[i];
parsedTable[column.colNameStr] = [];
}
return parsedTable;
};
AccessTable.prototype.getOverflowRecord = function (recordPointer) {
var recordOffset = (recordPointer & 0xFF) >>> 0;
var pageNum = recordPointer >>> 8;
var recordPage = this.dataPages[pageNum * this.pageSize];
if (!recordPage)
return;
var parsedData = parsing_primitives_1.parseDataPageHeader(recordPage, this.version);
if (recordOffset > parsedData.recordOffsets.length)
return;
var start = parsedData.recordOffsets[recordOffset];
if ((start & 0x8000) >>> 0)
start = (start & 0xFFF) >>> 0;
else
console.log("Overflow record flag is not present " + start);
var record;
if (recordOffset === 0) {
record = recordPage.slice(start);
}
else {
var end = parsedData.recordOffsets[recordOffset - 1];
if ((end & 0x8000) >>> 0)
end = (end & 0xFFF) >>> 0;
record = recordPage.slice(start, end);
}
return record;
};
AccessTable.prototype.parseFixedLengthData = function (originalRecord, column, nullTable) {
var columnName = column.colNameStr;
var parsedType;
if (column.type === utils_1.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);
var record = originalRecord.slice(column.fixedOffset);
parsedType = utils_1.parseType(column.type, record, this.version);
}
if (this.parsedTable[columnName] === undefined)
this.parsedTable[columnName] = [];
this.parsedTable[columnName].push(parsedType);
};
AccessTable.prototype.parseDynamicLengthRecordsMetadata = function (reverseRecord, originalRecord, nullTableLength) {
if (this.version > 3) {
reverseRecord = reverseRecord.slice(nullTableLength + 1);
if (reverseRecord.length > 1 && reverseRecord[0] === 0)
reverseRecord = reverseRecord.slice(1);
return parsing_primitives_1.parseRelativeObjectMetadataStruct(reverseRecord, undefined, this.version);
}
var variableLengthJumpTableCNT = Math.floor((originalRecord.length - 1) / 256);
reverseRecord = reverseRecord.slice(nullTableLength);
var relativeRecordMetadata;
try {
relativeRecordMetadata = parsing_primitives_1.parseRelativeObjectMetadataStruct(reverseRecord, variableLengthJumpTableCNT, this.version);
relativeRecordMetadata.relativeMetadataEnd += nullTableLength;
}
catch (_a) {
throw new Error("Failed parsing record");
}
if (relativeRecordMetadata && relativeRecordMetadata.variableLengthFieldCount !== this.tableHeader.variableColumns) {
var tmpBuffer = Buffer.allocUnsafe(2);
tmpBuffer.writeUInt16LE(this.tableHeader.variableColumns);
var metadataStart = reverseRecord.indexOf(tmpBuffer);
if (metadataStart !== 1 && metadataStart < 10) {
reverseRecord = reverseRecord.slice(metadataStart);
try {
relativeRecordMetadata = parsing_primitives_1.parseRelativeObjectMetadataStruct(reverseRecord, variableLengthJumpTableCNT, this.version);
}
catch (_b) {
throw new Error("Failed to parse record metadata: " + originalRecord);
}
relativeRecordMetadata.relativeMetadataEnd += metadataStart;
}
else {
console.log("Record did not parse correctly. Number of columns: " + this.tableHeader.variableColumns + ". Number of parsed columns: " + relativeRecordMetadata.variableLengthFieldCount);
return;
}
}
return relativeRecordMetadata;
};
AccessTable.prototype.parseMemo = function (relativeObjData, column) {
console.log("Parsing memo field " + relativeObjData);
var parsedMemo = parsing_primitives_1.MEMO.parse(relativeObjData);
var memoData;
var memoType;
if (parsedMemo.memoLength & 0x80000000) {
console.log("Memo data inline");
memoData = relativeObjData.slice(parsedMemo.memoEnd);
memoType = utils_1.DataType.Text;
}
else if (parsedMemo.memoLength & 0x40000000) {
console.log("LVAL type 1");
var tmp = this.getOverflowRecord(parsedMemo.recordPointer);
if (tmp === undefined)
throw new Error("LVAL type 1 memoData is undefined");
memoData = tmp;
memoType = utils_1.DataType.Text;
}
else {
console.log("LVAL type 2");
console.log("memo lval type 2 currently not supported");
memoData = relativeObjData;
memoType = column.type;
}
return utils_1.parseType(memoType, memoData, memoData.length, this.version);
};
AccessTable.prototype.parseDynamicLengthData = function (originalRecord, relativeRecordMetadata, relativeRecordsColumnMap) {
var relativeOffsets = relativeRecordMetadata.variableLengthFieldOffsets;
var jumpTableAddition = 0;
var i = -1;
for (var _i = 0, _a = Object.keys(relativeRecordsColumnMap); _i < _a.length; _i++) {
var columnIndex = _a[_i];
i += 1;
var column = relativeRecordsColumnMap[columnIndex];
var colName = column.colNameStr;
if (this.version === 3) {
if (relativeRecordMetadata.variableLengthJumpTable.includes(i))
jumpTableAddition = (jumpTableAddition + 0x100) >>> 0;
}
var relStart = relativeOffsets[i];
var relEnd = void 0;
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;
}
var relativeObjData = originalRecord.slice(relStart + jumpTableAddition, relEnd + jumpTableAddition);
var parsedType = void 0;
if (column.type === utils_1.DataType.Memo) {
try {
parsedType = this.parseMemo(relativeObjData, column);
}
catch (_b) {
console.log("Failed to parse memo field. Using data as bytes");
parsedType = relativeObjData.toString();
}
}
else {
parsedType = utils_1.parseType(column.type, relativeObjData, relativeObjData.length, this.version);
}
if (this.parsedTable[colName] === undefined)
this.parsedTable[colName] = [];
this.parsedTable[colName].push(parsedType);
}
};
AccessTable.prototype.parseRow = function (record) {
var originalRecord = Buffer.allocUnsafe(record.length);
record.copy(originalRecord);
var reverseRecord = Buffer.allocUnsafe(record.length);
record.copy(reverseRecord);
reverseRecord = reverseRecord.reverse();
var nullTableLen = Math.floor((this.tableHeader.columnCount + 7) / 8);
var nullTable = [];
if (nullTableLen && nullTableLen < originalRecord.length) {
var nullTableBuffer = record.slice(nullTableLen === 0 ? 0 : record.length - nullTableLen);
for (var i = 0; i < nullTable.length * 8; ++i)
nullTable.push(((nullTableBuffer[Math.floor(i / 8)]) & (1 << (i % 8) >>> 0) >>> 0) !== 0); // CHECK MOD
}
else {
throw new Error("Failed to parse null table column count " + this.tableHeader.columnCount);
}
if (this.version > 3)
record = record.slice(2);
else
record = record.slice(1);
var relativeRecordsColumnMap = {};
for (var _i = 0, _a = Object.keys(this.columns); _i < _a.length; _i++) {
var i = _a[_i];
var column = this.columns[i];
if (!column.columnFlags.fixedLength) {
relativeRecordsColumnMap[i] = column;
continue;
}
this.parseFixedLengthData(record, column, nullTable);
}
if (relativeRecordsColumnMap) {
var metadata = this.parseDynamicLengthRecordsMetadata(reverseRecord, originalRecord, nullTableLen);
if (metadata === undefined)
return;
this.parseDynamicLengthData(originalRecord, metadata, relativeRecordsColumnMap);
}
};
AccessTable.prototype.parse = function () {
if (!this.table.linkedPages)
return this.createEmptyTable();
for (var _i = 0, _a = this.table.linkedPages; _i < _a.length; _i++) {
var dataChunk = _a[_i];
var originalData = dataChunk;
var parsedData = parsing_primitives_1.parseDataPageHeader(originalData, this.version);
var lastOffset = undefined;
for (var _b = 0, _c = parsedData.recordOffsets; _b < _c.length; _b++) {
var recOffset = _c[_b];
if ((recOffset & 0x8000) >>> 0) {
lastOffset = (recOffset & 0xFFF) >>> 0;
continue;
}
if ((recOffset & 0x4000) >>> 0) {
var recPtrOffset = (recOffset & 0xFFF) >>> 0;
lastOffset = recPtrOffset;
var overflowRecPtrBuffer = originalData.slice(recPtrOffset, recPtrOffset + 4);
var overflowRecPtr = overflowRecPtrBuffer.readUInt32LE(0);
var record_1 = this.getOverflowRecord(overflowRecPtr);
if (record_1 !== undefined)
this.parseRow(record_1);
continue;
}
var record = void 0;
if (!lastOffset)
record = originalData.slice(recOffset);
else
record = originalData.slice(recOffset, lastOffset);
lastOffset = recOffset;
if (record)
this.parseRow(record);
}
}
return this.parsedTable;
};
return AccessTable;
}());
//# sourceMappingURL=index.js.map