From 0655941e16a2c8c24e1f7f22a129be74481ae566 Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 14:09:54 -0500 Subject: [PATCH 01/17] Initial commit --- src/uucore/src/lib/features/fsxattr.rs | 63 +++++++++++++------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/src/uucore/src/lib/features/fsxattr.rs b/src/uucore/src/lib/features/fsxattr.rs index d9683264652..70117ffd730 100644 --- a/src/uucore/src/lib/features/fsxattr.rs +++ b/src/uucore/src/lib/features/fsxattr.rs @@ -137,38 +137,39 @@ pub fn get_acl_perm_bits_from_xattr>(source: P) -> u32 { // Only default acl entries get inherited by objects under the path i.e. if child directories // will have their permissions modified. - if let Ok(entries) = retrieve_xattrs(source) { - let mut perm: u32 = 0; - if let Some(value) = entries.get(&OsString::from("system.posix_acl_default")) { - // value is xattr byte vector - // value follows a starts with a 4 byte header, and then has posix_acl_entries, each - // posix_acl_entry is separated by a u32 sequence i.e. 0xFFFFFFFF - // - // struct posix_acl_entries { - // e_tag: u16 - // e_perm: u16 - // e_id: u32 - // } - // - // Reference: `https://github.com/torvalds/linux/blob/master/include/uapi/linux/posix_acl_xattr.h` - // - // The value of the header is 0x0002, so we skip the first four bytes of the value and - // process the rest - - let acl_entries = value - .split_at(3) - .1 - .iter() - .filter(|&x| *x != 255) - .copied() - .collect::>(); - - for entry in acl_entries.chunks_exact(4) { - // Third byte and fourth byte will be the perm bits - perm = (perm << 3) | u32::from(entry[2]) | u32::from(entry[3]); - } - return perm; + let mut perm: u32 = 0; + if let Some(value) = xattr::get(source, "system.posix_acl_default") + .ok() + .flatten() + { + // value is xattr byte vector + // value follows a starts with a 4 byte header, and then has posix_acl_entries, each + // posix_acl_entry is separated by a u32 sequence i.e. 0xFFFFFFFF + // + // struct posix_acl_entries { + // e_tag: u16 + // e_perm: u16 + // e_id: u32 + // } + // + // Reference: `https://github.com/torvalds/linux/blob/master/include/uapi/linux/posix_acl_xattr.h` + // + // The value of the header is 0x0002, so we skip the first four bytes of the value and + // process the rest + + let acl_entries = value + .split_at(3) + .1 + .iter() + .filter(|&x| *x != 255) + .copied() + .collect::>(); + + for entry in acl_entries.chunks_exact(4) { + // Third byte and fourth byte will be the perm bits + perm = (perm << 3) | u32::from(entry[2]) | u32::from(entry[3]); } + return perm; } 0 } From 115f226f73fbf4ceb493485564b61303c495fbaf Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:01:56 -0500 Subject: [PATCH 02/17] No need to list all acls for security cap --- src/uucore/src/lib/features/fsxattr.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/uucore/src/lib/features/fsxattr.rs b/src/uucore/src/lib/features/fsxattr.rs index 70117ffd730..6c5a2ff6250 100644 --- a/src/uucore/src/lib/features/fsxattr.rs +++ b/src/uucore/src/lib/features/fsxattr.rs @@ -112,13 +112,10 @@ pub fn has_acl>(file: P) -> bool { /// `true` if the file has an extended attribute named "security.capability", `false` otherwise. pub fn has_security_cap_acl>(file: P) -> bool { // don't use exacl here, it is doing more getxattr call then needed - xattr::list_deref(file).is_ok_and(|mut acl| { - #[cfg(unix)] - return acl.contains(OsStr::from_bytes(b"security.capability")); - - #[cfg(not(unix))] - return false; - }) + xattr::get_deref(file, "security.capability") + .ok() + .flatten() + .is_some() } /// Returns the permissions bits of a file or directory which has Access Control List (ACL) entries based on its From 8d4d9ba1de0d9fd026d868b108b6703a4552e05e Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:04:49 -0500 Subject: [PATCH 03/17] Fix lints --- src/uucore/src/lib/features/fsxattr.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/uucore/src/lib/features/fsxattr.rs b/src/uucore/src/lib/features/fsxattr.rs index 6c5a2ff6250..ae174ee841e 100644 --- a/src/uucore/src/lib/features/fsxattr.rs +++ b/src/uucore/src/lib/features/fsxattr.rs @@ -6,11 +6,9 @@ // spell-checker:ignore getxattr posix_acl_default //! Set of functions to manage xattr on files and dirs -use itertools::Itertools; use rustc_hash::FxHashMap; -use std::ffi::{OsStr, OsString}; +use std::ffi::OsString; #[cfg(unix)] -use std::os::unix::ffi::OsStrExt; use std::path::Path; /// Copies extended attributes (xattrs) from one file or directory to another. From f46ed7301ac6762e8ddbd70b542534466cb2fc1b Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 16:50:51 -0500 Subject: [PATCH 04/17] Cache xattr list only --- src/uu/ls/src/colors.rs | 2 +- src/uu/ls/src/ls.rs | 51 +++++++++++++++++++++----- src/uucore/src/lib/features/fsxattr.rs | 18 ++++++++- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/uu/ls/src/colors.rs b/src/uu/ls/src/colors.rs index 197fd2c8cec..3f8f56b3e55 100644 --- a/src/uu/ls/src/colors.rs +++ b/src/uu/ls/src/colors.rs @@ -530,7 +530,7 @@ pub(crate) fn color_name( let has_capabilities = style_manager .colors .has_explicit_style_for(Indicator::Capabilities) - && uucore::fsxattr::has_security_cap_acl(path.p_buf.as_path()); + && path.has_security_cap(); // If the file has capabilities, use a specific style for `ca` (capabilities) if has_capabilities { diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 507689eee41..e9325184b35 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -9,12 +9,11 @@ #[cfg(unix)] use rustc_hash::FxHashMap; use rustc_hash::FxHashSet; -use std::borrow::Cow; -use std::cell::RefCell; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt}; #[cfg(windows)] use std::os::windows::fs::MetadataExt; +use std::{borrow::Cow, cell::RefCell}; use std::{ cell::{LazyCell, OnceCell}, cmp::Reverse, @@ -42,7 +41,7 @@ use thiserror::Error; #[cfg(unix)] use uucore::entries; #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] -use uucore::fsxattr::has_acl; +use uucore::fsxattr::retrieve_xattr_list; #[cfg(unix)] use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR}; #[cfg(any( @@ -63,14 +62,13 @@ use uucore::{ error::{UError, UResult, set_exit_code}, format::human::{SizeFormat, human_readable}, format_usage, - fs::FileInformation, - fs::display_permissions, + fs::{FileInformation, display_permissions}, fsext::{MetadataTimeField, metadata_get_time}, line_ending::LineEnding, os_str_as_bytes_lossy, - parser::parse_glob, - parser::parse_size::parse_size_non_zero_u64, - parser::shortcut_value_parser::ShortcutValueParser, + parser::{ + parse_glob, parse_size::parse_size_non_zero_u64, shortcut_value_parser::ShortcutValueParser, + }, quoting_style::{QuotingStyle, locale_aware_escape_dir_name, locale_aware_escape_name}, show, show_error, show_warning, time::{FormatSystemTimeFallback, format, format_system_time}, @@ -1932,6 +1930,8 @@ struct PathData { // can be used to avoid reading the filetype. Can be also called d_type: // https://www.gnu.org/software/libc/manual/html_node/Directory-Entries.html de: RefCell>>, + #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] + xattrs: OnceCell>, security_context: OnceCell>, // Name of the file - will be empty for . or .. display_name: OsString, @@ -1983,6 +1983,8 @@ impl PathData { // nearly free compared to a metadata() call on a Path let ft: OnceCell> = OnceCell::new(); let md: OnceCell> = OnceCell::new(); + #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] + let xattrs: OnceCell> = OnceCell::new(); let security_context: OnceCell> = OnceCell::new(); let de: RefCell>> = if let Some(de) = dir_entry { @@ -2006,6 +2008,8 @@ impl PathData { md, ft, de, + #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] + xattrs, security_context, display_name, p_buf, @@ -2051,6 +2055,33 @@ impl PathData { .as_ref() } + #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] + pub fn has_acl(&self) -> bool { + if self.file_type().is_some_and(|ft| ft.is_symlink()) { + return false; + } + + let opt_xattrs = self.xattrs.get_or_init(|| retrieve_xattr_list(self.path())); + // don't use exacl here, it is doing more getxattr call then needed + opt_xattrs + .iter() + .map(|key| key.to_string_lossy()) + .any(|xattr| xattr.contains("acl")) + } + + #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] + pub fn has_security_cap(&self) -> bool { + if self.file_type().is_some_and(|ft| ft.is_symlink()) { + return false; + } + + let cap_key = OsStr::new("security.capability"); + + let xattrs = self.xattrs.get_or_init(|| retrieve_xattr_list(self.path())); + // don't use exacl here, it is doing more getxattr call then needed + xattrs.get(cap_key).is_some() + } + fn file_type(&self) -> Option<&FileType> { self.ft .get_or_init(|| self.metadata().map(Metadata::file_type)) @@ -3018,7 +3049,7 @@ fn display_item_long( // TODO: See how Mac should work here let is_acl_set = false; #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] - let is_acl_set = has_acl(item.path()); + let is_acl_set = item.has_acl(); output_display.extend(display_permissions(md, true).as_bytes()); if item.security_context(config).len() > 1 { // GNU `ls` uses a "." character to indicate a file with a security context, @@ -3737,7 +3768,7 @@ fn calculate_padding_collection( // TODO: See how Mac should work here let is_acl_set = false; #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] - let is_acl_set = has_acl(item.display_name()); + let is_acl_set = item.has_acl(); if context_len > 1 || is_acl_set { padding_collections.link_count += 1; } diff --git a/src/uucore/src/lib/features/fsxattr.rs b/src/uucore/src/lib/features/fsxattr.rs index ae174ee841e..ff526b71108 100644 --- a/src/uucore/src/lib/features/fsxattr.rs +++ b/src/uucore/src/lib/features/fsxattr.rs @@ -6,7 +6,7 @@ // spell-checker:ignore getxattr posix_acl_default //! Set of functions to manage xattr on files and dirs -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use std::ffi::OsString; #[cfg(unix)] use std::path::Path; @@ -62,6 +62,22 @@ pub fn retrieve_xattrs>(source: P) -> std::io::Result>(source: P) -> FxHashSet { + xattr::list(&source) + .unwrap_or_default() + .into_iter() + .collect() +} + /// Applies extended attributes (xattrs) to a given file or directory. /// /// # Arguments From f3a404044d22dcb1165c8a333ce98d21134cdd32 Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:01:58 -0500 Subject: [PATCH 05/17] Cleanup --- src/uu/ls/src/ls.rs | 26 ++++++++++++++++---------- src/uucore/src/lib/features/fsxattr.rs | 9 +++------ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index e9325184b35..53ace8c2a0f 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1931,7 +1931,7 @@ struct PathData { // https://www.gnu.org/software/libc/manual/html_node/Directory-Entries.html de: RefCell>>, #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] - xattrs: OnceCell>, + xattrs: OnceCell>>, security_context: OnceCell>, // Name of the file - will be empty for . or .. display_name: OsString, @@ -2061,12 +2061,16 @@ impl PathData { return false; } - let opt_xattrs = self.xattrs.get_or_init(|| retrieve_xattr_list(self.path())); - // don't use exacl here, it is doing more getxattr call then needed - opt_xattrs - .iter() - .map(|key| key.to_string_lossy()) - .any(|xattr| xattr.contains("acl")) + let opt_xattrs = self + .xattrs + .get_or_init(|| retrieve_xattr_list(self.path()).ok()); + + opt_xattrs.is_some_and(|inner| { + inner + .iter() + .map(|key| key.to_string_lossy()) + .any(|xattr| xattr.contains("acl")) + }) } #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] @@ -2077,9 +2081,11 @@ impl PathData { let cap_key = OsStr::new("security.capability"); - let xattrs = self.xattrs.get_or_init(|| retrieve_xattr_list(self.path())); - // don't use exacl here, it is doing more getxattr call then needed - xattrs.get(cap_key).is_some() + let xattrs = self + .xattrs + .get_or_init(|| retrieve_xattr_list(self.path()).ok()); + + xattrs.is_some_and(|inner| inner.get(cap_key).is_some()) } fn file_type(&self) -> Option<&FileType> { diff --git a/src/uucore/src/lib/features/fsxattr.rs b/src/uucore/src/lib/features/fsxattr.rs index ff526b71108..023bde0439b 100644 --- a/src/uucore/src/lib/features/fsxattr.rs +++ b/src/uucore/src/lib/features/fsxattr.rs @@ -54,7 +54,7 @@ pub fn copy_xattrs_skip_selinux>(source: P, dest: P) -> std::io:: /// A result containing a HashMap of attributes names and values, or an error. pub fn retrieve_xattrs>(source: P) -> std::io::Result>> { let mut attrs = FxHashMap::default(); - for attr_name in xattr::list(&source)? { + for attr_name in retrieve_xattr_list(&source)? { if let Some(value) = xattr::get(&source, &attr_name)? { attrs.insert(attr_name, value); } @@ -71,11 +71,8 @@ pub fn retrieve_xattrs>(source: P) -> std::io::Result>(source: P) -> FxHashSet { - xattr::list(&source) - .unwrap_or_default() - .into_iter() - .collect() +pub fn retrieve_xattr_list>(source: P) -> std::io::Result> { + Ok(xattr::list(&source)?.collect()) } /// Applies extended attributes (xattrs) to a given file or directory. From 4169abca1456100e2eea6446628aa06a1d7b957b Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:03:31 -0500 Subject: [PATCH 06/17] Cleanup --- src/uu/ls/src/ls.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 53ace8c2a0f..18d05c76989 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1984,7 +1984,7 @@ impl PathData { let ft: OnceCell> = OnceCell::new(); let md: OnceCell> = OnceCell::new(); #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] - let xattrs: OnceCell> = OnceCell::new(); + let xattrs: OnceCell>> = OnceCell::new(); let security_context: OnceCell> = OnceCell::new(); let de: RefCell>> = if let Some(de) = dir_entry { @@ -2065,7 +2065,7 @@ impl PathData { .xattrs .get_or_init(|| retrieve_xattr_list(self.path()).ok()); - opt_xattrs.is_some_and(|inner| { + opt_xattrs.as_ref().is_some_and(|inner| { inner .iter() .map(|key| key.to_string_lossy()) @@ -2085,7 +2085,9 @@ impl PathData { .xattrs .get_or_init(|| retrieve_xattr_list(self.path()).ok()); - xattrs.is_some_and(|inner| inner.get(cap_key).is_some()) + xattrs + .as_ref() + .is_some_and(|inner| inner.get(cap_key).is_some()) } fn file_type(&self) -> Option<&FileType> { From 8f3fc1fd8e12126491d8466e7482e1114e925e44 Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:07:57 -0500 Subject: [PATCH 07/17] Fix lints --- src/uu/ls/src/ls.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 18d05c76989..3e4e54419b7 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2057,7 +2057,7 @@ impl PathData { #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] pub fn has_acl(&self) -> bool { - if self.file_type().is_some_and(|ft| ft.is_symlink()) { + if self.file_type().is_some_and(FileType::is_symlink) { return false; } @@ -2075,7 +2075,7 @@ impl PathData { #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] pub fn has_security_cap(&self) -> bool { - if self.file_type().is_some_and(|ft| ft.is_symlink()) { + if self.file_type().is_some_and(FileType::is_symlink) { return false; } From 2b43fce592c4435e7c4f6d71aade3ba4ff40237d Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:12:57 -0500 Subject: [PATCH 08/17] Cleanup --- src/uu/ls/src/ls.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 3e4e54419b7..626dd9575a0 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2068,7 +2068,7 @@ impl PathData { opt_xattrs.as_ref().is_some_and(|inner| { inner .iter() - .map(|key| key.to_string_lossy()) + .filter_map(|key| key.to_str().map(|inner| inner.to_lowercase())) .any(|xattr| xattr.contains("acl")) }) } From e4ba5204824971b0fbfad6f94a95f80887fadfac Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:22:59 -0500 Subject: [PATCH 09/17] Fix lints --- src/uu/ls/src/ls.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 626dd9575a0..05a25815aa4 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2068,7 +2068,7 @@ impl PathData { opt_xattrs.as_ref().is_some_and(|inner| { inner .iter() - .filter_map(|key| key.to_str().map(|inner| inner.to_lowercase())) + .filter_map(|key| key.to_str().map(str::to_lowercase)) .any(|xattr| xattr.contains("acl")) }) } From 8329710f3eaee92f5d071717699b24a8f4071d04 Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:35:46 -0500 Subject: [PATCH 10/17] Cleanup --- src/uu/ls/src/ls.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 05a25815aa4..cdc2d526bd1 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2057,10 +2057,6 @@ impl PathData { #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] pub fn has_acl(&self) -> bool { - if self.file_type().is_some_and(FileType::is_symlink) { - return false; - } - let opt_xattrs = self .xattrs .get_or_init(|| retrieve_xattr_list(self.path()).ok()); @@ -2075,10 +2071,6 @@ impl PathData { #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] pub fn has_security_cap(&self) -> bool { - if self.file_type().is_some_and(FileType::is_symlink) { - return false; - } - let cap_key = OsStr::new("security.capability"); let xattrs = self From 3bf216fff320c45ab6daacf1f2cdb1e5bd1991e1 Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:09:49 -0500 Subject: [PATCH 11/17] Revert "Cleanup" This reverts commit 8329710f3eaee92f5d071717699b24a8f4071d04. --- src/uu/ls/src/ls.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index cdc2d526bd1..05a25815aa4 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2057,6 +2057,10 @@ impl PathData { #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] pub fn has_acl(&self) -> bool { + if self.file_type().is_some_and(FileType::is_symlink) { + return false; + } + let opt_xattrs = self .xattrs .get_or_init(|| retrieve_xattr_list(self.path()).ok()); @@ -2071,6 +2075,10 @@ impl PathData { #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] pub fn has_security_cap(&self) -> bool { + if self.file_type().is_some_and(FileType::is_symlink) { + return false; + } + let cap_key = OsStr::new("security.capability"); let xattrs = self From e20eb2246e47c7302cdbf25220fccf640cfbc0b5 Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:24:27 -0500 Subject: [PATCH 12/17] Cleanup --- src/uu/ls/src/ls.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 05a25815aa4..b5f77ea7a7e 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2057,15 +2057,15 @@ impl PathData { #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] pub fn has_acl(&self) -> bool { - if self.file_type().is_some_and(FileType::is_symlink) { - return false; - } + opt_xattrs.as_ref().is_some_and(|inner| { + if self.file_type().is_some_and(FileType::is_symlink) { + return false; + } - let opt_xattrs = self - .xattrs - .get_or_init(|| retrieve_xattr_list(self.path()).ok()); + let opt_xattrs = self + .xattrs + .get_or_init(|| retrieve_xattr_list(self.path()).ok()); - opt_xattrs.as_ref().is_some_and(|inner| { inner .iter() .filter_map(|key| key.to_str().map(str::to_lowercase)) @@ -2075,19 +2075,19 @@ impl PathData { #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] pub fn has_security_cap(&self) -> bool { - if self.file_type().is_some_and(FileType::is_symlink) { - return false; - } + xattrs.as_ref().is_some_and(|inner| { + if self.file_type().is_some_and(FileType::is_symlink) { + return false; + } - let cap_key = OsStr::new("security.capability"); + let cap_key = OsStr::new("security.capability"); - let xattrs = self - .xattrs - .get_or_init(|| retrieve_xattr_list(self.path()).ok()); + let xattrs = self + .xattrs + .get_or_init(|| retrieve_xattr_list(self.path()).ok()); - xattrs - .as_ref() - .is_some_and(|inner| inner.get(cap_key).is_some()) + inner.get(cap_key).is_some() + }) } fn file_type(&self) -> Option<&FileType> { From eebb20d147de6774de01aa49a63e02653fa01197 Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:27:46 -0500 Subject: [PATCH 13/17] Revert "Cleanup" This reverts commit e20eb2246e47c7302cdbf25220fccf640cfbc0b5. --- src/uu/ls/src/ls.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index b5f77ea7a7e..05a25815aa4 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2057,15 +2057,15 @@ impl PathData { #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] pub fn has_acl(&self) -> bool { - opt_xattrs.as_ref().is_some_and(|inner| { - if self.file_type().is_some_and(FileType::is_symlink) { - return false; - } + if self.file_type().is_some_and(FileType::is_symlink) { + return false; + } - let opt_xattrs = self - .xattrs - .get_or_init(|| retrieve_xattr_list(self.path()).ok()); + let opt_xattrs = self + .xattrs + .get_or_init(|| retrieve_xattr_list(self.path()).ok()); + opt_xattrs.as_ref().is_some_and(|inner| { inner .iter() .filter_map(|key| key.to_str().map(str::to_lowercase)) @@ -2075,19 +2075,19 @@ impl PathData { #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] pub fn has_security_cap(&self) -> bool { - xattrs.as_ref().is_some_and(|inner| { - if self.file_type().is_some_and(FileType::is_symlink) { - return false; - } + if self.file_type().is_some_and(FileType::is_symlink) { + return false; + } - let cap_key = OsStr::new("security.capability"); + let cap_key = OsStr::new("security.capability"); - let xattrs = self - .xattrs - .get_or_init(|| retrieve_xattr_list(self.path()).ok()); + let xattrs = self + .xattrs + .get_or_init(|| retrieve_xattr_list(self.path()).ok()); - inner.get(cap_key).is_some() - }) + xattrs + .as_ref() + .is_some_and(|inner| inner.get(cap_key).is_some()) } fn file_type(&self) -> Option<&FileType> { From cc01cff04f096a900c052ed44d1c2959d6cc76e1 Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:29:13 -0500 Subject: [PATCH 14/17] Cleanup --- src/uu/ls/src/ls.rs | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 05a25815aa4..adf4a259da4 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2057,37 +2057,30 @@ impl PathData { #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] pub fn has_acl(&self) -> bool { - if self.file_type().is_some_and(FileType::is_symlink) { - return false; - } - let opt_xattrs = self .xattrs .get_or_init(|| retrieve_xattr_list(self.path()).ok()); opt_xattrs.as_ref().is_some_and(|inner| { - inner - .iter() - .filter_map(|key| key.to_str().map(str::to_lowercase)) - .any(|xattr| xattr.contains("acl")) + self.file_type().is_some_and(FileType::is_symlink) + && inner + .iter() + .filter_map(|key| key.to_str().map(str::to_lowercase)) + .any(|xattr| xattr.contains("acl")) }) } #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] pub fn has_security_cap(&self) -> bool { - if self.file_type().is_some_and(FileType::is_symlink) { - return false; - } - let cap_key = OsStr::new("security.capability"); - let xattrs = self + let opt_xattrs = self .xattrs .get_or_init(|| retrieve_xattr_list(self.path()).ok()); - xattrs - .as_ref() - .is_some_and(|inner| inner.get(cap_key).is_some()) + opt_xattrs.as_ref().is_some_and(|inner| { + self.file_type().is_some_and(FileType::is_symlink) && inner.get(cap_key).is_some() + }) } fn file_type(&self) -> Option<&FileType> { From f4df5b9b2c4df9d09adcbe764e266a97383dce34 Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:29:45 -0500 Subject: [PATCH 15/17] Cleanup --- src/uu/ls/src/ls.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index adf4a259da4..ca7f2ac49e4 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2062,7 +2062,7 @@ impl PathData { .get_or_init(|| retrieve_xattr_list(self.path()).ok()); opt_xattrs.as_ref().is_some_and(|inner| { - self.file_type().is_some_and(FileType::is_symlink) + !self.file_type().is_some_and(FileType::is_symlink) && inner .iter() .filter_map(|key| key.to_str().map(str::to_lowercase)) @@ -2079,7 +2079,7 @@ impl PathData { .get_or_init(|| retrieve_xattr_list(self.path()).ok()); opt_xattrs.as_ref().is_some_and(|inner| { - self.file_type().is_some_and(FileType::is_symlink) && inner.get(cap_key).is_some() + !self.file_type().is_some_and(FileType::is_symlink) && inner.get(cap_key).is_some() }) } From 0ec16b5884bf94af669f896e739f71774b6a8184 Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Fri, 20 Mar 2026 23:27:08 -0500 Subject: [PATCH 16/17] Fixed --- src/uu/ls/src/ls.rs | 62 +++++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index ca7f2ac49e4..bf4476d9e6e 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -13,6 +13,8 @@ use rustc_hash::FxHashSet; use std::os::unix::fs::{FileTypeExt, MetadataExt}; #[cfg(windows)] use std::os::windows::fs::MetadataExt; +#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] +use std::sync::Once; use std::{borrow::Cow, cell::RefCell}; use std::{ cell::{LazyCell, OnceCell}, @@ -1930,8 +1932,6 @@ struct PathData { // can be used to avoid reading the filetype. Can be also called d_type: // https://www.gnu.org/software/libc/manual/html_node/Directory-Entries.html de: RefCell>>, - #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] - xattrs: OnceCell>>, security_context: OnceCell>, // Name of the file - will be empty for . or .. display_name: OsString, @@ -1939,6 +1939,10 @@ struct PathData { p_buf: PathBuf, must_dereference: bool, command_line: bool, + #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] + has_acl: OnceCell, + #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] + has_security_cap: OnceCell, } impl PathData { @@ -1983,8 +1987,6 @@ impl PathData { // nearly free compared to a metadata() call on a Path let ft: OnceCell> = OnceCell::new(); let md: OnceCell> = OnceCell::new(); - #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] - let xattrs: OnceCell>> = OnceCell::new(); let security_context: OnceCell> = OnceCell::new(); let de: RefCell>> = if let Some(de) = dir_entry { @@ -2008,13 +2010,15 @@ impl PathData { md, ft, de, - #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] - xattrs, security_context, display_name, p_buf, must_dereference, command_line, + #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] + has_acl: OnceCell::new(), + #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] + has_security_cap: OnceCell::new(), } } @@ -2057,30 +2061,40 @@ impl PathData { #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] pub fn has_acl(&self) -> bool { - let opt_xattrs = self - .xattrs - .get_or_init(|| retrieve_xattr_list(self.path()).ok()); - - opt_xattrs.as_ref().is_some_and(|inner| { - !self.file_type().is_some_and(FileType::is_symlink) - && inner - .iter() - .filter_map(|key| key.to_str().map(str::to_lowercase)) - .any(|xattr| xattr.contains("acl")) - }) + self.init_xattrs_once(); + + self.has_acl.get().is_some_and(|inner| *inner) } #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] pub fn has_security_cap(&self) -> bool { - let cap_key = OsStr::new("security.capability"); + self.init_xattrs_once(); - let opt_xattrs = self - .xattrs - .get_or_init(|| retrieve_xattr_list(self.path()).ok()); + self.has_security_cap.get().is_some_and(|inner| *inner) + } - opt_xattrs.as_ref().is_some_and(|inner| { - !self.file_type().is_some_and(FileType::is_symlink) && inner.get(cap_key).is_some() - }) + #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] + pub fn init_xattrs_once(&self) { + static START: Once = Once::new(); + + START.call_once(|| { + let _ = retrieve_xattr_list(self.path()).map(|inner| { + self.has_security_cap.get_or_init(|| { + let cap_key = OsStr::new("security.capability"); + + !self.file_type().is_some_and(FileType::is_symlink) + && inner.get(cap_key).is_some() + }); + + self.has_acl.get_or_init(|| { + !self.file_type().is_some_and(FileType::is_symlink) + && inner + .iter() + .filter_map(|key| key.to_str().map(str::to_lowercase)) + .any(|xattr| xattr.contains("acl")) + }); + }); + }); } fn file_type(&self) -> Option<&FileType> { From 615bf4b73cca6d426fc73987712e9eacd1e60b5e Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Fri, 20 Mar 2026 23:32:16 -0500 Subject: [PATCH 17/17] Fix lints --- src/uu/ls/src/ls.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index bf4476d9e6e..060b5160da5 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2082,8 +2082,7 @@ impl PathData { self.has_security_cap.get_or_init(|| { let cap_key = OsStr::new("security.capability"); - !self.file_type().is_some_and(FileType::is_symlink) - && inner.get(cap_key).is_some() + !self.file_type().is_some_and(FileType::is_symlink) && inner.contains(cap_key) }); self.has_acl.get_or_init(|| {