From c43db1b57a6d53ee7d024a2cae14343de8becf19 Mon Sep 17 00:00:00 2001 From: Markus Hoffrogge Date: Wed, 22 Jul 2026 22:40:46 +0200 Subject: [PATCH] chore(deps): fix npm audited vulnerabilities --- dist/cleanup/index.js | 1141 ++++++++++++++++++++++++++-------------- dist/setup/index.js | 1149 +++++++++++++++++++++++++++-------------- package-lock.json | 65 ++- 3 files changed, 1576 insertions(+), 779 deletions(-) diff --git a/dist/cleanup/index.js b/dist/cleanup/index.js index 02d067ed2..ef311c41d 100644 --- a/dist/cleanup/index.js +++ b/dist/cleanup/index.js @@ -178,103 +178,109 @@ function gte(i, y) { function expand(str, max, isTop) { var expansions = []; - var m = balanced('{', '}', str); - if (!m || /\$$/.test(m.pre)) return [str]; - - var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); - var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); - var isSequence = isNumericSequence || isAlphaSequence; - var isOptions = m.body.indexOf(',') >= 0; - if (!isSequence && !isOptions) { - // {a},b} - if (m.post.match(/,(?!,).*\}/)) { - str = m.pre + '{' + m.body + escClose + m.post; - return expand(str, max, true); + // The `{a},b}` rewrite below restarts expansion on a rewritten string with + // the same `max` and `isTop = true`. Loop instead of recursing so a long run + // of non-expanding `{}` groups can't exhaust the call stack. + for (;;) { + var m = balanced('{', '}', str); + if (!m || /\$$/.test(m.pre)) return [str]; + + var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); + var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); + var isSequence = isNumericSequence || isAlphaSequence; + var isOptions = m.body.indexOf(',') >= 0; + if (!isSequence && !isOptions) { + // {a},b} + if (m.post.match(/,(?!,).*\}/)) { + str = m.pre + '{' + m.body + escClose + m.post; + isTop = true + continue + } + return [str]; } - return [str]; - } - var n; - if (isSequence) { - n = m.body.split(/\.\./); - } else { - n = parseCommaParts(m.body); - if (n.length === 1) { - // x{{a,b}}y ==> x{a}y x{b}y - n = expand(n[0], max, false).map(embrace); + var n; + if (isSequence) { + n = m.body.split(/\.\./); + } else { + n = parseCommaParts(m.body); if (n.length === 1) { - var post = m.post.length - ? expand(m.post, max, false) - : ['']; - return post.map(function(p) { - return m.pre + n[0] + p; - }); + // x{{a,b}}y ==> x{a}y x{b}y + n = expand(n[0], max, false).map(embrace); + if (n.length === 1) { + var post = m.post.length + ? expand(m.post, max, false) + : ['']; + return post.map(function(p) { + return m.pre + n[0] + p; + }); + } } } - } - // at this point, n is the parts, and we know it's not a comma set - // with a single entry. + // at this point, n is the parts, and we know it's not a comma set + // with a single entry. + + // no need to expand pre, since it is guaranteed to be free of brace-sets + var pre = m.pre; + var post = m.post.length + ? expand(m.post, max, false) + : ['']; + + var N; + + if (isSequence) { + var x = numeric(n[0]); + var y = numeric(n[1]); + var width = Math.max(n[0].length, n[1].length) + var incr = n.length == 3 + ? Math.max(Math.abs(numeric(n[2])), 1) + : 1; + var test = lte; + var reverse = y < x; + if (reverse) { + incr *= -1; + test = gte; + } + var pad = n.some(isPadded); - // no need to expand pre, since it is guaranteed to be free of brace-sets - var pre = m.pre; - var post = m.post.length - ? expand(m.post, max, false) - : ['']; - - var N; - - if (isSequence) { - var x = numeric(n[0]); - var y = numeric(n[1]); - var width = Math.max(n[0].length, n[1].length) - var incr = n.length == 3 - ? Math.max(Math.abs(numeric(n[2])), 1) - : 1; - var test = lte; - var reverse = y < x; - if (reverse) { - incr *= -1; - test = gte; - } - var pad = n.some(isPadded); - - N = []; - - for (var i = x; test(i, y) && N.length < max; i += incr) { - var c; - if (isAlphaSequence) { - c = String.fromCharCode(i); - if (c === '\\') - c = ''; - } else { - c = String(i); - if (pad) { - var need = width - c.length; - if (need > 0) { - var z = new Array(need + 1).join('0'); - if (i < 0) - c = '-' + z + c.slice(1); - else - c = z + c; + N = []; + + for (var i = x; test(i, y) && N.length < max; i += incr) { + var c; + if (isAlphaSequence) { + c = String.fromCharCode(i); + if (c === '\\') + c = ''; + } else { + c = String(i); + if (pad) { + var need = width - c.length; + if (need > 0) { + var z = new Array(need + 1).join('0'); + if (i < 0) + c = '-' + z + c.slice(1); + else + c = z + c; + } } } + N.push(c); } - N.push(c); + } else { + N = concatMap(n, function(el) { return expand(el, max, false) }); } - } else { - N = concatMap(n, function(el) { return expand(el, max, false) }); - } - for (var j = 0; j < N.length; j++) { - for (var k = 0; k < post.length && expansions.length < max; k++) { - var expansion = pre + N[j] + post[k]; - if (!isTop || isSequence || expansion) - expansions.push(expansion); + for (var j = 0; j < N.length; j++) { + for (var k = 0; k < post.length && expansions.length < max; k++) { + var expansion = pre + N[j] + post[k]; + if (!isTop || isSequence || expansion) + expansions.push(expansion); + } } - } - return expansions; + return expansions; + } } @@ -53147,26 +53153,29 @@ class Matcher { // Get or create sibling tracking for current level const currentLevel = this.path.length; - if (!this.siblingStacks[currentLevel]) { - this.siblingStacks[currentLevel] = new Map(); + let level = this.siblingStacks[currentLevel]; + if (!level) { + // `counts` tells same-name siblings apart (the "counter" — nth + // among other s). `total` is every child seen at this level so + // far, kept as a running number instead of re-added from `counts` on + // every push — a parent with many differently-named children would + // otherwise cost more per child the more distinct names it has. + level = { counts: new Map(), total: 0 }; + this.siblingStacks[currentLevel] = level; } - const siblings = this.siblingStacks[currentLevel]; - // Create a unique key for sibling tracking that includes namespace const siblingKey = namespace ? `${namespace}:${tagName}` : tagName; // Calculate counter (how many times this tag appeared at this level) - const counter = siblings.get(siblingKey) || 0; + const counter = level.counts.get(siblingKey) || 0; - // Calculate position (total children at this level so far) - let position = 0; - for (const count of siblings.values()) { - position += count; - } + // Position = total children at this level seen before this one. + const position = level.total; - // Update sibling count for this tag - siblings.set(siblingKey, counter + 1); + // Update sibling count for this tag, and the level's running total. + level.counts.set(siblingKey, counter + 1); + level.total++; // Create new node const node = { @@ -53542,7 +53551,7 @@ class Matcher { snapshot() { return { path: this.path.map(node => ({ ...node })), - siblingStacks: this.siblingStacks.map(map => new Map(map)), + siblingStacks: this.siblingStacks.map(level => level ? { counts: new Map(level.counts), total: level.total } : level), keptAttrs: this._keptAttrs.map(entry => ({ ...entry })) }; } @@ -53554,7 +53563,7 @@ class Matcher { restore(snapshot) { this._pathStringCache = null; this.path = snapshot.path.map(node => ({ ...node })); - this.siblingStacks = snapshot.siblingStacks.map(map => new Map(map)); + this.siblingStacks = snapshot.siblingStacks.map(level => level ? { counts: new Map(level.counts), total: level.total } : level); this._keptAttrs = (snapshot.keptAttrs || []).map(entry => ({ ...entry })); } @@ -55564,6 +55573,389 @@ class XmlNode { } } +;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/node_modules/xml-naming/src/index.js +/** + * xml-naming + * Validates XML Name productions as defined in the XML 1.0 and 1.1 specifications. + * Covers: Name, NCName, QName, NMToken, NMTokens + * + * XML 1.0 spec: https://www.w3.org/TR/xml/#NT-Name + * XML 1.1 spec: https://www.w3.org/TR/xml11/#NT-NameStartChar + * XML NS spec: https://www.w3.org/TR/xml-names/#NT-NCName + */ + +// --------------------------------------------------------------------------- +// Character class strings — XML 1.0 +// +// NameStartChar ::= ":" | [A-Z] | "_" | [a-z] +// | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] +// | [#x370-#x37D] | [#x37F-#x1FFF] <- split to exclude #x0487 +// | [#x200C-#x200D] +// | [#x2070-#x218F] | [#x2C00-#x2FEF] +// | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] +// +// NameChar ::= NameStartChar | "-" | "." | [0-9] +// | #xB7 | [#x0300-#x036F] | [#x203F-#x2040] +// +// Note: \u0487 (Combining Cyrillic Millions Sign) was added in Unicode 4.0, +// after XML 1.0 was defined against Unicode 2.0. It falls inside the range +// \u037F-\u1FFF but must be excluded. We split that range into +// \u037F-\u0486 and \u0488-\u1FFF to exclude it explicitly. +// --------------------------------------------------------------------------- + +const src_nameStartChar10 = + ':A-Za-z_' + + '\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF' + + '\u0370-\u037D' + + '\u037F-\u0486\u0488-\u1FFF' + // split to exclude \u0487 + '\u200C-\u200D' + + '\u2070-\u218F' + + '\u2C00-\u2FEF' + + '\u3001-\uD7FF' + + '\uF900-\uFDCF' + + '\uFDF0-\uFFFD'; + +const src_nameChar10 = + src_nameStartChar10 + + '\\-\\.\\d' + + '\u00B7' + + '\u0300-\u036F' + + '\u203F-\u2040'; + +// --------------------------------------------------------------------------- +// Character class strings — XML 1.1 +// +// Differences from XML 1.0: +// +// NameStartChar: +// 1.0 has split ranges: \u00C0-\u00D6, \u00D8-\u00F6, \u00F8-\u02FF +// 1.1 merges them into: \u00C0-\u02FF +// (\u00D7 x and \u00F7 / are division symbols, excluded in both versions) +// +// 1.0 tops out at \uFFFD (BMP only) +// 1.1 adds \u{10000}-\u{EFFFF} (supplementary planes) +// These require the /u flag on the RegExp — see buildRegexes below. +// +// NameChar: +// 1.1 adds \u0487 (Combining Cyrillic Millions Sign, added in Unicode 4.0) +// --------------------------------------------------------------------------- + +const src_nameStartChar11 = + ':A-Za-z_' + + '\u00C0-\u02FF' + // merged — 1.0 had three split ranges here + '\u0370-\u037D' + + '\u037F-\u0486\u0488-\u1FFF' + // split to exclude \u0487 (combining mark, never a NameStartChar) + '\u200C-\u200D' + + '\u2070-\u218F' + + '\u2C00-\u2FEF' + + '\u3001-\uD7FF' + + '\uF900-\uFDCF' + + '\uFDF0-\uFFFD' + + '\u{10000}-\u{EFFFF}'; // supplementary planes — REQUIRES /u flag on RegExp + +const src_nameChar11 = + src_nameStartChar11 + + '\\-\\.\\d' + + '\u00B7' + + '\u0300-\u036F' + + '\u0487' + // Combining Cyrillic Millions Sign — valid in 1.1, not 1.0 + '\u203F-\u2040'; + +// --------------------------------------------------------------------------- +// Regex builders +// +// XML 1.0 regexes: no flags — BMP only, standard JS regex behaviour. +// XML 1.1 regexes: /u flag — required for \u{10000}-\u{EFFFF} to match actual +// supplementary code points rather than lone surrogates (which are illegal XML). +// --------------------------------------------------------------------------- + +const src_buildRegexes = (startChar, char, flags = '') => { + const ncStart = startChar.replace(':', ''); + const ncChar = char.replace(':', ''); + const ncNamePat = `[${ncStart}][${ncChar}]*`; + + return { + name: new RegExp(`^[${startChar}][${char}]*$`, flags), + ncName: new RegExp(`^${ncNamePat}$`, flags), + qName: new RegExp(`^${ncNamePat}(?::${ncNamePat})?$`, flags), + nmToken: new RegExp(`^[${char}]+$`, flags), + nmTokens: new RegExp(`^[${char}]+(?:\\s+[${char}]+)*$`, flags), + }; +}; + +const src_regexes10 = src_buildRegexes(src_nameStartChar10, src_nameChar10); // no /u — BMP only +const src_regexes11 = src_buildRegexes(src_nameStartChar11, src_nameChar11, 'u'); // /u — enables \u{10000}-\u{EFFFF} + +// --------------------------------------------------------------------------- +// ASCII-only fast path (opt-in, off by default) +// +// The XML 1.0 vs 1.1 NameStartChar/NameChar productions differ *only* in +// their non-ASCII ranges (merged vs split Latin-1 ranges, \u0487, and +// supplementary planes). Restricted to ASCII, both versions collapse to the +// same character classes, so a single regex pair covers both xmlVersion +// values — no /u flag needed. +// +// Rationale: unicode-aware regexes (the /u flag, required for XML 1.1's +// supplementary-plane range) are measurably slower in V8 than plain +// non-unicode regexes on the same input, even when the input is pure ASCII. +// For the common case — HTML/SVG ids, XML tags — names are ASCII, so callers +// who know this can opt in to skip the unicode-aware matching path entirely. +// This is a real but *conditional* win: mainly for XML 1.1 input (avoids /u), +// or at scale where the larger unicode character classes add engine +// overhead. It also changes behaviour (rejects legitimate non-ASCII XML +// 1.0/1.1 names), so it must never be silently enabled — hence off by +// default. +// --------------------------------------------------------------------------- + +const nameStartCharAscii = ':A-Za-z_'; +const nameCharAscii = nameStartCharAscii + '\\-\\.\\d'; + +const regexesAscii = src_buildRegexes(nameStartCharAscii, nameCharAscii); // no /u — ASCII only + +const src_getRegexes = (xmlVersion = '1.0', asciiOnly = false) => { + if (asciiOnly) return regexesAscii; + return xmlVersion === '1.1' ? src_regexes11 : src_regexes10; +}; + +// --------------------------------------------------------------------------- +// Boolean validators +// --------------------------------------------------------------------------- + +/** + * Returns true if the string is a valid XML Name. + * Colons are allowed anywhere (Name production). + * Used for: DOCTYPE entity names, notation names, DTD element declarations. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const xml_naming_src_name = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).name.test(str); + +/** + * Returns true if the string is a valid NCName (Non-Colonized Name). + * Colons are not permitted. + * Used for: namespace prefixes, local names, SVG id attributes. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const src_ncName = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).ncName.test(str); + +/** + * Returns true if the string is a valid QName (Qualified Name). + * Allows exactly one colon as a prefix separator: prefix:localName. + * Used for: element and attribute names in namespace-aware XML/SVG. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const src_qName = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).qName.test(str); + +/** + * Returns true if the string is a valid NMToken. + * Like Name but no restriction on the first character. + * Used for: DTD NMTOKEN attribute values. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const src_nmToken = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).nmToken.test(str); + +/** + * Returns true if the string is a valid NMTokens value. + * A whitespace-separated list of NMToken values. + * Used for: DTD NMTOKENS attribute values. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const src_nmTokens = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).nmTokens.test(str); + +// --------------------------------------------------------------------------- +// Memoized validator factory +// +// Real documents reuse a small vocabulary of tag/attribute names across many +// siblings (e.g. `id`, `class`, `href` repeated across hundreds of elements). +// The plain boolean validators above re-run the regex on every call +// regardless of repeats. `createValidator` returns a closure with a private +// string -> boolean cache, so repeated names after the first become O(1) +// lookups instead of regex tests. +// +// - opts (xmlVersion, asciiOnly) are fixed at creation time, so the regex is +// resolved once, not on every call. +// - The cache is private to the returned closure — no shared/global state, +// no cross-caller pollution. +// - `maxCacheSize` bounds memory: once the cache reaches this many entries, +// it stops accepting new ones (existing entries keep serving hits; new +// misses just fall through to the regex, uncached). This avoids unbounded +// growth against adversarial/high-cardinality input (e.g. validating +// attacker-supplied names with no repeats) without the cost/complexity of +// a full LRU, and without the perf cliff of reset-and-refill thrashing. +// - Call `.reset()` on the returned function to clear the cache manually +// (e.g. between unrelated parse calls). +// --------------------------------------------------------------------------- + +const src_PRODUCTIONS = (/* unused pure expression or super */ null && (['name', 'ncName', 'qName', 'nmToken', 'nmTokens'])); + +/** + * Returns a memoized boolean validator function for a single production, + * with opts fixed at creation time. + * + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean, maxCacheSize?: number }} [opts] + * maxCacheSize: max number of distinct strings to cache (default 2048). + * Once reached, new strings are validated but not cached; existing cached + * entries keep being served. + * @returns {((str: string) => boolean) & { reset: () => void }} + */ +const createValidator = (production, { xmlVersion = '1.0', asciiOnly = false, maxCacheSize = 2048 } = {}) => { + if (!src_PRODUCTIONS.includes(production)) { + throw new TypeError( + `Unknown production "${production}". Must be one of: ${src_PRODUCTIONS.join(', ')}` + ); + } + + const regex = src_getRegexes(xmlVersion, asciiOnly)[production]; + let cache = new Map(); + + const validator = (str) => { + const cached = cache.get(str); + if (cached !== undefined) return cached; + + const result = regex.test(str); + if (cache.size < maxCacheSize) cache.set(str, result); + return result; + }; + + validator.reset = () => { cache = new Map(); }; + + return validator; +}; + +// --------------------------------------------------------------------------- +// Diagnostic validator +// --------------------------------------------------------------------------- + +/** + * Validates a string against a named production and returns a detailed result. + * + * @param {string} str + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * @returns {{ valid: boolean, production: string, input: string, reason?: string, position?: number }} + */ +const src_validate = (str, production, { xmlVersion = '1.0', asciiOnly = false } = {}) => { + if (!src_PRODUCTIONS.includes(production)) { + throw new TypeError( + `Unknown production "${production}". Must be one of: ${src_PRODUCTIONS.join(', ')}` + ); + } + + const validators = { name: xml_naming_src_name, ncName: src_ncName, qName: src_qName, nmToken: src_nmToken, nmTokens: src_nmTokens }; + const isValid = validators[production](str, { xmlVersion, asciiOnly }); + + if (isValid) return { valid: true, production, input: str }; + + let reason = 'Does not match the production rules'; + let position; + + // Diagnostic fallback char checks must mirror the same character set the + // boolean validator above used, or the reported reason/position could + // contradict the `valid: false` result (e.g. flagging a char as illegal + // that the unicode-aware check would have accepted). + const startCharPattern = asciiOnly ? /^[:A-Za-z_]/ : /^[:A-Za-z_\u00C0-\uFFFD]/; + const namePattern = asciiOnly ? /[\w\-\\.:]/ : /[\w\-\\.:\u00B7\u00C0-\uFFFD]/; + + if (str.length === 0) { + reason = 'Input is empty'; + } else if (production === 'ncName' && str.includes(':')) { + position = str.indexOf(':'); + reason = 'Colon is not allowed in NCName'; + } else if (production === 'qName' && str.startsWith(':')) { + reason = 'QName cannot start with a colon'; + position = 0; + } else if (production === 'qName' && str.endsWith(':')) { + reason = 'QName cannot end with a colon'; + position = str.length - 1; + } else if (production === 'qName' && (str.match(/:/g) || []).length > 1) { + reason = 'QName can have at most one colon'; + position = str.lastIndexOf(':'); + } else if ( + ['name', 'ncName', 'qName'].includes(production) && + !startCharPattern.test(str[0]) + ) { + reason = `First character "${str[0]}" is not a valid NameStartChar`; + position = 0; + } else { + for (let i = 0; i < str.length; i++) { + if (!namePattern.test(str[i])) { + reason = `Character "${str[i]}" at position ${i} is not a valid NameChar`; + position = i; + break; + } + } + } + + return { valid: false, production, input: str, reason, position }; +}; + +// --------------------------------------------------------------------------- +// Batch validator +// --------------------------------------------------------------------------- + +/** + * Validates an array of strings against a named production. + * + * @param {string[]} strings + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * @returns {Array<{ valid: boolean, production: string, input: string, reason?: string, position?: number }>} + */ +const src_validateAll = (strings, production, opts = {}) => + strings.map(str => src_validate(str, production, opts)); + +// --------------------------------------------------------------------------- +// Sanitizer +// --------------------------------------------------------------------------- + +/** + * Transforms an invalid string into the nearest valid XML name for the given production. + * + * @param {string} str + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ replacement?: string, asciiOnly?: boolean }} [opts] + * asciiOnly: also replace any non-ASCII character, not just XML-illegal + * ones (default false). + * @returns {string} + */ +const src_sanitize = (str, production = 'name', { replacement = '_', asciiOnly = false } = {}) => { + if (!str) return replacement; + + let result = str; + + // Strip colons for NCName + if (production === 'ncName') { + result = result.replace(/:/g, ''); + } + + // Replace illegal characters + const allowedCharPattern = asciiOnly ? /[^\w\-\.:]/g : /[^\w\-\.:\u00B7\u00C0-\uFFFD]/g; + result = result.replace(allowedCharPattern, replacement); + + // Fix invalid start character for Name / NCName / QName + if (production !== 'nmToken' && production !== 'nmTokens') { + if (/^[\-\.\d]/.test(result)) { + result = replacement + result; + } + } + + return result || replacement; +}; ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/DocTypeReader.js @@ -55797,7 +56189,7 @@ class DocTypeReader { let elementName = xmlData.substring(startIndex, i); // Validate element name - if (!this.suppressValidationErr && !qName(elementName, { xmlVersion: this.xmlVersion })) { + if (!this.suppressValidationErr && !src_qName(elementName, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid element name: "${elementName}"`); } @@ -55971,7 +56363,7 @@ function hasSeq(data, seq, i) { } function validateEntityName(name, xmlVersion) { - if (qName(name, { xmlVersion: xmlVersion })) + if (src_qName(name, { xmlVersion: xmlVersion })) return name; else throw new Error(`Invalid entity name ${name}`); @@ -57755,26 +58147,6 @@ const MISC_SYMBOLS = { nVDash: '⊯', }; -/** - * All entities combined (if you need everything) - * @type {Record} - */ -const ALL_ENTITIES = { - ...BASIC_LATIN, - ...LATIN_ACCENTS, - ...LATIN_EXTENDED, - ...GREEK, - ...CYRILLIC, - ...MATH, - ...MATH_ADVANCED, - ...ARROWS, - ...SHAPES, - ...PUNCTUATION, - ...CURRENCY, - ...FRACTIONS, - ...MISC_SYMBOLS, -}; - const XML = { amp: "&", apos: "'", @@ -58453,6 +58825,152 @@ class EntityDecoder { return this._applyNCRAction(effective, token, cp); } } +;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql.js +/** + * SQL context patterns — high-precision rules only. + * + * These rules have very low false-positive risk and are safe to apply to + * general user text (names, descriptions, search queries, etc.). + * All patterns are ReDoS-safe — unlike the `sql-injection` npm package + * which has an active CVE on its own detection regexes. + * + * For exhaustive coverage including noisier heuristics (comment sequences, + * hex literals, stacked queries with semicolons), use 'SQL-STRICT' instead. + * Apply 'SQL-STRICT' only to strings that are specifically SQL fragments, + * not to general free-text fields. + */ + +const SQL_PATTERNS = [ + { + id: 'sql-block-comment-open', + description: 'SQL block comment open: /* ... */ — unusual in legitimate user text', + pattern: /\/\*/, + }, + { + id: 'sql-union-select', + description: 'UNION SELECT — most common SQL injection aggregation attack', + pattern: /\bUNION\s{1,20}(?:ALL\s{1,20})?SELECT\b/i, + }, + { + id: 'sql-drop-table', + description: 'DROP TABLE — destructive DDL injection', + pattern: /\bDROP\s{1,20}TABLE\b/i, + }, + { + id: 'sql-drop-database', + description: 'DROP DATABASE — destructive DDL injection', + pattern: /\bDROP\s{1,20}DATABASE\b/i, + }, + { + id: 'sql-insert-into', + description: 'INSERT INTO — data injection', + pattern: /\bINSERT\s{1,20}INTO\b/i, + }, + { + id: 'sql-delete-from', + description: 'DELETE FROM — data deletion injection', + pattern: /\bDELETE\s{1,20}FROM\b/i, + }, + { + id: 'sql-update-set', + description: 'UPDATE ... SET — data modification injection', + // Allows arbitrary content between UPDATE and SET (table name, alias, etc.) + pattern: /\bUPDATE\b[\s\S]{1,60}\bSET\b/i, + }, + { + id: 'sql-exec-xp', + description: 'EXEC xp_ — MSSQL extended stored procedure execution', + pattern: /\bEXEC(?:UTE)?\s{1,20}xp_/i, + }, + { + id: 'sql-tautology-string', + description: "Classic string tautology: ' OR '1'='1 or \" OR \"1\"=\"1\"", + // Last quote is optional — injection may truncate it: ' OR '1'='1-- + pattern: /'\s{0,10}OR\s{0,10}'[^']{0,20}'\s*=\s*'[^']{0,20}/i, + }, + { + id: 'sql-tautology-numeric', + description: 'Numeric tautology: OR 1=1', + pattern: /\bOR\s{1,10}1\s*=\s*1\b/i, + }, + { + id: 'sql-always-true-zero', + description: 'Numeric tautology: OR 0=0', + pattern: /\bOR\s{1,10}0\s*=\s*0\b/i, + }, + { + id: 'sql-sleep-benchmark', + description: 'Time-based blind injection: SLEEP() or BENCHMARK()', + pattern: /\b(?:SLEEP|BENCHMARK)\s*\(/i, + }, + { + id: 'sql-waitfor-delay', + description: 'MSSQL time-based blind injection: WAITFOR DELAY', + pattern: /\bWAITFOR\s{1,20}DELAY\b/i, + }, + { + id: 'sql-char-function', + description: 'CHAR() function — used to obfuscate injected strings', + pattern: /\bCHAR\s*\(\s*\d{1,3}/i, + }, + { + id: 'sql-information-schema', + description: 'INFORMATION_SCHEMA — reconnaissance query for table/column enumeration', + pattern: /\bINFORMATION_SCHEMA\b/i, + }, +]; + +/* harmony default export */ const sql = (SQL_PATTERNS); + +;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql-strict.js +/** + * SQL-STRICT context patterns. + * + * Extends the base 'SQL' context with three additional rules that are + * effective at detecting real injections but carry a higher false-positive + * risk on general free-text input. + * + * Use 'SQL-STRICT' when: + * - The string is specifically a SQL fragment or database identifier + * - You control the input domain (e.g. a dedicated SQL search field) + * - You can tolerate occasional false positives in exchange for broader coverage + * + * Use 'SQL' (not STRICT) when: + * - The field is general user text (names, descriptions, comments) + * - False positives would block legitimate content (e.g. "see note -- above") + * + * Rules moved here from 'SQL' due to false-positive risk: + * + * sql-line-comment — "--" fires on "see note -- above", "value--", CSS var(--primary) + * sql-stacked-query — "; SELECT" fires on legitimate prose with semicolons + SQL words + * sql-hex-encoding — "0xDEAD" fires on hex values in technical docs and log output + */ + + + +const SQL_STRICT_EXTRA = [ + { + id: 'sql-line-comment', + description: 'SQL line comment: -- followed by whitespace or end of string', + pattern: /--(?:\s|$)/, + }, + { + id: 'sql-stacked-query', + description: 'Stacked queries: semicolon immediately followed by a SQL keyword', + pattern: /;\s{0,10}(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC)\b/i, + }, + { + id: 'sql-hex-encoding', + description: 'Hex-encoded string injection: 0x41414141 style (MySQL)', + pattern: /\b0x[0-9a-f]{4,}/i, + }, +]; + +// SQL-STRICT = all base SQL rules + the three noisy extras +const SQL_STRICT_PATTERNS = [...sql, ...SQL_STRICT_EXTRA]; + +/* harmony default export */ const sql_strict = (SQL_STRICT_PATTERNS); + ;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/html.js /** * HTML context patterns. @@ -58720,152 +59238,6 @@ const SVG_PATTERNS = [ /* harmony default export */ const svg = (SVG_PATTERNS); -;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql.js -/** - * SQL context patterns — high-precision rules only. - * - * These rules have very low false-positive risk and are safe to apply to - * general user text (names, descriptions, search queries, etc.). - * All patterns are ReDoS-safe — unlike the `sql-injection` npm package - * which has an active CVE on its own detection regexes. - * - * For exhaustive coverage including noisier heuristics (comment sequences, - * hex literals, stacked queries with semicolons), use 'SQL-STRICT' instead. - * Apply 'SQL-STRICT' only to strings that are specifically SQL fragments, - * not to general free-text fields. - */ - -const SQL_PATTERNS = [ - { - id: 'sql-block-comment-open', - description: 'SQL block comment open: /* ... */ — unusual in legitimate user text', - pattern: /\/\*/, - }, - { - id: 'sql-union-select', - description: 'UNION SELECT — most common SQL injection aggregation attack', - pattern: /\bUNION\s{1,20}(?:ALL\s{1,20})?SELECT\b/i, - }, - { - id: 'sql-drop-table', - description: 'DROP TABLE — destructive DDL injection', - pattern: /\bDROP\s{1,20}TABLE\b/i, - }, - { - id: 'sql-drop-database', - description: 'DROP DATABASE — destructive DDL injection', - pattern: /\bDROP\s{1,20}DATABASE\b/i, - }, - { - id: 'sql-insert-into', - description: 'INSERT INTO — data injection', - pattern: /\bINSERT\s{1,20}INTO\b/i, - }, - { - id: 'sql-delete-from', - description: 'DELETE FROM — data deletion injection', - pattern: /\bDELETE\s{1,20}FROM\b/i, - }, - { - id: 'sql-update-set', - description: 'UPDATE ... SET — data modification injection', - // Allows arbitrary content between UPDATE and SET (table name, alias, etc.) - pattern: /\bUPDATE\b[\s\S]{1,60}\bSET\b/i, - }, - { - id: 'sql-exec-xp', - description: 'EXEC xp_ — MSSQL extended stored procedure execution', - pattern: /\bEXEC(?:UTE)?\s{1,20}xp_/i, - }, - { - id: 'sql-tautology-string', - description: "Classic string tautology: ' OR '1'='1 or \" OR \"1\"=\"1\"", - // Last quote is optional — injection may truncate it: ' OR '1'='1-- - pattern: /'\s{0,10}OR\s{0,10}'[^']{0,20}'\s*=\s*'[^']{0,20}/i, - }, - { - id: 'sql-tautology-numeric', - description: 'Numeric tautology: OR 1=1', - pattern: /\bOR\s{1,10}1\s*=\s*1\b/i, - }, - { - id: 'sql-always-true-zero', - description: 'Numeric tautology: OR 0=0', - pattern: /\bOR\s{1,10}0\s*=\s*0\b/i, - }, - { - id: 'sql-sleep-benchmark', - description: 'Time-based blind injection: SLEEP() or BENCHMARK()', - pattern: /\b(?:SLEEP|BENCHMARK)\s*\(/i, - }, - { - id: 'sql-waitfor-delay', - description: 'MSSQL time-based blind injection: WAITFOR DELAY', - pattern: /\bWAITFOR\s{1,20}DELAY\b/i, - }, - { - id: 'sql-char-function', - description: 'CHAR() function — used to obfuscate injected strings', - pattern: /\bCHAR\s*\(\s*\d{1,3}/i, - }, - { - id: 'sql-information-schema', - description: 'INFORMATION_SCHEMA — reconnaissance query for table/column enumeration', - pattern: /\bINFORMATION_SCHEMA\b/i, - }, -]; - -/* harmony default export */ const sql = (SQL_PATTERNS); - -;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql-strict.js -/** - * SQL-STRICT context patterns. - * - * Extends the base 'SQL' context with three additional rules that are - * effective at detecting real injections but carry a higher false-positive - * risk on general free-text input. - * - * Use 'SQL-STRICT' when: - * - The string is specifically a SQL fragment or database identifier - * - You control the input domain (e.g. a dedicated SQL search field) - * - You can tolerate occasional false positives in exchange for broader coverage - * - * Use 'SQL' (not STRICT) when: - * - The field is general user text (names, descriptions, comments) - * - False positives would block legitimate content (e.g. "see note -- above") - * - * Rules moved here from 'SQL' due to false-positive risk: - * - * sql-line-comment — "--" fires on "see note -- above", "value--", CSS var(--primary) - * sql-stacked-query — "; SELECT" fires on legitimate prose with semicolons + SQL words - * sql-hex-encoding — "0xDEAD" fires on hex values in technical docs and log output - */ - - - -const SQL_STRICT_EXTRA = [ - { - id: 'sql-line-comment', - description: 'SQL line comment: -- followed by whitespace or end of string', - pattern: /--(?:\s|$)/, - }, - { - id: 'sql-stacked-query', - description: 'Stacked queries: semicolon immediately followed by a SQL keyword', - pattern: /;\s{0,10}(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC)\b/i, - }, - { - id: 'sql-hex-encoding', - description: 'Hex-encoded string injection: 0x41414141 style (MySQL)', - pattern: /\b0x[0-9a-f]{4,}/i, - }, -]; - -// SQL-STRICT = all base SQL rules + the three noisy extras -const SQL_STRICT_PATTERNS = [...sql, ...SQL_STRICT_EXTRA]; - -/* harmony default export */ const sql_strict = (SQL_STRICT_PATTERNS); - ;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/shell.js /** * SHELL context patterns. @@ -59251,32 +59623,66 @@ const LOG_PATTERNS = [ /* harmony default export */ const contexts_log = (LOG_PATTERNS); -;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/registry.js +;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/index.js /** - * Context registry — maps context name strings to their pattern arrays. + * is-unsafe v2 + * + * Zero-dependency, DOM-free, pure predicate for detecting unsafe strings + * across HTML, XML, SVG, SQL, SQL-STRICT, SHELL, REDOS, NOSQL, and LOG contexts. * - * Adding a new context: create a file in ./contexts/, export a default array - * of pattern objects, and register it here. + * v2 change: contexts are imported as named pattern arrays rather than resolved + * via a string-keyed registry. This makes each context independently + * tree-shakeable — bundlers can drop any context you never import. * - * Context name guide: - * SQL — high-precision rules; safe for general text fields - * SQL-STRICT — SQL + three noisier rules (line comments, stacked queries, hex); - * use only for SQL-specific inputs - * REDOS — detects ReDoS-prone patterns when string will be compiled as RegExp + * @module is-unsafe */ +// ─── Context pattern arrays (named exports) ──────────────────────────────── +// Import only the ones you need. Each is independently tree-shakeable. + + + + + + + + + +// SQL-STRICT needs a quoted identifier because of the hyphen +// ─── VALID_CONTEXTS convenience re-export ───────────────────────────────── +// Importing this pulls in ALL contexts. Use it only when you need all of them +// (e.g. for validation UI, tooling, or exhaustive audits). +// If you only need a subset, import the named contexts directly instead. -/** @type {Record>} */ -const registry_CONTEXT_REGISTRY = { + + + + +// ─── Attach labels to named contexts ────────────────────────────────────── +// Each built-in PatternList carries its canonical name so matchList can read +// list.label directly — no registry lookup needed at match time. +// Custom PatternLists default to 'CUSTOM' unless the caller sets list.label. + +html.label = 'HTML'; +xml.label = 'XML'; +svg.label = 'SVG'; +sql.label = 'SQL'; +sql_strict.label = 'SQL-STRICT'; +shell.label = 'SHELL'; +redos.label = 'REDOS'; +nosql.label = 'NOSQL'; +contexts_log.label = 'LOG'; + +const VALID_CONTEXTS = Object.freeze({ HTML: html, XML: xml, SVG: svg, @@ -59286,45 +59692,29 @@ const registry_CONTEXT_REGISTRY = { REDOS: redos, NOSQL: nosql, LOG: contexts_log, -}; +}); -/* harmony default export */ const registry = (registry_CONTEXT_REGISTRY); +// ─── Types ──────────────────────────────────────────────────────────────── /** - * Enum of valid context names — e.g. `VALID_CONTEXTS.HTML === 'HTML'`. - * @type {Record} - */ -const VALID_CONTEXTS = Object.freeze( - Object.fromEntries(Object.keys(registry_CONTEXT_REGISTRY).map((k) => [k, k])) -); -;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/index.js -/** - * is-unsafe - * - * Zero-dependency, DOM-free, pure predicate for detecting unsafe strings - * across HTML, XML, SVG, SQL, SQL-STRICT, SHELL, REDOS, NOSQL, and LOG contexts. - * - * @module is-unsafe + * @typedef {{ id: string, description: string, pattern: RegExp }} Rule */ - - /** - * @typedef {'HTML'|'XML'|'SVG'|'SQL'|'SQL-STRICT'|'SHELL'|'REDOS'|'NOSQL'|'LOG'} ContextName + * @typedef {Rule[]} PatternList */ /** * @typedef {Object} MatchResult - * @property {string} context - The context in which the match was found - * @property {string} id - Rule identifier + * @property {string} context - Label identifying which context matched ('HTML', 'CUSTOM', etc.) + * @property {string} id - Rule identifier * @property {string} description - Human-readable description of what was matched - * @property {RegExp} pattern - The pattern that matched + * @property {RegExp} pattern - The pattern that matched */ -// ─── Validation helpers ──────────────────────────────────────────────────── +// ─── Internal helpers ────────────────────────────────────────────────────── /** - * Validate that `value` is a string. Throws TypeError if not. * @param {unknown} value */ function assertString(value) { @@ -59336,56 +59726,61 @@ function assertString(value) { } /** - * Validate that `context` is a recognised context name, an array of them, - * or a RegExp instance. Throws TypeError if not. - * @param {ContextName|ContextName[]|RegExp} context + * @param {unknown} context */ function assertContext(context) { if (context instanceof RegExp) return; - if (typeof context === 'string') { - if (!registry[context]) { - throw new TypeError( - `is-unsafe: unknown context "${context}". Valid contexts: ${Object.keys(VALID_CONTEXTS).join(', ')}` - ); - } - return; - } - if (Array.isArray(context)) { if (context.length === 0) { - throw new TypeError('is-unsafe: context array must not be empty'); + throw new TypeError('is-unsafe: context must not be an empty array'); } - for (const c of context) { - if (typeof c !== 'string' || !registry[c]) { - throw new TypeError( - `is-unsafe: unknown context "${c}" in array. Valid contexts: ${Object.keys(VALID_CONTEXTS).join(', ')}` - ); + // Detect array-of-arrays vs flat pattern list + if (Array.isArray(context[0])) { + // Array of PatternLists + for (const list of context) { + if (!Array.isArray(list) || list.length === 0) { + throw new TypeError( + 'is-unsafe: each context in the array must be a non-empty pattern array (PatternList)' + ); + } } } + // else: flat PatternList — trust it, no deep validation needed return; } throw new TypeError( - `is-unsafe: second argument must be a context string, array of context strings, or RegExp. Got: ${typeof context}` + `is-unsafe: second argument must be a PatternList (e.g. HTML), ` + + `an array of PatternLists (e.g. [HTML, XML]), or a RegExp. Got: ${typeof context}` ); } -// ─── Core matching logic ─────────────────────────────────────────────────── +/** + * Normalise any valid context arg into an array of PatternLists. + * + * @param {Rule[]|Rule[][]|RegExp} context + * @returns {{ lists: Rule[][]|null, regex: RegExp|null }} + */ +function normalise(context) { + if (context instanceof RegExp) return { lists: null, regex: context }; + // Distinguish PatternList (array of rule objects) from array of PatternLists + if (Array.isArray(context[0])) return { lists: context, regex: null }; + return { lists: [context], regex: null }; +} /** - * Test a single value against one named context's patterns. - * Returns the first matching MatchResult, or null if nothing matched. + * Test value against a single PatternList. Returns the first MatchResult or null. * * @param {string} value - * @param {string} contextName + * @param {Rule[]} list * @returns {MatchResult|null} */ -function matchContext(value, contextName) { - const patterns = registry[contextName]; - for (const rule of patterns) { +function matchList(value, list) { + const label = list.label ?? 'CUSTOM'; + for (const rule of list) { if (rule.pattern.test(value)) { - return { context: contextName, id: rule.id, description: rule.description, pattern: rule.pattern }; + return { context: label, id: rule.id, description: rule.description, pattern: rule.pattern }; } } return null; @@ -59396,103 +59791,95 @@ function matchContext(value, contextName) { /** * Returns `true` if `value` is unsafe in the given context(s), `false` otherwise. * - * @param {string} value - The string to test - * @param {ContextName|ContextName[]|RegExp} context - * - A named context ('HTML', 'XML', 'SVG', 'SQL', 'SQL-STRICT', 'SHELL', 'REDOS', 'NOSQL', 'LOG') - * - An array of named contexts — returns true if unsafe in **any** of them + * @param {string} value - The string to test + * @param {PatternList | PatternList[] | RegExp} context + * - A PatternList imported from is-unsafe (e.g. `HTML`, `XML`) + * - An array of PatternLists — returns true if unsafe in **any** of them * - A custom RegExp — returns true if the pattern matches * @returns {boolean} * * @example - * isUnsafe('', 'HTML') // true - * isUnsafe('hello world', 'HTML') // false - * isUnsafe('value', ['HTML', 'SQL']) // false - * isUnsafe('value', /my-pattern/i) // false + * import { isUnsafe, HTML, SQL } from 'is-unsafe'; + * + * isUnsafe('', HTML) // true + * isUnsafe('hello world', HTML) // false + * isUnsafe('value', [HTML, SQL]) // false + * isUnsafe('value', /my-pattern/i) // false */ function isUnsafe(value, context) { assertString(value); assertContext(context); - // Custom RegExp — caller-supplied pattern - if (context instanceof RegExp) { - return context.test(value); - } + const { lists, regex } = normalise(context); - // Single named context - if (typeof context === 'string') { - return matchContext(value, context) !== null; - } + if (regex) return regex.test(value); - // Array of named contexts — unsafe if ANY context matches - for (const c of context) { - if (matchContext(value, c) !== null) return true; + for (const list of lists) { + if (matchList(value, list) !== null) return true; } return false; } /** - * Like `isUnsafe`, but instead of a boolean returns the first `MatchResult` - * describing **why** the value was flagged, or `null` if it is safe. - * - * Useful for logging, error messages, or policy reporting. + * Like `isUnsafe`, but returns the first `MatchResult` describing **why** + * the value was flagged, or `null` if it is safe. * * @param {string} value - * @param {ContextName|ContextName[]|RegExp} context + * @param {PatternList | PatternList[] | RegExp} context * @returns {MatchResult|null} * * @example - * whyUnsafe('', 'HTML') + * import { whyUnsafe, HTML } from 'is-unsafe'; + * + * whyUnsafe('', HTML) * // { context: 'HTML', id: 'html-script-open', description: '...', pattern: /.../ } */ function whyUnsafe(value, context) { assertString(value); assertContext(context); - if (context instanceof RegExp) { - return context.test(value) - ? { context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: context } - : null; - } + const { lists, regex } = normalise(context); - if (typeof context === 'string') { - return matchContext(value, context); + if (regex) { + return regex.test(value) + ? { context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: regex } + : null; } - for (const c of context) { - const result = matchContext(value, c); + for (const list of lists) { + const result = matchList(value, list); if (result !== null) return result; } return null; } /** - * Returns all matching rules across the given context(s), or an empty array - * if the value is safe. Useful for comprehensive auditing. + * Returns **all** matching rules across the given context(s), or an empty + * array if the value is safe. Useful for comprehensive auditing. * * @param {string} value - * @param {ContextName|ContextName[]|RegExp} context + * @param {PatternList | PatternList[] | RegExp} context * @returns {MatchResult[]} */ function allUnsafe(value, context) { assertString(value); assertContext(context); + const { lists, regex } = normalise(context); const results = []; - if (context instanceof RegExp) { - if (context.test(value)) { - results.push({ context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: context }); + if (regex) { + if (regex.test(value)) { + results.push({ context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: regex }); } return results; } - const contexts = typeof context === 'string' ? [context] : context; - - for (const c of contexts) { - const patterns = CONTEXT_REGISTRY[c]; - for (const rule of patterns) { + for (const list of lists) { + const label = list.label ?? 'CUSTOM'; + for (const rule of list) { if (rule.pattern.test(value)) { - results.push({ context: c, id: rule.id, description: rule.description, pattern: rule.pattern }); + results.push({ context: label, id: rule.id, description: rule.description, pattern: rule.pattern }); } } } @@ -59502,6 +59889,7 @@ function allUnsafe(value, context) { /* harmony default export */ const src = ((/* unused pure expression or super */ null && (isUnsafe))); + ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js ///@ts-check @@ -59592,6 +59980,7 @@ class OrderedObjParser { this.ignoreAttributesFn = ignoreAttributes_getIgnoreAttributesFn(this.options.ignoreAttributes) this.entityExpansionCount = 0; this.currentExpandedLength = 0; + this.doctypefound = false; let namedEntities = { ...XML }; if (this.options.entityDecoder) { this.entityDecoder = this.options.entityDecoder @@ -59609,8 +59998,7 @@ class OrderedObjParser { // onExternalEntity: (name, value) => isUnsafe(value) ? 'block' : 'allow', onInputEntity: (name, value) => //TODO: VALID_CONTEXTS.HTML should be set only if this.options.htmlEntities - isUnsafe(value, [VALID_CONTEXTS.HTML, VALID_CONTEXTS.XML]) - ? ENTITY_ACTION.BLOCK : ENTITY_ACTION.ALLOW, + isUnsafe(value, [html, xml]) ? ENTITY_ACTION.BLOCK : ENTITY_ACTION.ALLOW, //postCheck: resolved => resolved }); @@ -59800,6 +60188,7 @@ const parseXml = function (xmlData) { // Reset entity expansion counters for this document this.entityExpansionCount = 0; this.currentExpandedLength = 0; + this.doctypefound = false; const options = this.options; const docTypeReader = new DocTypeReader(options.processEntities); const xmlLen = xmlData.length; @@ -59884,6 +60273,8 @@ const parseXml = function (xmlData) { i = endIndex; } else if (c1 === 33 && xmlData.charCodeAt(i + 2) === 68) { //'!D' + if (this.doctypefound) throw new Error("Multiple DOCTYPE declarations found."); + this.doctypefound = true; const result = docTypeReader.readDocType(xmlData, i); this.entityDecoder.addInputEntities(result.entities); i = result.i; diff --git a/dist/setup/index.js b/dist/setup/index.js index dc9c62d0d..8c7de79cc 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -178,103 +178,109 @@ function gte(i, y) { function expand(str, max, isTop) { var expansions = []; - var m = balanced('{', '}', str); - if (!m || /\$$/.test(m.pre)) return [str]; - - var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); - var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); - var isSequence = isNumericSequence || isAlphaSequence; - var isOptions = m.body.indexOf(',') >= 0; - if (!isSequence && !isOptions) { - // {a},b} - if (m.post.match(/,(?!,).*\}/)) { - str = m.pre + '{' + m.body + escClose + m.post; - return expand(str, max, true); - } - return [str]; - } - - var n; - if (isSequence) { - n = m.body.split(/\.\./); - } else { - n = parseCommaParts(m.body); - if (n.length === 1) { - // x{{a,b}}y ==> x{a}y x{b}y - n = expand(n[0], max, false).map(embrace); - if (n.length === 1) { - var post = m.post.length - ? expand(m.post, max, false) - : ['']; - return post.map(function(p) { - return m.pre + n[0] + p; - }); + // The `{a},b}` rewrite below restarts expansion on a rewritten string with + // the same `max` and `isTop = true`. Loop instead of recursing so a long run + // of non-expanding `{}` groups can't exhaust the call stack. + for (;;) { + var m = balanced('{', '}', str); + if (!m || /\$$/.test(m.pre)) return [str]; + + var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); + var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); + var isSequence = isNumericSequence || isAlphaSequence; + var isOptions = m.body.indexOf(',') >= 0; + if (!isSequence && !isOptions) { + // {a},b} + if (m.post.match(/,(?!,).*\}/)) { + str = m.pre + '{' + m.body + escClose + m.post; + isTop = true + continue } + return [str]; } - } - // at this point, n is the parts, and we know it's not a comma set - // with a single entry. - - // no need to expand pre, since it is guaranteed to be free of brace-sets - var pre = m.pre; - var post = m.post.length - ? expand(m.post, max, false) - : ['']; - - var N; - - if (isSequence) { - var x = numeric(n[0]); - var y = numeric(n[1]); - var width = Math.max(n[0].length, n[1].length) - var incr = n.length == 3 - ? Math.max(Math.abs(numeric(n[2])), 1) - : 1; - var test = lte; - var reverse = y < x; - if (reverse) { - incr *= -1; - test = gte; - } - var pad = n.some(isPadded); - - N = []; - - for (var i = x; test(i, y) && N.length < max; i += incr) { - var c; - if (isAlphaSequence) { - c = String.fromCharCode(i); - if (c === '\\') - c = ''; - } else { - c = String(i); - if (pad) { - var need = width - c.length; - if (need > 0) { - var z = new Array(need + 1).join('0'); - if (i < 0) - c = '-' + z + c.slice(1); - else - c = z + c; + var n; + if (isSequence) { + n = m.body.split(/\.\./); + } else { + n = parseCommaParts(m.body); + if (n.length === 1) { + // x{{a,b}}y ==> x{a}y x{b}y + n = expand(n[0], max, false).map(embrace); + if (n.length === 1) { + var post = m.post.length + ? expand(m.post, max, false) + : ['']; + return post.map(function(p) { + return m.pre + n[0] + p; + }); + } + } + } + + // at this point, n is the parts, and we know it's not a comma set + // with a single entry. + + // no need to expand pre, since it is guaranteed to be free of brace-sets + var pre = m.pre; + var post = m.post.length + ? expand(m.post, max, false) + : ['']; + + var N; + + if (isSequence) { + var x = numeric(n[0]); + var y = numeric(n[1]); + var width = Math.max(n[0].length, n[1].length) + var incr = n.length == 3 + ? Math.max(Math.abs(numeric(n[2])), 1) + : 1; + var test = lte; + var reverse = y < x; + if (reverse) { + incr *= -1; + test = gte; + } + var pad = n.some(isPadded); + + N = []; + + for (var i = x; test(i, y) && N.length < max; i += incr) { + var c; + if (isAlphaSequence) { + c = String.fromCharCode(i); + if (c === '\\') + c = ''; + } else { + c = String(i); + if (pad) { + var need = width - c.length; + if (need > 0) { + var z = new Array(need + 1).join('0'); + if (i < 0) + c = '-' + z + c.slice(1); + else + c = z + c; + } } } + N.push(c); } - N.push(c); + } else { + N = concatMap(n, function(el) { return expand(el, max, false) }); } - } else { - N = concatMap(n, function(el) { return expand(el, max, false) }); - } - for (var j = 0; j < N.length; j++) { - for (var k = 0; k < post.length && expansions.length < max; k++) { - var expansion = pre + N[j] + post[k]; - if (!isTop || isSequence || expansion) - expansions.push(expansion); + for (var j = 0; j < N.length; j++) { + for (var k = 0; k < post.length && expansions.length < max; k++) { + var expansion = pre + N[j] + post[k]; + if (!isTop || isSequence || expansion) + expansions.push(expansion); + } } - } - return expansions; + return expansions; + } } @@ -84187,26 +84193,29 @@ class Matcher { // Get or create sibling tracking for current level const currentLevel = this.path.length; - if (!this.siblingStacks[currentLevel]) { - this.siblingStacks[currentLevel] = new Map(); + let level = this.siblingStacks[currentLevel]; + if (!level) { + // `counts` tells same-name siblings apart (the "counter" — nth + // among other s). `total` is every child seen at this level so + // far, kept as a running number instead of re-added from `counts` on + // every push — a parent with many differently-named children would + // otherwise cost more per child the more distinct names it has. + level = { counts: new Map(), total: 0 }; + this.siblingStacks[currentLevel] = level; } - const siblings = this.siblingStacks[currentLevel]; - // Create a unique key for sibling tracking that includes namespace const siblingKey = namespace ? `${namespace}:${tagName}` : tagName; // Calculate counter (how many times this tag appeared at this level) - const counter = siblings.get(siblingKey) || 0; + const counter = level.counts.get(siblingKey) || 0; - // Calculate position (total children at this level so far) - let position = 0; - for (const count of siblings.values()) { - position += count; - } + // Position = total children at this level seen before this one. + const position = level.total; - // Update sibling count for this tag - siblings.set(siblingKey, counter + 1); + // Update sibling count for this tag, and the level's running total. + level.counts.set(siblingKey, counter + 1); + level.total++; // Create new node const node = { @@ -84582,7 +84591,7 @@ class Matcher { snapshot() { return { path: this.path.map(node => ({ ...node })), - siblingStacks: this.siblingStacks.map(map => new Map(map)), + siblingStacks: this.siblingStacks.map(level => level ? { counts: new Map(level.counts), total: level.total } : level), keptAttrs: this._keptAttrs.map(entry => ({ ...entry })) }; } @@ -84594,7 +84603,7 @@ class Matcher { restore(snapshot) { this._pathStringCache = null; this.path = snapshot.path.map(node => ({ ...node })); - this.siblingStacks = snapshot.siblingStacks.map(map => new Map(map)); + this.siblingStacks = snapshot.siblingStacks.map(level => level ? { counts: new Map(level.counts), total: level.total } : level); this._keptAttrs = (snapshot.keptAttrs || []).map(entry => ({ ...entry })); } @@ -86604,6 +86613,389 @@ class XmlNode { } } +;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/node_modules/xml-naming/src/index.js +/** + * xml-naming + * Validates XML Name productions as defined in the XML 1.0 and 1.1 specifications. + * Covers: Name, NCName, QName, NMToken, NMTokens + * + * XML 1.0 spec: https://www.w3.org/TR/xml/#NT-Name + * XML 1.1 spec: https://www.w3.org/TR/xml11/#NT-NameStartChar + * XML NS spec: https://www.w3.org/TR/xml-names/#NT-NCName + */ + +// --------------------------------------------------------------------------- +// Character class strings — XML 1.0 +// +// NameStartChar ::= ":" | [A-Z] | "_" | [a-z] +// | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] +// | [#x370-#x37D] | [#x37F-#x1FFF] <- split to exclude #x0487 +// | [#x200C-#x200D] +// | [#x2070-#x218F] | [#x2C00-#x2FEF] +// | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] +// +// NameChar ::= NameStartChar | "-" | "." | [0-9] +// | #xB7 | [#x0300-#x036F] | [#x203F-#x2040] +// +// Note: \u0487 (Combining Cyrillic Millions Sign) was added in Unicode 4.0, +// after XML 1.0 was defined against Unicode 2.0. It falls inside the range +// \u037F-\u1FFF but must be excluded. We split that range into +// \u037F-\u0486 and \u0488-\u1FFF to exclude it explicitly. +// --------------------------------------------------------------------------- + +const src_nameStartChar10 = + ':A-Za-z_' + + '\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF' + + '\u0370-\u037D' + + '\u037F-\u0486\u0488-\u1FFF' + // split to exclude \u0487 + '\u200C-\u200D' + + '\u2070-\u218F' + + '\u2C00-\u2FEF' + + '\u3001-\uD7FF' + + '\uF900-\uFDCF' + + '\uFDF0-\uFFFD'; + +const src_nameChar10 = + src_nameStartChar10 + + '\\-\\.\\d' + + '\u00B7' + + '\u0300-\u036F' + + '\u203F-\u2040'; + +// --------------------------------------------------------------------------- +// Character class strings — XML 1.1 +// +// Differences from XML 1.0: +// +// NameStartChar: +// 1.0 has split ranges: \u00C0-\u00D6, \u00D8-\u00F6, \u00F8-\u02FF +// 1.1 merges them into: \u00C0-\u02FF +// (\u00D7 x and \u00F7 / are division symbols, excluded in both versions) +// +// 1.0 tops out at \uFFFD (BMP only) +// 1.1 adds \u{10000}-\u{EFFFF} (supplementary planes) +// These require the /u flag on the RegExp — see buildRegexes below. +// +// NameChar: +// 1.1 adds \u0487 (Combining Cyrillic Millions Sign, added in Unicode 4.0) +// --------------------------------------------------------------------------- + +const src_nameStartChar11 = + ':A-Za-z_' + + '\u00C0-\u02FF' + // merged — 1.0 had three split ranges here + '\u0370-\u037D' + + '\u037F-\u0486\u0488-\u1FFF' + // split to exclude \u0487 (combining mark, never a NameStartChar) + '\u200C-\u200D' + + '\u2070-\u218F' + + '\u2C00-\u2FEF' + + '\u3001-\uD7FF' + + '\uF900-\uFDCF' + + '\uFDF0-\uFFFD' + + '\u{10000}-\u{EFFFF}'; // supplementary planes — REQUIRES /u flag on RegExp + +const src_nameChar11 = + src_nameStartChar11 + + '\\-\\.\\d' + + '\u00B7' + + '\u0300-\u036F' + + '\u0487' + // Combining Cyrillic Millions Sign — valid in 1.1, not 1.0 + '\u203F-\u2040'; + +// --------------------------------------------------------------------------- +// Regex builders +// +// XML 1.0 regexes: no flags — BMP only, standard JS regex behaviour. +// XML 1.1 regexes: /u flag — required for \u{10000}-\u{EFFFF} to match actual +// supplementary code points rather than lone surrogates (which are illegal XML). +// --------------------------------------------------------------------------- + +const src_buildRegexes = (startChar, char, flags = '') => { + const ncStart = startChar.replace(':', ''); + const ncChar = char.replace(':', ''); + const ncNamePat = `[${ncStart}][${ncChar}]*`; + + return { + name: new RegExp(`^[${startChar}][${char}]*$`, flags), + ncName: new RegExp(`^${ncNamePat}$`, flags), + qName: new RegExp(`^${ncNamePat}(?::${ncNamePat})?$`, flags), + nmToken: new RegExp(`^[${char}]+$`, flags), + nmTokens: new RegExp(`^[${char}]+(?:\\s+[${char}]+)*$`, flags), + }; +}; + +const src_regexes10 = src_buildRegexes(src_nameStartChar10, src_nameChar10); // no /u — BMP only +const src_regexes11 = src_buildRegexes(src_nameStartChar11, src_nameChar11, 'u'); // /u — enables \u{10000}-\u{EFFFF} + +// --------------------------------------------------------------------------- +// ASCII-only fast path (opt-in, off by default) +// +// The XML 1.0 vs 1.1 NameStartChar/NameChar productions differ *only* in +// their non-ASCII ranges (merged vs split Latin-1 ranges, \u0487, and +// supplementary planes). Restricted to ASCII, both versions collapse to the +// same character classes, so a single regex pair covers both xmlVersion +// values — no /u flag needed. +// +// Rationale: unicode-aware regexes (the /u flag, required for XML 1.1's +// supplementary-plane range) are measurably slower in V8 than plain +// non-unicode regexes on the same input, even when the input is pure ASCII. +// For the common case — HTML/SVG ids, XML tags — names are ASCII, so callers +// who know this can opt in to skip the unicode-aware matching path entirely. +// This is a real but *conditional* win: mainly for XML 1.1 input (avoids /u), +// or at scale where the larger unicode character classes add engine +// overhead. It also changes behaviour (rejects legitimate non-ASCII XML +// 1.0/1.1 names), so it must never be silently enabled — hence off by +// default. +// --------------------------------------------------------------------------- + +const nameStartCharAscii = ':A-Za-z_'; +const nameCharAscii = nameStartCharAscii + '\\-\\.\\d'; + +const regexesAscii = src_buildRegexes(nameStartCharAscii, nameCharAscii); // no /u — ASCII only + +const src_getRegexes = (xmlVersion = '1.0', asciiOnly = false) => { + if (asciiOnly) return regexesAscii; + return xmlVersion === '1.1' ? src_regexes11 : src_regexes10; +}; + +// --------------------------------------------------------------------------- +// Boolean validators +// --------------------------------------------------------------------------- + +/** + * Returns true if the string is a valid XML Name. + * Colons are allowed anywhere (Name production). + * Used for: DOCTYPE entity names, notation names, DTD element declarations. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const xml_naming_src_name = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).name.test(str); + +/** + * Returns true if the string is a valid NCName (Non-Colonized Name). + * Colons are not permitted. + * Used for: namespace prefixes, local names, SVG id attributes. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const src_ncName = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).ncName.test(str); + +/** + * Returns true if the string is a valid QName (Qualified Name). + * Allows exactly one colon as a prefix separator: prefix:localName. + * Used for: element and attribute names in namespace-aware XML/SVG. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const src_qName = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).qName.test(str); + +/** + * Returns true if the string is a valid NMToken. + * Like Name but no restriction on the first character. + * Used for: DTD NMTOKEN attribute values. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const src_nmToken = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).nmToken.test(str); + +/** + * Returns true if the string is a valid NMTokens value. + * A whitespace-separated list of NMToken values. + * Used for: DTD NMTOKENS attribute values. + * + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * asciiOnly: skip unicode-aware matching, ASCII names only (default false). + */ +const src_nmTokens = (str, { xmlVersion = '1.0', asciiOnly = false } = {}) => + src_getRegexes(xmlVersion, asciiOnly).nmTokens.test(str); + +// --------------------------------------------------------------------------- +// Memoized validator factory +// +// Real documents reuse a small vocabulary of tag/attribute names across many +// siblings (e.g. `id`, `class`, `href` repeated across hundreds of elements). +// The plain boolean validators above re-run the regex on every call +// regardless of repeats. `createValidator` returns a closure with a private +// string -> boolean cache, so repeated names after the first become O(1) +// lookups instead of regex tests. +// +// - opts (xmlVersion, asciiOnly) are fixed at creation time, so the regex is +// resolved once, not on every call. +// - The cache is private to the returned closure — no shared/global state, +// no cross-caller pollution. +// - `maxCacheSize` bounds memory: once the cache reaches this many entries, +// it stops accepting new ones (existing entries keep serving hits; new +// misses just fall through to the regex, uncached). This avoids unbounded +// growth against adversarial/high-cardinality input (e.g. validating +// attacker-supplied names with no repeats) without the cost/complexity of +// a full LRU, and without the perf cliff of reset-and-refill thrashing. +// - Call `.reset()` on the returned function to clear the cache manually +// (e.g. between unrelated parse calls). +// --------------------------------------------------------------------------- + +const src_PRODUCTIONS = (/* unused pure expression or super */ null && (['name', 'ncName', 'qName', 'nmToken', 'nmTokens'])); + +/** + * Returns a memoized boolean validator function for a single production, + * with opts fixed at creation time. + * + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean, maxCacheSize?: number }} [opts] + * maxCacheSize: max number of distinct strings to cache (default 2048). + * Once reached, new strings are validated but not cached; existing cached + * entries keep being served. + * @returns {((str: string) => boolean) & { reset: () => void }} + */ +const createValidator = (production, { xmlVersion = '1.0', asciiOnly = false, maxCacheSize = 2048 } = {}) => { + if (!src_PRODUCTIONS.includes(production)) { + throw new TypeError( + `Unknown production "${production}". Must be one of: ${src_PRODUCTIONS.join(', ')}` + ); + } + + const regex = src_getRegexes(xmlVersion, asciiOnly)[production]; + let cache = new Map(); + + const validator = (str) => { + const cached = cache.get(str); + if (cached !== undefined) return cached; + + const result = regex.test(str); + if (cache.size < maxCacheSize) cache.set(str, result); + return result; + }; + + validator.reset = () => { cache = new Map(); }; + + return validator; +}; + +// --------------------------------------------------------------------------- +// Diagnostic validator +// --------------------------------------------------------------------------- + +/** + * Validates a string against a named production and returns a detailed result. + * + * @param {string} str + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * @returns {{ valid: boolean, production: string, input: string, reason?: string, position?: number }} + */ +const src_validate = (str, production, { xmlVersion = '1.0', asciiOnly = false } = {}) => { + if (!src_PRODUCTIONS.includes(production)) { + throw new TypeError( + `Unknown production "${production}". Must be one of: ${src_PRODUCTIONS.join(', ')}` + ); + } + + const validators = { name: xml_naming_src_name, ncName: src_ncName, qName: src_qName, nmToken: src_nmToken, nmTokens: src_nmTokens }; + const isValid = validators[production](str, { xmlVersion, asciiOnly }); + + if (isValid) return { valid: true, production, input: str }; + + let reason = 'Does not match the production rules'; + let position; + + // Diagnostic fallback char checks must mirror the same character set the + // boolean validator above used, or the reported reason/position could + // contradict the `valid: false` result (e.g. flagging a char as illegal + // that the unicode-aware check would have accepted). + const startCharPattern = asciiOnly ? /^[:A-Za-z_]/ : /^[:A-Za-z_\u00C0-\uFFFD]/; + const namePattern = asciiOnly ? /[\w\-\\.:]/ : /[\w\-\\.:\u00B7\u00C0-\uFFFD]/; + + if (str.length === 0) { + reason = 'Input is empty'; + } else if (production === 'ncName' && str.includes(':')) { + position = str.indexOf(':'); + reason = 'Colon is not allowed in NCName'; + } else if (production === 'qName' && str.startsWith(':')) { + reason = 'QName cannot start with a colon'; + position = 0; + } else if (production === 'qName' && str.endsWith(':')) { + reason = 'QName cannot end with a colon'; + position = str.length - 1; + } else if (production === 'qName' && (str.match(/:/g) || []).length > 1) { + reason = 'QName can have at most one colon'; + position = str.lastIndexOf(':'); + } else if ( + ['name', 'ncName', 'qName'].includes(production) && + !startCharPattern.test(str[0]) + ) { + reason = `First character "${str[0]}" is not a valid NameStartChar`; + position = 0; + } else { + for (let i = 0; i < str.length; i++) { + if (!namePattern.test(str[i])) { + reason = `Character "${str[i]}" at position ${i} is not a valid NameChar`; + position = i; + break; + } + } + } + + return { valid: false, production, input: str, reason, position }; +}; + +// --------------------------------------------------------------------------- +// Batch validator +// --------------------------------------------------------------------------- + +/** + * Validates an array of strings against a named production. + * + * @param {string[]} strings + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ xmlVersion?: '1.0'|'1.1', asciiOnly?: boolean }} [opts] + * @returns {Array<{ valid: boolean, production: string, input: string, reason?: string, position?: number }>} + */ +const src_validateAll = (strings, production, opts = {}) => + strings.map(str => src_validate(str, production, opts)); + +// --------------------------------------------------------------------------- +// Sanitizer +// --------------------------------------------------------------------------- + +/** + * Transforms an invalid string into the nearest valid XML name for the given production. + * + * @param {string} str + * @param {'name'|'ncName'|'qName'|'nmToken'|'nmTokens'} production + * @param {{ replacement?: string, asciiOnly?: boolean }} [opts] + * asciiOnly: also replace any non-ASCII character, not just XML-illegal + * ones (default false). + * @returns {string} + */ +const src_sanitize = (str, production = 'name', { replacement = '_', asciiOnly = false } = {}) => { + if (!str) return replacement; + + let result = str; + + // Strip colons for NCName + if (production === 'ncName') { + result = result.replace(/:/g, ''); + } + + // Replace illegal characters + const allowedCharPattern = asciiOnly ? /[^\w\-\.:]/g : /[^\w\-\.:\u00B7\u00C0-\uFFFD]/g; + result = result.replace(allowedCharPattern, replacement); + + // Fix invalid start character for Name / NCName / QName + if (production !== 'nmToken' && production !== 'nmTokens') { + if (/^[\-\.\d]/.test(result)) { + result = replacement + result; + } + } + + return result || replacement; +}; ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/DocTypeReader.js @@ -86837,7 +87229,7 @@ class DocTypeReader { let elementName = xmlData.substring(startIndex, i); // Validate element name - if (!this.suppressValidationErr && !qName(elementName, { xmlVersion: this.xmlVersion })) { + if (!this.suppressValidationErr && !src_qName(elementName, { xmlVersion: this.xmlVersion })) { throw new Error(`Invalid element name: "${elementName}"`); } @@ -87011,7 +87403,7 @@ function hasSeq(data, seq, i) { } function validateEntityName(name, xmlVersion) { - if (qName(name, { xmlVersion: xmlVersion })) + if (src_qName(name, { xmlVersion: xmlVersion })) return name; else throw new Error(`Invalid entity name ${name}`); @@ -88795,26 +89187,6 @@ const MISC_SYMBOLS = { nVDash: '⊯', }; -/** - * All entities combined (if you need everything) - * @type {Record} - */ -const ALL_ENTITIES = { - ...BASIC_LATIN, - ...LATIN_ACCENTS, - ...LATIN_EXTENDED, - ...GREEK, - ...CYRILLIC, - ...MATH, - ...MATH_ADVANCED, - ...ARROWS, - ...SHAPES, - ...PUNCTUATION, - ...CURRENCY, - ...FRACTIONS, - ...MISC_SYMBOLS, -}; - const XML = { amp: "&", apos: "'", @@ -89493,6 +89865,152 @@ class EntityDecoder { return this._applyNCRAction(effective, token, cp); } } +;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql.js +/** + * SQL context patterns — high-precision rules only. + * + * These rules have very low false-positive risk and are safe to apply to + * general user text (names, descriptions, search queries, etc.). + * All patterns are ReDoS-safe — unlike the `sql-injection` npm package + * which has an active CVE on its own detection regexes. + * + * For exhaustive coverage including noisier heuristics (comment sequences, + * hex literals, stacked queries with semicolons), use 'SQL-STRICT' instead. + * Apply 'SQL-STRICT' only to strings that are specifically SQL fragments, + * not to general free-text fields. + */ + +const SQL_PATTERNS = [ + { + id: 'sql-block-comment-open', + description: 'SQL block comment open: /* ... */ — unusual in legitimate user text', + pattern: /\/\*/, + }, + { + id: 'sql-union-select', + description: 'UNION SELECT — most common SQL injection aggregation attack', + pattern: /\bUNION\s{1,20}(?:ALL\s{1,20})?SELECT\b/i, + }, + { + id: 'sql-drop-table', + description: 'DROP TABLE — destructive DDL injection', + pattern: /\bDROP\s{1,20}TABLE\b/i, + }, + { + id: 'sql-drop-database', + description: 'DROP DATABASE — destructive DDL injection', + pattern: /\bDROP\s{1,20}DATABASE\b/i, + }, + { + id: 'sql-insert-into', + description: 'INSERT INTO — data injection', + pattern: /\bINSERT\s{1,20}INTO\b/i, + }, + { + id: 'sql-delete-from', + description: 'DELETE FROM — data deletion injection', + pattern: /\bDELETE\s{1,20}FROM\b/i, + }, + { + id: 'sql-update-set', + description: 'UPDATE ... SET — data modification injection', + // Allows arbitrary content between UPDATE and SET (table name, alias, etc.) + pattern: /\bUPDATE\b[\s\S]{1,60}\bSET\b/i, + }, + { + id: 'sql-exec-xp', + description: 'EXEC xp_ — MSSQL extended stored procedure execution', + pattern: /\bEXEC(?:UTE)?\s{1,20}xp_/i, + }, + { + id: 'sql-tautology-string', + description: "Classic string tautology: ' OR '1'='1 or \" OR \"1\"=\"1\"", + // Last quote is optional — injection may truncate it: ' OR '1'='1-- + pattern: /'\s{0,10}OR\s{0,10}'[^']{0,20}'\s*=\s*'[^']{0,20}/i, + }, + { + id: 'sql-tautology-numeric', + description: 'Numeric tautology: OR 1=1', + pattern: /\bOR\s{1,10}1\s*=\s*1\b/i, + }, + { + id: 'sql-always-true-zero', + description: 'Numeric tautology: OR 0=0', + pattern: /\bOR\s{1,10}0\s*=\s*0\b/i, + }, + { + id: 'sql-sleep-benchmark', + description: 'Time-based blind injection: SLEEP() or BENCHMARK()', + pattern: /\b(?:SLEEP|BENCHMARK)\s*\(/i, + }, + { + id: 'sql-waitfor-delay', + description: 'MSSQL time-based blind injection: WAITFOR DELAY', + pattern: /\bWAITFOR\s{1,20}DELAY\b/i, + }, + { + id: 'sql-char-function', + description: 'CHAR() function — used to obfuscate injected strings', + pattern: /\bCHAR\s*\(\s*\d{1,3}/i, + }, + { + id: 'sql-information-schema', + description: 'INFORMATION_SCHEMA — reconnaissance query for table/column enumeration', + pattern: /\bINFORMATION_SCHEMA\b/i, + }, +]; + +/* harmony default export */ const sql = (SQL_PATTERNS); + +;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql-strict.js +/** + * SQL-STRICT context patterns. + * + * Extends the base 'SQL' context with three additional rules that are + * effective at detecting real injections but carry a higher false-positive + * risk on general free-text input. + * + * Use 'SQL-STRICT' when: + * - The string is specifically a SQL fragment or database identifier + * - You control the input domain (e.g. a dedicated SQL search field) + * - You can tolerate occasional false positives in exchange for broader coverage + * + * Use 'SQL' (not STRICT) when: + * - The field is general user text (names, descriptions, comments) + * - False positives would block legitimate content (e.g. "see note -- above") + * + * Rules moved here from 'SQL' due to false-positive risk: + * + * sql-line-comment — "--" fires on "see note -- above", "value--", CSS var(--primary) + * sql-stacked-query — "; SELECT" fires on legitimate prose with semicolons + SQL words + * sql-hex-encoding — "0xDEAD" fires on hex values in technical docs and log output + */ + + + +const SQL_STRICT_EXTRA = [ + { + id: 'sql-line-comment', + description: 'SQL line comment: -- followed by whitespace or end of string', + pattern: /--(?:\s|$)/, + }, + { + id: 'sql-stacked-query', + description: 'Stacked queries: semicolon immediately followed by a SQL keyword', + pattern: /;\s{0,10}(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC)\b/i, + }, + { + id: 'sql-hex-encoding', + description: 'Hex-encoded string injection: 0x41414141 style (MySQL)', + pattern: /\b0x[0-9a-f]{4,}/i, + }, +]; + +// SQL-STRICT = all base SQL rules + the three noisy extras +const SQL_STRICT_PATTERNS = [...sql, ...SQL_STRICT_EXTRA]; + +/* harmony default export */ const sql_strict = (SQL_STRICT_PATTERNS); + ;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/html.js /** * HTML context patterns. @@ -89760,152 +90278,6 @@ const SVG_PATTERNS = [ /* harmony default export */ const svg = (SVG_PATTERNS); -;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql.js -/** - * SQL context patterns — high-precision rules only. - * - * These rules have very low false-positive risk and are safe to apply to - * general user text (names, descriptions, search queries, etc.). - * All patterns are ReDoS-safe — unlike the `sql-injection` npm package - * which has an active CVE on its own detection regexes. - * - * For exhaustive coverage including noisier heuristics (comment sequences, - * hex literals, stacked queries with semicolons), use 'SQL-STRICT' instead. - * Apply 'SQL-STRICT' only to strings that are specifically SQL fragments, - * not to general free-text fields. - */ - -const SQL_PATTERNS = [ - { - id: 'sql-block-comment-open', - description: 'SQL block comment open: /* ... */ — unusual in legitimate user text', - pattern: /\/\*/, - }, - { - id: 'sql-union-select', - description: 'UNION SELECT — most common SQL injection aggregation attack', - pattern: /\bUNION\s{1,20}(?:ALL\s{1,20})?SELECT\b/i, - }, - { - id: 'sql-drop-table', - description: 'DROP TABLE — destructive DDL injection', - pattern: /\bDROP\s{1,20}TABLE\b/i, - }, - { - id: 'sql-drop-database', - description: 'DROP DATABASE — destructive DDL injection', - pattern: /\bDROP\s{1,20}DATABASE\b/i, - }, - { - id: 'sql-insert-into', - description: 'INSERT INTO — data injection', - pattern: /\bINSERT\s{1,20}INTO\b/i, - }, - { - id: 'sql-delete-from', - description: 'DELETE FROM — data deletion injection', - pattern: /\bDELETE\s{1,20}FROM\b/i, - }, - { - id: 'sql-update-set', - description: 'UPDATE ... SET — data modification injection', - // Allows arbitrary content between UPDATE and SET (table name, alias, etc.) - pattern: /\bUPDATE\b[\s\S]{1,60}\bSET\b/i, - }, - { - id: 'sql-exec-xp', - description: 'EXEC xp_ — MSSQL extended stored procedure execution', - pattern: /\bEXEC(?:UTE)?\s{1,20}xp_/i, - }, - { - id: 'sql-tautology-string', - description: "Classic string tautology: ' OR '1'='1 or \" OR \"1\"=\"1\"", - // Last quote is optional — injection may truncate it: ' OR '1'='1-- - pattern: /'\s{0,10}OR\s{0,10}'[^']{0,20}'\s*=\s*'[^']{0,20}/i, - }, - { - id: 'sql-tautology-numeric', - description: 'Numeric tautology: OR 1=1', - pattern: /\bOR\s{1,10}1\s*=\s*1\b/i, - }, - { - id: 'sql-always-true-zero', - description: 'Numeric tautology: OR 0=0', - pattern: /\bOR\s{1,10}0\s*=\s*0\b/i, - }, - { - id: 'sql-sleep-benchmark', - description: 'Time-based blind injection: SLEEP() or BENCHMARK()', - pattern: /\b(?:SLEEP|BENCHMARK)\s*\(/i, - }, - { - id: 'sql-waitfor-delay', - description: 'MSSQL time-based blind injection: WAITFOR DELAY', - pattern: /\bWAITFOR\s{1,20}DELAY\b/i, - }, - { - id: 'sql-char-function', - description: 'CHAR() function — used to obfuscate injected strings', - pattern: /\bCHAR\s*\(\s*\d{1,3}/i, - }, - { - id: 'sql-information-schema', - description: 'INFORMATION_SCHEMA — reconnaissance query for table/column enumeration', - pattern: /\bINFORMATION_SCHEMA\b/i, - }, -]; - -/* harmony default export */ const sql = (SQL_PATTERNS); - -;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/sql-strict.js -/** - * SQL-STRICT context patterns. - * - * Extends the base 'SQL' context with three additional rules that are - * effective at detecting real injections but carry a higher false-positive - * risk on general free-text input. - * - * Use 'SQL-STRICT' when: - * - The string is specifically a SQL fragment or database identifier - * - You control the input domain (e.g. a dedicated SQL search field) - * - You can tolerate occasional false positives in exchange for broader coverage - * - * Use 'SQL' (not STRICT) when: - * - The field is general user text (names, descriptions, comments) - * - False positives would block legitimate content (e.g. "see note -- above") - * - * Rules moved here from 'SQL' due to false-positive risk: - * - * sql-line-comment — "--" fires on "see note -- above", "value--", CSS var(--primary) - * sql-stacked-query — "; SELECT" fires on legitimate prose with semicolons + SQL words - * sql-hex-encoding — "0xDEAD" fires on hex values in technical docs and log output - */ - - - -const SQL_STRICT_EXTRA = [ - { - id: 'sql-line-comment', - description: 'SQL line comment: -- followed by whitespace or end of string', - pattern: /--(?:\s|$)/, - }, - { - id: 'sql-stacked-query', - description: 'Stacked queries: semicolon immediately followed by a SQL keyword', - pattern: /;\s{0,10}(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC)\b/i, - }, - { - id: 'sql-hex-encoding', - description: 'Hex-encoded string injection: 0x41414141 style (MySQL)', - pattern: /\b0x[0-9a-f]{4,}/i, - }, -]; - -// SQL-STRICT = all base SQL rules + the three noisy extras -const SQL_STRICT_PATTERNS = [...sql, ...SQL_STRICT_EXTRA]; - -/* harmony default export */ const sql_strict = (SQL_STRICT_PATTERNS); - ;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/contexts/shell.js /** * SHELL context patterns. @@ -90291,32 +90663,66 @@ const LOG_PATTERNS = [ /* harmony default export */ const contexts_log = (LOG_PATTERNS); -;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/registry.js +;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/index.js /** - * Context registry — maps context name strings to their pattern arrays. + * is-unsafe v2 + * + * Zero-dependency, DOM-free, pure predicate for detecting unsafe strings + * across HTML, XML, SVG, SQL, SQL-STRICT, SHELL, REDOS, NOSQL, and LOG contexts. * - * Adding a new context: create a file in ./contexts/, export a default array - * of pattern objects, and register it here. + * v2 change: contexts are imported as named pattern arrays rather than resolved + * via a string-keyed registry. This makes each context independently + * tree-shakeable — bundlers can drop any context you never import. * - * Context name guide: - * SQL — high-precision rules; safe for general text fields - * SQL-STRICT — SQL + three noisier rules (line comments, stacked queries, hex); - * use only for SQL-specific inputs - * REDOS — detects ReDoS-prone patterns when string will be compiled as RegExp + * @module is-unsafe */ +// ─── Context pattern arrays (named exports) ──────────────────────────────── +// Import only the ones you need. Each is independently tree-shakeable. + + + + + + + + + +// SQL-STRICT needs a quoted identifier because of the hyphen +// ─── VALID_CONTEXTS convenience re-export ───────────────────────────────── +// Importing this pulls in ALL contexts. Use it only when you need all of them +// (e.g. for validation UI, tooling, or exhaustive audits). +// If you only need a subset, import the named contexts directly instead. -/** @type {Record>} */ -const registry_CONTEXT_REGISTRY = { + + + + +// ─── Attach labels to named contexts ────────────────────────────────────── +// Each built-in PatternList carries its canonical name so matchList can read +// list.label directly — no registry lookup needed at match time. +// Custom PatternLists default to 'CUSTOM' unless the caller sets list.label. + +html.label = 'HTML'; +xml.label = 'XML'; +svg.label = 'SVG'; +sql.label = 'SQL'; +sql_strict.label = 'SQL-STRICT'; +shell.label = 'SHELL'; +redos.label = 'REDOS'; +nosql.label = 'NOSQL'; +contexts_log.label = 'LOG'; + +const VALID_CONTEXTS = Object.freeze({ HTML: html, XML: xml, SVG: svg, @@ -90326,45 +90732,29 @@ const registry_CONTEXT_REGISTRY = { REDOS: redos, NOSQL: nosql, LOG: contexts_log, -}; +}); -/* harmony default export */ const registry = (registry_CONTEXT_REGISTRY); +// ─── Types ──────────────────────────────────────────────────────────────── /** - * Enum of valid context names — e.g. `VALID_CONTEXTS.HTML === 'HTML'`. - * @type {Record} - */ -const VALID_CONTEXTS = Object.freeze( - Object.fromEntries(Object.keys(registry_CONTEXT_REGISTRY).map((k) => [k, k])) -); -;// CONCATENATED MODULE: ./node_modules/is-unsafe/src/index.js -/** - * is-unsafe - * - * Zero-dependency, DOM-free, pure predicate for detecting unsafe strings - * across HTML, XML, SVG, SQL, SQL-STRICT, SHELL, REDOS, NOSQL, and LOG contexts. - * - * @module is-unsafe + * @typedef {{ id: string, description: string, pattern: RegExp }} Rule */ - - /** - * @typedef {'HTML'|'XML'|'SVG'|'SQL'|'SQL-STRICT'|'SHELL'|'REDOS'|'NOSQL'|'LOG'} ContextName + * @typedef {Rule[]} PatternList */ /** * @typedef {Object} MatchResult - * @property {string} context - The context in which the match was found - * @property {string} id - Rule identifier + * @property {string} context - Label identifying which context matched ('HTML', 'CUSTOM', etc.) + * @property {string} id - Rule identifier * @property {string} description - Human-readable description of what was matched - * @property {RegExp} pattern - The pattern that matched + * @property {RegExp} pattern - The pattern that matched */ -// ─── Validation helpers ──────────────────────────────────────────────────── +// ─── Internal helpers ────────────────────────────────────────────────────── /** - * Validate that `value` is a string. Throws TypeError if not. * @param {unknown} value */ function assertString(value) { @@ -90376,56 +90766,61 @@ function assertString(value) { } /** - * Validate that `context` is a recognised context name, an array of them, - * or a RegExp instance. Throws TypeError if not. - * @param {ContextName|ContextName[]|RegExp} context + * @param {unknown} context */ function assertContext(context) { if (context instanceof RegExp) return; - if (typeof context === 'string') { - if (!registry[context]) { - throw new TypeError( - `is-unsafe: unknown context "${context}". Valid contexts: ${Object.keys(VALID_CONTEXTS).join(', ')}` - ); - } - return; - } - if (Array.isArray(context)) { if (context.length === 0) { - throw new TypeError('is-unsafe: context array must not be empty'); + throw new TypeError('is-unsafe: context must not be an empty array'); } - for (const c of context) { - if (typeof c !== 'string' || !registry[c]) { - throw new TypeError( - `is-unsafe: unknown context "${c}" in array. Valid contexts: ${Object.keys(VALID_CONTEXTS).join(', ')}` - ); + // Detect array-of-arrays vs flat pattern list + if (Array.isArray(context[0])) { + // Array of PatternLists + for (const list of context) { + if (!Array.isArray(list) || list.length === 0) { + throw new TypeError( + 'is-unsafe: each context in the array must be a non-empty pattern array (PatternList)' + ); + } } } + // else: flat PatternList — trust it, no deep validation needed return; } throw new TypeError( - `is-unsafe: second argument must be a context string, array of context strings, or RegExp. Got: ${typeof context}` + `is-unsafe: second argument must be a PatternList (e.g. HTML), ` + + `an array of PatternLists (e.g. [HTML, XML]), or a RegExp. Got: ${typeof context}` ); } -// ─── Core matching logic ─────────────────────────────────────────────────── +/** + * Normalise any valid context arg into an array of PatternLists. + * + * @param {Rule[]|Rule[][]|RegExp} context + * @returns {{ lists: Rule[][]|null, regex: RegExp|null }} + */ +function normalise(context) { + if (context instanceof RegExp) return { lists: null, regex: context }; + // Distinguish PatternList (array of rule objects) from array of PatternLists + if (Array.isArray(context[0])) return { lists: context, regex: null }; + return { lists: [context], regex: null }; +} /** - * Test a single value against one named context's patterns. - * Returns the first matching MatchResult, or null if nothing matched. + * Test value against a single PatternList. Returns the first MatchResult or null. * * @param {string} value - * @param {string} contextName + * @param {Rule[]} list * @returns {MatchResult|null} */ -function matchContext(value, contextName) { - const patterns = registry[contextName]; - for (const rule of patterns) { +function matchList(value, list) { + const label = list.label ?? 'CUSTOM'; + for (const rule of list) { if (rule.pattern.test(value)) { - return { context: contextName, id: rule.id, description: rule.description, pattern: rule.pattern }; + return { context: label, id: rule.id, description: rule.description, pattern: rule.pattern }; } } return null; @@ -90436,103 +90831,95 @@ function matchContext(value, contextName) { /** * Returns `true` if `value` is unsafe in the given context(s), `false` otherwise. * - * @param {string} value - The string to test - * @param {ContextName|ContextName[]|RegExp} context - * - A named context ('HTML', 'XML', 'SVG', 'SQL', 'SQL-STRICT', 'SHELL', 'REDOS', 'NOSQL', 'LOG') - * - An array of named contexts — returns true if unsafe in **any** of them + * @param {string} value - The string to test + * @param {PatternList | PatternList[] | RegExp} context + * - A PatternList imported from is-unsafe (e.g. `HTML`, `XML`) + * - An array of PatternLists — returns true if unsafe in **any** of them * - A custom RegExp — returns true if the pattern matches * @returns {boolean} * * @example - * isUnsafe('', 'HTML') // true - * isUnsafe('hello world', 'HTML') // false - * isUnsafe('value', ['HTML', 'SQL']) // false - * isUnsafe('value', /my-pattern/i) // false + * import { isUnsafe, HTML, SQL } from 'is-unsafe'; + * + * isUnsafe('', HTML) // true + * isUnsafe('hello world', HTML) // false + * isUnsafe('value', [HTML, SQL]) // false + * isUnsafe('value', /my-pattern/i) // false */ function isUnsafe(value, context) { assertString(value); assertContext(context); - // Custom RegExp — caller-supplied pattern - if (context instanceof RegExp) { - return context.test(value); - } + const { lists, regex } = normalise(context); - // Single named context - if (typeof context === 'string') { - return matchContext(value, context) !== null; - } + if (regex) return regex.test(value); - // Array of named contexts — unsafe if ANY context matches - for (const c of context) { - if (matchContext(value, c) !== null) return true; + for (const list of lists) { + if (matchList(value, list) !== null) return true; } return false; } /** - * Like `isUnsafe`, but instead of a boolean returns the first `MatchResult` - * describing **why** the value was flagged, or `null` if it is safe. - * - * Useful for logging, error messages, or policy reporting. + * Like `isUnsafe`, but returns the first `MatchResult` describing **why** + * the value was flagged, or `null` if it is safe. * * @param {string} value - * @param {ContextName|ContextName[]|RegExp} context + * @param {PatternList | PatternList[] | RegExp} context * @returns {MatchResult|null} * * @example - * whyUnsafe('', 'HTML') + * import { whyUnsafe, HTML } from 'is-unsafe'; + * + * whyUnsafe('', HTML) * // { context: 'HTML', id: 'html-script-open', description: '...', pattern: /.../ } */ function whyUnsafe(value, context) { assertString(value); assertContext(context); - if (context instanceof RegExp) { - return context.test(value) - ? { context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: context } - : null; - } + const { lists, regex } = normalise(context); - if (typeof context === 'string') { - return matchContext(value, context); + if (regex) { + return regex.test(value) + ? { context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: regex } + : null; } - for (const c of context) { - const result = matchContext(value, c); + for (const list of lists) { + const result = matchList(value, list); if (result !== null) return result; } return null; } /** - * Returns all matching rules across the given context(s), or an empty array - * if the value is safe. Useful for comprehensive auditing. + * Returns **all** matching rules across the given context(s), or an empty + * array if the value is safe. Useful for comprehensive auditing. * * @param {string} value - * @param {ContextName|ContextName[]|RegExp} context + * @param {PatternList | PatternList[] | RegExp} context * @returns {MatchResult[]} */ function allUnsafe(value, context) { assertString(value); assertContext(context); + const { lists, regex } = normalise(context); const results = []; - if (context instanceof RegExp) { - if (context.test(value)) { - results.push({ context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: context }); + if (regex) { + if (regex.test(value)) { + results.push({ context: 'CUSTOM', id: 'custom-regex', description: 'Matched caller-supplied pattern', pattern: regex }); } return results; } - const contexts = typeof context === 'string' ? [context] : context; - - for (const c of contexts) { - const patterns = CONTEXT_REGISTRY[c]; - for (const rule of patterns) { + for (const list of lists) { + const label = list.label ?? 'CUSTOM'; + for (const rule of list) { if (rule.pattern.test(value)) { - results.push({ context: c, id: rule.id, description: rule.description, pattern: rule.pattern }); + results.push({ context: label, id: rule.id, description: rule.description, pattern: rule.pattern }); } } } @@ -90542,6 +90929,7 @@ function allUnsafe(value, context) { /* harmony default export */ const src = ((/* unused pure expression or super */ null && (isUnsafe))); + ;// CONCATENATED MODULE: ./node_modules/fast-xml-parser/src/xmlparser/OrderedObjParser.js ///@ts-check @@ -90632,6 +91020,7 @@ class OrderedObjParser { this.ignoreAttributesFn = ignoreAttributes_getIgnoreAttributesFn(this.options.ignoreAttributes) this.entityExpansionCount = 0; this.currentExpandedLength = 0; + this.doctypefound = false; let namedEntities = { ...XML }; if (this.options.entityDecoder) { this.entityDecoder = this.options.entityDecoder @@ -90649,8 +91038,7 @@ class OrderedObjParser { // onExternalEntity: (name, value) => isUnsafe(value) ? 'block' : 'allow', onInputEntity: (name, value) => //TODO: VALID_CONTEXTS.HTML should be set only if this.options.htmlEntities - isUnsafe(value, [VALID_CONTEXTS.HTML, VALID_CONTEXTS.XML]) - ? ENTITY_ACTION.BLOCK : ENTITY_ACTION.ALLOW, + isUnsafe(value, [html, xml]) ? ENTITY_ACTION.BLOCK : ENTITY_ACTION.ALLOW, //postCheck: resolved => resolved }); @@ -90840,6 +91228,7 @@ const parseXml = function (xmlData) { // Reset entity expansion counters for this document this.entityExpansionCount = 0; this.currentExpandedLength = 0; + this.doctypefound = false; const options = this.options; const docTypeReader = new DocTypeReader(options.processEntities); const xmlLen = xmlData.length; @@ -90924,6 +91313,8 @@ const parseXml = function (xmlData) { i = endIndex; } else if (c1 === 33 && xmlData.charCodeAt(i + 2) === 68) { //'!D' + if (this.doctypefound) throw new Error("Multiple DOCTYPE declarations found."); + this.doctypefound = true; const result = docTypeReader.readDocType(xmlData, i); this.entityDecoder.addInputEntities(result.entities); i = result.i; diff --git a/package-lock.json b/package-lock.json index 19dfa9425..de260fd15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,9 +77,9 @@ "license": "MIT" }, "node_modules/@actions/cache/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1601,9 +1601,9 @@ } }, "node_modules/@nodable/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.0.0.tgz", + "integrity": "sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==", "funding": [ { "type": "github", @@ -3985,9 +3985,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.9.3.tgz", - "integrity": "sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==", + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.10.1.tgz", + "integrity": "sha512-IEMIf7298kXuZSRFoGfMYrl7is8LpavODgbNz1cwIudv7KwVFnuU+UsMporfq6PD6aXSlawZlARiA3UywCTfMw==", "funding": [ { "type": "github", @@ -3996,17 +3996,32 @@ ], "license": "MIT", "dependencies": { - "@nodable/entities": "^2.2.0", + "@nodable/entities": "^3.0.0", "fast-xml-builder": "^1.2.0", - "is-unsafe": "^1.0.1", - "path-expression-matcher": "^1.5.0", + "is-unsafe": "^2.0.0", + "path-expression-matcher": "^1.6.2", "strnum": "^2.4.1", - "xml-naming": "^0.1.0" + "xml-naming": "^0.3.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, + "node_modules/fast-xml-parser/node_modules/xml-naming": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", + "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -4237,9 +4252,9 @@ "license": "MIT" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -4509,9 +4524,9 @@ } }, "node_modules/is-unsafe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-1.0.1.tgz", - "integrity": "sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.0.tgz", + "integrity": "sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==", "funding": [ { "type": "github", @@ -5839,9 +5854,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.1.tgz", - "integrity": "sha512-h7bxdzhHk8Knyc4Tj+jMaa7fEEoUJy7p1qtbVgkYg1Uhpe5Np5VuGXCRZnkZvU+Q42M1vStt0ifa3ueykRJPmQ==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", "funding": [ { "type": "github", @@ -6591,9 +6606,9 @@ "license": "MIT" }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": {