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
74async fn read_exact_or_eof(
77 mut reader: Pin<&mut impl AsyncRead>,
78 mut buf: &mut [u8],
79) -> io::Result<usize> {
80 let mut bytes_read = 0;
81 loop {
82 match reader.read(buf).await {
83 Ok(0) => return Ok(bytes_read),
84 Ok(n) => {
85 bytes_read += n;
86 if n == buf.len() {
87 return Ok(bytes_read);
88 }
89 buf = &mut buf[n..];
90 }
91 Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
92 Err(e) => return Err(e),
93 }
94 }
95}
96
97pub async fn blake3_copy<R, W>(reader: R, writer: W) -> io::Result<(u64, blake3::Hash)>
101where
102 R: AsyncRead,
103 W: AsyncWrite,
104{
105 let mut reader = pin!(reader);
106 let mut writer = pin!(writer);
107 let mut hasher = blake3::Hasher::new();
108 let mut buffer = [0; 1 << 16]; let mut total = 0u64;
110 loop {
116 let bytes_read = read_exact_or_eof(reader.as_mut(), &mut buffer).await?;
117 if bytes_read == 0 {
118 break; }
120 total += bytes_read as u64;
121 let bytes = &buffer[..bytes_read];
122 hasher.update(bytes);
123 writer.write_all(bytes).await?;
124 if bytes_read < buffer.len() {
125 break; }
127 }
128 writer.flush().await?;
129 Ok((total, hasher.finalize()))
130}
131
132#[derive(Debug, thiserror::Error)]
133pub enum DirhashError {
134 #[error("Invalid path for directory hashing: {path:?}")]
135 InvalidPath { path: PathBuf },
136 #[error("Archive path is missing from the directory hash tree: {path:?}")]
137 MissingPath { path: PathBuf },
138 #[error("Archive contains duplicate entries for path: {path:?}")]
139 DuplicatePath { path: PathBuf },
140 #[error("Archive path is used as both a file and a directory: {path:?}")]
141 FileDirectoryConflict { path: PathBuf },
142 #[error("Encountered a symlink cycle while hashing a directory: {paths:?}")]
143 SymlinkCycle { paths: Vec<PathBuf> },
144 #[error(transparent)]
145 Io(#[from] io::Error),
146}
147
148struct SeenSymlinkNode<'a> {
150 canonical_path: PathBuf,
151 previous: Option<&'a Self>,
152}
153
154struct SeenSymlinks<'a> {
155 node: Option<SeenSymlinkNode<'a>>,
156}
157
158impl<'a> SeenSymlinks<'a> {
159 fn new() -> Self {
160 Self { node: None }
161 }
162
163 fn iter(&self) -> impl Iterator<Item = &Path> {
164 let mut node = self.node.as_ref();
165 std::iter::from_fn(move || {
166 if let Some(next_node) = node {
167 let next_path = &next_node.canonical_path;
168 node = next_node.previous;
169 Some(next_path.as_path())
170 } else {
171 None
172 }
173 })
174 }
175
176 fn push(&'a self, symlink_path: &Path) -> Result<Self, DirhashError> {
177 let canonical_path = canonical_path_to_symlink(symlink_path)?;
178 for seen in self.iter() {
180 if canonical_path == seen {
181 let mut paths: Vec<PathBuf> = self.iter().map(Path::to_owned).collect();
182 paths.reverse();
183 paths.push(canonical_path);
184 return Err(DirhashError::SymlinkCycle { paths });
185 }
186 }
187 Ok(Self {
188 node: Some(SeenSymlinkNode {
189 canonical_path,
190 previous: self.node.as_ref(),
191 }),
192 })
193 }
194}
195
196fn canonical_path_to_symlink(symlink_path: &Path) -> Result<PathBuf, DirhashError> {
199 let Some(filename) = symlink_path.file_name() else {
200 return Err(DirhashError::InvalidPath {
201 path: symlink_path.to_path_buf(),
202 });
203 };
204 let parent = symlink_path
205 .parent()
206 .filter(|parent| !parent.as_os_str().is_empty())
207 .unwrap_or(Path::new("."));
208 Ok(fs_err::canonicalize(parent)?.join(filename))
209}
210
211pub fn dirhash_path(path: &Path) -> Result<blake3::Hash, DirhashError> {
220 uv_configuration::initialize_rayon_once();
221 let seen_symlinks = SeenSymlinks::new();
222 dirhash_path_inner(path, &seen_symlinks)
223}
224
225fn dirhash_path_inner(
227 path: &Path,
228 seen_symlinks: &SeenSymlinks,
229) -> Result<blake3::Hash, DirhashError> {
230 let metadata = fs_err::symlink_metadata(path)?;
231 if metadata.is_symlink() {
232 let seen_symlinks = seen_symlinks.push(path)?;
233 dirhash_path_inner_resolved(path, &fs_err::metadata(path)?, &seen_symlinks)
234 } else {
235 dirhash_path_inner_resolved(path, &metadata, seen_symlinks)
236 }
237}
238
239fn dirhash_path_inner_resolved(
241 path: &Path,
242 metadata: &std::fs::Metadata,
243 seen_symlinks: &SeenSymlinks,
244) -> Result<blake3::Hash, DirhashError> {
245 if metadata.is_dir() {
246 let mut dir_contents = Vec::new();
248 for entry in fs_err::read_dir(path)? {
249 let entry = entry?;
250 let path = entry.path();
251 let Ok(name) = entry.file_name().into_string() else {
254 return Err(DirhashError::InvalidPath { path });
255 };
256 dir_contents.push((name, path));
257 }
258 dir_contents.sort_unstable();
260 let hashes = dir_contents
262 .par_iter()
263 .map(|(_, path)| dirhash_path_inner(path, seen_symlinks))
265 .collect::<Result<Vec<blake3::Hash>, _>>()?;
266 let dirhash_entries = dir_contents
267 .iter()
268 .zip(hashes)
269 .map(|((name, _), hash)| (name.as_str(), hash));
270 Ok(hash_dir_entries(dirhash_entries))
271 } else {
272 Ok(blake3::Hasher::new().update_mmap_rayon(path)?.finalize())
275 }
276}
277
278#[derive(Debug, Clone)]
279enum DirhashEntry {
280 File(blake3::Hash),
281 Directory(DirhashTree),
282}
283
284#[derive(Debug, Clone, Default)]
289pub struct DirhashTree {
290 children: BTreeMap<String, DirhashEntry>,
291}
292
293impl DirhashTree {
294 pub fn new() -> Self {
296 Self::default()
297 }
298
299 fn insertion_entry(
300 &mut self,
301 normalized_path: &str,
302 original_path: &str,
303 create_dirs: bool,
304 ) -> Result<Entry<'_, String, DirhashEntry>, DirhashError> {
305 if let Some((component, rest)) = normalized_path.split_once('/') {
306 if self.children.contains_key(component) {
308 match self.children.get_mut(component).unwrap() {
315 DirhashEntry::Directory(child) => {
316 child.insertion_entry(rest, original_path, create_dirs)
317 }
318 DirhashEntry::File(_) => Err(DirhashError::FileDirectoryConflict {
319 path: PathBuf::from(original_path),
320 }),
321 }
322 } else {
323 if create_dirs {
325 let child = self
326 .children
327 .entry(String::from(component))
328 .or_insert(DirhashEntry::Directory(Self::default()));
329 let DirhashEntry::Directory(child) = child else {
330 unreachable!()
331 };
332 child.insertion_entry(rest, original_path, create_dirs)
333 } else {
334 Err(DirhashError::MissingPath {
335 path: PathBuf::from(original_path),
336 })
337 }
338 }
339 } else {
340 Ok(self.children.entry(String::from(normalized_path)))
342 }
343 }
344
345 pub fn add_file(&mut self, path: &str, hash: blake3::Hash) -> Result<(), DirhashError> {
358 let normalized_path = normalize_dirhash_path(path)?;
359 let entry = self.insertion_entry(&normalized_path, path, true)?;
360 match entry {
361 Entry::Vacant(vacant) => {
362 vacant.insert(DirhashEntry::File(hash));
363 Ok(())
364 }
365 Entry::Occupied(_) => Err(DirhashError::DuplicatePath {
366 path: PathBuf::from(path),
367 }),
368 }
369 }
370
371 pub fn update_file(&mut self, path: &str, hash: blake3::Hash) -> Result<(), DirhashError> {
383 let normalized_path = normalize_dirhash_path(path)?;
384 let entry = self.insertion_entry(&normalized_path, path, false)?;
385 match entry {
386 Entry::Vacant(_) => Err(DirhashError::MissingPath {
387 path: PathBuf::from(path),
388 }),
389 Entry::Occupied(mut occupied) => match occupied.get_mut() {
390 DirhashEntry::File(prev_hash) => {
391 *prev_hash = hash;
392 Ok(())
393 }
394 DirhashEntry::Directory(_) => Err(DirhashError::FileDirectoryConflict {
395 path: PathBuf::from(path),
396 }),
397 },
398 }
399 }
400
401 pub fn add_empty_dir(&mut self, path: &str) -> Result<(), DirhashError> {
409 let normalized_path = normalize_dirhash_path(path)?;
410 let entry = self.insertion_entry(&normalized_path, path, true)?;
411 match entry {
412 Entry::Vacant(vacant) => {
413 vacant.insert(DirhashEntry::Directory(Self::default()));
414 Ok(())
415 }
416 Entry::Occupied(occupied) => match occupied.get() {
417 DirhashEntry::Directory(_) => Ok(()),
418 DirhashEntry::File(_) => Err(DirhashError::FileDirectoryConflict {
419 path: PathBuf::from(path),
420 }),
421 },
422 }
423 }
424
425 pub fn hash(&self) -> blake3::Hash {
431 hash_dir_entries(self.children.iter().map(|(name, entry)| {
432 let hash = match entry {
433 DirhashEntry::File(hash) => *hash,
434 DirhashEntry::Directory(child) => child.hash(),
435 };
436 (name.as_str(), hash)
437 }))
438 }
439}
440
441fn component_needs_normalization(component: &str) -> bool {
442 matches!(component, "" | "." | "..")
443}
444
445fn normalize_dirhash_path(mut path: &str) -> Result<Cow<'_, str>, DirhashError> {
446 if path.starts_with('/') {
447 return Err(DirhashError::InvalidPath {
448 path: PathBuf::from(path),
449 });
450 }
451 path = path.trim_start_matches("./");
452 path = path.trim_end_matches('/');
453 if !path.split('/').any(component_needs_normalization) {
454 return Ok(Cow::Borrowed(path));
455 }
456 let mut components = Vec::new();
457 for component in path.split('/') {
458 match component {
459 "" | "." => {}
460 ".." => {
461 if components.pop().is_none() {
462 return Err(DirhashError::InvalidPath {
463 path: PathBuf::from(path),
464 });
465 }
466 }
467 component => components.push(component),
468 }
469 }
470 if components.is_empty() {
471 return Err(DirhashError::InvalidPath {
472 path: PathBuf::from(path),
473 });
474 }
475 Ok(Cow::Owned(components.join("/")))
476}
477
478fn hash_dir_entries<'a, Iter>(entries: Iter) -> blake3::Hash
479where
480 Iter: IntoIterator<Item = (&'a str, blake3::Hash)>,
481{
482 let mut hasher = blake3::Hasher::new_derive_key("directory");
486 for (name, hash) in entries {
487 hasher.update(name.as_bytes());
488 hasher.update(&[0xff]);
489 hasher.update(hash.as_bytes());
490 }
491 hasher.finalize()
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 use std::cmp;
498 use std::task::{Context, Poll};
499
500 #[test]
501 fn test_normalize() {
502 let success_cases = [
503 ("foo", Cow::Borrowed("foo")),
504 ("foo", Cow::Borrowed("foo")),
505 ("foo/", Cow::Borrowed("foo")),
506 ("./foo", Cow::Borrowed("foo")),
507 ("././foo/bar///", Cow::Borrowed("foo/bar")),
508 ("foo//bar", Cow::Owned("foo/bar".to_string())),
509 ("foo/./bar", Cow::Owned("foo/bar".to_string())),
510 ("foo/.///./bar", Cow::Owned("foo/bar".to_string())),
511 ("foo/bar/..", Cow::Owned("foo".to_string())),
512 ("foo/bar/../../baz", Cow::Owned("baz".to_string())),
513 ];
514 for (path, expected) in success_cases {
515 let normalized = super::normalize_dirhash_path(path).unwrap();
516 assert_eq!(normalized, expected);
517 }
518 let error_cases = [
519 "",
520 "/",
521 "/foo",
522 "///foo",
523 "..",
524 "foo/..",
525 "foo/bar/../../../baz",
526 ];
527 for path in error_cases {
528 super::normalize_dirhash_path(path).unwrap_err();
529 }
530 }
531
532 #[test]
533 fn test_add_update_and_add_empty_dir() {
534 let a_hash = blake3::hash(b"hello");
543 let c_hash = blake3::hash(b"goodbye");
544 let d_hash = blake3::derive_key("directory", b"");
545 let mut b_input = Vec::new();
546 b_input.extend_from_slice(b"c.txt\xff");
547 b_input.extend_from_slice(c_hash.as_bytes());
548 b_input.extend_from_slice(b"d\xff");
549 b_input.extend_from_slice(&d_hash);
550 let b_hash = blake3::derive_key("directory", &b_input);
551 let mut root_input = Vec::new();
552 root_input.extend_from_slice(b"a.txt\xff");
553 root_input.extend_from_slice(a_hash.as_bytes());
554 root_input.extend_from_slice(b"b\xff");
555 root_input.extend_from_slice(&b_hash);
556 let root_hash = blake3::derive_key("directory", &root_input);
557 assert_eq!(
559 blake3::Hash::from_bytes(root_hash).to_hex().as_str(),
560 "e508467d129e0d19cefa96527f5f6cb3760530be4d931c527f2818a0dff5d517"
561 );
562
563 let mut tree = super::DirhashTree::default();
565 tree.add_file("a.txt", a_hash).unwrap();
566 tree.add_file("b/c.txt", c_hash).unwrap();
567 tree.add_empty_dir("b/d").unwrap();
568 assert_eq!(tree.hash(), root_hash);
569
570 tree.update_file("b/c.txt", [0; 32].into()).unwrap();
572 assert_ne!(tree.hash(), root_hash);
573 tree.update_file("b/c.txt", c_hash).unwrap();
575 assert_eq!(tree.hash(), root_hash);
576
577 tree.add_empty_dir("b").unwrap(); assert_eq!(tree.hash(), root_hash);
580 tree.add_empty_dir("e").unwrap(); assert_ne!(tree.hash(), root_hash);
583 }
584
585 #[test]
586 fn test_dirhash_path() -> Result<(), super::DirhashError> {
587 let temp_dir = tempfile::tempdir()?;
597 let root = temp_dir.path();
598 fs_err::write(root.join("a.txt"), b"hello")?;
599 fs_err::create_dir(root.join("b"))?;
600 fs_err::write(root.join("b/c.txt"), b"goodbye")?;
601 fs_err::create_dir(root.join("b/d"))?;
602
603 let mut expected = super::DirhashTree::default();
604 expected.add_file("a.txt", blake3::hash(b"hello"))?;
605 expected.add_file("b/c.txt", blake3::hash(b"goodbye"))?;
606 expected.add_empty_dir("b/d")?;
607
608 assert_eq!(super::dirhash_path(root)?, expected.hash());
609 assert_eq!(
612 super::dirhash_path(&root.join("a.txt"))?,
613 blake3::hash(b"hello")
614 );
615 Ok(())
616 }
617
618 #[cfg(unix)]
619 #[test]
620 fn test_dirhash_path_symlinks() -> Result<(), super::DirhashError> {
621 use fs_err::os::unix::fs::symlink;
622
623 let temp_dir = tempfile::tempdir()?;
634 let root = temp_dir.path();
635 fs_err::create_dir(root.join("dir1"))?;
636 fs_err::create_dir(root.join("dir2"))?;
637 fs_err::write(root.join("dir1/file.txt"), b"hello")?;
638 symlink("../dir2", root.join("dir1/dir_link"))?;
639 symlink("../dir1/file.txt", root.join("dir2/file_link"))?;
640
641 let mut in_memory = super::DirhashTree::default();
642 in_memory.add_file("dir1/file.txt", blake3::hash(b"hello"))?;
643 in_memory.add_file("dir1/dir_link/file_link", blake3::hash(b"hello"))?;
644 in_memory.add_file("dir2/file_link", blake3::hash(b"hello"))?;
645 let from_disk = super::dirhash_path(root)?;
646 assert_eq!(in_memory.hash(), from_disk);
647
648 fs_err::create_dir(root.join("dir2/inner"))?;
650 symlink("../../dir1", root.join("dir2/inner/dir_link"))?;
651 let error = super::dirhash_path(root).unwrap_err();
652 std::assert_matches!(error, super::DirhashError::SymlinkCycle { .. });
653 Ok(())
654 }
655
656 fn paint_input(buf: &mut [u8]) {
659 let mut value = 0u8;
660 for byte in buf {
661 *byte = value;
662 value = if value == 250 { 0 } else { value + 1 };
663 }
664 }
665
666 #[tokio::test]
667 async fn test_blake3_copy() -> io::Result<()> {
668 let input = b"hello";
669 let mut output = Vec::new();
670 let (bytes_read, hash) = Box::pin(super::blake3_copy(&input[..], &mut output)).await?;
671 assert_eq!(bytes_read, input.len() as u64);
672 assert_eq!(input, &output[..]);
673 assert_eq!(hash, blake3::hash(input));
674
675 let mut big_input = vec![0; 64_000 * 3];
676 paint_input(&mut big_input);
677 let mut big_output = Vec::new();
678 let (big_bytes_read, big_hash) =
679 Box::pin(super::blake3_copy(&big_input[..], &mut big_output)).await?;
680 assert_eq!(big_bytes_read, big_input.len() as u64);
681 assert_eq!(big_input, big_output);
682 assert_eq!(big_hash, blake3::hash(&big_input));
683 Ok(())
684 }
685
686 struct ShortReader<'a>(&'a [u8]);
688
689 impl AsyncRead for ShortReader<'_> {
690 fn poll_read(
691 mut self: Pin<&mut Self>,
692 _cx: &mut Context<'_>,
693 buf: &mut tokio::io::ReadBuf<'_>,
694 ) -> Poll<io::Result<()>> {
695 const SHORT_READ_LEN: usize = 251; let want = cmp::min(self.0.len(), buf.remaining());
697 let take = cmp::min(want, SHORT_READ_LEN);
698 buf.put_slice(&self.0[..take]);
699 self.0 = &self.0[take..];
700 Poll::Ready(Ok(()))
701 }
702 }
703
704 #[tokio::test]
706 async fn test_blake3_copy_short_reader() -> io::Result<()> {
707 let mut input = vec![0; 64_000 * 3];
708 paint_input(&mut input);
709 let mut output = Vec::new();
710 let (bytes_read, hash) =
711 Box::pin(super::blake3_copy(ShortReader(&input), &mut output)).await?;
712 assert_eq!(bytes_read, input.len() as u64);
713 assert_eq!(input, &output[..]);
714 assert_eq!(hash, blake3::hash(&input));
715 Ok(())
716 }
717
718 fn walk_test_vector_input(
721 input_dir: &serde_json::Map<String, serde_json::Value>,
722 dirhash_tree: &mut DirhashTree,
723 tempdir: &tempfile::TempDir,
724 relative_path: Option<&str>,
726 ) -> anyhow::Result<()> {
727 for (name, file_or_dir) in input_dir {
728 let entry_path = match relative_path {
731 Some(parent) => &format!("{parent}/{name}"),
732 None => name,
733 };
734 match file_or_dir {
735 serde_json::Value::String(file_text) => {
737 fs_err::write(tempdir.path().join(entry_path), file_text)?;
739 dirhash_tree.add_file(entry_path, blake3::hash(file_text.as_bytes()))?;
741 }
742 serde_json::Value::Object(input_subdir) => {
744 fs_err::create_dir(tempdir.path().join(entry_path))?;
746 dirhash_tree.add_empty_dir(entry_path)?;
750 walk_test_vector_input(input_subdir, dirhash_tree, tempdir, Some(entry_path))?;
752 }
753 _ => panic!("unexpected JSON type"),
754 }
755 }
756 Ok(())
757 }
758
759 #[derive(Debug, serde::Deserialize)]
760 struct JsonTestVector {
761 input: serde_json::Map<String, serde_json::Value>,
763 dirhash: String,
765 }
766
767 #[tokio::test]
774 async fn test_vectors_json() -> anyhow::Result<()> {
775 let test_vectors: Vec<JsonTestVector> =
776 serde_json::from_str(include_str!("../test_vectors/test_vectors.json"))?;
777 for JsonTestVector { input, dirhash } in &test_vectors {
778 let mut tree = DirhashTree::new();
779 let tempdir = tempfile::tempdir()?;
780 walk_test_vector_input(
782 input, &mut tree, &tempdir, None, )?;
784 assert_eq!(dirhash.as_str(), tree.hash().to_hex().as_str());
785 assert_eq!(
786 dirhash.as_str(),
787 dirhash_path(tempdir.path())?.to_hex().as_str(),
788 );
789 }
790 Ok(())
791 }
792}