Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 8.2 kB View raw
1/// see https://atproto.com/specs/did#at-protocol-did-identifier-syntax 2/// this parser is intentinonally lax: it should accept all valid DIDs, and 3/// may accept some invalid DIDs. 4/// 5/// at the moment this implementation might also be quite bad and incomplete 6pub fn parse_did(s: &str) -> Option<String> { 7 // for now, just working through the rules laid out in the docs in order, 8 // without much regard for efficiency for now. 9 10 // newer specs say max 2048 chars 11 if s.len() > 2048 { 12 return None; 13 } 14 15 // The entire URI is made up of a subset of ASCII, containing letters (A-Z, a-z), 16 // digits (0-9), period, underscore, colon, percent sign, or hyphen (._:%-) 17 if !s 18 .chars() 19 .all(|c| matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '.' | '_' | ':' | '%' | '-')) 20 { 21 return None; 22 } 23 24 // The URI is case-sensitive 25 // -> (nothing to check) 26 27 // The URI starts with lowercase `did:` 28 let unprefixed = s.strip_prefix("did:")?; 29 30 // The method segment is one or more lowercase letters (a-z), followed by : 31 let (method, identifier) = unprefixed.split_once(':')?; 32 if !method.chars().all(|c| c.is_ascii_lowercase()) { 33 return None; 34 } 35 36 // The remainder of the URI (the identifier) may contain any of the above-allowed 37 // ASCII characters, except for percent-sign (%) 38 // -> ok, ugh, gotta know our encoding context for this 39 40 // The URI (and thus the remaining identifier) may not end in ':'. 41 if identifier.ends_with(':') { 42 return None; 43 } 44 45 // Percent-sign (%) is used for "percent encoding" in the identifier section, and 46 // must always be followed by two hex characters 47 // -> again incoding context (bleh) 48 49 // Query (?) and fragment (#) sections are allowed in DID URIs, but not in DID 50 // identifiers. In the context of atproto, the query and fragment parts are not 51 // allowed. 52 // -> disallow here -- the uri decoder should already split them out first. 53 54 // DID identifiers do not generally have a maximum length restriction, but in the 55 // context of atproto, there is an initial hard limit of 2 KB. 56 // -> we're in atproto, so sure, let's enforce it. (would be sensible to do this 57 // -> first but we're following doc order) 58 if s.len() > (2 * 2_usize.pow(10)) { 59 return None; 60 } 61 62 // -> it's not actually written in the spec, but by example in the spec, the 63 // -> identifier cannot be empty 64 if identifier.is_empty() { 65 return None; 66 } 67 68 Some(s.to_string()) 69 // the only normalization we might want would be percent-decoding, but we 70 // probably leave that to the uri decoder 71} 72 73#[cfg(test)] 74mod tests { 75 use super::*; 76 77 #[test] 78 fn test_did_too_long() { 79 let long = concat!( 80 "did:long:zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 81 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 82 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 83 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 84 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 85 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 86 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 87 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 88 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 89 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 90 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 91 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 92 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 93 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 94 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 95 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 96 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 97 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 98 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 99 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 100 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 101 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 102 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 103 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 104 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 105 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 106 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 107 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 108 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 109 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 110 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 111 "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 112 ); 113 assert_eq!(parse_did(long), None); 114 } 115 116 #[test] 117 fn test_did_parse() { 118 for (case, expected, detail) in vec![ 119 ("", None, "empty str"), 120 (" ", None, "whitespace str"), 121 ("z", None, "not a did"), 122 ("did:plc", None, "no identifier separator colon"), 123 ("did:plc:", None, "missing identifier"), 124 ( 125 "did:web:bad-example.com", 126 Some("did:web:bad-example.com"), 127 "web did", 128 ), 129 ( 130 "did:plc:hdhoaan3xa3jiuq4fg4mefid", 131 Some("did:plc:hdhoaan3xa3jiuq4fg4mefid"), 132 "plc did", 133 ), 134 ( 135 "DID:plc:hdhoaan3xa3jiuq4fg4mefid", 136 None, 137 "'did:' prefix must be lowercase", 138 ), 139 ( 140 "did:ok:z", 141 Some("did:ok:z"), 142 "unknown did methods are allowed", 143 ), 144 ("did:BAD:z", None, "non-lowercase methods are not allowed"), 145 ("did:bad:z$z", None, "invalid chars are not allowed"), 146 ( 147 "did:ok:z:z", 148 Some("did:ok:z:z"), 149 "colons are allowed in identifier", 150 ), 151 ("did:bad:z:", None, "colons not are allowed at the end"), 152 ("did:bad:z?q=y", None, "queries are not allowed in atproto"), 153 ("did:bad:z#a", None, "anchors are not allowed in atproto"), 154 ] { 155 assert_eq!(parse_did(case), expected.map(|s| s.to_string()), "{detail}"); 156 } 157 } 158 159 #[test] 160 fn test_doc_exmples_atproto() { 161 // https://atproto.com/specs/did#at-protocol-did-identifier-syntax 162 for case in ["did:plc:z72i7hdynmk6r22z27h6tvur", "did:web:blueskyweb.xyz"] { 163 assert!(parse_did(case).is_some(), "should pass: {case}") 164 } 165 } 166 167 #[test] 168 fn test_doc_exmples_lexicon() { 169 // https://atproto.com/specs/did#at-protocol-did-identifier-syntax 170 for case in [ 171 "did:method:val:two", 172 "did:m:v", 173 "did:method::::val", 174 "did:method:-:_:.", 175 "did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N", 176 ] { 177 assert!(parse_did(case).is_some(), "should pass: {case}") 178 } 179 } 180 181 #[test] 182 fn test_doc_exmples_invalid() { 183 // https://atproto.com/specs/did#at-protocol-did-identifier-syntax 184 for case in [ 185 "did:METHOD:val", 186 "did:m123:val", 187 "DID:method:val", 188 "did:method:", 189 "did:method:val/two", 190 "did:method:val?two", 191 "did:method:val#two", 192 ] { 193 assert!(parse_did(case).is_none(), "should fail: {case}") 194 } 195 } 196}