Initial commit

Thanks @riley for the help with code review. Ily ♥️♥️♥️♥️♥️♥️♥️
This commit is contained in:
bad 2022-08-04 15:00:24 +02:00
commit cbeb52a9cb
12 changed files with 493 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
/Cargo.lock

8
Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "narinfo"
version = "0.1.0"
edition = "2021"
description = "A parser for the narinfo file format"
[dependencies]
derive_builder = { version = "0.11.2", default-features = false }

3
nix-cache-info.sample Normal file
View file

@ -0,0 +1,3 @@
StoreDir: /nix/store
WantMassQuery: 1
Priority: 40

2
rustfmt.toml Normal file
View file

@ -0,0 +1,2 @@
hard_tabs = true
normalize_comments = true

10
sample.narinfo Normal file
View file

@ -0,0 +1,10 @@
StorePath: /nix/store/zzxrhj9056vjlanfjkinvhd7458yc2z8-liblouis-3.22.0
URL: nar/0ccqg4il1m9qqh8b6x0x8nn7pjcphr82h2qdfc5gqq8dy7h2kp9x.nar.xz
Compression: xz
FileHash: sha256:0ccqg4il1m9qqh8b6x0x8nn7pjcphr82h2qdfc5gqq8dy7h2kp9x
FileSize: 1914556
NarHash: sha256:0c8ld5yxcr6a6j63mvrqbqiy08q6f85wd74817ai7pvd5nkidcqw
NarSize: 11374872
References: mhhlymrg2m70r8h94cwhv2d7a0c8l7g6-glibc-2.34-210 ppn8983d9b5r6k7mnhkbg6rqw7vgl1ij-libyaml-0.2.5 qm2lv1gpbyn0rsfai40cbvj3h4gz69yc-bash-5.1-p16 sn0w3f12547crckss4ybmnxmi29gpgq7-perl-5.34.1 zzxrhj9056vjlanfjkinvhd7458yc2z8-liblouis-3.22.0
Deriver: dlxmsgfc0am35fh0kiy88zqr91x2dn5j-liblouis-3.22.0.drv
Sig: cache.nixos.org-1:BJ5QGcOta2s76XC6sep9DbAv0x3TILh3hHSKyR+9rFWYuBDTWdHs1KHeUEpw2espE/zPPBp2yURO6/J4Dhf9DQ==

26
src/error.rs Normal file
View file

@ -0,0 +1,26 @@
#[derive(Debug)]
/// The error type returned by all the parsing functions in this crate.
pub enum ParsingError<'a> {
InvalidIntValue { line: &'a str },
UnknownKey { key: &'a str },
InvalidLine { line: &'a str },
InvalidSignature(&'a str),
MissingField(&'static str),
}
impl From<derive_builder::UninitializedFieldError> for ParsingError<'static> {
fn from(e: derive_builder::UninitializedFieldError) -> Self {
Self::MissingField(e.field_name())
}
}
impl<'a> ParsingError<'a> {
pub(crate) fn try_parse_int<'b>(value: &'b str, line: &'a str) -> ParsingResult<'a, usize> {
value
.parse()
.map_err(|_| ParsingError::InvalidIntValue { line })
}
}
/// The result type returned by all the parsing functions in this crate.
pub type ParsingResult<'a, T> = core::result::Result<T, ParsingError<'a>>;

15
src/lib.rs Normal file
View file

@ -0,0 +1,15 @@
#![no_std]
#![deny(clippy::all)]
//! Parse the nix substituter(cache) related files.
extern crate alloc;
pub mod error;
mod narinfo;
mod nix_cache_info;
mod sig;
pub use crate::narinfo::NarInfo;
pub use nix_cache_info::NixCacheInfo;
pub use sig::Sig;

129
src/narinfo/from_str.rs Normal file
View file

@ -0,0 +1,129 @@
use super::{NarInfo, NarInfoBuilder};
use crate::{
error::{ParsingError, ParsingResult},
sig::Sig,
};
use alloc::{borrow::Cow, vec::Vec};
impl<'a> NarInfo<'a> {
/// Parses the contents of a narinfo file from str.
/// This function is also used to implement the [TryFrom] trait for &'a str.
///
/// For duplicate keys the last value is used(same behaviour as libstore).
/// Unknown keys return an error(unlike libstore where they're simply ignored).
///
/// ```
/// # fn http_get_str(_url: &str) -> &'static str { include_str!("../../sample.narinfo") }
/// use narinfo::NarInfo;
/// let data: &str =
/// http_get_str("https://cache.nixos.org/zn2h0kln2b02x4x6jqxymd4sg9cwvdsx.narinfo");
///
/// let parsed = NarInfo::parse(data).unwrap();
/// assert_eq!(parsed.store_path, "/nix/store/zzxrhj9056vjlanfjkinvhd7458yc2z8-liblouis-3.22.0");
/// ```
pub fn parse(value: &'a str) -> ParsingResult<Self> {
let mut builder = NarInfoBuilder::default();
for line in value.lines() {
let (key, value) = line
.split_once(':')
.map(|(k, v)| (k, v.trim()))
.ok_or(ParsingError::InvalidLine { line })?;
match key {
"StorePath" => builder.store_path(Cow::from(value)),
"URL" => builder.url(value),
"Compression" => builder.compression(Some(Cow::from(value))),
"FileHash" => builder.file_hash(Some(value)),
"NarHash" => builder.nar_hash(Cow::from(value)),
"NarSize" => {
let size: usize = ParsingError::try_parse_int(value, line)?;
builder.nar_size(size)
}
"FileSize" => {
let size: usize = ParsingError::try_parse_int(value, line)?;
builder.file_size(Some(size))
}
"Deriver" => builder.deriver(Some(Cow::from(value))),
"System" => builder.system(Some(Cow::from(value))),
"References" => builder.references(value.split(' ').map(Cow::from).collect()),
// TODO: replace with try_collect once that gets stabilized
"Sig" => builder.sigs(value.split(' ').map(Sig::try_from).try_fold(
Vec::new(),
|mut a, c| {
c.map(|c| {
a.push(c);
a
})
},
)?),
_ => return Err(ParsingError::UnknownKey { key }),
};
}
builder.build()
}
}
impl<'a> TryFrom<&'a str> for NarInfo<'a> {
type Error = crate::error::ParsingError<'a>;
fn try_from(value: &'a str) -> ParsingResult<Self> {
NarInfo::parse(value)
}
}
#[cfg(test)]
mod tests {
use alloc::borrow::Cow;
use super::*;
#[test]
fn parses_sample_narinfo() {
let sample = include_str!("../../sample.narinfo");
let info = NarInfo::try_from(sample).unwrap();
assert_eq!(
info.store_path,
"/nix/store/zzxrhj9056vjlanfjkinvhd7458yc2z8-liblouis-3.22.0"
);
assert_eq!(
info.url,
"nar/0ccqg4il1m9qqh8b6x0x8nn7pjcphr82h2qdfc5gqq8dy7h2kp9x.nar.xz"
);
assert_eq!(info.compression, Some(Cow::Borrowed("xz")));
assert_eq!(
info.file_hash,
Some("sha256:0ccqg4il1m9qqh8b6x0x8nn7pjcphr82h2qdfc5gqq8dy7h2kp9x")
);
assert_eq!(info.file_size, Some(1914556));
assert_eq!(
info.nar_hash,
"sha256:0c8ld5yxcr6a6j63mvrqbqiy08q6f85wd74817ai7pvd5nkidcqw"
);
assert_eq!(info.nar_size, 11374872);
let expected_refs = [
"mhhlymrg2m70r8h94cwhv2d7a0c8l7g6-glibc-2.34-210",
"ppn8983d9b5r6k7mnhkbg6rqw7vgl1ij-libyaml-0.2.5",
"qm2lv1gpbyn0rsfai40cbvj3h4gz69yc-bash-5.1-p16",
"sn0w3f12547crckss4ybmnxmi29gpgq7-perl-5.34.1",
"zzxrhj9056vjlanfjkinvhd7458yc2z8-liblouis-3.22.0",
];
for (a, b) in info.references.iter().zip(expected_refs) {
assert_eq!(a, b);
}
assert_eq!(
info.deriver,
Some(Cow::Borrowed(
"dlxmsgfc0am35fh0kiy88zqr91x2dn5j-liblouis-3.22.0.drv"
))
);
let sig = &info.sigs[0];
assert_eq!(sig.key_name, "cache.nixos.org-1");
assert_eq!(sig.sig, "BJ5QGcOta2s76XC6sep9DbAv0x3TILh3hHSKyR+9rFWYuBDTWdHs1KHeUEpw2espE/zPPBp2yURO6/J4Dhf9DQ==");
assert!(info.sigs.len() == 1);
}
}

67
src/narinfo/mod.rs Normal file
View file

@ -0,0 +1,67 @@
mod from_str;
mod to_str;
use crate::sig::Sig;
use alloc::{borrow::Cow, vec::Vec};
pub use from_str::*;
pub use to_str::*;
use derive_builder::Builder;
type FileSize = usize;
/// Struct representing the narinfo file fetched from a nix substituter
/// Based on the [unofficial spec](https://fzakaria.github.io/nix-http-binary-cache-api-spec)
/// and the [libstore narinfo parsing code.](https://github.com/NixOS/nix/blob/6776e65fd960e25b55d11a03324f9007b6dc2a0b/src/libstore/nar-info.cc)
#[derive(Builder, Eq, PartialEq, Debug)]
#[builder(no_std)]
#[builder(build_fn(error = "crate::error::ParsingError<'static>"))]
pub struct NarInfo<'a> {
/// The full store path, including the name part (e.g., glibc-2.7). It must match the requested store path.
pub store_path: Cow<'a, str>,
/// The URL of the NAR, relative to the binary cache URL.
pub url: &'a str,
/// The compression method; Usuall xz or bzip2.
#[builder(default)]
pub compression: Option<Cow<'a, str>>,
/// The cryptographic hash of the NAR (decompressed) in base 32.
pub nar_hash: Cow<'a, str>,
/// The size of the decompressed NAR file.
pub nar_size: FileSize,
/// The cryptographic hash of the file to download in base32.
#[builder(default)]
pub file_hash: Option<&'a str>,
/// The size of the downloaded(compressed) NAR file.
#[builder(default)]
pub file_size: Option<FileSize>,
/// The deriver of the store path, without the Nix store prefix. This field is optional.
#[builder(default)]
pub deriver: Option<Cow<'a, str>>,
/// The Nix platform type of this binary(eg. x86_64-linux).
#[builder(default)]
pub system: Option<Cow<'a, str>>,
/// Store paths for direct runtime dependencies.
#[builder(default)]
pub references: Vec<Cow<'a, str>>,
/// A collection of the signatures signing this package.
#[builder(default)]
pub sigs: Vec<Sig<'a>>,
}
impl<'a> NarInfo<'a> {
/// Get the builder for this struct.
/// The builder is generated by [derive_builder](https://lib.rs/crates/derive_builder)
// The builder is is used internally by the parsing code so we might as well expose it.
// If we ever decide to not use derive_builder we can simply hide it behind an optional feature flag
pub fn builder() -> NarInfoBuilder<'a> {
NarInfoBuilder::default()
}
}

59
src/narinfo/to_str.rs Normal file
View file

@ -0,0 +1,59 @@
use super::NarInfo;
use core::fmt::{self, Write};
impl<'a> NarInfo<'a> {
/// Serializes the narinfo struct into the text format
/// ```
/// use narinfo::NarInfo;
///
/// let info = NarInfo::parse(include_str!("../../sample.narinfo")).unwrap();
/// let mut serialized = String::new();
/// info.serialize_into(&mut serialized).unwrap();
/// ```
pub fn serialize_into<T: Write>(&self, w: &mut T) -> fmt::Result {
write!(w, "StorePath: {}", self.store_path)?;
write!(w, "\nURL: {}", self.url)?;
write!(w, "\nNarHash: {}", self.nar_hash)?;
write!(w, "\nNarSize: {}", self.nar_size)?;
write!(w, "\nReferences: {}", self.references.join(" "))?;
write!(w, "\nSig: ")?;
for sig in self.sigs.iter() {
sig.serialize_into(w)?;
}
if let Some(compression) = &self.compression {
write!(w, "\nCompression: {}", compression)?;
};
if let Some(file_hash) = self.file_hash {
write!(w, "\nFileHash: {}", file_hash)?;
};
if let Some(file_size) = self.file_size {
write!(w, "\nFileSize: {}", file_size)?;
};
if let Some(deriver) = &self.deriver {
write!(w, "\nDeriver: {}", deriver)?;
};
Ok(())
}
}
#[cfg(test)]
mod tests {
use alloc::string::String;
use super::*;
#[test]
fn serialize_deserialize_eq() {
let sample = include_str!("../../sample.narinfo");
let info = NarInfo::parse(sample).unwrap();
let mut serialized = String::new();
info.serialize_into(&mut serialized).unwrap();
let info2 = NarInfo::parse(&serialized).unwrap();
assert_eq!(info, info2);
}
}

117
src/nix_cache_info.rs Normal file
View file

@ -0,0 +1,117 @@
use crate::error::{ParsingError, ParsingResult};
use alloc::borrow::Cow;
use core::fmt;
use core::fmt::Write;
use derive_builder::Builder;
/// Struct representing the nix-cache-info file fetched from a nix substituter, describing the
/// substituter
/// Based on the [unofficial spec](https://fzakaria.github.io/nix-http-binary-cache-api-spec)
/// and the [libstore narinfo parsing code.](https://github.com/NixOS/nix/blob/af4e8b00fb986acf32d7e4cd4fff7218b38958df/src/libstore/binary-cache-store.cc#L37)
#[derive(Builder, Eq, PartialEq, Debug)]
#[builder(no_std)]
#[builder(build_fn(error = "crate::error::ParsingError<'static>"))]
#[builder(setter(into))]
pub struct NixCacheInfo<'a> {
/// The path of the Nix store to which this binary cache applies. Binaries are not relocatable — a binary built for `/nix/store` wont generally work in `/home/alice/store` — so to prevent binaries from being used in a wrong store, a binary cache is only used if its StoreDir matches the local Nix configuration. The default path on nixos is `/nix/store`.
pub store_dir: Cow<'a, str>,
/// Query operations such as `nix-env -qas` can cause thousands of cache queries, and thus thousands of HTTP requests, to determine which packages are available in binary form. While these requests are small, not every server may appreciate a potential onslaught of queries. If WantMassQuery is set to 0 (default), “mass queries” such as `nix-env -qas` will skip this cache. Thus a package may appear not to have a binary substitute. However, the binary will still be used when you actually install the package. If WantMassQuery is set to 1, mass queries will use this cache.
#[builder(default)]
pub wants_mass_query: bool,
/// Each binary cache has a priority (defaulting to 50). Binary caches are checked for binaries in order of ascending priority; thus a higher number denotes a lower priority. The binary cache cache.nixos.org has priority 40.
#[builder(default = "50")]
pub priority: usize,
}
impl<'a> NixCacheInfo<'a> {
/// Parses the contents of a narinfo file from str.
/// This function is also used to implement the [TryFrom] trait for &'a str.
///
/// For duplicate keys the last value is used(same behaviour as libstore).
/// Unknown keys return an error(unlike libstore where they're simply ignored).
///
/// ```
/// # fn http_get_str(_url: &str) -> &'static str { include_str!("../nix-cache-info.sample") }
/// use narinfo::NixCacheInfo;
/// let data: &str =
/// http_get_str("https://cache.nixos.org/nix-cache-info");
///
/// let parsed = NixCacheInfo::parse(data).unwrap();
/// assert_eq!(parsed.store_dir, "/nix/store");
/// ```
pub fn parse(value: &'a str) -> ParsingResult<Self> {
let mut builder = NixCacheInfoBuilder::default();
for line in value.lines() {
let (key, value) = line
.split_once(':')
.map(|(k, v)| (k, v.trim()))
.ok_or(ParsingError::InvalidLine { line })?;
match key {
"StoreDir" => builder.store_dir(value),
"WantMassQuery" => {
let wants_mass_query: usize = ParsingError::try_parse_int(value, line)?;
builder.wants_mass_query(wants_mass_query >= 1)
}
"Priority" => builder.priority(ParsingError::try_parse_int(value, line)?),
_ => return Err(ParsingError::UnknownKey { key }),
};
}
builder.build()
}
/// Serializes the narinfo struct into the text format
/// ```
/// use narinfo::NixCacheInfo;
///
/// let info = NixCacheInfo::parse(include_str!("../nix-cache-info.sample")).unwrap();
/// let mut serialized = String::new();
/// info.serialize_into(&mut serialized).unwrap();
/// ```
pub fn serialize_into<T: Write>(&self, w: &mut T) -> fmt::Result {
write!(w, "StoreDir: {}", self.store_dir)?;
write!(w, "\nPriority: {}", self.priority)?;
let mass_query_int = if self.wants_mass_query { 1 } else { 0 };
write!(w, "\nWantMassQuery: {}", mass_query_int)?;
Ok(())
}
/// Get the builder for this struct.
/// The builder is generated by [derive_builder](https://lib.rs/crates/derive_builder)
// The builder is is used internally by the parsing code so we might as well expose it.
// If we ever decide to not use derive_builder we can simply hide it behind an optional feature flag
pub fn builder() -> NixCacheInfoBuilder<'a> {
NixCacheInfoBuilder::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::String;
static SAMPLE_NIX_CACHE_INFO: &str = include_str!("../nix-cache-info.sample");
#[test]
fn parses_sample_nix_cache_info() {
let info = NixCacheInfo::parse(SAMPLE_NIX_CACHE_INFO).unwrap();
assert_eq!(info.store_dir, "/nix/store");
assert_eq!(info.priority, 40);
assert!(info.wants_mass_query);
}
#[test]
fn serializes_then_deserializes_into_same_result() {
let info = NixCacheInfoBuilder::default()
.store_dir("/home/alice/nix")
.wants_mass_query(true)
.priority(69_usize)
.build()
.unwrap();
let mut serialized = String::new();
info.serialize_into(&mut serialized).unwrap();
let deserialized = NixCacheInfo::parse(&serialized).unwrap();
assert_eq!(deserialized.store_dir, "/home/alice/nix");
assert_eq!(deserialized.priority, 69);
assert!(deserialized.wants_mass_query);
}
}

55
src/sig.rs Normal file
View file

@ -0,0 +1,55 @@
use alloc::borrow::Cow;
use crate::error::{ParsingError, ParsingResult};
use core::fmt::{self, Write};
/// A signature of a nix narinfo file. Computed over the StorePath, NarHash, NarSize and References fields using the Ed25519 public-key signature system.
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct Sig<'a> {
/// The name of the key, eg `cache.example.org-1` for cache.nixos.org
pub key_name: Cow<'a, str>,
/// The actual signature
pub sig: Cow<'a, str>,
}
impl<'a> TryFrom<&'a str> for Sig<'a> {
type Error = ParsingError<'a>;
fn try_from(value: &'a str) -> ParsingResult<Self> {
Sig::parse(value)
}
}
// Neither the parse nor the serializa method is public since
// it doesn't really make sense to de/serialize the
// sig into the narinfo format outside of de/serializing a whole narinfo
impl<'a> Sig<'a> {
pub(crate) fn parse(value: &'a str) -> ParsingResult<Self> {
match value.split_once(':') {
Some((key, sig)) => Ok(Sig {
key_name: key.into(),
sig: sig.into(),
}),
None => Err(ParsingError::InvalidSignature(value)),
}
}
pub(crate) fn serialize_into<T: Write>(&self, w: &mut T) -> fmt::Result {
write!(w, "{}:{}", self.key_name, self.sig)
}
}
#[cfg(test)]
mod tests {
use alloc::string::String;
use super::*;
const SAMPLE_SIG: &str = "cache.nixos.org-1:BJ5QGcOta2s76XC6sep9DbAv0x3TILh3hHSKyR+9rFWYuBDTWdHs1KHeUEpw2espE/zPPBp2yURO6/J4Dhf9DQ==";
#[test]
fn serialize_deserialize_eq() {
let sig = Sig::parse(SAMPLE_SIG).unwrap();
let mut serialized = String::new();
sig.serialize_into(&mut serialized).unwrap();
assert_eq!(serialized, SAMPLE_SIG);
}
}