snix_castore/proto/
url.rs

1use crate::proto;
2use data_encoding::BASE64URL_NOPAD;
3use prost::Message;
4use tracing::warn;
5
6/// From a given root node and nar_size, writes a castore-infused NAR path to the writer.
7pub fn write_infused_nar_path(
8    w: &mut impl std::fmt::Write,
9    node: crate::Node,
10    nar_size: u64,
11) -> Result<(), std::fmt::Error> {
12    let proto_node = proto::Entry::from_name_and_node("".into(), node).encode_to_vec();
13
14    write!(
15        w,
16        "nar/snix-castore/{}?narsize={}",
17        BASE64URL_NOPAD.encode(&proto_node),
18        nar_size,
19    )
20}
21
22/// Detects and parses a castore-infused NAR path.
23#[tracing::instrument(level = "trace")]
24pub fn parse_infused_nar_path(s: &str) -> Option<(crate::Node, u64)> {
25    let s = s.strip_prefix("nar/snix-castore/")?;
26    let (node_enc, nar_size_str) = s.split_once("?narsize=")?;
27    let nar_size: u64 = nar_size_str
28        .parse()
29        .inspect_err(|err| {
30            warn!(%err, "got invalid narsize");
31        })
32        .ok()?;
33
34    let node = parse_urlsafe_proto(node_enc)?;
35
36    Some((node, nar_size))
37}
38
39/// Takes a urlsafe base64-encoded castore node (in proto) and attempts to parse it.
40pub fn parse_urlsafe_proto(node_enc: impl AsRef<[u8]>) -> Option<crate::Node> {
41    // check the proto size to be somewhat reasonable before parsing it.
42    if node_enc.as_ref().len() > BASE64URL_NOPAD.encode_len(4096) {
43        warn!("rejected too large root node");
44        return None;
45    }
46
47    let node_bytes = BASE64URL_NOPAD
48        .decode(node_enc.as_ref())
49        .inspect_err(|err| {
50            warn!(%err, "unable to decode node b64");
51        })
52        .ok()?;
53
54    // parse the proto
55    let node_proto: proto::Entry = Message::decode(node_bytes.as_slice())
56        .inspect_err(|err| {
57            warn!(%err, "unable to decode node proto");
58        })
59        .ok()?;
60
61    let root_node = node_proto
62        .try_into_anonymous_node()
63        .inspect_err(|err| {
64            warn!(%err, "node validation failed");
65        })
66        .ok()?;
67
68    Some(root_node)
69}
70
71#[cfg(test)]
72mod test {
73    use rstest::rstest;
74
75    use crate::Node;
76    use crate::fixtures::{
77        DIRECTORY_COMPLICATED, HELLOWORLD_BLOB_CONTENTS, HELLOWORLD_BLOB_DIGEST,
78    };
79    use crate::proto::{parse_infused_nar_path, write_infused_nar_path};
80
81    #[rstest]
82    #[case::directory_complicated(
83        Node::Directory { digest: DIRECTORY_COMPLICATED.digest(), size: DIRECTORY_COMPLICATED.size() },
84    )]
85    #[case::blob_helloworld(
86        Node::File { digest: *HELLOWORLD_BLOB_DIGEST, size: HELLOWORLD_BLOB_CONTENTS.len() as u64, executable: false }
87    )]
88    fn roundtrip(#[case] node: Node) {
89        let nar_size: u64 = 42; // dummy value
90        let path = {
91            let mut path = String::new();
92            write_infused_nar_path(&mut path, node.clone(), nar_size).expect("write string");
93            path
94        };
95
96        assert_eq!(
97            (node, nar_size),
98            parse_infused_nar_path(&path).expect("to parse"),
99            "expected to roundtrip"
100        );
101    }
102}