diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 17b09013aeb..926a41386e8 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -120,7 +120,8 @@ fn uu_tail(settings: &Settings) -> UResult<()> { Ok(()) } -fn tail_file( +fn tail_opened_file( + mut file: File, settings: &Settings, header_printer: &mut HeaderPrinter, input: &Input, @@ -128,84 +129,130 @@ fn tail_file( observer: &mut Observer, offset: u64, ) -> UResult<()> { - if !path.exists() { - set_exit_code(1); - show_error!( - "{}", - translate!("tail-error-cannot-open-no-such-file", "file" => input.display_name.clone(), "error" => translate!("tail-no-such-file-or-directory")) - ); + let st = file.metadata()?; + let blksize_limit = uucore::fs::sane_blksize::sane_blksize_from_metadata(&st); + header_printer.print_input(input); + + let mut reader; + if !settings.presume_input_pipe + && file.is_seekable(if input.is_stdin() { offset } else { 0 }) + && (!st.is_file() || st.len() > blksize_limit) + { + bounded_tail(&mut file, settings); + reader = BufReader::new(file); + } else { + reader = BufReader::new(file); + unbounded_tail(&mut reader, settings)?; + } + + if input.is_tailable() { + observer.add_path( + path, + input.display_name.as_str(), + Some(Box::new(reader)), + true, + )?; + } else { observer.add_bad_path(path, input.display_name.as_str(), false)?; - } else if path.is_dir() { - set_exit_code(1); - - header_printer.print_input(input); - let err_msg = translate!("tail-is-a-directory"); - - show_error!( - "{}", - translate!("tail-error-reading-file", "file" => input.display_name.clone(), "error" => err_msg) - ); - if settings.follow.is_some() { - let msg = if settings.retry { - "" - } else { - &translate!("tail-giving-up-on-this-name") - }; + } + + Ok(()) +} + +fn tail_file( + settings: &Settings, + header_printer: &mut HeaderPrinter, + input: &Input, + path: &Path, + observer: &mut Observer, + offset: u64, +) -> UResult<()> { + match path.metadata() { + Ok(md) if md.is_dir() => { + set_exit_code(1); + + header_printer.print_input(input); + let err_msg = translate!("tail-is-a-directory"); + show_error!( "{}", - translate!("tail-error-cannot-follow-file-type", "file" => input.display_name.clone(), "msg" => msg) + translate!( + "tail-error-reading-file", + "file" => input.display_name.clone(), + "error" => err_msg + ) ); - } - if !observer.follow_name_retry() { - // skip directory if not retry - return Ok(()); - } - observer.add_bad_path(path, input.display_name.as_str(), false)?; - } else { - #[cfg(unix)] - let open_result = open_file(path, settings.pid != 0); - #[cfg(not(unix))] - let open_result = File::open(path); - - match open_result { - Ok(mut file) => { - let st = file.metadata()?; - let blksize_limit = uucore::fs::sane_blksize::sane_blksize_from_metadata(&st); - header_printer.print_input(input); - let mut reader; - if !settings.presume_input_pipe - && file.is_seekable(if input.is_stdin() { offset } else { 0 }) - && (!st.is_file() || st.len() > blksize_limit) - { - bounded_tail(&mut file, settings); - reader = BufReader::new(file); - } else { - reader = BufReader::new(file); - unbounded_tail(&mut reader, settings)?; - } - if input.is_tailable() { - observer.add_path( - path, - input.display_name.as_str(), - Some(Box::new(reader)), - true, - )?; + if settings.follow.is_some() { + let msg = if settings.retry { + "" } else { - observer.add_bad_path(path, input.display_name.as_str(), false)?; - } - } - Err(e) if e.kind() == ErrorKind::PermissionDenied => { - observer.add_bad_path(path, input.display_name.as_str(), false)?; - show!(e.map_err_context(|| { - translate!("tail-error-cannot-open-for-reading", "file" => input.display_name.clone()) - })); + &translate!("tail-giving-up-on-this-name") + }; + show_error!( + "{}", + translate!( + "tail-error-cannot-follow-file-type", + "file" => input.display_name.clone(), + "msg" => msg + ) + ); } - Err(e) => { - observer.add_bad_path(path, input.display_name.as_str(), false)?; - return Err(e.map_err_context(|| { - translate!("tail-error-cannot-open-for-reading", "file" => input.display_name.clone()) - })); + if !observer.follow_name_retry() { + return Ok(()); } + observer.add_bad_path(path, input.display_name.as_str(), false)?; + return Ok(()); + } + Err(e) if e.kind() == ErrorKind::NotFound => { + set_exit_code(1); + show_error!( + "{}", + translate!( + "tail-error-cannot-open-no-such-file", + "file" => input.display_name.clone(), + "error" => translate!("tail-no-such-file-or-directory") + ) + ); + observer.add_bad_path(path, input.display_name.as_str(), false)?; + return Ok(()); + } + _ => {} + } + + #[cfg(unix)] + let open_result = open_file(path, settings.pid != 0); + #[cfg(not(unix))] + let open_result = File::open(path); + + match open_result { + Ok(file) => { + tail_opened_file( + file, + settings, + header_printer, + input, + path, + observer, + offset, + )?; + } + Err(e) if e.kind() == ErrorKind::PermissionDenied => { + observer.add_bad_path(path, input.display_name.as_str(), false)?; + show!(e.map_err_context(|| { + translate!( + "tail-error-cannot-open-for-reading", + "file" => input.display_name.clone() + ) + })); + } + Err(e) => { + observer.add_bad_path(path, input.display_name.as_str(), false)?; + return Err(e.map_err_context(|| { + translate!( + "tail-error-cannot-open-for-reading", + "file" => input.display_name.clone() + ) + })); } } diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 9d4a270e2b2..6954de1fb69 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -6,7 +6,7 @@ // spell-checker:ignore (ToDO) abcdefghijklmnopqrstuvwxyz efghijklmnopqrstuvwxyz vwxyz emptyfile file siette ocho nueve diez MULT // spell-checker:ignore (libs) kqueue // spell-checker:ignore (jargon) tailable untailable datasame runneradmin tmpi -// spell-checker:ignore (cmd) taskkill +// spell-checker:ignore (cmd) taskkill noaccess topsecret #![allow( clippy::unicode_not_nfc, clippy::cast_lossless, @@ -296,6 +296,57 @@ fn test_permission_denied_multiple() { .stdout_is("==> file1 <==\n\n==> file2 <==\n"); } +// TODO: Add similar test for windows +#[test] +#[cfg(all(unix, not(target_os = "android")))] +fn test_permission_denied_from_metadata_error() { + use std::fs; + use std::os::unix::fs::PermissionsExt; + + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.mkdir("noaccess"); + at.write("noaccess/secret", "topsecret\n"); + + // Make the directory non-searchable so path lookup fails with EACCES. + fs::set_permissions(at.plus("noaccess"), fs::Permissions::from_mode(0o000)).unwrap(); + + ts.ucmd() + .arg("noaccess/secret") + .fails_with_code(1) + .no_stdout() + .stderr_is("tail: cannot open 'noaccess/secret' for reading: Permission denied\n"); + + // Restore permissions so the test harness can clean up the temp directory. + fs::set_permissions(at.plus("noaccess"), fs::Permissions::from_mode(0o700)).unwrap(); +} + +// Same as above, but ensure we keep processing subsequent files. +// TODO: Add similar test for windows +#[test] +#[cfg(all(unix, not(target_os = "android")))] +fn test_permission_denied_from_metadata_error_multiple() { + use std::fs; + use std::os::unix::fs::PermissionsExt; + + let (at, mut ucmd) = at_and_ucmd!(); + + at.touch("file1"); + at.touch("file2"); + + at.mkdir("noaccess"); + at.write("noaccess/secret", "topsecret\n"); + fs::set_permissions(at.plus("noaccess"), fs::Permissions::from_mode(0o000)).unwrap(); + + ucmd.args(&["file1", "noaccess/secret", "file2"]) + .fails_with_code(1) + .stdout_is("==> file1 <==\n\n==> file2 <==\n") + .stderr_is("tail: cannot open 'noaccess/secret' for reading: Permission denied\n"); + + fs::set_permissions(at.plus("noaccess"), fs::Permissions::from_mode(0o700)).unwrap(); +} + #[test] fn test_follow_redirect_stdin_name_retry() { // $ touch f && tail -F - < f