Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376use std::fmt;
use jacquard_common::types::string::AtUri;use unicode_normalization::UnicodeNormalization;
/// Build the visible path for a Part membership.////// A bare Part name retains the historical `models/` root. Once the author/// supplies a relative path, it is preserved as-is (after `CanonicalPath`/// validation). Parts owned by another authority are put below an explicit/// source namespace. Jacquard validates the URI, collection, and rkey before/// this function reaches the string/SQL boundary.pub fn project_part_path( project: &AtUri, part: &AtUri, declared_name: &str,) -> Result<CanonicalPath, PathDiagnostic> { if project.authority().as_str() == part.authority().as_str() { let declared = if declared_name.contains('/') { declared_name.to_owned() } else { format!("models/{declared_name}") }; return CanonicalPath::parse(&declared); }
let Some(collection) = part.collection() else { return Err(PathDiagnostic::SourceIdentity); }; let Some(rkey) = part.rkey() else { return Err(PathDiagnostic::SourceIdentity); }; if collection.as_str() != "space.polymodel.library.part" { return Err(PathDiagnostic::SourceIdentity); } let path = format!( "external/{}/{}", encode_identity_segment(part.authority().as_str()), encode_identity_segment(rkey.as_str()), ); CanonicalPath::parse(&path)}
/// Decode an external Part path into its authority and rkey.////// The rkey is deliberately returned unchanged: Jacquard's validated rkey is/// already a safe relative path segment. This is a pure inverse of the/// authority encoding and is useful for route/path round-trip tests.#[cfg(test)]pub fn decode_external_part_path(path: &CanonicalPath) -> Option<(String, String)> { let segments = path.segments().collect::<Vec<_>>(); if segments.first().copied()? != "external" || segments.len() != 3 { return None; } let authority = decode_identity_segment(segments[1])?; let rkey = decode_identity_segment(segments[2])?; jacquard_common::types::recordkey::Rkey::<String>::new_owned(rkey.clone()).ok()?; Some((authority, rkey))}
/// Escape one authority component with a self-delimiting, ASCII-only codec.////// `x` is the only prefix and `_hh` is the only escape form. The unescaped/// alphabet excludes `_`, `%`, `.`, and path separators, so escaped-looking/// input, dot segments, Unicode normalization, and URL decoding cannot alias/// an encoded identity.fn encode_identity_segment(value: &str) -> String { let mut out = String::from("x"); for byte in value.as_bytes() { if byte.is_ascii_alphanumeric() || *byte == b'-' || *byte == b'~' { out.push(*byte as char); } else { out.push('_'); out.push(hex_digit(byte >> 4)); out.push(hex_digit(byte & 0x0f)); } } out}
#[cfg(test)]fn decode_identity_segment(value: &str) -> Option<String> { let bytes = value.strip_prefix('x')?.as_bytes(); let mut decoded = Vec::with_capacity(bytes.len()); let mut index = 0; while index < bytes.len() { match bytes[index] { b'_' if index + 2 < bytes.len() => { let high = from_hex(bytes[index + 1])?; let low = from_hex(bytes[index + 2])?; decoded.push(high << 4 | low); index += 3; } byte if byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'~' => { decoded.push(byte); index += 1; } _ => return None, } } String::from_utf8(decoded).ok()}
fn hex_digit(value: u8) -> char { char::from(b"0123456789abcdef"[value as usize])}
#[cfg(test)]fn from_hex(value: u8) -> Option<u8> { match value { b'0'..=b'9' => Some(value - b'0'), b'a'..=b'f' => Some(value - b'a' + 10), _ => None, }}
/// A validated, NFC-preserving virtual filename. It is never interpreted as a URL.#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]pub struct CanonicalPath(String);
impl CanonicalPath { pub fn parse(input: &str) -> Result<Self, PathDiagnostic> { if input.is_empty() || input.starts_with('/') || input.ends_with('/') { return Err(PathDiagnostic::Slash); } if input.contains('%') { return Err(PathDiagnostic::Percent); } if input.contains('\\') { return Err(PathDiagnostic::Backslash); } if input.contains('?') || input.contains('#') { return Err(PathDiagnostic::QueryOrFragment); } if input.chars().any(|c| c.is_control() || c == ':') { return Err(PathDiagnostic::ControlOrColon); } if input.nfc().collect::<String>() != input { return Err(PathDiagnostic::NotNfc); } for segment in input.split('/') { if segment.is_empty() || segment == "." || segment == ".." { return Err(PathDiagnostic::Segment); } } Ok(Self(input.to_owned())) }
pub fn as_str(&self) -> &str { &self.0 }
pub fn segments(&self) -> impl Iterator<Item = &str> { self.0.split('/') }}
impl fmt::Display for CanonicalPath { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) }}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]pub enum RootId { Mpd, Manifest, Models, Parts, P, External, OfficialParts, OfficialP,}
impl RootId { pub fn as_str(self) -> &'static str { match self { Self::Mpd => "mpd", Self::Manifest => "manifest", Self::Models => "models", Self::Parts => "parts", Self::P => "p", Self::External => "external", Self::OfficialParts => "official-parts", Self::OfficialP => "official-p", } }
pub fn case_insensitive(self) -> bool { !matches!(self, Self::Mpd | Self::Manifest | Self::External) }}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]pub enum PathDiagnostic { Slash, Percent, Backslash, QueryOrFragment, ControlOrColon, NotNfc, Segment, SourceIdentity,}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]pub struct LookupEntry { pub root: RootId, pub path: CanonicalPath, pub target: String,}
/// Resolve according to PM-75's fixed source precedence./// The maps contain already validated entries and only perform filename lookup.pub fn resolve_precedence( path: &CanonicalPath, mpd: &[LookupEntry], manifest: &[LookupEntry], uploaded: &[LookupEntry], official: &[LookupEntry],) -> Option<LookupEntry> { exact(path, mpd) .or_else(|| exact(path, manifest)) .or_else(|| { roots( path, uploaded, &[RootId::Models, RootId::Parts, RootId::P, RootId::External], ) }) .or_else(|| roots(path, official, &[RootId::OfficialParts, RootId::OfficialP]))}
fn exact(path: &CanonicalPath, entries: &[LookupEntry]) -> Option<LookupEntry> { entries.iter().find(|e| e.path == *path).cloned()}
fn roots(path: &CanonicalPath, entries: &[LookupEntry], roots: &[RootId]) -> Option<LookupEntry> { roots.iter().find_map(|root| { entries .iter() .filter(|e| e.root == *root) .find(|e| { e.path == *path || (root.case_insensitive() && e.path.as_str().eq_ignore_ascii_case(path.as_str())) }) .cloned() })}
#[cfg(test)]mod tests { use super::*;
#[test] fn rejects_unsafe_and_non_nfc_paths() { for value in [ "", "/a", "a/", "a//b", "a/../b", "a/./b", "a%2Fb", "a\\b", "a:b", "a?b", "a#b", "a\u{0000}b", ] { assert!(CanonicalPath::parse(value).is_err(), "{value:?}"); } assert!(CanonicalPath::parse("cafe\u{301}").is_err()); assert_eq!(CanonicalPath::parse("café").unwrap().as_str(), "café"); }
#[test] fn authority_path_codec_round_trips_and_is_injective() { let project = AtUri::new_owned("at://did:plc:owner/space.polymodel.library.thing/project").unwrap(); let mut paths = Vec::new(); for (authority, rkey) in [ ("did:plc:alpha", "same_rkey"), ("did:plc:beta", "same_rkey"), ("did:plc:alpha", "same-rkey"), ("did:plc:alpha", "a.b"), ("did:plc:alpha", "a~b:c"), ] { let part = AtUri::new_owned(format!( "at://{authority}/space.polymodel.library.part/{rkey}" )) .unwrap(); let path = project_part_path(&project, &part, "ignored.ldr").unwrap(); assert!(path.as_str().starts_with("external/x")); assert!(!path.as_str().starts_with('/')); assert!(!path.as_str().contains("..")); let (decoded_authority, decoded_rkey) = decode_external_part_path(&path).unwrap(); assert_eq!(decoded_authority, authority); assert_eq!(decoded_rkey, rkey); paths.push(path); } for (index, path) in paths.iter().enumerate() { assert!(paths[index + 1..].iter().all(|other| other != path)); } }
#[test] fn same_authority_preserves_declared_project_relative_path() { let project = AtUri::new_owned("at://did:plc:owner/space.polymodel.library.thing/project").unwrap(); let part = AtUri::new_owned("at://did:plc:owner/space.polymodel.library.part/part").unwrap(); assert_eq!( project_part_path(&project, &part, "models/sub/main.ldr") .unwrap() .as_str(), "models/sub/main.ldr" ); assert_eq!( project_part_path(&project, &part, "main.ldr") .unwrap() .as_str(), "models/main.ldr" ); }
#[test] fn external_codec_rejects_path_aliases_and_invalid_rkeys() { let project = AtUri::new_owned("at://did:plc:owner/space.polymodel.library.thing/project").unwrap(); let part = AtUri::new_owned("at://did:plc:external/space.polymodel.library.part/x").unwrap(); let valid = project_part_path(&project, &part, "x.ldr").unwrap(); assert_eq!( decode_external_part_path(&valid).unwrap(), ("did:plc:external".to_owned(), "x".to_owned()) ); let malformed_escape = CanonicalPath::parse("external/x_zz/x").unwrap(); assert!(decode_external_part_path(&malformed_escape).is_none()); assert!(CanonicalPath::parse("external/xdid_3aplc/a%2Fb").is_err()); assert!(CanonicalPath::parse("external/xdid_3aplc/../x").is_err()); assert!( AtUri::<String>::new_owned( "at://did:plc:owner/space.polymodel.library.part/cafe\u{301}" ) .is_err() ); }
#[test] fn precedence_and_ascii_case_policy_are_stable() { let path = CanonicalPath::parse("parts/3001.dat").unwrap(); let entry = |root, target: &str| LookupEntry { root, path: path.clone(), target: target.into(), }; let found = resolve_precedence(&path, &[], &[], &[entry(RootId::Models, "model")], &[]).unwrap(); assert_eq!(found.target, "model"); let upper = CanonicalPath::parse("PARTS/3001.DAT").unwrap(); assert!( resolve_precedence(&upper, &[], &[], &[entry(RootId::Models, "model")], &[]).is_some() ); let mpd = entry(RootId::Mpd, "mpd"); assert_eq!( resolve_precedence(&path, &[mpd], &[], &[], &[]) .unwrap() .target, "mpd" ); }}