Skip to content

Commit 825540a

Browse files
authored
feat: support for attachment columns (#392)
* feat: initial implementation * refactor: clean up * refactor: clean up * refactor: clean up * test: add tests * test: add tests * refactor: clean up * fix: correctly read OLE columns * test: update database * refactor: extract `maskTableId` * chore: remove comment * chore: remove unused files * refactor: clean up * refactor: more cleanup and tweaks * fix: correctly read multi-page OLE columns * docs: improve readme * chore: tweak docs * chore: add newline at the end
1 parent 1727ed1 commit 825540a

24 files changed

Lines changed: 431 additions & 46 deletions

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ The data types returned by `Table.getData()` depends on the column type. Null va
226226
| binary | `Buffer` |
227227
| boolean | `boolean` |
228228
| byte | `number` |
229-
| complex | `number` |
229+
| complex | `Attachment[]` |
230230
| currency | `string` |
231231
| datetime | `Date` |
232232
| datetimeextended | `string` |
@@ -240,6 +240,21 @@ The data types returned by `Table.getData()` depends on the column type. Null va
240240
| repid | `string` |
241241
| text | `string` |
242242

243+
### Attachment (complex columns)
244+
245+
Columns of type `complex` (attachment fields) return an array of `Attachment` objects. The type is exported from the library. Each item has:
246+
247+
| Property | Type | Description |
248+
| ----------- | -------- | ------------------------------------------ |
249+
| `name` | `string` | Original file name. |
250+
| `type` | `string` | File extension / MIME type. |
251+
| `data` | `Buffer` | Decoded file bytes. |
252+
| `url` | `string` | Optional URL if the attachment was linked. |
253+
| `timestamp` | `Date` | Optional modification timestamp. |
254+
| `flags` | `number` | Optional flags. |
255+
256+
Only `name`, `type`, and `data` are always present; the rest are set when stored in the database.
257+
243258
## Development
244259

245260
### Build

package-lock.json

Lines changed: 27 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"dependencies": {
4242
"browserify-aes": "^1.0.0",
4343
"create-hash": "^1.0.0",
44-
"fast-xml-parser": "^4.0.0 || ^5.0.0"
44+
"fast-xml-parser": "^4.0.0 || ^5.0.0",
45+
"pako": "^2.0.0"
4546
},
4647
"devDependencies": {
4748
"@semantic-release/changelog": "6.0.3",
@@ -52,6 +53,7 @@
5253
"@types/mocha": "10.0.10",
5354
"@types/mocha-each": "2.0.4",
5455
"@types/node": "18.19.78",
56+
"@types/pako": "2.0.4",
5557
"@typescript-eslint/eslint-plugin": "7.18.0",
5658
"@typescript-eslint/parser": "7.18.0",
5759
"chai": "6.0.1",

src/JetFormat/Jet4Format.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export const jet4Format: JetFormat = {
5252
sizeOffset: 23,
5353

5454
entrySize: 25,
55+
56+
complexTypeIdOffset: 9,
5557
},
5658
columnNames: {
5759
nameLengthSize: 2,

src/JetFormat/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ export interface JetFormat {
5454
fixedIndexOffset: number;
5555

5656
entrySize: number;
57+
58+
/**
59+
* Offset of the 4-byte complex type ID in the column definition. Omit if not supported.
60+
*/
61+
complexTypeIdOffset?: number;
5762
};
5863
columnNames: {
5964
/**

src/MDBReader.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import { PageType, assertPageType } from "./PageType.js";
33
import { type SysObject, isSysObjectType, isSystemObject, SysObjectTypes } from "./SysObject.js";
44
import { Table } from "./Table.js";
55
import type { SortOrder } from "./types.js";
6-
7-
const MSYS_OBJECTS_TABLE = "MSysObjects";
8-
const MSYS_OBJECTS_PAGE = 2;
6+
import { getMSysObjectsTable } from "./systemTables.js";
7+
import { maskTableId } from "./util.js";
98

109
export interface Options {
1110
password?: string | undefined;
@@ -26,7 +25,7 @@ export default class MDBReader {
2625

2726
this.#database = new Database(this.#buffer, password ?? "");
2827

29-
const mSysObjectsTable = new Table(MSYS_OBJECTS_TABLE, this.#database, MSYS_OBJECTS_PAGE).getData<{
28+
const mSysObjectsTable = getMSysObjectsTable(this.#database).getData<{
3029
Id: number;
3130
Name: string;
3231
Type: number;
@@ -40,7 +39,7 @@ export default class MDBReader {
4039
return {
4140
objectName: mSysObject.Name,
4241
objectType: isSysObjectType(objectType) ? objectType : null,
43-
tablePage: mSysObject.Id & 0x00ffffff,
42+
tablePage: maskTableId(mSysObject.Id),
4443
flags: mSysObject.Flags,
4544
};
4645
});

src/Table.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,20 +67,20 @@ export class Table {
6767

6868
this.#columnCount = this.#definitionBuffer.readUInt16LE(this.#database.format.tableDefinitionPage.columnCountOffset);
6969
this.#variableColumnCount = this.#definitionBuffer.readUInt16LE(
70-
this.#database.format.tableDefinitionPage.variableColumnCountOffset
70+
this.#database.format.tableDefinitionPage.variableColumnCountOffset,
7171
);
7272
// this.#fixedColumnCount = this.#columnCount - this.#variableColumnCount;
7373

7474
// this.#logicalIndexCount = this.#definitionBuffer.readInt32LE(
7575
// this.#database.format.tableDefinitionPage.logicalIndexCountOffset
7676
// );
7777
this.#realIndexCount = this.#definitionBuffer.readInt32LE(
78-
this.#database.format.tableDefinitionPage.realIndexCountOffset
78+
this.#database.format.tableDefinitionPage.realIndexCountOffset,
7979
);
8080

8181
// Usage Map
8282
const usageMapBuffer = this.#database.findPageRow(
83-
this.#definitionBuffer.readUInt32LE(this.#database.format.tableDefinitionPage.usageMapOffset)
83+
this.#definitionBuffer.readUInt32LE(this.#database.format.tableDefinitionPage.usageMapOffset),
8484
);
8585
this.#dataPages = findMapPages(usageMapBuffer, this.#database);
8686
}
@@ -137,23 +137,23 @@ export class Table {
137137
for (let i = 0; i < this.#columnCount; ++i) {
138138
const columnBuffer = this.#definitionBuffer.slice(
139139
curDefinitionPos,
140-
curDefinitionPos + this.#database.format.tableDefinitionPage.columnsDefinition.entrySize
140+
curDefinitionPos + this.#database.format.tableDefinitionPage.columnsDefinition.entrySize,
141141
);
142142

143143
const type = getColumnType(
144144
this.#definitionBuffer.readUInt8(
145-
curDefinitionPos + this.#database.format.tableDefinitionPage.columnsDefinition.typeOffset
146-
)
145+
curDefinitionPos + this.#database.format.tableDefinitionPage.columnsDefinition.typeOffset,
146+
),
147147
);
148148

149149
const nameLength = this.#definitionBuffer.readUIntLE(
150150
namesCursorPos,
151-
this.#database.format.tableDefinitionPage.columnNames.nameLengthSize
151+
this.#database.format.tableDefinitionPage.columnNames.nameLengthSize,
152152
);
153153
namesCursorPos += this.#database.format.tableDefinitionPage.columnNames.nameLengthSize;
154154
const name = uncompressText(
155155
this.#definitionBuffer.slice(namesCursorPos, namesCursorPos + nameLength),
156-
this.#database.format
156+
this.#database.format,
157157
);
158158
namesCursorPos += nameLength;
159159

@@ -162,17 +162,17 @@ export class Table {
162162
type,
163163
index: columnBuffer.readUInt8(this.#database.format.tableDefinitionPage.columnsDefinition.indexOffset),
164164
variableIndex: columnBuffer.readUInt8(
165-
this.#database.format.tableDefinitionPage.columnsDefinition.variableIndexOffset
165+
this.#database.format.tableDefinitionPage.columnsDefinition.variableIndexOffset,
166166
),
167167
size:
168168
type === ColumnTypes.Boolean
169169
? 0
170170
: columnBuffer.readUInt16LE(this.#database.format.tableDefinitionPage.columnsDefinition.sizeOffset),
171171
fixedIndex: columnBuffer.readUInt16LE(
172-
this.#database.format.tableDefinitionPage.columnsDefinition.fixedIndexOffset
172+
this.#database.format.tableDefinitionPage.columnsDefinition.fixedIndexOffset,
173173
),
174174
...parseColumnFlags(
175-
columnBuffer.readUInt8(this.#database.format.tableDefinitionPage.columnsDefinition.flagsOffset)
175+
columnBuffer.readUInt8(this.#database.format.tableDefinitionPage.columnsDefinition.flagsOffset),
176176
),
177177
};
178178

@@ -181,6 +181,18 @@ export class Table {
181181
column.scale = columnBuffer.readUInt8(12);
182182
}
183183

184+
if (type === ColumnTypes.Complex) {
185+
const complexTypeIdOffset = this.#database.format.tableDefinitionPage.columnsDefinition.complexTypeIdOffset;
186+
if (complexTypeIdOffset !== undefined) {
187+
column.complex = {
188+
typeId: columnBuffer.readInt32LE(complexTypeIdOffset),
189+
tableDefinitionPage: this.#firstDefinitionPage,
190+
};
191+
} else {
192+
throw new Error("Complex columns are not supported");
193+
}
194+
}
195+
184196
columns.push(column);
185197

186198
curDefinitionPos += this.#database.format.tableDefinitionPage.columnsDefinition.entrySize;
@@ -210,7 +222,7 @@ export class Table {
210222
rowOffset?: number | undefined;
211223
rowLimit?: number | undefined;
212224
}
213-
| undefined = {}
225+
| undefined = {},
214226
): TRow[] {
215227
const columnDefinitions = this.#getColumnDefinitions();
216228

@@ -287,7 +299,7 @@ export class Table {
287299
#getDataFromPage(
288300
pageBuffer: Buffer,
289301
recordOffsets: RecordOffset[],
290-
columns: ReadonlyArray<ColumnDefinition>
302+
columns: ReadonlyArray<ColumnDefinition>,
291303
): { [column: string]: Value }[] {
292304
const lastColumnIndex = Math.max(...columns.map((c) => c.index), 0);
293305
const data: { [column: string]: Value }[] = [];
@@ -341,7 +353,7 @@ export class Table {
341353

342354
const nullMask = pageBuffer.slice(
343355
recordEnd - bitmaskSize + 1,
344-
recordEnd - bitmaskSize + 1 + roundToFullByte(lastColumnIndex + 1)
356+
recordEnd - bitmaskSize + 1 + roundToFullByte(lastColumnIndex + 1),
345357
);
346358
let fixedColumnsFound = 0;
347359

src/column.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,20 @@ export interface ColumnDefinition extends Column {
3535
index: number;
3636
variableIndex: number;
3737
fixedIndex: number;
38+
39+
complex?: {
40+
typeId: number;
41+
tableDefinitionPage: number;
42+
};
3843
}
3944

45+
export type ComplexColumnDefinition = ColumnDefinition & {
46+
complex: {
47+
typeId: number;
48+
tableDefinitionPage: number;
49+
};
50+
};
51+
4052
const columnTypeMap: Record<number, ColumnType> = {
4153
0x01: ColumnTypes.Boolean,
4254
0x02: ColumnTypes.Byte,

src/data/complex.spec.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/data/complex.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)