use bstr::ByteSlice; use std::path::Path; use tracing::{debug, instrument}; use tvix_castore::{ blobservice::BlobService, directoryservice::DirectoryService, import::fs::ingest_path, Node, }; use nix_compat::{ nixhash::{CAHash, NixHash}, store_path::{self, StorePath}, }; use crate::{ nar::NarCalculationService, pathinfoservice::{PathInfo, PathInfoService}, proto::nar_info, }; impl From for nar_info::Ca { fn from(value: CAHash) -> Self { let hash_type: nar_info::ca::Hash = (&value).into(); let digest: bytes::Bytes = value.hash().to_string().into(); nar_info::Ca { r#type: hash_type.into(), digest, } } } pub fn log_node(name: &[u8], node: &Node, path: &Path) { match node { Node::Directory { digest, .. } => { debug!( path = ?path, name = %name.as_bstr(), digest = %digest, "import successful", ) } Node::File { digest, .. } => { debug!( path = ?path, name = %name.as_bstr(), digest = %digest, "import successful" ) } Node::Symlink { target } => { debug!( path = ?path, name = %name.as_bstr(), target = ?target, "import successful" ) } } } /// Transform a path into its base name and returns an [`std::io::Error`] if it is `..` or if the /// basename is not valid unicode. #[inline] pub fn path_to_name(path: &Path) -> std::io::Result<&str> { path.file_name() .and_then(|file_name| file_name.to_str()) .ok_or_else(|| { std::io::Error::new( std::io::ErrorKind::InvalidInput, "path must not be .. and the basename valid unicode", ) }) } /// Ingest the contents at the given path `path` into castore, and registers the /// resulting root node in the passed PathInfoService, using the "NAR sha256 /// digest" and the passed name for output path calculation. /// Inserts the PathInfo into the PathInfoService and returns it back to the caller. #[instrument(skip_all, fields(store_name=name, path=?path), err)] pub async fn import_path_as_nar_ca( path: P, name: &str, blob_service: BS, directory_service: DS, path_info_service: PS, nar_calculation_service: NS, ) -> Result where P: AsRef + std::fmt::Debug, BS: BlobService + Clone, DS: DirectoryService, PS: AsRef, NS: NarCalculationService, { // Ingest the contents at the given path `path` into castore. let root_node = ingest_path::<_, _, _, &[u8]>(blob_service, directory_service, path.as_ref(), None) .await .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; // Ask for the NAR size and sha256 let (nar_size, nar_sha256) = nar_calculation_service.calculate_nar(&root_node).await?; let ca = CAHash::Nar(NixHash::Sha256(nar_sha256)); // Calculate the output path. This might still fail, as some names are illegal. // FUTUREWORK: express the `name` at the type level to be valid and move the conversion // at the caller level. let output_path: StorePath = store_path::build_ca_path(name, &ca, std::iter::empty::<&str>(), false).map_err(|_| { std::io::Error::new( std::io::ErrorKind::InvalidData, format!("invalid name: {}", name), ) })?; // Insert a PathInfo. On success, return it back to the caller. Ok(path_info_service .as_ref() .put(PathInfo { store_path: output_path.to_owned(), node: root_node, // There's no reference scanning on imported paths references: vec![], nar_size, nar_sha256, signatures: vec![], deriver: None, ca: Some(ca), }) .await?) } #[cfg(test)] mod tests { use std::{ffi::OsStr, path::PathBuf}; use crate::import::path_to_name; use rstest::rstest; #[rstest] #[case::simple_path("a/b/c", "c")] #[case::simple_path_containing_dotdot("a/b/../c", "c")] #[case::path_containing_multiple_dotdot("a/b/../c/d/../e", "e")] fn test_path_to_name(#[case] path: &str, #[case] expected_name: &str) { let path: PathBuf = path.into(); assert_eq!(path_to_name(&path).expect("must succeed"), expected_name); } #[rstest] #[case::path_ending_in_dotdot(b"a/b/..")] #[case::non_unicode_path(b"\xf8\xa1\xa1\xa1\xa1")] fn test_invalid_path_to_name(#[case] invalid_path: &[u8]) { let path: PathBuf = unsafe { OsStr::from_encoded_bytes_unchecked(invalid_path) }.into(); path_to_name(&path).expect_err("must fail"); } }