Skip to main content

snix_glue/builtins/
import.rs

1//! Implements builtins used to import paths in the store.
2
3use crate::snix_store_io::SnixStoreIO;
4use snix_castore::Node;
5use snix_castore::import::ingest_entries;
6use snix_eval::{
7    ErrorKind, EvalIO, Value,
8    builtin_macros::builtins,
9    generators::{self, GenCo},
10};
11use std::path::Path;
12
13use std::rc::Rc;
14
15async fn filtered_ingest(
16    state: Rc<SnixStoreIO>,
17    co: GenCo,
18    path: &Path,
19    filter: Option<&Value>,
20) -> Result<Node, ErrorKind> {
21    let mut entries: Vec<walkdir::DirEntry> = vec![];
22    let mut it = walkdir::WalkDir::new(path)
23        .follow_links(false)
24        .follow_root_links(false)
25        .contents_first(false)
26        .into_iter();
27
28    // Skip root node.
29    entries.push(
30        it.next()
31            .ok_or_else(|| ErrorKind::IO {
32                path: Some(path.to_path_buf()),
33                error: std::io::Error::new(std::io::ErrorKind::NotFound, "No root node emitted")
34                    .into(),
35            })?
36            .map_err(|err| ErrorKind::IO {
37                path: Some(path.to_path_buf()),
38                error: std::io::Error::from(err).into(),
39            })?,
40    );
41
42    while let Some(entry) = it.next() {
43        // Entry could be a NotFound, if the root path specified does not exist.
44        let entry = entry.map_err(|err| ErrorKind::IO {
45            path: err.path().map(|p| p.to_path_buf()),
46            error: std::io::Error::from(err).into(),
47        })?;
48
49        // As per Nix documentation `:doc builtins.filterSource`.
50        let file_type = if entry.file_type().is_dir() {
51            "directory"
52        } else if entry.file_type().is_file() {
53            "regular"
54        } else if entry.file_type().is_symlink() {
55            "symlink"
56        } else {
57            "unknown"
58        };
59
60        let should_keep: bool = if let Some(filter) = filter {
61            generators::request_force(
62                &co,
63                generators::request_call_with(
64                    &co,
65                    filter.clone(),
66                    [
67                        Value::String(entry.path().as_os_str().as_encoded_bytes().into()),
68                        Value::String(file_type.into()),
69                    ],
70                )
71                .await,
72            )
73            .await
74            .as_bool()?
75        } else {
76            true
77        };
78
79        if !should_keep {
80            if file_type == "directory" {
81                it.skip_current_dir();
82            }
83            continue;
84        }
85
86        entries.push(entry);
87    }
88
89    let dir_entries = entries.into_iter().rev().map(Ok);
90
91    state.tokio_handle.block_on(async {
92        let entries = snix_castore::import::fs::dir_entries_to_ingestion_stream::<'_, _, _, &[u8]>(
93            &state.blob_service,
94            dir_entries,
95            path,
96            None, // TODO re-scan
97        );
98        ingest_entries(&state.directory_service, entries)
99            .await
100            .map_err(|e| ErrorKind::IO {
101                path: Some(path.to_path_buf()),
102                error: Rc::new(std::io::Error::other(e)),
103            })
104    })
105}
106
107#[builtins(state = "Rc<SnixStoreIO>")]
108mod import_builtins {
109    use super::*;
110
111    use crate::builtins::ImportError;
112    use crate::snix_store_io::SnixStoreIO;
113    use bstr::ByteSlice;
114    use nix_compat::nixhash::{CAHash, HashAlgo, NixHash};
115    use nix_compat::store_path::{StorePath, StorePathRef, build_ca_path};
116    use sha2::Digest;
117    use snix_castore::blobservice::BlobService;
118    use snix_eval::builtins::coerce_value_to_path;
119    use snix_eval::generators::Gen;
120    use snix_eval::{AddContext, FileType, NixContext, NixContextElement, NixString};
121    use snix_eval::{ErrorKind, Value, generators::GenCo};
122    use snix_store::path_info::PathInfo;
123    use std::rc::Rc;
124    use std::sync::Arc;
125    use tokio::io::AsyncWriteExt;
126
127    /// Helper function dealing with uploading something from a std::io::Read to
128    /// the passed [BlobService], returning the B3Digest and size.
129    /// This function is sync (and uses the tokio handle to block).
130    /// A sync closure getting a copy of all bytes read can be passed in,
131    /// allowing to do other hashing where needed.
132    fn copy_to_blobservice<F>(
133        tokio_handle: tokio::runtime::Handle,
134        blob_service: impl BlobService,
135        mut r: impl std::io::Read,
136        mut inspect_f: F,
137    ) -> std::io::Result<(snix_castore::B3Digest, u64)>
138    where
139        F: FnMut(&[u8]),
140    {
141        let mut blob_size = 0;
142
143        let mut blob_writer = tokio_handle.block_on(async { blob_service.open_write().await });
144
145        // read piece by piece and write to blob_writer.
146        // This is a bit manual due to EvalIO being sync, while the blob writer being async.
147        {
148            let mut buf = [0u8; 4096];
149
150            loop {
151                // read bytes into buffer, break out if EOF
152                let len = r.read(&mut buf)?;
153                if len == 0 {
154                    break;
155                }
156                blob_size += len as u64;
157
158                let data = &buf[0..len];
159
160                // write to blobwriter
161                tokio_handle.block_on(async { blob_writer.write_all(data).await })?;
162
163                // Call inspect_f
164                inspect_f(data);
165            }
166
167            let blob_digest = tokio_handle.block_on(async { blob_writer.close().await })?;
168
169            Ok((blob_digest, blob_size))
170        }
171    }
172
173    // This is a helper used by both builtins.path and builtins.filterSource.
174    async fn import_helper(
175        state: Rc<SnixStoreIO>,
176        co: GenCo,
177        path: std::path::PathBuf,
178        name: Option<&Value>,
179        filter: Option<&Value>,
180        recursive_ingestion: bool,
181        expected_sha256: Option<[u8; 32]>,
182    ) -> Result<Value, ErrorKind> {
183        // Determine the name, either chosen by the user or derived from the path.
184        let name: String = match name {
185            Some(name) => {
186                let nix_str = generators::request_force(&co, name.clone())
187                    .await
188                    .to_str()?;
189
190                nix_compat::store_path::validate_name(&nix_str)
191                    .map_err(|err| {
192                        ErrorKind::SnixError(Arc::new(nix_compat::store_path::Error::from(err)))
193                    })?
194                    .to_owned()
195            }
196            None => nix_compat::store_path::validate_name_as_os_str(path.file_name().ok_or_else(
197                || {
198                    std::io::Error::new(
199                        std::io::ErrorKind::InvalidFilename,
200                        "path without basename encountered",
201                    )
202                },
203            )?)
204            .map_err(|err| ErrorKind::SnixError(Arc::new(err)))?
205            .to_owned(),
206        };
207        // As a first step, we ingest the contents, and get back a root node,
208        // and optionally the sha256 a flat file.
209        let (root_node, ca) = match std::fs::metadata(&path)?.file_type().into() {
210            // Check if the path points to a regular file.
211            // If it does, the filter function is never executed, and we copy to the blobservice directly.
212            // If recursive is false, we need to calculate the sha256 digest of the raw contents,
213            // as that affects the output path calculation.
214            FileType::Regular => {
215                let mut file = state.open(&path)?;
216                let mut h = (!recursive_ingestion).then(sha2::Sha256::new);
217
218                let (blob_digest, blob_size) = copy_to_blobservice(
219                    state.tokio_handle.clone(),
220                    &state.blob_service,
221                    &mut file,
222                    |data| {
223                        // update blob_sha256 if needed.
224                        if let Some(h) = h.as_mut() {
225                            h.update(data)
226                        }
227                    },
228                )?;
229
230                (
231                    Node::File {
232                        digest: blob_digest,
233                        size: blob_size,
234                        executable: false,
235                    },
236                    h.map(|h| {
237                        // If non-recursive ingestion was requested, we return that one.
238                        let actual_sha256 = h.finalize().into();
239
240                        // If an expected hash was provided upfront, compare and bail out.
241                        if let Some(expected_sha256) = expected_sha256
242                            && actual_sha256 != expected_sha256
243                        {
244                            return Err(ImportError::HashMismatch(
245                                path.clone(),
246                                NixHash::Sha256(expected_sha256),
247                                NixHash::Sha256(actual_sha256),
248                            ));
249                        }
250                        Ok(CAHash::Flat(NixHash::Sha256(actual_sha256)))
251                    })
252                    .transpose()?,
253                )
254            }
255
256            FileType::Directory if !recursive_ingestion => {
257                return Err(ImportError::FlatImportOfNonFile(path))?;
258            }
259
260            // do the filtered ingest
261            FileType::Directory => (
262                filtered_ingest(state.clone(), co, path.as_ref(), filter).await?,
263                None,
264            ),
265            FileType::Symlink => {
266                // FUTUREWORK: Nix follows a symlink if it's at the root,
267                // except if it's not resolve-able (NixOS/nix#7761).
268                return Err(snix_eval::ErrorKind::IO {
269                    path: Some(path),
270                    error: Rc::new(std::io::Error::new(
271                        std::io::ErrorKind::Unsupported,
272                        "builtins.path pointing to a symlink is ill-defined.",
273                    )),
274                });
275            }
276            FileType::Unknown => {
277                return Err(snix_eval::ErrorKind::IO {
278                    path: Some(path),
279                    error: Rc::new(std::io::Error::new(
280                        std::io::ErrorKind::Unsupported,
281                        "unsupported file type",
282                    )),
283                });
284            }
285        };
286
287        // Calculate the NAR sha256.
288        let (nar_size, nar_sha256) = state
289            .tokio_handle
290            .block_on(async {
291                state
292                    .nar_calculation_service
293                    .as_ref()
294                    .calculate_nar(&root_node)
295                    .await
296            })
297            .map_err(|e| snix_eval::ErrorKind::SnixError(Arc::from(e)))?;
298
299        // Calculate the CA hash for the recursive cases, this is only already
300        // `Some(_)` for flat ingestion.
301        let ca = match ca {
302            None => {
303                // If an upfront-expected NAR hash was specified, compare.
304                if let Some(expected_nar_sha256) = expected_sha256
305                    && expected_nar_sha256 != nar_sha256
306                {
307                    return Err(ImportError::HashMismatch(
308                        path,
309                        NixHash::Sha256(expected_nar_sha256),
310                        NixHash::Sha256(nar_sha256),
311                    )
312                    .into());
313                }
314                CAHash::Nar(NixHash::Sha256(nar_sha256))
315            }
316            Some(ca) => ca,
317        };
318
319        let store_path = build_ca_path(&name, &ca, [], false)
320            .map_err(|e| snix_eval::ErrorKind::SnixError(Arc::from(e)))?;
321
322        let path_info = state
323            .tokio_handle
324            .block_on(async {
325                state
326                    .path_info_service
327                    .as_ref()
328                    .put(PathInfo {
329                        store_path,
330                        node: root_node,
331                        // There's no reference scanning on path contents ingested like this.
332                        references: vec![],
333                        nar_size,
334                        nar_sha256,
335                        signatures: vec![],
336                        deriver: None,
337                        ca: Some(ca),
338                    })
339                    .await
340            })
341            .map_err(|e| snix_eval::ErrorKind::IO {
342                path: Some(path),
343                error: Rc::new(std::io::Error::other(e)),
344            })?;
345
346        // We need to attach context to the final output path.
347        let outpath = path_info.store_path.to_absolute_path();
348
349        Ok(
350            NixString::new_context_from(NixContextElement::Plain(outpath.clone()).into(), outpath)
351                .into(),
352        )
353    }
354
355    #[builtin("path")]
356    async fn builtin_path(
357        state: Rc<SnixStoreIO>,
358        co: GenCo,
359        args: Value,
360    ) -> Result<Value, ErrorKind> {
361        let args = args.to_attrs()?;
362
363        let path = match coerce_value_to_path(
364            &co,
365            generators::request_force(&co, args.select_required("path")?.clone()).await,
366        )
367        .await?
368        {
369            Ok(path) => path,
370            Err(cek) => return Ok(cek.into()),
371        };
372
373        let filter = args.select("filter");
374
375        // Construct a sha256 hasher, which is needed for flat ingestion.
376        let recursive_ingestion = args
377            .select("recursive")
378            .map(|r| r.as_bool())
379            .transpose()?
380            .unwrap_or(true); // Yes, yes, Nix, by default, puts `recursive = true;`.
381
382        let expected_sha256 = args
383            .select("sha256")
384            .map(|h| {
385                h.to_str().and_then(|expected| {
386                    match NixHash::from_str(expected.to_str()?, Some(HashAlgo::Sha256)) {
387                        Ok(NixHash::Sha256(digest)) => Ok(digest),
388                        Ok(_) => unreachable!(),
389                        Err(e) => Err(ErrorKind::InvalidHash(e.to_string())),
390                    }
391                })
392            })
393            .transpose()?;
394
395        import_helper(
396            state,
397            co,
398            path,
399            args.select("name"),
400            filter,
401            recursive_ingestion,
402            expected_sha256,
403        )
404        .await
405    }
406
407    #[builtin("filterSource")]
408    async fn builtin_filter_source(
409        state: Rc<SnixStoreIO>,
410        co: GenCo,
411        #[lazy] filter: Value,
412        path: Value,
413    ) -> Result<Value, ErrorKind> {
414        let path =
415            match coerce_value_to_path(&co, generators::request_force(&co, path).await).await? {
416                Ok(path) => path,
417                Err(cek) => return Ok(cek.into()),
418            };
419
420        import_helper(state, co, path, None, Some(&filter), true, None).await
421    }
422
423    #[builtin("storePath")]
424    async fn builtin_store_path(
425        state: Rc<SnixStoreIO>,
426        co: GenCo,
427        path: Value,
428    ) -> Result<Value, ErrorKind> {
429        let p = match &path {
430            Value::String(s) => Path::new(s.as_bytes().to_os_str()?),
431            Value::Path(p) => p.as_path(),
432            _ => {
433                return Err(ErrorKind::TypeError {
434                    expected: "string or path",
435                    actual: path.type_of(),
436                });
437            }
438        };
439
440        // For this builtin, the path needs to start with an absolute store path.
441        let (store_path, _sub_path) = StorePathRef::from_absolute_path_full(p)
442            .map_err(|_e| ImportError::PathNotAbsoluteOrInvalid(p.to_path_buf()))?;
443
444        if state.path_exists(p)? {
445            Ok(Value::String(NixString::new_context_from(
446                [NixContextElement::Plain(store_path.to_absolute_path())].into(),
447                p.as_os_str().as_encoded_bytes(),
448            )))
449        } else {
450            Err(ErrorKind::IO {
451                path: Some(p.to_path_buf()),
452                error: Rc::new(std::io::ErrorKind::NotFound.into()),
453            })
454        }
455    }
456
457    #[builtin("toFile")]
458    async fn builtin_to_file(
459        state: Rc<SnixStoreIO>,
460        co: GenCo,
461        name: Value,
462        content: Value,
463    ) -> Result<Value, ErrorKind> {
464        if name.is_catchable() {
465            return Ok(name);
466        }
467
468        if content.is_catchable() {
469            return Ok(content);
470        }
471
472        let name = name
473            .to_str()
474            .context("evaluating the `name` parameter of builtins.toFile")?;
475        let content = content
476            .to_contextful_str()
477            .context("evaluating the `content` parameter of builtins.toFile")?;
478
479        if content.iter_ctx_derivation().count() > 0
480            || content.iter_ctx_single_outputs().count() > 0
481        {
482            return Err(ErrorKind::UnexpectedContext);
483        }
484
485        // upload contents to the blobservice and create a root node
486        let mut h = sha2::Sha256::new();
487        let (blob_digest, blob_size) = copy_to_blobservice(
488            state.tokio_handle.clone(),
489            &state.blob_service,
490            std::io::Cursor::new(&content),
491            |data| h.update(data),
492        )?;
493
494        let root_node = Node::File {
495            digest: blob_digest,
496            size: blob_size,
497            executable: false,
498        };
499
500        // calculate the nar hash
501        let (nar_size, nar_sha256) = state
502            .nar_calculation_service
503            .calculate_nar(&root_node)
504            .await
505            .map_err(|e| ErrorKind::SnixError(Arc::from(e)))?;
506
507        let ca_hash = CAHash::Text(h.finalize().into());
508
509        // persist via pathinfo service.
510        let store_path = state
511            .tokio_handle
512            .block_on(
513                state.path_info_service.put(PathInfo {
514                    store_path: build_ca_path(
515                        name.to_str()?,
516                        &ca_hash,
517                        content.iter_ctx_plain().map(|sp| {
518                            StorePathRef::from_absolute_path(sp.as_bytes())
519                                .expect("Snix bug: must parse as store path")
520                        }),
521                        false,
522                    )
523                    .map_err(|_e| {
524                        nix_compat::derivation::DerivationError::InvalidOutputName(
525                            name.to_str_lossy().into_owned(),
526                        )
527                    })
528                    .map_err(crate::builtins::DerivationError::InvalidDerivation)?,
529                    node: root_node,
530                    // assemble references from plain context.
531                    references: content
532                        .iter_ctx_plain()
533                        .map(|elem| StorePath::from_absolute_path(elem.as_bytes()))
534                        .collect::<Result<_, _>>()
535                        .map_err(|e| ErrorKind::SnixError(Arc::from(e)))?,
536                    nar_size,
537                    nar_sha256,
538                    signatures: vec![],
539                    deriver: None,
540                    ca: Some(ca_hash),
541                }),
542            )
543            .map_err(|e| ErrorKind::SnixError(Arc::from(e)))
544            .map(|path_info| path_info.store_path)?;
545
546        let abs_path = store_path.to_absolute_path();
547        let context: NixContext = NixContextElement::Plain(abs_path.clone()).into();
548
549        Ok(Value::from(NixString::new_context_from(context, abs_path)))
550    }
551}
552
553pub use import_builtins::builtins as import_builtins;