fix: accept RFC 3986 scheme:opaque-part URIs without //

is_valid_uri required a literal "://", but the atproto uri string
format follows RFC 3986's generic URI grammar, which also allows
"scheme:opaque-part" forms with no authority (e.g. urn:isbn:...).
Records using such values were rejected once production lexicons
enable strict validation.

Reported as #130.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jack Platten
2026-08-27 20:07:59 +00:00
committed by Tangled
co-authored by Claude Sonnet 5
parent c0caa93228
commit 739db41130
+25 -8
View File
@@ -35,14 +35,21 @@ pub fn is_valid_datetime(s: &str) -> bool {
}
pub fn is_valid_uri(s: &str) -> bool {
s.split_once("://").is_some_and(|(scheme, rest)| {
!scheme.is_empty()
&& scheme
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '.' || c == '-')
&& scheme.starts_with(|c: char| c.is_ascii_alphabetic())
&& !rest.is_empty()
})
let Some((scheme, rest)) = s.split_once(':') else {
return false;
};
let valid_scheme = !scheme.is_empty()
&& scheme.starts_with(|c: char| c.is_ascii_alphabetic())
&& scheme
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '.' || c == '-');
if !valid_scheme {
return false;
}
match rest.strip_prefix("//") {
Some(authority_and_path) => !authority_and_path.is_empty(),
None => !rest.is_empty(),
}
}
pub fn is_valid_cid(s: &str) -> bool {
@@ -151,6 +158,16 @@ mod tests {
assert!(!is_valid_uri("https://"));
}
#[test]
fn test_valid_uris_without_authority() {
// RFC 3986 hier-part doesn't require "//": scheme ":" opaque-part is also a URI.
assert!(is_valid_uri("urn:isbn:9780141439518"));
assert!(is_valid_uri("mailto:user@example.com"));
assert!(is_valid_uri("mbid:70766a5a-3f95-4b19-96c8-a2c9c4a5e6e5"));
assert!(!is_valid_uri("urn:"));
assert!(!is_valid_uri(":no-scheme"));
}
#[test]
fn test_valid_cids() {
assert!(is_valid_cid("bafyreiabcdef123456"));