Skip to main content

bee/manifest/
locator.rs

1//! [`ResourceLocator`] — a `Reference`-or-ENS-name wrapper used as
2//! the path segment of `/bzz/{ref}` and `/bytes/{ref}`. Mirrors
3//! bee-js `ResourceLocator`.
4//!
5//! Also defines [`resolve_path`], an offline manifest path-resolution
6//! helper: given an unmarshaled [`MantarayNode`] and a slash-delimited
7//! path, return the chunk reference of the target leaf.
8
9use std::fmt;
10
11use crate::manifest::{MantarayNode, is_null_address};
12use crate::swarm::{Error, Reference};
13
14/// Either a content reference (`Reference`) or an ENS name
15/// (`<label>.eth`). Either form is valid as the `{ref}` segment in
16/// Bee URLs; ENS resolution happens server-side.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub enum ResourceLocator {
19    /// A content reference (32 or 64 bytes).
20    Reference(Reference),
21    /// An ENS name. Bee accepts these in place of a hex reference.
22    Ens(String),
23}
24
25impl ResourceLocator {
26    /// Build from any input. Strings containing `.eth` are treated
27    /// as ENS names; otherwise the input is parsed as a hex
28    /// reference. Mirrors the `new ResourceLocator(...)` constructor
29    /// in bee-js.
30    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    /// Build from an existing reference.
38    pub fn from_reference(r: Reference) -> Self {
39        Self::Reference(r)
40    }
41
42    /// Build from a string assumed to be an ENS name. Empty input
43    /// returns [`Error::Argument`]; deeper validation is left to
44    /// Bee.
45    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    /// Renders the path-segment form: hex for [`ResourceLocator::Reference`],
56    /// the raw label for [`ResourceLocator::Ens`].
57    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
71/// Offline manifest path resolution: walk `manifest` and return the
72/// target [`Reference`] for `path`. Mirrors Bee's server-side
73/// behavior for `GET /bzz/{ref}/{path}`.
74///
75/// `path` is a slash-delimited UTF-8 string (a leading `/` is
76/// allowed and ignored). Returns [`Error::Argument`] when no fork
77/// matches the path or when the matched node has no target.
78pub 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
87/// The fork target of `node` as an owned [`Reference`], or `None`
88/// for branch-only nodes (NULL_ADDRESS).
89pub 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}