1use 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 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 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 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.build_state.blob_service,
94 dir_entries,
95 path,
96 None, );
98 ingest_entries(&state.build_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::{
116 StorePath, StorePathRef, build_ca_path, build_text_path_from_content_digest,
117 };
118 use sha2::Digest;
119 use snix_castore::blobservice::BlobService;
120 use snix_eval::builtins::coerce_value_to_path;
121 use snix_eval::generators::Gen;
122 use snix_eval::{AddContext, FileType, NixContext, NixContextElement, NixString};
123 use snix_eval::{ErrorKind, Value, generators::GenCo};
124 use snix_store::path_info::PathInfo;
125 use std::rc::Rc;
126 use std::sync::Arc;
127 use tokio::io::AsyncWriteExt;
128
129 fn copy_to_blobservice<F>(
135 tokio_handle: tokio::runtime::Handle,
136 blob_service: impl BlobService,
137 mut r: impl std::io::Read,
138 mut inspect_f: F,
139 ) -> std::io::Result<(snix_castore::B3Digest, u64)>
140 where
141 F: FnMut(&[u8]),
142 {
143 let mut blob_size = 0;
144
145 let mut blob_writer = tokio_handle.block_on(async { blob_service.open_write().await });
146
147 {
150 let mut buf = [0u8; 4096];
151
152 loop {
153 let len = r.read(&mut buf)?;
155 if len == 0 {
156 break;
157 }
158 blob_size += len as u64;
159
160 let data = &buf[0..len];
161
162 tokio_handle.block_on(async { blob_writer.write_all(data).await })?;
164
165 inspect_f(data);
167 }
168
169 let blob_digest = tokio_handle.block_on(async { blob_writer.close().await })?;
170
171 Ok((blob_digest, blob_size))
172 }
173 }
174
175 async fn import_helper(
177 state: Rc<SnixStoreIO>,
178 co: GenCo,
179 path: std::path::PathBuf,
180 name: Option<&Value>,
181 filter: Option<&Value>,
182 recursive_ingestion: bool,
183 expected_sha256: Option<[u8; 32]>,
184 ) -> Result<Value, ErrorKind> {
185 let name: String = match name {
187 Some(name) => {
188 let nix_str = generators::request_force(&co, name.clone())
189 .await
190 .to_str()?;
191
192 nix_compat::store_path::validate_name(&nix_str)
193 .map_err(|err| {
194 ErrorKind::SnixError(Arc::new(
195 nix_compat::store_path::ParseStorePathError::from(err),
196 ))
197 })?
198 .to_owned()
199 }
200 None => {
201 let file_name = path.file_name().ok_or_else(|| {
202 std::io::Error::new(
203 std::io::ErrorKind::InvalidFilename,
204 "path without basename encountered",
205 )
206 })?;
207 nix_compat::store_path::validate_name_from_os_str(file_name)
208 .map_err(|err| ErrorKind::SnixError(Arc::new(err)))?
209 .to_owned()
210 }
211 };
212 let (root_node, ca) = match std::fs::metadata(&path)?.file_type().into() {
215 FileType::Regular => {
220 let mut file = state.open(&path)?;
221 let mut h = (!recursive_ingestion).then(sha2::Sha256::new);
222
223 let (blob_digest, blob_size) = copy_to_blobservice(
224 state.tokio_handle.clone(),
225 &state.build_state.blob_service,
226 &mut file,
227 |data| {
228 if let Some(h) = h.as_mut() {
230 h.update(data)
231 }
232 },
233 )?;
234
235 (
236 Node::File {
237 digest: blob_digest,
238 size: blob_size,
239 executable: false,
240 },
241 h.map(|h| {
242 let actual_sha256 = h.finalize().into();
244
245 if let Some(expected_sha256) = expected_sha256
247 && actual_sha256 != expected_sha256
248 {
249 return Err(ImportError::HashMismatch(
250 path.clone(),
251 NixHash::Sha256(expected_sha256),
252 NixHash::Sha256(actual_sha256),
253 ));
254 }
255 Ok(CAHash::Flat(NixHash::Sha256(actual_sha256)))
256 })
257 .transpose()?,
258 )
259 }
260
261 FileType::Directory if !recursive_ingestion => {
262 return Err(ImportError::FlatImportOfNonFile(path))?;
263 }
264
265 FileType::Directory => (
267 filtered_ingest(state.clone(), co, path.as_ref(), filter).await?,
268 None,
269 ),
270 FileType::Symlink => {
271 return Err(snix_eval::ErrorKind::IO {
274 path: Some(path),
275 error: Rc::new(std::io::Error::new(
276 std::io::ErrorKind::Unsupported,
277 "builtins.path pointing to a symlink is ill-defined.",
278 )),
279 });
280 }
281 FileType::Unknown => {
282 return Err(snix_eval::ErrorKind::IO {
283 path: Some(path),
284 error: Rc::new(std::io::Error::new(
285 std::io::ErrorKind::Unsupported,
286 "unsupported file type",
287 )),
288 });
289 }
290 };
291
292 let (nar_size, nar_sha256) = state
294 .tokio_handle
295 .block_on(async {
296 state
297 .build_state
298 .nar_calculation_service
299 .as_ref()
300 .calculate_nar(&root_node)
301 .await
302 })
303 .map_err(|e| snix_eval::ErrorKind::SnixError(Arc::from(e)))?;
304
305 let ca = match ca {
308 None => {
309 if let Some(expected_nar_sha256) = expected_sha256
311 && expected_nar_sha256 != nar_sha256
312 {
313 return Err(ImportError::HashMismatch(
314 path,
315 NixHash::Sha256(expected_nar_sha256),
316 NixHash::Sha256(nar_sha256),
317 )
318 .into());
319 }
320 CAHash::Nar(NixHash::Sha256(nar_sha256))
321 }
322 Some(ca) => ca,
323 };
324
325 let store_path = build_ca_path(&name, recursive_ingestion, &ca.hash(), [], false)
326 .map_err(|e| snix_eval::ErrorKind::SnixError(Arc::from(e)))?
327 .to_owned();
328
329 let path_info = state
330 .tokio_handle
331 .block_on(async {
332 state
333 .build_state
334 .path_info_service
335 .as_ref()
336 .put(PathInfo {
337 store_path,
338 node: root_node,
339 references: vec![],
341 nar_size,
342 nar_sha256,
343 signatures: vec![],
344 deriver: None,
345 ca: Some(ca),
346 })
347 .await
348 })
349 .map_err(|e| snix_eval::ErrorKind::IO {
350 path: Some(path),
351 error: Rc::new(std::io::Error::other(e)),
352 })?;
353
354 let outpath = path_info.store_path.to_absolute_path();
356
357 Ok(
358 NixString::new_context_from(NixContextElement::Plain(outpath.clone()).into(), outpath)
359 .into(),
360 )
361 }
362
363 #[builtin("path")]
364 async fn builtin_path(
365 state: Rc<SnixStoreIO>,
366 co: GenCo,
367 args: Value,
368 ) -> Result<Value, ErrorKind> {
369 let args = args.to_attrs()?;
370
371 let path = match coerce_value_to_path(
372 &co,
373 generators::request_force(&co, args.select_required("path")?.clone()).await,
374 )
375 .await?
376 {
377 Ok(path) => path,
378 Err(cek) => return Ok(cek.into()),
379 };
380
381 let filter = args.select("filter");
382
383 let recursive_ingestion = args
385 .select("recursive")
386 .map(|r| r.as_bool())
387 .transpose()?
388 .unwrap_or(true); let expected_sha256 = args
391 .select("sha256")
392 .map(|h| {
393 h.to_str().and_then(|expected| {
394 match NixHash::from_str(expected.to_str()?, Some(HashAlgo::Sha256)) {
395 Ok(NixHash::Sha256(digest)) => Ok(digest),
396 Ok(_) => unreachable!(),
397 Err(e) => Err(ErrorKind::InvalidHash(e.to_string())),
398 }
399 })
400 })
401 .transpose()?;
402
403 import_helper(
404 state,
405 co,
406 path,
407 args.select("name"),
408 filter,
409 recursive_ingestion,
410 expected_sha256,
411 )
412 .await
413 }
414
415 #[builtin("filterSource")]
416 async fn builtin_filter_source(
417 state: Rc<SnixStoreIO>,
418 co: GenCo,
419 #[lazy] filter: Value,
420 path: Value,
421 ) -> Result<Value, ErrorKind> {
422 let path =
423 match coerce_value_to_path(&co, generators::request_force(&co, path).await).await? {
424 Ok(path) => path,
425 Err(cek) => return Ok(cek.into()),
426 };
427
428 import_helper(state, co, path, None, Some(&filter), true, None).await
429 }
430
431 #[builtin("storePath")]
432 async fn builtin_store_path(
433 state: Rc<SnixStoreIO>,
434 co: GenCo,
435 path: Value,
436 ) -> Result<Value, ErrorKind> {
437 let p = match &path {
438 Value::String(s) => Path::new(s.as_bytes().to_os_str()?),
439 Value::Path(p) => p.as_path(),
440 _ => {
441 return Err(ErrorKind::TypeError {
442 expected: "string or path",
443 actual: path.type_of(),
444 });
445 }
446 };
447
448 let (store_path, _sub_path) = StorePathRef::from_absolute_path_full(p)
450 .map_err(|_e| ImportError::PathNotAbsoluteOrInvalid(p.to_path_buf()))?;
451
452 if state.path_exists(p)? {
453 Ok(Value::String(NixString::new_context_from(
454 [NixContextElement::Plain(store_path.to_absolute_path())].into(),
455 p.as_os_str().as_encoded_bytes(),
456 )))
457 } else {
458 Err(ErrorKind::IO {
459 path: Some(p.to_path_buf()),
460 error: Rc::new(std::io::ErrorKind::NotFound.into()),
461 })
462 }
463 }
464
465 #[builtin("toFile")]
466 async fn builtin_to_file(
467 state: Rc<SnixStoreIO>,
468 co: GenCo,
469 name: Value,
470 content: Value,
471 ) -> Result<Value, ErrorKind> {
472 if name.is_catchable() {
473 return Ok(name);
474 }
475
476 if content.is_catchable() {
477 return Ok(content);
478 }
479
480 let name = name
481 .to_str()
482 .context("evaluating the `name` parameter of builtins.toFile")?;
483 let content = content
484 .to_contextful_str()
485 .context("evaluating the `content` parameter of builtins.toFile")?;
486
487 if content.iter_ctx_derivation().count() > 0
488 || content.iter_ctx_single_outputs().count() > 0
489 {
490 return Err(ErrorKind::UnexpectedContext);
491 }
492
493 let mut h = sha2::Sha256::new();
495 let (blob_digest, blob_size) = copy_to_blobservice(
496 state.tokio_handle.clone(),
497 &state.build_state.blob_service,
498 std::io::Cursor::new(&content),
499 |data| h.update(data),
500 )?;
501
502 let root_node = Node::File {
503 digest: blob_digest,
504 size: blob_size,
505 executable: false,
506 };
507
508 let (nar_size, nar_sha256) = state
510 .tokio_handle
511 .block_on(
512 state
513 .build_state
514 .nar_calculation_service
515 .calculate_nar(&root_node),
516 )
517 .map_err(|e| ErrorKind::SnixError(Arc::from(e)))?;
518
519 let content_digest: [u8; 32] = h.finalize().into();
520 let references = content.iter_ctx_plain().map(|sp| {
521 StorePathRef::from_absolute_path(sp.as_bytes())
522 .expect("Snix bug: must parse as store path")
523 });
524
525 let store_path = state
527 .tokio_handle
528 .block_on(
529 state.build_state.path_info_service.put(PathInfo {
530 store_path: build_text_path_from_content_digest(
531 name.to_str()?,
532 content_digest,
533 references,
534 )
535 .map_err(|_e| {
536 nix_compat::derivation::DerivationError::InvalidOutputs(
537 nix_compat::derivation::outputs::OutputsError::InvalidOutputName(
538 name.to_str_lossy().into_owned(),
539 ),
540 )
541 })
542 .map_err(crate::builtins::DerivationError::InvalidDerivation)?
543 .to_owned(),
544 node: root_node,
545 references: content
547 .iter_ctx_plain()
548 .map(|elem| StorePath::from_absolute_path(elem.as_bytes()))
549 .collect::<Result<_, _>>()
550 .map_err(|e| ErrorKind::SnixError(Arc::from(e)))?,
551 nar_size,
552 nar_sha256,
553 signatures: vec![],
554 deriver: None,
555 ca: Some(CAHash::Text(content_digest)),
556 }),
557 )
558 .map_err(|e| ErrorKind::SnixError(Arc::from(e)))
559 .map(|path_info| path_info.store_path)?;
560
561 let abs_path = store_path.to_absolute_path();
562 let context: NixContext = NixContextElement::Plain(abs_path.clone()).into();
563
564 Ok(Value::from(NixString::new_context_from(context, abs_path)))
565 }
566}
567
568pub use import_builtins::builtins as import_builtins;