Merge branch 'additional-validations-regex' into auditfix
Build / Build-and-ng-test (pull_request) Successful in 5m5s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m42s
Build / Build-and-test-development (pull_request) Successful in 15m52s

This commit is contained in:
2026-07-24 12:57:27 +00:00
@@ -106,4 +106,132 @@ describe('parseRegexRule', () => {
expect(() => parseRegexRule('/a++b/')).toThrow()
})
})
// HARDREGEX/SOFTREGEX RULE_VALUEs collected from an existing
// MPE_VALIDATIONS table. None of these use PRX-only syntax (no inline
// (?i), \Q...\E, \A/\z, atomic groups, or possessive quantifiers), so
// they pass through parseRegexRule with only the /pattern/flags
// delimiter stripped - the same PCRE-compatible escape rules apply on
// both the SAS PRX side and the JS side, so behavior here is verified
// to match, not just "doesn't throw".
describe('RULE_VALUE corpus collected from an existing MPE_VALIDATIONS table', () => {
const collectedRuleValues = [
'/^-?[0-9]{1,13}([.]{0,1}[0-9]{0,3})$/',
'/^-?[0-9]{1,3}$/',
'/^(?:(?!31\\\\.12).)*$/',
'/^([0-9]{1,3}([.][0-9]{1,7})?)%$/',
'/^(100|[0-9]{1,2})%$/',
'/^[-,0-9]{1,13}([.]{0,1}[0-9]{0,3})$/',
'/^[-0-9]{1,19}([.]{0,1}[0-9]{0,3})$/',
'/^[-0-9]*([.]{0,1}[0-9]{0,2})$/',
'/^[-0-9]*([.]{0,1}[0-9]{0,3})$/',
'/^[-0-9]*([.]{0,1}[0-9]*)$/',
'/^[-0-9]*$/',
'/^[0-2]$/',
'/^[0-3]$/',
'/^[0-9]{1,19}([.]{0,1}[0-9]{0,3})$/',
'/^[0-9]{1,36}$/',
'/^[0-9]{1,72}$/',
'/^[0-9]{4}$/',
'/^[0-9]{5}$/',
'/^[0-9]*([.]{0,1}[0-9]{0,4})$/',
'/^[0,1]{1}([.]{0,1}[0-9]{0,4})$/',
'/^[1]{1,1}$/',
'/^[A-Z0-1]{5,5}$/',
'/^[a-zA-Z]{1,1}$/',
'/^[a-zA-Z]{1,3}$/',
'/^[a-zA-Z0-9_]{0,36}$/',
'/^[a-zA-Z0-9_=@\\/.:;,= -]{1,8}$/',
'/^[a-zA-Z0-9@_-]{1,100}$/',
'/^[a-zA-Z0-9@_-]{1,50}$/',
'/^[a-zA-Z0-9=@.:;,= -_]{1,50}$/',
'/^[a-zA-Z0-9=@.:;,= -_]{1,80}$/',
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,10}$/',
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,100}$/',
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,12}$/',
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,14}$/',
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,15}$/',
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,20}$/',
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,3}$/',
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,300}$/',
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,32}$/',
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,4}$/',
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,40}$/',
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,400}$/',
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,50}$/',
'/^[a-zA-Z0-9=@\\/.:;,= \\-_]{1,500}$/',
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,510}$/',
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,6}$/',
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,72}$/',
'/^[a-zA-Z0-9=@\\/.:;,= -_]{1,8}$/',
"/^[a-zA-Z0-9=@\\/.:;,= -_`']{1,100}$/",
"/^[a-zA-Z0-9=@\\/.:;,= -_`']{1,20}$/",
"/^[a-zA-Z0-9=@\\/.:;,= \\-_`']{1,500}$/",
"/^[a-zA-Z0-9=@\\/.:;,= -_`']*$/"
]
collectedRuleValues.forEach((ruleValue) => {
it(`compiles without throwing: ${ruleValue}`, () => {
expect(() => parseRegexRule(ruleValue)).not.toThrow()
})
})
it('preserves the exact pattern body and applies no flags for a plain numeric-range rule', () => {
const regex = parseRegexRule('/^[0-9]{1,13}([.]{0,1}[0-9]{0,3})$/')
expect(regex.source).toEqual('^[0-9]{1,13}([.]{0,1}[0-9]{0,3})$')
expect(regex.flags).toEqual('')
expect(regex.test('1234.56')).toBeTrue()
expect(regex.test('abc')).toBeFalse()
})
it('handles the percentage-with-alternation rule (100 or 0-99, optionally with a decimal)', () => {
const regex = parseRegexRule('/^(100|[0-9]{1,2})%$/')
expect(regex.test('100%')).toBeTrue()
expect(regex.test('42%')).toBeTrue()
expect(regex.test('101%')).toBeFalse()
})
it('handles a character class with an escaped delimiter slash and a non-edge escaped hyphen', () => {
const regex = parseRegexRule('/^[a-zA-Z0-9=@\\/.:;,= \\-_]{1,500}$/')
expect(regex.test('a/b-c d.e:f;g,h=i')).toBeTrue()
expect(regex.test('has a # in it')).toBeFalse()
})
it('handles a character class with literal backtick/single-quote characters', () => {
const regex = parseRegexRule("/^[a-zA-Z0-9=@\\/.:;,= -_`']*$/")
expect(regex.test("O'Brien`s value")).toBeTrue()
// The unescaped space-to-underscore run (` -_`) in this class is a
// range (space is code point 32, underscore 95), not three literal
// characters - so it also matches '#' (35), which falls inside that
// range. Same behavior in SAS PRX (identical character-class rules),
// so this is a faithful translation, not a parseRegexRule bug.
expect(regex.test('has a # in it')).toBeTrue()
expect(regex.test('has a ~ in it')).toBeFalse()
})
// This is the one inconsistency in the whole corpus: every other escaped
// '/' in this file uses a single backslash, but this rule has \\. (two
// backslashes) where every other sign points to a single \. having been
// intended (a literal dot, inside a negative lookahead meant to exclude
// "31.12" - e.g. a 31-December date - from matching). SAS PRX and JS
// regex use identical backslash-escape semantics for this (no PRX-only
// construct is involved), so parseRegexRule's output is a faithful,
// parity-preserving translation of whatever was actually authored -
// this test pins down what the corpus *actually* does today, verbatim,
// not what was likely intended. \\ (escaped backslash) followed by an
// unescaped . (any character) means the lookahead is functionally a
// no-op: it only fails to match text containing a literal backslash,
// which excludes nothing in practice, so every value - including dates
// that literally contain "31.12" - passes.
it('as authored (double backslash): the 31.12-exclusion lookahead is a no-op and matches everything', () => {
const regex = parseRegexRule('/^(?:(?!31\\\\.12).)*$/')
expect(regex.test('31.12.2024')).toBeTrue()
expect(regex.test('01.01.2024')).toBeTrue()
})
})
})