1use std::fmt;
10
11use crate::manifest::{MantarayNode, is_null_address};
12use crate::swarm::{Error, Reference};
13
14#[derive(Clone, Debug, PartialEq, Eq)]
18pub enum ResourceLocator {
19 Reference(Reference),
21 Ens(String),
23}
24
25impl ResourceLocator {
26 pub fn parse(input: &str) -> Result<Self, Error> {
31 if input.contains(".eth") {
32 return Ok(Self::Ens(input.to_owned()));
33 }
34 Ok(Self::Reference(Reference::from_hex(input)?))
35 }
36
37 pub fn from_reference(r: Reference) -> Self {
39 Self::Reference(r)
40 }
41
42 pub fn from_ens(name: impl Into<String>) -> Result<Self, Error> {
46 let name = name.into();
47 if name.is_empty() {
48 return Err(Error::argument("empty ENS name"));
49 }
50 Ok(Self::Ens(name))
51 }
52}
53
54impl fmt::Display for ResourceLocator {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 match self {
59 ResourceLocator::Reference(r) => f.write_str(&r.to_hex()),
60 ResourceLocator::Ens(name) => f.write_str(name),
61 }
62 }
63}
64
65impl From<Reference> for ResourceLocator {
66 fn from(r: Reference) -> Self {
67 Self::Reference(r)
68 }
69}
70
71pub fn resolve_path(manifest: &MantarayNode, path: &str) -> Result<Reference, Error> {
79 let trimmed = path.trim_start_matches('/');
80 let node = manifest
81 .find(trimmed.as_bytes())
82 .ok_or_else(|| Error::argument(format!("no manifest entry for path {path:?}")))?;
83 target_reference(node)
84 .ok_or_else(|| Error::argument(format!("manifest entry for {path:?} has no target")))
85}
86
87pub fn target_reference(node: &MantarayNode) -> Option<Reference> {
90 if is_null_address(&node.target_address) {
91 return None;
92 }
93 Reference::new(&node.target_address).ok()
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 fn ref_at(byte: u8) -> Reference {
101 Reference::from_hex(&format!("{byte:02x}").repeat(32)).unwrap()
102 }
103
104 #[test]
105 fn resource_locator_parses_ens_names() {
106 let loc = ResourceLocator::parse("hello.eth").unwrap();
107 assert_eq!(loc.to_string(), "hello.eth");
108 assert!(matches!(loc, ResourceLocator::Ens(_)));
109 }
110
111 #[test]
112 fn resource_locator_parses_hex_reference() {
113 let hex = "ab".repeat(32);
114 let loc = ResourceLocator::parse(&hex).unwrap();
115 assert_eq!(loc.to_string(), hex);
116 assert!(matches!(loc, ResourceLocator::Reference(_)));
117 }
118
119 #[test]
120 fn resource_locator_rejects_invalid_hex() {
121 assert!(ResourceLocator::parse("not-hex").is_err());
122 }
123
124 #[test]
125 fn resource_locator_from_reference_round_trip() {
126 let r = Reference::from_hex(&"cd".repeat(32)).unwrap();
127 let loc = ResourceLocator::from_reference(r.clone());
128 assert_eq!(loc.to_string(), r.to_hex());
129 }
130
131 #[test]
132 fn resource_locator_from_ens_rejects_empty() {
133 assert!(ResourceLocator::from_ens("").is_err());
134 }
135
136 #[test]
137 fn resolve_path_finds_added_fork() {
138 let mut m = MantarayNode::new();
139 let target = ref_at(0xaa);
140 m.add_fork(b"index.html", Some(&target), None);
141
142 let got = resolve_path(&m, "/index.html").unwrap();
143 assert_eq!(got, target);
144 }
145
146 #[test]
147 fn resolve_path_handles_nested_paths() {
148 let mut m = MantarayNode::new();
149 let leaf = ref_at(0xbb);
150 m.add_fork(b"assets/logo.png", Some(&leaf), None);
151
152 let got = resolve_path(&m, "assets/logo.png").unwrap();
153 assert_eq!(got, leaf);
154 }
155
156 #[test]
157 fn resolve_path_returns_error_for_missing_path() {
158 let m = MantarayNode::new();
159 assert!(resolve_path(&m, "/nope").is_err());
160 }
161
162 #[test]
163 fn resolve_path_with_leading_slash_works() {
164 let mut m = MantarayNode::new();
165 let leaf = ref_at(0xcc);
166 m.add_fork(b"a", Some(&leaf), None);
167 let got = resolve_path(&m, "/a").unwrap();
168 assert_eq!(got, leaf);
169 }
170}