//! NAR info files describe a store path in a traditional Nix binary cache. //! Over the wire, they are formatted as "Key: value" pairs separated by newlines. //! //! It contains four kinds of information: //! 1. the description of the store path itself //! * store path prefix, digest, and name //! * NAR hash and size //! * references //! 2. authenticity information //! * zero or more signatures over that description //! * an optional [CAHash] for content-addressed paths (fixed outputs, sources, and derivations) //! 3. derivation metadata //! * deriver (the derivation that produced this path) //! * system (the system value of that derivation) //! 4. cache-specific information //! * URL of the compressed NAR, relative to the NAR info file //! * compression algorithm used for the NAR //! * hash and size of the compressed NAR use data_encoding::BASE64; use std::{ fmt::{self, Display}, mem, }; use crate::{ nixbase32, nixhash::{CAHash, NixHash}, store_path::StorePathRef, }; #[derive(Debug)] pub struct NarInfo<'a> { // core (authenticated, but unverified here) /// Store path described by this [NarInfo] pub store_path: StorePathRef<'a>, /// SHA-256 digest of the NAR file pub nar_hash: [u8; 32], /// Size of the NAR file in bytes pub nar_size: u64, /// Store paths known to be referenced by the contents pub references: Vec>, // authenticity /// Ed25519 signature over the path fingerprint pub signatures: Vec>, /// Content address (for content-defined paths) pub ca: Option, // derivation metadata /// Nix system triple of [deriver] pub system: Option<&'a str>, /// Store path of the derivation that produced this pub deriver: Option>, // cache-specific untrusted metadata /// Relative URL of the compressed NAR file pub url: &'a str, /// Compression method of the NAR file /// TODO(edef): default this to bzip2, and have None mean "none" (uncompressed) pub compression: Option<&'a str>, /// SHA-256 digest of the file at `url` pub file_hash: Option<[u8; 32]>, /// Size of the file at `url` in bytes pub file_size: Option, } impl<'a> NarInfo<'a> { pub fn parse(input: &'a str) -> Option { let mut store_path = None; let mut url = None; let mut compression = None; let mut file_hash = None; let mut file_size = None; let mut nar_hash = None; let mut nar_size = None; let mut references = None; let mut system = None; let mut deriver = None; let mut signatures = vec![]; let mut ca = None; for line in input.lines() { let (tag, val) = line.split_once(':')?; let val = val.strip_prefix(' ')?; match tag { "StorePath" => { let val = val.strip_prefix("/nix/store/")?; let val = StorePathRef::from_bytes(val.as_bytes()).ok()?; if store_path.replace(val).is_some() { return None; } } "URL" => { if val.is_empty() { return None; } if url.replace(val).is_some() { return None; } } "Compression" => { if val.is_empty() { return None; } if compression.replace(val).is_some() { return None; } } "FileHash" => { let val = val.strip_prefix("sha256:")?; let val = nixbase32::decode_fixed::<32>(val).ok()?; if file_hash.replace(val).is_some() { return None; } } "FileSize" => { let val = val.parse::().ok()?; if file_size.replace(val).is_some() { return None; } } "NarHash" => { let val = val.strip_prefix("sha256:")?; let val = nixbase32::decode_fixed::<32>(val).ok()?; if nar_hash.replace(val).is_some() { return None; } } "NarSize" => { let val = val.parse::().ok()?; if nar_size.replace(val).is_some() { return None; } } "References" => { let val: Vec = if !val.is_empty() { let mut prev = ""; val.split(' ') .map(|s| { if mem::replace(&mut prev, s) < s { StorePathRef::from_bytes(s.as_bytes()).ok() } else { // references are out of order None } }) .collect::>()? } else { vec![] }; if references.replace(val).is_some() { return None; } } "System" => { if val.is_empty() { return None; } if system.replace(val).is_some() { return None; } } "Deriver" => { let val = StorePathRef::from_bytes(val.as_bytes()).ok()?; if !val.name().ends_with(".drv") { return None; } if deriver.replace(val).is_some() { return None; } } "Sig" => { let val = Signature::parse(val)?; signatures.push(val); } "CA" => { let val = parse_ca(val)?; if ca.replace(val).is_some() { return None; } } _ => { // unknown field, ignore } } } Some(NarInfo { store_path: store_path?, nar_hash: nar_hash?, nar_size: nar_size?, references: references?, signatures, ca, system, deriver, url: url?, compression, file_hash, file_size, }) } } impl Display for NarInfo<'_> { fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { writeln!(w, "StorePath: /nix/store/{}", self.store_path)?; writeln!(w, "URL: {}", self.url)?; if let Some(compression) = self.compression { writeln!(w, "Compression: {compression}")?; } if let Some(file_hash) = self.file_hash { writeln!(w, "FileHash: {}", fmt_hash(&NixHash::Sha256(file_hash)))?; } if let Some(file_size) = self.file_size { writeln!(w, "FileSize: {file_size}")?; } writeln!(w, "NarHash: {}", fmt_hash(&NixHash::Sha256(self.nar_hash)))?; writeln!(w, "NarSize: {}", self.nar_size)?; write!(w, "References:")?; if self.references.is_empty() { write!(w, " ")?; } else { for path in &self.references { write!(w, " {path}")?; } } writeln!(w)?; if let Some(deriver) = &self.deriver { writeln!(w, "Deriver: {deriver}")?; } if let Some(system) = self.system { writeln!(w, "System: {system}")?; } for sig in &self.signatures { writeln!(w, "Sig: {sig}")?; } if let Some(ca) = &self.ca { writeln!(w, "CA: {}", fmt_ca(ca))?; } Ok(()) } } #[derive(Debug)] pub struct Signature<'a> { name: &'a str, bytes: [u8; 64], } impl<'a> Signature<'a> { pub fn parse(input: &'a str) -> Option> { let (name, bytes64) = input.split_once(':')?; let mut buf = [0; 66]; let mut bytes = [0; 64]; match BASE64.decode_mut(bytes64.as_bytes(), &mut buf) { Ok(64) => { bytes.copy_from_slice(&buf[..64]); } _ => { return None; } } Some(Signature { name, bytes }) } pub fn name(&self) -> &'a str { self.name } pub fn bytes(&self) -> &[u8; 64] { &self.bytes } } impl Display for Signature<'_> { fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { write!(w, "{}:{}", self.name, BASE64.encode(&self.bytes)) } } pub fn parse_ca(s: &str) -> Option { let (tag, s) = s.split_once(':')?; match tag { "text" => { let digest = s.strip_prefix("sha256:")?; let digest = nixbase32::decode_fixed(digest).ok()?; Some(CAHash::Text(digest)) } "fixed" => { if let Some(digest) = s.strip_prefix("r:sha256:") { let digest = nixbase32::decode_fixed(digest).ok()?; Some(CAHash::Nar(NixHash::Sha256(digest))) } else { parse_hash(s).map(CAHash::Flat) } } _ => None, } } #[allow(non_camel_case_types)] struct fmt_ca<'a>(&'a CAHash); impl Display for fmt_ca<'_> { fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { match self.0 { CAHash::Flat(h) => { write!(w, "fixed:{}", fmt_hash(h)) } &CAHash::Text(d) => { write!(w, "text:{}", fmt_hash(&NixHash::Sha256(d))) } CAHash::Nar(h) => { write!(w, "fixed:r:{}", fmt_hash(h)) } } } } fn parse_hash(s: &str) -> Option { let (tag, digest) = s.split_once(':')?; (match tag { "md5" => nixbase32::decode_fixed(digest).map(NixHash::Md5), "sha1" => nixbase32::decode_fixed(digest).map(NixHash::Sha1), "sha256" => nixbase32::decode_fixed(digest).map(NixHash::Sha256), "sha512" => nixbase32::decode_fixed(digest) .map(Box::new) .map(NixHash::Sha512), _ => return None, }) .ok() } #[allow(non_camel_case_types)] struct fmt_hash<'a>(&'a NixHash); impl Display for fmt_hash<'_> { fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { let (tag, digest) = match self.0 { NixHash::Md5(d) => ("md5", &d[..]), NixHash::Sha1(d) => ("sha1", &d[..]), NixHash::Sha256(d) => ("sha256", &d[..]), NixHash::Sha512(d) => ("sha512", &d[..]), }; write!(w, "{tag}:{}", nixbase32::encode(digest)) } } #[cfg(test)] mod test { use lazy_static::lazy_static; use pretty_assertions::assert_eq; use std::{io, str}; use super::NarInfo; lazy_static! { static ref CASES: &'static [&'static str] = { let data = zstd::decode_all(io::Cursor::new(include_bytes!("../testdata/narinfo.zst"))) .unwrap(); let data = str::from_utf8(Vec::leak(data)).unwrap(); Vec::leak( data.split_inclusive("\n\n") .map(|s| s.strip_suffix('\n').unwrap()) .collect::>(), ) }; } #[test] fn roundtrip() { for &input in *CASES { let parsed = NarInfo::parse(input).expect("should parse"); let output = format!("{parsed}"); assert_eq!(input, output, "should roundtrip"); } } }