@xml-auditfile/auditfile 0.0.0-stage → 0.0.2-alpha.3

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.
Files changed (83) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +183 -2
  3. package/package.json +62 -4
  4. package/src/auditfile.js +186 -0
  5. package/src/definitions/dialect.js +40 -0
  6. package/src/definitions/element-definition.js +27 -0
  7. package/src/definitions/localized-text.js +41 -0
  8. package/src/definitions/manifest.js +160 -0
  9. package/src/definitions/names.js +42 -0
  10. package/src/definitions/resources.js +29 -0
  11. package/src/definitions/view-definition.js +160 -0
  12. package/src/diagnostics/collector.js +139 -0
  13. package/src/diagnostics/diagnostic.js +53 -0
  14. package/src/diagnostics/report.js +56 -0
  15. package/src/generated/spec.js +74 -0
  16. package/src/index.js +20 -0
  17. package/src/internal/composite-key.js +2 -0
  18. package/src/internal/decimal.js +193 -0
  19. package/src/internal/encoding.js +118 -0
  20. package/src/internal/json.js +106 -0
  21. package/src/internal/sha256.js +147 -0
  22. package/src/internal/streams.js +232 -0
  23. package/src/internal/xml-reader.js +247 -0
  24. package/src/memory/memory-sink.js +68 -0
  25. package/src/reading/mapping-plan.js +91 -0
  26. package/src/reading/read-options.js +136 -0
  27. package/src/reading/read-session.js +446 -0
  28. package/src/reading/row-loader.js +362 -0
  29. package/src/reading/row.js +123 -0
  30. package/src/reading/source.js +146 -0
  31. package/src/reading/value-converter.js +136 -0
  32. package/src/schema/schema.js +256 -0
  33. package/src/sinks/sink.js +53 -0
  34. package/src/validation/reference-checker.js +146 -0
  35. package/src/validation/xaf-rules.js +167 -0
  36. package/src/versioning/version-resolver.js +119 -0
  37. package/src/versioning/version.js +217 -0
  38. package/src/views/view-result.js +20 -0
  39. package/src/views/view-runner.js +449 -0
  40. package/src/xsd/datatypes.js +141 -0
  41. package/src/xsd/regex.js +121 -0
  42. package/src/xsd/schema-set.js +555 -0
  43. package/src/xsd/validator.js +484 -0
  44. package/types/auditfile.d.ts +57 -0
  45. package/types/definitions/dialect.d.ts +27 -0
  46. package/types/definitions/element-definition.d.ts +16 -0
  47. package/types/definitions/localized-text.d.ts +25 -0
  48. package/types/definitions/manifest.d.ts +118 -0
  49. package/types/definitions/names.d.ts +25 -0
  50. package/types/definitions/resources.d.ts +9 -0
  51. package/types/definitions/view-definition.d.ts +85 -0
  52. package/types/diagnostics/collector.d.ts +38 -0
  53. package/types/diagnostics/diagnostic.d.ts +37 -0
  54. package/types/diagnostics/report.d.ts +32 -0
  55. package/types/generated/spec.d.ts +6 -0
  56. package/types/index.d.ts +18 -0
  57. package/types/internal/composite-key.d.ts +2 -0
  58. package/types/internal/decimal.d.ts +55 -0
  59. package/types/internal/encoding.d.ts +43 -0
  60. package/types/internal/json.d.ts +17 -0
  61. package/types/internal/sha256.d.ts +17 -0
  62. package/types/internal/streams.d.ts +35 -0
  63. package/types/internal/xml-reader.d.ts +50 -0
  64. package/types/memory/memory-sink.d.ts +29 -0
  65. package/types/reading/mapping-plan.d.ts +45 -0
  66. package/types/reading/read-options.d.ts +69 -0
  67. package/types/reading/read-session.d.ts +28 -0
  68. package/types/reading/row-loader.d.ts +18 -0
  69. package/types/reading/row.d.ts +53 -0
  70. package/types/reading/source.d.ts +73 -0
  71. package/types/reading/value-converter.d.ts +15 -0
  72. package/types/schema/schema.d.ts +116 -0
  73. package/types/sinks/sink.d.ts +59 -0
  74. package/types/validation/reference-checker.d.ts +11 -0
  75. package/types/validation/xaf-rules.d.ts +13 -0
  76. package/types/versioning/version-resolver.d.ts +25 -0
  77. package/types/versioning/version.d.ts +71 -0
  78. package/types/views/view-result.d.ts +13 -0
  79. package/types/views/view-runner.d.ts +1 -0
  80. package/types/xsd/datatypes.d.ts +40 -0
  81. package/types/xsd/regex.d.ts +6 -0
  82. package/types/xsd/schema-set.d.ts +20 -0
  83. package/types/xsd/validator.d.ts +48 -0
@@ -0,0 +1,136 @@
1
+ import { TextLanguage } from '../definitions/localized-text.js';
2
+ import { AuditfileVersion } from '../versioning/version.js';
3
+
4
+ /** How the reader treats the file. */
5
+ export const ReadMode = Object.freeze({
6
+ /** Validate against the XSD first; load only a valid file. */
7
+ Verified: 'verified',
8
+ /** Load directly with the manifest; elements outside the manifest are reported and skipped. */
9
+ Unverified: 'unverified',
10
+ });
11
+
12
+ /** XML events between two checks of the abort signal. @internal */
13
+ export const CANCELLATION_INTERVAL = 10_000;
14
+
15
+ /** Progress of a read. */
16
+ export class ReadProgress {
17
+ /** @internal */
18
+ constructor(rowsRead, totalRows) {
19
+ /** Rows read so far. */
20
+ this.rowsRead = rowsRead;
21
+ /** Total rows, when known from the scan phase; otherwise null. */
22
+ this.totalRows = totalRows;
23
+ Object.freeze(this);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * @typedef {object} ReadOptions Options for reading. The defaults suit most uses.
29
+ * @property {AuditfileVersion | string | null} [version] Force a version, e.g. `AuditfileVersion.Financial.V3_2` or "xaf/3.2". Null: determine from the namespace.
30
+ * @property {'verified' | 'unverified'} [mode] Verified (default) or unverified.
31
+ * @property {boolean} [useResolver] Derive the version from an unknown namespace, e.g. ".../XAF/3.2/". Off by default.
32
+ * @property {(() => unknown) | null} [xsd] Own XSD instead of the built-in one, as a factory of bytes or a stream. Null: use the built-in XSD.
33
+ * @property {boolean} [applyDialects] Apply known dialect corrections. On by default.
34
+ * @property {number} [batchSize] Rows per table handed to the sink at once (default 1000).
35
+ * @property {'nl' | 'en'} [language] Language of diagnostic messages (default Dutch).
36
+ * @property {number} [maxDiagnostics] Maximum number of diagnostics; the rest is summarized (default 1000).
37
+ * @property {number} [maxDepth] Maximum element nesting; deeper files are refused (default 64).
38
+ * @property {number} [maxCharactersInDocument] Maximum characters in the whole document, all parts together; a larger file stops with XAF-X001 (default 4 Gi; 0 is no limit).
39
+ * @property {number} [maxValueLength] Maximum characters of one value; a longer value is skipped with XAF-S008 and never held in memory as a whole (default 65536; the longest value an XSD allows is 999).
40
+ * @property {((progress: ReadProgress) => void) | null} [onProgress] Receives progress after every batch.
41
+ * @property {AbortSignal | null} [signal] Cancels reading, in the scan phase as well as the load phase; the call then rejects with the signal's reason.
42
+ */
43
+
44
+ const DEFAULTS = Object.freeze({
45
+ version: null,
46
+ mode: ReadMode.Verified,
47
+ useResolver: false,
48
+ xsd: null,
49
+ applyDialects: true,
50
+ batchSize: 1000,
51
+ language: TextLanguage.Dutch,
52
+ maxDiagnostics: 1000,
53
+ maxDepth: 64,
54
+ maxCharactersInDocument: 4 * 1024 ** 3,
55
+ maxValueLength: 65_536,
56
+ onProgress: null,
57
+ signal: null,
58
+ });
59
+
60
+ function positiveInteger(name, value) {
61
+ if (!Number.isSafeInteger(value) || value < 1) {
62
+ throw new RangeError(`${name} must be a positive integer.`);
63
+ }
64
+
65
+ return value;
66
+ }
67
+
68
+ function oneOf(name, value, allowed) {
69
+ if (!Object.values(allowed).includes(value)) {
70
+ throw new RangeError(`${name} must be one of ${Object.values(allowed).map((v) => `'${v}'`).join(', ')}.`);
71
+ }
72
+
73
+ return value;
74
+ }
75
+
76
+ function optionalFunction(name, value) {
77
+ if (value !== null && typeof value !== 'function') {
78
+ throw new TypeError(`${name} must be a function.`);
79
+ }
80
+
81
+ return value;
82
+ }
83
+
84
+ /**
85
+ * Checks and completes options; unknown names are refused so a typo never goes unnoticed.
86
+ * @param {ReadOptions | null | undefined} options
87
+ * @returns {Required<ReadOptions> & { version: AuditfileVersion | null }}
88
+ * @internal
89
+ */
90
+ export function normalizeOptions(options) {
91
+ if (options === null || options === undefined) {
92
+ return DEFAULTS;
93
+ }
94
+
95
+ if (typeof options !== 'object') {
96
+ throw new TypeError('options must be an object.');
97
+ }
98
+
99
+ for (const name of Object.keys(options)) {
100
+ if (!Object.prototype.hasOwnProperty.call(DEFAULTS, name)) {
101
+ throw new TypeError(`Unknown read option '${name}'.`);
102
+ }
103
+ }
104
+
105
+ const merged = { ...DEFAULTS };
106
+ for (const name of Object.keys(DEFAULTS)) {
107
+ if (options[name] !== undefined) {
108
+ merged[name] = options[name];
109
+ }
110
+ }
111
+
112
+ if (typeof merged.version === 'string') {
113
+ merged.version = AuditfileVersion.parse(merged.version);
114
+ } else if (merged.version !== null && !(merged.version instanceof AuditfileVersion)) {
115
+ throw new TypeError('version must be an AuditfileVersion or a key such as "xaf/3.2".');
116
+ }
117
+
118
+ oneOf('mode', merged.mode, ReadMode);
119
+ oneOf('language', merged.language, TextLanguage);
120
+ positiveInteger('batchSize', merged.batchSize);
121
+ positiveInteger('maxDiagnostics', merged.maxDiagnostics);
122
+ positiveInteger('maxDepth', merged.maxDepth);
123
+ positiveInteger('maxValueLength', merged.maxValueLength);
124
+ if (!Number.isSafeInteger(merged.maxCharactersInDocument) || merged.maxCharactersInDocument < 0) {
125
+ throw new RangeError('maxCharactersInDocument must be a non-negative integer.');
126
+ }
127
+ optionalFunction('xsd', merged.xsd);
128
+ optionalFunction('onProgress', merged.onProgress);
129
+ merged.useResolver = merged.useResolver === true;
130
+ merged.applyDialects = merged.applyDialects !== false;
131
+ if (merged.signal !== null && typeof merged.signal?.throwIfAborted !== 'function') {
132
+ throw new TypeError('signal must be an AbortSignal.');
133
+ }
134
+
135
+ return Object.freeze(merged);
136
+ }
@@ -0,0 +1,446 @@
1
+ import { DiagnosticCollector, Location } from '../diagnostics/collector.js';
2
+ import { AuditfileReport } from '../diagnostics/report.js';
3
+ import { Sha256 } from '../internal/sha256.js';
4
+ import { ConcatenatedStream, iterate, SourceError } from '../internal/streams.js';
5
+ import { END, START, TEXT, XmlEncodingError, XmlReader, XmlSyntaxError } from '../internal/xml-reader.js';
6
+ import { AuditfileSchema } from '../schema/schema.js';
7
+ import { ReferenceChecker } from '../validation/reference-checker.js';
8
+ import { XAF, XafRules } from '../validation/xaf-rules.js';
9
+ import { resolveVersion } from '../versioning/version-resolver.js';
10
+ import { SchemaSet } from '../xsd/schema-set.js';
11
+ import { XsdValidator } from '../xsd/validator.js';
12
+ import { MappingPlan } from './mapping-plan.js';
13
+ import { CANCELLATION_INTERVAL, ReadMode } from './read-options.js';
14
+ import { RowLoader } from './row-loader.js';
15
+
16
+ const PEEK_LIMIT = 1024 * 1024;
17
+ const MAX_XSD_SIZE = 16 * 1024 * 1024;
18
+
19
+ const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
20
+
21
+ /**
22
+ * One read of one source: resolve the version, optionally scan (validate and count),
23
+ * then load (stream rows). Holds the diagnostics and the part statistics.
24
+ */
25
+ export class ReadSession {
26
+ #source;
27
+ #options;
28
+ #started = now();
29
+ #rowCounts = new Map();
30
+ #parts = [];
31
+ #loaded = false;
32
+ #loadStarted = false;
33
+ // A single-use source is opened once: the document used for version detection is kept for loading.
34
+ #pending = null;
35
+
36
+ /** @type {import('../versioning/version-resolver.js').Resolution | null} */
37
+ resolution = null;
38
+ /** @type {AuditfileSchema | null} */
39
+ schema = null;
40
+ /** True when the load phase must validate against the XSD itself (single-use source). */
41
+ validateDuringLoad = false;
42
+
43
+ constructor(source, options) {
44
+ this.#source = source;
45
+ this.#options = options;
46
+ this.diagnostics = new DiagnosticCollector(options.language, options.maxDiagnostics);
47
+ }
48
+
49
+ get parts() {
50
+ return this.#source.parts;
51
+ }
52
+
53
+ /** Resolves the version and runs the scan phase when useful. Returns false when loading must not start. */
54
+ async prepare(sinkNeedsRowCounts, scanOnly = false) {
55
+ const verified = this.#options.mode === ReadMode.Verified;
56
+ const wantScan = scanOnly || verified || sinkNeedsRowCounts || this.#source.parts.length > 1;
57
+
58
+ if (wantScan && this.#source.canReadTwice) {
59
+ await this.#scan(verified);
60
+ if (this.diagnostics.hasFatal || (verified && this.diagnostics.hasErrors)) {
61
+ return false;
62
+ }
63
+
64
+ return !scanOnly;
65
+ }
66
+
67
+ if (wantScan) {
68
+ this.diagnostics.add('XAF-S005', Location.NONE);
69
+ }
70
+
71
+ this.validateDuringLoad = verified;
72
+ if (!await this.#resolveFromPeek()) {
73
+ return false;
74
+ }
75
+
76
+ const r = this.resolution;
77
+ this.schema = AuditfileSchema.create(r.version, r.fileNamespace, r.source, r.dialect?.name ?? null, null);
78
+ return !scanOnly;
79
+ }
80
+
81
+ /** The load phase as a stream of rows. Diagnostics are collected along the way. Can be iterated once. */
82
+ async *load() {
83
+ // A second iteration would read the file again and add to the same counts and diagnostics.
84
+ if (this.#loadStarted) {
85
+ throw new Error('The rows of an audit file can be iterated only once; collect them in an array to use them twice.');
86
+ }
87
+
88
+ this.#loadStarted = true;
89
+ if (this.schema === null) {
90
+ return;
91
+ }
92
+
93
+ let document = this.#pending;
94
+ this.#pending = null;
95
+ if (document === null) {
96
+ const stream = new ConcatenatedStream(this.#source.parts, this.#parts.length === 0);
97
+ document = await this.#open(stream);
98
+ if (document === null) {
99
+ return;
100
+ }
101
+ }
102
+
103
+ const { stream, reader } = document;
104
+ try {
105
+ const validate = this.validateDuringLoad ? await this.#validator(stream) : null;
106
+ if (validate === undefined) {
107
+ return;
108
+ }
109
+
110
+ const observers = [new ReferenceChecker(this.schema, this.diagnostics)];
111
+ if (this.resolution.version.type === XAF) {
112
+ observers.push(new XafRules(this.schema, this.diagnostics));
113
+ }
114
+
115
+ const loader = new RowLoader(reader, this.schema, this.resolution, this.#options, this.diagnostics, observers, validate);
116
+ for await (const row of loader.rows()) {
117
+ this.#rowCounts.set(row.table.name, (this.#rowCounts.get(row.table.name) ?? 0) + 1);
118
+ yield row;
119
+ }
120
+
121
+ if (!loader.stopped) {
122
+ for (const observer of observers) {
123
+ observer.onEnd();
124
+ }
125
+
126
+ this.#checkTables(this.#rowCounts);
127
+ }
128
+ } finally {
129
+ await reader.close();
130
+ }
131
+
132
+ if (this.#parts.length === 0) {
133
+ this.#parts = stream.completed;
134
+ }
135
+
136
+ // Without a scan phase a dialect recognized by the software description is only known now; the report names it.
137
+ const r = this.resolution;
138
+ if ((r.dialect?.name ?? null) !== this.schema.dialect) {
139
+ this.schema = AuditfileSchema.create(r.version, r.fileNamespace, r.source, r.dialect?.name ?? null,
140
+ this.schema.hasRowCounts ? this.#rowCountsFromSchema() : null);
141
+ }
142
+
143
+ this.#loaded = true;
144
+ }
145
+
146
+ /** True when the load phase has read the whole file. */
147
+ get loaded() {
148
+ return this.#loaded;
149
+ }
150
+
151
+ createReport(loaded) {
152
+ return new AuditfileReport(this.schema, this.#parts, this.diagnostics.items, this.diagnostics.counts, this.diagnostics.isTruncated,
153
+ this.#loaded ? this.#rowCounts : this.#rowCountsFromSchema(), loaded, now() - this.#started);
154
+ }
155
+
156
+ /** Releases a document kept for loading that will not be loaded after all. */
157
+ async dispose() {
158
+ const pending = this.#pending;
159
+ this.#pending = null;
160
+ await pending?.reader.close();
161
+ }
162
+
163
+ #rowCountsFromSchema() {
164
+ return new Map((this.schema?.tables ?? []).filter((t) => t.rowCount > 0).map((t) => [t.name, t.rowCount]));
165
+ }
166
+
167
+ async #scan(validate) {
168
+ const stream = new ConcatenatedStream(this.#source.parts, true);
169
+ const document = await this.#open(stream);
170
+ if (document === null) {
171
+ return;
172
+ }
173
+
174
+ const { reader } = document;
175
+ try {
176
+ const feed = validate ? await this.#validator(stream) : null;
177
+ if (feed === undefined) {
178
+ return;
179
+ }
180
+
181
+ let counts;
182
+ try {
183
+ counts = await this.#countRows(reader, feed);
184
+ if (counts === null) {
185
+ return;
186
+ }
187
+ } catch (error) {
188
+ if (this.#xmlError(error)) {
189
+ return;
190
+ }
191
+
192
+ throw error;
193
+ }
194
+
195
+ this.#parts = stream.completed;
196
+ this.#checkTables(counts);
197
+ const r = this.resolution;
198
+ this.schema = AuditfileSchema.create(r.version, r.fileNamespace, r.source, r.dialect?.name ?? null, counts);
199
+ } finally {
200
+ await reader.close();
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Walks the document (feeding the XSD validator, when validating) and counts the rows per table.
206
+ * Recognizes a dialect by the software description on the way. Returns null after XAF-X003.
207
+ */
208
+ async #countRows(reader, feed) {
209
+ const r = this.resolution;
210
+ const plan = new MappingPlan(AuditfileSchema.create(r.version, r.fileNamespace, r.source, null, null));
211
+ const softwareNode = plan.root.children.get(r.version.rootElement)?.children.get('header')?.children.get('softwareDesc');
212
+ const counts = new Map(r.version.manifest.tables.map((t) => [t.name, 0]));
213
+ const namespace = r.version.namespace;
214
+ const signal = this.#options.signal;
215
+ const nodes = [];
216
+ let software = null;
217
+ let softwareDepth = -1;
218
+ let events = 0;
219
+
220
+ for (let event = await reader.next(); event !== null; event = reader.poll() ?? await reader.next()) {
221
+ if (events++ % CANCELLATION_INTERVAL === 0) {
222
+ signal?.throwIfAborted();
223
+ }
224
+
225
+ feed?.(event);
226
+
227
+ // The dialect is recognized here already, so the schema handed to the sink names it.
228
+ if (software !== null) {
229
+ if (event.kind === TEXT && software.length <= this.#options.maxValueLength) {
230
+ software += event.text;
231
+ } else if (event.kind === END && event.depth === softwareDepth) {
232
+ r.useSoftware(software, this.#options.applyDialects);
233
+ software = null;
234
+ }
235
+ }
236
+
237
+ if (event.kind !== START) {
238
+ continue;
239
+ }
240
+
241
+ const depth = event.depth;
242
+ if (depth >= this.#options.maxDepth) {
243
+ this.diagnostics.add('XAF-X003', new Location(event.part, event.line, event.column, null), { max: this.#options.maxDepth });
244
+ return null;
245
+ }
246
+
247
+ const parent = depth === 0 ? plan.root : nodes[depth - 1];
248
+ const node = parent != null && event.ns === namespace ? parent.children.get(event.local) ?? null : null;
249
+ nodes[depth] = node;
250
+ if (node?.table != null) {
251
+ const name = node.table.table.name;
252
+ counts.set(name, counts.get(name) + 1);
253
+ } else if (node !== null && node === softwareNode && r.dialect === null) {
254
+ software = '';
255
+ softwareDepth = depth;
256
+ }
257
+ }
258
+
259
+ return counts;
260
+ }
261
+
262
+ /** Reports a well-formedness, encoding or source error; returns false for anything else. */
263
+ #xmlError(error) {
264
+ if (error instanceof SourceError) {
265
+ this.diagnostics.add('XAF-X002', Location.NONE, { part: error.part, detail: error.message });
266
+ return true;
267
+ }
268
+
269
+ if (error instanceof XmlSyntaxError) {
270
+ this.diagnostics.add('XAF-X001', new Location(error.part, error.line, error.column, null), { detail: error.message });
271
+ return true;
272
+ }
273
+
274
+ if (error instanceof XmlEncodingError) {
275
+ this.diagnostics.add('XAF-X004', new Location(error.part, null, null, null), { encoding: error.encoding });
276
+ return true;
277
+ }
278
+
279
+ return false;
280
+ }
281
+
282
+ /**
283
+ * Opens the document and reads up to the root element, resolving the version on first use.
284
+ * Returns the reader positioned before the root element, or null when the file cannot be read.
285
+ */
286
+ async #open(stream) {
287
+ const reader = new XmlReader(stream, { maxCharacters: this.#options.maxCharactersInDocument });
288
+ let root = null;
289
+ try {
290
+ for (let event = await reader.next(); event !== null; event = await reader.next()) {
291
+ if (event.kind === START) {
292
+ root = reader.rootOffset >= PEEK_LIMIT ? null : event;
293
+ break;
294
+ }
295
+
296
+ // Only while the root has not been parsed yet: a source may hand over the whole file at once.
297
+ if (reader.position > PEEK_LIMIT && reader.rootOffset === null) {
298
+ break;
299
+ }
300
+ }
301
+
302
+ if (root === null) {
303
+ if (reader.position > PEEK_LIMIT) {
304
+ this.diagnostics.add('XAF-X002', Location.NONE, { part: this.#source.parts[0].name, detail: 'The root element was not found within the first megabyte.' });
305
+ } else {
306
+ this.diagnostics.add('XAF-X001', new Location(1, null, null, null), { detail: 'Root element is missing.' });
307
+ }
308
+ }
309
+ } catch (error) {
310
+ // A source that cannot be read is a finding (X002); a wrong argument is the caller's error.
311
+ if (!this.#xmlError(error)) {
312
+ await reader.close();
313
+ throw error;
314
+ }
315
+ }
316
+
317
+ if (root === null) {
318
+ await reader.close();
319
+ return null;
320
+ }
321
+
322
+ if (this.resolution === null) {
323
+ this.resolution = resolveVersion(root.local, root.ns, this.#options, this.diagnostics);
324
+ if (this.resolution === null) {
325
+ await reader.close();
326
+ return null;
327
+ }
328
+ }
329
+
330
+ if (this.resolution.needsMapping) {
331
+ reader.mapNamespace(this.resolution.fileNamespace, this.resolution.version.namespace);
332
+ }
333
+
334
+ reader.pushBack(root);
335
+ return { stream, reader };
336
+ }
337
+
338
+ async #resolveFromPeek() {
339
+ if (this.resolution !== null) {
340
+ return true;
341
+ }
342
+
343
+ const stream = new ConcatenatedStream(this.#source.parts, !this.#source.canReadTwice);
344
+ const document = await this.#open(stream);
345
+ if (document === null || this.#source.canReadTwice) {
346
+ await document?.reader.close();
347
+ return this.resolution !== null;
348
+ }
349
+
350
+ this.#pending = document;
351
+ return true;
352
+ }
353
+
354
+ /**
355
+ * The XSD validator as an event consumer that reports XAF-D001.
356
+ * Returns undefined (after XAF-X002) when a custom XSD cannot be read or compiled.
357
+ */
358
+ async #validator(stream) {
359
+ const version = this.resolution.version;
360
+ let set;
361
+ if (this.#options.xsd === null) {
362
+ set = version.createSchemaSet();
363
+ } else {
364
+ try {
365
+ const bytes = await readAll(this.#options.xsd());
366
+ if (Sha256.hex(bytes) !== version.manifest.xsdSha256) {
367
+ this.diagnostics.add('XAF-D002', Location.NONE, { version: version.key });
368
+ }
369
+
370
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
371
+ set = SchemaSet.compile('custom.xsd', (name) => (name === 'custom.xsd' ? text : undefined));
372
+ } catch (error) {
373
+ this.diagnostics.add('XAF-X002', Location.NONE, { part: 'XSD', detail: error?.message ?? String(error) });
374
+ return undefined;
375
+ }
376
+ }
377
+
378
+ // Keys and references are checked by the ReferenceChecker (XAF-R003, XAF-R004) from the manifest, which holds every
379
+ // constraint of the XSD. The XSD's own identity constraints use XPaths without a namespace prefix, so in a namespaced
380
+ // document they never match; evaluating them only costs time.
381
+ let part = stream.part;
382
+ const validator = new XsdValidator(set, (message, line, column) => {
383
+ this.diagnostics.add('XAF-D001', new Location(part, line, column, null), { detail: message });
384
+ }, this.#options.maxValueLength, false);
385
+
386
+ return (event) => {
387
+ part = event.part;
388
+ switch (event.kind) {
389
+ case START:
390
+ validator.start(event);
391
+ break;
392
+ case END:
393
+ validator.end(event);
394
+ break;
395
+ case TEXT:
396
+ validator.text(event);
397
+ break;
398
+ }
399
+ };
400
+ }
401
+
402
+ #checkTables(counts) {
403
+ const manifest = this.resolution.version.manifest;
404
+ for (const table of manifest.requiredTables.filter((t) => !(counts.get(t) > 0))) {
405
+ this.diagnostics.add('XAF-S001', Location.NONE, { table });
406
+ }
407
+
408
+ for (const table of manifest.expectedTables.filter((t) => !(counts.get(t) > 0))) {
409
+ this.diagnostics.add('XAF-S002', Location.NONE, { table });
410
+ }
411
+ }
412
+ }
413
+
414
+ /** Reads a custom XSD completely, refusing absurd sizes. */
415
+ async function readAll(stream) {
416
+ const source = await stream;
417
+ if (typeof source === 'string') {
418
+ return new TextEncoder().encode(source);
419
+ }
420
+
421
+ const iterator = iterate(source, false);
422
+ const chunks = [];
423
+ let length = 0;
424
+ try {
425
+ for (let result = await iterator.next(); !result.done; result = await iterator.next()) {
426
+ const chunk = new Uint8Array(result.value);
427
+ length += chunk.length;
428
+ if (length > MAX_XSD_SIZE) {
429
+ throw new RangeError('The XSD is larger than 16 MB.');
430
+ }
431
+
432
+ chunks.push(chunk);
433
+ }
434
+ } finally {
435
+ await iterator.return?.();
436
+ }
437
+
438
+ const bytes = new Uint8Array(length);
439
+ let offset = 0;
440
+ for (const chunk of chunks) {
441
+ bytes.set(chunk, offset);
442
+ offset += chunk.length;
443
+ }
444
+
445
+ return bytes;
446
+ }