From 16412238c2b268904672d334dd67354a791e9ef2 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Tue, 14 Jul 2026 17:49:47 -0700 Subject: [PATCH 1/2] fix: rec_copy fails on root node --- packages/copper/Cargo.toml | 2 +- packages/copper/src/fs/dir.rs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/copper/Cargo.toml b/packages/copper/Cargo.toml index d8c156a..72b3650 100644 --- a/packages/copper/Cargo.toml +++ b/packages/copper/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pistonite-cu" -version = "0.9.0" +version = "0.9.1" edition = "2024" description = "Battery-included common utils to speed up development of rust tools" repository = "https://github.com/Pistonite/cu" diff --git a/packages/copper/src/fs/dir.rs b/packages/copper/src/fs/dir.rs index 7d39576..3c30019 100644 --- a/packages/copper/src/fs/dir.rs +++ b/packages/copper/src/fs/dir.rs @@ -335,8 +335,13 @@ fn rec_copy_inefficiently_impl(from: &Path, to: &Path) -> crate::Result<()> { walker.include_dir_entries(true); for entry in walker.walk()? { let entry = entry?; - let rel_path = cu::check!(entry.rel_path()?, "failed to get relative path for entry")?; + let rel_path = entry.rel_path()?; if !entry.is_dir() { + let rel_path = cu::check!( + rel_path, + "unexpected: failed to get relative path of file entry: '{}'", + entry.path().display() + )?; let Some(file_name) = entry.file_name() else { cu::trace!( "skipping file with no file name: '{}'", @@ -362,7 +367,10 @@ fn rec_copy_inefficiently_impl(from: &Path, to: &Path) -> crate::Result<()> { // copy the file crate::fs::copy(entry.path(), target)?; } else { - let target_path = to.join(rel_path); + let target_path = match rel_path { + Some(rel_path) => to.join(rel_path), + None => to.to_path_buf(), + }; make_dir_impl(&target_path)?; dir_cache.insert(target_path); } From 59644da2a1dc95c67ecc3eddc3ee3c45da7fc327 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Tue, 14 Jul 2026 17:58:03 -0700 Subject: [PATCH 2/2] make walk not return root --- packages/copper/src/fs/dir.rs | 15 +++----- packages/copper/src/fs/walk2.rs | 35 +++++++++-------- packages/copper/tests/walk2.rs | 66 ++++++++++++--------------------- 3 files changed, 47 insertions(+), 69 deletions(-) diff --git a/packages/copper/src/fs/dir.rs b/packages/copper/src/fs/dir.rs index 3c30019..49cb662 100644 --- a/packages/copper/src/fs/dir.rs +++ b/packages/copper/src/fs/dir.rs @@ -329,6 +329,11 @@ fn rec_copy_inefficiently_impl(from: &Path, to: &Path) -> crate::Result<()> { crate::bail!("target exists and is not a directory"); } + cu::check!( + make_dir_impl(to), + "rec_copy: failed to create target directory" + )?; + // cache paths that are known to be directories in `to` let mut dir_cache = BTreeSet::new(); let mut walker = crate::fs::walker(from); @@ -337,11 +342,6 @@ fn rec_copy_inefficiently_impl(from: &Path, to: &Path) -> crate::Result<()> { let entry = entry?; let rel_path = entry.rel_path()?; if !entry.is_dir() { - let rel_path = cu::check!( - rel_path, - "unexpected: failed to get relative path of file entry: '{}'", - entry.path().display() - )?; let Some(file_name) = entry.file_name() else { cu::trace!( "skipping file with no file name: '{}'", @@ -367,10 +367,7 @@ fn rec_copy_inefficiently_impl(from: &Path, to: &Path) -> crate::Result<()> { // copy the file crate::fs::copy(entry.path(), target)?; } else { - let target_path = match rel_path { - Some(rel_path) => to.join(rel_path), - None => to.to_path_buf(), - }; + let target_path = to.join(rel_path); make_dir_impl(&target_path)?; dir_cache.insert(target_path); } diff --git a/packages/copper/src/fs/walk2.rs b/packages/copper/src/fs/walk2.rs index 0126715..2d75d67 100644 --- a/packages/copper/src/fs/walk2.rs +++ b/packages/copper/src/fs/walk2.rs @@ -263,13 +263,12 @@ impl WalkBuilder { /// /// ```rust,no_run /// # use pistonite_cu as cu; -/// fn list() -> cu::Result<()> { -/// for entry in cu::fs::walk(".")? { -/// let entry = entry?; -/// cu::info!("depth {}: {}", entry.depth(), entry.path().display()); -/// } -/// Ok(()) +/// # fn list() -> cu::Result<()> { +/// for entry in cu::fs::walk(".")? { +/// let entry = entry?; +/// cu::info!("depth {}: {}", entry.depth(), entry.path().display()); /// } +/// # Ok(()) } /// ``` pub struct Walk { inner: IgnoreWalk, @@ -294,6 +293,10 @@ impl Walk { let (entry, file_type) = loop { let entry = crate::some!(self.inner.next()); let entry = crate::check!(entry, "walk: failed to read the next entry")?; + if entry.depth() == 0 { + // skip root node + continue; + } match entry.file_type() { None => { // stdin @@ -377,17 +380,14 @@ impl WalkEntry { /// /// ```rust,no_run /// # use pistonite_cu as cu; - /// fn print_relative() -> cu::Result<()> { - /// for entry in cu::fs::walk("src")? { - /// let entry = entry?; - /// if let Some(rel) = entry.rel_path()? { - /// cu::info!("{}", rel.display()); - /// } - /// } - /// Ok(()) + /// # fn print_relative() -> cu::Result<()> { + /// for entry in cu::fs::walk("src")? { + /// let entry = entry?; + /// cu::info!("{}", entry.rel_path()?.display()); /// } + /// # Ok(()) } /// ``` - pub fn rel_path(&self) -> cu::Result> { + pub fn rel_path(&self) -> cu::Result { // ensure root is a prefix of inner path let root_norm = self.root.normalize()?; // note we cannot normalize the path after join since it might be a symlink @@ -409,13 +409,12 @@ impl WalkEntry { )?; } let Some(next) = path_iter.next() else { - // is root - return Ok(None); + cu::bail!("unexpected: walk entry path is same as root"); }; let mut p = PathBuf::new(); p.push(next); p.extend(path_iter); - Ok(Some(p)) + Ok(p) } /// Get the file type diff --git a/packages/copper/tests/walk2.rs b/packages/copper/tests/walk2.rs index d5635cb..a8f01a3 100644 --- a/packages/copper/tests/walk2.rs +++ b/packages/copper/tests/walk2.rs @@ -110,14 +110,8 @@ fn collect(w: cu::fs::Walk) -> cu::Result> { let mut set = BTreeSet::new(); for entry in w { let entry = entry?; - match entry.rel_path()? { - Some(p) => { - set.insert(norm(&p)); - } - None => { - set.insert(".".to_string()); - } - } + let rel_path = entry.rel_path()?; + set.insert(norm(&rel_path)); } Ok(set) } @@ -163,8 +157,7 @@ fn include_dir_entries_true() -> cu::Result<()> { } // ...alongside the files... assert!(got.contains("sub/c.txt")); - // ...and the root entry (rel_path == None). - assert!(got.contains("."), "expected root entry in {got:?}"); + assert!(!got.contains(".")); Ok(()) } @@ -326,49 +319,38 @@ fn entry_metadata_depth_relpath() -> cu::Result<()> { let mut b = cu::fs::walker(fx.root()); b.include_dir_entries(true); - let mut saw_root = false; let mut saw_top_file = false; let mut saw_nested_file = false; let mut saw_dir = false; for entry in b.walk()? { let entry = entry?; - match entry.rel_path()? { - None => { - // the root entry - assert_eq!(entry.depth(), 0, "root depth should be 0"); - assert!(entry.is_dir()); - saw_root = true; + assert!(entry.depth() > 0, "should not emit root entry"); + let rel = norm(&entry.rel_path()?); + // rel_path must never carry a leading "./" + assert!(!rel.starts_with("./"), "rel_path has leading ./: {rel}"); + match rel.as_str() { + "a.txt" => { + assert_eq!(entry.depth(), 1); + assert!(entry.is_file()); + assert_eq!(entry.file_name().unwrap(), "a.txt"); + entry.metadata()?; + saw_top_file = true; + } + "sub/nested/e.txt" => { + assert_eq!(entry.depth(), 3, "nested file should be depth 3"); + assert!(entry.is_file()); + saw_nested_file = true; } - Some(rel) => { - let rel = norm(&rel); - // rel_path must never carry a leading "./" - assert!(!rel.starts_with("./"), "rel_path has leading ./: {rel}"); - match rel.as_str() { - "a.txt" => { - assert_eq!(entry.depth(), 1); - assert!(entry.is_file()); - assert_eq!(entry.file_name().unwrap(), "a.txt"); - entry.metadata()?; - saw_top_file = true; - } - "sub/nested/e.txt" => { - assert_eq!(entry.depth(), 3, "nested file should be depth 3"); - assert!(entry.is_file()); - saw_nested_file = true; - } - "sub" => { - assert_eq!(entry.depth(), 1); - assert!(entry.is_dir()); - saw_dir = true; - } - _ => {} - } + "sub" => { + assert_eq!(entry.depth(), 1); + assert!(entry.is_dir()); + saw_dir = true; } + _ => {} } } - assert!(saw_root, "did not observe root entry"); assert!(saw_top_file, "did not observe a.txt"); assert!(saw_nested_file, "did not observe sub/nested/e.txt"); assert!(saw_dir, "did not observe sub dir");