Repository: pkolaczk/fclones (language: rust)

Issue:
Single quotes in input path names cause malformed header error
A minimal reproducible example using PowerShell on Windows 10:

```
mkdir "test'dir"
./fclones group "test'dir" > dupes.txt
Get-Content dupes.txt | ./fclones remove
```

This script fails in the removal stage:

```
fclones.exe: error: Input error: Malformed header: Failed to parse command arguments: Unclosed single quote
```

The same thing happens if I pipe the output of `fclones group` directly to `fclones remove`. Here's the content of `dupes.txt`:

```
# Report by fclones 0.29.1
# Timestamp: [removed for privacy]
# Command: 'C:\Software\fclones-0.29.1\fclones.exe' group 'test\'dir'
# Base dir: C:\\Software\\fclones-0.29.1
# Total: 0 B (0 B) in 0 files in 0 groups
# Redundant: 0 B (0 B) in 0 files
# Missing: 0 B (0 B) in 0 files
```

If I change the third line to this:

```
# Command: 'C:\Software\fclones-0.29.1\fclones.exe' group "test'dir"
```

then `fclones remove` works fine.

BTW, thanks for this great little piece of software!


Discussion / hints:
Confirmed, thank you for reporting.

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 56a7f09..56d6be6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -443,7 +443,7 @@ dependencies = [
 
 [[package]]
 name = "fclones"
-version = "0.29.1"
+version = "0.29.2"
 dependencies = [
  "assert_matches",
  "atomic-counter",
diff --git a/Cargo.toml b/Cargo.toml
index 8ee8431..270cbfb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "fclones"
-version = "0.29.1"
+version = "0.29.2"
 description = "Finds and removes duplicate files"
 authors = ["Piotr Kołaczkowski <pkolaczk@gmail.com>"]
 homepage = "https://github.com/pkolaczk/fclones"
diff --git a/src/arg.rs b/src/arg.rs
index 31aac4a..e23b7c7 100644
--- a/src/arg.rs
+++ b/src/arg.rs
@@ -140,11 +140,11 @@ pub fn quote(s: OsString) -> String {
     let lossy = s.to_string_lossy();
     if lossy
         .chars()
-        .any(|c| c < '\u{20}' || c == '\u{7f}' || c == '\u{fffd}')
+        .any(|c| c < '\u{20}' || c == '\u{7f}' || c == '\u{fffd}' || c == '\'')
     {
-        format!("$'{}'", to_stfu8(s))
+        format!("$'{}'", to_stfu8(s).replace('\'', "\\'"))
     } else if lossy.chars().any(|c| SPECIAL_CHARS.contains(&c)) {
-        format!("'{}'", lossy.replace('\'', "\\'"))
+        format!("'{lossy}'")
     } else {
         lossy.to_string()
     }
@@ -349,9 +349,9 @@ pub fn split(s: &str) -> Result<Vec<Arg>, ParseError> {
                 None => return Err(ParseError::new("Unclosed single quote")),
                 Some('\\') => DollarQuotedBackslash,
                 Some('\'') => {
-                    let quoted_slice = &s[dollar_quote_start..pos];
+                    let quoted_slice = &s[dollar_quote_start..pos].replace("\\'", "'");
                     let decoded = from_stfu8(quoted_slice).map_err(|e| {
-                        ParseError::new(format!("Failed to decode STFU-8 chunk: {}", e).as_str())
+                        ParseError::new(format!("Failed to decode STFU-8 chunk: {e}").as_str())
                     })?;
                     word.push(decoded.as_os_str());
                     Unquoted
@@ -406,6 +406,12 @@ mod test {
         assert_eq!(quote(OsString::from("a\\b")), "'a\\b'");
     }
 
+    #[test]
+    fn quote_path_with_single_quotes() {
+        assert_eq!(quote(OsString::from("a'b")), "$'a\\'b'");
+        assert_eq!(quote(OsString::from("a'b'")), "$'a\\'b\\''");
+    }
+
     #[test]
     fn split_unquoted_args() {
         assert_eq!(
@@ -438,6 +444,14 @@ mod test {
         )
     }
 
+    #[test]
+    fn split_escaped_single_quote() {
+        assert_eq!(
+            split("$'single\\'quote'").unwrap(),
+            vec![Arg::from("single'quote")]
+        );
+    }
+
     #[test]
     fn split_spaces_escaping() {
         assert_eq!(
diff --git a/src/cache.rs b/src/cache.rs
index 78d5c4a..f083f4b 100644
--- a/src/cache.rs
+++ b/src/cache.rs
@@ -49,14 +49,14 @@ impl HashCache {
         transform: Option<&str>,
         algorithm: HashFn,
     ) -> Result<HashCache, Error> {
-        create_dir_all(&database_path.to_path_buf()).map_err(|e| {
+        create_dir_all(database_path.to_path_buf()).map_err(|e| {
             format!(
                 "Count not create hash database directory {}: {}",
                 database_path.to_escaped_string(),
                 e
             )
         })?;
-        let db = sled::open(&database_path.to_path_buf()).map_err(|e| {
+        let db = sled::open(database_path.to_path_buf()).map_err(|e| {
             format!(
                 "Failed to open hash database at {}: {}",
                 database_path.to_escaped_string(),
@@ -89,7 +89,7 @@ impl HashCache {
         let value = CachedFileInfo {
             modified_timestamp_ms: file
                 .modified()
-                .map_err(|e| format!("Unable to get file modification timestamp: {}", e))?
+                .map_err(|e| format!("Unable to get file modification timestamp: {e}"))?
                 .duration_since(UNIX_EPOCH)
                 .unwrap_or(Duration::ZERO)
                 .as_millis() as u64,
@@ -100,7 +100,7 @@ impl HashCache {
 
         self.cache
             .insert(key, &value)
-            .map_err(|e| format!("Failed to write entry to cache: {}", e))?;
+            .map_err(|e| format!("Failed to write entry to cache: {e}"))?;
         Ok(())
     }
 
@@ -117,7 +117,7 @@ impl HashCache {
         let value = self
             .cache
             .get(key)
-            .map_err(|e| format!("Failed to retrieve entry from cache: {}", e))?;
+            .map_err(|e| format!("Failed to retrieve entry from cache: {e}"))?;
         let value = match value {
             Some(v) => v,
             None => return Ok(None), // not found in cache
@@ -125,7 +125,7 @@ impl HashCache {
 
         let modified = metadata
             .modified()
-            .map_err(|e| format!("Unable to get file modification timestamp: {}", e))?
+            .map_err(|e| format!("Unable to get file modification timestamp: {e}"))?
             .duration_since(UNIX_EPOCH)
             .unwrap_or(Duration::ZERO)
             .as_millis() as u64;
diff --git a/src/config.rs b/src/config.rs
index d9e40ef..95d02c0 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -57,7 +57,7 @@ impl FromStr for OutputFormat {
             "fdupes" => Ok(OutputFormat::Fdupes),
             "csv" => Ok(OutputFormat::Csv),
             "json" => Ok(OutputFormat::Json),
-            s => Err(format!("Unrecognized output format: {}", s)),
+            s => Err(format!("Unrecognized output format: {s}")),
         }
     }
 }
@@ -102,7 +102,7 @@ fn parse_date_time(s: &str) -> Result<DateTime<FixedOffset>, String> {
             let local_offset = *Local::now().offset();
             Ok(DateTime::from_utc(dt, local_offset))
         }
-        Err(e) => Err(format!("Failed to parse {} as date: {}", s, e)),
+        Err(e) => Err(format!("Failed to parse {s} as date: {e}")),
     }
 }
 
@@ -118,7 +118,7 @@ fn parse_thread_count_option(s: &str) -> Result<(OsString, Parallelism), String>
     let value = value.to_string();
     let mut pool_sizes = value
         .split(',')
-        .map(|v| v.parse::<usize>().map_err(|e| format!("{}: {}", e, v)));
+        .map(|v| v.parse::<usize>().map_err(|e| format!("{e}: {v}")));
 
     let random = match pool_sizes.next() {
         Some(v) => v?,
@@ -565,7 +565,7 @@ impl FromStr for Priority {
             "least-recently-accessed" | "lra" => Ok(Priority::LeastRecentlyAccessed),
             "most-nested" => Ok(Priority::MostNested),
             "least-nested" => Ok(Priority::LeastNested),
-            _ => Err(format!("Unrecognized priority: {}", s)),
+            _ => Err(format!("Unrecognized priority: {s}")),
         }
     }
 }
diff --git a/src/dedupe.rs b/src/dedupe.rs
index ec8bd11..cd9a724 100644
--- a/src/dedupe.rs
+++ b/src/dedupe.rs
@@ -152,7 +152,7 @@ impl FsCommand {
     }
 
     fn hardlink(target: &Path, link: &Path) -> io::Result<()> {
-        fs::hard_link(&target.to_path_buf(), &link.to_path_buf()).map_err(|e| {
+        fs::hard_link(target.to_path_buf(), link.to_path_buf()).map_err(|e| {
             io::Error::new(
                 e.kind(),
                 format!(
@@ -191,7 +191,7 @@ impl FsCommand {
     /// Renames/moves a file from one location to another.
     /// If the target exists, it would be overwritten.
     pub fn unsafe_rename(source: &Path, target: &Path) -> io::Result<()> {
-        fs::rename(&source.to_path_buf(), &target.to_path_buf()).map_err(|e| {
+        fs::rename(source.to_path_buf(), target.to_path_buf()).map_err(|e| {
             io::Error::new(
                 e.kind(),
                 format!(
@@ -207,7 +207,7 @@ impl FsCommand {
     /// Copies a file from one location to another.
     /// If the target exists, it would be overwritten.
     fn unsafe_copy(source: &Path, target: &Path) -> io::Result<()> {
-        fs::copy(&source.to_path_buf(), &target.to_path_buf()).map_err(|e| {
+        fs::copy(source.to_path_buf(), target.to_path_buf()).map_err(|e| {
             io::Error::new(
                 e.kind(),
                 format!(
@@ -353,14 +353,14 @@ impl FsCommand {
         match self {
             FsCommand::Remove { file, .. } => {
                 let path = file.path.quote();
-                result.push(format!("rm {}", path));
+                result.push(format!("rm {path}"));
             }
             FsCommand::SoftLink { target, link, .. } => {
                 let tmp = Self::temp_file(&link.path);
                 let target = target.path.quote();
                 let link = link.path.quote();
                 result.push(format!("mv {} {}", link, tmp.quote()));
-                result.push(format!("ln -s {} {}", target, link));
+                result.push(format!("ln -s {target} {link}"));
                 result.push(format!("rm {}", tmp.quote()));
             }
             FsCommand::HardLink { target, link, .. } => {
@@ -368,7 +368,7 @@ impl FsCommand {
                 let target = target.path.quote();
                 let link = link.path.quote();
                 result.push(format!("mv {} {}", link, tmp.quote()));
-                result.push(format!("ln {} {}", target, link));
+                result.push(format!("ln {target} {link}"));
                 result.push(format!("rm {}", tmp.quote()));
             }
             FsCommand::RefLink { target, link, .. } => {
@@ -377,7 +377,7 @@ impl FsCommand {
                 let link = link.path.quote();
                 // Not really what happens on Linux, there the `mv` is also a reflink.
                 result.push(format!("mv {} {}", link, tmp.quote()));
-                result.push(format!("cp --reflink=always {} {}", target, link));
+                result.push(format!("cp --reflink=always {target} {link}"));
                 result.push(format!("rm {}", tmp.quote()));
             }
             FsCommand::Move {
@@ -659,9 +659,7 @@ impl PartitionedFileGroup {
         let root = source_path
             .root()
             .to_string_lossy()
-            .replace('/', "")
-            .replace('\\', "")
-            .replace(':', "");
+            .replace(['/', '\\', ':'], "");
         let suffix = source_path.strip_root();
         if root.is_empty() {
             target_dir.join(suffix)
@@ -1016,7 +1014,7 @@ pub fn log_script(
                     processed_count += 1;
                     reclaimed_space += cmd.space_to_reclaim();
                     for line in cmd.to_shell_str() {
-                        writeln!(out, "{}", line)?;
+                        writeln!(out, "{line}")?;
                     }
                 }
             }
@@ -1033,6 +1031,7 @@ pub fn log_script(
 #[cfg(test)]
 mod test {
     use std::collections::HashSet;
+    use std::default::Default;
     use std::fs::{create_dir, create_dir_all};
     use std::path::PathBuf;
     use std::str::FromStr;
@@ -1193,7 +1192,7 @@ mod test {
         let ctime_2 = create_file_newer_than(&file_2, ctime_1);
         create_file_newer_than(&file_3, ctime_2);
 
-        let group = FileGroup {
+        FileGroup {
             file_len: FileLen(0),
             file_hash,
             files: vec![
@@ -1201,8 +1200,7 @@ mod test {
                 Path::from(&file_2),
                 Path::from(&file_3),
             ],
-        };
-        group
+        }
     }
 
     #[test]
@@ -1220,8 +1218,10 @@ mod test {
     fn test_partition_bails_out_if_file_modified_too_late() {
         with_dir("dedupe/partition/modification", |root| {
             let group = make_group(root, FileHash::from_str("00").unwrap());
-            let mut config = DedupeConfig::default();
-            config.modified_before = Some(DateTime::from(Local::now() - Duration::days(1)));
+            let config = DedupeConfig {
+                modified_before: Some(DateTime::from(Local::now() - Duration::days(1))),
+                ..DedupeConfig::default()
+            };
             let partitioned = partition(group, &config, &StdLog::new());
             assert!(partitioned.is_err());
         })
@@ -1233,24 +1233,17 @@ mod test {
             let group = make_group(root, FileHash::from_str("00").unwrap());
             let path = group.files[0].clone();
             write_file(&path.to_path_buf(), "foo");
-
-            let mut config = DedupeConfig::default();
-            config.priority = vec![Priority::MostRecentlyModified];
+            let config = DedupeConfig {
+                priority: vec![Priority::MostRecentlyModified],
+                ..DedupeConfig::default()
+            };
             let partitioned = partition(group, &config, &StdLog::new()).unwrap();
-            assert!(partitioned
-                .to_drop
-                .iter()
-                .find(|m| m.path == path)
-                .is_none());
-            assert!(partitioned
-                .to_keep
-                .iter()
-                .find(|m| m.path == path)
-                .is_none());
+            assert!(!partitioned.to_drop.iter().any(|m| m.path == path));
+            assert!(!partitioned.to_keep.iter().any(|m| m.path == path));
         })
     }
 
-    fn path_set(v: &Vec<PathAndMetadata>) -> HashSet<&Path> {
+    fn path_set(v: &[PathAndMetadata]) -> HashSet<&Path> {
         v.iter().map(|f| &f.path).collect()
     }
 
@@ -1263,11 +1256,13 @@ mod test {
                 return;
             }
             let group = make_group(root, FileHash::from_str("00").unwrap());
-            let mut config = DedupeConfig::default();
-            config.priority = vec![Priority::Newest];
+            let mut config = DedupeConfig {
+                priority: vec![Priority::Newest],
+                ..DedupeConfig::default()
+            };
             let partitioned_1 = partition(group.clone(), &config, &StdLog::new()).unwrap();
             config.priority = vec![Priority::Oldest];
-            let partitioned_2 = partition(group.clone(), &config, &StdLog::new()).unwrap();
+            let partitioned_2 = partition(group, &config, &StdLog::new()).unwrap();
 
             assert_ne!(
                 path_set(&partitioned_1.to_keep),
@@ -1289,11 +1284,17 @@ mod test {
             let path = group.files[0].clone();
             write_file(&path.to_path_buf(), "foo");
 
-            let mut config = DedupeConfig::default();
-            config.priority = vec![Priority::MostRecentlyModified];
+            let config = DedupeConfig {
+                priority: vec![Priority::MostRecentlyModified],
+                ..DedupeConfig::default()
+            };
             let partitioned_1 = partition(group.clone(), &config, &StdLog::new()).unwrap();
-            config.priority = vec![Priority::LeastRecentlyModified];
-            let partitioned_2 = partition(group.clone(), &config, &StdLog::new()).unwrap();
+
+            let config = DedupeConfig {
+                priority: vec![Priority::LeastRecentlyModified],
+                ..DedupeConfig::default()
+            };
+            let partitioned_2 = partition(group, &config, &StdLog::new()).unwrap();
 
             assert_ne!(
                 path_set(&partitioned_1.to_keep),
@@ -1310,9 +1311,11 @@ mod test {
     fn test_partition_respects_keep_patterns() {
         with_dir("dedupe/partition/keep", |root| {
             let group = make_group(root, FileHash::from_str("00").unwrap());
-            let mut config = DedupeConfig::default();
-            config.priority = vec![Priority::LeastRecentlyModified];
-            config.keep_name_patterns = vec![Pattern::glob("*_1").unwrap()];
+            let mut config = DedupeConfig {
+                priority: vec![Priority::LeastRecentlyModified],
+                keep_name_patterns: vec![Pattern::glob("*_1").unwrap()],
+                ..DedupeConfig::default()
+            };
             let p = partition(group.clone(), &config, &StdLog::new()).unwrap();
             assert_eq!(p.to_keep.len(), 1);
             assert_eq!(&p.to_keep[0].path, &group.files[0]);
@@ -1329,9 +1332,11 @@ mod test {
     fn test_partition_respects_drop_patterns() {
         with_dir("dedupe/partition/drop", |root| {
             let group = make_group(root, FileHash::from_str("00").unwrap());
-            let mut config = DedupeConfig::default();
-            config.priority = vec![Priority::LeastRecentlyModified];
-            config.name_patterns = vec![Pattern::glob("*_3").unwrap()];
+            let mut config = DedupeConfig {
+                priority: vec![Priority::LeastRecentlyModified],
+                name_patterns: vec![Pattern::glob("*_3").unwrap()],
+                ..DedupeConfig::default()
+            };
             let p = partition(group.clone(), &config, &StdLog::new()).unwrap();
             assert_eq!(p.to_drop.len(), 1);
             assert_eq!(&p.to_drop[0].path, &group.files[2]);
@@ -1360,10 +1365,12 @@ mod test {
                 files: group1.files.into_iter().chain(group2.files).collect(),
             };
 
-            let mut config = DedupeConfig::default();
-            config.isolated_roots = vec![Path::from(&root1), Path::from(&root2)];
+            let config = DedupeConfig {
+                isolated_roots: vec![Path::from(&root1), Path::from(&root2)],
+                ..DedupeConfig::default()
+            };
 
-            let p = partition(group.clone(), &config, &StdLog::new()).unwrap();
+            let p = partition(group, &config, &StdLog::new()).unwrap();
             assert_eq!(p.to_drop.len(), 3);
             assert!(p
                 .to_drop
@@ -1407,7 +1414,7 @@ mod test {
             };
 
             let config = DedupeConfig::default();
-            let p = partition(group.clone(), &config, &StdLog::new()).unwrap();
+            let p = partition(group, &config, &StdLog::new()).unwrap();
 
             // drop A files because file_a2 appears after file_b1 in the files vector
             assert_eq!(p.to_drop.len(), 2);
@@ -1431,8 +1438,10 @@ mod test {
             log.log_stderr_to_stdout = true;
 
             let group = make_group(root, FileHash::from_str("00").unwrap());
-            let mut config = DedupeConfig::default();
-            config.priority = vec![Priority::LeastRecentlyModified];
+            let config = DedupeConfig {
+                priority: vec![Priority::LeastRecentlyModified],
+                ..DedupeConfig::default()
+            };
             let script = dedupe(vec![group], DedupeOp::Remove, &config, &log);
             let dedupe_result = run_script(script, !config.no_lock, &log);
             assert_eq!(dedupe_result.processed_count, 2);
@@ -1454,8 +1463,10 @@ mod test {
             let group_3 = make_group(&root.join("group_3"), FileHash::from_str("02").unwrap());
             let groups = vec![group_1, group_2, group_3];
 
-            let mut config = DedupeConfig::default();
-            config.priority = vec![Priority::LeastRecentlyModified];
+            let config = DedupeConfig {
+                priority: vec![Priority::LeastRecentlyModified],
+                ..DedupeConfig::default()
+            };
 
             let script = dedupe(groups, DedupeOp::Remove, &config, &log);
 
@@ -1495,8 +1506,10 @@ mod test {
             fs::hard_link(&file_a1, &file_a2).unwrap();
             fs::hard_link(&file_b1, &file_b2).unwrap();
 
-            let mut group_config = GroupConfig::default();
-            group_config.paths = vec![Path::from(root)];
+            let group_config = GroupConfig {
+                paths: vec![Path::from(root)],
+                ..GroupConfig::default()
+            };
 
             let groups = group_files(&group_config, &log).unwrap();
             let dedupe_config = DedupeConfig::default();
@@ -1537,9 +1550,11 @@ mod test {
             fs::symlink(&file_a1, &file_a2).unwrap();
             fs::symlink(&file_b1, &file_b2).unwrap();
 
-            let mut group_config = GroupConfig::default();
-            group_config.paths = vec![Path::from(root)];
-            group_config.symbolic_links = true;
+            let group_config = GroupConfig {
+                paths: vec![Path::from(root)],
+                symbolic_links: true,
+                ..GroupConfig::default()
+            };
 
             let groups = group_files(&group_config, &log).unwrap();
             let dedupe_config = DedupeConfig::default();
diff --git a/src/file.rs b/src/file.rs
index 619aef9..4b2be56 100644
--- a/src/file.rs
+++ b/src/file.rs
@@ -201,7 +201,7 @@ impl FileId {
     #[cfg(unix)]
     pub fn new(file: &Path) -> io::Result<FileId> {
         use std::os::unix::fs::MetadataExt;
-        match fs::metadata(&file.to_path_buf()) {
+        match fs::metadata(file.to_path_buf()) {
             Ok(metadata) => Ok(FileId {
                 inode: metadata.ino(),
                 device: metadata.dev(),
@@ -293,7 +293,7 @@ pub struct FileMetadata {
 impl FileMetadata {
     pub fn new(path: &Path) -> io::Result<FileMetadata> {
         let path_buf = path.to_path_buf();
-        let metadata = fs::metadata(&path_buf).map_err(|e| {
+        let metadata = fs::metadata(path_buf).map_err(|e| {
             io::Error::new(
                 e.kind(),
                 format!("Failed to read metadata of {}: {}", path.display(), e),
@@ -363,7 +363,7 @@ impl FileInfo {
             path,
             id,
             len: file_len,
-            location: device_index << 48 | inode_id as u64 & OFFSET_MASK,
+            location: device_index << 48 | inode_id & OFFSET_MASK,
         })
     }
 
@@ -420,7 +420,7 @@ pub(crate) fn file_info_or_log_err(
 /// Returns the physical offset of the first data block of the file
 #[cfg(target_os = "linux")]
 pub(crate) fn get_physical_file_location(path: &Path) -> io::Result<Option<u64>> {
-    let mut extents = fiemap::fiemap(&path.to_path_buf())?;
+    let mut extents = fiemap::fiemap(path.to_path_buf())?;
     match extents.next() {
         Some(fe) => Ok(Some(fe?.fe_physical)),
         None => Ok(None),
@@ -582,7 +582,7 @@ mod test {
     #[test]
     fn test_format_bytes() {
         let file_len = FileLen(16000);
-        let human_readable = format!("{}", file_len);
+        let human_readable = format!("{file_len}");
         assert_eq!(human_readable, "16.0 KB");
     }
 }
diff --git a/src/group.rs b/src/group.rs
index 930faa6..08df6c7 100644
--- a/src/group.rs
+++ b/src/group.rs
@@ -133,13 +133,13 @@ impl<'a> GroupCtx<'a> {
         let transform = match config.transform() {
             None => None,
             Some(Ok(transform)) => Some(transform),
-            Some(Err(e)) => return Err(Error::new(format!("Invalid transform: {}", e))),
+            Some(Err(e)) => return Err(Error::new(format!("Invalid transform: {e}"))),
         };
         let base_dir = Path::from(current_dir().unwrap_or_default());
         let group_filter = config.group_filter();
         let path_selector = config
             .path_selector(&base_dir)
-            .map_err(|e| format!("Invalid pattern: {}", e))?;
+            .map_err(|e| format!("Invalid pattern: {e}"))?;
         let hasher = if config.cache {
             FileHasher::new_cached(config.hash_fn, transform, log)?
         } else {
@@ -171,12 +171,11 @@ impl<'a> GroupCtx<'a> {
             let name = name.to_string_lossy();
             match name.strip_prefix("dev:") {
                 Some(name) if devices.get_by_name(OsStr::new(name)).is_none() => {
-                    return Err(Error::new(format!("Unknown device: {}", name)));
+                    return Err(Error::new(format!("Unknown device: {name}")));
                 }
                 None if !allowed_pool_names.contains(&name.as_ref()) => {
                     return Err(Error::new(format!(
-                        "Unknown thread pool or device type: {}",
-                        name
+                        "Unknown thread pool or device type: {name}"
                     )));
                 }
                 _ => {}
@@ -1499,7 +1498,7 @@ mod test {
                     },
                     len: FileLen(0),
                     location: i as u64,
-                    path: Path::from(format!("file{}", i)),
+                    path: Path::from(format!("file{i}")),
                 }],
             })
         }
@@ -1539,8 +1538,10 @@ mod test {
             write_test_file(&file2, b"aaa", b"", b"");
 
             let log = test_log();
-            let mut config = GroupConfig::default();
-            config.paths = vec![file1.into(), file2.into()];
+            let config = GroupConfig {
+                paths: vec![file1.into(), file2.into()],
+                ..GroupConfig::default()
+            };
             let results = group_files(&config, &log).unwrap();
             assert_eq!(results.len(), 1);
             assert_eq!(results[0].file_len, FileLen(3));
@@ -1557,9 +1558,10 @@ mod test {
             write_test_file(&file2, &[0; MAX_PREFIX_LEN], &[1; 4096], &[2; 4096]);
 
             let log = test_log();
-            let mut config = GroupConfig::default();
-            config.paths = vec![file1.into(), file2.into()];
-
+            let config = GroupConfig {
+                paths: vec![file1.into(), file2.into()],
+                ..GroupConfig::default()
+            };
             let results = group_files(&config, &log).unwrap();
             assert_eq!(results.len(), 1);
             assert_eq!(results[0].files.len(), 2);
@@ -1578,14 +1580,16 @@ mod test {
             let file2 = Path::from(file2);
 
             let log = test_log();
-            let mut config = GroupConfig::default();
-            config.paths = vec![file1.clone(), file2.clone()];
-            config.rf_over = Some(0);
+            let config = GroupConfig {
+                paths: vec![file1.clone(), file2.clone()],
+                rf_over: Some(0),
+                ..GroupConfig::default()
+            };
 
             let results = group_files(&config, &log).unwrap();
             assert_eq!(results.len(), 2);
-            assert_eq!(results[0].paths(), vec![Path::from(file1.canonicalize())]);
-            assert_eq!(results[1].paths(), vec![Path::from(file2.canonicalize())]);
+            assert_eq!(results[0].paths(), vec![file1.canonicalize()]);
+            assert_eq!(results[1].paths(), vec![file2.canonicalize()]);
         });
     }
 
@@ -1598,9 +1602,11 @@ mod test {
             write_test_file(&file2, b"bbb", b"", b"");
 
             let log = test_log();
-            let mut config = GroupConfig::default();
-            config.paths = vec![file1.into(), file2.into()];
-            config.unique = true;
+            let config = GroupConfig {
+                paths: vec![file1.into(), file2.into()],
+                unique: true,
+                ..GroupConfig::default()
+            };
 
             let results = group_files(&config, &log).unwrap();
             assert_eq!(results.len(), 2);
@@ -1620,9 +1626,11 @@ mod test {
             write_test_file(&file2, &prefix, &mid, b"suffix2");
 
             let log = test_log();
-            let mut config = GroupConfig::default();
-            config.paths = vec![file1.into(), file2.into()];
-            config.unique = true;
+            let config = GroupConfig {
+                paths: vec![file1.into(), file2.into()],
+                unique: true,
+                ..GroupConfig::default()
+            };
 
             let results = group_files(&config, &log).unwrap();
             assert_eq!(results.len(), 2);
@@ -1642,9 +1650,11 @@ mod test {
             write_test_file(&file2, &prefix, b"middle2", &suffix);
 
             let log = test_log();
-            let mut config = GroupConfig::default();
-            config.paths = vec![file1.into(), file2.into()];
-            config.unique = true;
+            let config = GroupConfig {
+                paths: vec![file1.into(), file2.into()],
+                unique: true,
+                ..GroupConfig::default()
+            };
 
             let results = group_files(&config, &log).unwrap();
             assert_eq!(results.len(), 2);
@@ -1662,10 +1672,11 @@ mod test {
             hard_link(&file1, &file2).unwrap();
 
             let log = test_log();
-
-            let mut config = GroupConfig::default();
-            config.paths = vec![file1.into(), file2.into()];
-            config.unique = true; // hardlinks to a common file should be treated as one file
+            let config = GroupConfig {
+                paths: vec![file1.into(), file2.into()],
+                unique: true, // hardlinks to a common file should be treated as one file
+                ..GroupConfig::default()
+            };
 
             let results = group_files(&config, &log).unwrap();
             assert_eq!(results.len(), 1);
@@ -1683,13 +1694,14 @@ mod test {
             std::os::unix::fs::symlink(&file1, &file2).unwrap();
 
             let log = test_log();
-            let mut config = GroupConfig::default();
-
-            // If both hard_links and symbolic_links is set to true, symbolic links should
-            // be treated as duplicates.
-            config.paths = vec![file1.into(), file2.into()];
-            config.match_links = true;
-            config.symbolic_links = true;
+            let mut config = GroupConfig {
+                paths: vec![file1.into(), file2.into()],
+                // If both hard_links and symbolic_links is set to true, symbolic links should
+                // be treated as duplicates.
+                match_links: true,
+                symbolic_links: true,
+                ..GroupConfig::default()
+            };
 
             let results = group_files(&config, &log).unwrap();
             assert_eq!(results.len(), 1);
@@ -1718,11 +1730,14 @@ mod test {
             let file1 = root.join("file1");
             write_test_file(&file1, b"foo", b"", b"");
             let log = test_log();
-            let mut config = GroupConfig::default();
+
             let file1 = Path::from(file1);
-            config.paths = vec![file1.clone(), file1.clone(), file1.clone()];
-            config.unique = true;
-            config.match_links = true;
+            let config = GroupConfig {
+                paths: vec![file1.clone(), file1.clone(), file1],
+                match_links: true,
+                unique: true,
+                ..GroupConfig::default()
+            };
 
             let results = group_files(&config, &log).unwrap();
             assert_eq!(results.len(), 1);
@@ -1737,17 +1752,19 @@ mod test {
 
         with_dir("main/duplicate_input_files_non_canonical", |root| {
             let dir = root.join("dir");
-            symlink(&root, &dir).unwrap();
+            symlink(root, dir).unwrap();
 
             let file1 = root.join("file1");
             let file2 = root.join("dir/file1");
             write_test_file(&file1, b"foo", b"", b"");
 
             let log = test_log();
-            let mut config = GroupConfig::default();
-            config.paths = vec![file1.into(), file2.into()];
-            config.unique = true;
-            config.match_links = true;
+            let config = GroupConfig {
+                paths: vec![file1.into(), file2.into()],
+                match_links: true,
+                unique: true,
+                ..GroupConfig::default()
+            };
 
             let results = group_files(&config, &log).unwrap();
             assert_eq!(results.len(), 1);
@@ -1770,9 +1787,11 @@ mod test {
             write_test_file(&file2, b"foo", b"", b"");
 
             let log = test_log();
-            let mut config = GroupConfig::default();
-            config.paths = vec![root1.into(), root2.into()];
-            config.isolate = true;
+            let mut config = GroupConfig {
+                paths: vec![root1.into(), root2.into()],
+                isolate: true,
+                ..GroupConfig::default()
+            };
 
             let results = group_files(&config, &log).unwrap();
             assert_eq!(results.len(), 0);
@@ -1796,10 +1815,12 @@ mod test {
             write_file(&input_path_2, "aa|23456");
 
             let log = test_log();
-            let mut config = GroupConfig::default();
-            config.paths = vec![input_path_1.into(), input_path_2.into()];
-            // a transform that takes only the first two bytes of each file
-            config.transform = Some("dd count=2 bs=1".to_string());
+            let config = GroupConfig {
+                paths: vec![input_path_1.into(), input_path_2.into()],
+                // a transform that takes only the first two bytes of each file
+                transform: Some("dd count=2 bs=1".to_string()),
+                ..GroupConfig::default()
+            };
 
             let results = group_files(&config, &log).unwrap();
             assert_eq!(results.len(), 1);
@@ -1820,9 +1841,11 @@ mod test {
             let file3 = Path::from(file3);
 
             let log = test_log();
-            let mut config = GroupConfig::default();
-            config.unique = true;
-            config.paths = vec![file1.into(), file2.into(), file3.clone()];
+            let config = GroupConfig {
+                unique: true,
+                paths: vec![file1.into(), file2.into(), file3.clone()],
+                ..GroupConfig::default()
+            };
             let results = group_files(&config, &log).unwrap();
             assert_eq!(results.len(), 1);
             assert_eq!(results[0].files[0].path, file3);
@@ -1837,10 +1860,13 @@ mod test {
 
             let report_file = root.join("report.txt");
             let log = test_log();
-            let mut config = GroupConfig::default();
-            config.paths = vec![file.into()];
-            config.unique = true;
-            config.output = Some(report_file.clone());
+
+            let config = GroupConfig {
+                paths: vec![file.into()],
+                unique: true,
+                output: Some(report_file.clone()),
+                ..GroupConfig::default()
+            };
 
             let results = group_files(&config, &log).unwrap();
             write_report(&config, &log, &results).unwrap();
@@ -1865,7 +1891,7 @@ mod test {
                     device: 0,
                 },
                 len: FileLen(1024),
-                location: id as u64 * 1024,
+                location: id * 1024,
             }
         }
 
@@ -1900,11 +1926,11 @@ mod test {
         let mut file = OpenOptions::new()
             .write(true)
             .create(true)
-            .open(&path)
+            .open(path)
             .unwrap();
-        file.write(prefix).unwrap();
-        file.write(mid).unwrap();
-        file.write(suffix).unwrap();
+        file.write_all(prefix).unwrap();
+        file.write_all(mid).unwrap();
+        file.write_all(suffix).unwrap();
     }
 
     fn test_log() -> StdLog {
diff --git a/src/hasher.rs b/src/hasher.rs
index 84d2609..f3e411e 100644
--- a/src/hasher.rs
+++ b/src/hasher.rs
@@ -78,7 +78,7 @@ impl FromStr for HashFn {
             "sha3-256" => Ok(Self::Sha3_256),
             #[cfg(feature = "sha3")]
             "sha3-512" => Ok(Self::Sha3_512),
-            _ => Err(format!("Unknown hash algorithm: {}", s)),
+            _ => Err(format!("Unknown hash algorithm: {s}")),
         }
     }
 }
@@ -409,8 +409,7 @@ impl FileHasher<'_> {
                 Ok(len_and_hash) => len_and_hash,
                 Err(e) => {
                     self.log.warn(format!(
-                        "Failed to load hash of file id = {} from the cache: {}",
-                        key, e
+                        "Failed to load hash of file id = {key} from the cache: {e}"
                     ));
                     None
                 }
@@ -431,8 +430,7 @@ impl FileHasher<'_> {
         {
             if let Err(e) = cache.put(key, metadata, data_len, hash) {
                 self.log.warn(format!(
-                    "Failed to store hash of file {} in the cache: {}",
-                    key, e
+                    "Failed to store hash of file {key} in the cache: {e}"
                 ))
             }
         };
@@ -444,7 +442,7 @@ fn format_output_stream(output: &str) -> String {
     if output.is_empty() {
         output
     } else {
-        format!("\n{}\n", output)
+        format!("\n{output}\n")
     }
 }
 
diff --git a/src/lock.rs b/src/lock.rs
index da58432..a5755b3 100644
--- a/src/lock.rs
+++ b/src/lock.rs
@@ -61,7 +61,7 @@ impl FileLock {
             .read(false)
             .write(true)
             .create(false)
-            .open(&path_buf)
+            .open(path_buf)
             .map_err(|e| {
                 io::Error::new(
                     error_kind(&e),
diff --git a/src/log.rs b/src/log.rs
index 6803755..765c74a 100644
--- a/src/log.rs
+++ b/src/log.rs
@@ -134,9 +134,9 @@ impl StdLog {
     /// Does not interfere with progress bar.
     fn eprintln<I: Display>(&self, msg: I) {
         match self.progress_bar.lock().unwrap().upgrade() {
-            Some(pb) if pb.is_visible() => pb.println(format!("{}", msg)),
-            _ if self.log_stderr_to_stdout => println!("{}", msg),
-            _ => eprintln!("{}", msg),
+            Some(pb) if pb.is_visible() => pb.println(format!("{msg}")),
+            _ if self.log_stderr_to_stdout => println!("{msg}"),
+            _ => eprintln!("{msg}"),
         }
     }
 
diff --git a/src/main.rs b/src/main.rs
index cbd3f4e..c80178a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -54,7 +54,7 @@ fn check_input_paths_exist(config: &GroupConfig, log: &dyn Log) -> Result<(), Er
     let depth = config.depth;
     let input_paths = config
         .input_paths()
-        .filter(|p| match fs::metadata(&p.to_path_buf()) {
+        .filter(|p| match fs::metadata(p.to_path_buf()) {
             Ok(m) if m.is_dir() && depth == Some(0) => {
                 log.warn(format!(
                     "Skipping directory {} because recursive scan is disabled.",
@@ -119,7 +119,7 @@ fn run_group(mut config: GroupConfig, log: &dyn Log) -> Result<(), Error> {
     let results = group_files(&config, log).map_err(|e| Error::new(e.message))?;
 
     write_report(&config, log, &results)
-        .map_err(|e| Error::new(format!("Failed to write report: {}", e)))
+        .map_err(|e| Error::new(format!("Failed to write report: {e}")))
 }
 
 /// Depending on the `output` configuration field, returns either a reference to the standard
@@ -141,7 +141,7 @@ fn get_output_writer(config: &DedupeConfig) -> Result<Box<dyn Write + Send>, Err
 fn get_command_config(header: &ReportHeader) -> Result<Config, Error> {
     let mut command: Config = Config::try_parse_from(&header.command).map_err(|e| {
         let message: String = extract_error_cause(&e.to_string());
-        format!("Unrecognized earlier fclones configuration: {}", message)
+        format!("Unrecognized earlier fclones configuration: {message}")
     })?;
 
     // Configure the same base directory as set when running the previous command.
@@ -153,7 +153,7 @@ fn get_command_config(header: &ReportHeader) -> Result<Config, Error> {
 }
 
 pub fn run_dedupe(op: DedupeOp, config: DedupeConfig, log: &dyn Log) -> Result<(), Error> {
-    let input_error = |e: io::Error| format!("Input error: {}", e);
+    let input_error = |e: io::Error| format!("Input error: {e}");
     let mut dedupe_config = config;
     let mut reader = open_report(stdin()).map_err(input_error)?;
     let header = reader.read_header().map_err(input_error)?;
@@ -228,7 +228,7 @@ pub fn run_dedupe(op: DedupeOp, config: DedupeConfig, log: &dyn Log) -> Result<(
     let script = dedupe(groups, op, &dedupe_config, log);
     if dedupe_config.dry_run {
         let out = get_output_writer(&dedupe_config)?;
-        let result = log_script(script, out).map_err(|e| format!("Output error: {}", e))?;
+        let result = log_script(script, out).map_err(|e| format!("Output error: {e}"))?;
         log.info(format!(
             "Would process {} files and reclaim {}{} space",
             result.processed_count, upto, result.reclaimed_space
@@ -240,7 +240,7 @@ pub fn run_dedupe(op: DedupeOp, config: DedupeConfig, log: &dyn Log) -> Result<(
             result.processed_count, upto, result.reclaimed_space
         ));
     };
-    result.map_err(|e| Error::new(format!("Failed to read file list: {}", e)))
+    result.map_err(|e| Error::new(format!("Failed to read file list: {e}")))
 }
 
 fn main() {
@@ -258,7 +258,7 @@ fn main() {
     let cwd = match std::env::current_dir() {
         Ok(cwd) => cwd,
         Err(e) => {
-            log.err(format!("Cannot determine current working directory: {}", e));
+            log.err(format!("Cannot determine current working directory: {e}"));
             exit(1);
         }
     };
diff --git a/src/path.rs b/src/path.rs
index aaf1aff..ffc51d7 100644
--- a/src/path.rs
+++ b/src/path.rs
@@ -165,7 +165,7 @@ impl Path {
             self_components.next();
             other_components.next();
         }
-        self_components.peek() == None
+        self_components.peek().is_none()
     }
 
     /// Converts this path to a standard library path buffer.
@@ -351,7 +351,7 @@ impl Visitor<'_> for PathVisitor {
     where
         E: Error,
     {
-        Path::from_escaped_string(v).map_err(|e| E::custom(format!("Invalid path: {}", e)))
+        Path::from_escaped_string(v).map_err(|e| E::custom(format!("Invalid path: {e}")))
     }
 }
 
diff --git a/src/pattern.rs b/src/pattern.rs
index 8219bcc..aa3d82a 100644
--- a/src/pattern.rs
+++ b/src/pattern.rs
@@ -304,7 +304,7 @@ mod test {
     }
 
     fn native_dir_sep(str: &str) -> String {
-        str.replace("/", MAIN_SEPARATOR.to_string().as_str())
+        str.replace('/', MAIN_SEPARATOR.to_string().as_str())
     }
 
     #[test]
diff --git a/src/reflink.rs b/src/reflink.rs
index 8336d95..a6b47fc 100644
--- a/src/reflink.rs
+++ b/src/reflink.rs
@@ -33,7 +33,7 @@ pub fn reflink(src: &PathAndMetadata, dest: &PathAndMetadata, log: &dyn Log) ->
     .map_err(|e| {
         io::Error::new(
             e.kind(),
-            format!("Failed to deduplicate {} -> {}: {}", dest, src, e),
+            format!("Failed to deduplicate {dest} -> {src}: {e}"),
         )
     });
 
@@ -116,13 +116,10 @@ fn reflink_overwrite(target: &std::path::Path, link: &std::path::Path) -> io::Re
     use nix::request_code_write;
     use std::os::unix::prelude::AsRawFd;
 
-    let src = fs::File::open(&target)?;
+    let src = fs::File::open(target)?;
 
     // This operation does not require `.truncate(true)` because the files are already of the same size.
-    let dest = fs::OpenOptions::new()
-        .create(true)
-        .write(true)
-        .open(&link)?;
+    let dest = fs::OpenOptions::new().create(true).write(true).open(link)?;
 
     // From /usr/include/linux/fs.h:
     // #define FICLONE		_IOW(0x94, 9, int)
@@ -193,7 +190,7 @@ fn restore_metadata(path: &std::path::Path, metadata: &Metadata) -> io::Result<(
         )
     })?;
 
-    fs::set_permissions(&path, metadata.permissions()).map_err(|e| {
+    fs::set_permissions(path, metadata.permissions()).map_err(|e| {
         io::Error::new(
             e.kind(),
             format!("Failed to set permissions for {}: {}", path.display(), e),
@@ -282,8 +279,8 @@ fn restore_xattrs(path: &std::path::Path, xattrs: Vec<XAttr>) -> io::Result<()>
 // Reflink which expects the destination to not exist.
 #[cfg(any(not(any(target_os = "linux", target_os = "android")), test))]
 fn copy_by_reflink(src: &crate::path::Path, dest: &crate::path::Path) -> io::Result<()> {
-    reflink::reflink(&src.to_path_buf(), &dest.to_path_buf())
-        .map_err(|e| io::Error::new(e.kind(), format!("Failed to reflink: {}", e)))
+    reflink::reflink(src.to_path_buf(), dest.to_path_buf())
+        .map_err(|e| io::Error::new(e.kind(), format!("Failed to reflink: {e}")))
 }
 
 // Create a reflink by removing the file and making a reflink copy of the original.
@@ -382,15 +379,15 @@ pub mod test {
         let test_root = "/dev/shm/tmp.fclones.reflink.testfailure";
 
         // Usually /dev/shm is mounted as a tmpfs which does not support reflinking, so test there.
-        with_dir(&test_root, |root| {
+        with_dir(test_root, |root| {
             // Always clean up files in /dev/shm, even after failure
             struct CleanupGuard<'a>(&'a str);
             impl<'a> Drop for CleanupGuard<'a> {
                 fn drop(&mut self) {
-                    fs::remove_dir_all(&self.0).unwrap();
+                    fs::remove_dir_all(self.0).unwrap();
                 }
             }
-            let _guard = CleanupGuard(&test_root);
+            let _guard = CleanupGuard(test_root);
 
             let log = StdLog::new();
             let file_path_1 = root.join("file_1");
diff --git a/src/report.rs b/src/report.rs
index bd452c7..8a76d26 100644
--- a/src/report.rs
+++ b/src/report.rs
@@ -87,9 +87,7 @@ impl<W: Write> ReportWriter<W> {
         writeln!(
             self.out,
             "{}",
-            style(format!("# {}", line))
-                .cyan()
-                .force_styling(self.color)
+            style(format!("# {line}")).cyan().force_styling(self.color)
         )
     }
 
@@ -131,7 +129,7 @@ impl<W: Write> ReportWriter<W> {
             "Timestamp: {}",
             header.timestamp.format(TIMESTAMP_FMT)
         ))?;
-        self.write_header_line(&format!("Command: {}", command))?;
+        self.write_header_line(&format!("Command: {command}"))?;
         self.write_header_line(&format!(
             "Base dir: {}",
             header.base_dir.to_escaped_string()
@@ -212,7 +210,7 @@ impl<W: Write> ReportWriter<W> {
             .flexible(true)
             .from_writer(&mut self.out);
 
-        wtr.write_record(&["size", "hash", "count", "files"])?;
+        wtr.write_record(["size", "hash", "count", "files"])?;
         for g in groups {
             let g = g.as_ref();
             let mut record = csv::StringRecord::new();
@@ -387,7 +385,7 @@ where
         let captures = GROUP_HEADER_RE.captures(header_str).ok_or_else(|| {
             Error::new(
                 ErrorKind::InvalidData,
-                format!("Malformed group header: {}", header_str),
+                format!("Malformed group header: {header_str}"),
             )
         })?;
 
@@ -413,13 +411,13 @@ where
             if !path_str.starts_with("    ") || path_str.trim().is_empty() {
                 return Err(Error::new(
                     ErrorKind::InvalidData,
-                    format!("Path expected: {}", path_str),
+                    format!("Path expected: {path_str}"),
                 ));
             }
             let path = Path::from_escaped_string(path_str.trim()).map_err(|e| {
                 Error::new(
                     ErrorKind::InvalidData,
-                    format!("Invalid path {}: {}", path_str, e),
+                    format!("Invalid path {path_str}: {e}"),
                 )
             })?;
             paths.push(path);
@@ -482,7 +480,7 @@ impl<R: BufRead> TextReportReader<R> {
             .ok_or_else(|| {
                 Error::new(
                     ErrorKind::InvalidData,
-                    format!("Malformed header: Missing {}", name),
+                    format!("Malformed header: Missing {name}"),
                 )
             })?
             .iter()
@@ -496,8 +494,7 @@ impl<R: BufRead> TextReportReader<R> {
             Error::new(
                 ErrorKind::InvalidData,
                 format!(
-                    "Malformed header: Failed to parse {}: {}. Expected timestamp format: {}",
-                    name, e, TIMESTAMP_FMT
+                    "Malformed header: Failed to parse {name}: {e}. Expected timestamp format: {TIMESTAMP_FMT}"
                 ),
             )
         })
@@ -509,14 +506,13 @@ impl<R: BufRead> TextReportReader<R> {
                 Error::new(
                     ErrorKind::InvalidData,
                     format!(
-                        "Malformed header: Failed to parse {}: {}. Expected integer value.",
-                        name, e
+                        "Malformed header: Failed to parse {name}: {e}. Expected integer value."
                     ),
                 )
             }),
             None => Err(Error::new(
                 ErrorKind::InvalidData,
-                format!("Malformed header: Missing {}", name),
+                format!("Malformed header: Missing {name}"),
             )),
         }
     }
@@ -559,7 +555,7 @@ impl<R: BufRead + Send + 'static> ReportReader for TextReportReader<R> {
         let command = arg::split(&command).map_err(|e| {
             Error::new(
                 ErrorKind::InvalidData,
-                format!("Malformed header: Failed to parse command arguments: {}", e),
+                format!("Malformed header: Failed to parse command arguments: {e}"),
             )
         })?;
         let base_dir = self.read_extract(&BASE_DIR_RE, "base dir")?.swap_remove(0);
@@ -614,7 +610,7 @@ impl JsonReportReader {
         let report: DeserializedReport = serde_json::from_reader(stream).map_err(|e| {
             Error::new(
                 ErrorKind::InvalidData,
-                format!("Failed to deserialize JSON report: {}", e),
+                format!("Failed to deserialize JSON report: {e}"),
             )
         })?;
         Ok(JsonReportReader { report })
@@ -638,7 +634,7 @@ impl ReportReader for JsonReportReader {
                         Path::from_escaped_string(s.as_str()).map_err(|e| {
                             io::Error::new(
                                 io::ErrorKind::InvalidData,
-                                format!("Invalid path {}: {}", s, e),
+                                format!("Invalid path {s}: {e}"),
                             )
                         })
                     })
@@ -727,7 +723,7 @@ mod test {
         let input = output.reopen().unwrap();
 
         let mut writer = ReportWriter::new(output, false);
-        writer.write_as_text(&header, groups.iter()).unwrap();
+        writer.write_as_text(header, groups.iter()).unwrap();
         let mut reader = Box::new(TextReportReader::new(BufReader::new(input)));
         reader.read_header().unwrap();
 
@@ -860,7 +856,7 @@ mod test {
         let input = output.reopen().unwrap();
 
         let mut writer = ReportWriter::new(output, false);
-        writer.write_as_json(&header, groups.iter()).unwrap();
+        writer.write_as_json(header, groups.iter()).unwrap();
         let mut reader = Box::new(JsonReportReader::new(input).unwrap());
         reader.read_header().unwrap();
 
diff --git a/src/selector.rs b/src/selector.rs
index 2b82258..2d74201 100644
--- a/src/selector.rs
+++ b/src/selector.rs
@@ -205,7 +205,7 @@ mod test {
             .include_paths(vec![Pattern::glob("**/public-?.jpg").unwrap()])
             .exclude_paths(vec![Pattern::glob("**/private-?.jpg").unwrap()]);
 
-        println!("{:?}", selector);
+        println!("{selector:?}");
 
         // matching absolute:
         assert!(selector.matches_full_path(&Path::from("/public-1.jpg")));
diff --git a/src/transform.rs b/src/transform.rs
index 6aee5d3..62a7a30 100644
--- a/src/transform.rs
+++ b/src/transform.rs
@@ -175,7 +175,7 @@ impl Transform {
             Err(e) => {
                 return Err(io::Error::new(
                     e.kind(),
-                    format!("Cannot launch {}: {}", program, e),
+                    format!("Cannot launch {program}: {e}"),
                 ))
             }
         }
@@ -364,7 +364,7 @@ fn execute(command: &mut Command, input: Input, output: Output) -> io::Result<Ex
             // However if waiting fails here, the child process likely doesn't run, so that's not
             // a problem.
             let _ignore = child_ref.lock().unwrap().wait();
-            let _ignore = OpenOptions::new().write(true).open(&output_pipe);
+            let _ignore = OpenOptions::new().write(true).open(output_pipe);
         }
         str
     });
@@ -463,7 +463,7 @@ mod test {
             let input_path = root.join("input.txt");
             let mut input = File::create(&input_path).unwrap();
             let content = b"content";
-            input.write(content).unwrap();
+            input.write_all(content).unwrap();
             drop(input);
 
             let log = StdLog::default();
@@ -485,7 +485,7 @@ mod test {
             let input_path = root.join("input.txt");
             let mut input = File::create(&input_path).unwrap();
             let content = b"content";
-            input.write(content).unwrap();
+            input.write_all(content).unwrap();
             drop(input);
 
             let log = StdLog::default();
diff --git a/src/util.rs b/src/util.rs
index bfb12d0..4ecebb7 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -133,8 +133,8 @@ pub mod test {
         let mut delay = std::time::Duration::from_millis(1);
         loop {
             thread::sleep(delay);
-            create_file(&f);
-            let ctime = fs::metadata(&f).unwrap().modified().unwrap();
+            create_file(f);
+            let ctime = fs::metadata(f).unwrap().modified().unwrap();
             if ctime != time {
                 return ctime;
             }
@@ -146,13 +146,13 @@ pub mod test {
     /// Panics on errors.
     pub fn write_file(path: &std::path::Path, content: &str) {
         let mut f = File::create(path).unwrap();
-        write!(&mut f, "{}", content).unwrap();
+        write!(&mut f, "{content}").unwrap();
     }
 
     /// Reads contents of a file to a string.
     /// Panics on errors.
     pub fn read_file(path: &std::path::Path) -> String {
-        let f = File::open(&path).unwrap();
+        let f = File::open(path).unwrap();
         let mut r = BufReader::new(f);
         let mut result = String::new();
         r.read_to_string(&mut result).unwrap();
diff --git a/src/walk.rs b/src/walk.rs
index 8794809..9d83beb 100644
--- a/src/walk.rs
+++ b/src/walk.rs
@@ -51,7 +51,7 @@ impl Entry {
     }
 
     pub fn from_path(path: Path) -> io::Result<Entry> {
-        symlink_metadata(&path.to_path_buf()).map(|meta| Entry::new(meta.file_type(), path))
+        symlink_metadata(path.to_path_buf()).map(|meta| Entry::new(meta.file_type(), path))
     }
 
     pub fn from_dir_entry(base: &Arc<Path>, dir_entry: DirEntry) -> io::Result<Entry> {
@@ -69,7 +69,7 @@ impl IgnoreStack {
         let gitignore = GitignoreBuilder::new("/").build_global();
         if let Some(err) = gitignore.1 {
             if let Some(log) = log {
-                log.warn(format!("Error loading global gitignore rules: {}", err))
+                log.warn(format!("Error loading global gitignore rules: {err}"))
             }
         }
         IgnoreStack(Arc::new(vec![gitignore.0]))
@@ -476,7 +476,7 @@ mod test {
             let file = test_root.join("file.txt");
             let link1 = test_root.join("link1");
             let link2 = test_root.join("link2");
-            File::create(&file).unwrap();
+            File::create(file).unwrap();
             symlink(PathBuf::from("file.txt"), &link1).unwrap(); // link1 -> file.txt
             symlink(PathBuf::from("link1"), &link2).unwrap(); // link2 -> link1
 
@@ -541,7 +541,7 @@ mod test {
             create_dir(&dir).unwrap();
             File::create(&file).unwrap();
             // create a link back to the top level, so a cycle is formed
-            symlink(test_root.canonicalize().unwrap(), &link).unwrap();
+            symlink(test_root.canonicalize().unwrap(), link).unwrap();
 
             let mut walk = Walk::new();
             walk.follow_links = true;
@@ -556,8 +556,8 @@ mod test {
             create_dir(&hidden_dir).unwrap();
             let hidden_file_1 = hidden_dir.join("file.txt");
             let hidden_file_2 = test_root.join(".file.txt");
-            File::create(&hidden_file_1).unwrap();
-            File::create(&hidden_file_2).unwrap();
+            File::create(hidden_file_1).unwrap();
+            File::create(hidden_file_2).unwrap();
 
             let mut walk = Walk::new();
             walk.hidden = false;
@@ -578,11 +578,11 @@ mod test {
             writeln!(gitignore, "**/bar").unwrap();
             drop(gitignore);
 
-            create_dir(&test_root.join("foo")).unwrap();
+            create_dir(test_root.join("foo")).unwrap();
             create_file(&test_root.join("foo").join("bar"));
             create_file(&test_root.join("bar.log"));
-            create_dir(&test_root.join("dir")).unwrap();
-            create_dir(&test_root.join("dir").join("bar")).unwrap();
+            create_dir(test_root.join("dir")).unwrap();
+            create_dir(test_root.join("dir").join("bar")).unwrap();
             create_file(&test_root.join("dir").join("bar").join("file"));
 
             let walk = Walk::new();


Test patch: