@ifc-lite/parser 4.3.1 → 5.0.0
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/dist/attribute-helpers.d.ts +38 -4
- package/dist/attribute-helpers.d.ts.map +1 -1
- package/dist/attribute-helpers.js +71 -4
- package/dist/attribute-helpers.js.map +1 -1
- package/dist/columnar-parser-attributes.d.ts.map +1 -1
- package/dist/columnar-parser-attributes.js +35 -5
- package/dist/columnar-parser-attributes.js.map +1 -1
- package/dist/columnar-parser-indexes.d.ts.map +1 -1
- package/dist/columnar-parser-indexes.js +17 -2
- package/dist/columnar-parser-indexes.js.map +1 -1
- package/dist/columnar-parser-relationships.d.ts.map +1 -1
- package/dist/columnar-parser-relationships.js +6 -1
- package/dist/columnar-parser-relationships.js.map +1 -1
- package/dist/columnar-parser.d.ts +3 -1
- package/dist/columnar-parser.d.ts.map +1 -1
- package/dist/columnar-parser.js +27 -76
- package/dist/columnar-parser.js.map +1 -1
- package/dist/compact-entity-index-transport.d.ts +23 -0
- package/dist/compact-entity-index-transport.d.ts.map +1 -0
- package/dist/compact-entity-index-transport.js +28 -0
- package/dist/compact-entity-index-transport.js.map +1 -0
- package/dist/compact-entity-index.d.ts +0 -10
- package/dist/compact-entity-index.d.ts.map +1 -1
- package/dist/compact-entity-index.js +31 -24
- package/dist/compact-entity-index.js.map +1 -1
- package/dist/data-store-transport.d.ts +2 -12
- package/dist/data-store-transport.d.ts.map +1 -1
- package/dist/data-store-transport.js +7 -20
- package/dist/data-store-transport.js.map +1 -1
- package/dist/entity-extractor.d.ts.map +1 -1
- package/dist/entity-extractor.js +101 -6
- package/dist/entity-extractor.js.map +1 -1
- package/dist/entity-refs-from-index.d.ts.map +1 -1
- package/dist/entity-refs-from-index.js +12 -2
- package/dist/entity-refs-from-index.js.map +1 -1
- package/dist/entity-scanner.d.ts +63 -0
- package/dist/entity-scanner.d.ts.map +1 -1
- package/dist/entity-scanner.js +79 -2
- package/dist/entity-scanner.js.map +1 -1
- package/dist/express-id.d.ts +47 -0
- package/dist/express-id.d.ts.map +1 -0
- package/dist/express-id.js +57 -0
- package/dist/express-id.js.map +1 -0
- package/dist/generated/entities.d.ts +1 -1
- package/dist/generated/entities.d.ts.map +1 -1
- package/dist/generated/schema-registry.js +146 -146
- package/dist/generated/schema-registry.js.map +1 -1
- package/dist/georef-epset.d.ts +25 -0
- package/dist/georef-epset.d.ts.map +1 -0
- package/dist/georef-epset.js +214 -0
- package/dist/georef-epset.js.map +1 -0
- package/dist/georef-extractor.d.ts +9 -12
- package/dist/georef-extractor.d.ts.map +1 -1
- package/dist/georef-extractor.js +66 -304
- package/dist/georef-extractor.js.map +1 -1
- package/dist/georef-map-conversion.d.ts +29 -0
- package/dist/georef-map-conversion.d.ts.map +1 -0
- package/dist/georef-map-conversion.js +75 -0
- package/dist/georef-map-conversion.js.map +1 -0
- package/dist/georef-transform.d.ts +25 -0
- package/dist/georef-transform.d.ts.map +1 -0
- package/dist/georef-transform.js +97 -0
- package/dist/georef-transform.js.map +1 -0
- package/dist/ifc-schema.d.ts +4 -2
- package/dist/ifc-schema.d.ts.map +1 -1
- package/dist/ifc-schema.js +15 -5
- package/dist/ifc-schema.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/map-unit-label.d.ts +43 -0
- package/dist/map-unit-label.d.ts.map +1 -0
- package/dist/map-unit-label.js +140 -0
- package/dist/map-unit-label.js.map +1 -0
- package/dist/material-extractor.d.ts +2 -17
- package/dist/material-extractor.d.ts.map +1 -1
- package/dist/material-extractor.js +12 -37
- package/dist/material-extractor.js.map +1 -1
- package/dist/material-layer-reader.d.ts +45 -0
- package/dist/material-layer-reader.d.ts.map +1 -0
- package/dist/material-layer-reader.js +63 -0
- package/dist/material-layer-reader.js.map +1 -0
- package/dist/material-resolver.d.ts.map +1 -1
- package/dist/material-resolver.js +14 -14
- package/dist/material-resolver.js.map +1 -1
- package/dist/measure-unit-scale.d.ts +41 -0
- package/dist/measure-unit-scale.d.ts.map +1 -0
- package/dist/measure-unit-scale.js +53 -0
- package/dist/measure-unit-scale.js.map +1 -0
- package/dist/on-demand-cache.d.ts +3 -0
- package/dist/on-demand-cache.d.ts.map +1 -0
- package/dist/on-demand-cache.js +25 -0
- package/dist/on-demand-cache.js.map +1 -0
- package/dist/on-demand-extractors.d.ts +5 -21
- package/dist/on-demand-extractors.d.ts.map +1 -1
- package/dist/on-demand-extractors.js +64 -437
- package/dist/on-demand-extractors.js.map +1 -1
- package/dist/on-demand-georeferencing.d.ts +19 -0
- package/dist/on-demand-georeferencing.d.ts.map +1 -0
- package/dist/on-demand-georeferencing.js +139 -0
- package/dist/on-demand-georeferencing.js.map +1 -0
- package/dist/owning-project.d.ts +54 -0
- package/dist/owning-project.d.ts.map +1 -0
- package/dist/owning-project.js +76 -0
- package/dist/owning-project.js.map +1 -0
- package/dist/parser.worker.d.ts +14 -0
- package/dist/parser.worker.d.ts.map +1 -1
- package/dist/parser.worker.js +2 -0
- package/dist/parser.worker.js.map +1 -1
- package/dist/project-units-symbols.d.ts +33 -0
- package/dist/project-units-symbols.d.ts.map +1 -0
- package/dist/project-units-symbols.js +265 -0
- package/dist/project-units-symbols.js.map +1 -0
- package/dist/project-units.d.ts +11 -19
- package/dist/project-units.d.ts.map +1 -1
- package/dist/project-units.js +9 -267
- package/dist/project-units.js.map +1 -1
- package/dist/property-set-merge.d.ts +65 -8
- package/dist/property-set-merge.d.ts.map +1 -1
- package/dist/property-set-merge.js +130 -29
- package/dist/property-set-merge.js.map +1 -1
- package/dist/property-value-parser.d.ts +55 -0
- package/dist/property-value-parser.d.ts.map +1 -0
- package/dist/property-value-parser.js +277 -0
- package/dist/property-value-parser.js.map +1 -0
- package/dist/quantity-collect.d.ts +135 -0
- package/dist/quantity-collect.d.ts.map +1 -0
- package/dist/quantity-collect.js +253 -0
- package/dist/quantity-collect.js.map +1 -0
- package/dist/query-backend-maps.d.ts +5 -8
- package/dist/query-backend-maps.d.ts.map +1 -1
- package/dist/query-backend-maps.js +159 -15
- package/dist/query-backend-maps.js.map +1 -1
- package/dist/relationship-extractor.d.ts.map +1 -1
- package/dist/relationship-extractor.js +18 -0
- package/dist/relationship-extractor.js.map +1 -1
- package/dist/scan-entities-balanced.d.ts +38 -0
- package/dist/scan-entities-balanced.d.ts.map +1 -0
- package/dist/scan-entities-balanced.js +258 -0
- package/dist/scan-entities-balanced.js.map +1 -0
- package/dist/scan-worker-inline.d.ts +17 -11
- package/dist/scan-worker-inline.d.ts.map +1 -1
- package/dist/scan-worker-inline.js +9 -235
- package/dist/scan-worker-inline.js.map +1 -1
- package/dist/scan-worker-source.d.ts +9 -0
- package/dist/scan-worker-source.d.ts.map +1 -0
- package/dist/scan-worker-source.js +383 -0
- package/dist/scan-worker-source.js.map +1 -0
- package/dist/source-header.d.ts +23 -1
- package/dist/source-header.d.ts.map +1 -1
- package/dist/source-header.js +133 -43
- package/dist/source-header.js.map +1 -1
- package/dist/spatial-hierarchy-builder.js +3 -3
- package/dist/spatial-hierarchy-builder.js.map +1 -1
- package/dist/step-lexing.d.ts +94 -0
- package/dist/step-lexing.d.ts.map +1 -1
- package/dist/step-lexing.js +292 -25
- package/dist/step-lexing.js.map +1 -1
- package/dist/step-trivia.d.ts +60 -0
- package/dist/step-trivia.d.ts.map +1 -0
- package/dist/step-trivia.js +63 -0
- package/dist/step-trivia.js.map +1 -0
- package/dist/tokenizer.d.ts +20 -21
- package/dist/tokenizer.d.ts.map +1 -1
- package/dist/tokenizer.js +164 -203
- package/dist/tokenizer.js.map +1 -1
- package/dist/unit-extractor.d.ts +56 -1
- package/dist/unit-extractor.d.ts.map +1 -1
- package/dist/unit-extractor.js +80 -6
- package/dist/unit-extractor.js.map +1 -1
- package/dist/worker-parser.d.ts +11 -1
- package/dist/worker-parser.d.ts.map +1 -1
- package/dist/worker-parser.js +16 -2
- package/dist/worker-parser.js.map +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
/**
|
|
5
|
+
* The scan worker's source, as a string.
|
|
6
|
+
*
|
|
7
|
+
* Split from `scan-worker-inline.ts`, which owns the Blob/Worker plumbing and
|
|
8
|
+
* the result decoding. The scanner is a string because a Blob worker cannot
|
|
9
|
+
* import at runtime, so this file is the one place the third copy of the STEP
|
|
10
|
+
* scan rules lives -- see the comments inside for which `step-lexing.ts`
|
|
11
|
+
* function each mirrors.
|
|
12
|
+
*/
|
|
13
|
+
import { MAX_EXPRESS_ID } from './express-id.js';
|
|
14
|
+
/**
|
|
15
|
+
* Self-contained entity scanner code (runs inside Web Worker).
|
|
16
|
+
* This is the same algorithm as StepTokenizer.scanEntitiesFast() but
|
|
17
|
+
* written as a standalone function for worker embedding.
|
|
18
|
+
*/
|
|
19
|
+
/** Exported for direct testing (run inside a mock `self`); the runtime path
|
|
20
|
+
* wraps it in a Blob worker in `scan-worker-inline.ts`. */
|
|
21
|
+
export const WORKER_CODE = `
|
|
22
|
+
'use strict';
|
|
23
|
+
self.onmessage = function(e) {
|
|
24
|
+
var buf = new Uint8Array(e.data);
|
|
25
|
+
var len = buf.length;
|
|
26
|
+
var pos = 0;
|
|
27
|
+
var line = 1;
|
|
28
|
+
|
|
29
|
+
// Pre-allocate result array (estimate ~13,500 entities per MB)
|
|
30
|
+
var estimatedCount = Math.max((len / 1024 / 1024) * 13500, 1000) | 0;
|
|
31
|
+
// Pack results into typed arrays for fast transfer. Uint32Array for the ids:
|
|
32
|
+
// that is the express-id storage contract every consumer of this scan holds
|
|
33
|
+
// to (CompactEntityIndex, the entity/property/quantity tables, the wasm
|
|
34
|
+
// boundary, Rust's ColumnarIndex), so the guard below refuses anything wider
|
|
35
|
+
// rather than carrying it one buffer further and truncating downstream
|
|
36
|
+
// (#3395). The worker runs from a Blob URL and cannot import at runtime, so
|
|
37
|
+
// the bound below is interpolated from express-id.ts when this template is
|
|
38
|
+
// evaluated -- one home for the number, not a copy that can drift.
|
|
39
|
+
// Whether a STEP comment opens at p.
|
|
40
|
+
function opensCommentAt(p) {
|
|
41
|
+
return p + 1 < len && buf[p] === 0x2F && buf[p + 1] === 0x2A;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Index just past the '*/' closing the comment at p, or -1 when it never
|
|
45
|
+
// closes. Counts the newlines it crosses so line numbers stay in step.
|
|
46
|
+
//
|
|
47
|
+
// Kept behaviourally identical to skipComment/skipTrivia in step-lexing.ts,
|
|
48
|
+
// which this cannot import: the worker source is a string, so this copy of
|
|
49
|
+
// the rule has to live here. Comments do not nest, per ISO 10303-21.
|
|
50
|
+
function skipCommentAt(p) {
|
|
51
|
+
var q = p + 2;
|
|
52
|
+
while (q + 1 < len) {
|
|
53
|
+
if (buf[q] === 0x2A && buf[q + 1] === 0x2F) return q + 2;
|
|
54
|
+
if (buf[q] === 0x0A) line++;
|
|
55
|
+
q++;
|
|
56
|
+
}
|
|
57
|
+
return -1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Skip whitespace, comments, and any run of the two -- 10303-21 allows a
|
|
61
|
+
// comment wherever whitespace is allowed, INCLUDING inside a record.
|
|
62
|
+
// Returns -1 when a comment opens and never closes: everything from there on
|
|
63
|
+
// is inside it, so there is nothing left to find.
|
|
64
|
+
//
|
|
65
|
+
// The whitespace byte set (space, tab, CR, LF, form feed, vertical tab) is
|
|
66
|
+
// kept byte-for-byte in sync with isSpaceByte in step-lexing.ts and its
|
|
67
|
+
// three inline twins in tokenizer.ts's scanEntitiesFast -- this file is a
|
|
68
|
+
// string because a Blob worker cannot import at runtime, not a reason for
|
|
69
|
+
// the rule itself to drift.
|
|
70
|
+
function skipTriviaAt(p) {
|
|
71
|
+
for (;;) {
|
|
72
|
+
while (p < len) {
|
|
73
|
+
var t = buf[p];
|
|
74
|
+
if (t === 0x20 || t === 0x09 || t === 0x0D || t === 0x0C || t === 0x0B) { p++; }
|
|
75
|
+
else if (t === 0x0A) { line++; p++; }
|
|
76
|
+
else break;
|
|
77
|
+
}
|
|
78
|
+
if (!opensCommentAt(p)) return p;
|
|
79
|
+
var e = skipCommentAt(p);
|
|
80
|
+
if (e < 0) return -1;
|
|
81
|
+
p = e;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
var ids = new Uint32Array(estimatedCount);
|
|
86
|
+
var offsets = new Uint32Array(estimatedCount);
|
|
87
|
+
var lengths = new Uint32Array(estimatedCount);
|
|
88
|
+
var lines = new Uint32Array(estimatedCount);
|
|
89
|
+
// Type names stored separately (strings)
|
|
90
|
+
var types = new Array(estimatedCount);
|
|
91
|
+
var count = 0;
|
|
92
|
+
// Records refused by the express-id bound, reported back to the caller.
|
|
93
|
+
var oversizedIds = 0;
|
|
94
|
+
// 0 or 1, never a count of how many: whether this scan stopped early on an
|
|
95
|
+
// unclosed string, an unclosed comment, or a declaration cut off before its
|
|
96
|
+
// own '(' -- set once, by the single post-loop check at the bottom of this
|
|
97
|
+
// function. Reported back to the caller. Mirrors tokenizer.ts's
|
|
98
|
+
// scanEntitiesFast contract exactly, including the 0-or-1 shape.
|
|
99
|
+
var malformedRecords = 0;
|
|
100
|
+
// Set on the way to that post-loop check, not counted at each site:
|
|
101
|
+
// 'stopped' for an unclosed string or comment that ran to end of buffer
|
|
102
|
+
// with nothing left to find, 'declOpen' while a #id=TYPE( header is
|
|
103
|
+
// incomplete. 'declOpen' stays armed ONLY when the reason for abandoning
|
|
104
|
+
// is running out of buffer (pos >= len); a mismatch with buffer still
|
|
105
|
+
// left (bad byte, oversized id) clears it, because the scan resumes
|
|
106
|
+
// byte-by-byte from wherever it gave up, and a #ref token inside the
|
|
107
|
+
// abandoned record's own argument list reads as a fresh, equally
|
|
108
|
+
// incomplete attempt that must not report "cut off" just because
|
|
109
|
+
// nothing later happens to clear it.
|
|
110
|
+
var stopped = false;
|
|
111
|
+
var declOpen = false;
|
|
112
|
+
|
|
113
|
+
// Type name cache (IFC files have ~776 unique types across millions of entities)
|
|
114
|
+
var typeCache = new Map();
|
|
115
|
+
|
|
116
|
+
function growArrays() {
|
|
117
|
+
var newSize = (count * 2) | 0;
|
|
118
|
+
var newIds = new Uint32Array(newSize);
|
|
119
|
+
newIds.set(ids);
|
|
120
|
+
ids = newIds;
|
|
121
|
+
var newOffsets = new Uint32Array(newSize);
|
|
122
|
+
newOffsets.set(offsets);
|
|
123
|
+
offsets = newOffsets;
|
|
124
|
+
var newLengths = new Uint32Array(newSize);
|
|
125
|
+
newLengths.set(lengths);
|
|
126
|
+
lengths = newLengths;
|
|
127
|
+
var newLines = new Uint32Array(newSize);
|
|
128
|
+
newLines.set(lines);
|
|
129
|
+
lines = newLines;
|
|
130
|
+
types.length = newSize;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
while (pos < len) {
|
|
134
|
+
var ch = buf[pos];
|
|
135
|
+
|
|
136
|
+
if (ch === 0x23) { // '#'
|
|
137
|
+
var startOffset = pos;
|
|
138
|
+
var startLine = line;
|
|
139
|
+
pos++;
|
|
140
|
+
|
|
141
|
+
// Read express ID
|
|
142
|
+
var expressId = 0;
|
|
143
|
+
var hasDigits = false;
|
|
144
|
+
while (pos < len) {
|
|
145
|
+
var c = buf[pos];
|
|
146
|
+
if (c >= 0x30 && c <= 0x39) {
|
|
147
|
+
expressId = expressId * 10 + (c - 0x30);
|
|
148
|
+
hasDigits = true;
|
|
149
|
+
pos++;
|
|
150
|
+
} else {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!hasDigits) continue;
|
|
155
|
+
declOpen = true;
|
|
156
|
+
|
|
157
|
+
// Whitespace AND comments: '#1 /* was #7 */ =' is a declaration. The
|
|
158
|
+
// inline loop stays for the common case; skipTriviaAt runs only once a
|
|
159
|
+
// comment actually opens. Mirrors tokenizer.ts's scanEntitiesFast.
|
|
160
|
+
while (pos < len) {
|
|
161
|
+
var c2 = buf[pos];
|
|
162
|
+
if (c2 === 0x20 || c2 === 0x09 || c2 === 0x0D || c2 === 0x0C || c2 === 0x0B) { pos++; }
|
|
163
|
+
else if (c2 === 0x0A) { line++; pos++; }
|
|
164
|
+
else break;
|
|
165
|
+
}
|
|
166
|
+
if (opensCommentAt(pos)) { pos = skipTriviaAt(pos); if (pos < 0) { stopped = true; break; } }
|
|
167
|
+
|
|
168
|
+
// Check for '='. A byte that is not '=' with buffer left to scan is
|
|
169
|
+
// not a truncation -- clear declOpen so a reference token inside a
|
|
170
|
+
// LATER abandoned record's argument list (see the oversized-id note
|
|
171
|
+
// below) cannot leave it stuck armed with nothing left to clear it.
|
|
172
|
+
if (pos >= len) continue;
|
|
173
|
+
if (buf[pos] !== 0x3D) { declOpen = false; continue; }
|
|
174
|
+
pos++;
|
|
175
|
+
|
|
176
|
+
// Express-id bound, identical to StepTokenizer.scanEntitiesFast -- this
|
|
177
|
+
// worker is that scan's twin and must reject the same records, and count
|
|
178
|
+
// the same ones, or which scan path ran decides both whether an id
|
|
179
|
+
// collides with another and what the user is told was dropped. The
|
|
180
|
+
// single '>' subsumes a safe-integer check: a digit run accumulated as a
|
|
181
|
+
// double is non-negative and integral, and every value past 2^32 --
|
|
182
|
+
// including one past 2^53, where two distinct ids collide onto one
|
|
183
|
+
// double -- fails it. Tested only after '=' has matched, because that is
|
|
184
|
+
// the DECLARATION shape Rust's EntityScanner validates before refusing:
|
|
185
|
+
// the 'continue' below resumes inside the refused record's argument
|
|
186
|
+
// list, so an oversized '#ref' in there arrives here too and would be
|
|
187
|
+
// counted as a second dropped record. Count the refusal; a record that
|
|
188
|
+
// vanishes without a trace is the same defect wearing a different hat.
|
|
189
|
+
if (expressId > ${MAX_EXPRESS_ID}) { oversizedIds++; declOpen = false; continue; }
|
|
190
|
+
|
|
191
|
+
// Skip whitespace and comments
|
|
192
|
+
while (pos < len) {
|
|
193
|
+
var c3 = buf[pos];
|
|
194
|
+
if (c3 === 0x20 || c3 === 0x09 || c3 === 0x0D || c3 === 0x0C || c3 === 0x0B) { pos++; }
|
|
195
|
+
else if (c3 === 0x0A) { line++; pos++; }
|
|
196
|
+
else break;
|
|
197
|
+
}
|
|
198
|
+
if (opensCommentAt(pos)) { pos = skipTriviaAt(pos); if (pos < 0) { stopped = true; break; } }
|
|
199
|
+
|
|
200
|
+
// Read type name. Must start A-Z; a bad start byte with buffer left
|
|
201
|
+
// clears declOpen for the same reason as the '=' check.
|
|
202
|
+
var typeStart = pos;
|
|
203
|
+
if (pos >= len) continue;
|
|
204
|
+
if (buf[pos] < 0x41 || buf[pos] > 0x5A) { declOpen = false; continue; }
|
|
205
|
+
|
|
206
|
+
while (pos < len) {
|
|
207
|
+
var c4 = buf[pos];
|
|
208
|
+
if ((c4 >= 0x41 && c4 <= 0x5A) || (c4 >= 0x61 && c4 <= 0x7A) ||
|
|
209
|
+
(c4 >= 0x30 && c4 <= 0x39) || c4 === 0x5F) {
|
|
210
|
+
pos++;
|
|
211
|
+
} else {
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (pos === typeStart) continue;
|
|
216
|
+
|
|
217
|
+
// Cache type name — use length + hash compound key and verify the actual
|
|
218
|
+
// bytes on a hit. Length alone can't disambiguate a 32-bit hash collision
|
|
219
|
+
// (e.g. "Aa"/"BB"), so without the byte compare a crafted/unlucky file
|
|
220
|
+
// could have one type silently misread as another. Mirrors tokenizer.ts.
|
|
221
|
+
var typeLen = pos - typeStart;
|
|
222
|
+
var typeHash = typeLen;
|
|
223
|
+
for (var i = typeStart; i < pos; i++) {
|
|
224
|
+
typeHash = (typeHash * 31 + buf[i]) | 0;
|
|
225
|
+
}
|
|
226
|
+
var cacheKey = typeLen + ':' + typeHash;
|
|
227
|
+
var typeName = typeCache.get(cacheKey);
|
|
228
|
+
var cacheHitMatches = false;
|
|
229
|
+
if (typeName !== undefined && typeName.length === typeLen) {
|
|
230
|
+
cacheHitMatches = true;
|
|
231
|
+
for (var v = 0; v < typeLen; v++) {
|
|
232
|
+
if (typeName.charCodeAt(v) !== buf[typeStart + v]) {
|
|
233
|
+
cacheHitMatches = false;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (typeName === undefined || !cacheHitMatches) {
|
|
239
|
+
typeName = String.fromCharCode.apply(null, buf.subarray(typeStart, pos));
|
|
240
|
+
typeCache.set(cacheKey, typeName);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Skip whitespace and comments
|
|
244
|
+
while (pos < len) {
|
|
245
|
+
var c5 = buf[pos];
|
|
246
|
+
if (c5 === 0x20 || c5 === 0x09 || c5 === 0x0D || c5 === 0x0C || c5 === 0x0B) { pos++; }
|
|
247
|
+
else if (c5 === 0x0A) { line++; pos++; }
|
|
248
|
+
else break;
|
|
249
|
+
}
|
|
250
|
+
if (opensCommentAt(pos)) { pos = skipTriviaAt(pos); if (pos < 0) { stopped = true; break; } }
|
|
251
|
+
|
|
252
|
+
// Check for '('. Same EOF-vs-mismatch split as '=' and the type name.
|
|
253
|
+
if (pos >= len) continue;
|
|
254
|
+
if (buf[pos] !== 0x28) { declOpen = false; continue; }
|
|
255
|
+
declOpen = false; // Header complete: '(' found.
|
|
256
|
+
|
|
257
|
+
// Skip to semicolon (handling strings)
|
|
258
|
+
var inString = false;
|
|
259
|
+
var foundTerminator = false;
|
|
260
|
+
while (pos < len) {
|
|
261
|
+
var c6 = buf[pos];
|
|
262
|
+
if (c6 === 0x27) { // quote
|
|
263
|
+
if (inString && pos + 1 < len && buf[pos + 1] === 0x27) {
|
|
264
|
+
pos += 2;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
inString = !inString;
|
|
268
|
+
} else if (c6 === 0x2F && !inString && opensCommentAt(pos)) {
|
|
269
|
+
// The ';' that ends a record can be preceded by a comment holding
|
|
270
|
+
// its own ';'. Take the comment whole, which also makes the quotes
|
|
271
|
+
// and parens inside it text -- the other direction of the rule the
|
|
272
|
+
// quote branch above gives for a '/*' inside a literal.
|
|
273
|
+
var ce = skipCommentAt(pos);
|
|
274
|
+
if (ce < 0) {
|
|
275
|
+
// Unterminated: this record has no terminator, and neither has
|
|
276
|
+
// anything after it. Drop it and stop.
|
|
277
|
+
pos = len;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
pos = ce;
|
|
281
|
+
continue;
|
|
282
|
+
} else if (c6 === 0x3B && !inString) { // semicolon
|
|
283
|
+
var entityLength = pos - startOffset + 1;
|
|
284
|
+
|
|
285
|
+
// Grow if needed
|
|
286
|
+
if (count >= ids.length) growArrays();
|
|
287
|
+
|
|
288
|
+
ids[count] = expressId;
|
|
289
|
+
offsets[count] = startOffset;
|
|
290
|
+
lengths[count] = entityLength;
|
|
291
|
+
lines[count] = startLine;
|
|
292
|
+
types[count] = typeName;
|
|
293
|
+
count++;
|
|
294
|
+
|
|
295
|
+
pos++;
|
|
296
|
+
foundTerminator = true;
|
|
297
|
+
break;
|
|
298
|
+
} else if (c6 === 0x0A) {
|
|
299
|
+
line++;
|
|
300
|
+
}
|
|
301
|
+
pos++;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Ran off the end without an unquoted ';' -- usually an unescaped
|
|
305
|
+
// quote left open, or the unterminated-comment break above (mirrors
|
|
306
|
+
// tokenizer.ts's scanEntitiesFast). Not resynced: with no known
|
|
307
|
+
// terminator, guessing a resume point risks fabricating entities from
|
|
308
|
+
// misaligned bytes. Recorded in 'stopped', not incremented here -- see
|
|
309
|
+
// the post-loop check below.
|
|
310
|
+
if (!foundTerminator) { stopped = true; }
|
|
311
|
+
} else if (ch === 0x0A) {
|
|
312
|
+
line++;
|
|
313
|
+
pos++;
|
|
314
|
+
} else if (ch === 0x27) { // quote
|
|
315
|
+
// Consume a string literal whole. HEADER records carry no '#', so this
|
|
316
|
+
// loop walks them byte by byte, and a '/*' inside a description would
|
|
317
|
+
// otherwise open a comment that never closes and take DATA with it.
|
|
318
|
+
var sp = pos + 1;
|
|
319
|
+
var closed = false;
|
|
320
|
+
while (sp < len) {
|
|
321
|
+
if (buf[sp] === 0x27) {
|
|
322
|
+
if (sp + 1 < len && buf[sp + 1] === 0x27) { sp += 2; continue; }
|
|
323
|
+
sp++;
|
|
324
|
+
closed = true;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
if (buf[sp] === 0x0A) { line++; }
|
|
328
|
+
sp++;
|
|
329
|
+
}
|
|
330
|
+
pos = sp;
|
|
331
|
+
// Ran off the end without a closing quote: everything from the open
|
|
332
|
+
// quote to EOF was consumed looking for one, so nothing after it was
|
|
333
|
+
// ever a candidate '#' -- the same "no terminator" shape as inside a
|
|
334
|
+
// record, just outside one (a HEADER string, most often).
|
|
335
|
+
if (!closed) { stopped = true; break; }
|
|
336
|
+
} else if (opensCommentAt(pos)) {
|
|
337
|
+
// Skip a comment region BETWEEN records. A record that is commented out
|
|
338
|
+
// is still a well-formed #id = TYPE(...), so every check above accepts
|
|
339
|
+
// it and only skipping the region rejects it.
|
|
340
|
+
var cp = skipCommentAt(pos);
|
|
341
|
+
if (cp < 0) {
|
|
342
|
+
// Unterminated: everything to EOF is commented out.
|
|
343
|
+
pos = len;
|
|
344
|
+
stopped = true;
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
pos = cp;
|
|
348
|
+
} else {
|
|
349
|
+
pos++;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ONE post-loop check, not an increment at every exit site above: the scan
|
|
354
|
+
// stopped early if it hit an explicit "no terminator" boundary ('stopped'),
|
|
355
|
+
// or the last #id=TYPE( header was cut short before its own '(' was found
|
|
356
|
+
// ('declOpen'). Always 0 or 1 -- the scan stops at the first one, so there
|
|
357
|
+
// is nothing further to accumulate.
|
|
358
|
+
if (stopped || declOpen) { malformedRecords = 1; }
|
|
359
|
+
|
|
360
|
+
// Trim arrays once, reuse for both message and transfer list
|
|
361
|
+
var needsTrim = ids.buffer.byteLength > count * 4;
|
|
362
|
+
var trimmedIds = needsTrim ? ids.slice(0, count) : ids;
|
|
363
|
+
var trimmedOffsets = needsTrim ? offsets.slice(0, count) : offsets;
|
|
364
|
+
var trimmedLengths = needsTrim ? lengths.slice(0, count) : lengths;
|
|
365
|
+
var trimmedLines = needsTrim ? lines.slice(0, count) : lines;
|
|
366
|
+
self.postMessage({
|
|
367
|
+
ids: trimmedIds.buffer,
|
|
368
|
+
offsets: trimmedOffsets.buffer,
|
|
369
|
+
lengths: trimmedLengths.buffer,
|
|
370
|
+
lines: trimmedLines.buffer,
|
|
371
|
+
types: types.slice(0, count),
|
|
372
|
+
count: count,
|
|
373
|
+
oversizedIds: oversizedIds,
|
|
374
|
+
malformedRecords: malformedRecords,
|
|
375
|
+
}, [
|
|
376
|
+
trimmedIds.buffer,
|
|
377
|
+
trimmedOffsets.buffer,
|
|
378
|
+
trimmedLengths.buffer,
|
|
379
|
+
trimmedLines.buffer,
|
|
380
|
+
]);
|
|
381
|
+
};
|
|
382
|
+
`;
|
|
383
|
+
//# sourceMappingURL=scan-worker-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan-worker-source.js","sourceRoot":"","sources":["../src/scan-worker-source.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;;GAQG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;;;GAIG;AACH;4DAC4D;AAC5D,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAwKH,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiMrC,CAAC"}
|
package/dist/source-header.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* strings (e.g. `'CoordinateReference [..., ProjectSite: Origin]'`), which a
|
|
9
9
|
* splitter that ignores quote state would mis-split.
|
|
10
10
|
*/
|
|
11
|
-
import type { IfcSourceHeader } from '@ifc-lite/data';
|
|
11
|
+
import type { IfcSourceHeader, IfcStoreBase } from '@ifc-lite/data';
|
|
12
12
|
import { type IfcSourceBytes } from './source-bytes.js';
|
|
13
13
|
/**
|
|
14
14
|
* Parse the HEADER section of a STEP/IFC buffer into {@link IfcSourceHeader}.
|
|
@@ -17,4 +17,26 @@ import { type IfcSourceBytes } from './source-bytes.js';
|
|
|
17
17
|
* truncated at the first `ENDSEC` so the DATA section is never scanned.
|
|
18
18
|
*/
|
|
19
19
|
export declare function parseSourceHeader(buffer: Uint8Array | IfcSourceBytes): IfcSourceHeader | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Determine which IFC schema a STEP buffer declares (issue #3278).
|
|
22
|
+
*
|
|
23
|
+
* The `FILE_SCHEMA` declaration is authoritative and is read from the
|
|
24
|
+
* already-parsed {@link IfcSourceHeader}; free text elsewhere in the header is
|
|
25
|
+
* not. That distinction is the whole point. `FILE_DESCRIPTION` and `FILE_NAME`
|
|
26
|
+
* carry author, organisation, preprocessor and originating-system strings, and
|
|
27
|
+
* exporters routinely stamp a schema token into their product name ("SomeApp
|
|
28
|
+
* IFC4 Exporter") — which a raw substring scan of the header bytes cannot tell
|
|
29
|
+
* apart from a declaration. Reading the record also reaches declarations that
|
|
30
|
+
* sit past the first 2 KB: ISO 10303-21 puts `FILE_SCHEMA` *after* `FILE_NAME`,
|
|
31
|
+
* and a long author or organisation list pushes it out of a small fixed window.
|
|
32
|
+
*
|
|
33
|
+
* Free on the hot path: {@link parseSourceHeader} already runs on every parse,
|
|
34
|
+
* so nothing extra is scanned. The raw decode below now happens only for a file
|
|
35
|
+
* that declares no schema at all.
|
|
36
|
+
*
|
|
37
|
+
* When no `FILE_SCHEMA` identifier resolves, fall back to the historical raw
|
|
38
|
+
* scan of the first 2000 bytes rather than refusing, so every file that
|
|
39
|
+
* resolves today keeps resolving the same way.
|
|
40
|
+
*/
|
|
41
|
+
export declare function detectSchemaVersion(buffer: Uint8Array | IfcSourceBytes, header: IfcSourceHeader | undefined): IfcStoreBase['schemaVersion'];
|
|
20
42
|
//# sourceMappingURL=source-header.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-header.d.ts","sourceRoot":"","sources":["../src/source-header.ts"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"source-header.d.ts","sourceRoot":"","sources":["../src/source-header.ts"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAGpE,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AA4JvE;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,UAAU,GAAG,cAAc,GAClC,eAAe,GAAG,SAAS,CA2D7B;AA8BD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,GAAG,cAAc,EACnC,MAAM,EAAE,eAAe,GAAG,SAAS,GAClC,YAAY,CAAC,eAAe,CAAC,CAuB/B"}
|
package/dist/source-header.js
CHANGED
|
@@ -3,38 +3,33 @@
|
|
|
3
3
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
4
|
import { decodeStepStringLiteral } from '@ifc-lite/encoding';
|
|
5
5
|
import { asSourceBytes } from './source-bytes.js';
|
|
6
|
+
import { matchesKeywordAt, StepTextScan } from './step-lexing.js';
|
|
6
7
|
/** Headers are tiny; cap the decode so a huge file's body is never scanned. */
|
|
7
8
|
const MAX_HEADER_BYTES = 64 * 1024;
|
|
8
9
|
/**
|
|
9
10
|
* Split STEP record arguments at top-level commas, respecting paren/bracket
|
|
10
|
-
* nesting
|
|
11
|
-
* still-escaped argument substrings (trimmed).
|
|
11
|
+
* nesting, single-quoted strings (with `''` escapes) and comments. Returns the
|
|
12
|
+
* raw, still-escaped argument substrings (trimmed).
|
|
13
|
+
*
|
|
14
|
+
* A comment is dropped rather than copied through: it is not part of the
|
|
15
|
+
* argument's value, and its commas are not separators.
|
|
12
16
|
*/
|
|
13
17
|
function splitTopLevel(inner) {
|
|
14
18
|
const args = [];
|
|
15
19
|
let depth = 0;
|
|
16
|
-
let inString = false;
|
|
17
20
|
let current = '';
|
|
21
|
+
const scan = new StepTextScan(inner);
|
|
18
22
|
for (let i = 0; i < inner.length; i++) {
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
i++;
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
inString = false;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
23
|
+
const skip = scan.skipLexicalAt(i);
|
|
24
|
+
if (skip >= 0) {
|
|
25
|
+
// A literal is part of the argument's text; a comment is not.
|
|
26
|
+
if (inner[i] === "'")
|
|
27
|
+
current += inner.slice(i, skip);
|
|
28
|
+
i = skip - 1;
|
|
31
29
|
continue;
|
|
32
30
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
current += ch;
|
|
36
|
-
}
|
|
37
|
-
else if (ch === '(' || ch === '[') {
|
|
31
|
+
const ch = inner[i];
|
|
32
|
+
if (ch === '(' || ch === '[') {
|
|
38
33
|
depth++;
|
|
39
34
|
current += ch;
|
|
40
35
|
}
|
|
@@ -111,38 +106,56 @@ function decodeStringList(arg) {
|
|
|
111
106
|
.filter((v) => v !== undefined);
|
|
112
107
|
}
|
|
113
108
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
109
|
+
* Index of `keyword` occurring as a RECORD, outside any string or comment, or
|
|
110
|
+
* -1.
|
|
111
|
+
*
|
|
112
|
+
* A plain `indexOf` is not enough here and the reason is the same one #3278 is
|
|
113
|
+
* about, one level down: header FREE TEXT is not a declaration. STEP strings
|
|
114
|
+
* are single-quoted with `''` as the escape, and a `FILE_DESCRIPTION` item is
|
|
115
|
+
* free to contain the literal text `FILE_SCHEMA(('IFC2X3'))` -- an exporter
|
|
116
|
+
* stamping its own header into a description, a file round-tripped through a
|
|
117
|
+
* tool that quotes what it read. `indexOf` would take that quoted copy as the
|
|
118
|
+
* declaration and answer IFC2X3 for an IFC4X3 file. The same applies to
|
|
119
|
+
* `ENDSEC`: a quoted one would truncate the header before the real
|
|
120
|
+
* `FILE_SCHEMA` record, losing the declaration entirely.
|
|
117
121
|
*/
|
|
118
|
-
function
|
|
119
|
-
const
|
|
120
|
-
|
|
122
|
+
function indexOfRecord(text, keyword) {
|
|
123
|
+
const scan = new StepTextScan(text);
|
|
124
|
+
for (let i = 0; i < text.length; i++) {
|
|
125
|
+
const skip = scan.skipLexicalAt(i);
|
|
126
|
+
if (skip >= 0) {
|
|
127
|
+
i = skip - 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (matchesKeywordAt(text, i, keyword))
|
|
131
|
+
return i;
|
|
132
|
+
}
|
|
133
|
+
return -1;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Extract the argument substring inside the parentheses of `KEYWORD( ... )`.
|
|
137
|
+
* Quote-, comment- and nesting-aware so a quoted
|
|
138
|
+
* `)` never closes the record early, and so a quoted KEYWORD is never mistaken
|
|
139
|
+
* for the record itself. Returns `null` if not found.
|
|
140
|
+
*/
|
|
141
|
+
function extractRecordArgs(text, keyword) {
|
|
142
|
+
const at = indexOfRecord(text, keyword);
|
|
121
143
|
if (at < 0)
|
|
122
144
|
return null;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
i++;
|
|
145
|
+
const scan = new StepTextScan(text);
|
|
146
|
+
let i = scan.skipTrivia(at + keyword.length);
|
|
126
147
|
if (text[i] !== '(')
|
|
127
148
|
return null;
|
|
128
149
|
const start = i;
|
|
129
150
|
let depth = 0;
|
|
130
|
-
let inString = false;
|
|
131
151
|
for (; i < text.length; i++) {
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
if (text[i + 1] === "'")
|
|
136
|
-
i++;
|
|
137
|
-
else
|
|
138
|
-
inString = false;
|
|
139
|
-
}
|
|
152
|
+
const skip = scan.skipLexicalAt(i);
|
|
153
|
+
if (skip >= 0) {
|
|
154
|
+
i = skip - 1;
|
|
140
155
|
continue;
|
|
141
156
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
else if (ch === '(') {
|
|
157
|
+
const ch = text[i];
|
|
158
|
+
if (ch === '(') {
|
|
146
159
|
depth++;
|
|
147
160
|
}
|
|
148
161
|
else if (ch === ')') {
|
|
@@ -163,7 +176,7 @@ export function parseSourceHeader(buffer) {
|
|
|
163
176
|
const src = asSourceBytes(buffer);
|
|
164
177
|
const cap = Math.min(src.byteLength, MAX_HEADER_BYTES);
|
|
165
178
|
let text = src.decodeUtf8(0, cap);
|
|
166
|
-
const endSec = text
|
|
179
|
+
const endSec = indexOfRecord(text, 'ENDSEC');
|
|
167
180
|
if (endSec >= 0)
|
|
168
181
|
text = text.slice(0, endSec);
|
|
169
182
|
const descRecord = extractRecordArgs(text, 'FILE_DESCRIPTION');
|
|
@@ -216,4 +229,81 @@ export function parseSourceHeader(buffer) {
|
|
|
216
229
|
schemaIdentifiers,
|
|
217
230
|
};
|
|
218
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Resolve one `FILE_SCHEMA` identifier to the schema version a store carries.
|
|
234
|
+
*
|
|
235
|
+
* Matched by PREFIX, longest first: the spellings that reach us in the wild
|
|
236
|
+
* carry addendum/corrigendum suffixes (`IFC4X3_ADD2`, `IFC4X1`, `IFC2X3_TC1`),
|
|
237
|
+
* and `IFC4X3` itself begins with `IFC4`, so the `IFC4X3` branch has to be
|
|
238
|
+
* tried before the `IFC4` one. Returns `undefined` for an identifier naming no
|
|
239
|
+
* schema we model, so the caller can keep looking.
|
|
240
|
+
*/
|
|
241
|
+
function schemaFromIdentifier(identifier) {
|
|
242
|
+
const token = identifier.trim().toUpperCase();
|
|
243
|
+
if (token.startsWith('IFC5'))
|
|
244
|
+
return 'IFC5';
|
|
245
|
+
if (token.startsWith('IFC4X3'))
|
|
246
|
+
return 'IFC4X3';
|
|
247
|
+
if (token.startsWith('IFC4'))
|
|
248
|
+
return 'IFC4';
|
|
249
|
+
if (token.startsWith('IFC2X3'))
|
|
250
|
+
return 'IFC2X3';
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
/** Upper-case the ASCII letters and nothing else. See `detectSchemaVersion`. */
|
|
254
|
+
function asciiUpper(text) {
|
|
255
|
+
let out = '';
|
|
256
|
+
for (let i = 0; i < text.length; i++) {
|
|
257
|
+
const c = text.charCodeAt(i);
|
|
258
|
+
out += c >= 97 && c <= 122 ? String.fromCharCode(c - 32) : text[i];
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Determine which IFC schema a STEP buffer declares (issue #3278).
|
|
264
|
+
*
|
|
265
|
+
* The `FILE_SCHEMA` declaration is authoritative and is read from the
|
|
266
|
+
* already-parsed {@link IfcSourceHeader}; free text elsewhere in the header is
|
|
267
|
+
* not. That distinction is the whole point. `FILE_DESCRIPTION` and `FILE_NAME`
|
|
268
|
+
* carry author, organisation, preprocessor and originating-system strings, and
|
|
269
|
+
* exporters routinely stamp a schema token into their product name ("SomeApp
|
|
270
|
+
* IFC4 Exporter") — which a raw substring scan of the header bytes cannot tell
|
|
271
|
+
* apart from a declaration. Reading the record also reaches declarations that
|
|
272
|
+
* sit past the first 2 KB: ISO 10303-21 puts `FILE_SCHEMA` *after* `FILE_NAME`,
|
|
273
|
+
* and a long author or organisation list pushes it out of a small fixed window.
|
|
274
|
+
*
|
|
275
|
+
* Free on the hot path: {@link parseSourceHeader} already runs on every parse,
|
|
276
|
+
* so nothing extra is scanned. The raw decode below now happens only for a file
|
|
277
|
+
* that declares no schema at all.
|
|
278
|
+
*
|
|
279
|
+
* When no `FILE_SCHEMA` identifier resolves, fall back to the historical raw
|
|
280
|
+
* scan of the first 2000 bytes rather than refusing, so every file that
|
|
281
|
+
* resolves today keeps resolving the same way.
|
|
282
|
+
*/
|
|
283
|
+
export function detectSchemaVersion(buffer, header) {
|
|
284
|
+
for (const identifier of header?.schemaIdentifiers ?? []) {
|
|
285
|
+
const version = schemaFromIdentifier(identifier);
|
|
286
|
+
if (version !== undefined)
|
|
287
|
+
return version;
|
|
288
|
+
}
|
|
289
|
+
const src = asSourceBytes(buffer);
|
|
290
|
+
const headerEnd = Math.min(src.byteLength, 2000);
|
|
291
|
+
// ASCII-only, for the same reason `matchesKeywordAt` is. `toUpperCase()`
|
|
292
|
+
// maps `ı` (dotless i) to `I`, so a FILE_DESCRIPTION mentioning `ıFC5` chose
|
|
293
|
+
// IFC5 for a file that never said so. This scan is already a loose
|
|
294
|
+
// last-resort substring match -- it only runs when no FILE_SCHEMA identifier
|
|
295
|
+
// resolved at all -- but loose is not a reason to accept a fold 10303-21
|
|
296
|
+
// does not use. Offsets are not taken from this copy, so a copy is fine here
|
|
297
|
+
// where it was not in the record scan.
|
|
298
|
+
const headerText = asciiUpper(src.decodeUtf8(0, headerEnd));
|
|
299
|
+
if (headerText.includes('IFC5'))
|
|
300
|
+
return 'IFC5';
|
|
301
|
+
if (headerText.includes('IFC4X3'))
|
|
302
|
+
return 'IFC4X3';
|
|
303
|
+
if (headerText.includes('IFC4'))
|
|
304
|
+
return 'IFC4';
|
|
305
|
+
if (headerText.includes('IFC2X3'))
|
|
306
|
+
return 'IFC2X3';
|
|
307
|
+
return 'IFC4'; // Default fallback
|
|
308
|
+
}
|
|
219
309
|
//# sourceMappingURL=source-header.js.map
|