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

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,121 @@
1
+ // Translates an XSD pattern facet to a JavaScript regular expression.
2
+ // XSD patterns are always anchored and have no ^/$ anchors; unsupported constructs are refused, never approximated.
3
+
4
+ const SYNTAX = new Set(['\\', '|', '.', '-', '^', '?', '*', '+', '{', '}', '(', ')', '[', ']', '$']);
5
+ const CATEGORY = /^(?:[LMNPSZC][a-z]?)$/;
6
+
7
+ /**
8
+ * @param {string} pattern
9
+ * @returns {RegExp}
10
+ * @throws {Error} The pattern uses a construct this validator does not support.
11
+ */
12
+ export function translatePattern(pattern) {
13
+ let out = '';
14
+ let inClass = false;
15
+ for (let i = 0; i < pattern.length; i++) {
16
+ const c = pattern[i];
17
+ if (c === '\\') {
18
+ const n = pattern[++i];
19
+ switch (n) {
20
+ case 'd':
21
+ out += '\\p{Nd}';
22
+ break;
23
+ case 'D':
24
+ out += '\\P{Nd}';
25
+ break;
26
+ case 's':
27
+ out += inClass ? ' \\t\\n\\r' : '[ \\t\\n\\r]';
28
+ break;
29
+ case 'S':
30
+ if (inClass) {
31
+ throw unsupported(pattern, '\\S inside a character class');
32
+ }
33
+
34
+ out += '[^ \\t\\n\\r]';
35
+ break;
36
+ case 'n':
37
+ case 'r':
38
+ case 't':
39
+ out += `\\${n}`;
40
+ break;
41
+ case 'p':
42
+ case 'P': {
43
+ const end = pattern.indexOf('}', i);
44
+ const name = pattern[i + 1] === '{' && end > i ? pattern.slice(i + 2, end) : '';
45
+ if (!CATEGORY.test(name)) {
46
+ throw unsupported(pattern, `\\${n}{${name}}`);
47
+ }
48
+
49
+ out += `\\${n}{${name}}`;
50
+ i = end;
51
+ break;
52
+ }
53
+
54
+ default:
55
+ if (n === undefined || !SYNTAX.has(n)) {
56
+ throw unsupported(pattern, `\\${n ?? ''}`);
57
+ }
58
+
59
+ // In a JavaScript unicode pattern "\-" is valid only inside a class.
60
+ out += n === '-' && !inClass ? '-' : `\\${n}`;
61
+ }
62
+
63
+ continue;
64
+ }
65
+
66
+ if (inClass) {
67
+ if (c === '[') {
68
+ throw unsupported(pattern, 'character class subtraction');
69
+ }
70
+
71
+ if (c === ']') {
72
+ inClass = false;
73
+ }
74
+
75
+ out += c;
76
+ continue;
77
+ }
78
+
79
+ switch (c) {
80
+ case '[':
81
+ inClass = true;
82
+ out += c;
83
+ if (pattern[i + 1] === '^') {
84
+ out += '^';
85
+ i++;
86
+ }
87
+
88
+ break;
89
+ case '^':
90
+ case '$':
91
+ out += `\\${c}`;
92
+ break;
93
+ case '.':
94
+ out += '[^\\n\\r]';
95
+ break;
96
+ case '(':
97
+ if (pattern[i + 1] === '?') {
98
+ throw unsupported(pattern, '(?');
99
+ }
100
+
101
+ out += c;
102
+ break;
103
+ default:
104
+ out += c;
105
+ }
106
+ }
107
+
108
+ if (inClass) {
109
+ throw unsupported(pattern, 'an unclosed character class');
110
+ }
111
+
112
+ try {
113
+ return new RegExp(`^(?:${out})$`, 'u');
114
+ } catch (error) {
115
+ throw new Error(`XSD pattern '${pattern}' is not valid: ${error.message}`);
116
+ }
117
+ }
118
+
119
+ function unsupported(pattern, construct) {
120
+ return new Error(`XSD pattern '${pattern}' uses ${construct}, which this validator does not support.`);
121
+ }
@@ -0,0 +1,555 @@
1
+ import { SaxesParser } from 'saxes';
2
+ import { Decimal } from '../internal/decimal.js';
3
+ import { BUILTINS } from './datatypes.js';
4
+ import { translatePattern } from './regex.js';
5
+
6
+ // Compiles the XSD subset used by auditfile-spec: global and local elements, sequences, attributes,
7
+ // simple types restricted by facets, and key/keyref/unique. Anything else is refused with a clear error,
8
+ // so a custom XSD is never validated half.
9
+
10
+ export const XS = 'http://www.w3.org/2001/XMLSchema';
11
+
12
+ /** @param {string} ns @param {string} local */
13
+ export const qualified = (ns, local) => `{${ns}}${local}`;
14
+
15
+ /** Accepts any content; used for elements without a type. */
16
+ export const ANY_TYPE = Object.freeze({ kind: 'any' });
17
+
18
+ /** A light element tree of an XSD document. */
19
+ function parseTree(text, name) {
20
+ const parser = new SaxesParser({ xmlns: true, position: true, forceXMLVersion: true, defaultXMLVersion: '1.0' });
21
+ const stack = [];
22
+ let root = null;
23
+ parser.on('doctype', () => {
24
+ throw new Error(`${name}: a DTD is not allowed.`);
25
+ });
26
+ parser.on('error', (error) => {
27
+ throw new Error(`${name}: ${error.message}`);
28
+ });
29
+ parser.on('opentag', (tag) => {
30
+ const parent = stack.at(-1);
31
+ const bindings = new Map(parent?.bindings ?? [['xml', 'http://www.w3.org/XML/1998/namespace']]);
32
+ for (const [prefix, uri] of Object.entries(tag.ns ?? {})) {
33
+ bindings.set(prefix, uri);
34
+ }
35
+
36
+ const attributes = new Map();
37
+ for (const attribute of Object.values(tag.attributes)) {
38
+ if (attribute.uri === '') {
39
+ attributes.set(attribute.local, attribute.value);
40
+ }
41
+ }
42
+
43
+ const node = { ns: tag.uri, local: tag.local, attributes, bindings, children: [], line: parser.line };
44
+ if (parent === undefined) {
45
+ root = node;
46
+ } else {
47
+ parent.children.push(node);
48
+ }
49
+
50
+ stack.push(node);
51
+ });
52
+ parser.on('closetag', () => {
53
+ stack.pop();
54
+ });
55
+ parser.write(text).close();
56
+ return root;
57
+ }
58
+
59
+ /** One XSD document: its tree, target namespace and form defaults. */
60
+ class SchemaDocument {
61
+ constructor(name, root) {
62
+ if (root === null || root.ns !== XS || root.local !== 'schema') {
63
+ throw new Error(`${name} is not an XSD schema.`);
64
+ }
65
+
66
+ this.name = name;
67
+ this.root = root;
68
+ this.targetNamespace = root.attributes.get('targetNamespace') ?? '';
69
+ this.elementQualified = root.attributes.get('elementFormDefault') === 'qualified';
70
+ this.attributeQualified = root.attributes.get('attributeFormDefault') === 'qualified';
71
+ }
72
+ }
73
+
74
+ function fail(document, node, message) {
75
+ return new Error(`${document.name} (line ${node.line}): ${message}`);
76
+ }
77
+
78
+ /** XS children of a node, without annotations. */
79
+ function children(document, node) {
80
+ return node.children.filter((child) => {
81
+ if (child.ns !== XS) {
82
+ throw fail(document, child, `unexpected element {${child.ns}}${child.local}.`);
83
+ }
84
+
85
+ return child.local !== 'annotation';
86
+ });
87
+ }
88
+
89
+ function allow(document, node, names) {
90
+ for (const name of node.attributes.keys()) {
91
+ if (!names.includes(name)) {
92
+ throw fail(document, node, `attribute '${name}' on xs:${node.local} is not supported by this validator.`);
93
+ }
94
+ }
95
+ }
96
+
97
+ function resolveQName(document, node, value) {
98
+ const colon = value.indexOf(':');
99
+ const prefix = colon < 0 ? '' : value.slice(0, colon);
100
+ const local = colon < 0 ? value : value.slice(colon + 1);
101
+ const ns = node.bindings.get(prefix);
102
+ if (ns === undefined) {
103
+ if (prefix === '') {
104
+ return { ns: '', local };
105
+ }
106
+
107
+ throw fail(document, node, `prefix '${prefix}' is not declared.`);
108
+ }
109
+
110
+ return { ns, local };
111
+ }
112
+
113
+ function occurs(document, node, name, fallback) {
114
+ const value = node.attributes.get(name);
115
+ if (value === undefined) {
116
+ return fallback;
117
+ }
118
+
119
+ if (value === 'unbounded' && name === 'maxOccurs') {
120
+ return Infinity;
121
+ }
122
+
123
+ if (!/^\d+$/.test(value)) {
124
+ throw fail(document, node, `${name}="${value}" is not valid.`);
125
+ }
126
+
127
+ return Number(value);
128
+ }
129
+
130
+ /** Normalizes "a/./b/../c" style names relative to the including document. */
131
+ function resolveLocation(base, location) {
132
+ if (/^[a-z][a-z0-9+.-]*:/i.test(location) || location.startsWith('/')) {
133
+ throw new Error(`${base}: schemaLocation '${location}' must be relative; only embedded definitions are allowed.`);
134
+ }
135
+
136
+ const segments = base.split('/').slice(0, -1);
137
+ for (const segment of location.split('/')) {
138
+ if (segment === '..') {
139
+ if (segments.length === 0) {
140
+ throw new Error(`${base}: schemaLocation '${location}' points outside the definitions.`);
141
+ }
142
+
143
+ segments.pop();
144
+ } else if (segment !== '.' && segment !== '') {
145
+ segments.push(segment);
146
+ }
147
+ }
148
+
149
+ return segments.join('/');
150
+ }
151
+
152
+ /** A compiled XSD: the global element declarations of all its documents. */
153
+ export class SchemaSet {
154
+ /** @type {Map<string, object>} Global element declarations by "{ns}local". */
155
+ elements = new Map();
156
+ /** @type {Map<string, { document: SchemaDocument, node: object }>} */
157
+ #simpleTypes = new Map();
158
+ /** @type {Map<string, { document: SchemaDocument, node: object }>} */
159
+ #complexTypes = new Map();
160
+ /** @type {Map<string, object>} */
161
+ #compiled = new Map();
162
+ /** Number of schema documents. */
163
+ count = 0;
164
+
165
+ /**
166
+ * @param {string} mainName
167
+ * @param {(name: string) => string | undefined} load Returns the text of a schema document, or undefined.
168
+ */
169
+ static compile(mainName, load) {
170
+ const set = new SchemaSet();
171
+ const documents = [];
172
+ const loaded = new Set();
173
+ const queue = [mainName];
174
+ while (queue.length > 0) {
175
+ const name = queue.shift();
176
+ if (loaded.has(name)) {
177
+ continue;
178
+ }
179
+
180
+ loaded.add(name);
181
+ const text = load(name);
182
+ if (text === undefined) {
183
+ throw new Error(`Refused to resolve '${name}': only embedded definitions are allowed.`);
184
+ }
185
+
186
+ const document = new SchemaDocument(name, parseTree(text, name));
187
+ documents.push(document);
188
+ for (const node of children(document, document.root)) {
189
+ if (node.local === 'import' || node.local === 'include') {
190
+ const location = node.attributes.get('schemaLocation');
191
+ if (location === undefined) {
192
+ throw fail(document, node, `xs:${node.local} without schemaLocation is not supported.`);
193
+ }
194
+
195
+ queue.push(resolveLocation(name, location));
196
+ }
197
+ }
198
+ }
199
+
200
+ for (const document of documents) {
201
+ for (const node of children(document, document.root)) {
202
+ const key = qualified(document.targetNamespace, node.attributes.get('name') ?? '');
203
+ switch (node.local) {
204
+ case 'simpleType':
205
+ set.#simpleTypes.set(key, { document, node });
206
+ break;
207
+ case 'complexType':
208
+ set.#complexTypes.set(key, { document, node });
209
+ break;
210
+ case 'element':
211
+ case 'import':
212
+ case 'include':
213
+ break;
214
+ default:
215
+ throw fail(document, node, `xs:${node.local} is not supported by this validator.`);
216
+ }
217
+ }
218
+ }
219
+
220
+ for (const document of documents) {
221
+ for (const node of children(document, document.root)) {
222
+ if (node.local === 'element') {
223
+ const element = set.#element(document, node, true);
224
+ set.elements.set(qualified(element.ns, element.local), element);
225
+ }
226
+ }
227
+ }
228
+
229
+ set.count = documents.length;
230
+ return Object.freeze(set);
231
+ }
232
+
233
+ #element(document, node, global) {
234
+ allow(document, node, global ? ['name', 'type', 'id'] : ['name', 'type', 'minOccurs', 'maxOccurs', 'form', 'id']);
235
+ const local = node.attributes.get('name');
236
+ if (local === undefined) {
237
+ throw fail(document, node, 'xs:element without a name (ref) is not supported by this validator.');
238
+ }
239
+
240
+ const form = node.attributes.get('form');
241
+ const ns = global || (form === undefined ? document.elementQualified : form === 'qualified') ? document.targetNamespace : '';
242
+ const element = {
243
+ ns,
244
+ local,
245
+ min: global ? 1 : occurs(document, node, 'minOccurs', 1),
246
+ max: global ? 1 : occurs(document, node, 'maxOccurs', 1),
247
+ type: ANY_TYPE,
248
+ constraints: [],
249
+ };
250
+
251
+ const typeName = node.attributes.get('type');
252
+ let anonymous = null;
253
+ for (const child of children(document, node)) {
254
+ switch (child.local) {
255
+ case 'simpleType':
256
+ case 'complexType':
257
+ anonymous = child;
258
+ break;
259
+ case 'key':
260
+ case 'keyref':
261
+ case 'unique':
262
+ element.constraints.push(this.#constraint(document, child));
263
+ break;
264
+ default:
265
+ throw fail(document, child, `xs:${child.local} in xs:element is not supported by this validator.`);
266
+ }
267
+ }
268
+
269
+ if (typeName !== undefined) {
270
+ element.type = this.#typeByName(document, node, typeName);
271
+ } else if (anonymous !== null) {
272
+ element.type = anonymous.local === 'simpleType'
273
+ ? this.#simpleType(document, anonymous)
274
+ : this.#complexType(document, anonymous);
275
+ }
276
+
277
+ return element;
278
+ }
279
+
280
+ #typeByName(document, node, value) {
281
+ const { ns, local } = resolveQName(document, node, value);
282
+ if (ns === XS) {
283
+ if (local === 'anyType') {
284
+ return ANY_TYPE;
285
+ }
286
+
287
+ return this.#builtin(document, node, local);
288
+ }
289
+
290
+ const key = qualified(ns, local);
291
+ if (this.#compiled.has(key)) {
292
+ const compiled = this.#compiled.get(key);
293
+ if (compiled === null) {
294
+ throw fail(document, node, `type '${value}' is defined in terms of itself.`);
295
+ }
296
+
297
+ return compiled;
298
+ }
299
+
300
+ const simple = this.#simpleTypes.get(key);
301
+ const complex = this.#complexTypes.get(key);
302
+ if (simple === undefined && complex === undefined) {
303
+ throw fail(document, node, `type '${value}' is not defined.`);
304
+ }
305
+
306
+ this.#compiled.set(key, null);
307
+ const type = simple !== undefined
308
+ ? this.#simpleType(simple.document, simple.node, local)
309
+ : this.#complexType(complex.document, complex.node);
310
+ this.#compiled.set(key, type);
311
+ return type;
312
+ }
313
+
314
+ #builtin(document, node, local) {
315
+ const builtin = BUILTINS.get(local);
316
+ if (builtin === undefined) {
317
+ throw fail(document, node, `built-in type xs:${local} is not supported by this validator.`);
318
+ }
319
+
320
+ return { kind: 'simple', name: `xs:${local}`, builtin, base: null, whiteSpace: builtin.whiteSpace, facets: null };
321
+ }
322
+
323
+ #simpleType(document, node, name = null) {
324
+ allow(document, node, ['name', 'id']);
325
+ const parts = children(document, node);
326
+ if (parts.length !== 1 || parts[0].local !== 'restriction') {
327
+ throw fail(document, node, 'only xs:simpleType with xs:restriction is supported by this validator.');
328
+ }
329
+
330
+ const restriction = parts[0];
331
+ allow(document, restriction, ['base', 'id']);
332
+ let base;
333
+ const facetNodes = [];
334
+ for (const child of children(document, restriction)) {
335
+ if (child.local === 'simpleType') {
336
+ base = this.#simpleType(document, child);
337
+ } else {
338
+ facetNodes.push(child);
339
+ }
340
+ }
341
+
342
+ const baseName = restriction.attributes.get('base');
343
+ if (baseName !== undefined) {
344
+ base = this.#typeByName(document, restriction, baseName);
345
+ }
346
+
347
+ if (base === undefined || base.kind !== 'simple') {
348
+ throw fail(document, restriction, 'xs:restriction needs a simple base type.');
349
+ }
350
+
351
+ const facets = { enumeration: null, patterns: [], length: null, minLength: null, maxLength: null, totalDigits: null,
352
+ fractionDigits: null, minInclusive: null, maxInclusive: null, minExclusive: null, maxExclusive: null };
353
+ let whiteSpace = base.whiteSpace;
354
+ for (const facet of facetNodes) {
355
+ allow(document, facet, ['value', 'fixed', 'id']);
356
+ const value = facet.attributes.get('value');
357
+ if (value === undefined) {
358
+ throw fail(document, facet, `xs:${facet.local} needs a value.`);
359
+ }
360
+
361
+ switch (facet.local) {
362
+ case 'enumeration':
363
+ (facets.enumeration ??= []).push(value);
364
+ break;
365
+ case 'pattern':
366
+ try {
367
+ facets.patterns.push({ source: value, regex: translatePattern(value) });
368
+ } catch (error) {
369
+ throw fail(document, facet, error.message);
370
+ }
371
+
372
+ break;
373
+ case 'length':
374
+ case 'minLength':
375
+ case 'maxLength':
376
+ case 'totalDigits':
377
+ case 'fractionDigits':
378
+ if (!/^\d+$/.test(value)) {
379
+ throw fail(document, facet, `xs:${facet.local} value '${value}' is not a non-negative integer.`);
380
+ }
381
+
382
+ facets[facet.local] = Number(value);
383
+ break;
384
+ case 'minInclusive':
385
+ case 'maxInclusive':
386
+ case 'minExclusive':
387
+ case 'maxExclusive': {
388
+ const bound = base.builtin.numeric ? base.builtin.parse(value.trim()) : null;
389
+ if (!(bound instanceof Decimal)) {
390
+ throw fail(document, facet, `xs:${facet.local} is supported on numeric types only.`);
391
+ }
392
+
393
+ facets[facet.local] = bound;
394
+ break;
395
+ }
396
+
397
+ case 'whiteSpace':
398
+ if (!['preserve', 'replace', 'collapse'].includes(value)) {
399
+ throw fail(document, facet, `xs:whiteSpace value '${value}' is not valid.`);
400
+ }
401
+
402
+ whiteSpace = value;
403
+ break;
404
+ default:
405
+ throw fail(document, facet, `facet xs:${facet.local} is not supported by this validator.`);
406
+ }
407
+ }
408
+
409
+ if (facets.enumeration !== null && base.builtin.numeric) {
410
+ facets.enumeration = facets.enumeration.map((value) => base.builtin.parse(value.trim()) ?? value);
411
+ }
412
+
413
+ return { kind: 'simple', name: name ?? base.name, builtin: base.builtin, base, whiteSpace, facets };
414
+ }
415
+
416
+ #complexType(document, node) {
417
+ allow(document, node, ['name', 'mixed', 'id']);
418
+ if (node.attributes.has('mixed') && node.attributes.get('mixed') !== 'false') {
419
+ throw fail(document, node, 'mixed content is not supported by this validator.');
420
+ }
421
+
422
+ const type = { kind: 'complex', particles: [], attributes: new Map() };
423
+ let sequences = 0;
424
+ for (const child of children(document, node)) {
425
+ switch (child.local) {
426
+ case 'sequence':
427
+ if (++sequences > 1) {
428
+ throw fail(document, child, 'more than one xs:sequence.');
429
+ }
430
+
431
+ allow(document, child, ['id']);
432
+ for (const particle of children(document, child)) {
433
+ if (particle.local !== 'element') {
434
+ throw fail(document, particle, `xs:${particle.local} in xs:sequence is not supported by this validator.`);
435
+ }
436
+
437
+ type.particles.push(this.#element(document, particle, false));
438
+ }
439
+
440
+ break;
441
+ case 'attribute': {
442
+ allow(document, child, ['name', 'type', 'use', 'form', 'id']);
443
+ const local = child.attributes.get('name');
444
+ if (local === undefined) {
445
+ throw fail(document, child, 'xs:attribute without a name (ref) is not supported by this validator.');
446
+ }
447
+
448
+ const form = child.attributes.get('form');
449
+ const ns = (form === undefined ? document.attributeQualified : form === 'qualified') ? document.targetNamespace : '';
450
+ const use = child.attributes.get('use') ?? 'optional';
451
+ const inner = children(document, child).find((c) => c.local === 'simpleType');
452
+ const typeName = child.attributes.get('type');
453
+ const attributeType = typeName !== undefined
454
+ ? this.#typeByName(document, child, typeName)
455
+ : inner !== undefined ? this.#simpleType(document, inner) : this.#builtin(document, child, 'anySimpleType');
456
+ if (attributeType.kind !== 'simple') {
457
+ throw fail(document, child, 'an attribute needs a simple type.');
458
+ }
459
+
460
+ if (use !== 'prohibited') {
461
+ type.attributes.set(qualified(ns, local), { local, type: attributeType, required: use === 'required' });
462
+ }
463
+
464
+ break;
465
+ }
466
+
467
+ default:
468
+ throw fail(document, child, `xs:${child.local} in xs:complexType is not supported by this validator.`);
469
+ }
470
+ }
471
+
472
+ return type;
473
+ }
474
+
475
+ #constraint(document, node) {
476
+ allow(document, node, ['name', 'refer', 'id']);
477
+ const kind = node.local;
478
+ const name = node.attributes.get('name');
479
+ let selector = null;
480
+ const fields = [];
481
+ for (const child of children(document, node)) {
482
+ allow(document, child, ['xpath', 'id']);
483
+ const xpath = child.attributes.get('xpath');
484
+ if (xpath === undefined || (child.local !== 'selector' && child.local !== 'field')) {
485
+ throw fail(document, child, `xs:${child.local} in xs:${kind} is not supported by this validator.`);
486
+ }
487
+
488
+ if (child.local === 'selector') {
489
+ selector = xpath.split('|').map((path) => this.#path(document, child, path, false));
490
+ } else {
491
+ fields.push(this.#path(document, child, xpath, true));
492
+ }
493
+ }
494
+
495
+ if (name === undefined || selector === null || fields.length === 0) {
496
+ throw fail(document, node, `xs:${kind} needs a name, a selector and at least one field.`);
497
+ }
498
+
499
+ let refer = null;
500
+ if (kind === 'keyref') {
501
+ const value = node.attributes.get('refer');
502
+ if (value === undefined) {
503
+ throw fail(document, node, 'xs:keyref needs refer.');
504
+ }
505
+
506
+ const { ns, local } = resolveQName(document, node, value);
507
+ refer = qualified(ns, local);
508
+ }
509
+
510
+ return { kind, name: qualified(document.targetNamespace, name), displayName: name, selector, fields, refer };
511
+ }
512
+
513
+ /** Parses a restricted XPath: "./a/b", "a/b", "@x", "a/@x" or ".". Steps match {ns}local or *. */
514
+ #path(document, node, xpath, field) {
515
+ const steps = [];
516
+ let attribute = null;
517
+ const parts = xpath.trim().split('/').map((p) => p.trim());
518
+ parts.forEach((part, i) => {
519
+ if (part === '.' && i === 0) {
520
+ return;
521
+ }
522
+
523
+ if (part === '' || part.includes('::') || part.includes('[') || part === '..' || part === '.') {
524
+ throw fail(document, node, `XPath '${xpath}' is not supported by this validator.`);
525
+ }
526
+
527
+ if (part.startsWith('@')) {
528
+ if (!field || i !== parts.length - 1) {
529
+ throw fail(document, node, `XPath '${xpath}' is not supported by this validator.`);
530
+ }
531
+
532
+ attribute = this.#nameTest(document, node, part.slice(1));
533
+ return;
534
+ }
535
+
536
+ steps.push(this.#nameTest(document, node, part));
537
+ });
538
+
539
+ return { steps, attribute };
540
+ }
541
+
542
+ #nameTest(document, node, test) {
543
+ if (test === '*') {
544
+ return null;
545
+ }
546
+
547
+ // XPath 1.0: an unprefixed name has no namespace; the default namespace does not apply.
548
+ if (!test.includes(':')) {
549
+ return qualified('', test);
550
+ }
551
+
552
+ const { ns, local } = resolveQName(document, node, test);
553
+ return qualified(ns, local);
554
+ }
555
+ }