nix_compat/nar/listing/
mod.rs

1//! Parser for the Nix archive listing format, aka .ls.
2//!
3//! LS files are produced by the C++ Nix implementation via `write-nar-listing=1` query parameter
4//! passed to a store implementation when transferring store paths.
5//!
6//! Listing files contains metadata about a file and its offset in the corresponding NAR.
7//!
8//! NOTE: LS entries does not offer any integrity field to validate the retrieved file at the provided
9//! offset. Validating the contents is the caller's responsibility.
10
11use std::{
12    collections::HashMap,
13    path::{Component, Path},
14};
15
16use serde::Deserialize;
17
18#[cfg(test)]
19mod test;
20
21#[derive(Debug, thiserror::Error)]
22pub enum ListingError {
23    // TODO: add an enum of what component was problematic
24    // reusing `std::path::Component` is not possible as it contains a lifetime.
25    /// An unsupported path component can be:
26    /// - either a Windows prefix (`C:\\`, `\\share\\`)
27    /// - either a parent directory (`..`)
28    /// - either a root directory (`/`)
29    #[error("unsupported path component")]
30    UnsupportedPathComponent,
31    #[error("invalid encoding for entry component")]
32    InvalidEncoding,
33}
34
35#[derive(Debug, Deserialize)]
36#[serde(tag = "type", rename_all = "lowercase")]
37pub enum ListingEntry {
38    Regular {
39        size: u64,
40        #[serde(default)]
41        executable: bool,
42        #[serde(rename = "narOffset")]
43        nar_offset: u64,
44    },
45    Directory {
46        // It's tempting to think that the key should be a `Vec<u8>`
47        // but Nix does not support that and will fail to emit a listing version 1 for any non-UTF8
48        // encodeable string.
49        entries: HashMap<String, ListingEntry>,
50    },
51    Symlink {
52        target: String,
53    },
54}
55
56impl ListingEntry {
57    /// Given a relative path without `..` component, this will locate, relative to this entry, a
58    /// deeper entry.
59    ///
60    /// If the path is invalid, a listing error [`ListingError`] will be returned.
61    /// If the entry cannot be found, `None` will be returned.
62    pub fn locate<P: AsRef<Path>>(&self, path: P) -> Result<Option<&ListingEntry>, ListingError> {
63        // We perform a simple DFS on the components of the path
64        // while rejecting dangerous components, e.g. `..` or `/`
65        // Files and symlinks are *leaves*, i.e. we return them
66        let mut cur = self;
67        for component in path.as_ref().components() {
68            match component {
69                Component::CurDir => continue,
70                Component::RootDir | Component::Prefix(_) | Component::ParentDir => {
71                    return Err(ListingError::UnsupportedPathComponent)
72                }
73                Component::Normal(file_or_dir_name) => {
74                    if let Self::Directory { entries } = cur {
75                        // As Nix cannot encode non-UTF8 components in the listing (see comment on
76                        // the `Directory` enum variant), invalid encodings path components are
77                        // errors.
78                        let entry_name = file_or_dir_name
79                            .to_str()
80                            .ok_or(ListingError::InvalidEncoding)?;
81
82                        if let Some(new_entry) = entries.get(entry_name) {
83                            cur = new_entry;
84                        } else {
85                            return Ok(None);
86                        }
87                    } else {
88                        return Ok(None);
89                    }
90                }
91            }
92        }
93
94        // By construction, we found the node that corresponds to the path traversal.
95        Ok(Some(cur))
96    }
97}
98
99#[derive(Debug)]
100pub struct ListingVersion<const V: u8>;
101
102#[derive(Debug, thiserror::Error)]
103#[error("Invalid version: {0}")]
104struct ListingVersionError(u8);
105
106impl<'de, const V: u8> Deserialize<'de> for ListingVersion<V> {
107    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
108    where
109        D: serde::Deserializer<'de>,
110    {
111        let value = u8::deserialize(deserializer)?;
112        if value == V {
113            Ok(ListingVersion::<V>)
114        } else {
115            Err(serde::de::Error::custom(ListingVersionError(value)))
116        }
117    }
118}
119
120#[derive(Debug, Deserialize)]
121#[serde(untagged)]
122#[non_exhaustive]
123pub enum Listing {
124    V1 {
125        root: ListingEntry,
126        version: ListingVersion<1>,
127    },
128}