Skip to main content

nix_compat/nix_http/
mod.rs

1use bstr::ByteSlice;
2use tracing::trace;
3
4use crate::nixbase32;
5
6/// The mime type used for NAR files, both compressed and uncompressed
7pub const MIME_TYPE_NAR: &str = "application/x-nix-nar";
8/// The mime type used for NARInfo files
9pub const MIME_TYPE_NARINFO: &str = "text/x-nix-narinfo";
10/// The mime type used for the `nix-cache-info` file
11pub const MIME_TYPE_CACHE_INFO: &str = "text/x-nix-cache-info";
12/// The mime type used for `$outhash.ls` files
13pub const MIME_TYPE_NAR_LISTING: &str = "application/json";
14
15/// Parses a `14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0k.nar.xz`
16/// string and returns the nixbase32-decoded digest, as well as the compression
17/// suffix (which might be empty).
18pub fn parse_nar_str<S>(s: &S) -> Option<([u8; 32], &[u8])>
19where
20    S: AsRef<[u8]> + ?Sized,
21{
22    if s.as_ref().len() < 52 + 4 {
23        trace!("nar_str too short");
24        return None;
25    }
26    let (hash_str, suffix) = s.as_ref().split_at(52);
27    // we know this is 52 bytes, so it's ok to unwrap here.
28    let hash_str_fixed: [u8; 52] = hash_str.try_into().unwrap();
29
30    // suffix needs to start with ".nar".
31    let compression_suffix = suffix
32        .as_bstr()
33        .strip_prefix(b".nar")
34        .ok_or_else(|| {
35            trace!("suffix does not start with .nar");
36        })
37        .ok()?;
38
39    let digest = nixbase32::decode_fixed(hash_str_fixed)
40        .inspect_err(|err| {
41            trace!(%err, "invalid nixbase32 encoding");
42        })
43        .ok()?;
44
45    Some((digest, compression_suffix))
46}
47
48#[derive(Debug, PartialEq, Eq)]
49pub enum RequestType {
50    Narinfo,
51    Listing,
52}
53
54/// Parses a `3mzh8lvgbynm9daj7c82k2sfsfhrsfsy.narinfo` or `3mzh8lvgbynm9daj7c82k2sfsfhrsfsy.ls`
55/// string and returns the nixbase32-decoded digest, and what was requested.
56pub fn parse_outhash_str(s: impl AsRef<[u8]>) -> Option<([u8; 20], RequestType)> {
57    if s.as_ref().len() < 32 + 3 {
58        trace!("outhash_str too short");
59        return None;
60    }
61
62    let (hash_str, suffix) = s.as_ref().split_at(32);
63    // we know this is 32 bytes, so it's ok to unwrap here.
64    let hash_str_fixed: [u8; 32] = hash_str.try_into().unwrap();
65
66    let request_type = match suffix {
67        b".narinfo" => RequestType::Narinfo,
68        b".ls" => RequestType::Listing,
69        _ => {
70            trace!("invalid string, no .narinfo or .ls suffix");
71            return None;
72        }
73    };
74
75    let digest = nixbase32::decode_fixed(hash_str_fixed)
76        .inspect_err(|err| {
77            trace!(%err, "invalid nixbase32 encoding");
78        })
79        .ok()?;
80
81    Some((digest, request_type))
82}
83
84#[cfg(test)]
85mod test {
86    use crate::nix_http::RequestType;
87
88    use super::{parse_nar_str, parse_outhash_str};
89    use hex_literal::hex;
90
91    #[test]
92    fn parse_nar_str_success() {
93        assert_eq!(
94            (
95                hex!("13a8cf7ca57f68a9f1752acee36a72a55187d3a954443c112818926f26109d91"),
96                "".as_bytes()
97            ),
98            parse_nar_str("14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0k.nar").unwrap()
99        );
100
101        assert_eq!(
102            (
103                hex!("13a8cf7ca57f68a9f1752acee36a72a55187d3a954443c112818926f26109d91"),
104                ".xz".as_bytes()
105            ),
106            parse_nar_str("14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0k.nar.xz").unwrap()
107        )
108    }
109
110    #[test]
111    fn parse_nar_str_failure() {
112        assert!(parse_nar_str("14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0").is_none());
113        assert!(
114            parse_nar_str("14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0🦊.nar").is_none()
115        )
116    }
117    #[test]
118    fn parse_outhash_str_success() {
119        assert_eq!(
120            (
121                hex!("8a12321522fd91efbd60ebb2481af88580f61600"),
122                RequestType::Narinfo
123            ),
124            parse_outhash_str("00bgd045z0d4icpbc2yyz4gx48ak44la.narinfo").unwrap()
125        );
126        assert_eq!(
127            (
128                hex!("8a12321522fd91efbd60ebb2481af88580f61600"),
129                RequestType::Listing
130            ),
131            parse_outhash_str("00bgd045z0d4icpbc2yyz4gx48ak44la.ls").unwrap()
132        );
133    }
134
135    #[test]
136    fn parse_outhash_str_failure() {
137        assert!(parse_outhash_str("00bgd045z0d4icpbc2yyz4gx48ak44la").is_none());
138        assert!(parse_outhash_str("/00bgd045z0d4icpbc2yyz4gx48ak44la").is_none());
139        assert!(parse_outhash_str("000000").is_none());
140        assert!(parse_outhash_str("00bgd045z0d4icpbc2yyz4gx48ak44l🦊.narinfo").is_none());
141        assert!(parse_outhash_str("00bgd045z0d4icpbc2yyz4gx48ak44la.nah").is_none());
142    }
143}