Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 92 additions & 69 deletions src/uu/tail/src/tail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,85 +118,108 @@ 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"))
);
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")
};
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 settings.follow.is_some() {
let msg = if settings.retry {
""
} else {
&translate!("tail-giving-up-on-this-name")
};
show_error!(
"{}",
translate!(
"tail-error-cannot-follow-file-type",
"file" => input.display_name.clone(),
"msg" => msg
)
);
}
if !observer.follow_name_retry() {
return Ok(());
}
observer.add_bad_path(path, input.display_name.as_str(), false)?;
return Ok(());
}
if !observer.follow_name_retry() {
// skip directory if not retry
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(());
}
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,
)?;
} 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())
}));
_ => {}
}

#[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)?;
}
Err(e) => {
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)?;
return Err(e.map_err_context(|| {
translate!("tail-error-cannot-open-for-reading", "file" => input.display_name.clone())
}));
}
}
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()
)
}));
}
}

Ok(())
Expand Down
52 changes: 52 additions & 0 deletions tests/by-util/test_tail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,58 @@
.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");

Check failure on line 288 in tests/by-util/test_tail.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'noaccess' (file:'tests/by-util/test_tail.rs', line:288)
at.write("noaccess/secret", "topsecret\n");

Check failure on line 289 in tests/by-util/test_tail.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'topsecret' (file:'tests/by-util/test_tail.rs', line:289)

Check failure on line 289 in tests/by-util/test_tail.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'noaccess' (file:'tests/by-util/test_tail.rs', line:289)

// Make the directory non-searchable so path lookup fails with EACCES.
fs::set_permissions(at.plus("noaccess"), fs::Permissions::from_mode(0o000)).unwrap();

Check failure on line 292 in tests/by-util/test_tail.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'noaccess' (file:'tests/by-util/test_tail.rs', line:292)

ts.ucmd()
.arg("noaccess/secret")

Check failure on line 295 in tests/by-util/test_tail.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'noaccess' (file:'tests/by-util/test_tail.rs', line:295)
.fails_with_code(1)
.no_stdout()
.stderr_is("tail: cannot open 'noaccess/secret' for reading: Permission denied\n");

Check failure on line 298 in tests/by-util/test_tail.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'noaccess' (file:'tests/by-util/test_tail.rs', line:298)

// 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");

Check failure on line 318 in tests/by-util/test_tail.rs

View workflow job for this annotation

GitHub Actions / Style/spelling (ubuntu-latest, feat_os_unix)

ERROR: `cspell`: Unknown word 'topsecret' (file:'tests/by-util/test_tail.rs', line:318)
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
Expand Down
Loading