This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

1//! NAR unpacking and store path listings. 2 3use std::collections::BTreeMap; 4use std::fs; 5use std::io; 6use std::os::unix::fs::{MetadataExt, PermissionsExt}; 7use std::path::Path; 8 9use futures_util::TryStreamExt; 10use harmonia_file_core::{Directory, FileSystemObject, FileTree, Regular, Symlink}; 11use harmonia_file_nar::{parse_nar, restore, NarWriteError}; 12use harmonia_utils_hash::{fmt::Any, Algorithm, Context, Hash}; 13use serde::Deserialize; 14use tokio::io::{AsyncRead, AsyncReadExt}; 15use tokio_util::io::InspectReader; 16 17/// Does `actual` match a narinfo NarHash (`sha256:` in any encoding)? 18pub fn hash_matches(expected: &str, actual: &Hash) -> bool { 19 expected 20 .parse::<Any<Hash>>() 21 .is_ok_and(|e| e.into_hash() == *actual) 22} 23 24#[derive(Debug, Clone, PartialEq, Deserialize)] 25pub struct Size { 26 pub size: u64, 27} 28 29/// The metadata of a store path, as in the caches' `.ls` listings. 30pub type Tree = FileTree<Size>; 31 32/// Parse a `.ls` listing (`{"version": 1, "root": {...}}`). 33pub fn from_listing(body: &[u8]) -> io::Result<Tree> { 34 #[derive(Deserialize)] 35 struct Listing { 36 root: Tree, 37 } 38 let l: Listing = serde_json::from_slice(body).map_err(io::Error::other)?; 39 Ok(l.root) 40} 41 42/// Listing of an unpacked store path (for paths without `.ls`). 43pub fn from_path(p: &Path) -> io::Result<Tree> { 44 let m = fs::symlink_metadata(p)?; 45 let t = m.file_type(); 46 Ok(FileTree(if t.is_dir() { 47 let mut entries = BTreeMap::new(); 48 for d in fs::read_dir(p)? { 49 let d = d?; 50 let name = d.file_name().into_string().map_err(|n| { 51 io::Error::other(format!("non-UTF-8 name {n:?} in {}", p.display())) 52 })?; 53 entries.insert(name, Box::new(from_path(&d.path())?)); 54 } 55 FileSystemObject::Directory(Directory { entries }) 56 } else if t.is_symlink() { 57 let target = fs::read_link(p)?.to_string_lossy().into_owned(); 58 FileSystemObject::Symlink(Symlink { target }) 59 } else { 60 FileSystemObject::Regular(Regular { 61 executable: m.mode() & 0o100 != 0, 62 contents: Size { size: m.size() }, 63 }) 64 })) 65} 66 67/// Unpack the NAR read from `r` to `dest` (which must not exist), read-only 68/// like in the store. Returns the NAR's sha256 and size; checking them is 69/// up to the caller. Data after the NAR counts into the size. 70pub async fn unpack(r: impl AsyncRead + Unpin + Send, dest: &Path) -> io::Result<(Hash, u64)> { 71 let mut hash = Context::new(Algorithm::SHA256); 72 let mut size = 0; 73 let mut r = InspectReader::new(r, |b: &[u8]| { 74 hash.update(b); 75 size += b.len() as u64; 76 }); 77 let events = 78 parse_nar(&mut r).map_err(|e| NarWriteError::create_file_error(dest.to_owned(), e)); 79 restore(events, dest).await.map_err(io::Error::other)?; 80 r.read(&mut [0; 1]).await?; 81 drop(r); 82 let d = dest.to_owned(); 83 tokio::task::spawn_blocking(move || read_only(&d)) 84 .await 85 .map_err(io::Error::other)??; 86 Ok((hash.finish(), size)) 87} 88 89/// Store permissions: 0444 or 0555 for files, 0555 for directories. 90fn read_only(p: &Path) -> io::Result<()> { 91 let m = fs::symlink_metadata(p)?; 92 let mode = if m.is_symlink() { 93 return Ok(()); 94 } else if m.is_dir() { 95 for d in fs::read_dir(p)? { 96 read_only(&d?.path())?; 97 } 98 0o555 99 } else if m.mode() & 0o100 != 0 { 100 0o555 101 } else { 102 0o444 103 }; 104 fs::set_permissions(p, fs::Permissions::from_mode(mode)) 105} 106 107/// Remove a (partially) unpacked tree, including read-only directories. 108pub fn remove(p: &Path) -> io::Result<()> { 109 let m = match fs::symlink_metadata(p) { 110 Ok(m) => m, 111 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()), 112 Err(e) => return Err(e), 113 }; 114 if m.is_dir() { 115 fs::set_permissions(p, fs::Permissions::from_mode(0o755))?; 116 for d in fs::read_dir(p)? { 117 remove(&d?.path())?; 118 } 119 fs::remove_dir(p) 120 } else { 121 fs::remove_file(p) 122 } 123} 124 125#[cfg(test)] 126mod tests { 127 use super::*; 128 129 #[test] 130 fn hash_matches_nix() { 131 // nix hash convert --to nix32 sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU= 132 let d = Algorithm::SHA256.digest(b""); 133 assert!(hash_matches( 134 "sha256:0mdqa9w1p6cmli6976v4wi0sw9r4p5prkj7lzfd1877wk11c9c73", 135 &d 136 )); 137 assert!(hash_matches( 138 "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 139 &d 140 )); 141 assert!(!hash_matches( 142 "0mdqa9w1p6cmli6976v4wi0sw9r4p5prkj7lzfd1877wk11c9c73", 143 &d 144 )); 145 } 146 147 #[tokio::test] 148 async fn unpack_and_list() { 149 let tmp = std::env::temp_dir().join(format!("lazy-store-test-{}", std::process::id())); 150 let (src, dir) = (tmp.join("src"), tmp.join("out")); 151 let _ = remove(&tmp); 152 fs::create_dir_all(&src).unwrap(); 153 fs::write(src.join("bin"), "#!/bin/sh\n").unwrap(); 154 fs::set_permissions(src.join("bin"), fs::Permissions::from_mode(0o755)).unwrap(); 155 std::os::unix::fs::symlink("bin", src.join("link")).unwrap(); 156 let data: Vec<u8> = harmonia_file_nar::NarByteStream::new(src.clone()) 157 .try_collect::<Vec<_>>() 158 .await 159 .unwrap() 160 .concat(); 161 162 let (h, n) = unpack(&data[..], &dir).await.unwrap(); 163 assert_eq!(n, data.len() as u64); 164 assert_eq!(h, Algorithm::SHA256.digest(&data)); 165 assert_eq!(fs::metadata(dir.join("bin")).unwrap().mode() & 0o777, 0o555); 166 assert_eq!(fs::metadata(&dir).unwrap().mode() & 0o777, 0o555); 167 let t = from_path(&dir).unwrap(); 168 let ls = br#"{"version":1,"root":{"type":"directory","entries":{ 169 "bin":{"type":"regular","size":10,"executable":true,"narOffset":96}, 170 "link":{"type":"symlink","target":"bin"}}}}"#; 171 assert_eq!(from_listing(ls).unwrap(), t); 172 remove(&dir).unwrap(); 173 174 // the caller's NarSize check rejects it 175 let mut trailing = data.clone(); 176 trailing.push(0); 177 let (_, n) = unpack(&trailing[..], &dir).await.unwrap(); 178 assert_eq!(n, trailing.len() as u64); 179 remove(&tmp).unwrap(); 180 } 181}