fast-xml-validator 1.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/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "fast-xml-validator",
3
+ "version": "1.0.0",
4
+ "description": "Validate XML for Syntax and Business rules without C/C++ based libraries",
5
+ "main": "./lib/fxv.cjs",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "module": "./src/fxv.js",
9
+ "types": "./src/fxv.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": {
13
+ "types": "./src/fxv.d.ts",
14
+ "default": "./src/fxv.js"
15
+ },
16
+ "require": {
17
+ "types": "./lib/fxv.d.cts",
18
+ "default": "./lib/fxv.cjs"
19
+ }
20
+ }
21
+ },
22
+ "scripts": {
23
+ "test": "c8 --reporter=lcov --reporter=text jasmine specs/*spec.js",
24
+ "bundle": "webpack --config webpack.cjs.config.js"
25
+ },
26
+ "files": [
27
+ "lib",
28
+ "src",
29
+ "CHANGELOG.md"
30
+ ],
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/NaturalIntelligence/fast-xml-validator.git"
34
+ },
35
+ "keywords": [
36
+ "fast",
37
+ "xml",
38
+ "validator",
39
+ "validate",
40
+ "syntax",
41
+ "rules",
42
+ "business",
43
+ "schema"
44
+ ],
45
+ "author": "Amit Gupta (https://solothought.com)",
46
+ "license": "MIT",
47
+ "devDependencies": {
48
+ "@babel/core": "^7.13.10",
49
+ "@babel/plugin-transform-runtime": "^7.13.10",
50
+ "@babel/preset-env": "^7.13.10",
51
+ "@babel/register": "^7.13.8",
52
+ "@types/node": "20",
53
+ "babel-loader": "^8.2.2",
54
+ "c8": "^10.1.3",
55
+ "eslint": "^8.3.0",
56
+ "jasmine": "^5.6.0",
57
+ "typescript": "5",
58
+ "webpack": "^5.64.4",
59
+ "webpack-cli": "^4.9.1"
60
+ },
61
+ "funding": [
62
+ {
63
+ "type": "github",
64
+ "url": "https://github.com/sponsors/NaturalIntelligence"
65
+ }
66
+ ],
67
+ "dependencies": {
68
+ "detailed-xml-validator": "^2.1.0"
69
+ }
70
+ }
@@ -0,0 +1,279 @@
1
+ import { isName } from './util.js';
2
+
3
+ const defaultOptions = {
4
+ maxEntityCount: Infinity,
5
+ maxEntitySize: Infinity,
6
+ };
7
+
8
+ export default class DocTypeValidator {
9
+ constructor(options) {
10
+ this.options = Object.assign({}, defaultOptions, options);
11
+ this.suppressValidationErr = !options;
12
+ }
13
+
14
+ validateDocType(xmlData, i) {
15
+ if (
16
+ xmlData[i + 3] !== 'O' ||
17
+ xmlData[i + 4] !== 'C' ||
18
+ xmlData[i + 5] !== 'T' ||
19
+ xmlData[i + 6] !== 'Y' ||
20
+ xmlData[i + 7] !== 'P' ||
21
+ xmlData[i + 8] !== 'E'
22
+ ) {
23
+ throw new Error(`Invalid Tag instead of DOCTYPE`);
24
+ }
25
+
26
+ i += 9;
27
+ let angleBracketsCount = 1;
28
+ let hasBody = false;
29
+ let comment = false;
30
+ let entityCount = 0;
31
+
32
+ for (; i < xmlData.length; i++) {
33
+ if (xmlData[i] === '<' && !comment) {
34
+ if (hasBody && hasSeq(xmlData, '!ENTITY', i)) {
35
+ i += 7;
36
+ [entityCount, i] = this._validateEntityExp(xmlData, i + 1, entityCount);
37
+ // continue;
38
+ } else if (hasBody && hasSeq(xmlData, '!ELEMENT', i)) {
39
+ i += 8;
40
+ i = this._validateElementExp(xmlData, i + 1);
41
+ // continue;
42
+ } else if (hasBody && hasSeq(xmlData, '!ATTLIST', i)) {
43
+ i += 8;
44
+ i = this._validateAttlistExp(xmlData, i + 1);
45
+ continue;
46
+ } else if (hasBody && hasSeq(xmlData, '!NOTATION', i)) {
47
+ i += 9;
48
+ i = this._validateNotationExp(xmlData, i + 1);
49
+ // continue;
50
+ } else if (hasSeq(xmlData, '!--', i)) {
51
+ comment = true;
52
+ } else {
53
+ throw new Error(`Invalid DOCTYPE`);
54
+ }
55
+ angleBracketsCount++;
56
+ } else if (xmlData[i] === '>') {
57
+ if (comment) {
58
+ if (xmlData[i - 1] === '-' && xmlData[i - 2] === '-') {
59
+ comment = false;
60
+ angleBracketsCount--;
61
+ }
62
+ } else {
63
+ angleBracketsCount--;
64
+ }
65
+ if (angleBracketsCount === 0) break;
66
+ } else if (xmlData[i] === '[') {
67
+ hasBody = true;
68
+ }
69
+ }
70
+
71
+ if (angleBracketsCount !== 0) {
72
+ throw new Error(`Unclosed DOCTYPE`);
73
+ }
74
+
75
+ return i;
76
+ }
77
+
78
+ _validateEntityExp(xmlData, i, entityCount) {
79
+ i = skipWhitespace(xmlData, i);
80
+
81
+ // Read and validate entity name
82
+ const nameStart = i;
83
+ while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") {
84
+ i++;
85
+ }
86
+ const entityName = xmlData.substring(nameStart, i);
87
+ validateName(entityName);
88
+
89
+ i = skipWhitespace(xmlData, i);
90
+
91
+ // Check for unsupported constructs
92
+ if (!this.suppressValidationErr) {
93
+ if (xmlData.substring(i, i + 6).toUpperCase() === 'SYSTEM') {
94
+ throw new Error('External entities are not supported');
95
+ } else if (xmlData[i] === '%') {
96
+ throw new Error('Parameter entities are not supported');
97
+ }
98
+ }
99
+
100
+ // Read and validate the entity value (validates quoting + size limit)
101
+ let entityValue;
102
+ [i, entityValue] = readQuotedValue(xmlData, i, 'entity');
103
+
104
+ const { maxEntitySize, maxEntityCount } = this.options;
105
+
106
+ if (entityValue.length > maxEntitySize) {
107
+ throw new Error(
108
+ `Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${maxEntitySize})`
109
+ );
110
+ }
111
+
112
+ // Only count non-parameter entities (parameter entities contain '&')
113
+ if (entityValue.indexOf('&') === -1) {
114
+ entityCount++;
115
+ if (entityCount > maxEntityCount) {
116
+ throw new Error(
117
+ `Entity count (${entityCount}) exceeds maximum allowed (${maxEntityCount})`
118
+ );
119
+ }
120
+ }
121
+
122
+ return [entityCount, i - 1];
123
+ }
124
+
125
+ _validateNotationExp(xmlData, i) {
126
+ i = skipWhitespace(xmlData, i);
127
+
128
+ // Read and validate notation name
129
+ const nameStart = i;
130
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
131
+ const notationName = xmlData.substring(nameStart, i);
132
+ !this.suppressValidationErr && validateName(notationName);
133
+
134
+ i = skipWhitespace(xmlData, i);
135
+
136
+ const identifierType = xmlData.substring(i, i + 6).toUpperCase();
137
+ if (!this.suppressValidationErr && identifierType !== 'SYSTEM' && identifierType !== 'PUBLIC') {
138
+ throw new Error(`Expected SYSTEM or PUBLIC, found "${identifierType}"`);
139
+ }
140
+ i += identifierType.length;
141
+ i = skipWhitespace(xmlData, i);
142
+
143
+ if (identifierType === 'PUBLIC') {
144
+ [i] = readQuotedValue(xmlData, i, 'publicIdentifier');
145
+ i = skipWhitespace(xmlData, i);
146
+ if (xmlData[i] === '"' || xmlData[i] === "'") {
147
+ [i] = readQuotedValue(xmlData, i, 'systemIdentifier');
148
+ }
149
+ } else if (identifierType === 'SYSTEM') {
150
+ [i] = readQuotedValue(xmlData, i, 'systemIdentifier');
151
+ }
152
+
153
+ return --i;
154
+ }
155
+
156
+ _validateElementExp(xmlData, i) {
157
+ i = skipWhitespace(xmlData, i);
158
+
159
+ // Read and validate element name
160
+ const nameStart = i;
161
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
162
+ const elementName = xmlData.substring(nameStart, i);
163
+ if (!this.suppressValidationErr && !isName(elementName)) {
164
+ throw new Error(`Invalid element name: "${elementName}"`);
165
+ }
166
+
167
+ i = skipWhitespace(xmlData, i);
168
+
169
+ // Validate content model keyword or group
170
+ if (xmlData[i] === 'E' && hasSeq(xmlData, 'MPTY', i)) {
171
+ i += 4;
172
+ } else if (xmlData[i] === 'A' && hasSeq(xmlData, 'NY', i)) {
173
+ i += 2;
174
+ } else if (xmlData[i] === '(') {
175
+ i++;
176
+ while (i < xmlData.length && xmlData[i] !== ')') i++;
177
+ if (xmlData[i] !== ')') throw new Error('Unterminated content model');
178
+ } else if (!this.suppressValidationErr) {
179
+ throw new Error(`Invalid Element Expression, found "${xmlData[i]}"`);
180
+ }
181
+
182
+ return i;
183
+ }
184
+
185
+ _validateAttlistExp(xmlData, i) {
186
+ i = skipWhitespace(xmlData, i);
187
+
188
+ // Read element name
189
+ const elemNameStart = i;
190
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
191
+ const elementName = xmlData.substring(elemNameStart, i);
192
+ validateName(elementName);
193
+
194
+ i = skipWhitespace(xmlData, i);
195
+
196
+ // Read attribute name
197
+ const attrNameStart = i;
198
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
199
+ const attributeName = xmlData.substring(attrNameStart, i);
200
+ validateName(attributeName);
201
+
202
+ i = skipWhitespace(xmlData, i);
203
+
204
+ // Read attribute type
205
+ if (xmlData.substring(i, i + 8).toUpperCase() === 'NOTATION') {
206
+ i += 8;
207
+ i = skipWhitespace(xmlData, i);
208
+ if (xmlData[i] !== '(') throw new Error(`Expected '(', found "${xmlData[i]}"`);
209
+ i++;
210
+ while (i < xmlData.length && xmlData[i] !== ')') {
211
+ const notationStart = i;
212
+ while (i < xmlData.length && xmlData[i] !== '|' && xmlData[i] !== ')') i++;
213
+ const notation = xmlData.substring(notationStart, i).trim();
214
+ validateName(notation);
215
+ if (xmlData[i] === '|') {
216
+ i++;
217
+ i = skipWhitespace(xmlData, i);
218
+ }
219
+ }
220
+ if (xmlData[i] !== ')') throw new Error('Unterminated list of notations');
221
+ i++;
222
+ } else {
223
+ const typeStart = i;
224
+ while (i < xmlData.length && !/\s/.test(xmlData[i])) i++;
225
+ const attributeType = xmlData.substring(typeStart, i);
226
+ const validTypes = ['CDATA', 'ID', 'IDREF', 'IDREFS', 'ENTITY', 'ENTITIES', 'NMTOKEN', 'NMTOKENS'];
227
+ if (!this.suppressValidationErr && !validTypes.includes(attributeType.toUpperCase())) {
228
+ throw new Error(`Invalid attribute type: "${attributeType}"`);
229
+ }
230
+ }
231
+
232
+ i = skipWhitespace(xmlData, i);
233
+
234
+ // Read default value declaration
235
+ if (xmlData.substring(i, i + 9).toUpperCase() === '#REQUIRED') {
236
+ i += 9;
237
+ } else if (xmlData.substring(i, i + 8).toUpperCase() === '#IMPLIED') {
238
+ i += 8;
239
+ } else {
240
+ [i] = readQuotedValue(xmlData, i, 'ATTLIST default value');
241
+ }
242
+
243
+ return i;
244
+ }
245
+ }
246
+
247
+ // ─── Helpers ────────────────────────────────────────────────────────────────
248
+
249
+ function skipWhitespace(data, index) {
250
+ while (index < data.length && /\s/.test(data[index])) index++;
251
+ return index;
252
+ }
253
+
254
+ function hasSeq(data, seq, i) {
255
+ for (let j = 0; j < seq.length; j++) {
256
+ if (seq[j] !== data[i + j + 1]) return false;
257
+ }
258
+ return true;
259
+ }
260
+
261
+ function validateName(name) {
262
+ if (!isName(name)) throw new Error(`Invalid entity name "${name}"`);
263
+ }
264
+
265
+ /**
266
+ * Read a single- or double-quoted string and return [nextIndex, value].
267
+ */
268
+ function readQuotedValue(xmlData, i, type) {
269
+ const startChar = xmlData[i];
270
+ if (startChar !== '"' && startChar !== "'") {
271
+ throw new Error(`Expected quoted string for ${type}, found "${startChar}"`);
272
+ }
273
+ i++;
274
+ const start = i;
275
+ while (i < xmlData.length && xmlData[i] !== startChar) i++;
276
+ if (xmlData[i] !== startChar) throw new Error(`Unterminated ${type} value`);
277
+ const value = xmlData.substring(start, i);
278
+ return [++i, value];
279
+ }
@@ -0,0 +1,10 @@
1
+ export default class ValidationError extends Error {
2
+ constructor(message, code, line, col) {
3
+ super(message);
4
+ this.name = this.constructor.name;
5
+ this.code = code;
6
+ this.line = line;
7
+ this.col = col;
8
+ Error.captureStackTrace?.(this, this.constructor);
9
+ }
10
+ }
package/src/fxv.d.ts ADDED
@@ -0,0 +1,115 @@
1
+
2
+ export type docTypeOptions = {
3
+ /**
4
+ * maximum number of entity present in DOCTYPE
5
+ *
6
+ * Defaults to `Infinity`
7
+ */
8
+ maxEntityCount?: number;
9
+
10
+ /**
11
+ * maximum size of each entity present in DOCTYPE
12
+ *
13
+ * Defaults to `Infinity`
14
+ */
15
+ maxEntitySize?: number;
16
+ }
17
+
18
+ export type validationOptions = {
19
+ /**
20
+ * Whether to allow attributes without value
21
+ *
22
+ * Defaults to `false`
23
+ */
24
+ allowBooleanAttributes?: boolean;
25
+
26
+ /**
27
+ * List of tags without closing tags
28
+ *
29
+ * Defaults to `[]`
30
+ */
31
+ unpairedTags?: string[];
32
+
33
+ docType?: docTypeOptions;
34
+ };
35
+
36
+ export type ValidationError = {
37
+ err: {
38
+ code: string;
39
+ msg: string,
40
+ line: number,
41
+ col: number
42
+ };
43
+ };
44
+
45
+ export class SyntaxValidator {
46
+ static validate(xmlData: string, options?: validationOptions): true | ValidationError;
47
+ }
48
+
49
+
50
+ export interface BusinessRulesValidatorOptions {
51
+ unknownAllow?: boolean;
52
+ boolean?: string[];
53
+ }
54
+
55
+ export type ValidationFailure = {
56
+ code: string;
57
+ path: string;
58
+ value?: string;
59
+ actual?: number | string;
60
+ expected?: number | string;
61
+ }
62
+
63
+ export type CustomValidatorFn = (
64
+ value: string,
65
+ path: string
66
+ ) => ValidationFailure | Record<string, unknown> | undefined | null | void;
67
+
68
+ export class BusinessRulesValidator {
69
+ /**
70
+ * Create a new Validator with a rules XML string.
71
+ *
72
+ * @param rules - XML string whose attributes define validation constraints.
73
+ * @param options - Optional configuration.
74
+ *
75
+ * @throws {Error} If `rules` is empty, not a string, or contains XML syntax errors.
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * const validator = new Validator(`
80
+ * <students>
81
+ * <student repeatable minOccurs="1">
82
+ * <email unique="true" nillable="false"></email>
83
+ * <age type="integer" range="18..65"></age>
84
+ * </student>
85
+ * </students>
86
+ * `);
87
+ * ```
88
+ */
89
+ constructor(rules: string, options?: BusinessRulesValidatorOptions);
90
+
91
+ /**
92
+ * Register a custom validation function under a name.
93
+ * Reference it in rules XML with `checkBy="<name>"`.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * validator.register("isEmail", (value, path) => {
98
+ * if (!value.includes("@")) {
99
+ * return { code: "invalid-email", path, value };
100
+ * }
101
+ * });
102
+ * ```
103
+ */
104
+ register(name: string, fn: CustomValidatorFn): void;
105
+
106
+ /**
107
+ * Validate an XML string against the rules.
108
+ *
109
+ * @param xmldata - XML string to validate.
110
+ * @returns Array of failure objects. Empty array means the document is valid.
111
+ *
112
+ * @throws {Error} If `xmldata` is empty, not a string, or contains XML syntax errors.
113
+ */
114
+ validate(xmldata: string): ValidationFailure[];
115
+ }
package/src/fxv.js ADDED
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ import { validate as validateSyntax } from './validator.js';
4
+ import BusinessRulesValidator from 'detailed-xml-validator';
5
+
6
+ class SyntaxValidator {
7
+ static validate(xmlData, options) {
8
+ return validateSyntax(xmlData, options);
9
+ }
10
+ }
11
+
12
+ export { SyntaxValidator, BusinessRulesValidator };
package/src/util.js ADDED
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const nameStartChar = ':A-Za-z_\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD';
4
+ const nameChar = nameStartChar + '\\-.\\d\\u00B7\\u0300-\\u036F\\u203F-\\u2040';
5
+ export const nameRegexp = '[' + nameStartChar + '][' + nameChar + ']*';
6
+ const regexName = new RegExp('^' + nameRegexp + '$');
7
+
8
+ export function getAllMatches(string, regex) {
9
+ const matches = [];
10
+ let match = regex.exec(string);
11
+ while (match) {
12
+ const allmatches = [];
13
+ allmatches.startIndex = regex.lastIndex - match[0].length;
14
+ const len = match.length;
15
+ for (let index = 0; index < len; index++) {
16
+ allmatches.push(match[index]);
17
+ }
18
+ matches.push(allmatches);
19
+ match = regex.exec(string);
20
+ }
21
+ return matches;
22
+ }
23
+
24
+ export const isName = function (string) {
25
+ const match = regexName.exec(string);
26
+ return !(match === null || typeof match === 'undefined');
27
+ }
28
+
29
+ export function isExist(v) {
30
+ return typeof v !== 'undefined';
31
+ }
32
+
33
+ export function isEmptyObject(obj) {
34
+ return Object.keys(obj).length === 0;
35
+ }
36
+
37
+ export function getValue(v) {
38
+ if (exports.isExist(v)) {
39
+ return v;
40
+ } else {
41
+ return '';
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Dangerous property names that could lead to prototype pollution or security issues
47
+ */
48
+ export const DANGEROUS_PROPERTY_NAMES = [
49
+ // '__proto__',
50
+ // 'constructor',
51
+ // 'prototype',
52
+ 'hasOwnProperty',
53
+ 'toString',
54
+ 'valueOf',
55
+ '__defineGetter__',
56
+ '__defineSetter__',
57
+ '__lookupGetter__',
58
+ '__lookupSetter__'
59
+ ];
60
+
61
+ export const criticalProperties = ["__proto__", "constructor", "prototype"];