diff options
Diffstat (limited to 'tvix/nix-compat')
44 files changed, 2618 insertions, 399 deletions
diff --git a/tvix/nix-compat/Cargo.toml b/tvix/nix-compat/Cargo.toml index 876ac3ecadd0..58137e4de2e1 100644 --- a/tvix/nix-compat/Cargo.toml +++ b/tvix/nix-compat/Cargo.toml @@ -4,48 +4,51 @@ version = "0.1.0" edition = "2021" [features] -# async NAR writer +# async NAR writer. Also needs the `wire` feature. async = ["tokio"] # code emitting low-level packets used in the daemon protocol. -wire = ["tokio", "pin-project-lite"] +wire = ["tokio", "pin-project-lite", "bytes"] +test = [] # Enable all features by default. -default = ["async", "wire"] +default = ["async", "wire", "nix-compat-derive"] [dependencies] -bitflags = "2.4.1" -bstr = { version = "1.6.0", features = ["alloc", "unicode", "serde"] } -data-encoding = "2.3.3" -ed25519 = "2.2.3" -ed25519-dalek = "2.1.0" -enum-primitive-derive = "0.3.0" -glob = "0.3.0" -nom = "7.1.3" -num-traits = "0.2.18" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -sha2 = "0.10.6" -thiserror = "1.0.38" - -[dependencies.tokio] -optional = true -version = "1.32.0" -features = ["io-util", "macros"] - -[dependencies.pin-project-lite] +bitflags = { workspace = true } +bstr = { workspace = true, features = ["alloc", "unicode", "serde"] } +data-encoding = { workspace = true } +ed25519 = { workspace = true } +ed25519-dalek = { workspace = true } +enum-primitive-derive = { workspace = true } +glob = { workspace = true } +mimalloc = { workspace = true } +nom = { workspace = true } +num-traits = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +sha2 = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +bytes = { workspace = true, optional = true } +tokio = { workspace = true, features = ["io-util", "macros"], optional = true } +pin-project-lite = { workspace = true, optional = true } + +[dependencies.nix-compat-derive] +path = "../nix-compat-derive" optional = true -version = "0.2.13" [dev-dependencies] -criterion = { version = "0.5", features = ["html_reports"] } -futures = { version = "0.3.30", default-features = false, features = ["executor"] } -hex-literal = "0.4.1" -lazy_static = "1.4.0" -pretty_assertions = "1.4.0" -rstest = "0.19.0" -serde_json = "1.0" -tokio-test = "0.4.3" -zstd = "^0.13.0" +criterion = { workspace = true, features = ["html_reports"] } +futures = { workspace = true } +hex-literal = { workspace = true } +lazy_static = { workspace = true } +mimalloc = { workspace = true } +pretty_assertions = { workspace = true } +rstest = { workspace = true } +serde_json = { workspace = true } +smol_str = { workspace = true } +tokio-test = { workspace = true } +zstd = { workspace = true } [[bench]] name = "derivation_parse_aterm" diff --git a/tvix/nix-compat/benches/derivation_parse_aterm.rs b/tvix/nix-compat/benches/derivation_parse_aterm.rs index 4ace7d4480f4..6557dd17af37 100644 --- a/tvix/nix-compat/benches/derivation_parse_aterm.rs +++ b/tvix/nix-compat/benches/derivation_parse_aterm.rs @@ -1,8 +1,12 @@ use std::path::Path; use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use mimalloc::MiMalloc; use nix_compat::derivation::Derivation; +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + const RESOURCES_PATHS: &str = "src/derivation/tests/derivation_tests/ok"; fn bench_aterm_parser(c: &mut Criterion) { diff --git a/tvix/nix-compat/benches/narinfo_parse.rs b/tvix/nix-compat/benches/narinfo_parse.rs index 7ffd24d12bc3..f35ba8468a88 100644 --- a/tvix/nix-compat/benches/narinfo_parse.rs +++ b/tvix/nix-compat/benches/narinfo_parse.rs @@ -1,8 +1,12 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion, Throughput}; use lazy_static::lazy_static; +use mimalloc::MiMalloc; use nix_compat::narinfo::NarInfo; use std::{io, str}; +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + const SAMPLE: &str = r#"StorePath: /nix/store/1pajsq519irjy86vli20bgq1wr1q3pny-banking-0.3.0 URL: nar/0rdn027rxqbl42bv9jxhsipgq2hwqdapvwmdzligmzdmz2p9vybs.nar.xz Compression: xz diff --git a/tvix/nix-compat/build.rs b/tvix/nix-compat/build.rs new file mode 100644 index 000000000000..c66b97016245 --- /dev/null +++ b/tvix/nix-compat/build.rs @@ -0,0 +1,5 @@ +fn main() { + // Pick up new test case files + // https://github.com/la10736/rstest/issues/256 + println!("cargo:rerun-if-changed=src/derivation/tests/derivation_tests") +} diff --git a/tvix/nix-compat/default.nix b/tvix/nix-compat/default.nix index 9df76e12fce1..34938e3d6428 100644 --- a/tvix/nix-compat/default.nix +++ b/tvix/nix-compat/default.nix @@ -1,7 +1,11 @@ -{ depot, ... }: +{ depot, lib, ... }: -depot.tvix.crates.workspaceMembers.nix-compat.build.override { +(depot.tvix.crates.workspaceMembers.nix-compat.build.override { runTests = true; - # make sure we also enable async here, so run the tests behind that feature flag. - features = [ "default" "async" "wire" ]; -} +}).overrideAttrs (old: rec { + meta.ci.targets = lib.filter (x: lib.hasPrefix "with-features" x || x == "no-features") (lib.attrNames passthru); + passthru = old.passthru // (depot.tvix.utils.mkFeaturePowerset { + inherit (old) crateName; + features = [ "async" "wire" ]; + }); +}) diff --git a/tvix/nix-compat/src/aterm/mod.rs b/tvix/nix-compat/src/aterm/mod.rs index 8806b6caf2e5..bb3b77bc7399 100644 --- a/tvix/nix-compat/src/aterm/mod.rs +++ b/tvix/nix-compat/src/aterm/mod.rs @@ -2,6 +2,6 @@ mod escape; mod parser; pub(crate) use escape::escape_bytes; -pub(crate) use parser::parse_bstr_field; -pub(crate) use parser::parse_str_list; +pub(crate) use parser::parse_bytes_field; pub(crate) use parser::parse_string_field; +pub(crate) use parser::parse_string_list; diff --git a/tvix/nix-compat/src/aterm/parser.rs b/tvix/nix-compat/src/aterm/parser.rs index a30cb40ab08d..a570573a8700 100644 --- a/tvix/nix-compat/src/aterm/parser.rs +++ b/tvix/nix-compat/src/aterm/parser.rs @@ -11,8 +11,10 @@ use nom::multi::separated_list0; use nom::sequence::delimited; use nom::IResult; -/// Parse a bstr and undo any escaping. -fn parse_escaped_bstr(i: &[u8]) -> IResult<&[u8], BString> { +/// Parse a bstr and undo any escaping (which is why this needs to allocate). +// FUTUREWORK: have a version for fields that are known to not need escaping +// (like store paths), and use &str. +fn parse_escaped_bytes(i: &[u8]) -> IResult<&[u8], BString> { escaped_transform( is_not("\"\\"), '\\', @@ -29,14 +31,14 @@ fn parse_escaped_bstr(i: &[u8]) -> IResult<&[u8], BString> { /// Parse a field in double quotes, undo any escaping, and return the unquoted /// and decoded `Vec<u8>`. -pub(crate) fn parse_bstr_field(i: &[u8]) -> IResult<&[u8], BString> { +pub(crate) fn parse_bytes_field(i: &[u8]) -> IResult<&[u8], BString> { // inside double quotes… delimited( nomchar('\"'), // There is alt(( // …either is a bstr after unescaping - parse_escaped_bstr, + parse_escaped_bytes, // …or an empty string. map(tag(b""), |_| BString::default()), )), @@ -45,8 +47,8 @@ pub(crate) fn parse_bstr_field(i: &[u8]) -> IResult<&[u8], BString> { } /// Parse a field in double quotes, undo any escaping, and return the unquoted -/// and decoded string, if it's a valid string. Or fail parsing if the bytes are -/// no valid UTF-8. +/// and decoded [String], if it's valid UTF-8. +/// Or fail parsing if the bytes are no valid UTF-8. pub(crate) fn parse_string_field(i: &[u8]) -> IResult<&[u8], String> { // inside double quotes… delimited( @@ -54,18 +56,18 @@ pub(crate) fn parse_string_field(i: &[u8]) -> IResult<&[u8], String> { // There is alt(( // either is a String after unescaping - nom::combinator::map_opt(parse_escaped_bstr, |escaped_bstr| { - String::from_utf8(escaped_bstr.into()).ok() + nom::combinator::map_opt(parse_escaped_bytes, |escaped_bytes| { + String::from_utf8(escaped_bytes.into()).ok() }), // or an empty string. - map(tag(b""), |_| String::new()), + map(tag(b""), |_| "".to_string()), )), nomchar('\"'), )(i) } -/// Parse a list of of string fields (enclosed in brackets) -pub(crate) fn parse_str_list(i: &[u8]) -> IResult<&[u8], Vec<String>> { +/// Parse a list of string fields (enclosed in brackets) +pub(crate) fn parse_string_list(i: &[u8]) -> IResult<&[u8], Vec<String>> { // inside brackets delimited( nomchar('['), @@ -89,7 +91,7 @@ mod tests { #[case] expected: &[u8], #[case] exp_rest: &[u8], ) { - let (rest, parsed) = super::parse_bstr_field(input).expect("must parse"); + let (rest, parsed) = super::parse_bytes_field(input).expect("must parse"); assert_eq!(exp_rest, rest, "expected remainder"); assert_eq!(expected, parsed); } @@ -118,7 +120,7 @@ mod tests { #[case::empty_list(b"[]", vec![], b"")] #[case::empty_list_with_rest(b"[]blub", vec![], b"blub")] fn parse_list(#[case] input: &[u8], #[case] expected: Vec<String>, #[case] exp_rest: &[u8]) { - let (rest, parsed) = super::parse_str_list(input).expect("must parse"); + let (rest, parsed) = super::parse_string_list(input).expect("must parse"); assert_eq!(exp_rest, rest, "expected remainder"); assert_eq!(expected, parsed); } diff --git a/tvix/nix-compat/src/bin/drvfmt.rs b/tvix/nix-compat/src/bin/drvfmt.rs index ddc1f0389f26..fca22c2cb2ae 100644 --- a/tvix/nix-compat/src/bin/drvfmt.rs +++ b/tvix/nix-compat/src/bin/drvfmt.rs @@ -3,6 +3,11 @@ use std::{collections::BTreeMap, io::Read}; use nix_compat::derivation::Derivation; use serde_json::json; +use mimalloc::MiMalloc; + +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + /// construct a serde_json::Value from a Derivation. /// Some environment values can be non-valid UTF-8 strings. /// `serde_json` prints them out really unreadable. diff --git a/tvix/nix-compat/src/derivation/mod.rs b/tvix/nix-compat/src/derivation/mod.rs index 6e12e3ea86e6..6baeaba38299 100644 --- a/tvix/nix-compat/src/derivation/mod.rs +++ b/tvix/nix-compat/src/derivation/mod.rs @@ -36,11 +36,11 @@ pub struct Derivation { /// Map from drv path to output names used from this derivation. #[serde(rename = "inputDrvs")] - pub input_derivations: BTreeMap<StorePath, BTreeSet<String>>, + pub input_derivations: BTreeMap<StorePath<String>, BTreeSet<String>>, /// Plain store paths of additional inputs. #[serde(rename = "inputSrcs")] - pub input_sources: BTreeSet<StorePath>, + pub input_sources: BTreeSet<StorePath<String>>, /// Maps output names to Output. pub outputs: BTreeMap<String, Output>, @@ -127,7 +127,10 @@ impl Derivation { /// the `name` with a `.drv` suffix as name, all [Derivation::input_sources] and /// keys of [Derivation::input_derivations] as references, and the ATerm string of /// the [Derivation] as content. - pub fn calculate_derivation_path(&self, name: &str) -> Result<StorePath, DerivationError> { + pub fn calculate_derivation_path( + &self, + name: &str, + ) -> Result<StorePath<String>, DerivationError> { // append .drv to the name let name = &format!("{}.drv", name); @@ -141,7 +144,6 @@ impl Derivation { .collect(); build_text_path(name, self.to_aterm_bytes(), references) - .map(|s| s.to_owned()) .map_err(|_e| DerivationError::InvalidOutputName(name.to_string())) } @@ -210,7 +212,7 @@ impl Derivation { self.input_derivations .iter() .map(|(drv_path, output_names)| { - let hash = fn_lookup_hash_derivation_modulo(&drv_path.into()); + let hash = fn_lookup_hash_derivation_modulo(&drv_path.as_ref()); (hash, output_names.to_owned()) }), diff --git a/tvix/nix-compat/src/derivation/output.rs b/tvix/nix-compat/src/derivation/output.rs index 266617f587f8..0b81ef3c3155 100644 --- a/tvix/nix-compat/src/derivation/output.rs +++ b/tvix/nix-compat/src/derivation/output.rs @@ -1,5 +1,4 @@ use crate::nixhash::CAHash; -use crate::store_path::StorePathRef; use crate::{derivation::OutputError, store_path::StorePath}; use serde::de::Unexpected; use serde::{Deserialize, Serialize}; @@ -10,7 +9,7 @@ use std::borrow::Cow; #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] pub struct Output { /// Store path of build result. - pub path: Option<StorePath>, + pub path: Option<StorePath<String>>, #[serde(flatten)] pub ca_hash: Option<CAHash>, // we can only represent a subset here. @@ -33,10 +32,10 @@ impl<'de> Deserialize<'de> for Output { &"a string", ))?; - let path = StorePathRef::from_absolute_path(path.as_bytes()) + let path = StorePath::from_absolute_path(path.as_bytes()) .map_err(|_| serde::de::Error::invalid_value(Unexpected::Str(path), &"StorePath"))?; Ok(Self { - path: Some(path.to_owned()), + path: Some(path), ca_hash: CAHash::from_map::<D>(&fields)?, }) } diff --git a/tvix/nix-compat/src/derivation/parse_error.rs b/tvix/nix-compat/src/derivation/parse_error.rs index fc97f1a9883b..f625d9aeb724 100644 --- a/tvix/nix-compat/src/derivation/parse_error.rs +++ b/tvix/nix-compat/src/derivation/parse_error.rs @@ -20,7 +20,7 @@ pub enum ErrorKind { DuplicateInputDerivationOutputName(String, String), #[error("duplicate input source: {0}")] - DuplicateInputSource(StorePath), + DuplicateInputSource(StorePath<String>), #[error("nix hash error: {0}")] NixHashError(nixhash::Error), diff --git a/tvix/nix-compat/src/derivation/parser.rs b/tvix/nix-compat/src/derivation/parser.rs index 2775294960fe..4fff7181ba40 100644 --- a/tvix/nix-compat/src/derivation/parser.rs +++ b/tvix/nix-compat/src/derivation/parser.rs @@ -3,7 +3,6 @@ //! //! [ATerm]: http://program-transformation.org/Tools/ATermFormat.html -use bstr::BString; use nom::bytes::complete::tag; use nom::character::complete::char as nomchar; use nom::combinator::{all_consuming, map_res}; @@ -14,7 +13,7 @@ use thiserror; use crate::derivation::parse_error::{into_nomerror, ErrorKind, NomError, NomResult}; use crate::derivation::{write, CAHash, Derivation, Output}; -use crate::store_path::{self, StorePath, StorePathRef}; +use crate::store_path::{self, StorePath}; use crate::{aterm, nixhash}; #[derive(Debug, thiserror::Error)] @@ -73,7 +72,7 @@ fn parse_output(i: &[u8]) -> NomResult<&[u8], (String, Output)> { terminated(aterm::parse_string_field, nomchar(',')), terminated(aterm::parse_string_field, nomchar(',')), terminated(aterm::parse_string_field, nomchar(',')), - aterm::parse_bstr_field, + aterm::parse_bytes_field, ))(i) .map_err(into_nomerror) }, @@ -102,7 +101,7 @@ fn parse_output(i: &[u8]) -> NomResult<&[u8], (String, Output)> { path: if output_path.is_empty() { None } else { - Some(string_to_store_path(i, output_path)?) + Some(string_to_store_path(i, &output_path)?) }, ca_hash: hash_with_mode, }, @@ -132,12 +131,12 @@ fn parse_outputs(i: &[u8]) -> NomResult<&[u8], BTreeMap<String, Output>> { match res { Ok((rst, outputs_lst)) => { - let mut outputs: BTreeMap<String, Output> = BTreeMap::default(); + let mut outputs = BTreeMap::default(); for (output_name, output) in outputs_lst.into_iter() { if outputs.contains_key(&output_name) { return Err(nom::Err::Failure(NomError { input: i, - code: ErrorKind::DuplicateMapKey(output_name), + code: ErrorKind::DuplicateMapKey(output_name.to_string()), })); } outputs.insert(output_name, output); @@ -149,11 +148,13 @@ fn parse_outputs(i: &[u8]) -> NomResult<&[u8], BTreeMap<String, Output>> { } } -fn parse_input_derivations(i: &[u8]) -> NomResult<&[u8], BTreeMap<StorePath, BTreeSet<String>>> { - let (i, input_derivations_list) = parse_kv::<Vec<String>, _>(aterm::parse_str_list)(i)?; +fn parse_input_derivations( + i: &[u8], +) -> NomResult<&[u8], BTreeMap<StorePath<String>, BTreeSet<String>>> { + let (i, input_derivations_list) = parse_kv(aterm::parse_string_list)(i)?; // This is a HashMap of drv paths to a list of output names. - let mut input_derivations: BTreeMap<StorePath, BTreeSet<String>> = BTreeMap::new(); + let mut input_derivations: BTreeMap<StorePath<String>, BTreeSet<_>> = BTreeMap::new(); for (input_derivation, output_names) in input_derivations_list { let mut new_output_names = BTreeSet::new(); @@ -170,7 +171,7 @@ fn parse_input_derivations(i: &[u8]) -> NomResult<&[u8], BTreeMap<StorePath, BTr new_output_names.insert(output_name); } - let input_derivation: StorePath = string_to_store_path(i, input_derivation)?; + let input_derivation = string_to_store_path(i, input_derivation.as_str())?; input_derivations.insert(input_derivation, new_output_names); } @@ -178,16 +179,16 @@ fn parse_input_derivations(i: &[u8]) -> NomResult<&[u8], BTreeMap<StorePath, BTr Ok((i, input_derivations)) } -fn parse_input_sources(i: &[u8]) -> NomResult<&[u8], BTreeSet<StorePath>> { - let (i, input_sources_lst) = aterm::parse_str_list(i).map_err(into_nomerror)?; +fn parse_input_sources(i: &[u8]) -> NomResult<&[u8], BTreeSet<StorePath<String>>> { + let (i, input_sources_lst) = aterm::parse_string_list(i).map_err(into_nomerror)?; let mut input_sources: BTreeSet<_> = BTreeSet::new(); for input_source in input_sources_lst.into_iter() { - let input_source: StorePath = string_to_store_path(i, input_source)?; + let input_source = string_to_store_path(i, input_source.as_str())?; if input_sources.contains(&input_source) { return Err(nom::Err::Failure(NomError { input: i, - code: ErrorKind::DuplicateInputSource(input_source), + code: ErrorKind::DuplicateInputSource(input_source.to_owned()), })); } else { input_sources.insert(input_source); @@ -197,24 +198,27 @@ fn parse_input_sources(i: &[u8]) -> NomResult<&[u8], BTreeSet<StorePath>> { Ok((i, input_sources)) } -fn string_to_store_path( - i: &[u8], - path_str: String, -) -> Result<StorePath, nom::Err<NomError<&[u8]>>> { - #[cfg(debug_assertions)] - let path_str2 = path_str.clone(); - - let path: StorePath = StorePathRef::from_absolute_path(path_str.as_bytes()) - .map_err(|e: store_path::Error| { +fn string_to_store_path<'a, 'i, S>( + i: &'i [u8], + path_str: &'a str, +) -> Result<StorePath<S>, nom::Err<NomError<&'i [u8]>>> +where + S: std::cmp::Eq + + std::fmt::Display + + std::clone::Clone + + std::ops::Deref<Target = str> + + std::convert::From<&'a str>, +{ + let path = + StorePath::from_absolute_path(path_str.as_bytes()).map_err(|e: store_path::Error| { nom::Err::Failure(NomError { input: i, code: e.into(), }) - })? - .to_owned(); + })?; #[cfg(debug_assertions)] - assert_eq!(path_str2, path.to_absolute_path()); + assert_eq!(path_str, path.to_absolute_path()); Ok(path) } @@ -240,9 +244,9 @@ pub fn parse_derivation(i: &[u8]) -> NomResult<&[u8], Derivation> { // // parse builder |i| terminated(aterm::parse_string_field, nomchar(','))(i).map_err(into_nomerror), // // parse arguments - |i| terminated(aterm::parse_str_list, nomchar(','))(i).map_err(into_nomerror), + |i| terminated(aterm::parse_string_list, nomchar(','))(i).map_err(into_nomerror), // parse environment - parse_kv::<BString, _>(aterm::parse_bstr_field), + parse_kv(aterm::parse_bytes_field), )), nomchar(')'), ) @@ -376,11 +380,11 @@ mod tests { }; static ref EXP_AB_MAP: BTreeMap<String, BString> = { let mut b = BTreeMap::new(); - b.insert("a".to_string(), b"1".as_bstr().to_owned()); - b.insert("b".to_string(), b"2".as_bstr().to_owned()); + b.insert("a".to_string(), b"1".into()); + b.insert("b".to_string(), b"2".into()); b }; - static ref EXP_INPUT_DERIVATIONS_SIMPLE: BTreeMap<StorePath, BTreeSet<String>> = { + static ref EXP_INPUT_DERIVATIONS_SIMPLE: BTreeMap<StorePath<String>, BTreeSet<String>> = { let mut b = BTreeMap::new(); b.insert( StorePath::from_bytes(b"8bjm87p310sb7r2r0sg4xrynlvg86j8k-hello-2.12.1.tar.gz.drv") @@ -427,8 +431,8 @@ mod tests { #[case] expected: &BTreeMap<String, BString>, #[case] exp_rest: &[u8], ) { - let (rest, parsed) = super::parse_kv::<BString, _>(crate::aterm::parse_bstr_field)(input) - .expect("must parse"); + let (rest, parsed) = + super::parse_kv(crate::aterm::parse_bytes_field)(input).expect("must parse"); assert_eq!(exp_rest, rest, "expected remainder"); assert_eq!(*expected, parsed); } @@ -437,8 +441,7 @@ mod tests { #[test] fn parse_kv_fail_dup_keys() { let input: &'static [u8] = b"[(\"a\",\"1\"),(\"a\",\"2\")]"; - let e = super::parse_kv::<BString, _>(crate::aterm::parse_bstr_field)(input) - .expect_err("must fail"); + let e = super::parse_kv(crate::aterm::parse_bytes_field)(input).expect_err("must fail"); match e { nom::Err::Failure(e) => { @@ -454,7 +457,7 @@ mod tests { #[case::simple(EXP_INPUT_DERIVATIONS_SIMPLE_ATERM.as_bytes(), &EXP_INPUT_DERIVATIONS_SIMPLE)] fn parse_input_derivations( #[case] input: &'static [u8], - #[case] expected: &BTreeMap<StorePath, BTreeSet<String>>, + #[case] expected: &BTreeMap<StorePath<String>, BTreeSet<String>>, ) { let (rest, parsed) = super::parse_input_derivations(input).expect("must parse"); diff --git a/tvix/nix-compat/src/derivation/write.rs b/tvix/nix-compat/src/derivation/write.rs index 735b781574e1..42dadcd76064 100644 --- a/tvix/nix-compat/src/derivation/write.rs +++ b/tvix/nix-compat/src/derivation/write.rs @@ -6,7 +6,7 @@ use crate::aterm::escape_bytes; use crate::derivation::{ca_kind_prefix, output::Output}; use crate::nixbase32; -use crate::store_path::{StorePath, StorePathRef, STORE_DIR_WITH_SLASH}; +use crate::store_path::{StorePath, STORE_DIR_WITH_SLASH}; use bstr::BString; use data_encoding::HEXLOWER; @@ -32,16 +32,12 @@ pub const QUOTE: char = '"'; /// the context a lot. pub(crate) trait AtermWriteable { fn aterm_write(&self, writer: &mut impl Write) -> std::io::Result<()>; - - fn aterm_bytes(&self) -> Vec<u8> { - let mut bytes = Vec::new(); - self.aterm_write(&mut bytes) - .expect("unexpected write errors to Vec"); - bytes - } } -impl AtermWriteable for StorePathRef<'_> { +impl<S> AtermWriteable for StorePath<S> +where + S: std::cmp::Eq + std::ops::Deref<Target = str>, +{ fn aterm_write(&self, writer: &mut impl Write) -> std::io::Result<()> { write_char(writer, QUOTE)?; writer.write_all(STORE_DIR_WITH_SLASH.as_bytes())?; @@ -53,13 +49,6 @@ impl AtermWriteable for StorePathRef<'_> { } } -impl AtermWriteable for StorePath { - fn aterm_write(&self, writer: &mut impl Write) -> std::io::Result<()> { - let r: StorePathRef = self.into(); - r.aterm_write(writer) - } -} - impl AtermWriteable for String { fn aterm_write(&self, writer: &mut impl Write) -> std::io::Result<()> { write_field(writer, self, true) @@ -186,7 +175,7 @@ pub(crate) fn write_input_derivations( pub(crate) fn write_input_sources( writer: &mut impl Write, - input_sources: &BTreeSet<StorePath>, + input_sources: &BTreeSet<StorePath<String>>, ) -> Result<(), io::Error> { write_char(writer, BRACKET_OPEN)?; write_array_elements( diff --git a/tvix/nix-compat/src/lib.rs b/tvix/nix-compat/src/lib.rs index a71ede3eecf0..f30c557889a8 100644 --- a/tvix/nix-compat/src/lib.rs +++ b/tvix/nix-compat/src/lib.rs @@ -1,8 +1,12 @@ +extern crate self as nix_compat; + pub(crate) mod aterm; pub mod derivation; pub mod nar; pub mod narinfo; +pub mod nix_http; pub mod nixbase32; +pub mod nixcpp; pub mod nixhash; pub mod path_info; pub mod store_path; @@ -11,7 +15,7 @@ pub mod store_path; pub mod wire; #[cfg(feature = "wire")] -mod nix_daemon; +pub mod nix_daemon; #[cfg(feature = "wire")] pub use nix_daemon::worker_protocol; #[cfg(feature = "wire")] diff --git a/tvix/nix-compat/src/nar/listing/mod.rs b/tvix/nix-compat/src/nar/listing/mod.rs new file mode 100644 index 000000000000..5a9a3b4d3613 --- /dev/null +++ b/tvix/nix-compat/src/nar/listing/mod.rs @@ -0,0 +1,128 @@ +//! 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>, + }, +} diff --git a/tvix/nix-compat/src/nar/listing/test.rs b/tvix/nix-compat/src/nar/listing/test.rs new file mode 100644 index 000000000000..5b2ac3f166fe --- /dev/null +++ b/tvix/nix-compat/src/nar/listing/test.rs @@ -0,0 +1,59 @@ +use std::{collections::HashMap, path::PathBuf, str::FromStr}; + +use crate::nar; + +#[test] +fn weird_paths() { + let root = nar::listing::ListingEntry::Directory { + entries: HashMap::new(), + }; + + root.locate("../../../../etc/passwd") + .expect_err("Failed to reject `../` fragment in a path during traversal"); + + // Gated on Windows as C:\\ is parsed as `Component::Normal(_)` on Linux. + #[cfg(target_os = "windows")] + root.locate("C:\\\\Windows\\System32") + .expect_err("Failed to reject Windows-style prefixes"); + + root.locate("/etc/passwd") + .expect_err("Failed to reject absolute UNIX paths"); +} + +#[test] +fn nixos_release() { + let listing_bytes = include_bytes!("../tests/nixos-release.ls"); + let listing: nar::listing::Listing = serde_json::from_slice(listing_bytes).unwrap(); + + let nar::listing::Listing::V1 { root, .. } = listing; + assert!(matches!(root, nar::listing::ListingEntry::Directory { .. })); + + let build_products = root + .locate(PathBuf::from_str("nix-support/hydra-build-products").unwrap()) + .expect("Failed to locate a known file in a directory") + .expect("File was unexpectedly not found in the listing"); + + assert!(matches!( + build_products, + nar::listing::ListingEntry::Regular { .. } + )); + + let nonexisting_file = root + .locate(PathBuf::from_str("nix-support/does-not-exist").unwrap()) + .expect("Failed to locate an unknown file in a directory"); + + assert!( + nonexisting_file.is_none(), + "Non-existing file was unexpectedly found in the listing" + ); + + let existing_dir = root + .locate(PathBuf::from_str("nix-support").unwrap()) + .expect("Failed to locate a known directory in a directory") + .expect("Directory was expectedly found in the listing"); + + assert!(matches!( + existing_dir, + nar::listing::ListingEntry::Directory { .. } + )); +} diff --git a/tvix/nix-compat/src/nar/mod.rs b/tvix/nix-compat/src/nar/mod.rs index c678d26ffb38..d0e8ee8a412f 100644 --- a/tvix/nix-compat/src/nar/mod.rs +++ b/tvix/nix-compat/src/nar/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod wire; +pub mod listing; pub mod reader; pub mod writer; diff --git a/tvix/nix-compat/src/nar/reader/mod.rs b/tvix/nix-compat/src/nar/reader/mod.rs index 9e9237ead363..eef3b10f3c28 100644 --- a/tvix/nix-compat/src/nar/reader/mod.rs +++ b/tvix/nix-compat/src/nar/reader/mod.rs @@ -16,7 +16,7 @@ use std::marker::PhantomData; // Required reading for understanding this module. use crate::nar::wire; -#[cfg(feature = "async")] +#[cfg(all(feature = "async", feature = "wire"))] pub mod r#async; mod read; @@ -29,9 +29,11 @@ struct ArchiveReader<'a, 'r> { inner: &'a mut Reader<'r>, /// In debug mode, also track when we need to abandon this archive reader. + /// /// The archive reader must be abandoned when: /// * An error is encountered at any point /// * A file or directory reader is dropped before being read entirely. + /// /// All of these checks vanish in release mode. status: ArchiveReaderStatus<'a>, } diff --git a/tvix/nix-compat/src/nar/tests/nixos-release.ls b/tvix/nix-compat/src/nar/tests/nixos-release.ls new file mode 100644 index 000000000000..9dd350b7cf86 --- /dev/null +++ b/tvix/nix-compat/src/nar/tests/nixos-release.ls @@ -0,0 +1 @@ +{"root":{"entries":{"iso":{"entries":{"nixos-minimal-new-kernel-no-zfs-24.11pre660688.bee6b69aad74-x86_64-linux.iso":{"narOffset":440,"size":1051721728,"type":"regular"}},"type":"directory"},"nix-support":{"entries":{"hydra-build-products":{"narOffset":1051722544,"size":211,"type":"regular"},"system":{"narOffset":1051722944,"size":13,"type":"regular"}},"type":"directory"}},"type":"directory"},"version":1} \ No newline at end of file diff --git a/tvix/nix-compat/src/nar/wire/mod.rs b/tvix/nix-compat/src/nar/wire/mod.rs index 9e99b530ce15..ddf021bc1fa1 100644 --- a/tvix/nix-compat/src/nar/wire/mod.rs +++ b/tvix/nix-compat/src/nar/wire/mod.rs @@ -39,7 +39,7 @@ //! TOK_NAR ::= "nix-archive-1" "(" "type" //! TOK_SYM ::= "symlink" "target" //! TOK_REG ::= "regular" "contents" -//! TOK_EXE ::= "regular" "executable" "" +//! TOK_EXE ::= "regular" "executable" "" "contents" //! TOK_DIR ::= "directory" //! TOK_ENT ::= "entry" "(" "name" //! TOK_NOD ::= "node" "(" "type" @@ -97,7 +97,7 @@ const TOK_PAD_PAR: [u8; 24] = *b"\0\0\0\0\0\0\0\0\x01\0\0\0\0\0\0\0)\0\0\0\0\0\0 #[derive(Debug)] pub(crate) enum PadPar {} -#[cfg(feature = "async")] +#[cfg(all(feature = "async", feature = "wire"))] impl crate::wire::reader::Tag for PadPar { const PATTERN: &'static [u8] = &TOK_PAD_PAR; @@ -119,6 +119,8 @@ fn tokens() { (&TOK_ENT, &["entry", "(", "name"]), (&TOK_NOD, &["node", "(", "type"]), (&TOK_PAR, &[")"]), + #[cfg(feature = "async")] + (&TOK_PAD_PAR, &["", ")"]), ]; for &(tok, xs) in cases { diff --git a/tvix/nix-compat/src/nar/writer/sync.rs b/tvix/nix-compat/src/nar/writer/sync.rs index 6270129028fa..b441479ac60b 100644 --- a/tvix/nix-compat/src/nar/writer/sync.rs +++ b/tvix/nix-compat/src/nar/writer/sync.rs @@ -35,11 +35,8 @@ use std::io::{ Write, }; -/// Convenience type alias for types implementing [`Write`]. -pub type Writer<'a> = dyn Write + Send + 'a; - /// Create a new NAR, writing the output to the specified writer. -pub fn open<'a, 'w: 'a>(writer: &'a mut Writer<'w>) -> io::Result<Node<'a, 'w>> { +pub fn open<W: Write>(writer: &mut W) -> io::Result<Node<W>> { let mut node = Node { writer }; node.write(&wire::TOK_NAR)?; Ok(node) @@ -49,11 +46,11 @@ pub fn open<'a, 'w: 'a>(writer: &'a mut Writer<'w>) -> io::Result<Node<'a, 'w>> /// /// A NAR can be thought of as a tree of nodes represented by this type. Each /// node can be a file, a symlink or a directory containing other nodes. -pub struct Node<'a, 'w: 'a> { - writer: &'a mut Writer<'w>, +pub struct Node<'a, W: Write> { + writer: &'a mut W, } -impl<'a, 'w> Node<'a, 'w> { +impl<'a, W: Write> Node<'a, W> { fn write(&mut self, data: &[u8]) -> io::Result<()> { self.writer.write_all(data) } @@ -123,12 +120,59 @@ impl<'a, 'w> Node<'a, 'w> { Ok(()) } + /// Make this node a single file but let the user handle the writing of the file contents. + /// The user gets access to a writer to write the file contents to, plus a struct they must + /// invoke a function on to finish writing the NAR file. + /// + /// It is the caller's responsibility to write the correct number of bytes to the writer and + /// invoke [`FileManualWrite::close`], or invalid archives will be produced silently. + /// + /// ```rust + /// # use std::io::BufReader; + /// # use std::io::Write; + /// # + /// # // Output location to write the NAR to. + /// # let mut sink: Vec<u8> = Vec::new(); + /// # + /// # // Instantiate writer for this output location. + /// # let mut nar = nix_compat::nar::writer::open(&mut sink)?; + /// # + /// let contents = "Hello world\n".as_bytes(); + /// let size = contents.len() as u64; + /// let executable = false; + /// + /// let (writer, skip) = nar + /// .file_manual_write(executable, size)?; + /// + /// // Write the contents + /// writer.write_all(&contents)?; + /// + /// // Close the file node + /// skip.close(writer)?; + /// # Ok::<(), std::io::Error>(()) + /// ``` + pub fn file_manual_write( + mut self, + executable: bool, + size: u64, + ) -> io::Result<(&'a mut W, FileManualWrite)> { + self.write(if executable { + &wire::TOK_EXE + } else { + &wire::TOK_REG + })?; + + self.write(&size.to_le_bytes())?; + + Ok((self.writer, FileManualWrite { size })) + } + /// Make this node a directory, the content of which is set using the /// resulting [`Directory`] value. /// /// It is the caller's responsibility to invoke [`Directory::close`], /// or invalid archives will be produced silently. - pub fn directory(mut self) -> io::Result<Directory<'a, 'w>> { + pub fn directory(mut self) -> io::Result<Directory<'a, W>> { self.write(&wire::TOK_DIR)?; Ok(Directory::new(self)) } @@ -145,13 +189,13 @@ fn into_name(_name: &[u8]) -> Name { } /// Content of a NAR node that represents a directory. -pub struct Directory<'a, 'w> { - node: Node<'a, 'w>, +pub struct Directory<'a, W: Write> { + node: Node<'a, W>, prev_name: Option<Name>, } -impl<'a, 'w> Directory<'a, 'w> { - fn new(node: Node<'a, 'w>) -> Self { +impl<'a, W: Write> Directory<'a, W> { + fn new(node: Node<'a, W>) -> Self { Self { node, prev_name: None, @@ -166,7 +210,7 @@ impl<'a, 'w> Directory<'a, 'w> { /// It is the caller's responsibility to ensure that directory entries are /// written in order of ascending name. If this is not ensured, this method /// may panic or silently produce invalid archives. - pub fn entry(&mut self, name: &[u8]) -> io::Result<Node<'_, 'w>> { + pub fn entry(&mut self, name: &[u8]) -> io::Result<Node<'_, W>> { debug_assert!( name.len() <= wire::MAX_NAME_LEN, "name.len() > {}", @@ -222,3 +266,24 @@ impl<'a, 'w> Directory<'a, 'w> { Ok(()) } } + +/// Content of a NAR node that represents a file whose contents are being written out manually. +/// Returned by the `file_manual_write` function. +#[must_use] +pub struct FileManualWrite { + size: u64, +} + +impl FileManualWrite { + /// Finish writing the file structure to the NAR after having manually written the file contents. + /// + /// **Important:** This *must* be called with the writer returned by file_manual_write after + /// the file contents have been manually and fully written. Otherwise the resulting NAR file + /// will be invalid. + pub fn close<W: Write>(self, writer: &mut W) -> io::Result<()> { + let mut node = Node { writer }; + node.pad(self.size)?; + node.write(&wire::TOK_PAR)?; + Ok(()) + } +} diff --git a/tvix/nix-compat/src/narinfo/mod.rs b/tvix/nix-compat/src/narinfo/mod.rs index b1c10bceb200..21aecf80b5a2 100644 --- a/tvix/nix-compat/src/narinfo/mod.rs +++ b/tvix/nix-compat/src/narinfo/mod.rs @@ -27,13 +27,15 @@ use std::{ use crate::{nixbase32, nixhash::CAHash, store_path::StorePathRef}; mod fingerprint; -mod public_keys; mod signature; +mod signing_keys; +mod verifying_keys; pub use fingerprint::fingerprint; - -pub use public_keys::{Error as PubKeyError, PubKey}; -pub use signature::{Error as SignatureError, Signature}; +pub use signature::{Error as SignatureError, Signature, SignatureRef}; +pub use signing_keys::parse_keypair; +pub use signing_keys::{Error as SigningKeyError, SigningKey}; +pub use verifying_keys::{Error as VerifyingKeyError, VerifyingKey}; #[derive(Debug)] pub struct NarInfo<'a> { @@ -49,7 +51,7 @@ pub struct NarInfo<'a> { pub references: Vec<StorePathRef<'a>>, // authenticity /// Ed25519 signature over the path fingerprint - pub signatures: Vec<Signature<'a>>, + pub signatures: Vec<SignatureRef<'a>>, /// Content address (for content-defined paths) pub ca: Option<CAHash>, // derivation metadata @@ -244,7 +246,7 @@ impl<'a> NarInfo<'a> { }; } "Sig" => { - let val = Signature::parse(val) + let val = SignatureRef::parse(val) .map_err(|e| Error::UnableToParseSignature(signatures.len(), e))?; signatures.push(val); @@ -297,6 +299,21 @@ impl<'a> NarInfo<'a> { self.references.iter(), ) } + + /// Adds a signature, using the passed signer to sign. + /// This is generic over algo implementations / providers, + /// so users can bring their own signers. + pub fn add_signature<S>(&mut self, signer: &'a SigningKey<S>) + where + S: ed25519::signature::Signer<ed25519::Signature>, + { + // calculate the fingerprint to sign + let fp = self.fingerprint(); + + let sig = signer.sign(fp.as_bytes()); + + self.signatures.push(sig); + } } impl Display for NarInfo<'_> { @@ -392,6 +409,12 @@ pub enum Error { } #[cfg(test)] +const DUMMY_KEYPAIR: &str = "cache.example.com-1:cCta2MEsRNuYCgWYyeRXLyfoFpKhQJKn8gLMeXWAb7vIpRKKo/3JoxJ24OYa3DxT2JVV38KjK/1ywHWuMe2JEw=="; +#[cfg(test)] +const DUMMY_VERIFYING_KEY: &str = + "cache.example.com-1:yKUSiqP9yaMSduDmGtw8U9iVVd/Coyv9csB1rjHtiRM="; + +#[cfg(test)] mod test { use hex_literal::hex; use lazy_static::lazy_static; @@ -524,4 +547,46 @@ Sig: cache.nixos.org-1:HhaiY36Uk3XV1JGe9d9xHnzAapqJXprU1YZZzSzxE97jCuO5RR7vlG2kF parsed.nar_hash, ); } + + /// Adds a signature to a NARInfo, using key material parsed from DUMMY_KEYPAIR. + /// It then ensures signature verification with the parsed + /// DUMMY_VERIFYING_KEY succeeds. + #[test] + fn sign() { + let mut narinfo = NarInfo::parse( + r#"StorePath: /nix/store/0vpqfxbkx0ffrnhbws6g9qwhmliksz7f-perl-HTTP-Cookies-6.01 +URL: nar/0i5biw0g01514llhfswxy6xfav8lxxdq1xg6ik7hgsqbpw0f06yi.nar.xz +Compression: xz +FileHash: sha256:0i5biw0g01514llhfswxy6xfav8lxxdq1xg6ik7hgsqbpw0f06yi +FileSize: 7120 +NarHash: sha256:0h1bm4sj1cnfkxgyhvgi8df1qavnnv94sd0v09wcrm971602shfg +NarSize: 22552 +References: +CA: fixed:r:sha1:1ak1ymbmsfx7z8kh09jzkr3a4dvkrfjw +"#, + ) + .expect("should parse"); + + let fp = narinfo.fingerprint(); + + // load our keypair from the fixtures + let (signing_key, _verifying_key) = + super::parse_keypair(super::DUMMY_KEYPAIR).expect("must succeed"); + + // add signature + narinfo.add_signature(&signing_key); + + // ensure the signature is added + let new_sig = narinfo.signatures.last().unwrap(); + assert_eq!(signing_key.name(), *new_sig.name()); + + // verify the new signature against the verifying key + let verifying_key = super::VerifyingKey::parse(super::DUMMY_VERIFYING_KEY) + .expect("parsing dummy verifying key"); + + assert!( + verifying_key.verify(&fp, new_sig), + "expect signature to be valid" + ); + } } diff --git a/tvix/nix-compat/src/narinfo/signature.rs b/tvix/nix-compat/src/narinfo/signature.rs index fd197e771d98..33c49128c2d5 100644 --- a/tvix/nix-compat/src/narinfo/signature.rs +++ b/tvix/nix-compat/src/narinfo/signature.rs @@ -1,21 +1,44 @@ -use std::fmt::{self, Display}; +use std::{ + fmt::{self, Display}, + ops::Deref, +}; use data_encoding::BASE64; -use ed25519_dalek::SIGNATURE_LENGTH; use serde::{Deserialize, Serialize}; +const SIGNATURE_LENGTH: usize = std::mem::size_of::<ed25519::SignatureBytes>(); + #[derive(Clone, Debug, Eq, PartialEq)] -pub struct Signature<'a> { - name: &'a str, - bytes: [u8; SIGNATURE_LENGTH], +pub struct Signature<S> { + name: S, + bytes: ed25519::SignatureBytes, } -impl<'a> Signature<'a> { - pub fn new(name: &'a str, bytes: [u8; SIGNATURE_LENGTH]) -> Self { +/// Type alias of a [Signature] using a `&str` as `name` field. +pub type SignatureRef<'a> = Signature<&'a str>; + +/// Represents the signatures that Nix emits. +/// It consists of a name (an identifier for a public key), and an ed25519 +/// signature (64 bytes). +/// It is generic over the string type that's used for the name, and there's +/// [SignatureRef] as a type alias for one containing &str. +impl<S> Signature<S> +where + S: Deref<Target = str>, +{ + /// Constructs a new [Signature] from a name and public key. + pub fn new(name: S, bytes: ed25519::SignatureBytes) -> Self { Self { name, bytes } } - pub fn parse(input: &'a str) -> Result<Self, Error> { + /// Parses a [Signature] from a string containing the name, a colon, and 64 + /// base64-encoded bytes (plus padding). + /// These strings are commonly seen in the `Signature:` field of a NARInfo + /// file. + pub fn parse<'a>(input: &'a str) -> Result<Self, Error> + where + S: From<&'a str>, + { let (name, bytes64) = input.split_once(':').ok_or(Error::MissingSeparator)?; if name.is_empty() @@ -39,14 +62,19 @@ impl<'a> Signature<'a> { Err(_) => return Err(Error::DecodeError(input.to_string())), } - Ok(Signature { name, bytes }) + Ok(Self { + name: name.into(), + bytes, + }) } - pub fn name(&self) -> &'a str { - self.name + /// Returns the name field of the signature. + pub fn name(&self) -> &S { + &self.name } - pub fn bytes(&self) -> &[u8; SIGNATURE_LENGTH] { + /// Returns the 64 bytes of signatures. + pub fn bytes(&self) -> &ed25519::SignatureBytes { &self.bytes } @@ -56,9 +84,21 @@ impl<'a> Signature<'a> { verifying_key.verify_strict(fingerprint, &signature).is_ok() } + + /// Constructs a [SignatureRef] from this signature. + pub fn as_ref(&self) -> SignatureRef<'_> { + SignatureRef { + name: self.name.deref(), + bytes: self.bytes, + } + } } -impl<'de: 'a, 'a> Deserialize<'de> for Signature<'a> { +impl<'a, 'de, S> Deserialize<'de> for Signature<S> +where + S: Deref<Target = str> + From<&'a str>, + 'de: 'a, +{ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, @@ -70,10 +110,13 @@ impl<'de: 'a, 'a> Deserialize<'de> for Signature<'a> { } } -impl<'a> Serialize for Signature<'a> { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> +impl<S: Display> Serialize for Signature<S> +where + S: Deref<Target = str>, +{ + fn serialize<SR>(&self, serializer: SR) -> Result<SR::Ok, SR::Error> where - S: serde::Serializer, + SR: serde::Serializer, { let string: String = self.to_string(); @@ -81,6 +124,15 @@ impl<'a> Serialize for Signature<'a> { } } +impl<S> Display for Signature<S> +where + S: Display, +{ + fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { + write!(w, "{}:{}", self.name, BASE64.encode(&self.bytes)) + } +} + #[derive(Debug, thiserror::Error)] pub enum Error { #[error("Invalid name: {0}")] @@ -93,12 +145,6 @@ pub enum Error { DecodeError(String), } -impl Display for Signature<'_> { - fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { - write!(w, "{}:{}", self.name, BASE64.encode(&self.bytes)) - } -} - #[cfg(test)] mod test { use data_encoding::BASE64; @@ -143,7 +189,7 @@ mod test { #[case] fp: &str, #[case] expect_valid: bool, ) { - let sig = Signature::parse(sig_str).expect("must parse"); + let sig = Signature::<&str>::parse(sig_str).expect("must parse"); assert_eq!(expect_valid, sig.verify(fp.as_bytes(), verifying_key)); } @@ -158,7 +204,7 @@ mod test { "u01BybwQhyI5H1bW1EIWXssMDhDDIvXOG5uh8Qzgdyjz6U1qg6DHhMAvXZOUStIj6X5t4/ufFgR8i3fjf0bMAw==" )] fn parse_fail(#[case] input: &'static str) { - Signature::parse(input).expect_err("must fail"); + Signature::<&str>::parse(input).expect_err("must fail"); } #[test] @@ -177,8 +223,29 @@ mod test { let serialized = serde_json::to_string(&signature_actual).expect("must serialize"); assert_eq!(signature_str_json, &serialized); - let deserialized: Signature<'_> = + let deserialized: Signature<&str> = serde_json::from_str(signature_str_json).expect("must deserialize"); assert_eq!(&signature_actual, &deserialized); } + + /// Construct a [Signature], using different String types for the name field. + #[test] + fn signature_owned() { + let signature1 = Signature::<String>::parse("cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==").expect("must parse"); + let signature2 = Signature::<smol_str::SmolStr>::parse("cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==").expect("must parse"); + let signature3 = Signature::<&str>::parse("cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==").expect("must parse"); + + assert!( + signature1.verify(FINGERPRINT.as_bytes(), &PUB_CACHE_NIXOS_ORG_1), + "must verify" + ); + assert!( + signature2.verify(FINGERPRINT.as_bytes(), &PUB_CACHE_NIXOS_ORG_1), + "must verify" + ); + assert!( + signature3.verify(FINGERPRINT.as_bytes(), &PUB_CACHE_NIXOS_ORG_1), + "must verify" + ); + } } diff --git a/tvix/nix-compat/src/narinfo/signing_keys.rs b/tvix/nix-compat/src/narinfo/signing_keys.rs new file mode 100644 index 000000000000..cf513b7ba475 --- /dev/null +++ b/tvix/nix-compat/src/narinfo/signing_keys.rs @@ -0,0 +1,119 @@ +//! This module provides tooling to parse private key (pairs) produced by Nix +//! and its +//! `nix-store --generate-binary-cache-key name path.secret path.pub` command. +//! It produces `ed25519_dalek` keys, but the `NarInfo::add_signature` function +//! is generic, allowing other signers. + +use data_encoding::BASE64; +use ed25519_dalek::{PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH}; + +use super::{SignatureRef, VerifyingKey}; + +pub struct SigningKey<S> { + name: String, + signing_key: S, +} + +impl<S> SigningKey<S> +where + S: ed25519::signature::Signer<ed25519::Signature>, +{ + /// Constructs a singing key, using a name and a signing key. + pub fn new(name: String, signing_key: S) -> Self { + Self { name, signing_key } + } + + /// Signs a fingerprint using the internal signing key, returns the [SignatureRef] + pub(crate) fn sign<'a>(&'a self, fp: &[u8]) -> SignatureRef<'a> { + SignatureRef::new(&self.name, self.signing_key.sign(fp).to_bytes()) + } + + pub fn name(&self) -> &str { + &self.name + } +} + +/// Parses a SigningKey / VerifyingKey from a byte slice in the format that Nix uses. +pub fn parse_keypair( + input: &str, +) -> Result<(SigningKey<ed25519_dalek::SigningKey>, VerifyingKey), Error> { + let (name, bytes64) = input.split_once(':').ok_or(Error::MissingSeparator)?; + + if name.is_empty() + || !name + .chars() + .all(|c| char::is_alphanumeric(c) || c == '-' || c == '.') + { + return Err(Error::InvalidName(name.to_string())); + } + + const DECODED_BYTES_LEN: usize = SECRET_KEY_LENGTH + PUBLIC_KEY_LENGTH; + if bytes64.len() != BASE64.encode_len(DECODED_BYTES_LEN) { + return Err(Error::InvalidSigningKeyLen(bytes64.len())); + } + + let mut buf = [0; DECODED_BYTES_LEN + 2]; // 64 bytes + 2 bytes padding + let mut bytes = [0; DECODED_BYTES_LEN]; + match BASE64.decode_mut(bytes64.as_bytes(), &mut buf) { + Ok(len) if len == DECODED_BYTES_LEN => { + bytes.copy_from_slice(&buf[..DECODED_BYTES_LEN]); + } + Ok(_) => unreachable!(), + // keeping DecodePartial gets annoying lifetime-wise + Err(_) => return Err(Error::DecodeError(input.to_string())), + } + + let bytes_signing_key: [u8; SECRET_KEY_LENGTH] = { + let mut b = [0u8; SECRET_KEY_LENGTH]; + b.copy_from_slice(&bytes[0..SECRET_KEY_LENGTH]); + b + }; + let bytes_verifying_key: [u8; PUBLIC_KEY_LENGTH] = { + let mut b = [0u8; PUBLIC_KEY_LENGTH]; + b.copy_from_slice(&bytes[SECRET_KEY_LENGTH..]); + b + }; + + let signing_key = SigningKey::new( + name.to_string(), + ed25519_dalek::SigningKey::from_bytes(&bytes_signing_key), + ); + + let verifying_key = VerifyingKey::new( + name.to_string(), + ed25519_dalek::VerifyingKey::from_bytes(&bytes_verifying_key) + .map_err(Error::InvalidVerifyingKey)?, + ); + + Ok((signing_key, verifying_key)) +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Invalid name: {0}")] + InvalidName(String), + #[error("Missing separator")] + MissingSeparator, + #[error("Invalid signing key len: {0}")] + InvalidSigningKeyLen(usize), + #[error("Unable to base64-decode signing key: {0}")] + DecodeError(String), + #[error("VerifyingKey error: {0}")] + InvalidVerifyingKey(ed25519_dalek::SignatureError), +} + +#[cfg(test)] +mod test { + use crate::narinfo::DUMMY_KEYPAIR; + #[test] + fn parse() { + let (_signing_key, _verifying_key) = + super::parse_keypair(DUMMY_KEYPAIR).expect("must succeed"); + } + + #[test] + fn parse_fail() { + assert!(super::parse_keypair("cache.example.com-1:cCta2MEsRNuYCgWYyeRXLyfoFpKhQJKn8gLMeXWAb7vIpRKKo/3JoxJ24OYa3DxT2JVV38KjK/1ywHWuMe2JE").is_err()); + assert!(super::parse_keypair("cache.example.com-1cCta2MEsRNuYCgWYyeRXLyfoFpKhQJKn8gLMeXWAb7vIpRKKo/3JoxJ24OYa3DxT2JVV38KjK/1ywHWuMe2JE").is_err()); + } +} diff --git a/tvix/nix-compat/src/narinfo/public_keys.rs b/tvix/nix-compat/src/narinfo/verifying_keys.rs index 27dd90e096db..67ef2e3a459c 100644 --- a/tvix/nix-compat/src/narinfo/public_keys.rs +++ b/tvix/nix-compat/src/narinfo/verifying_keys.rs @@ -4,21 +4,21 @@ use std::fmt::Display; use data_encoding::BASE64; -use ed25519_dalek::{VerifyingKey, PUBLIC_KEY_LENGTH}; +use ed25519_dalek::PUBLIC_KEY_LENGTH; -use super::Signature; +use super::SignatureRef; /// This represents a ed25519 public key and "name". /// These are normally passed in the `trusted-public-keys` Nix config option, /// and consist of a name and base64-encoded ed25519 pubkey, separated by a `:`. -#[derive(Debug)] -pub struct PubKey { +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct VerifyingKey { name: String, - verifying_key: VerifyingKey, + verifying_key: ed25519_dalek::VerifyingKey, } -impl PubKey { - pub fn new(name: String, verifying_key: VerifyingKey) -> Self { +impl VerifyingKey { + pub fn new(name: String, verifying_key: ed25519_dalek::VerifyingKey) -> Self { Self { name, verifying_key, @@ -37,7 +37,7 @@ impl PubKey { } if bytes64.len() != BASE64.encode_len(PUBLIC_KEY_LENGTH) { - return Err(Error::InvalidPubKeyLen(bytes64.len())); + return Err(Error::InvalidVerifyingKeyLen(bytes64.len())); } let mut buf = [0; PUBLIC_KEY_LENGTH + 1]; @@ -51,7 +51,8 @@ impl PubKey { Err(_) => return Err(Error::DecodeError(input.to_string())), } - let verifying_key = VerifyingKey::from_bytes(&bytes).map_err(Error::InvalidVerifyingKey)?; + let verifying_key = + ed25519_dalek::VerifyingKey::from_bytes(&bytes).map_err(Error::InvalidVerifyingKey)?; Ok(Self { name: name.to_string(), @@ -68,8 +69,8 @@ impl PubKey { /// which means the name in the signature has to match, /// and the signature bytes themselves need to be a valid signature made by /// the signing key identified by [Self::verifying key]. - pub fn verify(&self, fingerprint: &str, signature: &Signature) -> bool { - if self.name() != signature.name() { + pub fn verify(&self, fingerprint: &str, signature: &SignatureRef<'_>) -> bool { + if self.name() != *signature.name() { return false; } @@ -84,14 +85,14 @@ pub enum Error { #[error("Missing separator")] MissingSeparator, #[error("Invalid pubkey len: {0}")] - InvalidPubKeyLen(usize), + InvalidVerifyingKeyLen(usize), #[error("VerifyingKey error: {0}")] InvalidVerifyingKey(ed25519_dalek::SignatureError), #[error("Unable to base64-decode pubkey: {0}")] DecodeError(String), } -impl Display for PubKey { +impl Display for VerifyingKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, @@ -108,9 +109,9 @@ mod test { use ed25519_dalek::PUBLIC_KEY_LENGTH; use rstest::rstest; - use crate::narinfo::Signature; + use crate::narinfo::SignatureRef; - use super::PubKey; + use super::VerifyingKey; const FINGERPRINT: &str = "1;/nix/store/syd87l2rxw8cbsxmxl853h0r6pdwhwjr-curl-7.82.0-bin;sha256:1b4sb93wp679q4zx9k1ignby1yna3z7c4c2ri3wphylbc2dwsys0;196040;/nix/store/0jqd0rlxzra1rs38rdxl43yh6rxchgc6-curl-7.82.0,/nix/store/6w8g7njm4mck5dmjxws0z1xnrxvl81xa-glibc-2.34-115,/nix/store/j5jxw3iy7bbz4a57fh9g2xm2gxmyal8h-zlib-1.2.12,/nix/store/yxvjs9drzsphm9pcf42a4byzj1kb9m7k-openssl-1.1.1n"; #[rstest] @@ -122,7 +123,7 @@ mod test { #[case] exp_name: &'static str, #[case] exp_verifying_key_bytes: &[u8; PUBLIC_KEY_LENGTH], ) { - let pubkey = PubKey::parse(input).expect("must parse"); + let pubkey = VerifyingKey::parse(input).expect("must parse"); assert_eq!(exp_name, pubkey.name()); assert_eq!(exp_verifying_key_bytes, pubkey.verifying_key.as_bytes()); } @@ -132,7 +133,7 @@ mod test { #[case::missing_padding("cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY")] #[case::wrong_length("cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDS")] fn parse_fail(#[case] input: &'static str) { - PubKey::parse(input).expect_err("must fail"); + VerifyingKey::parse(input).expect_err("must fail"); } #[rstest] @@ -144,8 +145,8 @@ mod test { #[case] signature_str: &'static str, #[case] expected: bool, ) { - let pubkey = PubKey::parse(pubkey_str).expect("must parse"); - let signature = Signature::parse(signature_str).expect("must parse"); + let pubkey = VerifyingKey::parse(pubkey_str).expect("must parse"); + let signature = SignatureRef::parse(signature_str).expect("must parse"); assert_eq!(expected, pubkey.verify(fingerprint, &signature)); } diff --git a/tvix/nix-compat/src/nix_daemon/de/bytes.rs b/tvix/nix-compat/src/nix_daemon/de/bytes.rs new file mode 100644 index 000000000000..7daced54eef7 --- /dev/null +++ b/tvix/nix-compat/src/nix_daemon/de/bytes.rs @@ -0,0 +1,70 @@ +use bytes::Bytes; + +use super::{Error, NixDeserialize, NixRead}; + +impl NixDeserialize for Bytes { + async fn try_deserialize<R>(reader: &mut R) -> Result<Option<Self>, R::Error> + where + R: ?Sized + NixRead + Send, + { + reader.try_read_bytes().await + } +} + +impl NixDeserialize for String { + async fn try_deserialize<R>(reader: &mut R) -> Result<Option<Self>, R::Error> + where + R: ?Sized + NixRead + Send, + { + if let Some(buf) = reader.try_read_bytes().await? { + String::from_utf8(buf.to_vec()) + .map_err(R::Error::invalid_data) + .map(Some) + } else { + Ok(None) + } + } +} + +#[cfg(test)] +mod test { + use std::io; + + use hex_literal::hex; + use rstest::rstest; + use tokio_test::io::Builder; + + use crate::nix_daemon::de::{NixRead, NixReader}; + + #[rstest] + #[case::empty("", &hex!("0000 0000 0000 0000"))] + #[case::one(")", &hex!("0100 0000 0000 0000 2900 0000 0000 0000"))] + #[case::two("it", &hex!("0200 0000 0000 0000 6974 0000 0000 0000"))] + #[case::three("tea", &hex!("0300 0000 0000 0000 7465 6100 0000 0000"))] + #[case::four("were", &hex!("0400 0000 0000 0000 7765 7265 0000 0000"))] + #[case::five("where", &hex!("0500 0000 0000 0000 7768 6572 6500 0000"))] + #[case::six("unwrap", &hex!("0600 0000 0000 0000 756E 7772 6170 0000"))] + #[case::seven("where's", &hex!("0700 0000 0000 0000 7768 6572 6527 7300"))] + #[case::aligned("read_tea", &hex!("0800 0000 0000 0000 7265 6164 5F74 6561"))] + #[case::more_bytes("read_tess", &hex!("0900 0000 0000 0000 7265 6164 5F74 6573 7300 0000 0000 0000"))] + #[case::utf8("The quick brown 🦊 jumps over 13 lazy 🐶.", &hex!("2D00 0000 0000 0000 5468 6520 7175 6963 6b20 6272 6f77 6e20 f09f a68a 206a 756d 7073 206f 7665 7220 3133 206c 617a 7920 f09f 90b6 2e00 0000"))] + #[tokio::test] + async fn test_read_string(#[case] expected: &str, #[case] data: &[u8]) { + let mock = Builder::new().read(data).build(); + let mut reader = NixReader::new(mock); + let actual: String = reader.read_value().await.unwrap(); + assert_eq!(actual, expected); + } + + #[tokio::test] + async fn test_read_string_invalid() { + let mock = Builder::new() + .read(&hex!("0300 0000 0000 0000 EDA0 8000 0000 0000")) + .build(); + let mut reader = NixReader::new(mock); + assert_eq!( + io::ErrorKind::InvalidData, + reader.read_value::<String>().await.unwrap_err().kind() + ); + } +} diff --git a/tvix/nix-compat/src/nix_daemon/de/collections.rs b/tvix/nix-compat/src/nix_daemon/de/collections.rs new file mode 100644 index 000000000000..cf79f584506a --- /dev/null +++ b/tvix/nix-compat/src/nix_daemon/de/collections.rs @@ -0,0 +1,105 @@ +use std::{collections::BTreeMap, future::Future}; + +use super::{NixDeserialize, NixRead}; + +#[allow(clippy::manual_async_fn)] +impl<T> NixDeserialize for Vec<T> +where + T: NixDeserialize + Send, +{ + fn try_deserialize<R>( + reader: &mut R, + ) -> impl Future<Output = Result<Option<Self>, R::Error>> + Send + '_ + where + R: ?Sized + NixRead + Send, + { + async move { + if let Some(len) = reader.try_read_value::<usize>().await? { + let mut ret = Vec::with_capacity(len); + for _ in 0..len { + ret.push(reader.read_value().await?); + } + Ok(Some(ret)) + } else { + Ok(None) + } + } + } +} + +#[allow(clippy::manual_async_fn)] +impl<K, V> NixDeserialize for BTreeMap<K, V> +where + K: NixDeserialize + Ord + Send, + V: NixDeserialize + Send, +{ + fn try_deserialize<R>( + reader: &mut R, + ) -> impl Future<Output = Result<Option<Self>, R::Error>> + Send + '_ + where + R: ?Sized + NixRead + Send, + { + async move { + if let Some(len) = reader.try_read_value::<usize>().await? { + let mut ret = BTreeMap::new(); + for _ in 0..len { + let key = reader.read_value().await?; + let value = reader.read_value().await?; + ret.insert(key, value); + } + Ok(Some(ret)) + } else { + Ok(None) + } + } + } +} + +#[cfg(test)] +mod test { + use std::collections::BTreeMap; + use std::fmt; + + use hex_literal::hex; + use rstest::rstest; + use tokio_test::io::Builder; + + use crate::nix_daemon::de::{NixDeserialize, NixRead, NixReader}; + + #[rstest] + #[case::empty(vec![], &hex!("0000 0000 0000 0000"))] + #[case::one(vec![0x29], &hex!("0100 0000 0000 0000 2900 0000 0000 0000"))] + #[case::two(vec![0x7469, 10], &hex!("0200 0000 0000 0000 6974 0000 0000 0000 0A00 0000 0000 0000"))] + #[tokio::test] + async fn test_read_small_vec(#[case] expected: Vec<usize>, #[case] data: &[u8]) { + let mock = Builder::new().read(data).build(); + let mut reader = NixReader::new(mock); + let actual: Vec<usize> = reader.read_value().await.unwrap(); + assert_eq!(actual, expected); + } + + fn empty_map() -> BTreeMap<usize, u64> { + BTreeMap::new() + } + macro_rules! map { + ($($key:expr => $value:expr),*) => {{ + let mut ret = BTreeMap::new(); + $(ret.insert($key, $value);)* + ret + }}; + } + + #[rstest] + #[case::empty(empty_map(), &hex!("0000 0000 0000 0000"))] + #[case::one(map![0x7469usize => 10u64], &hex!("0100 0000 0000 0000 6974 0000 0000 0000 0A00 0000 0000 0000"))] + #[tokio::test] + async fn test_read_small_btree_map<E>(#[case] expected: E, #[case] data: &[u8]) + where + E: NixDeserialize + PartialEq + fmt::Debug, + { + let mock = Builder::new().read(data).build(); + let mut reader = NixReader::new(mock); + let actual: E = reader.read_value().await.unwrap(); + assert_eq!(actual, expected); + } +} diff --git a/tvix/nix-compat/src/nix_daemon/de/int.rs b/tvix/nix-compat/src/nix_daemon/de/int.rs new file mode 100644 index 000000000000..eecf641cfe99 --- /dev/null +++ b/tvix/nix-compat/src/nix_daemon/de/int.rs @@ -0,0 +1,100 @@ +use super::{Error, NixDeserialize, NixRead}; + +impl NixDeserialize for u64 { + async fn try_deserialize<R>(reader: &mut R) -> Result<Option<Self>, R::Error> + where + R: ?Sized + NixRead + Send, + { + reader.try_read_number().await + } +} + +impl NixDeserialize for usize { + async fn try_deserialize<R>(reader: &mut R) -> Result<Option<Self>, R::Error> + where + R: ?Sized + NixRead + Send, + { + if let Some(value) = reader.try_read_number().await? { + value.try_into().map_err(R::Error::invalid_data).map(Some) + } else { + Ok(None) + } + } +} + +impl NixDeserialize for bool { + async fn try_deserialize<R>(reader: &mut R) -> Result<Option<Self>, R::Error> + where + R: ?Sized + NixRead + Send, + { + Ok(reader.try_read_number().await?.map(|v| v != 0)) + } +} +impl NixDeserialize for i64 { + async fn try_deserialize<R>(reader: &mut R) -> Result<Option<Self>, R::Error> + where + R: ?Sized + NixRead + Send, + { + Ok(reader.try_read_number().await?.map(|v| v as i64)) + } +} + +#[cfg(test)] +mod test { + use hex_literal::hex; + use rstest::rstest; + use tokio_test::io::Builder; + + use crate::nix_daemon::de::{NixRead, NixReader}; + + #[rstest] + #[case::simple_false(false, &hex!("0000 0000 0000 0000"))] + #[case::simple_true(true, &hex!("0100 0000 0000 0000"))] + #[case::other_true(true, &hex!("1234 5600 0000 0000"))] + #[case::max_true(true, &hex!("FFFF FFFF FFFF FFFF"))] + #[tokio::test] + async fn test_read_bool(#[case] expected: bool, #[case] data: &[u8]) { + let mock = Builder::new().read(data).build(); + let mut reader = NixReader::new(mock); + let actual: bool = reader.read_value().await.unwrap(); + assert_eq!(actual, expected); + } + + #[rstest] + #[case::zero(0, &hex!("0000 0000 0000 0000"))] + #[case::one(1, &hex!("0100 0000 0000 0000"))] + #[case::other(0x563412, &hex!("1234 5600 0000 0000"))] + #[case::max_value(u64::MAX, &hex!("FFFF FFFF FFFF FFFF"))] + #[tokio::test] + async fn test_read_u64(#[case] expected: u64, #[case] data: &[u8]) { + let mock = Builder::new().read(data).build(); + let mut reader = NixReader::new(mock); + let actual: u64 = reader.read_value().await.unwrap(); + assert_eq!(actual, expected); + } + + #[rstest] + #[case::zero(0, &hex!("0000 0000 0000 0000"))] + #[case::one(1, &hex!("0100 0000 0000 0000"))] + #[case::other(0x563412, &hex!("1234 5600 0000 0000"))] + #[case::max_value(usize::MAX, &usize::MAX.to_le_bytes())] + #[tokio::test] + async fn test_read_usize(#[case] expected: usize, #[case] data: &[u8]) { + let mock = Builder::new().read(data).build(); + let mut reader = NixReader::new(mock); + let actual: usize = reader.read_value().await.unwrap(); + assert_eq!(actual, expected); + } + + // FUTUREWORK: Test this on supported hardware + #[tokio::test] + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + async fn test_read_usize_overflow() { + let mock = Builder::new().read(&u64::MAX.to_le_bytes()).build(); + let mut reader = NixReader::new(mock); + assert_eq!( + std::io::ErrorKind::InvalidData, + reader.read_value::<usize>().await.unwrap_err().kind() + ); + } +} diff --git a/tvix/nix-compat/src/nix_daemon/de/mock.rs b/tvix/nix-compat/src/nix_daemon/de/mock.rs new file mode 100644 index 000000000000..31cc3a4897ba --- /dev/null +++ b/tvix/nix-compat/src/nix_daemon/de/mock.rs @@ -0,0 +1,261 @@ +use std::collections::VecDeque; +use std::fmt; +use std::io; +use std::thread; + +use bytes::Bytes; +use thiserror::Error; + +use crate::nix_daemon::ProtocolVersion; + +use super::NixRead; + +#[derive(Debug, Error, PartialEq, Eq, Clone)] +pub enum Error { + #[error("custom error '{0}'")] + Custom(String), + #[error("invalid data '{0}'")] + InvalidData(String), + #[error("missing data '{0}'")] + MissingData(String), + #[error("IO error {0} '{1}'")] + IO(io::ErrorKind, String), + #[error("wrong read: expected {0} got {1}")] + WrongRead(OperationType, OperationType), +} + +impl Error { + pub fn expected_read_number() -> Error { + Error::WrongRead(OperationType::ReadNumber, OperationType::ReadBytes) + } + + pub fn expected_read_bytes() -> Error { + Error::WrongRead(OperationType::ReadBytes, OperationType::ReadNumber) + } +} + +impl super::Error for Error { + fn custom<T: fmt::Display>(msg: T) -> Self { + Self::Custom(msg.to_string()) + } + + fn io_error(err: std::io::Error) -> Self { + Self::IO(err.kind(), err.to_string()) + } + + fn invalid_data<T: fmt::Display>(msg: T) -> Self { + Self::InvalidData(msg.to_string()) + } + + fn missing_data<T: fmt::Display>(msg: T) -> Self { + Self::MissingData(msg.to_string()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum OperationType { + ReadNumber, + ReadBytes, +} + +impl fmt::Display for OperationType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ReadNumber => write!(f, "read_number"), + Self::ReadBytes => write!(f, "read_bytess"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum Operation { + ReadNumber(Result<u64, Error>), + ReadBytes(Result<Bytes, Error>), +} + +impl From<Operation> for OperationType { + fn from(value: Operation) -> Self { + match value { + Operation::ReadNumber(_) => OperationType::ReadNumber, + Operation::ReadBytes(_) => OperationType::ReadBytes, + } + } +} + +pub struct Builder { + version: ProtocolVersion, + ops: VecDeque<Operation>, +} + +impl Builder { + pub fn new() -> Builder { + Builder { + version: Default::default(), + ops: VecDeque::new(), + } + } + + pub fn version<V: Into<ProtocolVersion>>(&mut self, version: V) -> &mut Self { + self.version = version.into(); + self + } + + pub fn read_number(&mut self, value: u64) -> &mut Self { + self.ops.push_back(Operation::ReadNumber(Ok(value))); + self + } + + pub fn read_number_error(&mut self, err: Error) -> &mut Self { + self.ops.push_back(Operation::ReadNumber(Err(err))); + self + } + + pub fn read_bytes(&mut self, value: Bytes) -> &mut Self { + self.ops.push_back(Operation::ReadBytes(Ok(value))); + self + } + + pub fn read_slice(&mut self, data: &[u8]) -> &mut Self { + let value = Bytes::copy_from_slice(data); + self.ops.push_back(Operation::ReadBytes(Ok(value))); + self + } + + pub fn read_bytes_error(&mut self, err: Error) -> &mut Self { + self.ops.push_back(Operation::ReadBytes(Err(err))); + self + } + + pub fn build(&mut self) -> Mock { + Mock { + version: self.version, + ops: self.ops.clone(), + } + } +} + +impl Default for Builder { + fn default() -> Self { + Self::new() + } +} + +pub struct Mock { + version: ProtocolVersion, + ops: VecDeque<Operation>, +} + +impl NixRead for Mock { + type Error = Error; + + fn version(&self) -> ProtocolVersion { + self.version + } + + async fn try_read_number(&mut self) -> Result<Option<u64>, Self::Error> { + match self.ops.pop_front() { + Some(Operation::ReadNumber(ret)) => ret.map(Some), + Some(Operation::ReadBytes(_)) => Err(Error::expected_read_bytes()), + None => Ok(None), + } + } + + async fn try_read_bytes_limited( + &mut self, + _limit: std::ops::RangeInclusive<usize>, + ) -> Result<Option<Bytes>, Self::Error> { + match self.ops.pop_front() { + Some(Operation::ReadBytes(ret)) => ret.map(Some), + Some(Operation::ReadNumber(_)) => Err(Error::expected_read_number()), + None => Ok(None), + } + } +} + +impl Drop for Mock { + fn drop(&mut self) { + // No need to panic again + if thread::panicking() { + return; + } + if let Some(op) = self.ops.front() { + panic!("reader dropped with {op:?} operation still unread") + } + } +} + +#[cfg(test)] +mod test { + use bytes::Bytes; + use hex_literal::hex; + + use crate::nix_daemon::de::NixRead; + + use super::{Builder, Error}; + + #[tokio::test] + async fn read_slice() { + let mut mock = Builder::new() + .read_number(10) + .read_slice(&[]) + .read_slice(&hex!("0000 1234 5678 9ABC DEFF")) + .build(); + assert_eq!(10, mock.read_number().await.unwrap()); + assert_eq!(&[] as &[u8], &mock.read_bytes().await.unwrap()[..]); + assert_eq!( + &hex!("0000 1234 5678 9ABC DEFF"), + &mock.read_bytes().await.unwrap()[..] + ); + assert_eq!(None, mock.try_read_number().await.unwrap()); + assert_eq!(None, mock.try_read_bytes().await.unwrap()); + } + + #[tokio::test] + async fn read_bytes() { + let mut mock = Builder::new() + .read_number(10) + .read_bytes(Bytes::from_static(&[])) + .read_bytes(Bytes::from_static(&hex!("0000 1234 5678 9ABC DEFF"))) + .build(); + assert_eq!(10, mock.read_number().await.unwrap()); + assert_eq!(&[] as &[u8], &mock.read_bytes().await.unwrap()[..]); + assert_eq!( + &hex!("0000 1234 5678 9ABC DEFF"), + &mock.read_bytes().await.unwrap()[..] + ); + assert_eq!(None, mock.try_read_number().await.unwrap()); + assert_eq!(None, mock.try_read_bytes().await.unwrap()); + } + + #[tokio::test] + async fn read_number() { + let mut mock = Builder::new().read_number(10).build(); + assert_eq!(10, mock.read_number().await.unwrap()); + assert_eq!(None, mock.try_read_number().await.unwrap()); + assert_eq!(None, mock.try_read_bytes().await.unwrap()); + } + + #[tokio::test] + async fn expect_number() { + let mut mock = Builder::new().read_number(10).build(); + assert_eq!( + Error::expected_read_number(), + mock.read_bytes().await.unwrap_err() + ); + } + + #[tokio::test] + async fn expect_bytes() { + let mut mock = Builder::new().read_slice(&[]).build(); + assert_eq!( + Error::expected_read_bytes(), + mock.read_number().await.unwrap_err() + ); + } + + #[test] + #[should_panic] + fn operations_left() { + let _ = Builder::new().read_number(10).build(); + } +} diff --git a/tvix/nix-compat/src/nix_daemon/de/mod.rs b/tvix/nix-compat/src/nix_daemon/de/mod.rs new file mode 100644 index 000000000000..f85ccd8fea0e --- /dev/null +++ b/tvix/nix-compat/src/nix_daemon/de/mod.rs @@ -0,0 +1,225 @@ +use std::error::Error as StdError; +use std::future::Future; +use std::ops::RangeInclusive; +use std::{fmt, io}; + +use ::bytes::Bytes; + +use super::ProtocolVersion; + +mod bytes; +mod collections; +mod int; +#[cfg(any(test, feature = "test"))] +pub mod mock; +mod reader; + +pub use reader::{NixReader, NixReaderBuilder}; + +/// Like serde the `Error` trait allows `NixRead` implementations to add +/// custom error handling for `NixDeserialize`. +pub trait Error: Sized + StdError { + /// A totally custom non-specific error. + fn custom<T: fmt::Display>(msg: T) -> Self; + + /// Some kind of std::io::Error occured. + fn io_error(err: std::io::Error) -> Self { + Self::custom(format_args!("There was an I/O error {}", err)) + } + + /// The data read from `NixRead` is invalid. + /// This could be that some bytes were supposed to be valid UFT-8 but weren't. + fn invalid_data<T: fmt::Display>(msg: T) -> Self { + Self::custom(msg) + } + + /// Required data is missing. This is mostly like an EOF + fn missing_data<T: fmt::Display>(msg: T) -> Self { + Self::custom(msg) + } +} + +impl Error for io::Error { + fn custom<T: fmt::Display>(msg: T) -> Self { + io::Error::new(io::ErrorKind::Other, msg.to_string()) + } + + fn io_error(err: std::io::Error) -> Self { + err + } + + fn invalid_data<T: fmt::Display>(msg: T) -> Self { + io::Error::new(io::ErrorKind::InvalidData, msg.to_string()) + } + + fn missing_data<T: fmt::Display>(msg: T) -> Self { + io::Error::new(io::ErrorKind::UnexpectedEof, msg.to_string()) + } +} + +/// A reader of data from the Nix daemon protocol. +/// Basically there are two basic types in the Nix daemon protocol +/// u64 and a bytes buffer. Everything else is more or less built on +/// top of these two types. +pub trait NixRead: Send { + type Error: Error + Send; + + /// Some types are serialized differently depending on the version + /// of the protocol and so this can be used for implementing that. + fn version(&self) -> ProtocolVersion; + + /// Read a single u64 from the protocol. + /// This returns an Option to support graceful shutdown. + fn try_read_number( + &mut self, + ) -> impl Future<Output = Result<Option<u64>, Self::Error>> + Send + '_; + + /// Read bytes from the protocol. + /// A size limit on the returned bytes has to be specified. + /// This returns an Option to support graceful shutdown. + fn try_read_bytes_limited( + &mut self, + limit: RangeInclusive<usize>, + ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send + '_; + + /// Read bytes from the protocol without a limit. + /// The default implementation just calls `try_read_bytes_limited` with a + /// limit of `0..=usize::MAX` but other implementations are free to have a + /// reader wide limit. + /// This returns an Option to support graceful shutdown. + fn try_read_bytes( + &mut self, + ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send + '_ { + self.try_read_bytes_limited(0..=usize::MAX) + } + + /// Read a single u64 from the protocol. + /// This will return an error if the number could not be read. + fn read_number(&mut self) -> impl Future<Output = Result<u64, Self::Error>> + Send + '_ { + async move { + match self.try_read_number().await? { + Some(v) => Ok(v), + None => Err(Self::Error::missing_data("unexpected end-of-file")), + } + } + } + + /// Read bytes from the protocol. + /// A size limit on the returned bytes has to be specified. + /// This will return an error if the number could not be read. + fn read_bytes_limited( + &mut self, + limit: RangeInclusive<usize>, + ) -> impl Future<Output = Result<Bytes, Self::Error>> + Send + '_ { + async move { + match self.try_read_bytes_limited(limit).await? { + Some(v) => Ok(v), + None => Err(Self::Error::missing_data("unexpected end-of-file")), + } + } + } + + /// Read bytes from the protocol. + /// The default implementation just calls `read_bytes_limited` with a + /// limit of `0..=usize::MAX` but other implementations are free to have a + /// reader wide limit. + /// This will return an error if the bytes could not be read. + fn read_bytes(&mut self) -> impl Future<Output = Result<Bytes, Self::Error>> + Send + '_ { + self.read_bytes_limited(0..=usize::MAX) + } + + /// Read a value from the protocol. + /// Uses `NixDeserialize::deserialize` to read a value. + fn read_value<V: NixDeserialize>( + &mut self, + ) -> impl Future<Output = Result<V, Self::Error>> + Send + '_ { + V::deserialize(self) + } + + /// Read a value from the protocol. + /// Uses `NixDeserialize::try_deserialize` to read a value. + /// This returns an Option to support graceful shutdown. + fn try_read_value<V: NixDeserialize>( + &mut self, + ) -> impl Future<Output = Result<Option<V>, Self::Error>> + Send + '_ { + V::try_deserialize(self) + } +} + +impl<T: ?Sized + NixRead> NixRead for &mut T { + type Error = T::Error; + + fn version(&self) -> ProtocolVersion { + (**self).version() + } + + fn try_read_number( + &mut self, + ) -> impl Future<Output = Result<Option<u64>, Self::Error>> + Send + '_ { + (**self).try_read_number() + } + + fn try_read_bytes_limited( + &mut self, + limit: RangeInclusive<usize>, + ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send + '_ { + (**self).try_read_bytes_limited(limit) + } + + fn try_read_bytes( + &mut self, + ) -> impl Future<Output = Result<Option<Bytes>, Self::Error>> + Send + '_ { + (**self).try_read_bytes() + } + + fn read_number(&mut self) -> impl Future<Output = Result<u64, Self::Error>> + Send + '_ { + (**self).read_number() + } + + fn read_bytes_limited( + &mut self, + limit: RangeInclusive<usize>, + ) -> impl Future<Output = Result<Bytes, Self::Error>> + Send + '_ { + (**self).read_bytes_limited(limit) + } + + fn read_bytes(&mut self) -> impl Future<Output = Result<Bytes, Self::Error>> + Send + '_ { + (**self).read_bytes() + } + + fn try_read_value<V: NixDeserialize>( + &mut self, + ) -> impl Future<Output = Result<Option<V>, Self::Error>> + Send + '_ { + (**self).try_read_value() + } + + fn read_value<V: NixDeserialize>( + &mut self, + ) -> impl Future<Output = Result<V, Self::Error>> + Send + '_ { + (**self).read_value() + } +} + +/// A data structure that can be deserialized from the Nix daemon +/// worker protocol. +pub trait NixDeserialize: Sized { + /// Read a value from the reader. + /// This returns an Option to support gracefull shutdown. + fn try_deserialize<R>( + reader: &mut R, + ) -> impl Future<Output = Result<Option<Self>, R::Error>> + Send + '_ + where + R: ?Sized + NixRead + Send; + + fn deserialize<R>(reader: &mut R) -> impl Future<Output = Result<Self, R::Error>> + Send + '_ + where + R: ?Sized + NixRead + Send, + { + async move { + match Self::try_deserialize(reader).await? { + Some(v) => Ok(v), + None => Err(R::Error::missing_data("unexpected end-of-file")), + } + } + } +} diff --git a/tvix/nix-compat/src/nix_daemon/de/reader.rs b/tvix/nix-compat/src/nix_daemon/de/reader.rs new file mode 100644 index 000000000000..87c623b2220c --- /dev/null +++ b/tvix/nix-compat/src/nix_daemon/de/reader.rs @@ -0,0 +1,527 @@ +use std::future::poll_fn; +use std::io::{self, Cursor}; +use std::ops::RangeInclusive; +use std::pin::Pin; +use std::task::{ready, Context, Poll}; + +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use pin_project_lite::pin_project; +use tokio::io::{AsyncBufRead, AsyncRead, AsyncReadExt, ReadBuf}; + +use crate::nix_daemon::ProtocolVersion; +use crate::wire::EMPTY_BYTES; + +use super::{Error, NixRead}; + +pub struct NixReaderBuilder { + buf: Option<BytesMut>, + reserved_buf_size: usize, + max_buf_size: usize, + version: ProtocolVersion, +} + +impl Default for NixReaderBuilder { + fn default() -> Self { + Self { + buf: Default::default(), + reserved_buf_size: 8192, + max_buf_size: 8192, + version: Default::default(), + } + } +} + +impl NixReaderBuilder { + pub fn set_buffer(mut self, buf: BytesMut) -> Self { + self.buf = Some(buf); + self + } + + pub fn set_reserved_buf_size(mut self, size: usize) -> Self { + self.reserved_buf_size = size; + self + } + + pub fn set_max_buf_size(mut self, size: usize) -> Self { + self.max_buf_size = size; + self + } + + pub fn set_version(mut self, version: ProtocolVersion) -> Self { + self.version = version; + self + } + + pub fn build<R>(self, reader: R) -> NixReader<R> { + let buf = self.buf.unwrap_or_else(|| BytesMut::with_capacity(0)); + NixReader { + buf, + inner: reader, + reserved_buf_size: self.reserved_buf_size, + max_buf_size: self.max_buf_size, + version: self.version, + } + } +} + +pin_project! { + pub struct NixReader<R> { + #[pin] + inner: R, + buf: BytesMut, + reserved_buf_size: usize, + max_buf_size: usize, + version: ProtocolVersion, + } +} + +impl NixReader<Cursor<Vec<u8>>> { + pub fn builder() -> NixReaderBuilder { + NixReaderBuilder::default() + } +} + +impl<R> NixReader<R> +where + R: AsyncReadExt, +{ + pub fn new(reader: R) -> NixReader<R> { + NixReader::builder().build(reader) + } + + pub fn buffer(&self) -> &[u8] { + &self.buf[..] + } + + #[cfg(test)] + pub(crate) fn buffer_mut(&mut self) -> &mut BytesMut { + &mut self.buf + } + + /// Remaining capacity in internal buffer + pub fn remaining_mut(&self) -> usize { + self.buf.capacity() - self.buf.len() + } + + fn poll_force_fill_buf( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll<io::Result<usize>> { + // Ensure that buffer has space for at least reserved_buf_size bytes + if self.remaining_mut() < self.reserved_buf_size { + let me = self.as_mut().project(); + me.buf.reserve(*me.reserved_buf_size); + } + let me = self.project(); + let n = { + let dst = me.buf.spare_capacity_mut(); + let mut buf = ReadBuf::uninit(dst); + let ptr = buf.filled().as_ptr(); + ready!(me.inner.poll_read(cx, &mut buf)?); + + // Ensure the pointer does not change from under us + assert_eq!(ptr, buf.filled().as_ptr()); + buf.filled().len() + }; + + // SAFETY: This is guaranteed to be the number of initialized (and read) + // bytes due to the invariants provided by `ReadBuf::filled`. + unsafe { + me.buf.advance_mut(n); + } + Poll::Ready(Ok(n)) + } +} + +impl<R> NixReader<R> +where + R: AsyncReadExt + Unpin, +{ + async fn force_fill(&mut self) -> io::Result<usize> { + let mut p = Pin::new(self); + let read = poll_fn(|cx| p.as_mut().poll_force_fill_buf(cx)).await?; + Ok(read) + } +} + +impl<R> NixRead for NixReader<R> +where + R: AsyncReadExt + Send + Unpin, +{ + type Error = io::Error; + + fn version(&self) -> ProtocolVersion { + self.version + } + + async fn try_read_number(&mut self) -> Result<Option<u64>, Self::Error> { + let mut buf = [0u8; 8]; + let read = self.read_buf(&mut &mut buf[..]).await?; + if read == 0 { + return Ok(None); + } + if read < 8 { + self.read_exact(&mut buf[read..]).await?; + } + let num = Buf::get_u64_le(&mut &buf[..]); + Ok(Some(num)) + } + + async fn try_read_bytes_limited( + &mut self, + limit: RangeInclusive<usize>, + ) -> Result<Option<Bytes>, Self::Error> { + assert!( + *limit.end() <= self.max_buf_size, + "The limit must be smaller than {}", + self.max_buf_size + ); + match self.try_read_number().await? { + Some(raw_len) => { + // Check that length is in range and convert to usize + let len = raw_len + .try_into() + .ok() + .filter(|v| limit.contains(v)) + .ok_or_else(|| Self::Error::invalid_data("bytes length out of range"))?; + + // Calculate 64bit aligned length and convert to usize + let aligned: usize = raw_len + .checked_add(7) + .map(|v| v & !7) + .ok_or_else(|| Self::Error::invalid_data("bytes length out of range"))? + .try_into() + .map_err(Self::Error::invalid_data)?; + + // Ensure that there is enough space in buffer for contents + if self.buf.len() + self.remaining_mut() < aligned { + self.buf.reserve(aligned - self.buf.len()); + } + while self.buf.len() < aligned { + if self.force_fill().await? == 0 { + return Err(Self::Error::missing_data( + "unexpected end-of-file reading bytes", + )); + } + } + let mut contents = self.buf.split_to(aligned); + + let padding = aligned - len; + // Ensure padding is all zeros + if contents[len..] != EMPTY_BYTES[..padding] { + return Err(Self::Error::invalid_data("non-zero padding")); + } + + contents.truncate(len); + Ok(Some(contents.freeze())) + } + None => Ok(None), + } + } + + fn try_read_bytes( + &mut self, + ) -> impl std::future::Future<Output = Result<Option<Bytes>, Self::Error>> + Send + '_ { + self.try_read_bytes_limited(0..=self.max_buf_size) + } + + fn read_bytes( + &mut self, + ) -> impl std::future::Future<Output = Result<Bytes, Self::Error>> + Send + '_ { + self.read_bytes_limited(0..=self.max_buf_size) + } +} + +impl<R: AsyncRead> AsyncRead for NixReader<R> { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll<io::Result<()>> { + let rem = ready!(self.as_mut().poll_fill_buf(cx))?; + let amt = std::cmp::min(rem.len(), buf.remaining()); + buf.put_slice(&rem[0..amt]); + self.consume(amt); + Poll::Ready(Ok(())) + } +} + +impl<R: AsyncRead> AsyncBufRead for NixReader<R> { + fn poll_fill_buf(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<&[u8]>> { + if self.as_ref().project_ref().buf.is_empty() { + ready!(self.as_mut().poll_force_fill_buf(cx))?; + } + let me = self.project(); + Poll::Ready(Ok(&me.buf[..])) + } + + fn consume(self: Pin<&mut Self>, amt: usize) { + let me = self.project(); + me.buf.advance(amt) + } +} + +#[cfg(test)] +mod test { + use std::time::Duration; + + use hex_literal::hex; + use rstest::rstest; + use tokio_test::io::Builder; + + use super::*; + use crate::nix_daemon::de::NixRead; + + #[tokio::test] + async fn test_read_u64() { + let mock = Builder::new().read(&hex!("0100 0000 0000 0000")).build(); + let mut reader = NixReader::new(mock); + + assert_eq!(1, reader.read_number().await.unwrap()); + assert_eq!(hex!(""), reader.buffer()); + + let mut buf = Vec::new(); + reader.read_to_end(&mut buf).await.unwrap(); + assert_eq!(hex!(""), &buf[..]); + } + + #[tokio::test] + async fn test_read_u64_rest() { + let mock = Builder::new() + .read(&hex!("0100 0000 0000 0000 0123 4567 89AB CDEF")) + .build(); + let mut reader = NixReader::new(mock); + + assert_eq!(1, reader.read_number().await.unwrap()); + assert_eq!(hex!("0123 4567 89AB CDEF"), reader.buffer()); + + let mut buf = Vec::new(); + reader.read_to_end(&mut buf).await.unwrap(); + assert_eq!(hex!("0123 4567 89AB CDEF"), &buf[..]); + } + + #[tokio::test] + async fn test_read_u64_partial() { + let mock = Builder::new() + .read(&hex!("0100 0000")) + .wait(Duration::ZERO) + .read(&hex!("0000 0000 0123 4567 89AB CDEF")) + .wait(Duration::ZERO) + .read(&hex!("0100 0000")) + .build(); + let mut reader = NixReader::new(mock); + + assert_eq!(1, reader.read_number().await.unwrap()); + assert_eq!(hex!("0123 4567 89AB CDEF"), reader.buffer()); + + let mut buf = Vec::new(); + reader.read_to_end(&mut buf).await.unwrap(); + assert_eq!(hex!("0123 4567 89AB CDEF 0100 0000"), &buf[..]); + } + + #[tokio::test] + async fn test_read_u64_eof() { + let mock = Builder::new().build(); + let mut reader = NixReader::new(mock); + + assert_eq!( + io::ErrorKind::UnexpectedEof, + reader.read_number().await.unwrap_err().kind() + ); + } + + #[tokio::test] + async fn test_try_read_u64_none() { + let mock = Builder::new().build(); + let mut reader = NixReader::new(mock); + + assert_eq!(None, reader.try_read_number().await.unwrap()); + } + + #[tokio::test] + async fn test_try_read_u64_eof() { + let mock = Builder::new().read(&hex!("0100 0000 0000")).build(); + let mut reader = NixReader::new(mock); + + assert_eq!( + io::ErrorKind::UnexpectedEof, + reader.try_read_number().await.unwrap_err().kind() + ); + } + + #[tokio::test] + async fn test_try_read_u64_eof2() { + let mock = Builder::new() + .read(&hex!("0100")) + .wait(Duration::ZERO) + .read(&hex!("0000 0000")) + .build(); + let mut reader = NixReader::new(mock); + + assert_eq!( + io::ErrorKind::UnexpectedEof, + reader.try_read_number().await.unwrap_err().kind() + ); + } + + #[rstest] + #[case::empty(b"", &hex!("0000 0000 0000 0000"))] + #[case::one(b")", &hex!("0100 0000 0000 0000 2900 0000 0000 0000"))] + #[case::two(b"it", &hex!("0200 0000 0000 0000 6974 0000 0000 0000"))] + #[case::three(b"tea", &hex!("0300 0000 0000 0000 7465 6100 0000 0000"))] + #[case::four(b"were", &hex!("0400 0000 0000 0000 7765 7265 0000 0000"))] + #[case::five(b"where", &hex!("0500 0000 0000 0000 7768 6572 6500 0000"))] + #[case::six(b"unwrap", &hex!("0600 0000 0000 0000 756E 7772 6170 0000"))] + #[case::seven(b"where's", &hex!("0700 0000 0000 0000 7768 6572 6527 7300"))] + #[case::aligned(b"read_tea", &hex!("0800 0000 0000 0000 7265 6164 5F74 6561"))] + #[case::more_bytes(b"read_tess", &hex!("0900 0000 0000 0000 7265 6164 5F74 6573 7300 0000 0000 0000"))] + #[tokio::test] + async fn test_read_bytes(#[case] expected: &[u8], #[case] data: &[u8]) { + let mock = Builder::new().read(data).build(); + let mut reader = NixReader::new(mock); + let actual = reader.read_bytes().await.unwrap(); + assert_eq!(&actual[..], expected); + } + + #[tokio::test] + async fn test_read_bytes_empty() { + let mock = Builder::new().build(); + let mut reader = NixReader::new(mock); + + assert_eq!( + io::ErrorKind::UnexpectedEof, + reader.read_bytes().await.unwrap_err().kind() + ); + } + + #[tokio::test] + async fn test_try_read_bytes_none() { + let mock = Builder::new().build(); + let mut reader = NixReader::new(mock); + + assert_eq!(None, reader.try_read_bytes().await.unwrap()); + } + + #[tokio::test] + async fn test_try_read_bytes_missing_data() { + let mock = Builder::new() + .read(&hex!("0500")) + .wait(Duration::ZERO) + .read(&hex!("0000 0000")) + .build(); + let mut reader = NixReader::new(mock); + + assert_eq!( + io::ErrorKind::UnexpectedEof, + reader.try_read_bytes().await.unwrap_err().kind() + ); + } + + #[tokio::test] + async fn test_try_read_bytes_missing_padding() { + let mock = Builder::new() + .read(&hex!("0200 0000 0000 0000")) + .wait(Duration::ZERO) + .read(&hex!("1234")) + .build(); + let mut reader = NixReader::new(mock); + + assert_eq!( + io::ErrorKind::UnexpectedEof, + reader.try_read_bytes().await.unwrap_err().kind() + ); + } + + #[tokio::test] + async fn test_read_bytes_bad_padding() { + let mock = Builder::new() + .read(&hex!("0200 0000 0000 0000")) + .wait(Duration::ZERO) + .read(&hex!("1234 0100 0000 0000")) + .build(); + let mut reader = NixReader::new(mock); + + assert_eq!( + io::ErrorKind::InvalidData, + reader.read_bytes().await.unwrap_err().kind() + ); + } + + #[tokio::test] + async fn test_read_bytes_limited_out_of_range() { + let mock = Builder::new().read(&hex!("FFFF 0000 0000 0000")).build(); + let mut reader = NixReader::new(mock); + + assert_eq!( + io::ErrorKind::InvalidData, + reader.read_bytes_limited(0..=50).await.unwrap_err().kind() + ); + } + + #[tokio::test] + async fn test_read_bytes_length_overflow() { + let mock = Builder::new().read(&hex!("F9FF FFFF FFFF FFFF")).build(); + let mut reader = NixReader::builder() + .set_max_buf_size(usize::MAX) + .build(mock); + + assert_eq!( + io::ErrorKind::InvalidData, + reader + .read_bytes_limited(0..=usize::MAX) + .await + .unwrap_err() + .kind() + ); + } + + // FUTUREWORK: Test this on supported hardware + #[tokio::test] + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + async fn test_bytes_length_conversion_overflow() { + let len = (usize::MAX as u64) + 1; + let mock = Builder::new().read(&len.to_le_bytes()).build(); + let mut reader = NixReader::new(mock); + assert_eq!( + std::io::ErrorKind::InvalidData, + reader.read_value::<usize>().await.unwrap_err().kind() + ); + } + + // FUTUREWORK: Test this on supported hardware + #[tokio::test] + #[cfg(any(target_pointer_width = "16", target_pointer_width = "32"))] + async fn test_bytes_aligned_length_conversion_overflow() { + let len = (usize::MAX - 6) as u64; + let mock = Builder::new().read(&len.to_le_bytes()).build(); + let mut reader = NixReader::new(mock); + assert_eq!( + std::io::ErrorKind::InvalidData, + reader.read_value::<usize>().await.unwrap_err().kind() + ); + } + + #[tokio::test] + async fn test_buffer_resize() { + let mock = Builder::new() + .read(&hex!("0100")) + .read(&hex!("0000 0000 0000")) + .build(); + let mut reader = NixReader::builder().set_reserved_buf_size(8).build(mock); + // buffer has no capacity initially + assert_eq!(0, reader.buffer_mut().capacity()); + + assert_eq!(2, reader.force_fill().await.unwrap()); + + // After first read buffer should have capacity we chose + assert_eq!(8, reader.buffer_mut().capacity()); + + // Because there was only 6 bytes remaining in buffer, + // which is enough to read the last 6 bytes, but we require + // capacity for 8 bytes, it doubled the capacity + assert_eq!(6, reader.force_fill().await.unwrap()); + assert_eq!(16, reader.buffer_mut().capacity()); + + assert_eq!(1, reader.read_number().await.unwrap()); + } +} diff --git a/tvix/nix-compat/src/nix_daemon/mod.rs b/tvix/nix-compat/src/nix_daemon/mod.rs index fe652377d1b4..11413e85fd1b 100644 --- a/tvix/nix-compat/src/nix_daemon/mod.rs +++ b/tvix/nix-compat/src/nix_daemon/mod.rs @@ -2,3 +2,5 @@ pub mod worker_protocol; mod protocol_version; pub use protocol_version::ProtocolVersion; + +pub mod de; diff --git a/tvix/nix-compat/src/nix_daemon/protocol_version.rs b/tvix/nix-compat/src/nix_daemon/protocol_version.rs index 8fd2b085c962..19da28d484dd 100644 --- a/tvix/nix-compat/src/nix_daemon/protocol_version.rs +++ b/tvix/nix-compat/src/nix_daemon/protocol_version.rs @@ -1,3 +1,6 @@ +/// The latest version that is currently supported by nix-compat. +static DEFAULT_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::from_parts(1, 37); + /// Protocol versions are represented as a u16. /// The upper 8 bits are the major version, the lower bits the minor. /// This is not aware of any endianness, use [crate::wire::read_u64] to get an @@ -20,6 +23,12 @@ impl ProtocolVersion { } } +impl Default for ProtocolVersion { + fn default() -> Self { + DEFAULT_PROTOCOL_VERSION + } +} + impl PartialOrd for ProtocolVersion { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { Some(self.cmp(other)) @@ -45,6 +54,13 @@ impl From<u16> for ProtocolVersion { } } +#[cfg(any(test, feature = "test"))] +impl From<(u8, u8)> for ProtocolVersion { + fn from((major, minor): (u8, u8)) -> Self { + Self::from_parts(major, minor) + } +} + impl TryFrom<u64> for ProtocolVersion { type Error = &'static str; diff --git a/tvix/nix-compat/src/nix_http/mod.rs b/tvix/nix-compat/src/nix_http/mod.rs new file mode 100644 index 000000000000..89ba147b8071 --- /dev/null +++ b/tvix/nix-compat/src/nix_http/mod.rs @@ -0,0 +1,115 @@ +use tracing::trace; + +use crate::nixbase32; + +/// The mime type used for NAR files, both compressed and uncompressed +pub const MIME_TYPE_NAR: &str = "application/x-nix-nar"; +/// The mime type used for NARInfo files +pub const MIME_TYPE_NARINFO: &str = "text/x-nix-narinfo"; +/// The mime type used for the `nix-cache-info` file +pub const MIME_TYPE_CACHE_INFO: &str = "text/x-nix-cache-info"; + +/// Parses a `14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0k.nar` +/// string and returns the nixbase32-decoded digest, as well as the compression +/// suffix (which might be empty). +pub fn parse_nar_str(s: &str) -> Option<([u8; 32], &str)> { + if !s.is_char_boundary(52) { + trace!("invalid string, no char boundary at 52"); + return None; + } + + let (hash_str, suffix) = s.split_at(52); + + // we know hash_str is 52 bytes, so it's ok to unwrap here. + let hash_str_fixed: [u8; 52] = hash_str.as_bytes().try_into().unwrap(); + + match suffix.strip_prefix(".nar") { + Some(compression_suffix) => match nixbase32::decode_fixed(hash_str_fixed) { + Err(e) => { + trace!(err=%e, "invalid nixbase32 encoding"); + None + } + Ok(digest) => Some((digest, compression_suffix)), + }, + None => { + trace!("no .nar suffix"); + None + } + } +} + +/// Parses a `3mzh8lvgbynm9daj7c82k2sfsfhrsfsy.narinfo` string and returns the +/// nixbase32-decoded digest. +pub fn parse_narinfo_str(s: &str) -> Option<[u8; 20]> { + if !s.is_char_boundary(32) { + trace!("invalid string, no char boundary at 32"); + return None; + } + + match s.split_at(32) { + (hash_str, ".narinfo") => { + // we know this is 32 bytes, so it's ok to unwrap here. + let hash_str_fixed: [u8; 32] = hash_str.as_bytes().try_into().unwrap(); + + match nixbase32::decode_fixed(hash_str_fixed) { + Err(e) => { + trace!(err=%e, "invalid nixbase32 encoding"); + None + } + Ok(digest) => Some(digest), + } + } + _ => { + trace!("invalid string, no .narinfo suffix"); + None + } + } +} + +#[cfg(test)] +mod test { + use super::{parse_nar_str, parse_narinfo_str}; + use hex_literal::hex; + + #[test] + fn parse_nar_str_success() { + assert_eq!( + ( + hex!("13a8cf7ca57f68a9f1752acee36a72a55187d3a954443c112818926f26109d91"), + "" + ), + parse_nar_str("14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0k.nar").unwrap() + ); + + assert_eq!( + ( + hex!("13a8cf7ca57f68a9f1752acee36a72a55187d3a954443c112818926f26109d91"), + ".xz" + ), + parse_nar_str("14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0k.nar.xz").unwrap() + ) + } + + #[test] + fn parse_nar_str_failure() { + assert!(parse_nar_str("14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0").is_none()); + assert!( + parse_nar_str("14cx20k6z4hq508kqi2lm79qfld5f9mf7kiafpqsjs3zlmycza0🦊.nar").is_none() + ) + } + #[test] + fn parse_narinfo_str_success() { + assert_eq!( + hex!("8a12321522fd91efbd60ebb2481af88580f61600"), + parse_narinfo_str("00bgd045z0d4icpbc2yyz4gx48ak44la.narinfo").unwrap() + ); + } + + #[test] + fn parse_narinfo_str_failure() { + assert!(parse_narinfo_str("00bgd045z0d4icpbc2yyz4gx48ak44la").is_none()); + assert!(parse_narinfo_str("/00bgd045z0d4icpbc2yyz4gx48ak44la").is_none()); + assert!(parse_narinfo_str("000000").is_none()); + assert!(parse_narinfo_str("00bgd045z0d4icpbc2yyz4gx48ak44l🦊.narinfo").is_none()); + } +} diff --git a/tvix/nix-compat/src/nixbase32.rs b/tvix/nix-compat/src/nixbase32.rs index b7ffc1dc2bcd..8d34e4cedce6 100644 --- a/tvix/nix-compat/src/nixbase32.rs +++ b/tvix/nix-compat/src/nixbase32.rs @@ -62,6 +62,12 @@ pub fn decode(input: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> { let input = input.as_ref(); let output_len = decode_len(input.len()); + if input.len() != encode_len(output_len) { + return Err(DecodeError { + position: input.len().min(encode_len(output_len)), + kind: DecodeKind::Length, + }); + } let mut output: Vec<u8> = vec![0x00; output_len]; decode_inner(input, &mut output)?; @@ -163,6 +169,10 @@ mod tests { #[case::invalid_encoding_1("zz", None)] // this is an even more specific example - it'd decode as 00000000 11 #[case::invalid_encoding_2("c0", None)] + // This has an invalid length + #[case::invalid_encoding_3("0", None)] + // This has an invalid length + #[case::invalid_encoding_4("0zz", None)] #[test] fn decode(#[case] enc: &str, #[case] dec: Option<&[u8]>) { match dec { @@ -201,6 +211,11 @@ mod tests { #[test] fn decode_len() { assert_eq!(super::decode_len(0), 0); + assert_eq!(super::decode_len(1), 0); + assert_eq!(super::decode_len(2), 1); + assert_eq!(super::decode_len(3), 1); + assert_eq!(super::decode_len(4), 2); + assert_eq!(super::decode_len(5), 3); assert_eq!(super::decode_len(32), 20); } } diff --git a/tvix/nix-compat/src/nixcpp/conf.rs b/tvix/nix-compat/src/nixcpp/conf.rs new file mode 100644 index 000000000000..68308115f988 --- /dev/null +++ b/tvix/nix-compat/src/nixcpp/conf.rs @@ -0,0 +1,202 @@ +use std::{fmt::Display, str::FromStr}; + +/// Represents configuration as stored in /etc/nix/nix.conf. +/// This list is not exhaustive, feel free to add more. +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct NixConfig<'a> { + pub allowed_users: Option<Vec<&'a str>>, + pub auto_optimise_store: Option<bool>, + pub cores: Option<u64>, + pub max_jobs: Option<u64>, + pub require_sigs: Option<bool>, + pub sandbox: Option<SandboxSetting>, + pub sandbox_fallback: Option<bool>, + pub substituters: Option<Vec<&'a str>>, + pub system_features: Option<Vec<&'a str>>, + pub trusted_public_keys: Option<Vec<crate::narinfo::VerifyingKey>>, + pub trusted_substituters: Option<Vec<&'a str>>, + pub trusted_users: Option<Vec<&'a str>>, + pub extra_platforms: Option<Vec<&'a str>>, + pub extra_sandbox_paths: Option<Vec<&'a str>>, + pub experimental_features: Option<Vec<&'a str>>, + pub builders_use_substitutes: Option<bool>, +} + +impl<'a> NixConfig<'a> { + /// Parses configuration from a file like `/etc/nix/nix.conf`, returning + /// a [NixConfig] with all values contained in there. + /// It does not support parsing multiple config files, merging semantics, + /// and also does not understand `include` and `!include` statements. + pub fn parse(input: &'a str) -> Result<Self, Error> { + let mut out = Self::default(); + + for line in input.lines() { + // strip comments at the end of the line + let line = if let Some((line, _comment)) = line.split_once('#') { + line + } else { + line + }; + + // skip comments and empty lines + if line.trim().is_empty() { + continue; + } + + let (tag, val) = line + .split_once('=') + .ok_or_else(|| Error::InvalidLine(line.to_string()))?; + + // trim whitespace + let tag = tag.trim(); + let val = val.trim(); + + #[inline] + fn parse_val<'a>(this: &mut NixConfig<'a>, tag: &str, val: &'a str) -> Option<()> { + match tag { + "allowed-users" => { + this.allowed_users = Some(val.split_whitespace().collect()); + } + "auto-optimise-store" => { + this.auto_optimise_store = Some(val.parse::<bool>().ok()?); + } + "cores" => { + this.cores = Some(val.parse().ok()?); + } + "max-jobs" => { + this.max_jobs = Some(val.parse().ok()?); + } + "require-sigs" => { + this.require_sigs = Some(val.parse().ok()?); + } + "sandbox" => this.sandbox = Some(val.parse().ok()?), + "sandbox-fallback" => this.sandbox_fallback = Some(val.parse().ok()?), + "substituters" => this.substituters = Some(val.split_whitespace().collect()), + "system-features" => { + this.system_features = Some(val.split_whitespace().collect()) + } + "trusted-public-keys" => { + this.trusted_public_keys = Some( + val.split_whitespace() + .map(crate::narinfo::VerifyingKey::parse) + .collect::<Result<Vec<crate::narinfo::VerifyingKey>, _>>() + .ok()?, + ) + } + "trusted-substituters" => { + this.trusted_substituters = Some(val.split_whitespace().collect()) + } + "trusted-users" => this.trusted_users = Some(val.split_whitespace().collect()), + "extra-platforms" => { + this.extra_platforms = Some(val.split_whitespace().collect()) + } + "extra-sandbox-paths" => { + this.extra_sandbox_paths = Some(val.split_whitespace().collect()) + } + "experimental-features" => { + this.experimental_features = Some(val.split_whitespace().collect()) + } + "builders-use-substitutes" => { + this.builders_use_substitutes = Some(val.parse().ok()?) + } + _ => return None, + } + Some(()) + } + + parse_val(&mut out, tag, val) + .ok_or_else(|| Error::InvalidValue(tag.to_string(), val.to_string()))? + } + + Ok(out) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("Invalid line: {0}")] + InvalidLine(String), + #[error("Unrecognized key: {0}")] + UnrecognizedKey(String), + #[error("Invalid value '{1}' for key '{0}'")] + InvalidValue(String, String), +} + +/// Valid values for the Nix 'sandbox' setting +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SandboxSetting { + True, + False, + Relaxed, +} + +impl Display for SandboxSetting { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SandboxSetting::True => write!(f, "true"), + SandboxSetting::False => write!(f, "false"), + SandboxSetting::Relaxed => write!(f, "relaxed"), + } + } +} + +impl FromStr for SandboxSetting { + type Err = &'static str; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(match s { + "true" => Self::True, + "false" => Self::False, + "relaxed" => Self::Relaxed, + _ => return Err("invalid value"), + }) + } +} + +#[cfg(test)] +mod tests { + use crate::{narinfo::VerifyingKey, nixcpp::conf::SandboxSetting}; + + use super::NixConfig; + + #[test] + pub fn test_parse() { + let config = NixConfig::parse(include_str!("../../testdata/nix.conf")).expect("must parse"); + + assert_eq!( + NixConfig { + allowed_users: Some(vec!["*"]), + auto_optimise_store: Some(false), + cores: Some(0), + max_jobs: Some(8), + require_sigs: Some(true), + sandbox: Some(SandboxSetting::True), + sandbox_fallback: Some(false), + substituters: Some(vec!["https://nix-community.cachix.org", "https://cache.nixos.org/"]), + system_features: Some(vec!["nixos-test", "benchmark", "big-parallel", "kvm"]), + trusted_public_keys: Some(vec![ + VerifyingKey::parse("cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=") + .expect("failed to parse pubkey"), + VerifyingKey::parse("nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=") + .expect("failed to parse pubkey") + ]), + trusted_substituters: Some(vec![]), + trusted_users: Some(vec!["flokli"]), + extra_platforms: Some(vec!["aarch64-linux", "i686-linux"]), + extra_sandbox_paths: Some(vec![ + "/run/binfmt", "/nix/store/swwyxyqpazzvbwx8bv40z7ih144q841f-qemu-aarch64-binfmt-P-x86_64-unknown-linux-musl" + ]), + experimental_features: Some(vec!["nix-command"]), + builders_use_substitutes: Some(true) + }, + config + ); + + // parse a config file using some non-space whitespaces, as well as comments right after the lines. + // ensure it contains the same data as initially parsed. + let other_config = NixConfig::parse(include_str!("../../testdata/other_nix.conf")) + .expect("other config must parse"); + + assert_eq!(config, other_config); + } +} diff --git a/tvix/nix-compat/src/nixcpp/mod.rs b/tvix/nix-compat/src/nixcpp/mod.rs new file mode 100644 index 000000000000..57518de8cc52 --- /dev/null +++ b/tvix/nix-compat/src/nixcpp/mod.rs @@ -0,0 +1,9 @@ +//! Contains code parsing some of the Nixcpp config files etc. +//! left by Nix *on the local disk*. +//! +//! This is only for Nix' own state/config. +//! +//! More "standardized" protocols, like parts of the Nix HTTP Binary Cache +//! protocol live elsewhere. + +pub mod conf; diff --git a/tvix/nix-compat/src/nixhash/ca_hash.rs b/tvix/nix-compat/src/nixhash/ca_hash.rs index 2bf5f966cefe..e6cbaf5b710a 100644 --- a/tvix/nix-compat/src/nixhash/ca_hash.rs +++ b/tvix/nix-compat/src/nixhash/ca_hash.rs @@ -47,12 +47,33 @@ impl CAHash { } } + /// Returns a colon-separated string consisting of mode, recursiveness and + /// hash algo. Used as a prefix in various string representations. + pub fn algo_str(&self) -> &'static str { + match self.mode() { + HashMode::Flat => match self.hash().as_ref() { + NixHash::Md5(_) => "fixed:md5", + NixHash::Sha1(_) => "fixed:sha1", + NixHash::Sha256(_) => "fixed:sha256", + NixHash::Sha512(_) => "fixed:sha512", + }, + HashMode::Nar => match self.hash().as_ref() { + NixHash::Md5(_) => "fixed:r:md5", + NixHash::Sha1(_) => "fixed:r:sha1", + NixHash::Sha256(_) => "fixed:r:sha256", + NixHash::Sha512(_) => "fixed:r:sha512", + }, + HashMode::Text => "text:sha256", + } + } + /// Constructs a [CAHash] from the textual representation, /// which is one of the three: /// - `text:sha256:$nixbase32sha256digest` /// - `fixed:r:$algo:$nixbase32digest` /// - `fixed:$algo:$nixbase32digest` - /// which is the format that's used in the NARInfo for example. + /// + /// These formats are used in NARInfo, for example. pub fn from_nix_hex_str(s: &str) -> Option<Self> { let (tag, s) = s.split_once(':')?; @@ -76,13 +97,11 @@ impl CAHash { /// Formats a [CAHash] in the Nix default hash format, which is the format /// that's used in NARInfos for example. pub fn to_nix_nixbase32_string(&self) -> String { - match self { - CAHash::Flat(nh) => format!("fixed:{}", nh.to_nix_nixbase32_string()), - CAHash::Nar(nh) => format!("fixed:r:{}", nh.to_nix_nixbase32_string()), - CAHash::Text(digest) => { - format!("text:sha256:{}", nixbase32::encode(digest)) - } - } + format!( + "{}:{}", + self.algo_str(), + nixbase32::encode(self.hash().digest_as_bytes()) + ) } /// This takes a serde_json::Map and turns it into this structure. This is necessary to do such @@ -90,11 +109,13 @@ impl CAHash { /// know whether we have a invalid or a missing NixHashWithMode structure in another structure, /// e.g. Output. /// This means we have this combinatorial situation: + /// /// - no hash, no hashAlgo: no [CAHash] so we return Ok(None). /// - present hash, missing hashAlgo: invalid, we will return missing_field /// - missing hash, present hashAlgo: same /// - present hash, present hashAlgo: either we return ourselves or a type/value validation - /// error. + /// error. + /// /// This function is for internal consumption regarding those needs until we have a better /// solution. Now this is said, let's explain how this works. /// diff --git a/tvix/nix-compat/src/store_path/mod.rs b/tvix/nix-compat/src/store_path/mod.rs index 707c41a92d74..177cc96ce20f 100644 --- a/tvix/nix-compat/src/store_path/mod.rs +++ b/tvix/nix-compat/src/store_path/mod.rs @@ -2,14 +2,15 @@ use crate::nixbase32; use data_encoding::{DecodeError, BASE64}; use serde::{Deserialize, Serialize}; use std::{ - fmt, - path::PathBuf, + fmt::{self, Display}, + ops::Deref, + path::Path, str::{self, FromStr}, }; use thiserror; #[cfg(target_family = "unix")] -use std::os::unix::ffi::OsStringExt; +use std::os::unix::ffi::OsStrExt; mod utils; @@ -54,17 +55,26 @@ pub enum Error { /// A [StorePath] does not encode any additional subpath "inside" the store /// path. #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct StorePath { +pub struct StorePath<S> +where + S: std::cmp::Eq + std::cmp::PartialEq, +{ digest: [u8; DIGEST_SIZE], - name: Box<str>, + name: S, } +/// Like [StorePath], but without a heap allocation for the name. +/// Used by [StorePath] for parsing. +pub type StorePathRef<'a> = StorePath<&'a str>; -impl StorePath { +impl<S> StorePath<S> +where + S: std::cmp::Eq + Deref<Target = str>, +{ pub fn digest(&self) -> &[u8; DIGEST_SIZE] { &self.digest } - pub fn name(&self) -> &str { + pub fn name(&self) -> &S { &self.name } @@ -74,60 +84,101 @@ impl StorePath { name: &self.name, } } -} -impl PartialOrd for StorePath { - fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { - Some(self.cmp(other)) + pub fn to_owned(&self) -> StorePath<String> { + StorePath { + digest: self.digest, + name: self.name.to_string(), + } } -} -/// `StorePath`s are sorted by their reverse digest to match the sorting order -/// of the nixbase32-encoded string. -impl Ord for StorePath { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.as_ref().cmp(&other.as_ref()) + /// Construct a [StorePath] by passing the `$digest-$name` string + /// that comes after [STORE_DIR_WITH_SLASH]. + pub fn from_bytes<'a>(s: &'a [u8]) -> Result<Self, Error> + where + S: From<&'a str>, + { + // the whole string needs to be at least: + // + // - 32 characters (encoded hash) + // - 1 dash + // - 1 character for the name + if s.len() < ENCODED_DIGEST_SIZE + 2 { + Err(Error::InvalidLength)? + } + + let digest = nixbase32::decode_fixed(&s[..ENCODED_DIGEST_SIZE])?; + + if s[ENCODED_DIGEST_SIZE] != b'-' { + return Err(Error::MissingDash); + } + + Ok(StorePath { + digest, + name: validate_name(&s[ENCODED_DIGEST_SIZE + 1..])?.into(), + }) } -} -impl FromStr for StorePath { - type Err = Error; + /// Construct a [StorePathRef] from a name and digest. + /// The name is validated, and the digest checked for size. + pub fn from_name_and_digest<'a>(name: &'a str, digest: &[u8]) -> Result<Self, Error> + where + S: From<&'a str>, + { + let digest_fixed = digest.try_into().map_err(|_| Error::InvalidLength)?; + Self::from_name_and_digest_fixed(name, digest_fixed) + } - /// Construct a [StorePath] by passing the `$digest-$name` string - /// that comes after [STORE_DIR_WITH_SLASH]. - fn from_str(s: &str) -> Result<Self, Self::Err> { - Self::from_bytes(s.as_bytes()) + /// Construct a [StorePathRef] from a name and digest of correct length. + /// The name is validated. + pub fn from_name_and_digest_fixed<'a>( + name: &'a str, + digest: [u8; DIGEST_SIZE], + ) -> Result<Self, Error> + where + S: From<&'a str>, + { + Ok(Self { + name: validate_name(name.as_bytes())?.into(), + digest, + }) } -} -impl StorePath { - /// Construct a [StorePath] by passing the `$digest-$name` string - /// that comes after [STORE_DIR_WITH_SLASH]. - pub fn from_bytes(s: &[u8]) -> Result<StorePath, Error> { - Ok(StorePathRef::from_bytes(s)?.to_owned()) + /// Construct a [StorePathRef] from an absolute store path string. + /// This is equivalent to calling [StorePathRef::from_bytes], but stripping + /// the [STORE_DIR_WITH_SLASH] prefix before. + pub fn from_absolute_path<'a>(s: &'a [u8]) -> Result<Self, Error> + where + S: From<&'a str>, + { + match s.strip_prefix(STORE_DIR_WITH_SLASH.as_bytes()) { + Some(s_stripped) => Self::from_bytes(s_stripped), + None => Err(Error::MissingStoreDir), + } } /// Decompose a string into a [StorePath] and a [PathBuf] containing the /// rest of the path, or an error. #[cfg(target_family = "unix")] - pub fn from_absolute_path_full(s: &str) -> Result<(StorePath, PathBuf), Error> { + pub fn from_absolute_path_full<'a>(s: &'a str) -> Result<(Self, &'a Path), Error> + where + S: From<&'a str>, + { // strip [STORE_DIR_WITH_SLASH] from s + match s.strip_prefix(STORE_DIR_WITH_SLASH) { None => Err(Error::MissingStoreDir), Some(rest) => { - // put rest in a PathBuf - let mut p = PathBuf::new(); - p.push(rest); - - let mut it = p.components(); + let mut it = Path::new(rest).components(); // The first component of the rest must be parse-able as a [StorePath] if let Some(first_component) = it.next() { // convert first component to StorePath - let first_component_bytes = first_component.as_os_str().to_owned().into_vec(); - let store_path = StorePath::from_bytes(&first_component_bytes)?; + let store_path = StorePath::from_bytes(first_component.as_os_str().as_bytes())?; + // collect rest - let rest_buf: PathBuf = it.collect(); + let rest_buf = it.as_path(); + Ok((store_path, rest_buf)) } else { Err(Error::InvalidLength) // Well, or missing "/"? @@ -139,139 +190,48 @@ impl StorePath { /// Returns an absolute store path string. /// That is just the string representation, prefixed with the store prefix /// ([STORE_DIR_WITH_SLASH]), - pub fn to_absolute_path(&self) -> String { - let sp_ref: StorePathRef = self.into(); - sp_ref.to_absolute_path() - } -} - -impl<'de> Deserialize<'de> for StorePath { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: serde::Deserializer<'de>, - { - let r = <StorePathRef<'de> as Deserialize<'de>>::deserialize(deserializer)?; - Ok(r.to_owned()) - } -} - -impl Serialize for StorePath { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + pub fn to_absolute_path(&self) -> String where - S: serde::Serializer, + S: Display, { - let r: StorePathRef = self.into(); - r.serialize(serializer) - } -} - -/// Like [StorePath], but without a heap allocation for the name. -/// Used by [StorePath] for parsing. -/// -#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)] -pub struct StorePathRef<'a> { - digest: [u8; DIGEST_SIZE], - name: &'a str, -} - -impl<'a> From<&'a StorePath> for StorePathRef<'a> { - fn from(&StorePath { digest, ref name }: &'a StorePath) -> Self { - StorePathRef { digest, name } + format!("{}{}", STORE_DIR_WITH_SLASH, self) } } -impl<'a> PartialOrd for StorePathRef<'a> { +impl<S> PartialOrd for StorePath<S> +where + S: std::cmp::PartialEq + std::cmp::Eq, +{ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { Some(self.cmp(other)) } } -/// `StorePathRef`s are sorted by their reverse digest to match the sorting order +/// `StorePath`s are sorted by their reverse digest to match the sorting order /// of the nixbase32-encoded string. -impl<'a> Ord for StorePathRef<'a> { +impl<S> Ord for StorePath<S> +where + S: std::cmp::PartialEq + std::cmp::Eq, +{ fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.digest.iter().rev().cmp(other.digest.iter().rev()) } } -impl<'a> StorePathRef<'a> { - pub fn digest(&self) -> &[u8; DIGEST_SIZE] { - &self.digest - } - - pub fn name(&self) -> &'a str { - self.name - } - - pub fn to_owned(&self) -> StorePath { - StorePath { - digest: self.digest, - name: self.name.into(), - } - } - - /// Construct a [StorePathRef] from a name and digest. - /// The name is validated, and the digest checked for size. - pub fn from_name_and_digest(name: &'a str, digest: &[u8]) -> Result<Self, Error> { - let digest_fixed = digest.try_into().map_err(|_| Error::InvalidLength)?; - Self::from_name_and_digest_fixed(name, digest_fixed) - } - - /// Construct a [StorePathRef] from a name and digest of correct length. - /// The name is validated. - pub fn from_name_and_digest_fixed( - name: &'a str, - digest: [u8; DIGEST_SIZE], - ) -> Result<Self, Error> { - Ok(Self { - name: validate_name(name.as_bytes())?, - digest, - }) - } - - /// Construct a [StorePathRef] from an absolute store path string. - /// This is equivalent to calling [StorePathRef::from_bytes], but stripping - /// the [STORE_DIR_WITH_SLASH] prefix before. - pub fn from_absolute_path(s: &'a [u8]) -> Result<Self, Error> { - match s.strip_prefix(STORE_DIR_WITH_SLASH.as_bytes()) { - Some(s_stripped) => Self::from_bytes(s_stripped), - None => Err(Error::MissingStoreDir), - } - } +impl<'a, 'b: 'a> FromStr for StorePath<String> { + type Err = Error; - /// Construct a [StorePathRef] by passing the `$digest-$name` string + /// Construct a [StorePath] by passing the `$digest-$name` string /// that comes after [STORE_DIR_WITH_SLASH]. - pub fn from_bytes(s: &'a [u8]) -> Result<Self, Error> { - // the whole string needs to be at least: - // - // - 32 characters (encoded hash) - // - 1 dash - // - 1 character for the name - if s.len() < ENCODED_DIGEST_SIZE + 2 { - Err(Error::InvalidLength)? - } - - let digest = nixbase32::decode_fixed(&s[..ENCODED_DIGEST_SIZE])?; - - if s[ENCODED_DIGEST_SIZE] != b'-' { - return Err(Error::MissingDash); - } - - Ok(StorePathRef { - digest, - name: validate_name(&s[ENCODED_DIGEST_SIZE + 1..])?, - }) - } - - /// Returns an absolute store path string. - /// That is just the string representation, prefixed with the store prefix - /// ([STORE_DIR_WITH_SLASH]), - pub fn to_absolute_path(&self) -> String { - format!("{}{}", STORE_DIR_WITH_SLASH, self) + fn from_str(s: &str) -> Result<Self, Self::Err> { + StorePath::<String>::from_bytes(s.as_bytes()) } } -impl<'de: 'a, 'a> Deserialize<'de> for StorePathRef<'a> { +impl<'a, 'de: 'a, S> Deserialize<'de> for StorePath<S> +where + S: std::cmp::Eq + Deref<Target = str> + From<&'a str>, +{ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, @@ -284,16 +244,19 @@ impl<'de: 'a, 'a> Deserialize<'de> for StorePathRef<'a> { &"store path prefix", ) })?; - StorePathRef::from_bytes(stripped.as_bytes()).map_err(|_| { + StorePath::from_bytes(stripped.as_bytes()).map_err(|_| { serde::de::Error::invalid_value(serde::de::Unexpected::Str(string), &"StorePath") }) } } -impl Serialize for StorePathRef<'_> { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> +impl<S> Serialize for StorePath<S> +where + S: std::cmp::Eq + Deref<Target = str> + Display, +{ + fn serialize<SR>(&self, serializer: SR) -> Result<SR::Ok, SR::Error> where - S: serde::Serializer, + SR: serde::Serializer, { let string: String = self.to_absolute_path(); string.serialize(serializer) @@ -347,13 +310,10 @@ pub(crate) fn validate_name(s: &(impl AsRef<[u8]> + ?Sized)) -> Result<&str, Err Ok(unsafe { str::from_utf8_unchecked(s) }) } -impl fmt::Display for StorePath { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - StorePathRef::from(self).fmt(f) - } -} - -impl fmt::Display for StorePathRef<'_> { +impl<S> fmt::Display for StorePath<S> +where + S: fmt::Display + std::cmp::Eq, +{ /// The string representation of a store path starts with a digest (20 /// bytes), [crate::nixbase32]-encoded, followed by a `-`, /// and ends with the name. @@ -386,12 +346,12 @@ mod tests { fn happy_path() { let example_nix_path_str = "00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432"; - let nixpath = StorePath::from_bytes(example_nix_path_str.as_bytes()) + let nixpath = StorePathRef::from_bytes(example_nix_path_str.as_bytes()) .expect("Error parsing example string"); let expected_digest: [u8; DIGEST_SIZE] = hex!("8a12321522fd91efbd60ebb2481af88580f61600"); - assert_eq!("net-tools-1.60_p20170221182432", nixpath.name()); + assert_eq!("net-tools-1.60_p20170221182432", *nixpath.name()); assert_eq!(nixpath.digest, expected_digest); assert_eq!(example_nix_path_str, nixpath.to_string()) @@ -426,8 +386,8 @@ mod tests { if w.len() < 2 { continue; } - let (pa, _) = StorePath::from_absolute_path_full(w[0]).expect("parseable"); - let (pb, _) = StorePath::from_absolute_path_full(w[1]).expect("parseable"); + let (pa, _) = StorePathRef::from_absolute_path_full(w[0]).expect("parseable"); + let (pb, _) = StorePathRef::from_absolute_path_full(w[1]).expect("parseable"); assert_eq!( Ordering::Less, pa.cmp(&pb), @@ -448,36 +408,38 @@ mod tests { /// https://github.com/NixOS/nix/pull/9867 (revert-of-revert) #[test] fn starts_with_dot() { - StorePath::from_bytes(b"fli4bwscgna7lpm7v5xgnjxrxh0yc7ra-.gitignore") + StorePathRef::from_bytes(b"fli4bwscgna7lpm7v5xgnjxrxh0yc7ra-.gitignore") .expect("must succeed"); } #[test] fn empty_name() { - StorePath::from_bytes(b"00bgd045z0d4icpbc2yy-").expect_err("must fail"); + StorePathRef::from_bytes(b"00bgd045z0d4icpbc2yy-").expect_err("must fail"); } #[test] fn excessive_length() { - StorePath::from_bytes(b"00bgd045z0d4icpbc2yy-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + StorePathRef::from_bytes(b"00bgd045z0d4icpbc2yy-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") .expect_err("must fail"); } #[test] fn invalid_hash_length() { - StorePath::from_bytes(b"00bgd045z0d4icpbc2yy-net-tools-1.60_p20170221182432") + StorePathRef::from_bytes(b"00bgd045z0d4icpbc2yy-net-tools-1.60_p20170221182432") .expect_err("must fail"); } #[test] fn invalid_encoding_hash() { - StorePath::from_bytes(b"00bgd045z0d4icpbc2yyz4gx48aku4la-net-tools-1.60_p20170221182432") - .expect_err("must fail"); + StorePathRef::from_bytes( + b"00bgd045z0d4icpbc2yyz4gx48aku4la-net-tools-1.60_p20170221182432", + ) + .expect_err("must fail"); } #[test] fn more_than_just_the_bare_nix_store_path() { - StorePath::from_bytes( + StorePathRef::from_bytes( b"00bgd045z0d4icpbc2yyz4gx48aku4la-net-tools-1.60_p20170221182432/bin/arp", ) .expect_err("must fail"); @@ -485,7 +447,7 @@ mod tests { #[test] fn no_dash_between_hash_and_name() { - StorePath::from_bytes(b"00bgd045z0d4icpbc2yyz4gx48ak44lanet-tools-1.60_p20170221182432") + StorePathRef::from_bytes(b"00bgd045z0d4icpbc2yyz4gx48ak44lanet-tools-1.60_p20170221182432") .expect_err("must fail"); } @@ -534,7 +496,7 @@ mod tests { #[test] fn serialize_owned() { - let nixpath_actual = StorePath::from_bytes( + let nixpath_actual = StorePathRef::from_bytes( b"00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432", ) .expect("can parse"); @@ -578,7 +540,8 @@ mod tests { let store_path_str_json = "\"/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432\""; - let store_path: StorePath = serde_json::from_str(store_path_str_json).expect("valid json"); + let store_path: StorePath<String> = + serde_json::from_str(store_path_str_json).expect("valid json"); assert_eq!( "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432", @@ -601,7 +564,7 @@ mod tests { StorePath::from_bytes(b"00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432").unwrap(), PathBuf::from("bin/arp/"))] fn from_absolute_path_full( #[case] s: &str, - #[case] exp_store_path: StorePath, + #[case] exp_store_path: StorePath<&str>, #[case] exp_path: PathBuf, ) { let (actual_store_path, actual_path) = @@ -615,15 +578,15 @@ mod tests { fn from_absolute_path_errors() { assert_eq!( Error::InvalidLength, - StorePath::from_absolute_path_full("/nix/store/").expect_err("must fail") + StorePathRef::from_absolute_path_full("/nix/store/").expect_err("must fail") ); assert_eq!( Error::InvalidLength, - StorePath::from_absolute_path_full("/nix/store/foo").expect_err("must fail") + StorePathRef::from_absolute_path_full("/nix/store/foo").expect_err("must fail") ); assert_eq!( Error::MissingStoreDir, - StorePath::from_absolute_path_full( + StorePathRef::from_absolute_path_full( "00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432" ) .expect_err("must fail") diff --git a/tvix/nix-compat/src/store_path/utils.rs b/tvix/nix-compat/src/store_path/utils.rs index d6f390db85c2..4bfbb72fcdde 100644 --- a/tvix/nix-compat/src/store_path/utils.rs +++ b/tvix/nix-compat/src/store_path/utils.rs @@ -1,6 +1,6 @@ use crate::nixbase32; use crate::nixhash::{CAHash, NixHash}; -use crate::store_path::{Error, StorePathRef, STORE_DIR}; +use crate::store_path::{Error, StorePath, StorePathRef, STORE_DIR}; use data_encoding::HEXLOWER; use sha2::{Digest, Sha256}; use thiserror; @@ -43,11 +43,17 @@ pub fn compress_hash<const OUTPUT_SIZE: usize>(input: &[u8]) -> [u8; OUTPUT_SIZE /// derivation or a literal text file that may contain references. /// If you don't want to have to pass the entire contents, you might want to use /// [build_ca_path] instead. -pub fn build_text_path<S: AsRef<str>, I: IntoIterator<Item = S>, C: AsRef<[u8]>>( - name: &str, +pub fn build_text_path<'a, S, SP, I, C>( + name: &'a str, content: C, references: I, -) -> Result<StorePathRef<'_>, BuildStorePathError> { +) -> Result<StorePath<SP>, BuildStorePathError> +where + S: AsRef<str>, + SP: std::cmp::Eq + std::ops::Deref<Target = str> + std::convert::From<&'a str>, + I: IntoIterator<Item = S>, + C: AsRef<[u8]>, +{ // produce the sha256 digest of the contents let content_digest = Sha256::new_with_prefix(content).finalize().into(); @@ -55,12 +61,17 @@ pub fn build_text_path<S: AsRef<str>, I: IntoIterator<Item = S>, C: AsRef<[u8]>> } /// This builds a store path from a [CAHash] and a list of references. -pub fn build_ca_path<'a, S: AsRef<str>, I: IntoIterator<Item = S>>( +pub fn build_ca_path<'a, S, SP, I>( name: &'a str, ca_hash: &CAHash, references: I, self_reference: bool, -) -> Result<StorePathRef<'a>, BuildStorePathError> { +) -> Result<StorePath<SP>, BuildStorePathError> +where + S: AsRef<str>, + SP: std::cmp::Eq + std::ops::Deref<Target = str> + std::convert::From<&'a str>, + I: IntoIterator<Item = S>, +{ // self references are only allowed for CAHash::Nar(NixHash::Sha256(_)). if self_reference && matches!(ca_hash, CAHash::Nar(NixHash::Sha256(_))) { return Err(BuildStorePathError::InvalidReference()); @@ -145,17 +156,20 @@ pub fn build_output_path<'a>( /// bytes. /// Inside a StorePath, that digest is printed nixbase32-encoded /// (32 characters). -fn build_store_path_from_fingerprint_parts<'a>( +fn build_store_path_from_fingerprint_parts<'a, S>( ty: &str, inner_digest: &[u8; 32], name: &'a str, -) -> Result<StorePathRef<'a>, Error> { +) -> Result<StorePath<S>, Error> +where + S: std::cmp::Eq + std::ops::Deref<Target = str> + std::convert::From<&'a str>, +{ let fingerprint = format!( "{ty}:sha256:{}:{STORE_DIR}:{name}", HEXLOWER.encode(inner_digest) ); // name validation happens in here. - StorePathRef::from_name_and_digest_fixed( + StorePath::from_name_and_digest_fixed( name, compress_hash(&Sha256::new_with_prefix(fingerprint).finalize()), ) @@ -216,7 +230,7 @@ mod test { // nix-repl> builtins.toFile "foo" "bar" // "/nix/store/vxjiwkjkn7x4079qvh1jkl5pn05j2aw0-foo" - let store_path = build_text_path("foo", "bar", Vec::<String>::new()) + let store_path: StorePathRef = build_text_path("foo", "bar", Vec::<String>::new()) .expect("build_store_path() should succeed"); assert_eq!( @@ -232,11 +246,11 @@ mod test { // nix-repl> builtins.toFile "baz" "${builtins.toFile "foo" "bar"}" // "/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz" - let inner = build_text_path("foo", "bar", Vec::<String>::new()) + let inner: StorePathRef = build_text_path("foo", "bar", Vec::<String>::new()) .expect("path_with_references() should succeed"); let inner_path = inner.to_absolute_path(); - let outer = build_text_path("baz", &inner_path, vec![inner_path.as_str()]) + let outer: StorePathRef = build_text_path("baz", &inner_path, vec![inner_path.as_str()]) .expect("path_with_references() should succeed"); assert_eq!( @@ -247,7 +261,7 @@ mod test { #[test] fn build_sha1_path() { - let outer = build_ca_path( + let outer: StorePathRef = build_ca_path( "bar", &CAHash::Nar(NixHash::Sha1(hex!( "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33" @@ -272,7 +286,7 @@ mod test { // // $ nix store make-content-addressed /nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz // rewrote '/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz' to '/nix/store/s89y431zzhmdn3k8r96rvakryddkpv2v-baz' - let outer = build_ca_path( + let outer: StorePathRef = build_ca_path( "baz", &CAHash::Nar(NixHash::Sha256( nixbase32::decode(b"1xqkzcb3909fp07qngljr4wcdnrh1gdam1m2n29i6hhrxlmkgkv1") diff --git a/tvix/nix-compat/src/wire/bytes/mod.rs b/tvix/nix-compat/src/wire/bytes/mod.rs index 2ed071e37985..74adfb49b6a4 100644 --- a/tvix/nix-compat/src/wire/bytes/mod.rs +++ b/tvix/nix-compat/src/wire/bytes/mod.rs @@ -1,9 +1,12 @@ +#[cfg(feature = "async")] +use std::mem::MaybeUninit; use std::{ io::{Error, ErrorKind}, - mem::MaybeUninit, ops::RangeInclusive, }; -use tokio::io::{self, AsyncReadExt, AsyncWriteExt, ReadBuf}; +#[cfg(feature = "async")] +use tokio::io::ReadBuf; +use tokio::io::{self, AsyncReadExt, AsyncWriteExt}; pub(crate) mod reader; pub use reader::BytesReader; @@ -11,7 +14,7 @@ mod writer; pub use writer::BytesWriter; /// 8 null bytes, used to write out padding. -const EMPTY_BYTES: &[u8; 8] = &[0u8; 8]; +pub(crate) const EMPTY_BYTES: &[u8; 8] = &[0u8; 8]; /// The length of the size field, in bytes is always 8. const LEN_SIZE: usize = 8; @@ -33,12 +36,9 @@ const LEN_SIZE: usize = 8; /// /// This buffers the entire payload into memory, /// a streaming version is available at [crate::wire::bytes::BytesReader]. -pub async fn read_bytes<R: ?Sized>( - r: &mut R, - allowed_size: RangeInclusive<usize>, -) -> io::Result<Vec<u8>> +pub async fn read_bytes<R>(r: &mut R, allowed_size: RangeInclusive<usize>) -> io::Result<Vec<u8>> where - R: AsyncReadExt + Unpin, + R: AsyncReadExt + Unpin + ?Sized, { // read the length field let len = r.read_u64_le().await?; @@ -82,13 +82,14 @@ where Ok(buf) } -pub(crate) async fn read_bytes_buf<'a, const N: usize, R: ?Sized>( +#[cfg(feature = "async")] +pub(crate) async fn read_bytes_buf<'a, const N: usize, R>( reader: &mut R, buf: &'a mut [MaybeUninit<u8>; N], allowed_size: RangeInclusive<usize>, ) -> io::Result<&'a [u8]> where - R: AsyncReadExt + Unpin, + R: AsyncReadExt + Unpin + ?Sized, { assert_eq!(N % 8, 0); assert!(*allowed_size.end() <= N); @@ -135,6 +136,7 @@ where } /// SAFETY: The bytes have to actually be initialized. +#[cfg(feature = "async")] unsafe fn assume_init_bytes(slice: &[MaybeUninit<u8>]) -> &[u8] { &*(slice as *const [MaybeUninit<u8>] as *const [u8]) } diff --git a/tvix/nix-compat/src/wire/bytes/reader/mod.rs b/tvix/nix-compat/src/wire/bytes/reader/mod.rs index 6bd376c06fb8..77950496ed6b 100644 --- a/tvix/nix-compat/src/wire/bytes/reader/mod.rs +++ b/tvix/nix-compat/src/wire/bytes/reader/mod.rs @@ -109,8 +109,6 @@ where } /// Remaining data length, ie not including data already read. - /// - /// If the size has not been read yet, this is [None]. pub fn len(&self) -> u64 { match self.state { State::Body { @@ -152,7 +150,7 @@ impl<R: AsyncRead + Unpin, T: Tag> AsyncRead for BytesReader<R, T> { let mut bytes_read = 0; ready!(with_limited(buf, remaining, |buf| { let ret = reader.poll_read(cx, buf); - bytes_read = buf.initialized().len(); + bytes_read = buf.filled().len(); ret }))?; @@ -414,6 +412,7 @@ mod tests { } /// Read the trailer immediately if there is no payload. + #[cfg(feature = "async")] #[tokio::test] async fn read_trailer_immediately() { use crate::nar::wire::PadPar; @@ -431,6 +430,7 @@ mod tests { } /// Read the trailer even if we only read the exact payload size. + #[cfg(feature = "async")] #[tokio::test] async fn read_exact_trailer() { use crate::nar::wire::PadPar; diff --git a/tvix/nix-compat/testdata/nix.conf b/tvix/nix-compat/testdata/nix.conf new file mode 100644 index 000000000000..1fa089053eb6 --- /dev/null +++ b/tvix/nix-compat/testdata/nix.conf @@ -0,0 +1,20 @@ +# WARNING: this file is generated from the nix.* options in +# your NixOS configuration, typically +# /etc/nixos/configuration.nix. Do not edit it! +allowed-users = * +auto-optimise-store = false +cores = 0 +max-jobs = 8 +require-sigs = true +sandbox = true +sandbox-fallback = false +substituters = https://nix-community.cachix.org https://cache.nixos.org/ +system-features = nixos-test benchmark big-parallel kvm +trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs= +trusted-substituters = +trusted-users = flokli +extra-platforms = aarch64-linux i686-linux +extra-sandbox-paths = /run/binfmt /nix/store/swwyxyqpazzvbwx8bv40z7ih144q841f-qemu-aarch64-binfmt-P-x86_64-unknown-linux-musl +experimental-features = nix-command + +builders-use-substitutes = true diff --git a/tvix/nix-compat/testdata/other_nix.conf b/tvix/nix-compat/testdata/other_nix.conf new file mode 100644 index 000000000000..63c4ea2ec78e --- /dev/null +++ b/tvix/nix-compat/testdata/other_nix.conf @@ -0,0 +1,18 @@ +# This file contains some more comments in various places. +allowed-users = * +auto-optimise-store = false +cores = 0 +max-jobs = 8 +require-sigs = true +sandbox=true +sandbox-fallback = false +substituters = https://nix-community.cachix.org https://cache.nixos.org/ #comment # stillcomment +system-features = nixos-test benchmark big-parallel kvm +trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs= +trusted-substituters = +trusted-users = flokli +extra-platforms = aarch64-linux i686-linux +extra-sandbox-paths = /run/binfmt /nix/store/swwyxyqpazzvbwx8bv40z7ih144q841f-qemu-aarch64-binfmt-P-x86_64-unknown-linux-musl +experimental-features = nix-command + +builders-use-substitutes = true |