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
116
117
118
119
120
121
122
123
124
125
126
127
128
//! Parser for the Nix archive listing format, aka .ls.
//!
//! LS files are produced by the C++ Nix implementation via `write-nar-listing=1` query parameter
//! passed to a store implementation when transferring store paths.
//!
//! Listing files contains metadata about a file and its offset in the corresponding NAR.
//!
//! NOTE: LS entries does not offer any integrity field to validate the retrieved file at the provided
//! offset. Validating the contents is the caller's responsibility.

use std::{
    collections::HashMap,
    path::{Component, Path},
};

use serde::Deserialize;

#[cfg(test)]
mod test;

#[derive(Debug, thiserror::Error)]
pub enum ListingError {
    // TODO: add an enum of what component was problematic
    // reusing `std::path::Component` is not possible as it contains a lifetime.
    /// An unsupported path component can be:
    /// - either a Windows prefix (`C:\\`, `\\share\\`)
    /// - either a parent directory (`..`)
    /// - either a root directory (`/`)
    #[error("unsupported path component")]
    UnsupportedPathComponent,
    #[error("invalid encoding for entry component")]
    InvalidEncoding,
}

#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum ListingEntry {
    Regular {
        size: u64,
        #[serde(default)]
        executable: bool,
        #[serde(rename = "narOffset")]
        nar_offset: u64,
    },
    Directory {
        // It's tempting to think that the key should be a `Vec<u8>`
        // but Nix does not support that and will fail to emit a listing version 1 for any non-UTF8
        // encodeable string.
        entries: HashMap<String, ListingEntry>,
    },
    Symlink {
        target: String,
    },
}

impl ListingEntry {
    /// Given a relative path without `..` component, this will locate, relative to this entry, a
    /// deeper entry.
    ///
    /// If the path is invalid, a listing error [`ListingError`] will be returned.
    /// If the entry cannot be found, `None` will be returned.
    pub fn locate<P: AsRef<Path>>(&self, path: P) -> Result<Option<&ListingEntry>, ListingError> {
        // We perform a simple DFS on the components of the path
        // while rejecting dangerous components, e.g. `..` or `/`
        // Files and symlinks are *leaves*, i.e. we return them
        let mut cur = self;
        for component in path.as_ref().components() {
            match component {
                Component::CurDir => continue,
                Component::RootDir | Component::Prefix(_) | Component::ParentDir => {
                    return Err(ListingError::UnsupportedPathComponent)
                }
                Component::Normal(file_or_dir_name) => {
                    if let Self::Directory { entries } = cur {
                        // As Nix cannot encode non-UTF8 components in the listing (see comment on
                        // the `Directory` enum variant), invalid encodings path components are
                        // errors.
                        let entry_name = file_or_dir_name
                            .to_str()
                            .ok_or(ListingError::InvalidEncoding)?;

                        if let Some(new_entry) = entries.get(entry_name) {
                            cur = new_entry;
                        } else {
                            return Ok(None);
                        }
                    } else {
                        return Ok(None);
                    }
                }
            }
        }

        // By construction, we found the node that corresponds to the path traversal.
        Ok(Some(cur))
    }
}

#[derive(Debug)]
pub struct ListingVersion<const V: u8>;

#[derive(Debug, thiserror::Error)]
#[error("Invalid version: {0}")]
struct ListingVersionError(u8);

impl<'de, const V: u8> Deserialize<'de> for ListingVersion<V> {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let value = u8::deserialize(deserializer)?;
        if value == V {
            Ok(ListingVersion::<V>)
        } else {
            Err(serde::de::Error::custom(ListingVersionError(value)))
        }
    }
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum Listing {
    V1 {
        root: ListingEntry,
        version: ListingVersion<1>,
    },
}