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.blob_service,
94 dir_entries,
95 path,
96 None, );
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 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 {
148 let mut buf = [0u8; 4096];
149
150 loop {
151 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 tokio_handle.block_on(async { blob_writer.write_all(data).await })?;
162
163 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 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 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 let (root_node, ca) = match std::fs::metadata(&path)?.file_type().into() {
210 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 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 let actual_sha256 = h.finalize().into();
239
240 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 FileType::Directory => (
262 filtered_ingest(state.clone(), co, path.as_ref(), filter).await?,
263 None,
264 ),
265 FileType::Symlink => {
266 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 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 let ca = match ca {
302 None => {
303 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 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 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 let recursive_ingestion = args
377 .select("recursive")
378 .map(|r| r.as_bool())
379 .transpose()?
380 .unwrap_or(true); 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 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 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 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 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 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;