From 75b7385efa314495b59a6d23d36a2b90ba20999f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:06:08 +0000 Subject: [PATCH 01/17] Initial plan From f00514a5d618f66139a81bdf563a6a1cdaed3eeb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:25:19 +0000 Subject: [PATCH 02/17] =?UTF-8?q?feat(tests):=20comprehensive=20RFC=203987?= =?UTF-8?q?=20=C2=A72.2=20grammar=20test=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Populate all 30+ previously empty arrays in valid_syntax.json with RFC-driven test cases covering every grammar production in RFC 3987 §2.2: ucschar, iunreserved, sub_delims, iuserinfo, h16, dec_octet, ls32, ipv6address (all 9 forms), unreserved, ipvfuture, ip_literal, ireg_name, ihost, port, iauthority, ipchar, isegment, ipath_abempty, isegment_nz, ipath_absolute, ipath_rootless, ipath_empty, ihier_part, iprivate, iquery, gen_delims, ifragment, isegment_nz_nc, ipath_noscheme, ipath, irelative_part, irelative_ref, reserved, scheme, pct_encoded, ipv4address - Add invalid test cases to 5 previously empty sections in invalid_syntax.json: isegment_nz_nc, ipath_noscheme, ipath, irelative_part, irelative_ref - Remove RFC-wrong entry: a new IRI entry added by the script using literal ASCII asterisks '******example.com:...' was invalid (asterisk is not a valid scheme character; scheme must start with ALPHA). Removed. - All existing entries preserved (no deduplication). - Source of truth: RFC 3987 §2.2 grammar exclusively. - No tests were run per task instructions. --- tests/_data/invalid_syntax.json | 209 ++++- tests/_data/valid_syntax.json | 1443 ++++++++++++++++++++++++++++++- 2 files changed, 1603 insertions(+), 49 deletions(-) diff --git a/tests/_data/invalid_syntax.json b/tests/_data/invalid_syntax.json index ab48fa1..26ac403 100644 --- a/tests/_data/invalid_syntax.json +++ b/tests/_data/invalid_syntax.json @@ -1747,11 +1747,208 @@ "reason": "Caret in fragment" } ], - "isegment_nz_nc": [], - "ipath_noscheme": [], - "ipath": [], - "irelative_part": [], - "irelative_ref": [], + "isegment_nz_nc": [ + { + "value": "", + "reason": "Empty string: isegment-nz-nc requires at least one char (1+)" + }, + { + "value": ":", + "reason": "Colon forbidden in isegment-nz-nc (no-colon segment constraint)" + }, + { + "value": "a:b", + "reason": "Colon within the segment violates no-colon constraint" + }, + { + "value": " ", + "reason": "Space not in iunreserved / pct-encoded / sub-delims / '@'" + }, + { + "value": "#", + "reason": "Fragment delimiter # is a gen-delim, not allowed in isegment-nz-nc" + }, + { + "value": "[", + "reason": "Left bracket is a gen-delim, not allowed in isegment-nz-nc" + }, + { + "value": "]", + "reason": "Right bracket is a gen-delim, not allowed in isegment-nz-nc" + }, + { + "value": "%GG", + "reason": "Invalid pct-encoded: G is not a hex digit" + }, + { + "value": "|", + "reason": "Pipe is not in the allowed character set for isegment-nz-nc" + }, + { + "value": "\\", + "reason": "Backslash is not in the allowed set for isegment-nz-nc" + }, + { + "value": "/", + "reason": "Slash separates segments; not part of a single isegment-nz-nc" + } + ], + "ipath_noscheme": [ + { + "value": ":segment", + "reason": "Colon starts first segment; colon forbidden in isegment-nz-nc" + }, + { + "value": "a:b", + "reason": "Colon in the sole first segment violates isegment-nz-nc" + }, + { + "value": "/segment", + "reason": "ipath-noscheme must not begin with '/'; that is ipath-abempty or ipath-absolute" + }, + { + "value": "//segment", + "reason": "Double slash is not a valid start for ipath-noscheme" + }, + { + "value": "seg ment", + "reason": "Space not allowed in path segment" + }, + { + "value": "seg#ment", + "reason": "Hash introduces a fragment delimiter; not part of ipath" + }, + { + "value": "seg[ment", + "reason": "Left bracket is a gen-delim, forbidden in path segments" + }, + { + "value": "seg|ment", + "reason": "Pipe is not allowed in path segment" + }, + { + "value": "seg%GG/sub", + "reason": "Invalid pct-encoded sequence in first segment" + } + ], + "ipath": [ + { + "value": "seg ment", + "reason": "Space is not allowed in any ipath alternative" + }, + { + "value": "seg|ment", + "reason": "Pipe is not allowed in any path segment" + }, + { + "value": "seg\\ment", + "reason": "Backslash is not allowed in path segments" + }, + { + "value": "seg[ment", + "reason": "Left bracket is a gen-delim, forbidden in path" + }, + { + "value": "seg]ment", + "reason": "Right bracket is a gen-delim, forbidden in path" + }, + { + "value": "seg{ment", + "reason": "Left curly brace not allowed in path" + }, + { + "value": "seg}ment", + "reason": "Right curly brace not allowed in path" + }, + { + "value": "seg^ment", + "reason": "Caret not allowed in path" + }, + { + "value": "seg\tment", + "reason": "Tab character not allowed in path" + }, + { + "value": "seg\nment", + "reason": "Newline not allowed in path" + }, + { + "value": "/seg%GG", + "reason": "Invalid pct-encoded sequence in path segment" + } + ], + "irelative_part": [ + { + "value": "//example.com:abc", + "reason": "Non-numeric port in authority" + }, + { + "value": "//exa mple.com", + "reason": "Space in reg-name within authority" + }, + { + "value": "/seg ment", + "reason": "Space in ipath-absolute segment" + }, + { + "value": ":segment", + "reason": "Colon starts ipath-noscheme first segment, forbidden by isegment-nz-nc" + }, + { + "value": "a:b", + "reason": "Colon in first segment of ipath-noscheme alternative: forbidden" + }, + { + "value": "seg|ment", + "reason": "Pipe not allowed in path segment" + }, + { + "value": "//host/%GG", + "reason": "Invalid pct-encoded in authority path" + }, + { + "value": "//[v1.]", + "reason": "Malformed IPvFuture in IP-literal (suffix missing after '.')" + } + ], + "irelative_ref": [ + { + "value": "//example.com/path#frag1#frag2", + "reason": "Multiple fragment delimiters; only one '#' allowed per IRI" + }, + { + "value": "//host path", + "reason": "Unencoded space in relative ref" + }, + { + "value": "//example.com/%", + "reason": "Incomplete pct-encoded sequence in path" + }, + { + "value": "//example.com:abc/path", + "reason": "Non-numeric port in relative ref authority" + }, + { + "value": "a:b", + "reason": "Colon in first segment of irelative-part: invalid as ipath-noscheme" + }, + { + "value": ":path", + "reason": "Starts with colon: invalid first char for ipath-noscheme (isegment-nz-nc)" + }, + { + "value": "seg\nment", + "reason": "Newline not allowed in irelative-ref" + }, + { + "value": "seg|ment", + "reason": "Pipe not allowed in irelative-ref path" + }, + { + "value": "seg%GG", + "reason": "Invalid pct-encoded sequence in irelative-ref" + } + ], "iri": [ { "value": "http://example.com/%ZZ", @@ -2066,4 +2263,4 @@ "reason": "Only three octets" } ] -} \ No newline at end of file +} diff --git a/tests/_data/valid_syntax.json b/tests/_data/valid_syntax.json index a6bba92..1d5dfc3 100644 --- a/tests/_data/valid_syntax.json +++ b/tests/_data/valid_syntax.json @@ -1,29 +1,934 @@ { - "ucschar": [], - "iunreserved": [], - "sub_delims": [], - "iuserinfo": [], - "h16": [], - "dec_octet": [], - "ls32": [], - "ipv6address": [], - "unreserved": [], - "ipvfuture": [], - "ip_literal": [], - "ireg_name": [], - "ihost": [], - "port": [], - "iauthority": [], - "ipchar": [], - "isegment": [], - "ipath_abempty": [], - "isegment_nz": [], - "ipath_absolute": [], - "ipath_rootless": [], - "ipath_empty": [], - "ihier_part": [], - "iprivate": [], - "iquery": [], + "ucschar": [ + { + "value": " ", + "reason": "U+00A0 NO-BREAK SPACE: boundary start of ucschar range %xA0-D7FF" + }, + { + "value": "中", + "reason": "U+4E2D CJK: within ucschar range %xA0-D7FF" + }, + { + "value": "퟿", + "reason": "U+D7FF: boundary end of ucschar range %xA0-D7FF" + }, + { + "value": "豈", + "reason": "U+F900: boundary start of ucschar range %xF900-FDCF" + }, + { + "value": "﷏", + "reason": "U+FDCF: boundary end of ucschar range %xF900-FDCF" + }, + { + "value": "ﷰ", + "reason": "U+FDF0: boundary start of ucschar range %xFDF0-FFEF" + }, + { + "value": "￯", + "reason": "U+FFEF: boundary end of ucschar range %xFDF0-FFEF" + }, + { + "value": "𐀀", + "reason": "U+10000: boundary start of supplementary ucschar range %x10000-1FFFD" + }, + { + "value": "🿽", + "reason": "U+1FFFD: boundary end of supplementary ucschar range %x10000-1FFFD" + }, + { + "value": "𠀀", + "reason": "U+20000: boundary start of supplementary ucschar range %x20000-2FFFD" + }, + { + "value": "𯿽", + "reason": "U+2FFFD: boundary end of supplementary ucschar range %x20000-2FFFD" + }, + { + "value": "󠀀", + "reason": "U+E0000: boundary start of supplementary ucschar range %xE0000-EFFFD" + }, + { + "value": "󯿽", + "reason": "U+EFFFD: boundary end of supplementary ucschar range %xE0000-EFFFD" + } + ], + "iunreserved": [ + { + "value": "a", + "reason": "Lowercase ALPHA is unreserved" + }, + { + "value": "Z", + "reason": "Uppercase ALPHA is unreserved" + }, + { + "value": "0", + "reason": "DIGIT 0 is unreserved" + }, + { + "value": "9", + "reason": "DIGIT 9 is unreserved" + }, + { + "value": "-", + "reason": "Hyphen is unreserved" + }, + { + "value": ".", + "reason": "Period is unreserved" + }, + { + "value": "_", + "reason": "Underscore is unreserved" + }, + { + "value": "~", + "reason": "Tilde is unreserved" + }, + { + "value": " ", + "reason": "ucschar U+00A0 via iunreserved (ucschar range %xA0-D7FF)" + }, + { + "value": "中", + "reason": "ucschar CJK U+4E2D via iunreserved" + } + ], + "sub_delims": [ + { + "value": "!", + "reason": "Exclamation mark is a sub-delim" + }, + { + "value": "$", + "reason": "Dollar sign is a sub-delim" + }, + { + "value": "&", + "reason": "Ampersand is a sub-delim" + }, + { + "value": "'", + "reason": "Single quote is a sub-delim" + }, + { + "value": "(", + "reason": "Left parenthesis is a sub-delim" + }, + { + "value": ")", + "reason": "Right parenthesis is a sub-delim" + }, + { + "value": "*", + "reason": "Asterisk is a sub-delim" + }, + { + "value": "+", + "reason": "Plus sign is a sub-delim" + }, + { + "value": ",", + "reason": "Comma is a sub-delim" + }, + { + "value": ";", + "reason": "Semicolon is a sub-delim" + }, + { + "value": "=", + "reason": "Equals sign is a sub-delim" + } + ], + "iuserinfo": [ + { + "value": "", + "reason": "Empty iuserinfo is valid (zero or more chars per RFC)" + }, + { + "value": "user", + "reason": "Simple username" + }, + { + "value": "user:password", + "reason": "Username:password (colon is allowed in iuserinfo)" + }, + { + "value": "user%40name", + "reason": "Percent-encoded at-sign in iuserinfo" + }, + { + "value": "user:pass:extra", + "reason": "Multiple colons allowed in iuserinfo" + }, + { + "value": "!$&'()*+,;=", + "reason": "All sub-delims valid in iuserinfo" + }, + { + "value": "用户", + "reason": "Chinese characters (ucschar) in iuserinfo" + }, + { + "value": "user%20name", + "reason": "Percent-encoded space in iuserinfo" + } + ], + "h16": [ + { + "value": "0", + "reason": "Single hex digit (minimum: 1HEXDIG)" + }, + { + "value": "f", + "reason": "Single lowercase hex digit" + }, + { + "value": "F", + "reason": "Single uppercase hex digit" + }, + { + "value": "FF", + "reason": "Two hex digits" + }, + { + "value": "A1B", + "reason": "Three hex digits" + }, + { + "value": "FFFF", + "reason": "Four hex digits (maximum: 4HEXDIG)" + }, + { + "value": "0000", + "reason": "Four zero digits" + }, + { + "value": "abcd", + "reason": "Lowercase 4-digit h16" + }, + { + "value": "1a2b", + "reason": "Mixed-case 4-digit h16" + } + ], + "dec_octet": [ + { + "value": "0", + "reason": "Single digit 0 (DIGIT form)" + }, + { + "value": "9", + "reason": "Single digit 9 (DIGIT form, max boundary)" + }, + { + "value": "10", + "reason": "Two-digit minimum (%x31-39 DIGIT form, 10)" + }, + { + "value": "99", + "reason": "Two-digit maximum (%x31-39 DIGIT form boundary)" + }, + { + "value": "100", + "reason": "Minimum for '1' 2DIGIT form" + }, + { + "value": "199", + "reason": "Maximum for '1' 2DIGIT form (boundary)" + }, + { + "value": "200", + "reason": "Minimum for '2' %x30-34 DIGIT form" + }, + { + "value": "249", + "reason": "Maximum for '2' %x30-34 DIGIT form (boundary)" + }, + { + "value": "250", + "reason": "Minimum for '25' %x30-35 form" + }, + { + "value": "255", + "reason": "Maximum dec-octet: '25' %x30-35 boundary" + } + ], + "ls32": [ + { + "value": "FFFF:FFFF", + "reason": "h16:h16 form with maximum values" + }, + { + "value": "0:0", + "reason": "Minimal h16:h16" + }, + { + "value": "a:b", + "reason": "Single hex digit each side (h16:h16)" + }, + { + "value": "abcd:ef01", + "reason": "Four-digit h16:h16" + }, + { + "value": "192.168.1.1", + "reason": "IPv4address form of ls32" + }, + { + "value": "0.0.0.0", + "reason": "All-zero IPv4 as ls32" + }, + { + "value": "255.255.255.255", + "reason": "Maximum IPv4 as ls32" + } + ], + "ipv6address": [ + { + "value": "2001:db8:85a3:0:0:8a2e:370:7334", + "reason": "Form 1: 6(h16:) ls32 — fully explicit 8-group" + }, + { + "value": "::1:2:3:4:5:6:7", + "reason": "Form 2: :: 5(h16:) ls32 — leading :: with five h16 groups then ls32" + }, + { + "value": "::1:2:3:4:192.168.1.1", + "reason": "Form 3: :: 4(h16:) ls32 with IPv4 ls32, no h16 before ::" + }, + { + "value": "2001::1:2:3:192.168.1.1", + "reason": "Form 3 variant: one h16 before ::, 4(h16:) ls32" + }, + { + "value": "2001:db8::1:2:192.168.1.1", + "reason": "Form 4: *1(h16:) h16 :: 3(h16:) ls32" + }, + { + "value": "::ffff:192.168.1.1", + "reason": "Form 6: :: h16: ls32 — IPv4-mapped, no h16 before ::" + }, + { + "value": "2001:db8:85a3::192.168.1.1", + "reason": "Form 7: *4(h16:) h16 :: ls32 with IPv4" + }, + { + "value": "::1", + "reason": "Form 8: :: h16 — IPv6 loopback" + }, + { + "value": "2001:db8::1", + "reason": "Form 8 variant: *1(h16:) h16 :: h16" + }, + { + "value": "::", + "reason": "Form 9: :: alone — all-zeros compressed" + }, + { + "value": "2001::", + "reason": "Form 9 variant: single h16 followed by ::" + }, + { + "value": "fe80::1", + "reason": "Form 8: link-local prefix with :: h16" + }, + { + "value": "64:ff9b::192.168.1.1", + "reason": "Form 7: IPv4-translated via :: and IPv4 ls32" + } + ], + "unreserved": [ + { + "value": "a", + "reason": "Lowercase ALPHA is unreserved" + }, + { + "value": "Z", + "reason": "Uppercase ALPHA is unreserved" + }, + { + "value": "0", + "reason": "DIGIT 0 is unreserved" + }, + { + "value": "9", + "reason": "DIGIT 9 is unreserved" + }, + { + "value": "-", + "reason": "Hyphen is unreserved" + }, + { + "value": ".", + "reason": "Period is unreserved" + }, + { + "value": "_", + "reason": "Underscore is unreserved" + }, + { + "value": "~", + "reason": "Tilde is unreserved" + } + ], + "ipvfuture": [ + { + "value": "v1.a", + "reason": "Minimal IPvFuture: version v1 with single unreserved char" + }, + { + "value": "v1.test", + "reason": "IPvFuture version 1 with unreserved suffix" + }, + { + "value": "vFF.abcd", + "reason": "IPvFuture with multi-hex version number" + }, + { + "value": "v1.~-._", + "reason": "IPvFuture suffix with unreserved chars ~-._" + }, + { + "value": "v1.!$&'()*+,;=", + "reason": "IPvFuture suffix with all sub-delims" + }, + { + "value": "v1.a:b", + "reason": "IPvFuture suffix with colon (colon allowed per RFC)" + }, + { + "value": "v123.host", + "reason": "IPvFuture with multi-digit hex version" + } + ], + "ip_literal": [ + { + "value": "[::1]", + "reason": "IPv6 loopback in brackets" + }, + { + "value": "[::]", + "reason": "All-zero compressed IPv6 in brackets" + }, + { + "value": "[2001:db8::1]", + "reason": "Typical IPv6 documentation address in brackets" + }, + { + "value": "[2001:db8:85a3:0:0:8a2e:370:7334]", + "reason": "Full explicit IPv6 in brackets" + }, + { + "value": "[::ffff:192.168.1.1]", + "reason": "IPv4-mapped IPv6 address in brackets" + }, + { + "value": "[v1.test]", + "reason": "Minimal IPvFuture in brackets" + }, + { + "value": "[vFF.a:b]", + "reason": "IPvFuture with hex version and colon in suffix" + }, + { + "value": "[v1.!$&'()*+,;=]", + "reason": "IPvFuture with sub-delims in suffix" + } + ], + "ireg_name": [ + { + "value": "", + "reason": "Empty reg-name is valid (zero or more chars per RFC)" + }, + { + "value": "example.com", + "reason": "Typical hostname as reg-name" + }, + { + "value": "xn--nxasmq6b.com", + "reason": "Punycode-encoded hostname label" + }, + { + "value": "%20example", + "reason": "Percent-encoded character in reg-name" + }, + { + "value": "пример", + "reason": "Cyrillic reg-name (ucschar via iunreserved)" + }, + { + "value": "example!test", + "reason": "Sub-delim ! allowed in reg-name" + }, + { + "value": "a$b&c", + "reason": "Multiple sub-delims in reg-name" + }, + { + "value": "中文", + "reason": "CJK characters (ucschar) in reg-name" + } + ], + "ihost": [ + { + "value": "example.com", + "reason": "Typical hostname (ireg-name)" + }, + { + "value": "", + "reason": "Empty host (ireg-name may be zero chars)" + }, + { + "value": "192.168.1.1", + "reason": "IPv4address as ihost" + }, + { + "value": "[::1]", + "reason": "IPv6 loopback as IP-literal" + }, + { + "value": "[2001:db8::1]", + "reason": "IPv6 documentation address as IP-literal" + }, + { + "value": "[v1.test]", + "reason": "IPvFuture as IP-literal" + }, + { + "value": "中文.公司", + "reason": "IDN-style host with CJK ucschar in reg-name" + } + ], + "port": [ + { + "value": "", + "reason": "Empty port is valid (port = *DIGIT, zero or more)" + }, + { + "value": "0", + "reason": "Port zero" + }, + { + "value": "80", + "reason": "Standard HTTP port" + }, + { + "value": "443", + "reason": "Standard HTTPS port" + }, + { + "value": "8080", + "reason": "Common alternate HTTP port" + }, + { + "value": "65535", + "reason": "Maximum 16-bit port (RFC does not restrict value range)" + }, + { + "value": "00", + "reason": "Leading zeros allowed (port = *DIGIT, no leading-zero restriction)" + } + ], + "iauthority": [ + { + "value": "example.com", + "reason": "Host only (no userinfo, no port)" + }, + { + "value": "", + "reason": "Empty authority (empty ireg-name, no port)" + }, + { + "value": "user@example.com", + "reason": "Userinfo and host" + }, + { + "value": "example.com:80", + "reason": "Host with numeric port" + }, + { + "value": "user:pass@example.com:443", + "reason": "Full authority: userinfo, host, port" + }, + { + "value": "[::1]", + "reason": "IPv6 literal as host" + }, + { + "value": "[::1]:8080", + "reason": "IPv6 literal host with port" + }, + { + "value": "用户@中文.com:8080", + "reason": "IRI authority with ucschar in userinfo and host" + } + ], + "ipchar": [ + { + "value": "a", + "reason": "ALPHA via iunreserved" + }, + { + "value": "Z", + "reason": "Uppercase ALPHA via iunreserved" + }, + { + "value": "0", + "reason": "DIGIT via iunreserved" + }, + { + "value": "-", + "reason": "Hyphen via iunreserved" + }, + { + "value": ".", + "reason": "Period via iunreserved" + }, + { + "value": "_", + "reason": "Underscore via iunreserved" + }, + { + "value": "~", + "reason": "Tilde via iunreserved" + }, + { + "value": " ", + "reason": "ucschar U+00A0 via iunreserved" + }, + { + "value": "%20", + "reason": "Percent-encoded space via pct-encoded" + }, + { + "value": "%2F", + "reason": "Percent-encoded slash via pct-encoded" + }, + { + "value": "!", + "reason": "Sub-delim ! via sub-delims" + }, + { + "value": "$", + "reason": "Sub-delim $ via sub-delims" + }, + { + "value": "&", + "reason": "Sub-delim & via sub-delims" + }, + { + "value": "'", + "reason": "Sub-delim ' via sub-delims" + }, + { + "value": "(", + "reason": "Sub-delim ( via sub-delims" + }, + { + "value": ")", + "reason": "Sub-delim ) via sub-delims" + }, + { + "value": "*", + "reason": "Sub-delim * via sub-delims" + }, + { + "value": "+", + "reason": "Sub-delim + via sub-delims" + }, + { + "value": ",", + "reason": "Sub-delim , via sub-delims" + }, + { + "value": ";", + "reason": "Sub-delim ; via sub-delims" + }, + { + "value": "=", + "reason": "Sub-delim = via sub-delims" + }, + { + "value": ":", + "reason": "Colon is directly allowed in ipchar" + }, + { + "value": "@", + "reason": "At-sign is directly allowed in ipchar" + } + ], + "isegment": [ + { + "value": "", + "reason": "Empty segment is valid (isegment = *ipchar, zero or more)" + }, + { + "value": "a", + "reason": "Single-char segment" + }, + { + "value": "segment", + "reason": "Multi-char segment" + }, + { + "value": "seg:ment", + "reason": "Segment with colon (ipchar allows colon)" + }, + { + "value": "seg@ment", + "reason": "Segment with at-sign (ipchar allows @)" + }, + { + "value": "%20", + "reason": "Percent-encoded segment" + }, + { + "value": "über", + "reason": "Segment with ucschar (ü)" + }, + { + "value": "seg!$&'()*+,;=", + "reason": "Segment with all sub-delims" + } + ], + "ipath_abempty": [ + { + "value": "", + "reason": "Empty ipath-abempty (zero repetitions of / isegment)" + }, + { + "value": "/", + "reason": "Single slash with empty segment" + }, + { + "value": "/segment", + "reason": "Single non-empty segment" + }, + { + "value": "/a/b/c", + "reason": "Three-level path" + }, + { + "value": "//double", + "reason": "Double slash: first empty segment, then 'double'" + }, + { + "value": "/über/path", + "reason": "Path with ucschar in segment" + }, + { + "value": "/seg:ment", + "reason": "Colon in segment (ipchar allows colon in ipath-abempty)" + }, + { + "value": "/seg%20ment", + "reason": "Percent-encoded space in segment" + } + ], + "isegment_nz": [ + { + "value": "a", + "reason": "Minimal single-char non-zero segment" + }, + { + "value": "segment", + "reason": "Multi-char segment" + }, + { + "value": "seg:ment", + "reason": "Segment with colon (ipchar allows colon)" + }, + { + "value": "%20", + "reason": "Percent-encoded non-empty segment" + }, + { + "value": "über", + "reason": "Non-zero segment with ucschar" + }, + { + "value": "seg!$&'()*+,;=:@", + "reason": "Segment with sub-delims, colon, at-sign" + } + ], + "ipath_absolute": [ + { + "value": "/", + "reason": "Just slash: ipath-absolute with no optional isegment-nz" + }, + { + "value": "/segment", + "reason": "Single segment after slash" + }, + { + "value": "/a/b/c", + "reason": "Multi-level absolute path" + }, + { + "value": "/über/path", + "reason": "Absolute path with ucschar" + }, + { + "value": "/seg:ment/sub", + "reason": "Colon in segment (ipath-absolute allows colon)" + }, + { + "value": "/%20/path", + "reason": "Percent-encoded first segment" + }, + { + "value": "/seg!$&'()*+,;=", + "reason": "Sub-delims in absolute path segment" + } + ], + "ipath_rootless": [ + { + "value": "a", + "reason": "Minimal single-char rootless path" + }, + { + "value": "segment", + "reason": "Single segment" + }, + { + "value": "a/b/c", + "reason": "Multi-segment rootless path" + }, + { + "value": "über/path", + "reason": "Rootless path with ucschar" + }, + { + "value": "seg:ment", + "reason": "Colon allowed in isegment-nz of ipath-rootless" + }, + { + "value": "%20/path", + "reason": "Percent-encoded first segment" + }, + { + "value": "scheme:like/path", + "reason": "Colon in first segment (valid rootless path)" + } + ], + "ipath_empty": [ + { + "value": "", + "reason": "Empty string is the only valid ipath-empty (0 ipchars per RFC)" + } + ], + "ihier_part": [ + { + "value": "//example.com/path", + "reason": "Form 1: // iauthority ipath-abempty" + }, + { + "value": "//example.com", + "reason": "Form 1: // iauthority with empty ipath-abempty" + }, + { + "value": "//user@example.com:80/path", + "reason": "Form 1: authority with userinfo and port" + }, + { + "value": "/absolute", + "reason": "Form 2: ipath-absolute" + }, + { + "value": "/a/b/c", + "reason": "Form 2: multi-level ipath-absolute" + }, + { + "value": "rootless", + "reason": "Form 3: ipath-rootless" + }, + { + "value": "seg:ment", + "reason": "Form 3: ipath-rootless with colon in segment" + }, + { + "value": "", + "reason": "Form 4: ipath-empty" + } + ], + "iprivate": [ + { + "value": "", + "reason": "U+E000: boundary start of iprivate range %xE000-F8FF" + }, + { + "value": "", + "reason": "U+F8FF: boundary end of iprivate range %xE000-F8FF" + }, + { + "value": "󰀀", + "reason": "U+F0000: boundary start of iprivate range %xF0000-FFFFD" + }, + { + "value": "󿿽", + "reason": "U+FFFFD: boundary end of iprivate range %xF0000-FFFFD" + }, + { + "value": "􀀀", + "reason": "U+100000: boundary start of iprivate range %x100000-10FFFD" + }, + { + "value": "􏿽", + "reason": "U+10FFFD: boundary end of iprivate range %x100000-10FFFD" + }, + { + "value": "", + "reason": "U+E001: mid-range within %xE000-F8FF iprivate" + }, + { + "value": "", + "reason": "U+F000: mid-range within %xE000-F8FF iprivate" + } + ], + "iquery": [ + { + "value": "", + "reason": "Empty query is valid (iquery = *(ipchar / iprivate / '/' / '?'))" + }, + { + "value": "q=1", + "reason": "Simple key=value query" + }, + { + "value": "key=value&key2=value2", + "reason": "Multiple params (& is sub-delim in ipchar)" + }, + { + "value": "/path", + "reason": "Slash allowed in iquery" + }, + { + "value": "?nested", + "reason": "Question mark allowed in iquery" + }, + { + "value": "über", + "reason": "ucschar (U+00FC) in query via iunreserved" + }, + { + "value": "", + "reason": "iprivate char U+E000 is allowed in iquery" + }, + { + "value": "q=a!b$c&d=e'f(g)h*i+j,k;l=m", + "reason": "All sub-delims in query" + }, + { + "value": "seg:ment@host", + "reason": "Colon and at-sign in query (via ipchar)" + }, + { + "value": "q=%20value", + "reason": "Percent-encoded value in query" + } + ], "absolute_iri": [ { "value": "ftp://пример.испытание.example.com/файл.txt", @@ -41,13 +946,236 @@ "reason": "" } ], - "gen_delims": [], - "ifragment": [], - "isegment_nz_nc": [], - "ipath_noscheme": [], - "ipath": [], - "irelative_part": [], - "irelative_ref": [], + "gen_delims": [ + { + "value": ":", + "reason": "Colon is a gen-delim" + }, + { + "value": "/", + "reason": "Slash is a gen-delim" + }, + { + "value": "?", + "reason": "Question mark is a gen-delim" + }, + { + "value": "#", + "reason": "Hash is a gen-delim" + }, + { + "value": "[", + "reason": "Left bracket is a gen-delim" + }, + { + "value": "]", + "reason": "Right bracket is a gen-delim" + }, + { + "value": "@", + "reason": "At-sign is a gen-delim" + } + ], + "ifragment": [ + { + "value": "", + "reason": "Empty fragment is valid (ifragment = *(ipchar / '/' / '?'))" + }, + { + "value": "section1", + "reason": "Simple alphanumeric fragment" + }, + { + "value": "über", + "reason": "Fragment with ucschar U+00FC" + }, + { + "value": "/path", + "reason": "Slash allowed in ifragment" + }, + { + "value": "?query", + "reason": "Question mark allowed in ifragment" + }, + { + "value": "seg:ment", + "reason": "Colon allowed in ifragment (via ipchar)" + }, + { + "value": "%20", + "reason": "Percent-encoded space in fragment" + }, + { + "value": "a!b$c&d=e'f(g)h*i+j,k;l=m", + "reason": "All sub-delims in fragment (via ipchar)" + }, + { + "value": "seg@host", + "reason": "At-sign in fragment (via ipchar)" + } + ], + "isegment_nz_nc": [ + { + "value": "a", + "reason": "Minimal single-char no-colon non-zero segment" + }, + { + "value": "segment", + "reason": "Multi-char no-colon segment" + }, + { + "value": "seg@ment", + "reason": "At-sign allowed in isegment-nz-nc" + }, + { + "value": "%20", + "reason": "Percent-encoded no-colon segment" + }, + { + "value": "über", + "reason": "No-colon segment with ucschar" + }, + { + "value": "seg!$&'()*+,;=", + "reason": "Sub-delims in no-colon segment (no colon)" + }, + { + "value": "a@b@c", + "reason": "Multiple at-signs allowed (isegment-nz-nc allows @)" + } + ], + "ipath_noscheme": [ + { + "value": "a", + "reason": "Minimal: single isegment-nz-nc" + }, + { + "value": "segment", + "reason": "Single no-colon first segment" + }, + { + "value": "a/b/c", + "reason": "Multi-segment noscheme path" + }, + { + "value": "über/path", + "reason": "Noscheme path with ucschar in first segment" + }, + { + "value": "@user/info", + "reason": "At-sign in first segment (isegment-nz-nc allows @)" + }, + { + "value": "seg!ment/sub", + "reason": "Sub-delim in first segment" + }, + { + "value": "a/b:c", + "reason": "Colon allowed in later segments (isegment allows colon)" + }, + { + "value": "%20/path", + "reason": "Percent-encoded first segment" + } + ], + "ipath": [ + { + "value": "", + "reason": "ipath-empty form" + }, + { + "value": "/", + "reason": "ipath-abempty with single empty segment" + }, + { + "value": "/segment", + "reason": "ipath-abempty or ipath-absolute" + }, + { + "value": "/a/b/c", + "reason": "Multi-level ipath-absolute" + }, + { + "value": "a/b", + "reason": "ipath-noscheme or ipath-rootless" + }, + { + "value": "seg:ment", + "reason": "ipath-rootless (colon OK in isegment-nz)" + }, + { + "value": "//empty/authority/form", + "reason": "ipath-abempty with double empty first segment" + }, + { + "value": "über/path", + "reason": "ipath-noscheme with ucschar in first segment" + } + ], + "irelative_part": [ + { + "value": "//example.com/path", + "reason": "Form 1: // iauthority ipath-abempty" + }, + { + "value": "//example.com", + "reason": "Form 1: // iauthority with empty ipath-abempty" + }, + { + "value": "//user@example.com:80/path", + "reason": "Form 1: full authority in irelative-part" + }, + { + "value": "/absolute", + "reason": "Form 2: ipath-absolute" + }, + { + "value": "segment", + "reason": "Form 3: ipath-noscheme (no colon in first segment)" + }, + { + "value": "a/b", + "reason": "Form 3: ipath-noscheme multi-segment" + }, + { + "value": "", + "reason": "Form 4: ipath-empty" + } + ], + "irelative_ref": [ + { + "value": "", + "reason": "Empty irelative-ref (all parts can be empty)" + }, + { + "value": "//example.com/path?q=1#frag", + "reason": "Full relative ref with authority, query, fragment" + }, + { + "value": "/path?q=1", + "reason": "Absolute path with query" + }, + { + "value": "über#frag", + "reason": "No-colon first segment with fragment" + }, + { + "value": "?q=1", + "reason": "Query only (empty irelative-part)" + }, + { + "value": "#frag", + "reason": "Fragment only (empty irelative-part and query)" + }, + { + "value": "//[::1]/path", + "reason": "IPv6 literal host in relative ref" + }, + { + "value": "a/b/c?key=val#section", + "reason": "Noscheme path with query and fragment" + } + ], "iri": [ { "value": "http:///example.com", @@ -81,6 +1209,26 @@ "value": "https://exämple.example.org/page#セクション1", "expect_lark": true, "reason": "" + }, + { + "value": "http://example.com?q=1&lang=über#第一章", + "expect_lark": true, + "reason": "IRI with ucschar in query and fragment" + }, + { + "value": "http://[2001:db8::1]/path", + "expect_lark": true, + "reason": "IRI with IPv6 literal host" + }, + { + "value": "http://[v1.test]/path", + "expect_lark": true, + "reason": "IRI with IPvFuture host" + }, + { + "value": "s://x", + "expect_lark": true, + "reason": "Minimal valid IRI: single-char scheme, host as single-char ireg-name" } ], "iri_reference": [ @@ -110,37 +1258,37 @@ "reason": "" }, { - "value": "ucschar/\u00A0-\uD7FF/\uF900-\uFDCF/\uFDF0-\uFFEF", + "value": "ucschar/ -퟿/豈-﷏/ﷰ-￯", "expect_lark": true, "reason": "`ucschar` ranges: %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF" }, { - "value": "ucschar/\uD800\uDC00-\uD83F\uDFFD/\uD840\uDC00-\uD87F\uDFFD/\uD880\uDC00-\uD8BF\uDFFD", + "value": "ucschar/𐀀-🿽/𠀀-𯿽/𰀀-𿿽", "expect_lark": true, "reason": "`ucschar` ranges: %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD" }, { - "value": "ucschar/\uD8C0\uDC00-\uD8FF\uDFFD/\uD900\uDC00-\uD93F\uDFFD/\uD940\uDC00-\uD97F\uDFFD", + "value": "ucschar/񀀀-񏿽/񐀀-񟿽/񠀀-񯿽", "expect_lark": true, "reason": "`ucschar` ranges: %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD" }, { - "value": "ucschar/\uD980\uDC00-\uD9BF\uDFFD/\uD9C0\uDC00-\uD9FF\uDFFD/\uDA00\uDC00-\uDA3F\uDFFD", + "value": "ucschar/񰀀-񿿽/򀀀-򏿽/򐀀-򟿽", "expect_lark": true, "reason": "`ucschar` ranges: %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD" }, { - "value": "ucschar/\uDA40\uDC00-\uDA7F\uDFFD/\uDA80\uDC00-\uDABF\uDFFD/\uDAC0\uDC00-\uDAFF\uDFFD", + "value": "ucschar/򠀀-򯿽/򰀀-򿿽/󀀀-󏿽", "expect_lark": true, "reason": "`ucschar` ranges: %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD" }, { - "value": "ucschar/\uDB00\uDC00-\uDB3F\uDFFD/\uDB40\uDC00-\uDB7F\uDFFD", + "value": "ucschar/󐀀-󟿽/󠀀-󯿽", "expect_lark": true, "reason": "`ucschar` ranges: %xD0000-DFFFD / %xE0000-EFFFD" }, { - "value": "iprivate?foo=\uE000-\uF8FF&bar=\uDB80\uDC00-\uDBBF\uDFFD&baz=\uDBC0\uDC00-\uDBFF\uDFFD", + "value": "iprivate?foo=-&bar=󰀀-󿿽&baz=􀀀-􏿽", "expect_lark": true, "reason": "`iprivate` ranges: %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD" }, @@ -158,10 +1306,219 @@ "value": "//[2041:0:140F::875B:131B]/foo", "expect_lark": true, "reason": "`IPv6address` case: [ *4( h16 \":\" ) h16 ] \"::\" ls32" + }, + { + "value": "http://example.com", + "expect_lark": true, + "reason": "Simple IRI as iri-reference (IRI variant)" + }, + { + "value": "//example.com/path", + "expect_lark": true, + "reason": "Protocol-relative reference (irelative-ref with authority)" + }, + { + "value": "?q=1#frag", + "expect_lark": true, + "reason": "Query and fragment only (empty irelative-part)" + }, + { + "value": "#section", + "expect_lark": true, + "reason": "Fragment-only IRI reference" + }, + { + "value": "", + "expect_lark": true, + "reason": "Empty string is a valid IRI reference (empty irelative-ref)" + } + ], + "reserved": [ + { + "value": ":", + "reason": "Colon is a gen-delim (reserved)" + }, + { + "value": "/", + "reason": "Slash is a gen-delim (reserved)" + }, + { + "value": "?", + "reason": "Question mark is a gen-delim (reserved)" + }, + { + "value": "#", + "reason": "Hash is a gen-delim (reserved)" + }, + { + "value": "[", + "reason": "Left bracket is a gen-delim (reserved)" + }, + { + "value": "]", + "reason": "Right bracket is a gen-delim (reserved)" + }, + { + "value": "@", + "reason": "At-sign is a gen-delim (reserved)" + }, + { + "value": "!", + "reason": "Exclamation mark is a sub-delim (reserved)" + }, + { + "value": "$", + "reason": "Dollar sign is a sub-delim (reserved)" + }, + { + "value": "&", + "reason": "Ampersand is a sub-delim (reserved)" + }, + { + "value": "'", + "reason": "Single quote is a sub-delim (reserved)" + }, + { + "value": "(", + "reason": "Left paren is a sub-delim (reserved)" + }, + { + "value": ")", + "reason": "Right paren is a sub-delim (reserved)" + }, + { + "value": "*", + "reason": "Asterisk is a sub-delim (reserved)" + }, + { + "value": "+", + "reason": "Plus is a sub-delim (reserved)" + }, + { + "value": ",", + "reason": "Comma is a sub-delim (reserved)" + }, + { + "value": ";", + "reason": "Semicolon is a sub-delim (reserved)" + }, + { + "value": "=", + "reason": "Equals sign is a sub-delim (reserved)" + } + ], + "scheme": [ + { + "value": "http", + "reason": "Standard HTTP scheme (ALPHA *( ALPHA / DIGIT / '+' / '-' / '.' ))" + }, + { + "value": "ftp", + "reason": "FTP scheme" + }, + { + "value": "a", + "reason": "Minimal scheme: single ALPHA" + }, + { + "value": "a123", + "reason": "Scheme with digits after initial ALPHA" + }, + { + "value": "a+b", + "reason": "Scheme with plus (allowed after first char)" + }, + { + "value": "a-b", + "reason": "Scheme with hyphen (allowed after first char)" + }, + { + "value": "a.b", + "reason": "Scheme with period (allowed after first char)" + }, + { + "value": "HTTPS", + "reason": "Uppercase scheme (ALPHA is case-insensitive per RFC)" + }, + { + "value": "a+b-c.2", + "reason": "Scheme with all allowed post-initial chars" + }, + { + "value": "coap+tcp", + "reason": "Real scheme with plus (CoAP over TCP)" + } + ], + "pct_encoded": [ + { + "value": "%20", + "reason": "Percent-encoded space (0x20)" + }, + { + "value": "%2F", + "reason": "Percent-encoded slash (0x2F)" + }, + { + "value": "%FF", + "reason": "Percent-encoded byte 0xFF" + }, + { + "value": "%00", + "reason": "Percent-encoded null byte (0x00)" + }, + { + "value": "%9a", + "reason": "Lowercase hex digits in pct-encoded" + }, + { + "value": "%Ab", + "reason": "Mixed-case hex digits in pct-encoded" + }, + { + "value": "%0A", + "reason": "Percent-encoded newline (0x0A)" + }, + { + "value": "%7E", + "reason": "Percent-encoded tilde (0x7E)" } ], - "reserved": [], - "scheme": [], - "pct_encoded": [], - "ipv4address": [] + "ipv4address": [ + { + "value": "0.0.0.0", + "reason": "All-zero IPv4 address (minimum boundary)" + }, + { + "value": "255.255.255.255", + "reason": "All-max IPv4 address (maximum boundary)" + }, + { + "value": "192.168.1.1", + "reason": "Typical private-range IPv4" + }, + { + "value": "10.0.0.1", + "reason": "10.x private range" + }, + { + "value": "127.0.0.1", + "reason": "Loopback address" + }, + { + "value": "1.2.3.4", + "reason": "Simple ascending IPv4" + }, + { + "value": "100.200.249.0", + "reason": "Tests 2xx and 24x dec-octets" + }, + { + "value": "250.251.252.253", + "reason": "Tests 25x dec-octet range" + }, + { + "value": "9.99.199.255", + "reason": "Covers all five dec-octet grammar alternatives" + } + ] } From 86d16c56e00c463783d7c6855c08c14b0dc6a9c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:27:28 +0000 Subject: [PATCH 03/17] style(tests): use explicit \\uXXXX escapes for non-ASCII chars in test data Re-serialize both JSON files with ensure_ascii=True so all non-ASCII characters (especially invisible private-use area chars like U+E000 and ucschar boundary values like U+00A0) appear as explicit \\uXXXX escape sequences. This makes the intent unambiguous in JSON source review. --- tests/_data/invalid_syntax.json | 56 ++++++------- tests/_data/valid_syntax.json | 134 ++++++++++++++++---------------- 2 files changed, 95 insertions(+), 95 deletions(-) diff --git a/tests/_data/invalid_syntax.json b/tests/_data/invalid_syntax.json index 26ac403..baf2843 100644 --- a/tests/_data/invalid_syntax.json +++ b/tests/_data/invalid_syntax.json @@ -5,19 +5,19 @@ "reason": "Control character not allowed in UCS range" }, { - "value": "", + "value": "\u007f", "reason": "DEL character is outside UCS allowed range" }, { - "value": "Ÿ", + "value": "\u009f", "reason": "Control character not part of ucschar" }, { - "value": "￾", + "value": "\ufffe", "reason": "Noncharacter in Unicode" }, { - "value": "￿", + "value": "\uffff", "reason": "Noncharacter in Unicode" }, { @@ -25,7 +25,7 @@ "reason": "Null byte is not allowed" }, { - "value": "€", + "value": "\u0080", "reason": "C1 control character, disallowed" } ], @@ -535,7 +535,7 @@ "reason": "Invalid hex digit" }, { - "value": "1:2:3:4:5:6:7:💀", + "value": "1:2:3:4:5:6:7:\ud83d\udc80", "reason": "Emoji is not valid in hex" }, { @@ -663,7 +663,7 @@ "reason": "Invalid character '#'" }, { - "value": "v1.💡", + "value": "v1.\ud83d\udca1", "reason": "Emoji is not allowed" }, { @@ -741,7 +741,7 @@ "reason": "Too many fields" }, { - "value": "[v1.💡]", + "value": "[v1.\ud83d\udca1]", "reason": "Emoji not allowed in IPvFuture" }, { @@ -1379,7 +1379,7 @@ "reason": "Newline character" }, { - "value": "💡", + "value": "\ud83d\udca1", "reason": "Emoji character" }, { @@ -1463,11 +1463,11 @@ "reason": "Space not allowed in iprivate" }, { - "value": "", + "value": "\u007f", "reason": "DEL character not in private range" }, { - "value": "ÿ", + "value": "\u00ff", "reason": "Extended Latin not private" }, { @@ -1475,63 +1475,63 @@ "reason": "NULL byte not private" }, { - "value": "豈", + "value": "\uf900", "reason": "CJK Compatibility Ideographs" }, { - "value": "퟿", + "value": "\ud7ff", "reason": "Upper limit of non-private BMP" }, { - "value": "￿", + "value": "\uffff", "reason": "Noncharacter" }, { - "value": "Ā", + "value": "\u0100", "reason": "Latin Extended-A" }, { - "value": "͸", + "value": "\u0378", "reason": "Unassigned code point" }, { - "value": "΀", + "value": "\u0380", "reason": "Unassigned code point" }, { - "value": "ჿFF", + "value": "\u10ffFF", "reason": "Outside defined private planes" }, { - "value": "", + "value": "\ufeff", "reason": "Byte Order Mark" }, { - "value": "💡", + "value": "\ud83d\udca1", "reason": "Emoji not in private range" }, { - "value": "🧃", + "value": "\ud83e\uddc3", "reason": "Emoji not in private range" }, { - "value": "💀", + "value": "\ud83d\udc80", "reason": "Emoji not in private range" }, { - "value": "🔥", + "value": "\ud83d\udd25", "reason": "Emoji not in private range" }, { - "value": "​", + "value": "\u200b", "reason": "Zero-width space not private" }, { - "value": "‮", + "value": "\u202e", "reason": "Bidi override character not private" }, { - "value": "⁠", + "value": "\u2060", "reason": "Word joiner character not private" } ], @@ -2017,7 +2017,7 @@ "reason": "Requires encoding" }, { - "value": "💡", + "value": "\ud83d\udca1", "reason": "Emoji is not a reserved character" } ], @@ -2083,7 +2083,7 @@ "reason": "Contains tab" }, { - "value": "http🙂", + "value": "http\ud83d\ude42", "reason": "Contains emoji" }, { diff --git a/tests/_data/valid_syntax.json b/tests/_data/valid_syntax.json index 1d5dfc3..4a602f9 100644 --- a/tests/_data/valid_syntax.json +++ b/tests/_data/valid_syntax.json @@ -1,55 +1,55 @@ { "ucschar": [ { - "value": " ", + "value": "\u00a0", "reason": "U+00A0 NO-BREAK SPACE: boundary start of ucschar range %xA0-D7FF" }, { - "value": "中", + "value": "\u4e2d", "reason": "U+4E2D CJK: within ucschar range %xA0-D7FF" }, { - "value": "퟿", + "value": "\ud7ff", "reason": "U+D7FF: boundary end of ucschar range %xA0-D7FF" }, { - "value": "豈", + "value": "\uf900", "reason": "U+F900: boundary start of ucschar range %xF900-FDCF" }, { - "value": "﷏", + "value": "\ufdcf", "reason": "U+FDCF: boundary end of ucschar range %xF900-FDCF" }, { - "value": "ﷰ", + "value": "\ufdf0", "reason": "U+FDF0: boundary start of ucschar range %xFDF0-FFEF" }, { - "value": "￯", + "value": "\uffef", "reason": "U+FFEF: boundary end of ucschar range %xFDF0-FFEF" }, { - "value": "𐀀", + "value": "\ud800\udc00", "reason": "U+10000: boundary start of supplementary ucschar range %x10000-1FFFD" }, { - "value": "🿽", + "value": "\ud83f\udffd", "reason": "U+1FFFD: boundary end of supplementary ucschar range %x10000-1FFFD" }, { - "value": "𠀀", + "value": "\ud840\udc00", "reason": "U+20000: boundary start of supplementary ucschar range %x20000-2FFFD" }, { - "value": "𯿽", + "value": "\ud87f\udffd", "reason": "U+2FFFD: boundary end of supplementary ucschar range %x20000-2FFFD" }, { - "value": "󠀀", + "value": "\udb40\udc00", "reason": "U+E0000: boundary start of supplementary ucschar range %xE0000-EFFFD" }, { - "value": "󯿽", + "value": "\udb7f\udffd", "reason": "U+EFFFD: boundary end of supplementary ucschar range %xE0000-EFFFD" } ], @@ -87,11 +87,11 @@ "reason": "Tilde is unreserved" }, { - "value": " ", + "value": "\u00a0", "reason": "ucschar U+00A0 via iunreserved (ucschar range %xA0-D7FF)" }, { - "value": "中", + "value": "\u4e2d", "reason": "ucschar CJK U+4E2D via iunreserved" } ], @@ -167,7 +167,7 @@ "reason": "All sub-delims valid in iuserinfo" }, { - "value": "用户", + "value": "\u7528\u6237", "reason": "Chinese characters (ucschar) in iuserinfo" }, { @@ -288,11 +288,11 @@ "ipv6address": [ { "value": "2001:db8:85a3:0:0:8a2e:370:7334", - "reason": "Form 1: 6(h16:) ls32 — fully explicit 8-group" + "reason": "Form 1: 6(h16:) ls32 \u2014 fully explicit 8-group" }, { "value": "::1:2:3:4:5:6:7", - "reason": "Form 2: :: 5(h16:) ls32 — leading :: with five h16 groups then ls32" + "reason": "Form 2: :: 5(h16:) ls32 \u2014 leading :: with five h16 groups then ls32" }, { "value": "::1:2:3:4:192.168.1.1", @@ -308,7 +308,7 @@ }, { "value": "::ffff:192.168.1.1", - "reason": "Form 6: :: h16: ls32 — IPv4-mapped, no h16 before ::" + "reason": "Form 6: :: h16: ls32 \u2014 IPv4-mapped, no h16 before ::" }, { "value": "2001:db8:85a3::192.168.1.1", @@ -316,7 +316,7 @@ }, { "value": "::1", - "reason": "Form 8: :: h16 — IPv6 loopback" + "reason": "Form 8: :: h16 \u2014 IPv6 loopback" }, { "value": "2001:db8::1", @@ -324,7 +324,7 @@ }, { "value": "::", - "reason": "Form 9: :: alone — all-zeros compressed" + "reason": "Form 9: :: alone \u2014 all-zeros compressed" }, { "value": "2001::", @@ -455,7 +455,7 @@ "reason": "Percent-encoded character in reg-name" }, { - "value": "пример", + "value": "\u043f\u0440\u0438\u043c\u0435\u0440", "reason": "Cyrillic reg-name (ucschar via iunreserved)" }, { @@ -467,7 +467,7 @@ "reason": "Multiple sub-delims in reg-name" }, { - "value": "中文", + "value": "\u4e2d\u6587", "reason": "CJK characters (ucschar) in reg-name" } ], @@ -497,7 +497,7 @@ "reason": "IPvFuture as IP-literal" }, { - "value": "中文.公司", + "value": "\u4e2d\u6587.\u516c\u53f8", "reason": "IDN-style host with CJK ucschar in reg-name" } ], @@ -561,7 +561,7 @@ "reason": "IPv6 literal host with port" }, { - "value": "用户@中文.com:8080", + "value": "\u7528\u6237@\u4e2d\u6587.com:8080", "reason": "IRI authority with ucschar in userinfo and host" } ], @@ -595,7 +595,7 @@ "reason": "Tilde via iunreserved" }, { - "value": " ", + "value": "\u00a0", "reason": "ucschar U+00A0 via iunreserved" }, { @@ -685,8 +685,8 @@ "reason": "Percent-encoded segment" }, { - "value": "über", - "reason": "Segment with ucschar (ü)" + "value": "\u00fcber", + "reason": "Segment with ucschar (\u00fc)" }, { "value": "seg!$&'()*+,;=", @@ -715,7 +715,7 @@ "reason": "Double slash: first empty segment, then 'double'" }, { - "value": "/über/path", + "value": "/\u00fcber/path", "reason": "Path with ucschar in segment" }, { @@ -745,7 +745,7 @@ "reason": "Percent-encoded non-empty segment" }, { - "value": "über", + "value": "\u00fcber", "reason": "Non-zero segment with ucschar" }, { @@ -767,7 +767,7 @@ "reason": "Multi-level absolute path" }, { - "value": "/über/path", + "value": "/\u00fcber/path", "reason": "Absolute path with ucschar" }, { @@ -797,7 +797,7 @@ "reason": "Multi-segment rootless path" }, { - "value": "über/path", + "value": "\u00fcber/path", "reason": "Rootless path with ucschar" }, { @@ -855,35 +855,35 @@ ], "iprivate": [ { - "value": "", + "value": "\ue000", "reason": "U+E000: boundary start of iprivate range %xE000-F8FF" }, { - "value": "", + "value": "\uf8ff", "reason": "U+F8FF: boundary end of iprivate range %xE000-F8FF" }, { - "value": "󰀀", + "value": "\udb80\udc00", "reason": "U+F0000: boundary start of iprivate range %xF0000-FFFFD" }, { - "value": "󿿽", + "value": "\udbbf\udffd", "reason": "U+FFFFD: boundary end of iprivate range %xF0000-FFFFD" }, { - "value": "􀀀", + "value": "\udbc0\udc00", "reason": "U+100000: boundary start of iprivate range %x100000-10FFFD" }, { - "value": "􏿽", + "value": "\udbff\udffd", "reason": "U+10FFFD: boundary end of iprivate range %x100000-10FFFD" }, { - "value": "", + "value": "\ue001", "reason": "U+E001: mid-range within %xE000-F8FF iprivate" }, { - "value": "", + "value": "\uf000", "reason": "U+F000: mid-range within %xE000-F8FF iprivate" } ], @@ -909,11 +909,11 @@ "reason": "Question mark allowed in iquery" }, { - "value": "über", + "value": "\u00fcber", "reason": "ucschar (U+00FC) in query via iunreserved" }, { - "value": "", + "value": "\ue000", "reason": "iprivate char U+E000 is allowed in iquery" }, { @@ -931,17 +931,17 @@ ], "absolute_iri": [ { - "value": "ftp://пример.испытание.example.com/файл.txt", + "value": "ftp://\u043f\u0440\u0438\u043c\u0435\u0440.\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435.example.com/\u0444\u0430\u0439\u043b.txt", "expect_lark": true, "reason": "" }, { - "value": "https://www.example.com/über", + "value": "https://www.example.com/\u00fcber", "expect_lark": true, "reason": "" }, { - "value": "https://exämple.example.org/pageセクション1", + "value": "https://ex\u00e4mple.example.org/page\u30bb\u30af\u30b7\u30e7\u30f31", "expect_lark": true, "reason": "" } @@ -986,7 +986,7 @@ "reason": "Simple alphanumeric fragment" }, { - "value": "über", + "value": "\u00fcber", "reason": "Fragment with ucschar U+00FC" }, { @@ -1032,7 +1032,7 @@ "reason": "Percent-encoded no-colon segment" }, { - "value": "über", + "value": "\u00fcber", "reason": "No-colon segment with ucschar" }, { @@ -1058,7 +1058,7 @@ "reason": "Multi-segment noscheme path" }, { - "value": "über/path", + "value": "\u00fcber/path", "reason": "Noscheme path with ucschar in first segment" }, { @@ -1108,7 +1108,7 @@ "reason": "ipath-abempty with double empty first segment" }, { - "value": "über/path", + "value": "\u00fcber/path", "reason": "ipath-noscheme with ucschar in first segment" } ], @@ -1156,7 +1156,7 @@ "reason": "Absolute path with query" }, { - "value": "über#frag", + "value": "\u00fcber#frag", "reason": "No-colon first segment with fragment" }, { @@ -1191,27 +1191,27 @@ "expect_lark": true }, { - "value": "mailto:用户@例子.公司@example.com", + "value": "mailto:\u7528\u6237@\u4f8b\u5b50.\u516c\u53f8@example.com", "expect_lark": true, "reason": "" }, { - "value": "ftp://пример.испытание.example.com/файл.txt", + "value": "ftp://\u043f\u0440\u0438\u043c\u0435\u0440.\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435.example.com/\u0444\u0430\u0439\u043b.txt", "expect_lark": true, "reason": "" }, { - "value": "https://www.example.com/über", + "value": "https://www.example.com/\u00fcber", "expect_lark": true, "reason": "" }, { - "value": "https://exämple.example.org/page#セクション1", + "value": "https://ex\u00e4mple.example.org/page#\u30bb\u30af\u30b7\u30e7\u30f31", "expect_lark": true, "reason": "" }, { - "value": "http://example.com?q=1&lang=über#第一章", + "value": "http://example.com?q=1&lang=\u00fcber#\u7b2c\u4e00\u7ae0", "expect_lark": true, "reason": "IRI with ucschar in query and fragment" }, @@ -1233,62 +1233,62 @@ ], "iri_reference": [ { - "value": "/documents/नियम.pdf", + "value": "/documents/\u0928\u093f\u092f\u092e.pdf", "expect_lark": true, "reason": "" }, { - "value": "../../фото/лошадь.jpg", + "value": "../../\u0444\u043e\u0442\u043e/\u043b\u043e\u0448\u0430\u0434\u044c.jpg", "expect_lark": true, "reason": "" }, { - "value": "index.html#संपर्क", + "value": "index.html#\u0938\u0902\u092a\u0930\u094d\u0915", "expect_lark": true, "reason": "" }, { - "value": "doc/guide.html#überblick", + "value": "doc/guide.html#\u00fcberblick", "expect_lark": true, "reason": "" }, { - "value": "пример/тест#часть2", + "value": "\u043f\u0440\u0438\u043c\u0435\u0440/\u0442\u0435\u0441\u0442#\u0447\u0430\u0441\u0442\u044c2", "expect_lark": true, "reason": "" }, { - "value": "ucschar/ -퟿/豈-﷏/ﷰ-￯", + "value": "ucschar/\u00a0-\ud7ff/\uf900-\ufdcf/\ufdf0-\uffef", "expect_lark": true, "reason": "`ucschar` ranges: %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF" }, { - "value": "ucschar/𐀀-🿽/𠀀-𯿽/𰀀-𿿽", + "value": "ucschar/\ud800\udc00-\ud83f\udffd/\ud840\udc00-\ud87f\udffd/\ud880\udc00-\ud8bf\udffd", "expect_lark": true, "reason": "`ucschar` ranges: %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD" }, { - "value": "ucschar/񀀀-񏿽/񐀀-񟿽/񠀀-񯿽", + "value": "ucschar/\ud8c0\udc00-\ud8ff\udffd/\ud900\udc00-\ud93f\udffd/\ud940\udc00-\ud97f\udffd", "expect_lark": true, "reason": "`ucschar` ranges: %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD" }, { - "value": "ucschar/񰀀-񿿽/򀀀-򏿽/򐀀-򟿽", + "value": "ucschar/\ud980\udc00-\ud9bf\udffd/\ud9c0\udc00-\ud9ff\udffd/\uda00\udc00-\uda3f\udffd", "expect_lark": true, "reason": "`ucschar` ranges: %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD" }, { - "value": "ucschar/򠀀-򯿽/򰀀-򿿽/󀀀-󏿽", + "value": "ucschar/\uda40\udc00-\uda7f\udffd/\uda80\udc00-\udabf\udffd/\udac0\udc00-\udaff\udffd", "expect_lark": true, "reason": "`ucschar` ranges: %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD" }, { - "value": "ucschar/󐀀-󟿽/󠀀-󯿽", + "value": "ucschar/\udb00\udc00-\udb3f\udffd/\udb40\udc00-\udb7f\udffd", "expect_lark": true, "reason": "`ucschar` ranges: %xD0000-DFFFD / %xE0000-EFFFD" }, { - "value": "iprivate?foo=-&bar=󰀀-󿿽&baz=􀀀-􏿽", + "value": "iprivate?foo=\ue000-\uf8ff&bar=\udb80\udc00-\udbbf\udffd&baz=\udbc0\udc00-\udbff\udffd", "expect_lark": true, "reason": "`iprivate` ranges: %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD" }, From 442947d1a9d3752ca26fe872b5d1f4d31f92eb6a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:29:07 +0000 Subject: [PATCH 04/17] style(tests): revert to raw UTF-8 encoding to preserve original style The previous ensure_ascii=True commit introduced an ambiguous \u10ffFF representation for the existing iprivate test entry (U+10FF followed by literal 'FF'). Revert to raw UTF-8 (ensure_ascii=False) which matches the original file style and avoids this ambiguity. The 'reason' field for each entry explicitly identifies codepoints by name/number. --- tests/_data/invalid_syntax.json | 56 ++++++------- tests/_data/valid_syntax.json | 134 ++++++++++++++++---------------- 2 files changed, 95 insertions(+), 95 deletions(-) diff --git a/tests/_data/invalid_syntax.json b/tests/_data/invalid_syntax.json index baf2843..26ac403 100644 --- a/tests/_data/invalid_syntax.json +++ b/tests/_data/invalid_syntax.json @@ -5,19 +5,19 @@ "reason": "Control character not allowed in UCS range" }, { - "value": "\u007f", + "value": "", "reason": "DEL character is outside UCS allowed range" }, { - "value": "\u009f", + "value": "Ÿ", "reason": "Control character not part of ucschar" }, { - "value": "\ufffe", + "value": "￾", "reason": "Noncharacter in Unicode" }, { - "value": "\uffff", + "value": "￿", "reason": "Noncharacter in Unicode" }, { @@ -25,7 +25,7 @@ "reason": "Null byte is not allowed" }, { - "value": "\u0080", + "value": "€", "reason": "C1 control character, disallowed" } ], @@ -535,7 +535,7 @@ "reason": "Invalid hex digit" }, { - "value": "1:2:3:4:5:6:7:\ud83d\udc80", + "value": "1:2:3:4:5:6:7:💀", "reason": "Emoji is not valid in hex" }, { @@ -663,7 +663,7 @@ "reason": "Invalid character '#'" }, { - "value": "v1.\ud83d\udca1", + "value": "v1.💡", "reason": "Emoji is not allowed" }, { @@ -741,7 +741,7 @@ "reason": "Too many fields" }, { - "value": "[v1.\ud83d\udca1]", + "value": "[v1.💡]", "reason": "Emoji not allowed in IPvFuture" }, { @@ -1379,7 +1379,7 @@ "reason": "Newline character" }, { - "value": "\ud83d\udca1", + "value": "💡", "reason": "Emoji character" }, { @@ -1463,11 +1463,11 @@ "reason": "Space not allowed in iprivate" }, { - "value": "\u007f", + "value": "", "reason": "DEL character not in private range" }, { - "value": "\u00ff", + "value": "ÿ", "reason": "Extended Latin not private" }, { @@ -1475,63 +1475,63 @@ "reason": "NULL byte not private" }, { - "value": "\uf900", + "value": "豈", "reason": "CJK Compatibility Ideographs" }, { - "value": "\ud7ff", + "value": "퟿", "reason": "Upper limit of non-private BMP" }, { - "value": "\uffff", + "value": "￿", "reason": "Noncharacter" }, { - "value": "\u0100", + "value": "Ā", "reason": "Latin Extended-A" }, { - "value": "\u0378", + "value": "͸", "reason": "Unassigned code point" }, { - "value": "\u0380", + "value": "΀", "reason": "Unassigned code point" }, { - "value": "\u10ffFF", + "value": "ჿFF", "reason": "Outside defined private planes" }, { - "value": "\ufeff", + "value": "", "reason": "Byte Order Mark" }, { - "value": "\ud83d\udca1", + "value": "💡", "reason": "Emoji not in private range" }, { - "value": "\ud83e\uddc3", + "value": "🧃", "reason": "Emoji not in private range" }, { - "value": "\ud83d\udc80", + "value": "💀", "reason": "Emoji not in private range" }, { - "value": "\ud83d\udd25", + "value": "🔥", "reason": "Emoji not in private range" }, { - "value": "\u200b", + "value": "​", "reason": "Zero-width space not private" }, { - "value": "\u202e", + "value": "‮", "reason": "Bidi override character not private" }, { - "value": "\u2060", + "value": "⁠", "reason": "Word joiner character not private" } ], @@ -2017,7 +2017,7 @@ "reason": "Requires encoding" }, { - "value": "\ud83d\udca1", + "value": "💡", "reason": "Emoji is not a reserved character" } ], @@ -2083,7 +2083,7 @@ "reason": "Contains tab" }, { - "value": "http\ud83d\ude42", + "value": "http🙂", "reason": "Contains emoji" }, { diff --git a/tests/_data/valid_syntax.json b/tests/_data/valid_syntax.json index 4a602f9..1d5dfc3 100644 --- a/tests/_data/valid_syntax.json +++ b/tests/_data/valid_syntax.json @@ -1,55 +1,55 @@ { "ucschar": [ { - "value": "\u00a0", + "value": " ", "reason": "U+00A0 NO-BREAK SPACE: boundary start of ucschar range %xA0-D7FF" }, { - "value": "\u4e2d", + "value": "中", "reason": "U+4E2D CJK: within ucschar range %xA0-D7FF" }, { - "value": "\ud7ff", + "value": "퟿", "reason": "U+D7FF: boundary end of ucschar range %xA0-D7FF" }, { - "value": "\uf900", + "value": "豈", "reason": "U+F900: boundary start of ucschar range %xF900-FDCF" }, { - "value": "\ufdcf", + "value": "﷏", "reason": "U+FDCF: boundary end of ucschar range %xF900-FDCF" }, { - "value": "\ufdf0", + "value": "ﷰ", "reason": "U+FDF0: boundary start of ucschar range %xFDF0-FFEF" }, { - "value": "\uffef", + "value": "￯", "reason": "U+FFEF: boundary end of ucschar range %xFDF0-FFEF" }, { - "value": "\ud800\udc00", + "value": "𐀀", "reason": "U+10000: boundary start of supplementary ucschar range %x10000-1FFFD" }, { - "value": "\ud83f\udffd", + "value": "🿽", "reason": "U+1FFFD: boundary end of supplementary ucschar range %x10000-1FFFD" }, { - "value": "\ud840\udc00", + "value": "𠀀", "reason": "U+20000: boundary start of supplementary ucschar range %x20000-2FFFD" }, { - "value": "\ud87f\udffd", + "value": "𯿽", "reason": "U+2FFFD: boundary end of supplementary ucschar range %x20000-2FFFD" }, { - "value": "\udb40\udc00", + "value": "󠀀", "reason": "U+E0000: boundary start of supplementary ucschar range %xE0000-EFFFD" }, { - "value": "\udb7f\udffd", + "value": "󯿽", "reason": "U+EFFFD: boundary end of supplementary ucschar range %xE0000-EFFFD" } ], @@ -87,11 +87,11 @@ "reason": "Tilde is unreserved" }, { - "value": "\u00a0", + "value": " ", "reason": "ucschar U+00A0 via iunreserved (ucschar range %xA0-D7FF)" }, { - "value": "\u4e2d", + "value": "中", "reason": "ucschar CJK U+4E2D via iunreserved" } ], @@ -167,7 +167,7 @@ "reason": "All sub-delims valid in iuserinfo" }, { - "value": "\u7528\u6237", + "value": "用户", "reason": "Chinese characters (ucschar) in iuserinfo" }, { @@ -288,11 +288,11 @@ "ipv6address": [ { "value": "2001:db8:85a3:0:0:8a2e:370:7334", - "reason": "Form 1: 6(h16:) ls32 \u2014 fully explicit 8-group" + "reason": "Form 1: 6(h16:) ls32 — fully explicit 8-group" }, { "value": "::1:2:3:4:5:6:7", - "reason": "Form 2: :: 5(h16:) ls32 \u2014 leading :: with five h16 groups then ls32" + "reason": "Form 2: :: 5(h16:) ls32 — leading :: with five h16 groups then ls32" }, { "value": "::1:2:3:4:192.168.1.1", @@ -308,7 +308,7 @@ }, { "value": "::ffff:192.168.1.1", - "reason": "Form 6: :: h16: ls32 \u2014 IPv4-mapped, no h16 before ::" + "reason": "Form 6: :: h16: ls32 — IPv4-mapped, no h16 before ::" }, { "value": "2001:db8:85a3::192.168.1.1", @@ -316,7 +316,7 @@ }, { "value": "::1", - "reason": "Form 8: :: h16 \u2014 IPv6 loopback" + "reason": "Form 8: :: h16 — IPv6 loopback" }, { "value": "2001:db8::1", @@ -324,7 +324,7 @@ }, { "value": "::", - "reason": "Form 9: :: alone \u2014 all-zeros compressed" + "reason": "Form 9: :: alone — all-zeros compressed" }, { "value": "2001::", @@ -455,7 +455,7 @@ "reason": "Percent-encoded character in reg-name" }, { - "value": "\u043f\u0440\u0438\u043c\u0435\u0440", + "value": "пример", "reason": "Cyrillic reg-name (ucschar via iunreserved)" }, { @@ -467,7 +467,7 @@ "reason": "Multiple sub-delims in reg-name" }, { - "value": "\u4e2d\u6587", + "value": "中文", "reason": "CJK characters (ucschar) in reg-name" } ], @@ -497,7 +497,7 @@ "reason": "IPvFuture as IP-literal" }, { - "value": "\u4e2d\u6587.\u516c\u53f8", + "value": "中文.公司", "reason": "IDN-style host with CJK ucschar in reg-name" } ], @@ -561,7 +561,7 @@ "reason": "IPv6 literal host with port" }, { - "value": "\u7528\u6237@\u4e2d\u6587.com:8080", + "value": "用户@中文.com:8080", "reason": "IRI authority with ucschar in userinfo and host" } ], @@ -595,7 +595,7 @@ "reason": "Tilde via iunreserved" }, { - "value": "\u00a0", + "value": " ", "reason": "ucschar U+00A0 via iunreserved" }, { @@ -685,8 +685,8 @@ "reason": "Percent-encoded segment" }, { - "value": "\u00fcber", - "reason": "Segment with ucschar (\u00fc)" + "value": "über", + "reason": "Segment with ucschar (ü)" }, { "value": "seg!$&'()*+,;=", @@ -715,7 +715,7 @@ "reason": "Double slash: first empty segment, then 'double'" }, { - "value": "/\u00fcber/path", + "value": "/über/path", "reason": "Path with ucschar in segment" }, { @@ -745,7 +745,7 @@ "reason": "Percent-encoded non-empty segment" }, { - "value": "\u00fcber", + "value": "über", "reason": "Non-zero segment with ucschar" }, { @@ -767,7 +767,7 @@ "reason": "Multi-level absolute path" }, { - "value": "/\u00fcber/path", + "value": "/über/path", "reason": "Absolute path with ucschar" }, { @@ -797,7 +797,7 @@ "reason": "Multi-segment rootless path" }, { - "value": "\u00fcber/path", + "value": "über/path", "reason": "Rootless path with ucschar" }, { @@ -855,35 +855,35 @@ ], "iprivate": [ { - "value": "\ue000", + "value": "", "reason": "U+E000: boundary start of iprivate range %xE000-F8FF" }, { - "value": "\uf8ff", + "value": "", "reason": "U+F8FF: boundary end of iprivate range %xE000-F8FF" }, { - "value": "\udb80\udc00", + "value": "󰀀", "reason": "U+F0000: boundary start of iprivate range %xF0000-FFFFD" }, { - "value": "\udbbf\udffd", + "value": "󿿽", "reason": "U+FFFFD: boundary end of iprivate range %xF0000-FFFFD" }, { - "value": "\udbc0\udc00", + "value": "􀀀", "reason": "U+100000: boundary start of iprivate range %x100000-10FFFD" }, { - "value": "\udbff\udffd", + "value": "􏿽", "reason": "U+10FFFD: boundary end of iprivate range %x100000-10FFFD" }, { - "value": "\ue001", + "value": "", "reason": "U+E001: mid-range within %xE000-F8FF iprivate" }, { - "value": "\uf000", + "value": "", "reason": "U+F000: mid-range within %xE000-F8FF iprivate" } ], @@ -909,11 +909,11 @@ "reason": "Question mark allowed in iquery" }, { - "value": "\u00fcber", + "value": "über", "reason": "ucschar (U+00FC) in query via iunreserved" }, { - "value": "\ue000", + "value": "", "reason": "iprivate char U+E000 is allowed in iquery" }, { @@ -931,17 +931,17 @@ ], "absolute_iri": [ { - "value": "ftp://\u043f\u0440\u0438\u043c\u0435\u0440.\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435.example.com/\u0444\u0430\u0439\u043b.txt", + "value": "ftp://пример.испытание.example.com/файл.txt", "expect_lark": true, "reason": "" }, { - "value": "https://www.example.com/\u00fcber", + "value": "https://www.example.com/über", "expect_lark": true, "reason": "" }, { - "value": "https://ex\u00e4mple.example.org/page\u30bb\u30af\u30b7\u30e7\u30f31", + "value": "https://exämple.example.org/pageセクション1", "expect_lark": true, "reason": "" } @@ -986,7 +986,7 @@ "reason": "Simple alphanumeric fragment" }, { - "value": "\u00fcber", + "value": "über", "reason": "Fragment with ucschar U+00FC" }, { @@ -1032,7 +1032,7 @@ "reason": "Percent-encoded no-colon segment" }, { - "value": "\u00fcber", + "value": "über", "reason": "No-colon segment with ucschar" }, { @@ -1058,7 +1058,7 @@ "reason": "Multi-segment noscheme path" }, { - "value": "\u00fcber/path", + "value": "über/path", "reason": "Noscheme path with ucschar in first segment" }, { @@ -1108,7 +1108,7 @@ "reason": "ipath-abempty with double empty first segment" }, { - "value": "\u00fcber/path", + "value": "über/path", "reason": "ipath-noscheme with ucschar in first segment" } ], @@ -1156,7 +1156,7 @@ "reason": "Absolute path with query" }, { - "value": "\u00fcber#frag", + "value": "über#frag", "reason": "No-colon first segment with fragment" }, { @@ -1191,27 +1191,27 @@ "expect_lark": true }, { - "value": "mailto:\u7528\u6237@\u4f8b\u5b50.\u516c\u53f8@example.com", + "value": "mailto:用户@例子.公司@example.com", "expect_lark": true, "reason": "" }, { - "value": "ftp://\u043f\u0440\u0438\u043c\u0435\u0440.\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435.example.com/\u0444\u0430\u0439\u043b.txt", + "value": "ftp://пример.испытание.example.com/файл.txt", "expect_lark": true, "reason": "" }, { - "value": "https://www.example.com/\u00fcber", + "value": "https://www.example.com/über", "expect_lark": true, "reason": "" }, { - "value": "https://ex\u00e4mple.example.org/page#\u30bb\u30af\u30b7\u30e7\u30f31", + "value": "https://exämple.example.org/page#セクション1", "expect_lark": true, "reason": "" }, { - "value": "http://example.com?q=1&lang=\u00fcber#\u7b2c\u4e00\u7ae0", + "value": "http://example.com?q=1&lang=über#第一章", "expect_lark": true, "reason": "IRI with ucschar in query and fragment" }, @@ -1233,62 +1233,62 @@ ], "iri_reference": [ { - "value": "/documents/\u0928\u093f\u092f\u092e.pdf", + "value": "/documents/नियम.pdf", "expect_lark": true, "reason": "" }, { - "value": "../../\u0444\u043e\u0442\u043e/\u043b\u043e\u0448\u0430\u0434\u044c.jpg", + "value": "../../фото/лошадь.jpg", "expect_lark": true, "reason": "" }, { - "value": "index.html#\u0938\u0902\u092a\u0930\u094d\u0915", + "value": "index.html#संपर्क", "expect_lark": true, "reason": "" }, { - "value": "doc/guide.html#\u00fcberblick", + "value": "doc/guide.html#überblick", "expect_lark": true, "reason": "" }, { - "value": "\u043f\u0440\u0438\u043c\u0435\u0440/\u0442\u0435\u0441\u0442#\u0447\u0430\u0441\u0442\u044c2", + "value": "пример/тест#часть2", "expect_lark": true, "reason": "" }, { - "value": "ucschar/\u00a0-\ud7ff/\uf900-\ufdcf/\ufdf0-\uffef", + "value": "ucschar/ -퟿/豈-﷏/ﷰ-￯", "expect_lark": true, "reason": "`ucschar` ranges: %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF" }, { - "value": "ucschar/\ud800\udc00-\ud83f\udffd/\ud840\udc00-\ud87f\udffd/\ud880\udc00-\ud8bf\udffd", + "value": "ucschar/𐀀-🿽/𠀀-𯿽/𰀀-𿿽", "expect_lark": true, "reason": "`ucschar` ranges: %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD" }, { - "value": "ucschar/\ud8c0\udc00-\ud8ff\udffd/\ud900\udc00-\ud93f\udffd/\ud940\udc00-\ud97f\udffd", + "value": "ucschar/񀀀-񏿽/񐀀-񟿽/񠀀-񯿽", "expect_lark": true, "reason": "`ucschar` ranges: %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD" }, { - "value": "ucschar/\ud980\udc00-\ud9bf\udffd/\ud9c0\udc00-\ud9ff\udffd/\uda00\udc00-\uda3f\udffd", + "value": "ucschar/񰀀-񿿽/򀀀-򏿽/򐀀-򟿽", "expect_lark": true, "reason": "`ucschar` ranges: %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD" }, { - "value": "ucschar/\uda40\udc00-\uda7f\udffd/\uda80\udc00-\udabf\udffd/\udac0\udc00-\udaff\udffd", + "value": "ucschar/򠀀-򯿽/򰀀-򿿽/󀀀-󏿽", "expect_lark": true, "reason": "`ucschar` ranges: %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD" }, { - "value": "ucschar/\udb00\udc00-\udb3f\udffd/\udb40\udc00-\udb7f\udffd", + "value": "ucschar/󐀀-󟿽/󠀀-󯿽", "expect_lark": true, "reason": "`ucschar` ranges: %xD0000-DFFFD / %xE0000-EFFFD" }, { - "value": "iprivate?foo=\ue000-\uf8ff&bar=\udb80\udc00-\udbbf\udffd&baz=\udbc0\udc00-\udbff\udffd", + "value": "iprivate?foo=-&bar=󰀀-󿿽&baz=􀀀-􏿽", "expect_lark": true, "reason": "`iprivate` ranges: %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD" }, From 26ebd7dcfd3ab827ea04d106c62f79938422f005 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:30:22 +0000 Subject: [PATCH 05/17] fix(tests): clarify ASCII space (U+0020) vs. ucschar space in reasons In isegment_nz_nc, ipath_noscheme and ipath invalid entries, update reason text to say 'ASCII space (U+0020)' rather than just 'space', to avoid confusion with U+00A0 NO-BREAK SPACE which IS valid as a ucschar within iunreserved and thus allowed in path segments. --- tests/_data/invalid_syntax.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/_data/invalid_syntax.json b/tests/_data/invalid_syntax.json index 26ac403..d8f8b5a 100644 --- a/tests/_data/invalid_syntax.json +++ b/tests/_data/invalid_syntax.json @@ -1762,7 +1762,7 @@ }, { "value": " ", - "reason": "Space not in iunreserved / pct-encoded / sub-delims / '@'" + "reason": "ASCII space (U+0020) not in iunreserved / pct-encoded / sub-delims / '@'" }, { "value": "#", @@ -1812,7 +1812,7 @@ }, { "value": "seg ment", - "reason": "Space not allowed in path segment" + "reason": "ASCII space (U+0020) not allowed in path segment (U+00A0 ucschar would be valid)" }, { "value": "seg#ment", @@ -1834,7 +1834,7 @@ "ipath": [ { "value": "seg ment", - "reason": "Space is not allowed in any ipath alternative" + "reason": "ASCII space (U+0020) is not allowed in any ipath alternative (U+00A0 ucschar would be valid)" }, { "value": "seg|ment", From 2f3d8aebdc65db945ab9fd6af334ea05c5013946 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:31:44 +0000 Subject: [PATCH 06/17] fix(tests): escape U+00A0 and private-use chars in JSON values Use explicit \uXXXX notation for U+00A0 (NO-BREAK SPACE) in value fields so they are visually distinct from ASCII space (U+0020). Also escape all private-use area characters (BMP E000-F8FF, Plane 15 F0000-FFFFD, Plane 16 100000-10FFFD) for clarity. Visible characters (CJK, Arabic, Hebrew etc.) remain as raw UTF-8. --- tests/_data/valid_syntax.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/_data/valid_syntax.json b/tests/_data/valid_syntax.json index 1d5dfc3..fd41ea0 100644 --- a/tests/_data/valid_syntax.json +++ b/tests/_data/valid_syntax.json @@ -1,7 +1,7 @@ { "ucschar": [ { - "value": " ", + "value": "\u00a0", "reason": "U+00A0 NO-BREAK SPACE: boundary start of ucschar range %xA0-D7FF" }, { @@ -87,7 +87,7 @@ "reason": "Tilde is unreserved" }, { - "value": " ", + "value": "\u00a0", "reason": "ucschar U+00A0 via iunreserved (ucschar range %xA0-D7FF)" }, { @@ -595,7 +595,7 @@ "reason": "Tilde via iunreserved" }, { - "value": " ", + "value": "\u00a0", "reason": "ucschar U+00A0 via iunreserved" }, { @@ -855,35 +855,35 @@ ], "iprivate": [ { - "value": "", + "value": "\ue000", "reason": "U+E000: boundary start of iprivate range %xE000-F8FF" }, { - "value": "", + "value": "\uf8ff", "reason": "U+F8FF: boundary end of iprivate range %xE000-F8FF" }, { - "value": "󰀀", + "value": "\udb80\udc00", "reason": "U+F0000: boundary start of iprivate range %xF0000-FFFFD" }, { - "value": "󿿽", + "value": "\udbbf\udffd", "reason": "U+FFFFD: boundary end of iprivate range %xF0000-FFFFD" }, { - "value": "􀀀", + "value": "\udbc0\udc00", "reason": "U+100000: boundary start of iprivate range %x100000-10FFFD" }, { - "value": "􏿽", + "value": "\udbff\udffd", "reason": "U+10FFFD: boundary end of iprivate range %x100000-10FFFD" }, { - "value": "", + "value": "\ue001", "reason": "U+E001: mid-range within %xE000-F8FF iprivate" }, { - "value": "", + "value": "\uf000", "reason": "U+F000: mid-range within %xE000-F8FF iprivate" } ], @@ -913,7 +913,7 @@ "reason": "ucschar (U+00FC) in query via iunreserved" }, { - "value": "", + "value": "\ue000", "reason": "iprivate char U+E000 is allowed in iquery" }, { @@ -1258,7 +1258,7 @@ "reason": "" }, { - "value": "ucschar/ -퟿/豈-﷏/ﷰ-￯", + "value": "ucschar/\u00a0-퟿/豈-﷏/ﷰ-￯", "expect_lark": true, "reason": "`ucschar` ranges: %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF" }, @@ -1288,7 +1288,7 @@ "reason": "`ucschar` ranges: %xD0000-DFFFD / %xE0000-EFFFD" }, { - "value": "iprivate?foo=-&bar=󰀀-󿿽&baz=􀀀-􏿽", + "value": "iprivate?foo=\ue000-\uf8ff&bar=\udb80\udc00-\udbbf\udffd&baz=\udbc0\udc00-\udbff\udffd", "expect_lark": true, "reason": "`iprivate` ranges: %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD" }, From 27653890a31f5d3fa12996826386772956ad683c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:32:22 +0000 Subject: [PATCH 07/17] style(tests): use uppercase hex in all \uXXXX escape sequences Normalise lowercase \uXXXX to uppercase \uXXXX throughout both JSON files for consistency with the uppercase codepoint notation used in reason strings (e.g. "U+00A0"). --- tests/_data/invalid_syntax.json | 2 +- tests/_data/valid_syntax.json | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/_data/invalid_syntax.json b/tests/_data/invalid_syntax.json index d8f8b5a..afa4763 100644 --- a/tests/_data/invalid_syntax.json +++ b/tests/_data/invalid_syntax.json @@ -1,7 +1,7 @@ { "ucschar": [ { - "value": "\u001f", + "value": "\u001F", "reason": "Control character not allowed in UCS range" }, { diff --git a/tests/_data/valid_syntax.json b/tests/_data/valid_syntax.json index fd41ea0..41dccf1 100644 --- a/tests/_data/valid_syntax.json +++ b/tests/_data/valid_syntax.json @@ -1,7 +1,7 @@ { "ucschar": [ { - "value": "\u00a0", + "value": "\u00A0", "reason": "U+00A0 NO-BREAK SPACE: boundary start of ucschar range %xA0-D7FF" }, { @@ -87,7 +87,7 @@ "reason": "Tilde is unreserved" }, { - "value": "\u00a0", + "value": "\u00A0", "reason": "ucschar U+00A0 via iunreserved (ucschar range %xA0-D7FF)" }, { @@ -595,7 +595,7 @@ "reason": "Tilde via iunreserved" }, { - "value": "\u00a0", + "value": "\u00A0", "reason": "ucschar U+00A0 via iunreserved" }, { @@ -855,35 +855,35 @@ ], "iprivate": [ { - "value": "\ue000", + "value": "\uE000", "reason": "U+E000: boundary start of iprivate range %xE000-F8FF" }, { - "value": "\uf8ff", + "value": "\uF8FF", "reason": "U+F8FF: boundary end of iprivate range %xE000-F8FF" }, { - "value": "\udb80\udc00", + "value": "\uDB80\uDC00", "reason": "U+F0000: boundary start of iprivate range %xF0000-FFFFD" }, { - "value": "\udbbf\udffd", + "value": "\uDBBF\uDFFD", "reason": "U+FFFFD: boundary end of iprivate range %xF0000-FFFFD" }, { - "value": "\udbc0\udc00", + "value": "\uDBC0\uDC00", "reason": "U+100000: boundary start of iprivate range %x100000-10FFFD" }, { - "value": "\udbff\udffd", + "value": "\uDBFF\uDFFD", "reason": "U+10FFFD: boundary end of iprivate range %x100000-10FFFD" }, { - "value": "\ue001", + "value": "\uE001", "reason": "U+E001: mid-range within %xE000-F8FF iprivate" }, { - "value": "\uf000", + "value": "\uF000", "reason": "U+F000: mid-range within %xE000-F8FF iprivate" } ], @@ -913,7 +913,7 @@ "reason": "ucschar (U+00FC) in query via iunreserved" }, { - "value": "\ue000", + "value": "\uE000", "reason": "iprivate char U+E000 is allowed in iquery" }, { @@ -1258,7 +1258,7 @@ "reason": "" }, { - "value": "ucschar/\u00a0-퟿/豈-﷏/ﷰ-￯", + "value": "ucschar/\u00A0-퟿/豈-﷏/ﷰ-￯", "expect_lark": true, "reason": "`ucschar` ranges: %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF" }, @@ -1288,7 +1288,7 @@ "reason": "`ucschar` ranges: %xD0000-DFFFD / %xE0000-EFFFD" }, { - "value": "iprivate?foo=\ue000-\uf8ff&bar=\udb80\udc00-\udbbf\udffd&baz=\udbc0\udc00-\udbff\udffd", + "value": "iprivate?foo=\uE000-\uF8FF&bar=\uDB80\uDC00-\uDBBF\uDFFD&baz=\uDBC0\uDC00-\uDBFF\uDFFD", "expect_lark": true, "reason": "`iprivate` ranges: %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD" }, From 9a3045f8203a2a523e93c519b216dc53852a68ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:33:03 +0000 Subject: [PATCH 08/17] docs(tests): clarify U+00A0 as NO-BREAK SPACE in ipath reason fields Add "NO-BREAK SPACE" name to U+00A0 references in the ipath_noscheme and ipath invalid-space entry reason strings, matching the explicit style used elsewhere in the file. --- tests/_data/invalid_syntax.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/_data/invalid_syntax.json b/tests/_data/invalid_syntax.json index afa4763..35069be 100644 --- a/tests/_data/invalid_syntax.json +++ b/tests/_data/invalid_syntax.json @@ -1812,7 +1812,7 @@ }, { "value": "seg ment", - "reason": "ASCII space (U+0020) not allowed in path segment (U+00A0 ucschar would be valid)" + "reason": "ASCII space (U+0020) not allowed in path segment (U+00A0 NO-BREAK SPACE as ucschar would be valid)" }, { "value": "seg#ment", @@ -1834,7 +1834,7 @@ "ipath": [ { "value": "seg ment", - "reason": "ASCII space (U+0020) is not allowed in any ipath alternative (U+00A0 ucschar would be valid)" + "reason": "ASCII space (U+0020) is not allowed in any ipath alternative (U+00A0 NO-BREAK SPACE as ucschar would be valid)" }, { "value": "seg|ment", From 6eb7dfe0c6f33d132ef117d76f24cf3f00009ad0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:34:04 +0000 Subject: [PATCH 09/17] Keep JSON fixtures ASCII-escaped --- tests/_data/invalid_syntax.json | 56 ++++++++--------- tests/_data/valid_syntax.json | 108 ++++++++++++++++---------------- tests/test_syntax.py | 18 +++++- 3 files changed, 98 insertions(+), 84 deletions(-) diff --git a/tests/_data/invalid_syntax.json b/tests/_data/invalid_syntax.json index 35069be..25ad4f6 100644 --- a/tests/_data/invalid_syntax.json +++ b/tests/_data/invalid_syntax.json @@ -5,19 +5,19 @@ "reason": "Control character not allowed in UCS range" }, { - "value": "", + "value": "\u007F", "reason": "DEL character is outside UCS allowed range" }, { - "value": "Ÿ", + "value": "\u009F", "reason": "Control character not part of ucschar" }, { - "value": "￾", + "value": "\uFFFE", "reason": "Noncharacter in Unicode" }, { - "value": "￿", + "value": "\uFFFF", "reason": "Noncharacter in Unicode" }, { @@ -25,7 +25,7 @@ "reason": "Null byte is not allowed" }, { - "value": "€", + "value": "\u0080", "reason": "C1 control character, disallowed" } ], @@ -535,7 +535,7 @@ "reason": "Invalid hex digit" }, { - "value": "1:2:3:4:5:6:7:💀", + "value": "1:2:3:4:5:6:7:\uD83D\uDC80", "reason": "Emoji is not valid in hex" }, { @@ -663,7 +663,7 @@ "reason": "Invalid character '#'" }, { - "value": "v1.💡", + "value": "v1.\uD83D\uDCA1", "reason": "Emoji is not allowed" }, { @@ -741,7 +741,7 @@ "reason": "Too many fields" }, { - "value": "[v1.💡]", + "value": "[v1.\uD83D\uDCA1]", "reason": "Emoji not allowed in IPvFuture" }, { @@ -1379,7 +1379,7 @@ "reason": "Newline character" }, { - "value": "💡", + "value": "\uD83D\uDCA1", "reason": "Emoji character" }, { @@ -1463,11 +1463,11 @@ "reason": "Space not allowed in iprivate" }, { - "value": "", + "value": "\u007F", "reason": "DEL character not in private range" }, { - "value": "ÿ", + "value": "\u00FF", "reason": "Extended Latin not private" }, { @@ -1475,63 +1475,63 @@ "reason": "NULL byte not private" }, { - "value": "豈", + "value": "\uF900", "reason": "CJK Compatibility Ideographs" }, { - "value": "퟿", + "value": "\uD7FF", "reason": "Upper limit of non-private BMP" }, { - "value": "￿", + "value": "\uFFFF", "reason": "Noncharacter" }, { - "value": "Ā", + "value": "\u0100", "reason": "Latin Extended-A" }, { - "value": "͸", + "value": "\u0378", "reason": "Unassigned code point" }, { - "value": "΀", + "value": "\u0380", "reason": "Unassigned code point" }, { - "value": "ჿFF", + "value": "\u10FFFF", "reason": "Outside defined private planes" }, { - "value": "", + "value": "\uFEFF", "reason": "Byte Order Mark" }, { - "value": "💡", + "value": "\uD83D\uDCA1", "reason": "Emoji not in private range" }, { - "value": "🧃", + "value": "\uD83E\uDDC3", "reason": "Emoji not in private range" }, { - "value": "💀", + "value": "\uD83D\uDC80", "reason": "Emoji not in private range" }, { - "value": "🔥", + "value": "\uD83D\uDD25", "reason": "Emoji not in private range" }, { - "value": "​", + "value": "\u200B", "reason": "Zero-width space not private" }, { - "value": "‮", + "value": "\u202E", "reason": "Bidi override character not private" }, { - "value": "⁠", + "value": "\u2060", "reason": "Word joiner character not private" } ], @@ -2017,7 +2017,7 @@ "reason": "Requires encoding" }, { - "value": "💡", + "value": "\uD83D\uDCA1", "reason": "Emoji is not a reserved character" } ], @@ -2083,7 +2083,7 @@ "reason": "Contains tab" }, { - "value": "http🙂", + "value": "http\uD83D\uDE42", "reason": "Contains emoji" }, { diff --git a/tests/_data/valid_syntax.json b/tests/_data/valid_syntax.json index 41dccf1..cc51d5c 100644 --- a/tests/_data/valid_syntax.json +++ b/tests/_data/valid_syntax.json @@ -5,51 +5,51 @@ "reason": "U+00A0 NO-BREAK SPACE: boundary start of ucschar range %xA0-D7FF" }, { - "value": "中", + "value": "\u4E2D", "reason": "U+4E2D CJK: within ucschar range %xA0-D7FF" }, { - "value": "퟿", + "value": "\uD7FF", "reason": "U+D7FF: boundary end of ucschar range %xA0-D7FF" }, { - "value": "豈", + "value": "\uF900", "reason": "U+F900: boundary start of ucschar range %xF900-FDCF" }, { - "value": "﷏", + "value": "\uFDCF", "reason": "U+FDCF: boundary end of ucschar range %xF900-FDCF" }, { - "value": "ﷰ", + "value": "\uFDF0", "reason": "U+FDF0: boundary start of ucschar range %xFDF0-FFEF" }, { - "value": "￯", + "value": "\uFFEF", "reason": "U+FFEF: boundary end of ucschar range %xFDF0-FFEF" }, { - "value": "𐀀", + "value": "\uD800\uDC00", "reason": "U+10000: boundary start of supplementary ucschar range %x10000-1FFFD" }, { - "value": "🿽", + "value": "\uD83F\uDFFD", "reason": "U+1FFFD: boundary end of supplementary ucschar range %x10000-1FFFD" }, { - "value": "𠀀", + "value": "\uD840\uDC00", "reason": "U+20000: boundary start of supplementary ucschar range %x20000-2FFFD" }, { - "value": "𯿽", + "value": "\uD87F\uDFFD", "reason": "U+2FFFD: boundary end of supplementary ucschar range %x20000-2FFFD" }, { - "value": "󠀀", + "value": "\uDB40\uDC00", "reason": "U+E0000: boundary start of supplementary ucschar range %xE0000-EFFFD" }, { - "value": "󯿽", + "value": "\uDB7F\uDFFD", "reason": "U+EFFFD: boundary end of supplementary ucschar range %xE0000-EFFFD" } ], @@ -91,7 +91,7 @@ "reason": "ucschar U+00A0 via iunreserved (ucschar range %xA0-D7FF)" }, { - "value": "中", + "value": "\u4E2D", "reason": "ucschar CJK U+4E2D via iunreserved" } ], @@ -167,7 +167,7 @@ "reason": "All sub-delims valid in iuserinfo" }, { - "value": "用户", + "value": "\u7528\u6237", "reason": "Chinese characters (ucschar) in iuserinfo" }, { @@ -288,11 +288,11 @@ "ipv6address": [ { "value": "2001:db8:85a3:0:0:8a2e:370:7334", - "reason": "Form 1: 6(h16:) ls32 — fully explicit 8-group" + "reason": "Form 1: 6(h16:) ls32 \u2014 fully explicit 8-group" }, { "value": "::1:2:3:4:5:6:7", - "reason": "Form 2: :: 5(h16:) ls32 — leading :: with five h16 groups then ls32" + "reason": "Form 2: :: 5(h16:) ls32 \u2014 leading :: with five h16 groups then ls32" }, { "value": "::1:2:3:4:192.168.1.1", @@ -308,7 +308,7 @@ }, { "value": "::ffff:192.168.1.1", - "reason": "Form 6: :: h16: ls32 — IPv4-mapped, no h16 before ::" + "reason": "Form 6: :: h16: ls32 \u2014 IPv4-mapped, no h16 before ::" }, { "value": "2001:db8:85a3::192.168.1.1", @@ -316,7 +316,7 @@ }, { "value": "::1", - "reason": "Form 8: :: h16 — IPv6 loopback" + "reason": "Form 8: :: h16 \u2014 IPv6 loopback" }, { "value": "2001:db8::1", @@ -324,7 +324,7 @@ }, { "value": "::", - "reason": "Form 9: :: alone — all-zeros compressed" + "reason": "Form 9: :: alone \u2014 all-zeros compressed" }, { "value": "2001::", @@ -455,7 +455,7 @@ "reason": "Percent-encoded character in reg-name" }, { - "value": "пример", + "value": "\u043F\u0440\u0438\u043C\u0435\u0440", "reason": "Cyrillic reg-name (ucschar via iunreserved)" }, { @@ -467,7 +467,7 @@ "reason": "Multiple sub-delims in reg-name" }, { - "value": "中文", + "value": "\u4E2D\u6587", "reason": "CJK characters (ucschar) in reg-name" } ], @@ -497,7 +497,7 @@ "reason": "IPvFuture as IP-literal" }, { - "value": "中文.公司", + "value": "\u4E2D\u6587.\u516C\u53F8", "reason": "IDN-style host with CJK ucschar in reg-name" } ], @@ -561,7 +561,7 @@ "reason": "IPv6 literal host with port" }, { - "value": "用户@中文.com:8080", + "value": "\u7528\u6237@\u4E2D\u6587.com:8080", "reason": "IRI authority with ucschar in userinfo and host" } ], @@ -685,8 +685,8 @@ "reason": "Percent-encoded segment" }, { - "value": "über", - "reason": "Segment with ucschar (ü)" + "value": "\u00FCber", + "reason": "Segment with ucschar (\u00FC)" }, { "value": "seg!$&'()*+,;=", @@ -715,7 +715,7 @@ "reason": "Double slash: first empty segment, then 'double'" }, { - "value": "/über/path", + "value": "/\u00FCber/path", "reason": "Path with ucschar in segment" }, { @@ -745,7 +745,7 @@ "reason": "Percent-encoded non-empty segment" }, { - "value": "über", + "value": "\u00FCber", "reason": "Non-zero segment with ucschar" }, { @@ -767,7 +767,7 @@ "reason": "Multi-level absolute path" }, { - "value": "/über/path", + "value": "/\u00FCber/path", "reason": "Absolute path with ucschar" }, { @@ -797,7 +797,7 @@ "reason": "Multi-segment rootless path" }, { - "value": "über/path", + "value": "\u00FCber/path", "reason": "Rootless path with ucschar" }, { @@ -909,7 +909,7 @@ "reason": "Question mark allowed in iquery" }, { - "value": "über", + "value": "\u00FCber", "reason": "ucschar (U+00FC) in query via iunreserved" }, { @@ -931,17 +931,17 @@ ], "absolute_iri": [ { - "value": "ftp://пример.испытание.example.com/файл.txt", + "value": "ftp://\u043F\u0440\u0438\u043C\u0435\u0440.\u0438\u0441\u043F\u044B\u0442\u0430\u043D\u0438\u0435.example.com/\u0444\u0430\u0439\u043B.txt", "expect_lark": true, "reason": "" }, { - "value": "https://www.example.com/über", + "value": "https://www.example.com/\u00FCber", "expect_lark": true, "reason": "" }, { - "value": "https://exämple.example.org/pageセクション1", + "value": "https://ex\u00E4mple.example.org/page\u30BB\u30AF\u30B7\u30E7\u30F31", "expect_lark": true, "reason": "" } @@ -986,7 +986,7 @@ "reason": "Simple alphanumeric fragment" }, { - "value": "über", + "value": "\u00FCber", "reason": "Fragment with ucschar U+00FC" }, { @@ -1032,7 +1032,7 @@ "reason": "Percent-encoded no-colon segment" }, { - "value": "über", + "value": "\u00FCber", "reason": "No-colon segment with ucschar" }, { @@ -1058,7 +1058,7 @@ "reason": "Multi-segment noscheme path" }, { - "value": "über/path", + "value": "\u00FCber/path", "reason": "Noscheme path with ucschar in first segment" }, { @@ -1108,7 +1108,7 @@ "reason": "ipath-abempty with double empty first segment" }, { - "value": "über/path", + "value": "\u00FCber/path", "reason": "ipath-noscheme with ucschar in first segment" } ], @@ -1156,7 +1156,7 @@ "reason": "Absolute path with query" }, { - "value": "über#frag", + "value": "\u00FCber#frag", "reason": "No-colon first segment with fragment" }, { @@ -1191,27 +1191,27 @@ "expect_lark": true }, { - "value": "mailto:用户@例子.公司@example.com", + "value": "mailto:\u7528\u6237@\u4F8B\u5B50.\u516C\u53F8@example.com", "expect_lark": true, "reason": "" }, { - "value": "ftp://пример.испытание.example.com/файл.txt", + "value": "ftp://\u043F\u0440\u0438\u043C\u0435\u0440.\u0438\u0441\u043F\u044B\u0442\u0430\u043D\u0438\u0435.example.com/\u0444\u0430\u0439\u043B.txt", "expect_lark": true, "reason": "" }, { - "value": "https://www.example.com/über", + "value": "https://www.example.com/\u00FCber", "expect_lark": true, "reason": "" }, { - "value": "https://exämple.example.org/page#セクション1", + "value": "https://ex\u00E4mple.example.org/page#\u30BB\u30AF\u30B7\u30E7\u30F31", "expect_lark": true, "reason": "" }, { - "value": "http://example.com?q=1&lang=über#第一章", + "value": "http://example.com?q=1&lang=\u00FCber#\u7B2C\u4E00\u7AE0", "expect_lark": true, "reason": "IRI with ucschar in query and fragment" }, @@ -1233,57 +1233,57 @@ ], "iri_reference": [ { - "value": "/documents/नियम.pdf", + "value": "/documents/\u0928\u093F\u092F\u092E.pdf", "expect_lark": true, "reason": "" }, { - "value": "../../фото/лошадь.jpg", + "value": "../../\u0444\u043E\u0442\u043E/\u043B\u043E\u0448\u0430\u0434\u044C.jpg", "expect_lark": true, "reason": "" }, { - "value": "index.html#संपर्क", + "value": "index.html#\u0938\u0902\u092A\u0930\u094D\u0915", "expect_lark": true, "reason": "" }, { - "value": "doc/guide.html#überblick", + "value": "doc/guide.html#\u00FCberblick", "expect_lark": true, "reason": "" }, { - "value": "пример/тест#часть2", + "value": "\u043F\u0440\u0438\u043C\u0435\u0440/\u0442\u0435\u0441\u0442#\u0447\u0430\u0441\u0442\u044C2", "expect_lark": true, "reason": "" }, { - "value": "ucschar/\u00A0-퟿/豈-﷏/ﷰ-￯", + "value": "ucschar/\u00A0-\uD7FF/\uF900-\uFDCF/\uFDF0-\uFFEF", "expect_lark": true, "reason": "`ucschar` ranges: %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF" }, { - "value": "ucschar/𐀀-🿽/𠀀-𯿽/𰀀-𿿽", + "value": "ucschar/\uD800\uDC00-\uD83F\uDFFD/\uD840\uDC00-\uD87F\uDFFD/\uD880\uDC00-\uD8BF\uDFFD", "expect_lark": true, "reason": "`ucschar` ranges: %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD" }, { - "value": "ucschar/񀀀-񏿽/񐀀-񟿽/񠀀-񯿽", + "value": "ucschar/\uD8C0\uDC00-\uD8FF\uDFFD/\uD900\uDC00-\uD93F\uDFFD/\uD940\uDC00-\uD97F\uDFFD", "expect_lark": true, "reason": "`ucschar` ranges: %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD" }, { - "value": "ucschar/񰀀-񿿽/򀀀-򏿽/򐀀-򟿽", + "value": "ucschar/\uD980\uDC00-\uD9BF\uDFFD/\uD9C0\uDC00-\uD9FF\uDFFD/\uDA00\uDC00-\uDA3F\uDFFD", "expect_lark": true, "reason": "`ucschar` ranges: %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD" }, { - "value": "ucschar/򠀀-򯿽/򰀀-򿿽/󀀀-󏿽", + "value": "ucschar/\uDA40\uDC00-\uDA7F\uDFFD/\uDA80\uDC00-\uDABF\uDFFD/\uDAC0\uDC00-\uDAFF\uDFFD", "expect_lark": true, "reason": "`ucschar` ranges: %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD" }, { - "value": "ucschar/󐀀-󟿽/󠀀-󯿽", + "value": "ucschar/\uDB00\uDC00-\uDB3F\uDFFD/\uDB40\uDC00-\uDB7F\uDFFD", "expect_lark": true, "reason": "`ucschar` ranges: %xD0000-DFFFD / %xE0000-EFFFD" }, diff --git a/tests/test_syntax.py b/tests/test_syntax.py index c096cc1..f650262 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -4,12 +4,20 @@ from typing import TYPE_CHECKING -import pytest +import re from typing import Any, Callable +import pytest + import rfc3987_syntax2 as sut -from . import valid_syntax_data, invalid_syntax_data, SyntaxCase, T_syntax_file +from . import ( + TESTS_DATA_FILES, + valid_syntax_data, + invalid_syntax_data, + SyntaxCase, + T_syntax_file, +) def syntax_data_as_params(src_cb: Callable[[], T_syntax_file]) -> Any: @@ -18,6 +26,12 @@ def syntax_data_as_params(src_cb: Callable[[], T_syntax_file]) -> Any: yield pytest.param(term, example, id=f"{term}-{example['value']}") +def test_fixture_json_files_are_ascii_only() -> None: + for path in TESTS_DATA_FILES.values(): + text = path.read_text(encoding="ascii") + assert not re.search(r"\\u[0-9a-f]*[a-f][0-9a-f]*", text) + + @pytest.mark.parametrize("term,valid_example", syntax_data_as_params(valid_syntax_data)) def test_is_valid_syntax(term: str, valid_example: SyntaxCase) -> None: actual = sut.is_valid_syntax(term=term, value=valid_example["value"]) From 02119b616e6f1ade69b4f9ec3a092e1bcb20c43c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:35:14 +0000 Subject: [PATCH 10/17] Tighten ASCII fixture regression test --- tests/test_syntax.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_syntax.py b/tests/test_syntax.py index f650262..de53ce0 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -29,7 +29,8 @@ def syntax_data_as_params(src_cb: Callable[[], T_syntax_file]) -> Any: def test_fixture_json_files_are_ascii_only() -> None: for path in TESTS_DATA_FILES.values(): text = path.read_text(encoding="ascii") - assert not re.search(r"\\u[0-9a-f]*[a-f][0-9a-f]*", text) + for escape in re.findall(r"\\u[0-9A-Fa-f]{4}", text): + assert escape[2:] == escape[2:].upper() @pytest.mark.parametrize("term,valid_example", syntax_data_as_params(valid_syntax_data)) From dc6524ba373ea794fa7ec3c2a163be2a321ac875 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:36:07 +0000 Subject: [PATCH 11/17] Clean up JSON fixture test imports --- tests/test_syntax.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_syntax.py b/tests/test_syntax.py index de53ce0..ad72bfd 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -2,10 +2,9 @@ # Copyright (c) 2026 Jan Kowalleck - modifications and maintenance # SPDX-License-Identifier: MIT -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Callable import re -from typing import Any, Callable import pytest From 075e436be42f0a1a04b3a8107764e5e205e09948 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:37:11 +0000 Subject: [PATCH 12/17] Disambiguate uppercase JSON unicode escapes --- tests/_data/invalid_syntax.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/_data/invalid_syntax.json b/tests/_data/invalid_syntax.json index 25ad4f6..cbf224e 100644 --- a/tests/_data/invalid_syntax.json +++ b/tests/_data/invalid_syntax.json @@ -1499,7 +1499,7 @@ "reason": "Unassigned code point" }, { - "value": "\u10FFFF", + "value": "\u10FF\u0046\u0046", "reason": "Outside defined private planes" }, { From 7fc0c0ae2166b4276b859ab378e9f59a740819c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:40:48 +0000 Subject: [PATCH 13/17] chore(tests): move fixture ascii policy to metadata --- tests/_data/.editorconfig | 9 +++++++++ tests/_data/.gitattributes | 2 ++ tests/test_syntax.py | 9 --------- 3 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 tests/_data/.editorconfig create mode 100644 tests/_data/.gitattributes diff --git a/tests/_data/.editorconfig b/tests/_data/.editorconfig new file mode 100644 index 0000000..82c922d --- /dev/null +++ b/tests/_data/.editorconfig @@ -0,0 +1,9 @@ +# Compatibility: EditorConfig has no ASCII charset setting. +# Keep these JSON fixtures ASCII-only and represent non-ASCII code points +# with JSON Unicode escapes. + +[*.json] +indent_style = space +indent_size = 4 +insert_final_newline = true +end_of_line = lf diff --git a/tests/_data/.gitattributes b/tests/_data/.gitattributes new file mode 100644 index 0000000..2fad8de --- /dev/null +++ b/tests/_data/.gitattributes @@ -0,0 +1,2 @@ +# Compatibility: keep fixture JSON files opaque to Git's text handling. +*.json binary diff --git a/tests/test_syntax.py b/tests/test_syntax.py index ad72bfd..b058c45 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -4,8 +4,6 @@ from typing import TYPE_CHECKING, Any, Callable -import re - import pytest import rfc3987_syntax2 as sut @@ -25,13 +23,6 @@ def syntax_data_as_params(src_cb: Callable[[], T_syntax_file]) -> Any: yield pytest.param(term, example, id=f"{term}-{example['value']}") -def test_fixture_json_files_are_ascii_only() -> None: - for path in TESTS_DATA_FILES.values(): - text = path.read_text(encoding="ascii") - for escape in re.findall(r"\\u[0-9A-Fa-f]{4}", text): - assert escape[2:] == escape[2:].upper() - - @pytest.mark.parametrize("term,valid_example", syntax_data_as_params(valid_syntax_data)) def test_is_valid_syntax(term: str, valid_example: SyntaxCase) -> None: actual = sut.is_valid_syntax(term=term, value=valid_example["value"]) From 9bbc3ce79e1117a00fc65e7377d881ac75f87381 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 20 Jul 2026 18:03:00 +0200 Subject: [PATCH 14/17] encoding Signed-off-by: Jan Kowalleck --- tests/_data/.editorconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/_data/.editorconfig b/tests/_data/.editorconfig index 82c922d..8ffdbaf 100644 --- a/tests/_data/.editorconfig +++ b/tests/_data/.editorconfig @@ -3,6 +3,7 @@ # with JSON Unicode escapes. [*.json] +charset = latin1 indent_style = space indent_size = 4 insert_final_newline = true From a2cf68fbf993fe6987566f5069540e5293eb1d7f Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 20 Jul 2026 18:33:52 +0200 Subject: [PATCH 15/17] use only entries gform that RFC Signed-off-by: Jan Kowalleck --- tests/_data/invalid_syntax.json | 68 -------------------- tests/_data/valid_syntax.json | 110 +------------------------------- tests/test_syntax.py | 4 +- 3 files changed, 5 insertions(+), 177 deletions(-) diff --git a/tests/_data/invalid_syntax.json b/tests/_data/invalid_syntax.json index cbf224e..e17ad63 100644 --- a/tests/_data/invalid_syntax.json +++ b/tests/_data/invalid_syntax.json @@ -1639,60 +1639,6 @@ "reason": "Colon in host" } ], - "gen_delims": [ - { - "value": ":path", - "reason": "Colon used as prefix" - }, - { - "value": "/dir//file", - "reason": "Double slash" - }, - { - "value": "param#extra", - "reason": "Fragment not part of param" - }, - { - "value": "query?again", - "reason": "Double query markers" - }, - { - "value": "#frag1#frag2", - "reason": "Multiple fragments" - }, - { - "value": "[host", - "reason": "Unclosed bracket" - }, - { - "value": "]host", - "reason": "Bracket without match" - }, - { - "value": "user@:host", - "reason": "Colon in host" - }, - { - "value": "mailto:user@example.com#", - "reason": "Dangling fragment" - }, - { - "value": ":@", - "reason": "Misuse of delimiters" - }, - { - "value": "/?#@", - "reason": "Invalid sequence of gen-delims" - }, - { - "value": "[:]", - "reason": "Unescaped gen-delims in host" - }, - { - "value": "http://[host]", - "reason": "Improper IPv6 use" - } - ], "ifragment": [ { "value": "frag ment", @@ -2007,20 +1953,6 @@ "reason": "Improper IPv6 format" } ], - "reserved": [ - { - "value": " ", - "reason": "Space is not a reserved character" - }, - { - "value": "%", - "reason": "Requires encoding" - }, - { - "value": "\uD83D\uDCA1", - "reason": "Emoji is not a reserved character" - } - ], "scheme": [ { "value": "1http", diff --git a/tests/_data/valid_syntax.json b/tests/_data/valid_syntax.json index 247e5eb..ffb6690 100644 --- a/tests/_data/valid_syntax.json +++ b/tests/_data/valid_syntax.json @@ -45,12 +45,12 @@ "reason": "U+2FFFD: boundary end of supplementary ucschar range %x20000-2FFFD" }, { - "value": "\uDB40\uDC00", - "reason": "U+E0000: boundary start of supplementary ucschar range %xE0000-EFFFD" + "value": "\uDB44\uDC00", + "reason": "U+E1000: boundary start of supplementary ucschar range %xE1000-EFFFD" }, { "value": "\uDB7F\uDFFD", - "reason": "U+EFFFD: boundary end of supplementary ucschar range %xE0000-EFFFD" + "reason": "U+EFFFD: boundary end of supplementary ucschar range %xE1000-EFFFD" } ], "iunreserved": [ @@ -946,36 +946,6 @@ "reason": "" } ], - "gen_delims": [ - { - "value": ":", - "reason": "Colon is a gen-delim" - }, - { - "value": "/", - "reason": "Slash is a gen-delim" - }, - { - "value": "?", - "reason": "Question mark is a gen-delim" - }, - { - "value": "#", - "reason": "Hash is a gen-delim" - }, - { - "value": "[", - "reason": "Left bracket is a gen-delim" - }, - { - "value": "]", - "reason": "Right bracket is a gen-delim" - }, - { - "value": "@", - "reason": "At-sign is a gen-delim" - } - ], "ifragment": [ { "value": "", @@ -1333,80 +1303,6 @@ "reason": "Empty string is a valid IRI reference (empty irelative-ref)" } ], - "reserved": [ - { - "value": ":", - "reason": "Colon is a gen-delim (reserved)" - }, - { - "value": "/", - "reason": "Slash is a gen-delim (reserved)" - }, - { - "value": "?", - "reason": "Question mark is a gen-delim (reserved)" - }, - { - "value": "#", - "reason": "Hash is a gen-delim (reserved)" - }, - { - "value": "[", - "reason": "Left bracket is a gen-delim (reserved)" - }, - { - "value": "]", - "reason": "Right bracket is a gen-delim (reserved)" - }, - { - "value": "@", - "reason": "At-sign is a gen-delim (reserved)" - }, - { - "value": "!", - "reason": "Exclamation mark is a sub-delim (reserved)" - }, - { - "value": "$", - "reason": "Dollar sign is a sub-delim (reserved)" - }, - { - "value": "&", - "reason": "Ampersand is a sub-delim (reserved)" - }, - { - "value": "'", - "reason": "Single quote is a sub-delim (reserved)" - }, - { - "value": "(", - "reason": "Left paren is a sub-delim (reserved)" - }, - { - "value": ")", - "reason": "Right paren is a sub-delim (reserved)" - }, - { - "value": "*", - "reason": "Asterisk is a sub-delim (reserved)" - }, - { - "value": "+", - "reason": "Plus is a sub-delim (reserved)" - }, - { - "value": ",", - "reason": "Comma is a sub-delim (reserved)" - }, - { - "value": ";", - "reason": "Semicolon is a sub-delim (reserved)" - }, - { - "value": "=", - "reason": "Equals sign is a sub-delim (reserved)" - } - ], "scheme": [ { "value": "http", diff --git a/tests/test_syntax.py b/tests/test_syntax.py index b058c45..a3c30f2 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -25,7 +25,7 @@ def syntax_data_as_params(src_cb: Callable[[], T_syntax_file]) -> Any: @pytest.mark.parametrize("term,valid_example", syntax_data_as_params(valid_syntax_data)) def test_is_valid_syntax(term: str, valid_example: SyntaxCase) -> None: - actual = sut.is_valid_syntax(term=term, value=valid_example["value"]) + actual = getattr(sut, f'is_valid_syntax_{term}')(valid_example["value"]) print("") print( f"Testing {term} with {valid_example['value']} : {valid_example['reason']}" @@ -37,7 +37,7 @@ def test_is_valid_syntax(term: str, valid_example: SyntaxCase) -> None: @pytest.mark.parametrize("term,invalid_example", syntax_data_as_params(invalid_syntax_data)) def test_not_is_valid_syntax(term: str, invalid_example: SyntaxCase) -> None: - actual = sut.is_valid_syntax(term=term, value=invalid_example["value"]) + actual = getattr(sut, f'is_valid_syntax_{term}')(invalid_example["value"]) print("") print( f"Testing {term} with {invalid_example['value']} : {invalid_example['reason']}" From 93b92488eb9f8a227f6e1a786376c88ce20392c0 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 20 Jul 2026 18:34:05 +0200 Subject: [PATCH 16/17] feat Signed-off-by: Jan Kowalleck --- src/rfc3987_syntax2/syntax_helpers.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/rfc3987_syntax2/syntax_helpers.py b/src/rfc3987_syntax2/syntax_helpers.py index ecdb943..bbf821f 100644 --- a/src/rfc3987_syntax2/syntax_helpers.py +++ b/src/rfc3987_syntax2/syntax_helpers.py @@ -22,9 +22,10 @@ "is_valid_syntax_iri", "is_valid_syntax_iri_reference", "is_valid_syntax_absolute_iri", + "is_valid_syntax_scheme", + "is_valid_syntax_ihier_part", "is_valid_syntax_irelative_ref", "is_valid_syntax_irelative_part", - "is_valid_syntax_ihier_part", "is_valid_syntax_iauthority", "is_valid_syntax_iuserinfo", "is_valid_syntax_ihost", @@ -52,11 +53,13 @@ "is_valid_syntax_ls32", "is_valid_syntax_ipv4address", "is_valid_syntax_dec_octet", + "is_valid_syntax_digit", + "is_valid_syntax_non_zero", "is_valid_syntax_unreserved", "is_valid_syntax_alpha", - "is_valid_syntax_digit", "is_valid_syntax_hexdig", "is_valid_syntax_port", + "is_valid_syntax_pct_encoded", ] @@ -173,12 +176,14 @@ def _get_syntax_parser() -> Lark: is_valid_syntax_absolute_iri = make_syntax_validator("absolute_iri") +is_valid_syntax_scheme = make_syntax_validator("scheme") + +is_valid_syntax_ihier_part = make_syntax_validator("ihier_part") + is_valid_syntax_irelative_ref = make_syntax_validator("irelative_ref") is_valid_syntax_irelative_part = make_syntax_validator("irelative_part") -is_valid_syntax_ihier_part = make_syntax_validator("ihier_part") - is_valid_syntax_iauthority = make_syntax_validator("iauthority") is_valid_syntax_iuserinfo = make_syntax_validator("iuserinfo") @@ -233,16 +238,20 @@ def _get_syntax_parser() -> Lark: is_valid_syntax_dec_octet = make_syntax_validator("dec_octet") +is_valid_syntax_digit = make_syntax_validator("digit") + +is_valid_syntax_non_zero = make_syntax_validator("non_zero") + is_valid_syntax_unreserved = make_syntax_validator("unreserved") is_valid_syntax_alpha = make_syntax_validator("alpha") -is_valid_syntax_digit = make_syntax_validator("digit") - is_valid_syntax_hexdig = make_syntax_validator("hexdig") is_valid_syntax_port = make_syntax_validator("port") +is_valid_syntax_pct_encoded = make_syntax_validator("pct_encoded") + # region lazy loaded attrs if TYPE_CHECKING: # types for lazy-loaded symbols From 42f379e88243b54d50209eb757ca70ef6284c495 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Mon, 20 Jul 2026 18:39:21 +0200 Subject: [PATCH 17/17] docs Signed-off-by: Jan Kowalleck --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c122dc3..2eedfc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,19 @@ All notable changes to this project will be documented in this file. * Fix `RFC3987_SYNTAX_TERMS` correctly containing "ipath_rootless" (via [#41]) * Fix `RFC3987_SYNTAX_TERMS` correctly containing "ipath_empty" (via [#41]) * Fix `ucschar` range (via [#45]) +* Added + * Function `is_valid_syntax_scheme` (via [#48]) + * Function `is_valid_syntax_non_zero` (via [#48]) + * Function `is_valid_syntax_pct_encoded` (via [#48]) * Performance * Public symbols use lazy loading (via [#43]) +* Tests + * Add grammar regression tests (via [#48]) [#41]: https://github.com/jkowalleck/rfc3987-syntax2/pull/41 [#43]: https://github.com/jkowalleck/rfc3987-syntax2/pull/43 [#45]: https://github.com/jkowalleck/rfc3987-syntax2/pull/45 +[#48]: https://github.com/jkowalleck/rfc3987-syntax2/pull/48 ## v1.2.0 - 2026-07-16