altium-toolkit 1.4.13 → 1.4.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@ import { AltiumParser } from '../core/altium/AltiumParser.mjs'
|
|
|
7
7
|
import { CircuitJsonModelAdapter } from '../core/circuit-json/CircuitJsonModelAdapter.mjs'
|
|
8
8
|
import { CircuitJsonSchematicImageProjection } from '../core/circuit-json/CircuitJsonSchematicImageProjection.mjs'
|
|
9
9
|
import { AltiumCircuitJsonProjection } from './AltiumCircuitJsonProjection.mjs'
|
|
10
|
+
import { AltiumOleInputTailNormalizer } from './AltiumOleInputTailNormalizer.mjs'
|
|
10
11
|
import { AltiumSchematicImageNormalizer } from './AltiumSchematicImageNormalizer.mjs'
|
|
11
12
|
import { ParserInput } from './ParserInput.mjs'
|
|
12
13
|
|
|
@@ -20,7 +21,9 @@ export class AltiumDocumentBuilder {
|
|
|
20
21
|
* @returns {{ native: Record<string, any>, model: object[], nativeSidecarCount: number }} Decoded source data.
|
|
21
22
|
*/
|
|
22
23
|
static decode(normalized) {
|
|
23
|
-
const buffer =
|
|
24
|
+
const buffer = AltiumOleInputTailNormalizer.normalize(
|
|
25
|
+
ParserInput.arrayBuffer(normalized.input.data)
|
|
26
|
+
)
|
|
24
27
|
const native = AltiumSchematicImageNormalizer.normalize(
|
|
25
28
|
AltiumParser.parseArrayBufferToRendererModel(
|
|
26
29
|
normalized.input.fileName,
|
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
const HEADER_BYTE_LENGTH = 512
|
|
5
|
+
const DIRECTORY_ENTRY_BYTE_LENGTH = 128
|
|
6
|
+
const END_OF_CHAIN = -2
|
|
7
|
+
const HEADER_SIGNATURE = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]
|
|
8
|
+
const VALID_SECTOR_SHIFTS = new Set([9, 12])
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Restores omitted physical padding only after proving every logical OLE byte
|
|
12
|
+
* and structural sector is present.
|
|
13
|
+
*/
|
|
14
|
+
export class AltiumOleInputTailNormalizer {
|
|
15
|
+
/**
|
|
16
|
+
* Returns an aligned owned buffer when only unused final-sector padding is
|
|
17
|
+
* absent, otherwise preserves the original input for strict native errors.
|
|
18
|
+
* @param {ArrayBuffer} arrayBuffer OLE candidate bytes.
|
|
19
|
+
* @returns {ArrayBuffer} Original or safely padded bytes.
|
|
20
|
+
*/
|
|
21
|
+
static normalize(arrayBuffer) {
|
|
22
|
+
if (!(arrayBuffer instanceof ArrayBuffer)) return arrayBuffer
|
|
23
|
+
const bytes = new Uint8Array(arrayBuffer)
|
|
24
|
+
if (!AltiumOleInputTailNormalizer.#hasOleSignature(bytes)) {
|
|
25
|
+
return arrayBuffer
|
|
26
|
+
}
|
|
27
|
+
if (bytes.byteLength < HEADER_BYTE_LENGTH) return arrayBuffer
|
|
28
|
+
|
|
29
|
+
const sourceView = new DataView(arrayBuffer)
|
|
30
|
+
const sectorShift = sourceView.getUint16(30, true)
|
|
31
|
+
if (!VALID_SECTOR_SHIFTS.has(sectorShift)) return arrayBuffer
|
|
32
|
+
const miniSectorShift = sourceView.getUint16(32, true)
|
|
33
|
+
if (miniSectorShift !== 6) return arrayBuffer
|
|
34
|
+
const sectorByteLength = 2 ** sectorShift
|
|
35
|
+
const miniSectorByteLength = 2 ** miniSectorShift
|
|
36
|
+
const payloadByteLength = bytes.byteLength - HEADER_BYTE_LENGTH
|
|
37
|
+
if (payloadByteLength % sectorByteLength === 0) return arrayBuffer
|
|
38
|
+
|
|
39
|
+
const alignedPayloadByteLength =
|
|
40
|
+
Math.ceil(payloadByteLength / sectorByteLength) * sectorByteLength
|
|
41
|
+
const normalizedBytes = new Uint8Array(
|
|
42
|
+
HEADER_BYTE_LENGTH + alignedPayloadByteLength
|
|
43
|
+
)
|
|
44
|
+
normalizedBytes.set(bytes)
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const isComplete =
|
|
48
|
+
AltiumOleInputTailNormalizer.#isLogicallyComplete(
|
|
49
|
+
new DataView(normalizedBytes.buffer),
|
|
50
|
+
bytes.byteLength,
|
|
51
|
+
sectorByteLength,
|
|
52
|
+
miniSectorByteLength
|
|
53
|
+
)
|
|
54
|
+
return isComplete ? normalizedBytes.buffer : arrayBuffer
|
|
55
|
+
} catch (_error) {
|
|
56
|
+
return arrayBuffer
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Checks the OLE header signature without parsing the document.
|
|
62
|
+
* @param {Uint8Array} bytes Source bytes.
|
|
63
|
+
* @returns {boolean} Whether the signature matches.
|
|
64
|
+
*/
|
|
65
|
+
static #hasOleSignature(bytes) {
|
|
66
|
+
return (
|
|
67
|
+
bytes.byteLength >= HEADER_SIGNATURE.length &&
|
|
68
|
+
HEADER_SIGNATURE.every((value, index) => bytes[index] === value)
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Verifies structural sectors and every regular logical stream byte.
|
|
74
|
+
* @param {DataView} view Zero-padded aligned candidate view.
|
|
75
|
+
* @param {number} sourceByteLength Original physical byte length.
|
|
76
|
+
* @param {number} sectorByteLength OLE sector size.
|
|
77
|
+
* @param {number} miniSectorByteLength OLE mini-sector size.
|
|
78
|
+
* @returns {boolean} Whether padding cannot synthesize declared data.
|
|
79
|
+
*/
|
|
80
|
+
static #isLogicallyComplete(
|
|
81
|
+
view,
|
|
82
|
+
sourceByteLength,
|
|
83
|
+
sectorByteLength,
|
|
84
|
+
miniSectorByteLength
|
|
85
|
+
) {
|
|
86
|
+
const fatSectorIds = AltiumOleInputTailNormalizer.#collectFatSectorIds(
|
|
87
|
+
view,
|
|
88
|
+
sourceByteLength,
|
|
89
|
+
sectorByteLength
|
|
90
|
+
)
|
|
91
|
+
const numberOfFatSectors = view.getUint32(44, true)
|
|
92
|
+
if (!numberOfFatSectors || fatSectorIds.length < numberOfFatSectors) {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
const fatEntries = AltiumOleInputTailNormalizer.#readFatEntries(
|
|
96
|
+
view,
|
|
97
|
+
fatSectorIds.slice(0, numberOfFatSectors),
|
|
98
|
+
sourceByteLength,
|
|
99
|
+
sectorByteLength
|
|
100
|
+
)
|
|
101
|
+
if (fatEntries.length !== (numberOfFatSectors * sectorByteLength) / 4) {
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
const directorySectorIds =
|
|
105
|
+
AltiumOleInputTailNormalizer.#readSectorChain(
|
|
106
|
+
view.getInt32(48, true),
|
|
107
|
+
fatEntries
|
|
108
|
+
)
|
|
109
|
+
if (
|
|
110
|
+
!directorySectorIds.length ||
|
|
111
|
+
!AltiumOleInputTailNormalizer.#hasFullSectors(
|
|
112
|
+
directorySectorIds,
|
|
113
|
+
sourceByteLength,
|
|
114
|
+
sectorByteLength
|
|
115
|
+
)
|
|
116
|
+
) {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const numberOfMiniFatSectors = view.getUint32(64, true)
|
|
121
|
+
let miniFatEntries = []
|
|
122
|
+
if (numberOfMiniFatSectors) {
|
|
123
|
+
const miniFatSectorIds =
|
|
124
|
+
AltiumOleInputTailNormalizer.#readSectorChain(
|
|
125
|
+
view.getInt32(60, true),
|
|
126
|
+
fatEntries
|
|
127
|
+
)
|
|
128
|
+
if (
|
|
129
|
+
miniFatSectorIds.length !== numberOfMiniFatSectors ||
|
|
130
|
+
!AltiumOleInputTailNormalizer.#hasFullSectors(
|
|
131
|
+
miniFatSectorIds.slice(0, numberOfMiniFatSectors),
|
|
132
|
+
sourceByteLength,
|
|
133
|
+
sectorByteLength
|
|
134
|
+
)
|
|
135
|
+
) {
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
const miniFatBytes = AltiumOleInputTailNormalizer.#readFullSectors(
|
|
139
|
+
view,
|
|
140
|
+
miniFatSectorIds.slice(0, numberOfMiniFatSectors),
|
|
141
|
+
sectorByteLength
|
|
142
|
+
)
|
|
143
|
+
miniFatEntries =
|
|
144
|
+
AltiumOleInputTailNormalizer.#readInt32Entries(miniFatBytes)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const directoryBytes = AltiumOleInputTailNormalizer.#readFullSectors(
|
|
148
|
+
view,
|
|
149
|
+
directorySectorIds,
|
|
150
|
+
sectorByteLength
|
|
151
|
+
)
|
|
152
|
+
return AltiumOleInputTailNormalizer.#areStreamsComplete(
|
|
153
|
+
directoryBytes,
|
|
154
|
+
fatEntries,
|
|
155
|
+
miniFatEntries,
|
|
156
|
+
view.getUint32(56, true),
|
|
157
|
+
sourceByteLength,
|
|
158
|
+
sectorByteLength,
|
|
159
|
+
miniSectorByteLength,
|
|
160
|
+
Math.floor(
|
|
161
|
+
(sourceByteLength - HEADER_BYTE_LENGTH) / sectorByteLength
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Decodes little-endian signed integers from structural table bytes.
|
|
168
|
+
* @param {Uint8Array} bytes Structural sector bytes.
|
|
169
|
+
* @returns {number[]} Decoded table entries.
|
|
170
|
+
*/
|
|
171
|
+
static #readInt32Entries(bytes) {
|
|
172
|
+
const view = new DataView(
|
|
173
|
+
bytes.buffer,
|
|
174
|
+
bytes.byteOffset,
|
|
175
|
+
bytes.byteLength
|
|
176
|
+
)
|
|
177
|
+
const entries = []
|
|
178
|
+
for (let offset = 0; offset < bytes.byteLength; offset += 4) {
|
|
179
|
+
entries.push(view.getInt32(offset, true))
|
|
180
|
+
}
|
|
181
|
+
return entries
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Collects FAT sector ids from the header and DIFAT chain.
|
|
186
|
+
* @param {DataView} view Aligned candidate view.
|
|
187
|
+
* @param {number} sourceByteLength Original physical byte length.
|
|
188
|
+
* @param {number} sectorByteLength OLE sector size.
|
|
189
|
+
* @returns {number[]} FAT sector ids.
|
|
190
|
+
*/
|
|
191
|
+
static #collectFatSectorIds(view, sourceByteLength, sectorByteLength) {
|
|
192
|
+
const sectorIds = []
|
|
193
|
+
for (let index = 0; index < 109; index += 1) {
|
|
194
|
+
const sectorId = view.getInt32(76 + index * 4, true)
|
|
195
|
+
if (sectorId >= 0) sectorIds.push(sectorId)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const numberOfDifatSectors = view.getUint32(72, true)
|
|
199
|
+
let currentSectorId = view.getInt32(68, true)
|
|
200
|
+
const visited = new Set()
|
|
201
|
+
const entriesPerSector = sectorByteLength / 4
|
|
202
|
+
for (
|
|
203
|
+
let index = 0;
|
|
204
|
+
index < numberOfDifatSectors && currentSectorId >= 0;
|
|
205
|
+
index += 1
|
|
206
|
+
) {
|
|
207
|
+
if (visited.has(currentSectorId)) return []
|
|
208
|
+
visited.add(currentSectorId)
|
|
209
|
+
if (
|
|
210
|
+
!AltiumOleInputTailNormalizer.#hasFullSector(
|
|
211
|
+
currentSectorId,
|
|
212
|
+
sourceByteLength,
|
|
213
|
+
sectorByteLength
|
|
214
|
+
)
|
|
215
|
+
) {
|
|
216
|
+
return []
|
|
217
|
+
}
|
|
218
|
+
const offset =
|
|
219
|
+
HEADER_BYTE_LENGTH + currentSectorId * sectorByteLength
|
|
220
|
+
for (let entry = 0; entry < entriesPerSector - 1; entry += 1) {
|
|
221
|
+
const sectorId = view.getInt32(offset + entry * 4, true)
|
|
222
|
+
if (sectorId >= 0) sectorIds.push(sectorId)
|
|
223
|
+
}
|
|
224
|
+
currentSectorId = view.getInt32(
|
|
225
|
+
offset + (entriesPerSector - 1) * 4,
|
|
226
|
+
true
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
if (numberOfDifatSectors && visited.size !== numberOfDifatSectors) {
|
|
230
|
+
return []
|
|
231
|
+
}
|
|
232
|
+
return sectorIds
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Reads every FAT entry from complete FAT sectors.
|
|
237
|
+
* @param {DataView} view Aligned candidate view.
|
|
238
|
+
* @param {number[]} sectorIds FAT sector ids.
|
|
239
|
+
* @param {number} sourceByteLength Original physical byte length.
|
|
240
|
+
* @param {number} sectorByteLength OLE sector size.
|
|
241
|
+
* @returns {number[]} FAT entries.
|
|
242
|
+
*/
|
|
243
|
+
static #readFatEntries(
|
|
244
|
+
view,
|
|
245
|
+
sectorIds,
|
|
246
|
+
sourceByteLength,
|
|
247
|
+
sectorByteLength
|
|
248
|
+
) {
|
|
249
|
+
if (
|
|
250
|
+
!AltiumOleInputTailNormalizer.#hasFullSectors(
|
|
251
|
+
sectorIds,
|
|
252
|
+
sourceByteLength,
|
|
253
|
+
sectorByteLength
|
|
254
|
+
)
|
|
255
|
+
) {
|
|
256
|
+
return []
|
|
257
|
+
}
|
|
258
|
+
const entries = []
|
|
259
|
+
for (const sectorId of sectorIds) {
|
|
260
|
+
const offset = HEADER_BYTE_LENGTH + sectorId * sectorByteLength
|
|
261
|
+
for (let index = 0; index < sectorByteLength / 4; index += 1) {
|
|
262
|
+
entries.push(view.getInt32(offset + index * 4, true))
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return entries
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Reads one FAT chain with loop and bounds protection.
|
|
270
|
+
* @param {number} startSectorId First sector id.
|
|
271
|
+
* @param {number[]} fatEntries FAT entries.
|
|
272
|
+
* @returns {number[]} Ordered sector ids, or an empty invalid chain.
|
|
273
|
+
*/
|
|
274
|
+
static #readSectorChain(startSectorId, fatEntries) {
|
|
275
|
+
if (startSectorId < 0) return []
|
|
276
|
+
const sectorIds = []
|
|
277
|
+
const visited = new Set()
|
|
278
|
+
let currentSectorId = startSectorId
|
|
279
|
+
while (currentSectorId >= 0) {
|
|
280
|
+
if (
|
|
281
|
+
currentSectorId >= fatEntries.length ||
|
|
282
|
+
visited.has(currentSectorId)
|
|
283
|
+
) {
|
|
284
|
+
return []
|
|
285
|
+
}
|
|
286
|
+
visited.add(currentSectorId)
|
|
287
|
+
sectorIds.push(currentSectorId)
|
|
288
|
+
const nextSectorId = fatEntries[currentSectorId]
|
|
289
|
+
if (nextSectorId === END_OF_CHAIN) return sectorIds
|
|
290
|
+
if (!Number.isInteger(nextSectorId) || nextSectorId < 0) return []
|
|
291
|
+
currentSectorId = nextSectorId
|
|
292
|
+
}
|
|
293
|
+
return []
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Verifies regular, root, and mini-stream entries against source bytes.
|
|
298
|
+
* @param {Uint8Array} directoryBytes Decoded directory sectors.
|
|
299
|
+
* @param {number[]} fatEntries FAT entries.
|
|
300
|
+
* @param {number[]} miniFatEntries Mini-FAT entries.
|
|
301
|
+
* @param {number} miniStreamCutoff OLE mini-stream cutoff.
|
|
302
|
+
* @param {number} sourceByteLength Original physical byte length.
|
|
303
|
+
* @param {number} sectorByteLength OLE sector size.
|
|
304
|
+
* @param {number} miniSectorByteLength OLE mini-sector size.
|
|
305
|
+
* @param {number} partialSectorId Physically partial final sector id.
|
|
306
|
+
* @returns {boolean} Whether every declared stream is complete.
|
|
307
|
+
*/
|
|
308
|
+
static #areStreamsComplete(
|
|
309
|
+
directoryBytes,
|
|
310
|
+
fatEntries,
|
|
311
|
+
miniFatEntries,
|
|
312
|
+
miniStreamCutoff,
|
|
313
|
+
sourceByteLength,
|
|
314
|
+
sectorByteLength,
|
|
315
|
+
miniSectorByteLength,
|
|
316
|
+
partialSectorId
|
|
317
|
+
) {
|
|
318
|
+
const view = new DataView(
|
|
319
|
+
directoryBytes.buffer,
|
|
320
|
+
directoryBytes.byteOffset,
|
|
321
|
+
directoryBytes.byteLength
|
|
322
|
+
)
|
|
323
|
+
const entryCount =
|
|
324
|
+
directoryBytes.byteLength / DIRECTORY_ENTRY_BYTE_LENGTH
|
|
325
|
+
const entries = []
|
|
326
|
+
for (let index = 0; index < entryCount; index += 1) {
|
|
327
|
+
const offset = index * DIRECTORY_ENTRY_BYTE_LENGTH
|
|
328
|
+
const type = view.getUint8(offset + 66)
|
|
329
|
+
const streamSize = Number(view.getBigUint64(offset + 120, true))
|
|
330
|
+
if (!Number.isSafeInteger(streamSize)) return false
|
|
331
|
+
entries.push({
|
|
332
|
+
startSectorId: view.getInt32(offset + 116, true),
|
|
333
|
+
streamSize,
|
|
334
|
+
type
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const rootEntry = entries.find((entry) => entry.type === 5)
|
|
339
|
+
const rootStreamByteLength = rootEntry?.streamSize ?? 0
|
|
340
|
+
let containsDeclaredPartialSector = false
|
|
341
|
+
for (const entry of entries) {
|
|
342
|
+
const { startSectorId, streamSize, type } = entry
|
|
343
|
+
const isRootStream = type === 5 && streamSize > 0
|
|
344
|
+
const isRegularStream = type === 2 && streamSize >= miniStreamCutoff
|
|
345
|
+
if (!isRootStream && !isRegularStream) continue
|
|
346
|
+
const sectorIds = AltiumOleInputTailNormalizer.#readSectorChain(
|
|
347
|
+
startSectorId,
|
|
348
|
+
fatEntries
|
|
349
|
+
)
|
|
350
|
+
if (
|
|
351
|
+
!AltiumOleInputTailNormalizer.#isStreamComplete(
|
|
352
|
+
sectorIds,
|
|
353
|
+
streamSize,
|
|
354
|
+
sourceByteLength,
|
|
355
|
+
sectorByteLength
|
|
356
|
+
)
|
|
357
|
+
) {
|
|
358
|
+
return false
|
|
359
|
+
}
|
|
360
|
+
if (sectorIds.includes(partialSectorId)) {
|
|
361
|
+
containsDeclaredPartialSector = true
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
for (const entry of entries) {
|
|
366
|
+
const { startSectorId, streamSize, type } = entry
|
|
367
|
+
const isMiniStream =
|
|
368
|
+
type === 2 && streamSize > 0 && streamSize < miniStreamCutoff
|
|
369
|
+
if (!isMiniStream) continue
|
|
370
|
+
if (!rootEntry || !miniFatEntries.length) return false
|
|
371
|
+
const miniSectorIds = AltiumOleInputTailNormalizer.#readSectorChain(
|
|
372
|
+
startSectorId,
|
|
373
|
+
miniFatEntries
|
|
374
|
+
)
|
|
375
|
+
if (
|
|
376
|
+
!AltiumOleInputTailNormalizer.#isMiniStreamComplete(
|
|
377
|
+
miniSectorIds,
|
|
378
|
+
streamSize,
|
|
379
|
+
rootStreamByteLength,
|
|
380
|
+
miniSectorByteLength
|
|
381
|
+
)
|
|
382
|
+
) {
|
|
383
|
+
return false
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return containsDeclaredPartialSector
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Checks one mini-stream chain against its declared root stream container.
|
|
391
|
+
* @param {number[]} miniSectorIds Ordered mini-sector ids.
|
|
392
|
+
* @param {number} streamByteLength Declared mini-stream length.
|
|
393
|
+
* @param {number} rootStreamByteLength Declared root stream length.
|
|
394
|
+
* @param {number} miniSectorByteLength OLE mini-sector size.
|
|
395
|
+
* @returns {boolean} Whether every mini-stream byte is contained.
|
|
396
|
+
*/
|
|
397
|
+
static #isMiniStreamComplete(
|
|
398
|
+
miniSectorIds,
|
|
399
|
+
streamByteLength,
|
|
400
|
+
rootStreamByteLength,
|
|
401
|
+
miniSectorByteLength
|
|
402
|
+
) {
|
|
403
|
+
const requiredSectorCount = Math.ceil(
|
|
404
|
+
streamByteLength / miniSectorByteLength
|
|
405
|
+
)
|
|
406
|
+
if (miniSectorIds.length !== requiredSectorCount) {
|
|
407
|
+
return false
|
|
408
|
+
}
|
|
409
|
+
for (let index = 0; index < miniSectorIds.length; index += 1) {
|
|
410
|
+
const miniSectorId = miniSectorIds[index]
|
|
411
|
+
const miniSectorOffset = miniSectorId * miniSectorByteLength
|
|
412
|
+
if (!Number.isSafeInteger(miniSectorOffset)) return false
|
|
413
|
+
const remaining = streamByteLength - index * miniSectorByteLength
|
|
414
|
+
const required = Math.min(
|
|
415
|
+
miniSectorByteLength,
|
|
416
|
+
Math.max(0, remaining)
|
|
417
|
+
)
|
|
418
|
+
if (!required) continue
|
|
419
|
+
const available = Math.max(
|
|
420
|
+
0,
|
|
421
|
+
Math.min(
|
|
422
|
+
miniSectorByteLength,
|
|
423
|
+
rootStreamByteLength - miniSectorOffset
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
if (available < required) return false
|
|
427
|
+
}
|
|
428
|
+
return true
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Checks every declared byte of one regular stream chain.
|
|
433
|
+
* @param {number[]} sectorIds Stream sector ids.
|
|
434
|
+
* @param {number} streamByteLength Declared logical stream length.
|
|
435
|
+
* @param {number} sourceByteLength Original physical byte length.
|
|
436
|
+
* @param {number} sectorByteLength OLE sector size.
|
|
437
|
+
* @returns {boolean} Whether the logical stream is physically complete.
|
|
438
|
+
*/
|
|
439
|
+
static #isStreamComplete(
|
|
440
|
+
sectorIds,
|
|
441
|
+
streamByteLength,
|
|
442
|
+
sourceByteLength,
|
|
443
|
+
sectorByteLength
|
|
444
|
+
) {
|
|
445
|
+
const requiredSectorCount = Math.ceil(
|
|
446
|
+
streamByteLength / sectorByteLength
|
|
447
|
+
)
|
|
448
|
+
if (sectorIds.length !== requiredSectorCount) return false
|
|
449
|
+
for (let index = 0; index < sectorIds.length; index += 1) {
|
|
450
|
+
const remaining = streamByteLength - index * sectorByteLength
|
|
451
|
+
const required = Math.min(sectorByteLength, Math.max(0, remaining))
|
|
452
|
+
if (!required) continue
|
|
453
|
+
const available =
|
|
454
|
+
AltiumOleInputTailNormalizer.#availableSectorByteLength(
|
|
455
|
+
sectorIds[index],
|
|
456
|
+
sourceByteLength,
|
|
457
|
+
sectorByteLength
|
|
458
|
+
)
|
|
459
|
+
if (available < required) return false
|
|
460
|
+
}
|
|
461
|
+
return true
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Returns whether every sector is fully present in the original source.
|
|
466
|
+
* @param {number[]} sectorIds Sector ids.
|
|
467
|
+
* @param {number} sourceByteLength Original physical byte length.
|
|
468
|
+
* @param {number} sectorByteLength OLE sector size.
|
|
469
|
+
* @returns {boolean} Whether all sectors are complete.
|
|
470
|
+
*/
|
|
471
|
+
static #hasFullSectors(sectorIds, sourceByteLength, sectorByteLength) {
|
|
472
|
+
return sectorIds.every((sectorId) =>
|
|
473
|
+
AltiumOleInputTailNormalizer.#hasFullSector(
|
|
474
|
+
sectorId,
|
|
475
|
+
sourceByteLength,
|
|
476
|
+
sectorByteLength
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Returns whether one full sector is physically present.
|
|
483
|
+
* @param {number} sectorId Sector id.
|
|
484
|
+
* @param {number} sourceByteLength Original physical byte length.
|
|
485
|
+
* @param {number} sectorByteLength OLE sector size.
|
|
486
|
+
* @returns {boolean} Whether the sector is complete.
|
|
487
|
+
*/
|
|
488
|
+
static #hasFullSector(sectorId, sourceByteLength, sectorByteLength) {
|
|
489
|
+
return (
|
|
490
|
+
Number.isInteger(sectorId) &&
|
|
491
|
+
sectorId >= 0 &&
|
|
492
|
+
AltiumOleInputTailNormalizer.#availableSectorByteLength(
|
|
493
|
+
sectorId,
|
|
494
|
+
sourceByteLength,
|
|
495
|
+
sectorByteLength
|
|
496
|
+
) === sectorByteLength
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Resolves physical source bytes available for one sector.
|
|
502
|
+
* @param {number} sectorId Sector id.
|
|
503
|
+
* @param {number} sourceByteLength Original physical byte length.
|
|
504
|
+
* @param {number} sectorByteLength OLE sector size.
|
|
505
|
+
* @returns {number} Available bytes from zero through one full sector.
|
|
506
|
+
*/
|
|
507
|
+
static #availableSectorByteLength(
|
|
508
|
+
sectorId,
|
|
509
|
+
sourceByteLength,
|
|
510
|
+
sectorByteLength
|
|
511
|
+
) {
|
|
512
|
+
const offset = HEADER_BYTE_LENGTH + sectorId * sectorByteLength
|
|
513
|
+
return Math.max(
|
|
514
|
+
0,
|
|
515
|
+
Math.min(sectorByteLength, sourceByteLength - offset)
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Concatenates complete structural sectors from the aligned view.
|
|
521
|
+
* @param {DataView} view Aligned candidate view.
|
|
522
|
+
* @param {number[]} sectorIds Sector ids.
|
|
523
|
+
* @param {number} sectorByteLength OLE sector size.
|
|
524
|
+
* @returns {Uint8Array} Concatenated bytes.
|
|
525
|
+
*/
|
|
526
|
+
static #readFullSectors(view, sectorIds, sectorByteLength) {
|
|
527
|
+
const bytes = new Uint8Array(sectorIds.length * sectorByteLength)
|
|
528
|
+
const source = new Uint8Array(view.buffer)
|
|
529
|
+
sectorIds.forEach((sectorId, index) => {
|
|
530
|
+
const offset = HEADER_BYTE_LENGTH + sectorId * sectorByteLength
|
|
531
|
+
bytes.set(
|
|
532
|
+
source.slice(offset, offset + sectorByteLength),
|
|
533
|
+
index * sectorByteLength
|
|
534
|
+
)
|
|
535
|
+
})
|
|
536
|
+
return bytes
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
Object.freeze(AltiumOleInputTailNormalizer.prototype)
|
|
541
|
+
Object.freeze(AltiumOleInputTailNormalizer)
|