parser_properties.rs (4047B)
1 use std::{fmt::Debug, str::FromStr}; 2 3 use radroots_blossom::{ 4 AuthorizationClaim, BlobUrl, MediaType, Sha256, 5 authorization::{AuthorizationAction, AuthorizationContent, ServerDomain}, 6 hash::{FileExtension, HashPath}, 7 }; 8 9 const ASCII_MUTATION_ALPHABET: &[u8] = 10 b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._/:?#%\\ @\t\n\r\0;=+"; 11 12 #[test] 13 fn upload_url_preserves_origin_without_replacing_or_approving_the_blob() { 14 let hash = Sha256::digest(b"bound upload bytes"); 15 for (origin, expected) in [ 16 ("https://media.example", "https://media.example/upload"), 17 ("https://media.example:443", "https://media.example/upload"), 18 ( 19 "https://media.example:8443", 20 "https://media.example:8443/upload", 21 ), 22 ("http://127.0.0.1:21100", "http://127.0.0.1:21100/upload"), 23 ("http://[::1]:21100", "http://[::1]:21100/upload"), 24 ( 25 "https://[2001:db8::1]:8443", 26 "https://[2001:db8::1]:8443/upload", 27 ), 28 ("http://insecure.example", "http://insecure.example/upload"), 29 ] { 30 for extension in ["", ".png", ".jpeg"] { 31 let blob = BlobUrl::parse(&format!("{origin}/{hash}{extension}")).unwrap(); 32 let original = blob.to_string(); 33 let approved_before = blob.clone().approve(); 34 assert_eq!(blob.upload_url(), expected); 35 assert_eq!(blob.upload_url(), expected); 36 assert_eq!(blob.to_string(), original); 37 assert_eq!(blob.hash_path().hash(), hash); 38 assert_eq!(blob.clone().approve(), approved_before); 39 assert!(BlobUrl::parse(&blob.upload_url()).is_err()); 40 } 41 } 42 } 43 44 #[test] 45 fn deterministic_parser_mutation_corpus_never_panics_and_round_trips_successes() { 46 let fixed = [ 47 "", 48 "get", 49 "cdn.example.com", 50 "text/plain", 51 "png", 52 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 53 "/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.png", 54 "https://cdn.example.com/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.png", 55 "upload image", 56 ]; 57 58 for value in fixed 59 .into_iter() 60 .map(str::to_owned) 61 .chain((0_u64..4_096).map(mutation_string)) 62 { 63 assert_parse_display_round_trip::<Sha256>(&value); 64 assert_parse_display_round_trip::<FileExtension>(&value); 65 assert_parse_display_round_trip::<HashPath>(&value); 66 assert_parse_display_round_trip::<BlobUrl>(&value); 67 assert_parse_display_round_trip::<MediaType>(&value); 68 assert_parse_display_round_trip::<AuthorizationAction>(&value); 69 assert_parse_display_round_trip::<AuthorizationContent>(&value); 70 assert_parse_display_round_trip::<ServerDomain>(&value); 71 72 let tags = vec![ 73 vec!["t".to_owned(), value.clone()], 74 vec!["expiration".to_owned(), value.clone()], 75 vec!["server".to_owned(), value.clone()], 76 vec!["x".to_owned(), value.clone()], 77 ]; 78 let _ = AuthorizationClaim::parse(&value, 1_800_000_000, &tags); 79 } 80 } 81 82 fn assert_parse_display_round_trip<T>(value: &str) 83 where 84 T: FromStr + ToString + PartialEq + Debug, 85 T::Err: Debug, 86 { 87 let Ok(parsed) = value.parse::<T>() else { 88 return; 89 }; 90 let canonical = parsed.to_string(); 91 assert_eq!(canonical.parse::<T>().unwrap(), parsed); 92 } 93 94 fn mutation_string(seed: u64) -> String { 95 let mut state = seed.wrapping_add(1).wrapping_mul(0x9e37_79b9_7f4a_7c15); 96 let length = usize::try_from(next(&mut state) % 385).unwrap(); 97 let mut value = String::with_capacity(length); 98 for _ in 0..length { 99 let index = usize::try_from(next(&mut state)).unwrap() % ASCII_MUTATION_ALPHABET.len(); 100 value.push(char::from(ASCII_MUTATION_ALPHABET[index])); 101 } 102 value 103 } 104 105 fn next(state: &mut u64) -> u64 { 106 *state ^= *state << 13; 107 *state ^= *state >> 7; 108 *state ^= *state << 17; 109 *state 110 }