Skip to main content

zsh/
attr.rs

1//! Extended attributes (xattr) module - port of Modules/attr.c
2//!
3//! Provides zgetattr, zsetattr, zdelattr, zlistattr builtins for
4//! manipulating extended file attributes.
5
6use std::ffi::CString;
7use std::io;
8
9/// Options for xattr operations
10#[derive(Debug, Default, Clone)]
11pub struct XattrOptions {
12    pub no_dereference: bool,
13}
14
15/// Get an extended attribute value
16#[cfg(target_os = "macos")]
17pub fn getxattr(path: &str, name: &str, options: &XattrOptions) -> io::Result<Vec<u8>> {
18    let path_c = CString::new(path)
19        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid path"))?;
20    let name_c = CString::new(name)
21        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid attr name"))?;
22
23    let flags = if options.no_dereference {
24        libc::XATTR_NOFOLLOW
25    } else {
26        0
27    };
28
29    let size = unsafe {
30        libc::getxattr(
31            path_c.as_ptr(),
32            name_c.as_ptr(),
33            std::ptr::null_mut(),
34            0,
35            0,
36            flags,
37        )
38    };
39
40    if size < 0 {
41        return Err(io::Error::last_os_error());
42    }
43
44    if size == 0 {
45        return Ok(Vec::new());
46    }
47
48    let mut buf = vec![0u8; size as usize];
49
50    let result = unsafe {
51        libc::getxattr(
52            path_c.as_ptr(),
53            name_c.as_ptr(),
54            buf.as_mut_ptr() as *mut libc::c_void,
55            size as usize,
56            0,
57            flags,
58        )
59    };
60
61    if result < 0 {
62        return Err(io::Error::last_os_error());
63    }
64
65    buf.truncate(result as usize);
66    Ok(buf)
67}
68
69#[cfg(target_os = "linux")]
70pub fn getxattr(path: &str, name: &str, options: &XattrOptions) -> io::Result<Vec<u8>> {
71    let path_c = CString::new(path)
72        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid path"))?;
73    let name_c = CString::new(name)
74        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid attr name"))?;
75
76    let size = if options.no_dereference {
77        unsafe { libc::lgetxattr(path_c.as_ptr(), name_c.as_ptr(), std::ptr::null_mut(), 0) }
78    } else {
79        unsafe { libc::getxattr(path_c.as_ptr(), name_c.as_ptr(), std::ptr::null_mut(), 0) }
80    };
81
82    if size < 0 {
83        return Err(io::Error::last_os_error());
84    }
85
86    if size == 0 {
87        return Ok(Vec::new());
88    }
89
90    let mut buf = vec![0u8; size as usize];
91
92    let result = if options.no_dereference {
93        unsafe {
94            libc::lgetxattr(
95                path_c.as_ptr(),
96                name_c.as_ptr(),
97                buf.as_mut_ptr() as *mut libc::c_void,
98                size as usize,
99            )
100        }
101    } else {
102        unsafe {
103            libc::getxattr(
104                path_c.as_ptr(),
105                name_c.as_ptr(),
106                buf.as_mut_ptr() as *mut libc::c_void,
107                size as usize,
108            )
109        }
110    };
111
112    if result < 0 {
113        return Err(io::Error::last_os_error());
114    }
115
116    buf.truncate(result as usize);
117    Ok(buf)
118}
119
120#[cfg(not(any(target_os = "macos", target_os = "linux")))]
121pub fn getxattr(_path: &str, _name: &str, _options: &XattrOptions) -> io::Result<Vec<u8>> {
122    Err(io::Error::new(
123        io::ErrorKind::Unsupported,
124        "xattr not supported",
125    ))
126}
127
128/// Set an extended attribute value
129#[cfg(target_os = "macos")]
130pub fn setxattr(path: &str, name: &str, value: &[u8], options: &XattrOptions) -> io::Result<()> {
131    let path_c = CString::new(path)
132        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid path"))?;
133    let name_c = CString::new(name)
134        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid attr name"))?;
135
136    let flags = if options.no_dereference {
137        libc::XATTR_NOFOLLOW
138    } else {
139        0
140    };
141
142    let result = unsafe {
143        libc::setxattr(
144            path_c.as_ptr(),
145            name_c.as_ptr(),
146            value.as_ptr() as *const libc::c_void,
147            value.len(),
148            0,
149            flags,
150        )
151    };
152
153    if result < 0 {
154        return Err(io::Error::last_os_error());
155    }
156
157    Ok(())
158}
159
160#[cfg(target_os = "linux")]
161pub fn setxattr(path: &str, name: &str, value: &[u8], options: &XattrOptions) -> io::Result<()> {
162    let path_c = CString::new(path)
163        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid path"))?;
164    let name_c = CString::new(name)
165        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid attr name"))?;
166
167    let result = if options.no_dereference {
168        unsafe {
169            libc::lsetxattr(
170                path_c.as_ptr(),
171                name_c.as_ptr(),
172                value.as_ptr() as *const libc::c_void,
173                value.len(),
174                0,
175            )
176        }
177    } else {
178        unsafe {
179            libc::setxattr(
180                path_c.as_ptr(),
181                name_c.as_ptr(),
182                value.as_ptr() as *const libc::c_void,
183                value.len(),
184                0,
185            )
186        }
187    };
188
189    if result < 0 {
190        return Err(io::Error::last_os_error());
191    }
192
193    Ok(())
194}
195
196#[cfg(not(any(target_os = "macos", target_os = "linux")))]
197pub fn setxattr(
198    _path: &str,
199    _name: &str,
200    _value: &[u8],
201    _options: &XattrOptions,
202) -> io::Result<()> {
203    Err(io::Error::new(
204        io::ErrorKind::Unsupported,
205        "xattr not supported",
206    ))
207}
208
209/// Remove an extended attribute
210#[cfg(target_os = "macos")]
211pub fn removexattr(path: &str, name: &str, options: &XattrOptions) -> io::Result<()> {
212    let path_c = CString::new(path)
213        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid path"))?;
214    let name_c = CString::new(name)
215        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid attr name"))?;
216
217    let flags = if options.no_dereference {
218        libc::XATTR_NOFOLLOW
219    } else {
220        0
221    };
222
223    let result = unsafe { libc::removexattr(path_c.as_ptr(), name_c.as_ptr(), flags) };
224
225    if result < 0 {
226        return Err(io::Error::last_os_error());
227    }
228
229    Ok(())
230}
231
232#[cfg(target_os = "linux")]
233pub fn removexattr(path: &str, name: &str, options: &XattrOptions) -> io::Result<()> {
234    let path_c = CString::new(path)
235        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid path"))?;
236    let name_c = CString::new(name)
237        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid attr name"))?;
238
239    let result = if options.no_dereference {
240        unsafe { libc::lremovexattr(path_c.as_ptr(), name_c.as_ptr()) }
241    } else {
242        unsafe { libc::removexattr(path_c.as_ptr(), name_c.as_ptr()) }
243    };
244
245    if result < 0 {
246        return Err(io::Error::last_os_error());
247    }
248
249    Ok(())
250}
251
252#[cfg(not(any(target_os = "macos", target_os = "linux")))]
253pub fn removexattr(_path: &str, _name: &str, _options: &XattrOptions) -> io::Result<()> {
254    Err(io::Error::new(
255        io::ErrorKind::Unsupported,
256        "xattr not supported",
257    ))
258}
259
260/// List extended attributes
261#[cfg(target_os = "macos")]
262pub fn listxattr(path: &str, options: &XattrOptions) -> io::Result<Vec<String>> {
263    let path_c = CString::new(path)
264        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid path"))?;
265
266    let flags = if options.no_dereference {
267        libc::XATTR_NOFOLLOW
268    } else {
269        0
270    };
271
272    let size = unsafe { libc::listxattr(path_c.as_ptr(), std::ptr::null_mut(), 0, flags) };
273
274    if size < 0 {
275        return Err(io::Error::last_os_error());
276    }
277
278    if size == 0 {
279        return Ok(Vec::new());
280    }
281
282    let mut buf = vec![0u8; size as usize];
283
284    let result = unsafe {
285        libc::listxattr(
286            path_c.as_ptr(),
287            buf.as_mut_ptr() as *mut libc::c_char,
288            size as usize,
289            flags,
290        )
291    };
292
293    if result < 0 {
294        return Err(io::Error::last_os_error());
295    }
296
297    buf.truncate(result as usize);
298    parse_xattr_list(&buf)
299}
300
301#[cfg(target_os = "linux")]
302pub fn listxattr(path: &str, options: &XattrOptions) -> io::Result<Vec<String>> {
303    let path_c = CString::new(path)
304        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid path"))?;
305
306    let size = if options.no_dereference {
307        unsafe { libc::llistxattr(path_c.as_ptr(), std::ptr::null_mut(), 0) }
308    } else {
309        unsafe { libc::listxattr(path_c.as_ptr(), std::ptr::null_mut(), 0) }
310    };
311
312    if size < 0 {
313        return Err(io::Error::last_os_error());
314    }
315
316    if size == 0 {
317        return Ok(Vec::new());
318    }
319
320    let mut buf = vec![0u8; size as usize];
321
322    let result = if options.no_dereference {
323        unsafe {
324            libc::llistxattr(
325                path_c.as_ptr(),
326                buf.as_mut_ptr() as *mut libc::c_char,
327                size as usize,
328            )
329        }
330    } else {
331        unsafe {
332            libc::listxattr(
333                path_c.as_ptr(),
334                buf.as_mut_ptr() as *mut libc::c_char,
335                size as usize,
336            )
337        }
338    };
339
340    if result < 0 {
341        return Err(io::Error::last_os_error());
342    }
343
344    buf.truncate(result as usize);
345    parse_xattr_list(&buf)
346}
347
348#[cfg(not(any(target_os = "macos", target_os = "linux")))]
349pub fn listxattr(_path: &str, _options: &XattrOptions) -> io::Result<Vec<String>> {
350    Err(io::Error::new(
351        io::ErrorKind::Unsupported,
352        "xattr not supported",
353    ))
354}
355
356fn parse_xattr_list(buf: &[u8]) -> io::Result<Vec<String>> {
357    let mut names = Vec::new();
358    let mut start = 0;
359
360    for (i, &byte) in buf.iter().enumerate() {
361        if byte == 0 {
362            if i > start {
363                let name = String::from_utf8_lossy(&buf[start..i]).into_owned();
364                names.push(name);
365            }
366            start = i + 1;
367        }
368    }
369
370    Ok(names)
371}
372
373/// Execute zgetattr builtin
374pub fn builtin_zgetattr(file: &str, attr: &str, options: &XattrOptions) -> (i32, Option<String>) {
375    match getxattr(file, attr, options) {
376        Ok(value) => {
377            let s = String::from_utf8_lossy(&value).into_owned();
378            (0, Some(s))
379        }
380        Err(e) => (1, Some(format!("zgetattr: {}: {}\n", file, e))),
381    }
382}
383
384/// Execute zsetattr builtin
385pub fn builtin_zsetattr(
386    file: &str,
387    attr: &str,
388    value: &str,
389    options: &XattrOptions,
390) -> (i32, String) {
391    match setxattr(file, attr, value.as_bytes(), options) {
392        Ok(()) => (0, String::new()),
393        Err(e) => (1, format!("zsetattr: {}: {}\n", file, e)),
394    }
395}
396
397/// Execute zdelattr builtin
398pub fn builtin_zdelattr(file: &str, attrs: &[&str], options: &XattrOptions) -> (i32, String) {
399    for attr in attrs {
400        if let Err(e) = removexattr(file, attr, options) {
401            return (1, format!("zdelattr: {}: {}\n", file, e));
402        }
403    }
404    (0, String::new())
405}
406
407/// Execute zlistattr builtin
408pub fn builtin_zlistattr(file: &str, options: &XattrOptions) -> (i32, Vec<String>, String) {
409    match listxattr(file, options) {
410        Ok(attrs) => (0, attrs, String::new()),
411        Err(e) => (1, Vec::new(), format!("zlistattr: {}: {}\n", file, e)),
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    #[test]
420    fn test_xattr_options_default() {
421        let opts = XattrOptions::default();
422        assert!(!opts.no_dereference);
423    }
424
425    #[test]
426    fn test_parse_xattr_list_empty() {
427        let buf: &[u8] = &[];
428        let result = parse_xattr_list(buf).unwrap();
429        assert!(result.is_empty());
430    }
431
432    #[test]
433    fn test_parse_xattr_list_single() {
434        let buf = b"user.test\0";
435        let result = parse_xattr_list(buf).unwrap();
436        assert_eq!(result, vec!["user.test"]);
437    }
438
439    #[test]
440    fn test_parse_xattr_list_multiple() {
441        let buf = b"user.test1\0user.test2\0user.test3\0";
442        let result = parse_xattr_list(buf).unwrap();
443        assert_eq!(result, vec!["user.test1", "user.test2", "user.test3"]);
444    }
445
446    #[test]
447    fn test_builtin_zgetattr_nonexistent() {
448        let opts = XattrOptions::default();
449        let (status, _) = builtin_zgetattr("/nonexistent/path", "user.test", &opts);
450        assert_eq!(status, 1);
451    }
452
453    #[test]
454    fn test_builtin_zsetattr_nonexistent() {
455        let opts = XattrOptions::default();
456        let (status, _) = builtin_zsetattr("/nonexistent/path", "user.test", "value", &opts);
457        assert_eq!(status, 1);
458    }
459
460    #[test]
461    fn test_builtin_zlistattr_nonexistent() {
462        let opts = XattrOptions::default();
463        let (status, _, _) = builtin_zlistattr("/nonexistent/path", &opts);
464        assert_eq!(status, 1);
465    }
466}