1use std::borrow::Cow;
65use std::collections::BTreeMap;
66use std::collections::btree_map::Entry;
67use std::io;
68use std::path::{Path, PathBuf};
69use std::pin::{Pin, pin};
70
71use rayon::prelude::*;
72use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
73
74use uv_threads::initialize_rayon_once;
75
76mod archive;
77mod seek;
78
79pub(crate) use archive::UnzipOutput;
80pub(crate) use archive::directory_tree_from_extracted;
81pub use archive::{DirectoryDigest, HashedFile, UnhashedFile};
82pub(crate) use seek::{unzip, unzip_and_hash};
83
84async fn read_exact_or_eof(
87 mut reader: Pin<&mut impl AsyncRead>,
88 mut buf: &mut [u8],
89) -> io::Result<usize> {
90 let mut bytes_read = 0;
91 loop {
92 match reader.read(buf).await {
93 Ok(0) => return Ok(bytes_read),
94 Ok(n) => {
95 bytes_read += n;
96 if n == buf.len() {
97 return Ok(bytes_read);
98 }
99 buf = &mut buf[n..];
100 }
101 Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
102 Err(e) => return Err(e),
103 }
104 }
105}
106
107pub async fn blake3_copy<R, W>(reader: R, writer: W) -> io::Result<(u64, blake3::Hash)>
111where
112 R: AsyncRead,
113 W: AsyncWrite,
114{
115 blake3_copy_with_buffer(reader, writer, &mut Vec::new()).await
116}
117
118pub(crate) async fn blake3_copy_with_buffer<R, W>(
120 reader: R,
121 writer: W,
122 buffer: &mut Vec<u8>,
123) -> io::Result<(u64, blake3::Hash)>
124where
125 R: AsyncRead,
126 W: AsyncWrite,
127{
128 let mut reader = pin!(reader);
129 let mut writer = pin!(writer);
130 let mut hasher = blake3::Hasher::new();
131 buffer.resize(1 << 16, 0); let mut total = 0u64;
133 loop {
139 let bytes_read = read_exact_or_eof(reader.as_mut(), buffer).await?;
140 if bytes_read == 0 {
141 break; }
143 total += bytes_read as u64;
144 let bytes = &buffer[..bytes_read];
145 hasher.update(bytes);
146 writer.write_all(bytes).await?;
147 if bytes_read < buffer.len() {
148 break; }
150 }
151 writer.flush().await?;
152 Ok((total, hasher.finalize()))
153}
154
155#[derive(Debug, thiserror::Error)]
156pub enum DirhashError {
157 #[error("Invalid path for directory hashing: {path:?}")]
158 InvalidPath { path: PathBuf },
159 #[error("Archive path is missing from the directory hash tree: {path:?}")]
160 MissingPath { path: PathBuf },
161 #[error("Archive contains duplicate entries for path: {path:?}")]
162 DuplicatePath { path: PathBuf },
163 #[error("Archive path is used as both a file and a directory: {path:?}")]
164 FileDirectoryConflict { path: PathBuf },
165 #[error("Encountered a symlink cycle while hashing a directory: {paths:?}")]
166 SymlinkCycle { paths: Vec<PathBuf> },
167 #[error(transparent)]
168 Io(#[from] io::Error),
169}
170
171struct SeenSymlinkNode<'a> {
173 canonical_path: PathBuf,
174 previous: Option<&'a Self>,
175}
176
177struct SeenSymlinks<'a> {
178 node: Option<SeenSymlinkNode<'a>>,
179}
180
181impl<'a> SeenSymlinks<'a> {
182 fn new() -> Self {
183 Self { node: None }
184 }
185
186 fn iter(&self) -> impl Iterator<Item = &Path> {
187 let mut node = self.node.as_ref();
188 std::iter::from_fn(move || {
189 if let Some(next_node) = node {
190 let next_path = &next_node.canonical_path;
191 node = next_node.previous;
192 Some(next_path.as_path())
193 } else {
194 None
195 }
196 })
197 }
198
199 fn push(&'a self, symlink_path: &Path) -> Result<Self, DirhashError> {
200 let canonical_path = canonical_path_to_symlink(symlink_path)?;
201 for seen in self.iter() {
203 if canonical_path == seen {
204 let mut paths: Vec<PathBuf> = self.iter().map(Path::to_owned).collect();
205 paths.reverse();
206 paths.push(canonical_path);
207 return Err(DirhashError::SymlinkCycle { paths });
208 }
209 }
210 Ok(Self {
211 node: Some(SeenSymlinkNode {
212 canonical_path,
213 previous: self.node.as_ref(),
214 }),
215 })
216 }
217}
218
219fn canonical_path_to_symlink(symlink_path: &Path) -> Result<PathBuf, DirhashError> {
222 let Some(filename) = symlink_path.file_name() else {
223 return Err(DirhashError::InvalidPath {
224 path: symlink_path.to_path_buf(),
225 });
226 };
227 let parent = symlink_path
228 .parent()
229 .filter(|parent| !parent.as_os_str().is_empty())
230 .unwrap_or(Path::new("."));
231 Ok(fs_err::canonicalize(parent)?.join(filename))
232}
233
234pub fn dirhash_path(path: &Path) -> Result<blake3::Hash, DirhashError> {
243 initialize_rayon_once();
244 let seen_symlinks = SeenSymlinks::new();
245 dirhash_path_inner(path, &seen_symlinks)
246}
247
248fn dirhash_path_inner(
250 path: &Path,
251 seen_symlinks: &SeenSymlinks,
252) -> Result<blake3::Hash, DirhashError> {
253 let metadata = fs_err::symlink_metadata(path)?;
254 if metadata.is_symlink() {
255 let seen_symlinks = seen_symlinks.push(path)?;
256 dirhash_path_inner_resolved(path, &fs_err::metadata(path)?, &seen_symlinks)
257 } else {
258 dirhash_path_inner_resolved(path, &metadata, seen_symlinks)
259 }
260}
261
262fn dirhash_path_inner_resolved(
264 path: &Path,
265 metadata: &std::fs::Metadata,
266 seen_symlinks: &SeenSymlinks,
267) -> Result<blake3::Hash, DirhashError> {
268 if metadata.is_dir() {
269 let mut dir_contents = Vec::new();
271 for entry in fs_err::read_dir(path)? {
272 let entry = entry?;
273 let path = entry.path();
274 let Ok(name) = entry.file_name().into_string() else {
277 return Err(DirhashError::InvalidPath { path });
278 };
279 dir_contents.push((name, path));
280 }
281 dir_contents.sort_unstable();
283 let hashes = dir_contents
285 .par_iter()
286 .map(|(_, path)| dirhash_path_inner(path, seen_symlinks))
288 .collect::<Result<Vec<blake3::Hash>, _>>()?;
289 let dirhash_entries = dir_contents
290 .iter()
291 .zip(hashes)
292 .map(|((name, _), hash)| (name.as_str(), hash));
293 Ok(hash_dir_entries(dirhash_entries))
294 } else {
295 Ok(blake3::Hasher::new().update_mmap_rayon(path)?.finalize())
298 }
299}
300
301#[derive(Debug, Clone)]
302enum DirhashEntry {
303 File(blake3::Hash),
304 Directory(DirhashTree),
305}
306
307#[derive(Debug, Clone, Default)]
312pub struct DirhashTree {
313 children: BTreeMap<String, DirhashEntry>,
314}
315
316impl DirhashTree {
317 pub fn new() -> Self {
319 Self::default()
320 }
321
322 fn insertion_entry(
323 &mut self,
324 normalized_path: &str,
325 original_path: &str,
326 create_dirs: bool,
327 ) -> Result<Entry<'_, String, DirhashEntry>, DirhashError> {
328 if let Some((component, rest)) = normalized_path.split_once('/') {
329 if self.children.contains_key(component) {
331 match self.children.get_mut(component).unwrap() {
338 DirhashEntry::Directory(child) => {
339 child.insertion_entry(rest, original_path, create_dirs)
340 }
341 DirhashEntry::File(_) => Err(DirhashError::FileDirectoryConflict {
342 path: PathBuf::from(original_path),
343 }),
344 }
345 } else {
346 if create_dirs {
348 let child = self
349 .children
350 .entry(String::from(component))
351 .or_insert(DirhashEntry::Directory(Self::default()));
352 let DirhashEntry::Directory(child) = child else {
353 unreachable!()
354 };
355 child.insertion_entry(rest, original_path, create_dirs)
356 } else {
357 Err(DirhashError::MissingPath {
358 path: PathBuf::from(original_path),
359 })
360 }
361 }
362 } else {
363 Ok(self.children.entry(String::from(normalized_path)))
365 }
366 }
367
368 pub fn add_file(&mut self, path: &str, hash: blake3::Hash) -> Result<(), DirhashError> {
381 let normalized_path = normalize_dirhash_path(path)?;
382 let entry = self.insertion_entry(&normalized_path, path, true)?;
383 match entry {
384 Entry::Vacant(vacant) => {
385 vacant.insert(DirhashEntry::File(hash));
386 Ok(())
387 }
388 Entry::Occupied(_) => Err(DirhashError::DuplicatePath {
389 path: PathBuf::from(path),
390 }),
391 }
392 }
393
394 pub fn update_file(&mut self, path: &str, hash: blake3::Hash) -> Result<(), DirhashError> {
406 let normalized_path = normalize_dirhash_path(path)?;
407 let entry = self.insertion_entry(&normalized_path, path, false)?;
408 match entry {
409 Entry::Vacant(_) => Err(DirhashError::MissingPath {
410 path: PathBuf::from(path),
411 }),
412 Entry::Occupied(mut occupied) => match occupied.get_mut() {
413 DirhashEntry::File(prev_hash) => {
414 *prev_hash = hash;
415 Ok(())
416 }
417 DirhashEntry::Directory(_) => Err(DirhashError::FileDirectoryConflict {
418 path: PathBuf::from(path),
419 }),
420 },
421 }
422 }
423
424 pub fn add_empty_dir(&mut self, path: &str) -> Result<(), DirhashError> {
432 let normalized_path = normalize_dirhash_path(path)?;
433 let entry = self.insertion_entry(&normalized_path, path, true)?;
434 match entry {
435 Entry::Vacant(vacant) => {
436 vacant.insert(DirhashEntry::Directory(Self::default()));
437 Ok(())
438 }
439 Entry::Occupied(occupied) => match occupied.get() {
440 DirhashEntry::Directory(_) => Ok(()),
441 DirhashEntry::File(_) => Err(DirhashError::FileDirectoryConflict {
442 path: PathBuf::from(path),
443 }),
444 },
445 }
446 }
447
448 pub fn hash(&self) -> blake3::Hash {
454 hash_dir_entries(self.children.iter().map(|(name, entry)| {
455 let hash = match entry {
456 DirhashEntry::File(hash) => *hash,
457 DirhashEntry::Directory(child) => child.hash(),
458 };
459 (name.as_str(), hash)
460 }))
461 }
462}
463
464fn component_needs_normalization(component: &str) -> bool {
465 matches!(component, "" | "." | "..")
466}
467
468fn normalize_dirhash_path(mut path: &str) -> Result<Cow<'_, str>, DirhashError> {
469 if path.starts_with('/') {
470 return Err(DirhashError::InvalidPath {
471 path: PathBuf::from(path),
472 });
473 }
474 path = path.trim_start_matches("./");
475 path = path.trim_end_matches('/');
476 if !path.split('/').any(component_needs_normalization) {
477 return Ok(Cow::Borrowed(path));
478 }
479 let mut components = Vec::new();
480 for component in path.split('/') {
481 match component {
482 "" | "." => {}
483 ".." => {
484 if components.pop().is_none() {
485 return Err(DirhashError::InvalidPath {
486 path: PathBuf::from(path),
487 });
488 }
489 }
490 component => components.push(component),
491 }
492 }
493 if components.is_empty() {
494 return Err(DirhashError::InvalidPath {
495 path: PathBuf::from(path),
496 });
497 }
498 Ok(Cow::Owned(components.join("/")))
499}
500
501fn hash_dir_entries<'a, Iter>(entries: Iter) -> blake3::Hash
502where
503 Iter: IntoIterator<Item = (&'a str, blake3::Hash)>,
504{
505 let mut hasher = blake3::Hasher::new_derive_key("directory");
509 for (name, hash) in entries {
510 hasher.update(name.as_bytes());
511 hasher.update(&[0xff]);
512 hasher.update(hash.as_bytes());
513 }
514 hasher.finalize()
515}
516
517#[cfg(test)]
518mod tests {
519 #[cfg(unix)]
520 use std::assert_matches;
521
522 use super::*;
523 use std::cmp;
524 use std::task::{Context, Poll};
525
526 #[test]
527 fn test_normalize() {
528 let success_cases = [
529 ("foo", Cow::Borrowed("foo")),
530 ("foo", Cow::Borrowed("foo")),
531 ("foo/", Cow::Borrowed("foo")),
532 ("./foo", Cow::Borrowed("foo")),
533 ("././foo/bar///", Cow::Borrowed("foo/bar")),
534 ("foo//bar", Cow::Owned("foo/bar".to_string())),
535 ("foo/./bar", Cow::Owned("foo/bar".to_string())),
536 ("foo/.///./bar", Cow::Owned("foo/bar".to_string())),
537 ("foo/bar/..", Cow::Owned("foo".to_string())),
538 ("foo/bar/../../baz", Cow::Owned("baz".to_string())),
539 ];
540 for (path, expected) in success_cases {
541 let normalized = super::normalize_dirhash_path(path).unwrap();
542 assert_eq!(normalized, expected);
543 }
544 let error_cases = [
545 "",
546 "/",
547 "/foo",
548 "///foo",
549 "..",
550 "foo/..",
551 "foo/bar/../../../baz",
552 ];
553 for path in error_cases {
554 super::normalize_dirhash_path(path).unwrap_err();
555 }
556 }
557
558 #[test]
559 fn test_add_update_and_add_empty_dir() {
560 let a_hash = blake3::hash(b"hello");
569 let c_hash = blake3::hash(b"goodbye");
570 let d_hash = blake3::derive_key("directory", b"");
571 let mut b_input = Vec::new();
572 b_input.extend_from_slice(b"c.txt\xff");
573 b_input.extend_from_slice(c_hash.as_bytes());
574 b_input.extend_from_slice(b"d\xff");
575 b_input.extend_from_slice(&d_hash);
576 let b_hash = blake3::derive_key("directory", &b_input);
577 let mut root_input = Vec::new();
578 root_input.extend_from_slice(b"a.txt\xff");
579 root_input.extend_from_slice(a_hash.as_bytes());
580 root_input.extend_from_slice(b"b\xff");
581 root_input.extend_from_slice(&b_hash);
582 let root_hash = blake3::derive_key("directory", &root_input);
583 assert_eq!(
585 blake3::Hash::from_bytes(root_hash).to_hex().as_str(),
586 "e508467d129e0d19cefa96527f5f6cb3760530be4d931c527f2818a0dff5d517"
587 );
588
589 let mut tree = super::DirhashTree::default();
591 tree.add_file("a.txt", a_hash).unwrap();
592 tree.add_file("b/c.txt", c_hash).unwrap();
593 tree.add_empty_dir("b/d").unwrap();
594 assert_eq!(tree.hash(), root_hash);
595
596 tree.update_file("b/c.txt", [0; 32].into()).unwrap();
598 assert_ne!(tree.hash(), root_hash);
599 tree.update_file("b/c.txt", c_hash).unwrap();
601 assert_eq!(tree.hash(), root_hash);
602
603 tree.add_empty_dir("b").unwrap(); assert_eq!(tree.hash(), root_hash);
606 tree.add_empty_dir("e").unwrap(); assert_ne!(tree.hash(), root_hash);
609 }
610
611 #[test]
612 fn test_dirhash_path() -> Result<(), super::DirhashError> {
613 let temp_dir = tempfile::tempdir()?;
623 let root = temp_dir.path();
624 fs_err::write(root.join("a.txt"), b"hello")?;
625 fs_err::create_dir(root.join("b"))?;
626 fs_err::write(root.join("b/c.txt"), b"goodbye")?;
627 fs_err::create_dir(root.join("b/d"))?;
628
629 let mut expected = super::DirhashTree::default();
630 expected.add_file("a.txt", blake3::hash(b"hello"))?;
631 expected.add_file("b/c.txt", blake3::hash(b"goodbye"))?;
632 expected.add_empty_dir("b/d")?;
633
634 assert_eq!(super::dirhash_path(root)?, expected.hash());
635 assert_eq!(
638 super::dirhash_path(&root.join("a.txt"))?,
639 blake3::hash(b"hello")
640 );
641 Ok(())
642 }
643
644 #[cfg(unix)]
645 #[test]
646 fn test_dirhash_path_symlinks() -> Result<(), super::DirhashError> {
647 use fs_err::os::unix::fs::symlink;
648
649 let temp_dir = tempfile::tempdir()?;
660 let root = temp_dir.path();
661 fs_err::create_dir(root.join("dir1"))?;
662 fs_err::create_dir(root.join("dir2"))?;
663 fs_err::write(root.join("dir1/file.txt"), b"hello")?;
664 symlink("../dir2", root.join("dir1/dir_link"))?;
665 symlink("../dir1/file.txt", root.join("dir2/file_link"))?;
666
667 let mut in_memory = super::DirhashTree::default();
668 in_memory.add_file("dir1/file.txt", blake3::hash(b"hello"))?;
669 in_memory.add_file("dir1/dir_link/file_link", blake3::hash(b"hello"))?;
670 in_memory.add_file("dir2/file_link", blake3::hash(b"hello"))?;
671 let from_disk = super::dirhash_path(root)?;
672 assert_eq!(in_memory.hash(), from_disk);
673
674 fs_err::create_dir(root.join("dir2/inner"))?;
676 symlink("../../dir1", root.join("dir2/inner/dir_link"))?;
677 let error = super::dirhash_path(root).unwrap_err();
678 assert_matches!(error, super::DirhashError::SymlinkCycle { .. });
679 Ok(())
680 }
681
682 fn paint_input(buf: &mut [u8]) {
685 let mut value = 0u8;
686 for byte in buf {
687 *byte = value;
688 value = if value == 250 { 0 } else { value + 1 };
689 }
690 }
691
692 #[tokio::test]
693 async fn test_blake3_copy() -> io::Result<()> {
694 let input = b"hello";
695 let mut output = Vec::new();
696 let (bytes_read, hash) = super::blake3_copy(&input[..], &mut output).await?;
697 assert_eq!(bytes_read, input.len() as u64);
698 assert_eq!(input, &output[..]);
699 assert_eq!(hash, blake3::hash(input));
700
701 let mut big_input = vec![0; 64_000 * 3];
702 paint_input(&mut big_input);
703 let mut big_output = Vec::new();
704 let (big_bytes_read, big_hash) =
705 super::blake3_copy(&big_input[..], &mut big_output).await?;
706 assert_eq!(big_bytes_read, big_input.len() as u64);
707 assert_eq!(big_input, big_output);
708 assert_eq!(big_hash, blake3::hash(&big_input));
709 Ok(())
710 }
711
712 #[tokio::test]
713 async fn test_blake3_copy_reuses_buffer() -> io::Result<()> {
714 let mut input = vec![0; 64_000 * 3];
715 paint_input(&mut input);
716 let mut buffer = Vec::new();
717 for input in [input.as_slice(), b"hello", b""] {
718 let mut output = Vec::new();
719 let (bytes_read, hash) =
720 super::blake3_copy_with_buffer(input, &mut output, &mut buffer).await?;
721 assert_eq!(bytes_read, input.len() as u64);
722 assert_eq!(input, output);
723 assert_eq!(hash, blake3::hash(input));
724 }
725 Ok(())
726 }
727
728 struct ShortReader<'a>(&'a [u8]);
730
731 impl AsyncRead for ShortReader<'_> {
732 fn poll_read(
733 mut self: Pin<&mut Self>,
734 _cx: &mut Context<'_>,
735 buf: &mut tokio::io::ReadBuf<'_>,
736 ) -> Poll<io::Result<()>> {
737 const SHORT_READ_LEN: usize = 251; let want = cmp::min(self.0.len(), buf.remaining());
739 let take = cmp::min(want, SHORT_READ_LEN);
740 buf.put_slice(&self.0[..take]);
741 self.0 = &self.0[take..];
742 Poll::Ready(Ok(()))
743 }
744 }
745
746 #[tokio::test]
748 async fn test_blake3_copy_short_reader() -> io::Result<()> {
749 let mut input = vec![0; 64_000 * 3];
750 paint_input(&mut input);
751 let mut output = Vec::new();
752 let (bytes_read, hash) = super::blake3_copy(ShortReader(&input), &mut output).await?;
753 assert_eq!(bytes_read, input.len() as u64);
754 assert_eq!(input, &output[..]);
755 assert_eq!(hash, blake3::hash(&input));
756 Ok(())
757 }
758
759 fn walk_test_vector_input(
762 input_dir: &serde_json::Map<String, serde_json::Value>,
763 dirhash_tree: &mut DirhashTree,
764 tempdir: &tempfile::TempDir,
765 relative_path: Option<&str>,
767 ) -> anyhow::Result<()> {
768 for (name, file_or_dir) in input_dir {
769 let entry_path = match relative_path {
772 Some(parent) => &format!("{parent}/{name}"),
773 None => name,
774 };
775 match file_or_dir {
776 serde_json::Value::String(file_text) => {
778 fs_err::write(tempdir.path().join(entry_path), file_text)?;
780 dirhash_tree.add_file(entry_path, blake3::hash(file_text.as_bytes()))?;
782 }
783 serde_json::Value::Object(input_subdir) => {
785 fs_err::create_dir(tempdir.path().join(entry_path))?;
787 dirhash_tree.add_empty_dir(entry_path)?;
791 walk_test_vector_input(input_subdir, dirhash_tree, tempdir, Some(entry_path))?;
793 }
794 _ => panic!("unexpected JSON type"),
795 }
796 }
797 Ok(())
798 }
799
800 #[derive(Debug, serde::Deserialize)]
801 struct JsonTestVector {
802 input: serde_json::Map<String, serde_json::Value>,
804 dirhash: String,
806 }
807
808 #[test]
812 fn test_vectors_json() -> anyhow::Result<()> {
813 let test_vectors: Vec<JsonTestVector> =
814 serde_json::from_str(include_str!("../test_vectors/test_vectors.json"))?;
815 for JsonTestVector { input, dirhash } in &test_vectors {
816 let mut tree = DirhashTree::new();
817 let tempdir = tempfile::tempdir()?;
818 walk_test_vector_input(
820 input, &mut tree, &tempdir, None, )?;
822
823 assert_eq!(dirhash.as_str(), tree.hash().to_hex().as_str());
825
826 assert_eq!(
828 dirhash.as_str(),
829 dirhash_path(tempdir.path())?.to_hex().as_str(),
830 );
831 }
832 Ok(())
833 }
834}