@regrapes/access-db-parser
Version:
A pure javascript Microsoft AccessDB files (.mdb, .accdb) parser
112 lines (103 loc) • 3.48 kB
text/typescript
import { TextDecoder } from 'util'
import UUID from 'uuid'
import type { Version, Dict } from './types'
export enum DataType {
Boolean = 1,
Int8 = 2,
Int16 = 3,
Int32 = 4,
Money = 5,
Float32 = 6,
Float64 = 7,
DateTime = 8,
Binary = 9,
Text = 10,
OLE = 11,
Memo = 12,
GUID = 15,
Bit96Bytes17 = 16,
Complex = 18,
}
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])]
export const parseType = (
dataType: DataType,
buffer: Buffer,
length?: number,
version: Version = 3,
textEncoding: BufferEncoding = 'utf8',
sanitizeTextBuffer = (buffer: 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) * 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
}
}
}
export const categorizePages = (dbData: Buffer, pageSize: number): [Dict<Buffer>, Dict<Buffer>, Dict<Buffer>] => {
if (dbData.length % pageSize)
throw new Error(`DB is not full or pageSize is wrong. pageSize: ${pageSize} dbData.length: ${dbData.length}`)
const pages: Dict<Buffer> = {}
for (let i = 0; i < dbData.length; i += pageSize) pages[i] = dbData.slice(i, i + pageSize)
const dataPages: Dict<Buffer> = {}
const tableDefs: Dict<Buffer> = {}
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]
}