snix_store/
import.rs

1use bstr::ByteSlice;
2use snix_castore::{
3    Node, blobservice::BlobService, directoryservice::DirectoryService, import::fs::ingest_path,
4};
5use std::path::Path;
6use tracing::{debug, instrument};
7
8use nix_compat::{
9    nixhash::{CAHash, NixHash},
10    store_path::{self, StorePath},
11};
12
13use crate::{
14    nar::NarCalculationService,
15    pathinfoservice::{PathInfo, PathInfoService},
16    proto::nar_info,
17};
18
19impl From<CAHash> for nar_info::Ca {
20    fn from(value: CAHash) -> Self {
21        let hash_type: nar_info::ca::Hash = (&value).into();
22        let digest: bytes::Bytes = value.hash().to_string().into();
23        nar_info::Ca {
24            r#type: hash_type.into(),
25            digest,
26        }
27    }
28}
29
30pub fn log_node(name: &[u8], node: &Node, path: &Path) {
31    match node {
32        Node::Directory { digest, .. } => {
33            debug!(
34                path = ?path,
35                name = %name.as_bstr(),
36                digest = %digest,
37                "import successful",
38            )
39        }
40        Node::File { digest, .. } => {
41            debug!(
42                path = ?path,
43                name = %name.as_bstr(),
44                digest = %digest,
45                "import successful"
46            )
47        }
48        Node::Symlink { target } => {
49            debug!(
50                path = ?path,
51                name = %name.as_bstr(),
52                target = ?target,
53                "import successful"
54            )
55        }
56    }
57}
58
59/// Transform a path into its base name and returns an [`std::io::Error`] if it is `..` or if the
60/// basename is not valid unicode.
61#[inline]
62pub fn path_to_name(path: &Path) -> std::io::Result<&str> {
63    path.file_name()
64        .and_then(|file_name| file_name.to_str())
65        .ok_or_else(|| {
66            std::io::Error::new(
67                std::io::ErrorKind::InvalidInput,
68                "path must not be .. and the basename valid unicode",
69            )
70        })
71}
72
73/// Ingest the contents at the given path `path` into castore, and registers the
74/// resulting root node in the passed PathInfoService, using the "NAR sha256
75/// digest" and the passed name for output path calculation.
76/// Inserts the PathInfo into the PathInfoService and returns it back to the caller.
77#[instrument(skip_all, fields(store_name=name, path=?path), err)]
78pub async fn import_path_as_nar_ca<BS, DS, PS, NS, P>(
79    path: P,
80    name: &str,
81    blob_service: BS,
82    directory_service: DS,
83    path_info_service: PS,
84    nar_calculation_service: NS,
85) -> Result<PathInfo, std::io::Error>
86where
87    P: AsRef<Path> + std::fmt::Debug,
88    BS: BlobService + Clone,
89    DS: DirectoryService,
90    PS: AsRef<dyn PathInfoService>,
91    NS: NarCalculationService,
92{
93    // Ingest the contents at the given path `path` into castore.
94    let root_node =
95        ingest_path::<_, _, _, &[u8]>(blob_service, directory_service, path.as_ref(), None)
96            .await
97            .map_err(std::io::Error::other)?;
98
99    // Ask for the NAR size and sha256
100    let (nar_size, nar_sha256) = nar_calculation_service
101        .calculate_nar(&root_node)
102        .await
103        .map_err(std::io::Error::other)?;
104
105    let ca = CAHash::Nar(NixHash::Sha256(nar_sha256));
106
107    // Calculate the output path. This might still fail, as some names are illegal.
108    // FUTUREWORK: express the `name` at the type level to be valid and move the conversion
109    // at the caller level.
110    let output_path: StorePath<String> =
111        store_path::build_ca_path(name, &ca, std::iter::empty::<&str>(), false).map_err(|_| {
112            std::io::Error::new(
113                std::io::ErrorKind::InvalidData,
114                format!("invalid name: {name}"),
115            )
116        })?;
117
118    // Insert a PathInfo. On success, return it back to the caller.
119    path_info_service
120        .as_ref()
121        .put(PathInfo {
122            store_path: output_path.to_owned(),
123            node: root_node,
124            // There's no reference scanning on imported paths
125            references: vec![],
126            nar_size,
127            nar_sha256,
128            signatures: vec![],
129            deriver: None,
130            ca: Some(ca),
131        })
132        .await
133        .map_err(std::io::Error::other)
134}
135
136#[cfg(test)]
137mod tests {
138    use std::{ffi::OsStr, os::unix::ffi::OsStrExt, path::PathBuf};
139
140    use crate::import::path_to_name;
141    use rstest::rstest;
142
143    #[rstest]
144    #[case::simple_path("a/b/c", "c")]
145    #[case::simple_path_containing_dotdot("a/b/../c", "c")]
146    #[case::path_containing_multiple_dotdot("a/b/../c/d/../e", "e")]
147
148    fn test_path_to_name(#[case] path: &str, #[case] expected_name: &str) {
149        let path: PathBuf = path.into();
150        assert_eq!(path_to_name(&path).expect("must succeed"), expected_name);
151    }
152
153    #[rstest]
154    #[case::path_ending_in_dotdot(b"a/b/..")]
155    #[case::non_unicode_path(b"\xf8\xa1\xa1\xa1\xa1")]
156    fn test_invalid_path_to_name(#[case] invalid_path: &[u8]) {
157        let path: PathBuf = OsStr::from_bytes(invalid_path).into();
158        path_to_name(&path).expect_err("must fail");
159    }
160}