diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index ffb90a940..2ce0cc3fd 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -52,4 +52,4 @@ name = "ls" path = "src/main.rs" [features] -feat_selinux = ["selinux"] +feat_selinux = ["selinux", "uucore/selinux"] diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 7ab752a15..572daa61e 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1900,11 +1900,7 @@ impl PathData { None => OnceCell::new(), }; - let security_context = if config.context { - get_security_context(config, &p_buf, must_dereference) - } else { - String::new() - }; + let security_context = get_security_context(config, &p_buf, must_dereference); Self { md: OnceCell::new(), @@ -3339,7 +3335,10 @@ fn get_security_context(config: &Config, p_buf: &Path, must_dereference: bool) - Err(err) => { // The Path couldn't be dereferenced, so return early and set exit code 1 // to indicate a minor error - show!(LsError::IOErrorContext(p_buf.to_path_buf(), err, false)); + // Only show error when context display is requested to avoid duplicate messages + if config.context { + show!(LsError::IOErrorContext(p_buf.to_path_buf(), err, false)); + } return substitute_string; } Ok(_md) => (), diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index af5fb71b1..af6b2dd18 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -4263,8 +4263,7 @@ fn test_ls_context_long() { let line: Vec<_> = result.stdout_str().split(' ').collect(); assert!(line[0].ends_with('.')); assert!(line[4].starts_with("unconfined_u")); - let s: Vec<_> = line[4].split(':').collect(); - assert!(s.len() == 4); + validate_selinux_context(line[4]); } } @@ -4298,6 +4297,113 @@ fn test_ls_context_format() { } } +/// Helper function to validate `SELinux` context format +#[cfg(feature = "feat_selinux")] +fn validate_selinux_context(context: &str) { + assert!( + context.contains(':'), + "Expected SELinux context format (user:role:type:level), got: {}", + context + ); + + assert_eq!( + context.split(':').count(), + 4, + "SELinux context should have 4 components separated by colons, got: {}", + context + ); +} + +#[test] +#[cfg(feature = "feat_selinux")] +fn test_ls_selinux_context_format() { + if !uucore::selinux::is_selinux_enabled() { + println!("test skipped: Kernel has no support for SElinux context"); + return; + } + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("file"); + at.symlink_file("file", "link"); + + // Test that ls -lnZ properly shows the context + for file in ["file", "link"] { + let result = scene.ucmd().args(&["-lnZ", file]).succeeds(); + let output = result.stdout_str(); + + let lines: Vec<&str> = output.lines().collect(); + assert!(!lines.is_empty(), "Output should contain at least one line"); + + let first_line = lines[0]; + let parts: Vec<&str> = first_line.split_whitespace().collect(); + assert!(parts.len() >= 6, "Line should have at least 6 fields"); + + // The 5th field (0-indexed position 4) should contain the SELinux context + // Format: permissions links owner group context size date time name + let context = parts[4]; + validate_selinux_context(context); + } +} + +#[test] +#[cfg(feature = "feat_selinux")] +fn test_ls_selinux_context_indicator() { + if !uucore::selinux::is_selinux_enabled() { + println!("test skipped: Kernel has no support for SElinux context"); + return; + } + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("file"); + at.symlink_file("file", "link"); + + // Test that ls -l shows "." indicator for files with SELinux contexts + for file in ["file", "link"] { + let result = scene.ucmd().args(&["-l", file]).succeeds(); + let output = result.stdout_str(); + + // The 11th character should be "." indicating SELinux context + // -rw-rw-r--. (permissions + context indicator) + let lines: Vec<&str> = output.lines().collect(); + assert!(!lines.is_empty(), "Output should contain at least one line"); + + let first_line = lines[0]; + let chars: Vec = first_line.chars().collect(); + assert!( + chars.len() >= 11, + "Line should be at least 11 characters long" + ); + + // The 11th character (0-indexed position 10) should be "." for SELinux context + assert_eq!( + chars[10], '.', + "Expected '.' indicator for SELinux context in position 11, got '{}' in line: {}", + chars[10], first_line + ); + } + + // Test that ls -lnZ properly shows the context + for file in ["file", "link"] { + let result = scene.ucmd().args(&["-lnZ", file]).succeeds(); + let output = result.stdout_str(); + + let lines: Vec<&str> = output.lines().collect(); + assert!(!lines.is_empty(), "Output should contain at least one line"); + + let first_line = lines[0]; + let parts: Vec<&str> = first_line.split_whitespace().collect(); + assert!(parts.len() >= 6, "Line should have at least 6 fields"); + + // The 5th field (0-indexed position 4) should contain the SELinux context + // Format: permissions links owner group context size date time name + validate_selinux_context(parts[4]); + } +} + #[test] #[allow(non_snake_case)] fn test_ls_a_A() {