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}