@regrapes/access-db-parser
Version:
A pure javascript Microsoft AccessDB files (.mdb, .accdb) parser
247 lines (231 loc) • 7.14 kB
text/typescript
import { TextDecoder } from 'util'
import { Parser } from 'binary-parser'
import type { Version } from './types'
const originalArray = Parser.prototype.array
Parser.prototype.array = function (this: any, varName: any, options: any) {
if (options.length === 0) {
return this.setNextParser('array', varName, options)
}
return originalArray.call(this, varName, options)
}
export const ACCESSHEADER = new Parser()
.seek(4)
.string('jetString', {
zeroTerminated: true,
})
.uint32le('jetVersion')
.seek(126)
export 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')
export const TDEF_HEADER = new Parser()
.seek(2)
.uint16le('peekVersion')
.seek(-2)
.uint16le('tdefVer')
.uint32le('nextPagePtr')
.saveOffset('headerEnd')
export const parseTableHead = (buffer: Buffer, version: 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)
export const parseTableData = (buffer: Buffer, realIndexCount: number, columnCount: number, version: 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}`) as any,
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: typeof COLUMN_NAMES_V3 = version === 3 ? COLUMN_NAMES_V3 : (COLUMN_NAMES_V4 as any)
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 as unknown as Buffer
columnName.colNameStr = new TextDecoder('utf-16le').decode(buffer)
}
}
return res
}
export const parseDataPageHeader = (buffer: Buffer, version: 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)
export const parseRelativeObjectMetadataStruct = (buffer: Buffer, variableJumpTablesCNT = 0, version: 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 = { ...part1, ...part2 }
return result
}
}
export type PropType<TObj, TProp extends keyof TObj> = TObj[TProp]
export type Column = PropType<ReturnType<typeof parseTableData>, 'column'>[0] & {
colNameStr: string
}
export type TableHeader = ReturnType<typeof parseTableHead>