Skip to main content

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::BTreeMap,
13    path::{Component, Path},
14};
15
16use serde::{Deserialize, Serialize, ser::SerializeMap};
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: BTreeMap<String, ListingEntry>,
50    },
51    Symlink {
52        target: String,
53    },
54}
55
56// Custom Serialize impl, as otherwise type would always come first, causing noncanonical JSON.
57impl Serialize for ListingEntry {
58    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
59    where
60        S: serde::Serializer,
61    {
62        match self {
63            ListingEntry::Regular {
64                size,
65                executable,
66                nar_offset,
67            } => {
68                let mut map = serializer.serialize_map(Some(if *executable { 4 } else { 3 }))?;
69                if *executable {
70                    map.serialize_entry("executable", &true)?;
71                }
72                map.serialize_entry("narOffset", nar_offset)?;
73                map.serialize_entry("size", size)?;
74                map.serialize_entry("type", "regular")?;
75                map.end()
76            }
77            ListingEntry::Directory { entries } => {
78                let mut map = serializer.serialize_map(Some(2))?;
79                map.serialize_entry("entries", entries)?;
80                map.serialize_entry("type", "directory")?;
81                map.end()
82            }
83            ListingEntry::Symlink { target } => {
84                let mut map = serializer.serialize_map(Some(2))?;
85                map.serialize_entry("target", target)?;
86                map.serialize_entry("type", "symlink")?;
87                map.end()
88            }
89        }
90    }
91}
92
93impl ListingEntry {
94    /// Given a relative path without `..` component, this will locate, relative to this entry, a
95    /// deeper entry.
96    ///
97    /// If the path is invalid, a listing error [`ListingError`] will be returned.
98    /// If the entry cannot be found, `None` will be returned.
99    pub fn locate<P: AsRef<Path>>(&self, path: P) -> Result<Option<&ListingEntry>, ListingError> {
100        // We perform a simple DFS on the components of the path
101        // while rejecting dangerous components, e.g. `..` or `/`
102        // Files and symlinks are *leaves*, i.e. we return them
103        let mut cur = self;
104        for component in path.as_ref().components() {
105            match component {
106                Component::CurDir => continue,
107                Component::RootDir | Component::Prefix(_) | Component::ParentDir => {
108                    return Err(ListingError::UnsupportedPathComponent);
109                }
110                Component::Normal(file_or_dir_name) => {
111                    if let Self::Directory { entries } = cur {
112                        // As Nix cannot encode non-UTF8 components in the listing (see comment on
113                        // the `Directory` enum variant), invalid encodings path components are
114                        // errors.
115                        let entry_name = file_or_dir_name
116                            .to_str()
117                            .ok_or(ListingError::InvalidEncoding)?;
118
119                        if let Some(new_entry) = entries.get(entry_name) {
120                            cur = new_entry;
121                        } else {
122                            return Ok(None);
123                        }
124                    } else {
125                        return Ok(None);
126                    }
127                }
128            }
129        }
130
131        // By construction, we found the node that corresponds to the path traversal.
132        Ok(Some(cur))
133    }
134}
135
136#[derive(Debug)]
137pub struct ListingVersion<const V: u8>;
138
139#[derive(Debug, thiserror::Error)]
140#[error("Invalid version: {0}")]
141struct ListingVersionError(u8);
142
143impl<'de, const V: u8> Deserialize<'de> for ListingVersion<V> {
144    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
145    where
146        D: serde::Deserializer<'de>,
147    {
148        let value = u8::deserialize(deserializer)?;
149        if value == V {
150            Ok(ListingVersion::<V>)
151        } else {
152            Err(serde::de::Error::custom(ListingVersionError(value)))
153        }
154    }
155}
156
157impl<const V: u8> Serialize for ListingVersion<V> {
158    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
159    where
160        S: serde::Serializer,
161    {
162        V.serialize(serializer)
163    }
164}
165
166#[derive(Debug, Deserialize, Serialize)]
167#[serde(untagged)]
168#[non_exhaustive]
169pub enum Listing {
170    V1 {
171        root: ListingEntry,
172        version: ListingVersion<1>,
173    },
174}