1use std::{
8 collections::HashMap,
9 fs::Permissions,
10 os::unix::fs::PermissionsExt,
11 path::{Path, PathBuf},
12};
13
14use async_recursion::async_recursion;
15use tokio::fs;
16
17use crate::{config::PathPair, vm::VIRTIOFS_TAG_PREFIX, MicrosandboxResult};
18
19pub const OPAQUE_WHITEOUT_MARKER: &str = ".wh..wh..opq";
25
26pub const WHITEOUT_PREFIX: &str = ".wh.";
28
29struct PermissionGuard {
35 path: PathBuf,
36 original_mode: u32,
37}
38
39impl PermissionGuard {
40 fn new(path: impl AsRef<Path>, mode_to_add: u32) -> MicrosandboxResult<Self> {
42 let path = path.as_ref().to_path_buf();
43 let metadata = std::fs::metadata(&path)?;
44 let original_mode = metadata.permissions().mode();
45
46 let mut perms = metadata.permissions();
48 perms.set_mode(original_mode | mode_to_add);
49 std::fs::set_permissions(&path, perms)?;
50
51 Ok(Self {
52 path,
53 original_mode,
54 })
55 }
56}
57
58impl Drop for PermissionGuard {
59 fn drop(&mut self) {
60 if let Ok(mut perms) = std::fs::metadata(&self.path).and_then(|m| Ok(m.permissions())) {
62 perms.set_mode(self.original_mode);
63 let _ = fs::set_permissions(&self.path, perms);
64 }
65 }
66}
67
68pub async fn patch_with_sandbox_scripts(
87 scripts_dir: &Path,
88 scripts: &HashMap<String, String>,
89 shell_path: impl AsRef<Path>,
90) -> MicrosandboxResult<()> {
91 if scripts_dir.exists() {
93 fs::remove_dir_all(&scripts_dir).await?;
94 }
95
96 fs::create_dir_all(&scripts_dir).await?;
98
99 let shell_path = shell_path.as_ref().to_string_lossy();
101 for (script_name, script_content) in scripts.iter() {
102 let script_path = scripts_dir.join(script_name);
104
105 let full_content = format!("#!{}\n{}\n", shell_path, script_content);
107 fs::write(&script_path, full_content).await?;
108
109 fs::set_permissions(&script_path, Permissions::from_mode(0o750)).await?;
111 }
112
113 let shell_script_path = scripts_dir.join("shell");
115 fs::write(&shell_script_path, shell_path.to_string()).await?;
116 fs::set_permissions(&shell_script_path, Permissions::from_mode(0o750)).await?;
117
118 Ok(())
119}
120
121pub async fn patch_with_virtiofs_mounts(
147 root_path: &Path,
148 mapped_dirs: &[PathPair],
149) -> MicrosandboxResult<()> {
150 let fstab_path = root_path.join("etc/fstab");
151
152 if let Some(parent) = fstab_path.parent() {
154 fs::create_dir_all(parent).await?;
155 }
156
157 let mut fstab_content = if fstab_path.exists() {
159 fs::read_to_string(&fstab_path).await?
160 } else {
161 String::new()
162 };
163
164 if fstab_content.is_empty() {
166 fstab_content.push_str(
167 "# /etc/fstab: static file system information.\n\
168 # <file system>\t<mount point>\t<type>\t<options>\t<dump>\t<pass>\n",
169 );
170 }
171
172 for (idx, dir) in mapped_dirs.iter().enumerate() {
174 let tag = format!("{}_{}", VIRTIOFS_TAG_PREFIX, idx);
175 tracing::debug!("adding virtiofs mount for {}", tag);
176 let guest_path = dir.get_guest();
177
178 fstab_content.push_str(&format!(
180 "{}\t{}\tvirtiofs\tdefaults\t0\t0\n",
181 tag, guest_path
182 ));
183
184 let guest_path_str = guest_path.as_str();
187 let relative_path = guest_path_str.strip_prefix('/').unwrap_or(guest_path_str);
188 let mount_point = root_path.join(relative_path);
189 fs::create_dir_all(mount_point).await?;
190 }
191
192 fs::write(&fstab_path, fstab_content).await?;
194
195 let perms = fs::metadata(&fstab_path).await?.permissions();
197 let mut new_perms = perms;
198 new_perms.set_mode(0o644);
199 fs::set_permissions(&fstab_path, new_perms).await?;
200
201 Ok(())
202}
203
204async fn _patch_with_hostnames(
229 root_path: &Path,
230 hostname_mappings: &[(std::net::Ipv4Addr, String)],
231) -> MicrosandboxResult<()> {
232 let hosts_path = root_path.join("etc/hosts");
233
234 if let Some(parent) = hosts_path.parent() {
236 fs::create_dir_all(parent).await?;
237 }
238
239 let mut hosts_content = if hosts_path.exists() {
241 fs::read_to_string(&hosts_path).await?
242 } else {
243 String::new()
244 };
245
246 if hosts_content.is_empty() {
248 hosts_content.push_str(
249 "# /etc/hosts: static table lookup for hostnames.\n\
250 # <ip-address>\t<hostname>\n\n\
251 127.0.0.1\tlocalhost\n\
252 ::1\tlocalhost ip6-localhost ip6-loopback\n",
253 );
254 }
255
256 for (ip_addr, hostname) in hostname_mappings {
258 let entry = format!("{}\t{}", ip_addr, hostname);
260 if !hosts_content.contains(&entry) {
261 hosts_content.push_str(&format!("{}\n", entry));
262 }
263 }
264
265 fs::write(&hosts_path, hosts_content).await?;
267
268 let perms = fs::metadata(&hosts_path).await?.permissions();
270 let mut new_perms = perms;
271 new_perms.set_mode(0o644);
272 fs::set_permissions(&hosts_path, new_perms).await?;
273
274 Ok(())
275}
276
277pub async fn patch_with_default_dns_settings(root_paths: &[PathBuf]) -> MicrosandboxResult<()> {
304 if root_paths.is_empty() {
305 return Ok(());
306 }
307
308 let mut has_nameserver = false;
310 for root_path in root_paths {
311 let resolv_path = root_path.join("etc/resolv.conf");
312 if resolv_path.exists() {
313 let content = fs::read_to_string(&resolv_path).await?;
314 if content
315 .lines()
316 .any(|line| line.trim_start().starts_with("nameserver "))
317 {
318 has_nameserver = true;
319 break;
320 }
321 }
322 }
323
324 if !has_nameserver {
326 let top_layer = root_paths.last().unwrap();
328 let resolv_path = top_layer.join("etc/resolv.conf");
329
330 if let Some(parent) = resolv_path.parent() {
332 fs::create_dir_all(parent).await?;
333 }
334
335 let mut resolv_content = String::from("# /etc/resolv.conf: DNS resolver configuration\n");
337 resolv_content.push_str("nameserver 1.1.1.1\n");
338 resolv_content.push_str("nameserver 8.8.8.8\n");
339
340 fs::write(&resolv_path, resolv_content).await?;
342
343 let perms = fs::metadata(&resolv_path).await?.permissions();
345 let mut new_perms = perms;
346 new_perms.set_mode(0o644);
347 fs::set_permissions(&resolv_path, new_perms).await?;
348 }
349
350 Ok(())
351}
352
353#[async_recursion(?Send)]
372pub async fn copy_dir_recursive(src_dir: &Path, dst_dir: &Path) -> MicrosandboxResult<()> {
373 if !src_dir.exists() {
376 return Err(crate::MicrosandboxError::PathNotFound(format!(
377 "source directory does not exist: {}",
378 src_dir.display()
379 )));
380 }
381
382 if !dst_dir.exists() {
384 fs::create_dir_all(dst_dir).await?;
385 }
386
387 let src_metadata = fs::metadata(src_dir).await?;
389 let src_perms = src_metadata.permissions();
390 fs::set_permissions(dst_dir, src_perms.clone()).await?;
391
392 let _src_guard = PermissionGuard::new(src_dir, 0o500)?;
395
396 let mut entries = fs::read_dir(src_dir).await?;
398
399 while let Some(entry) = entries.next_entry().await? {
400 let src_path = entry.path();
401 let dst_path = dst_dir.join(entry.file_name());
402
403 let file_type = entry.file_type().await?;
404
405 if file_type.is_dir() {
406 copy_dir_recursive(&src_path, &dst_path).await?;
408 } else if file_type.is_file() {
409 copy_file_with_permissions(&src_path, &dst_path).await?;
411 } else if file_type.is_symlink() {
412 let target = fs::read_link(&src_path).await?;
414 fs::symlink(target, &dst_path).await?;
415 }
416 }
417
418 Ok(())
419}
420
421async fn copy_file_with_permissions(
439 src_file: impl AsRef<Path>,
440 dst_file: impl AsRef<Path>,
441) -> MicrosandboxResult<()> {
442 let src_file = src_file.as_ref();
443 let dst_file = dst_file.as_ref();
444
445 let _src_guard = PermissionGuard::new(src_file, 0o400)?;
447
448 fs::copy(src_file, dst_file).await?;
450
451 let src_metadata = fs::metadata(src_file).await?;
453 let src_perms = src_metadata.permissions();
454 fs::set_permissions(dst_file, src_perms).await?;
455
456 Ok(())
457}
458
459pub async fn patch_with_stat_override(root_path: &Path) -> MicrosandboxResult<()> {
472 let xattr_name = "user.containers.override_stat";
474
475 let xattr_value = "0:0:0555";
477
478 let path_str = root_path.to_str().ok_or_else(|| {
480 crate::MicrosandboxError::InvalidArgument(format!(
481 "Could not convert path to string: {}",
482 root_path.display()
483 ))
484 })?;
485
486 match xattr::set(path_str, xattr_name, xattr_value.as_bytes()) {
488 Ok(_) => {
489 tracing::debug!(
490 "Set xattr {} = {} on {}",
491 xattr_name,
492 xattr_value,
493 root_path.display()
494 );
495 Ok(())
496 }
497 Err(err) => Err(crate::MicrosandboxError::Io(std::io::Error::new(
498 std::io::ErrorKind::Other,
499 format!("Failed to set xattr on {}: {}", root_path.display(), err),
500 ))),
501 }
502}
503
504#[cfg(test)]
509mod tests {
510 use std::path::PathBuf;
511
512 use tempfile::TempDir;
513
514 use crate::MicrosandboxError;
515
516 use super::*;
517
518 #[tokio::test]
519 async fn test_patch_rootfs_with_virtiofs_mounts() -> anyhow::Result<()> {
520 let root_dir = TempDir::new()?;
522 let root_path = root_dir.path();
523
524 let host_dir = TempDir::new()?;
526 let host_data = host_dir.path().join("data");
527 let host_config = host_dir.path().join("config");
528 let host_app = host_dir.path().join("app");
529
530 fs::create_dir_all(&host_data).await?;
532 fs::create_dir_all(&host_config).await?;
533 fs::create_dir_all(&host_app).await?;
534
535 let mapped_dirs = vec![
537 format!("{}:/container/data", host_data.display()).parse::<PathPair>()?,
538 format!("{}:/etc/app/config", host_config.display()).parse::<PathPair>()?,
539 format!("{}:/app", host_app.display()).parse::<PathPair>()?,
540 ];
541
542 patch_with_virtiofs_mounts(root_path, &mapped_dirs).await?;
544
545 let fstab_path = root_path.join("etc/fstab");
547 assert!(fstab_path.exists());
548
549 let fstab_content = fs::read_to_string(&fstab_path).await?;
550
551 assert!(fstab_content.contains("# /etc/fstab: static file system information"));
553 assert!(fstab_content
554 .contains("<file system>\t<mount point>\t<type>\t<options>\t<dump>\t<pass>"));
555
556 assert!(fstab_content.contains("virtiofs_0\t/container/data\tvirtiofs\tdefaults\t0\t0"));
558 assert!(fstab_content.contains("virtiofs_1\t/etc/app/config\tvirtiofs\tdefaults\t0\t0"));
559 assert!(fstab_content.contains("virtiofs_2\t/app\tvirtiofs\tdefaults\t0\t0"));
560
561 assert!(root_path.join("container/data").exists());
563 assert!(root_path.join("etc/app/config").exists());
564 assert!(root_path.join("app").exists());
565
566 let perms = fs::metadata(&fstab_path).await?.permissions();
568 assert_eq!(perms.mode() & 0o777, 0o644);
569
570 let host_logs = host_dir.path().join("logs");
572 fs::create_dir_all(&host_logs).await?;
573
574 let new_mapped_dirs = vec![
575 format!("{}:/container/data", host_data.display()).parse::<PathPair>()?, format!("{}:/var/log", host_logs.display()).parse::<PathPair>()?, ];
578
579 patch_with_virtiofs_mounts(root_path, &new_mapped_dirs).await?;
581
582 let updated_content = fs::read_to_string(&fstab_path).await?;
584 assert!(updated_content.contains("virtiofs_0\t/container/data\tvirtiofs\tdefaults\t0\t0"));
585 assert!(updated_content.contains("virtiofs_1\t/var/log\tvirtiofs\tdefaults\t0\t0"));
586
587 assert!(root_path.join("var/log").exists());
589
590 Ok(())
591 }
592
593 #[tokio::test]
594 async fn test_patch_rootfs_with_virtiofs_mounts_permission_errors() -> anyhow::Result<()> {
595 if std::env::var("CI").is_ok() {
597 println!("Skipping permission test in CI environment");
598 return Ok(());
599 }
600
601 let readonly_dir = TempDir::new()?;
603 let readonly_path = readonly_dir.path();
604 let etc_path = readonly_path.join("etc");
605 fs::create_dir_all(&etc_path).await?;
606
607 let mut perms = fs::metadata(&etc_path).await?.permissions();
609 perms.set_mode(0o400); fs::set_permissions(&etc_path, perms).await?;
611
612 let actual_perms = fs::metadata(&etc_path).await?.permissions();
614 println!("Set /etc permissions to: {:o}", actual_perms.mode());
615
616 let host_dir = TempDir::new()?;
618 let host_path = host_dir.path().join("test");
619 fs::create_dir_all(&host_path).await?;
620
621 let mapped_dirs =
622 vec![format!("{}:/container/data", host_path.display()).parse::<PathPair>()?];
623
624 let result = patch_with_virtiofs_mounts(readonly_path, &mapped_dirs).await;
626
627 if result.is_ok() {
629 println!("Warning: Write succeeded despite read-only permissions");
630 println!(
631 "Current /etc permissions: {:o}",
632 fs::metadata(&etc_path).await?.permissions().mode()
633 );
634 if etc_path.join("fstab").exists() {
635 println!(
636 "fstab file was created with permissions: {:o}",
637 fs::metadata(etc_path.join("fstab"))
638 .await?
639 .permissions()
640 .mode()
641 );
642 }
643 }
644
645 assert!(
646 result.is_err(),
647 "Expected error when writing fstab to read-only /etc directory. \
648 Current /etc permissions: {:o}",
649 fs::metadata(&etc_path).await?.permissions().mode()
650 );
651 assert!(matches!(result.unwrap_err(), MicrosandboxError::Io(_)));
652
653 Ok(())
654 }
655
656 #[tokio::test]
657 async fn test_patch_with_hostnames() -> anyhow::Result<()> {
658 use std::net::Ipv4Addr;
659
660 let root_dir = TempDir::new()?;
662 let root_path = root_dir.path();
663
664 let hostname_mappings = vec![
666 (Ipv4Addr::new(192, 168, 1, 100), "host1.local".to_string()),
667 (Ipv4Addr::new(192, 168, 1, 101), "host2.local".to_string()),
668 ];
669
670 _patch_with_hostnames(root_path, &hostname_mappings).await?;
672
673 let hosts_path = root_path.join("etc/hosts");
675 assert!(hosts_path.exists());
676
677 let hosts_content = fs::read_to_string(&hosts_path).await?;
678
679 assert!(hosts_content.contains("# /etc/hosts: static table lookup for hostnames"));
681 assert!(hosts_content.contains("127.0.0.1\tlocalhost"));
682 assert!(hosts_content.contains("::1\tlocalhost ip6-localhost ip6-loopback"));
683
684 assert!(hosts_content.contains("192.168.1.100\thost1.local"));
686 assert!(hosts_content.contains("192.168.1.101\thost2.local"));
687
688 let perms = fs::metadata(&hosts_path).await?.permissions();
690 assert_eq!(perms.mode() & 0o777, 0o644);
691
692 let new_mappings = vec![
694 (Ipv4Addr::new(192, 168, 1, 100), "host1.local".to_string()), (Ipv4Addr::new(192, 168, 1, 102), "host3.local".to_string()), ];
697
698 _patch_with_hostnames(root_path, &new_mappings).await?;
700
701 let updated_content = fs::read_to_string(&hosts_path).await?;
703
704 assert!(updated_content.contains("127.0.0.1\tlocalhost"));
706 assert!(updated_content.contains("::1\tlocalhost ip6-localhost ip6-loopback"));
707
708 assert!(updated_content.contains("192.168.1.100\thost1.local"));
710 assert!(updated_content.contains("192.168.1.102\thost3.local"));
711
712 let count = updated_content
714 .lines()
715 .filter(|line| line.contains("192.168.1.100"))
716 .count();
717 assert_eq!(count, 1, "Should not have duplicate entries");
718
719 Ok(())
720 }
721
722 #[tokio::test]
723 async fn test_copy_dir_complex_permissions() -> anyhow::Result<()> {
724 if std::env::var("CI").is_ok() {
726 println!("Skipping permission test in CI environment");
727 return Ok(());
728 }
729
730 let src_root = TempDir::new()?;
732 let dst_root = TempDir::new()?;
733
734 let src_path = src_root.path();
735 let dst_path = dst_root.path();
736
737 let noaccess_dir = src_path.join("noaccess");
751 let hidden_dir = noaccess_dir.join("hidden");
752 let hidden_file = hidden_dir.join("file");
753
754 let readonly_dir = src_path.join("readonly");
755 let nested_dir = readonly_dir.join("nested");
756 let nested_file = nested_dir.join("file");
757
758 let normal_dir = src_path.join("normal");
759 let normal_file = normal_dir.join("file");
760
761 fs::create_dir_all(&hidden_dir).await?;
763 fs::create_dir_all(&nested_dir).await?;
764 fs::create_dir_all(&normal_dir).await?;
765
766 fs::write(&hidden_file, "hidden content").await?;
768 fs::write(&nested_file, "nested content").await?;
769 fs::write(&normal_file, "normal content").await?;
770
771 fs::set_permissions(&noaccess_dir, Permissions::from_mode(0o000)).await?; fs::set_permissions(&hidden_dir, Permissions::from_mode(0o700)).await?; fs::set_permissions(&hidden_file, Permissions::from_mode(0o600)).await?; fs::set_permissions(&readonly_dir, Permissions::from_mode(0o400)).await?; fs::set_permissions(&nested_dir, Permissions::from_mode(0o500)).await?; fs::set_permissions(&nested_file, Permissions::from_mode(0o400)).await?; fs::set_permissions(&normal_dir, Permissions::from_mode(0o755)).await?; fs::set_permissions(&normal_file, Permissions::from_mode(0o644)).await?; let noaccess_perms = fs::metadata(&noaccess_dir).await?.permissions().mode() & 0o777;
785 let readonly_perms = fs::metadata(&readonly_dir).await?.permissions().mode() & 0o777;
786
787 println!("No access dir permissions: {:o}", noaccess_perms);
788 println!("Read-only dir permissions: {:o}", readonly_perms);
789
790 copy_dir_recursive(src_path, dst_path).await?;
793
794 let dst_noaccess_dir = dst_path.join("noaccess");
796 let dst_hidden_dir = dst_noaccess_dir.join("hidden");
797 let dst_hidden_file = dst_hidden_dir.join("file");
798
799 let dst_readonly_dir = dst_path.join("readonly");
800 let dst_nested_dir = dst_readonly_dir.join("nested");
801 let dst_nested_file = dst_nested_dir.join("file");
802
803 let dst_normal_dir = dst_path.join("normal");
804 let dst_normal_file = dst_normal_dir.join("file");
805
806 assert!(
808 dst_noaccess_dir.exists(),
809 "No-access directory was not copied"
810 );
811 assert!(dst_hidden_dir.exists(), "Hidden directory was not copied");
812 assert!(dst_hidden_file.exists(), "Hidden file was not copied");
813
814 assert!(
815 dst_readonly_dir.exists(),
816 "Read-only directory was not copied"
817 );
818 assert!(dst_nested_dir.exists(), "Nested directory was not copied");
819 assert!(dst_nested_file.exists(), "Nested file was not copied");
820
821 assert!(dst_normal_dir.exists(), "Normal directory was not copied");
822 assert!(dst_normal_file.exists(), "Normal file was not copied");
823
824 assert_eq!(
826 fs::read_to_string(&dst_hidden_file).await?,
827 "hidden content"
828 );
829 assert_eq!(
830 fs::read_to_string(&dst_nested_file).await?,
831 "nested content"
832 );
833 assert_eq!(
834 fs::read_to_string(&dst_normal_file).await?,
835 "normal content"
836 );
837
838 let dst_noaccess_perms =
840 fs::metadata(&dst_noaccess_dir).await?.permissions().mode() & 0o777;
841 let dst_hidden_perms = fs::metadata(&dst_hidden_dir).await?.permissions().mode() & 0o777;
842 let dst_hidden_file_perms =
843 fs::metadata(&dst_hidden_file).await?.permissions().mode() & 0o777;
844
845 let dst_readonly_perms =
846 fs::metadata(&dst_readonly_dir).await?.permissions().mode() & 0o777;
847 let dst_nested_perms = fs::metadata(&dst_nested_dir).await?.permissions().mode() & 0o777;
848 let dst_nested_file_perms =
849 fs::metadata(&dst_nested_file).await?.permissions().mode() & 0o777;
850
851 let dst_normal_perms = fs::metadata(&dst_normal_dir).await?.permissions().mode() & 0o777;
852 let dst_normal_file_perms =
853 fs::metadata(&dst_normal_file).await?.permissions().mode() & 0o777;
854
855 assert_eq!(
857 dst_noaccess_perms, 0o000,
858 "No-access directory permissions not preserved"
859 );
860 assert_eq!(
861 dst_hidden_perms, 0o700,
862 "Hidden directory permissions not preserved"
863 );
864 assert_eq!(
865 dst_hidden_file_perms, 0o600,
866 "Hidden file permissions not preserved"
867 );
868
869 assert_eq!(
870 dst_readonly_perms, 0o400,
871 "Read-only directory permissions not preserved"
872 );
873 assert_eq!(
874 dst_nested_perms, 0o500,
875 "Nested directory permissions not preserved"
876 );
877 assert_eq!(
878 dst_nested_file_perms, 0o400,
879 "Nested file permissions not preserved"
880 );
881
882 assert_eq!(
883 dst_normal_perms, 0o755,
884 "Normal directory permissions not preserved"
885 );
886 assert_eq!(
887 dst_normal_file_perms, 0o644,
888 "Normal file permissions not preserved"
889 );
890
891 Ok(())
892 }
893
894 #[tokio::test]
895 async fn test_copy_dir_nonexistent_source() -> anyhow::Result<()> {
896 let dst_root = TempDir::new()?;
898 let dst_path = dst_root.path();
899
900 let src_path = PathBuf::from("/nonexistent/directory");
902
903 let result = copy_dir_recursive(&src_path, dst_path).await;
905
906 assert!(
907 result.is_err(),
908 "Expected an error when source doesn't exist"
909 );
910 assert!(
911 matches!(result.unwrap_err(), MicrosandboxError::PathNotFound(_)),
912 "Expected a PathNotFound error"
913 );
914
915 Ok(())
916 }
917
918 #[tokio::test]
919 async fn test_patch_with_default_dns_settings() -> anyhow::Result<()> {
920 let root_dir = TempDir::new()?;
922 let root_path = root_dir.path();
923
924 patch_with_default_dns_settings(&[root_path.to_path_buf()]).await?;
926
927 let resolv_path = root_path.join("etc/resolv.conf");
929 assert!(resolv_path.exists());
930
931 let resolv_content = fs::read_to_string(&resolv_path).await?;
932
933 assert!(resolv_content.contains("# /etc/resolv.conf: DNS resolver configuration"));
935 assert!(resolv_content.contains("nameserver 1.1.1.1"));
936 assert!(resolv_content.contains("nameserver 8.8.8.8"));
937
938 let perms = fs::metadata(&resolv_path).await?.permissions();
940 assert_eq!(perms.mode() & 0o777, 0o644);
941
942 let root_dir2 = TempDir::new()?;
944 let root_path2 = root_dir2.path();
945 let resolv_path2 = root_path2.join("etc/resolv.conf");
946 fs::create_dir_all(resolv_path2.parent().unwrap()).await?;
947 fs::write(&resolv_path2, "# Empty resolv.conf\n").await?;
948
949 patch_with_default_dns_settings(&[root_path2.to_path_buf()]).await?;
950
951 let content2 = fs::read_to_string(&resolv_path2).await?;
953 assert!(content2.contains("nameserver 1.1.1.1"));
954 assert!(content2.contains("nameserver 8.8.8.8"));
955
956 let root_dir3 = TempDir::new()?;
958 let root_path3 = root_dir3.path();
959 let resolv_path3 = root_path3.join("etc/resolv.conf");
960 fs::create_dir_all(resolv_path3.parent().unwrap()).await?;
961 fs::write(
962 &resolv_path3,
963 "# Existing nameservers\nnameserver 192.168.1.1\n",
964 )
965 .await?;
966
967 patch_with_default_dns_settings(&[root_path3.to_path_buf()]).await?;
968
969 let content3 = fs::read_to_string(&resolv_path3).await?;
971 assert!(content3.contains("nameserver 192.168.1.1"));
972 assert!(!content3.contains("nameserver 1.1.1.1"));
973 assert!(!content3.contains("nameserver 8.8.8.8"));
974
975 let root_dir4 = TempDir::new()?;
977 let lower_layer1 = root_dir4.path().join("lower1");
978 let lower_layer2 = root_dir4.path().join("lower2");
979 let patch_layer = root_dir4.path().join("patch");
980
981 fs::create_dir_all(&lower_layer1).await?;
983 fs::create_dir_all(&lower_layer2).await?;
984 fs::create_dir_all(&patch_layer).await?;
985
986 patch_with_default_dns_settings(&[
988 lower_layer1.clone(),
989 lower_layer2.clone(),
990 patch_layer.clone(),
991 ])
992 .await?;
993
994 assert!(!lower_layer1.join("etc/resolv.conf").exists());
996 assert!(!lower_layer2.join("etc/resolv.conf").exists());
997 let patch_resolv = patch_layer.join("etc/resolv.conf");
998 assert!(patch_resolv.exists());
999 let content = fs::read_to_string(&patch_resolv).await?;
1000 assert!(content.contains("nameserver 1.1.1.1"));
1001
1002 let root_dir5 = TempDir::new()?;
1004 let lower_layer = root_dir5.path().join("lower");
1005 let patch_layer = root_dir5.path().join("patch");
1006 fs::create_dir_all(&lower_layer.join("etc")).await?;
1007 fs::create_dir_all(&patch_layer).await?;
1008
1009 fs::write(
1011 lower_layer.join("etc/resolv.conf"),
1012 "nameserver 192.168.1.1\n",
1013 )
1014 .await?;
1015
1016 patch_with_default_dns_settings(&[lower_layer.clone(), patch_layer.clone()]).await?;
1017
1018 assert!(!patch_layer.join("etc/resolv.conf").exists());
1020 let lower_content = fs::read_to_string(lower_layer.join("etc/resolv.conf")).await?;
1021 assert!(lower_content.contains("nameserver 192.168.1.1"));
1022
1023 Ok(())
1024 }
1025
1026 #[tokio::test]
1027 async fn test_patch_with_stat_override() -> anyhow::Result<()> {
1028 if !xattr::SUPPORTED_PLATFORM {
1030 println!("Skipping xattr test on unsupported platform");
1031 return Ok(());
1032 }
1033
1034 let root_dir = TempDir::new()?;
1036 let root_path = root_dir.path();
1037
1038 patch_with_stat_override(root_path).await?;
1040
1041 let xattr_value =
1043 xattr::get(root_path, "user.containers.override_stat").expect("Failed to get xattr");
1044
1045 assert!(xattr_value.is_some(), "xattr was not set");
1047 assert_eq!(xattr_value.unwrap(), b"0:0:0555", "xattr value incorrect");
1048
1049 Ok(())
1050 }
1051}