From acbf2cf8125ea0a8f3bdfda763d111191b8976c4 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 28 Oct 2025 16:42:09 -0400 Subject: [PATCH] Fix various URI fragment issues Signed-off-by: Juan Cruz Viotti --- schemas/ietf/uri/url.json | 6 +- test/ietf/uri/uri-reference.test.json | 155 ++++++++++++++++++++++++++ test/ietf/uri/url.test.json | 100 +++++++++++++++++ 3 files changed, 258 insertions(+), 3 deletions(-) diff --git a/schemas/ietf/uri/url.json b/schemas/ietf/uri/url.json index 4dd5e16..6cfcb97 100644 --- a/schemas/ietf/uri/url.json +++ b/schemas/ietf/uri/url.json @@ -18,7 +18,7 @@ "allOf": [ { "$comment": "Valid authority structure - host can be empty", - "pattern": "^[^:]+://(?:[^@/]*@)?(?:\\[[^\\]]+\\]|[^:/]*)(?::[0-9]*)?(?:/.*)?$" + "pattern": "^[^:]+://(?:[^@/]*@)?(?:\\[[^\\]]+\\]|[^:/]*)(?::[0-9]*)?(?:/.*|[?#].*)?$" } ], "anyOf": [ @@ -35,14 +35,14 @@ "not": { "pattern": "^[^:]+://(?:[^@]*@)?(?:(?:25[6-9]|2[6-9][0-9]|[3-9][0-9]{2}|[1-9][0-9]{3,})[.]|[.](?:25[6-9]|2[6-9][0-9]|[3-9][0-9]{2}|[1-9][0-9]{3,})[.]|[.](?:25[6-9]|2[6-9][0-9]|[3-9][0-9]{2}|[1-9][0-9]{3,})[.]|[.](?:25[6-9]|2[6-9][0-9]|[3-9][0-9]{2}|[1-9][0-9]{3,})(?:[:/]|$))" }, - "pattern": "^[^:]+://(?:[^@]*@)?[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+(?:[:/]|$)" + "pattern": "^[^:]+://(?:[^@]*@)?[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+(?:[:/?#]|$)" }, { "$comment": "Domain or reg-name (not IPv4)", "not": { "pattern": "^[^:]+://(?:[^@]*@)?[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+(?:[:/]|$)" }, - "pattern": "^[^:]+://(?:[^@]*@)?[A-Za-z0-9.-]+(?:[:/]|$)" + "pattern": "^[^:]+://(?:[^@]*@)?[A-Za-z0-9.-]+(?:[:/?#]|$)" } ], "pattern": "^[A-Za-z][A-Za-z0-9+\\-.]*://" diff --git a/test/ietf/uri/uri-reference.test.json b/test/ietf/uri/uri-reference.test.json index 92f1d84..08bde46 100644 --- a/test/ietf/uri/uri-reference.test.json +++ b/test/ietf/uri/uri-reference.test.json @@ -556,6 +556,161 @@ "description": "Fragment with hash in value", "data": "#fragment#with#hashes", "valid": true + }, + { + "description": "URL with fragment without path", + "data": "http://example.com#fragment", + "valid": true + }, + { + "description": "URL with query without path", + "data": "http://example.com?query=value", + "valid": true + }, + { + "description": "URL with query and fragment without path", + "data": "http://example.com?query=value#fragment", + "valid": true + }, + { + "description": "URL with empty fragment without path", + "data": "http://example.com#", + "valid": true + }, + { + "description": "URL with empty query without path", + "data": "http://example.com?", + "valid": true + }, + { + "description": "HTTPS with fragment without path", + "data": "https://secure.example.com#section", + "valid": true + }, + { + "description": "HTTPS with query without path", + "data": "https://secure.example.com?token=abc123", + "valid": true + }, + { + "description": "FTP with fragment without path", + "data": "ftp://ftp.example.com#readme", + "valid": true + }, + { + "description": "FTP with query without path", + "data": "ftp://ftp.example.com?mode=binary", + "valid": true + }, + { + "description": "Network path with query without path", + "data": "//example.com?search=test", + "valid": true + }, + { + "description": "Network path with fragment without path", + "data": "//example.com#top", + "valid": true + }, + { + "description": "Network path with query and fragment without path", + "data": "//example.com?q=1#section", + "valid": true + }, + { + "description": "IPv4 URL with fragment without path", + "data": "http://192.168.1.1#section", + "valid": true + }, + { + "description": "IPv4 URL with query without path", + "data": "http://192.168.1.1?param=value", + "valid": true + }, + { + "description": "IPv6 URL with fragment without path", + "data": "http://[::1]#anchor", + "valid": true + }, + { + "description": "IPv6 URL with query without path", + "data": "http://[::1]?key=val", + "valid": true + }, + { + "description": "URL with port and fragment without path", + "data": "http://example.com:8080#section", + "valid": true + }, + { + "description": "URL with port and query without path", + "data": "http://example.com:8080?search=term", + "valid": true + }, + { + "description": "URL with userinfo and fragment without path", + "data": "http://user@example.com#top", + "valid": true + }, + { + "description": "URL with userinfo and query without path", + "data": "http://user@example.com?q=test", + "valid": true + }, + { + "description": "URL with full userinfo and query without path", + "data": "http://user:pass@example.com?param=value", + "valid": true + }, + { + "description": "URL with full userinfo and fragment without path", + "data": "http://user:pass@example.com#anchor", + "valid": true + }, + { + "description": "Complex URL with all authority components and query without path", + "data": "http://user:pass@example.com:8080?key=value", + "valid": true + }, + { + "description": "Complex URL with all authority components and fragment without path", + "data": "http://user:pass@example.com:8080#section", + "valid": true + }, + { + "description": "URL with multiple query parameters without path", + "data": "http://example.com?a=1&b=2&c=3", + "valid": true + }, + { + "description": "URL with percent-encoded query without path", + "data": "http://example.com?key=value%20here", + "valid": true + }, + { + "description": "URL with complex query encoding without path", + "data": "http://example.com?a=b&c=%2Fd%2Fe", + "valid": true + }, + { + "description": "URL with fragment containing special characters without path", + "data": "http://example.com#section-1.2", + "valid": true + }, + { + "description": "URL with empty query and non-empty fragment without path", + "data": "http://example.com?#fragment", + "valid": true + }, + { + "description": "Custom scheme with fragment without path", + "data": "custom://example.com#section", + "valid": true + }, + { + "description": "Custom scheme with query without path", + "data": "custom://example.com?param=value", + "valid": true } ] } diff --git a/test/ietf/uri/url.test.json b/test/ietf/uri/url.test.json index b7a1f64..66d2807 100644 --- a/test/ietf/uri/url.test.json +++ b/test/ietf/uri/url.test.json @@ -216,6 +216,106 @@ "description": "URI with empty authority and path", "data": "scheme:///path", "valid": true + }, + { + "description": "Fragment without path", + "data": "http://example.com#fragment", + "valid": true + }, + { + "description": "Query without path", + "data": "http://example.com?query=value", + "valid": true + }, + { + "description": "Query and fragment without path", + "data": "http://example.com?query=value#fragment", + "valid": true + }, + { + "description": "Empty fragment without path", + "data": "http://example.com#", + "valid": true + }, + { + "description": "Empty query without path", + "data": "http://example.com?", + "valid": true + }, + { + "description": "Multiple query parameters without path", + "data": "http://example.com?a=1&b=2&c=3", + "valid": true + }, + { + "description": "Query with percent-encoding without path", + "data": "http://example.com?key=value%20here", + "valid": true + }, + { + "description": "Complex query without path", + "data": "http://example.com?a=b&c=%2Fd%2Fe", + "valid": true + }, + { + "description": "Fragment with special characters without path", + "data": "http://example.com#section-1.2", + "valid": true + }, + { + "description": "IPv4 with fragment without path", + "data": "http://192.168.1.1#anchor", + "valid": true + }, + { + "description": "IPv4 with query without path", + "data": "http://192.168.1.1?param=value", + "valid": true + }, + { + "description": "IPv6 with fragment without path", + "data": "http://[2001:db8::1]#section", + "valid": true + }, + { + "description": "IPv6 with query without path", + "data": "http://[2001:db8::1]?key=val", + "valid": true + }, + { + "description": "Port with fragment without path", + "data": "http://example.com:8080#top", + "valid": true + }, + { + "description": "Port with query without path", + "data": "http://example.com:8080?search=term", + "valid": true + }, + { + "description": "Userinfo with fragment without path", + "data": "http://user@example.com#section", + "valid": true + }, + { + "description": "Userinfo with query without path", + "data": "http://user@example.com?q=test", + "valid": true + }, + { + "description": "Complex authority with query without path", + "data": "http://user:pass@example.com:8080?param=value", + "valid": true + }, + { + "description": "Complex authority with fragment without path", + "data": "http://user:pass@example.com:8080#anchor", + "valid": true + }, + { + "description": "Complex authority with query and fragment without path", + "data": "http://user:pass@example.com:8080?q=1#top", + "valid": true } ] }