1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
use tracing::trace;

use crate::nixbase32;

/// The mime type used for NAR files, both compressed and uncompressed
pub const MIME_TYPE_NAR: &str = "application/x-nix-nar";
/// The mime type used for NARInfo files
pub const MIME_TYPE_NARINFO: &str = "text/x-nix-narinfo";
/// The mime type used for the `nix-cache-info` file
pub const MIME_TYPE_CACHE_INFO: &str = "text/x-nix-cache-info";

/// Parses a `14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0k.nar`
/// string and returns the nixbase32-decoded digest, as well as the compression
/// suffix (which might be empty).
pub fn parse_nar_str(s: &str) -> Option<([u8; 32], &str)> {
    if !s.is_char_boundary(52) {
        trace!("invalid string, no char boundary at 52");
        return None;
    }

    let (hash_str, suffix) = s.split_at(52);

    // we know hash_str is 52 bytes, so it's ok to unwrap here.
    let hash_str_fixed: [u8; 52] = hash_str.as_bytes().try_into().unwrap();

    match suffix.strip_prefix(".nar") {
        Some(compression_suffix) => match nixbase32::decode_fixed(hash_str_fixed) {
            Err(e) => {
                trace!(err=%e, "invalid nixbase32 encoding");
                None
            }
            Ok(digest) => Some((digest, compression_suffix)),
        },
        None => {
            trace!("no .nar suffix");
            None
        }
    }
}

/// Parses a `3mzh8lvgbynm9daj7c82k2sfsfhrsfsy.narinfo` string and returns the
/// nixbase32-decoded digest.
pub fn parse_narinfo_str(s: &str) -> Option<[u8; 20]> {
    if !s.is_char_boundary(32) {
        trace!("invalid string, no char boundary at 32");
        return None;
    }

    match s.split_at(32) {
        (hash_str, ".narinfo") => {
            // we know this is 32 bytes, so it's ok to unwrap here.
            let hash_str_fixed: [u8; 32] = hash_str.as_bytes().try_into().unwrap();

            match nixbase32::decode_fixed(hash_str_fixed) {
                Err(e) => {
                    trace!(err=%e, "invalid nixbase32 encoding");
                    None
                }
                Ok(digest) => Some(digest),
            }
        }
        _ => {
            trace!("invalid string, no .narinfo suffix");
            None
        }
    }
}

#[cfg(test)]
mod test {
    use super::{parse_nar_str, parse_narinfo_str};
    use hex_literal::hex;

    #[test]
    fn parse_nar_str_success() {
        assert_eq!(
            (
                hex!("13a8cf7ca57f68a9f1752acee36a72a55187d3a954443c112818926f26109d91"),
                ""
            ),
            parse_nar_str("14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0k.nar").unwrap()
        );

        assert_eq!(
            (
                hex!("13a8cf7ca57f68a9f1752acee36a72a55187d3a954443c112818926f26109d91"),
                ".xz"
            ),
            parse_nar_str("14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0k.nar.xz").unwrap()
        )
    }

    #[test]
    fn parse_nar_str_failure() {
        assert!(parse_nar_str("14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0").is_none());
        assert!(
            parse_nar_str("14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0🦊.nar").is_none()
        )
    }
    #[test]
    fn parse_narinfo_str_success() {
        assert_eq!(
            hex!("8a12321522fd91efbd60ebb2481af88580f61600"),
            parse_narinfo_str("00bgd045z0d4icpbc2yyz4gx48ak44la.narinfo").unwrap()
        );
    }

    #[test]
    fn parse_narinfo_str_failure() {
        assert!(parse_narinfo_str("00bgd045z0d4icpbc2yyz4gx48ak44la").is_none());
        assert!(parse_narinfo_str("/00bgd045z0d4icpbc2yyz4gx48ak44la").is_none());
        assert!(parse_narinfo_str("000000").is_none());
        assert!(parse_narinfo_str("00bgd045z0d4icpbc2yyz4gx48ak44l🦊.narinfo").is_none());
    }
}