//! 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>,
},
}