From 3a680a4cfea800da62b0f2ea77ac30d990cdd013 Mon Sep 17 00:00:00 2001 From: Tavian Barnes Date: Fri, 4 Apr 2025 14:20:00 -0400 Subject: [PATCH] find: Fix -printf interactions with -L - %l should only trigger for broken symlinks - %Y should follow symlinks --- src/find/matchers/mod.rs | 18 ++++++++-- src/find/matchers/printf.rs | 65 +++++++++++++++++++++---------------- 2 files changed, 52 insertions(+), 31 deletions(-) diff --git a/src/find/matchers/mod.rs b/src/find/matchers/mod.rs index ca2ed15e..f2b70801 100644 --- a/src/find/matchers/mod.rs +++ b/src/find/matchers/mod.rs @@ -84,6 +84,8 @@ pub enum Follow { Roots, /// Always follow symlinks (-L). Always, + /// Follow all links, treating broken links as errors (-printf %Y). + Force, } impl Follow { @@ -93,14 +95,20 @@ impl Follow { Self::Never => false, Self::Roots => depth == 0, Self::Always => true, + Self::Force => true, } } /// Get metadata for a [WalkEntry]. pub fn metadata(self, entry: &WalkEntry) -> Result { if self.follow_at_depth(entry.depth()) == entry.follow() { - // Same follow flag, re-use cached metadata - entry.metadata().cloned() + if self == Self::Force && entry.file_type().is_symlink() { + // Broken link, return an error + self.metadata_at_depth(entry.path(), entry.depth()) + } else { + // Same follow flag, re-use cached metadata + entry.metadata().cloned() + } } else if !entry.follow() && !entry.file_type().is_symlink() { // Not a symlink, re-use cached metadata entry.metadata().cloned() @@ -126,7 +134,11 @@ impl Follow { let path = path.as_ref(); if self.follow_at_depth(depth) { - match path.metadata().map_err(WalkError::from) { + let meta = path.metadata().map_err(WalkError::from); + if self == Self::Force { + return meta; + } + match meta { Ok(meta) => return Ok(meta), Err(e) if !e.is_not_found() => return Err(e), _ => {} diff --git a/src/find/matchers/printf.rs b/src/find/matchers/printf.rs index aa7b0662..8fc93ebf 100644 --- a/src/find/matchers/printf.rs +++ b/src/find/matchers/printf.rs @@ -12,7 +12,7 @@ use std::{borrow::Cow, io::Write}; use chrono::{format::StrftimeItems, DateTime, Local}; -use super::{FileType, Matcher, MatcherIO, WalkEntry, WalkError}; +use super::{FileType, Follow, Matcher, MatcherIO, WalkEntry}; #[cfg(unix)] use std::os::unix::prelude::MetadataExt; @@ -359,18 +359,6 @@ fn get_starting_point(file_info: &WalkEntry) -> &Path { .unwrap() } -fn format_non_link_file_type(file_type: FileType) -> char { - match file_type { - FileType::Regular => 'f', - FileType::Directory => 'd', - FileType::BlockDevice => 'b', - FileType::CharDevice => 'c', - FileType::Fifo => 'p', - FileType::Socket => 's', - _ => 'U', - } -} - fn format_directive<'entry>( file_info: &'entry WalkEntry, directive: &FormatDirective, @@ -525,7 +513,7 @@ fn format_directive<'entry>( FormatDirective::StartingPoint => get_starting_point(file_info).to_string_lossy(), FormatDirective::SymlinkTarget => { - if file_info.path_is_symlink() { + if file_info.file_type().is_symlink() { fs::read_link(file_info.path())? .to_string_lossy() .into_owned() @@ -535,22 +523,43 @@ fn format_directive<'entry>( } } - FormatDirective::Type { follow_links } => if file_info.path_is_symlink() { - if *follow_links { - match file_info.path().metadata().map_err(WalkError::from) { - Ok(meta) => format_non_link_file_type(meta.file_type().into()), - Err(e) if e.is_not_found() => 'N', - Err(e) if e.is_loop() => 'L', - Err(_) => '?', - } + FormatDirective::Type { follow_links } => { + let follow = if *follow_links { + Follow::Force + } else if file_info.follow() { + Follow::Always } else { - 'l' - } - } else { - format_non_link_file_type(file_info.file_type()) + Follow::Never + }; + let meta = follow.metadata(file_info); + let ftype = meta + .as_ref() + .map(|m| m.file_type().into()) + .unwrap_or(FileType::Unknown); + + let ret = match ftype { + FileType::Regular => "f", + FileType::Directory => "d", + FileType::BlockDevice => "b", + FileType::CharDevice => "c", + FileType::Fifo => "p", + FileType::Socket => "s", + FileType::Symlink => "l", + FileType::Unknown if *follow_links => { + match meta { + Err(e) if e.is_not_found() => "N", + Err(e) if e.is_loop() => "L", + Err(_) => { + // TODO: matcher_io.set_exit_code(1); + "?" + } + _ => "U", + } + } + FileType::Unknown => "U", + }; + ret.into() } - .to_string() - .into(), #[cfg(not(unix))] FormatDirective::User { .. } => "0".into(),