Skip to main content

disk_core/
xattr.rs

1//! Shared extended-attribute type, used across every filesystem that carries
2//! POSIX-style xattrs (SquashFS, ext, XFS).
3//!
4//! An xattr is a `name` in a namespace (`user.`, `trusted.`, `security.`,
5//! `system.`) mapped to an arbitrary **binary** `value`. The canonical example
6//! is `security.capability` — a packed struct granting a binary its Linux
7//! capabilities (e.g. `cap_net_raw` on `ping`), which is why xattrs must survive
8//! a SquashFS rebuild and why an editor exposes the raw bytes rather than
9//! pretending the value is text.
10
11// `hex.len().is_multiple_of(2)` is an inherent method only since Rust 1.87; the
12// vintage 1.73 build gets it from this trait. See CONTRIBUTING.md § Rust 1.73.
13#[cfg(feature = "rust173-polyfill")]
14use crate::rust173::IntIsMultipleOf as _;
15
16/// One extended attribute: a fully-qualified name and its raw value bytes.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Xattr {
19    /// Fully-qualified name including its namespace prefix, e.g.
20    /// `security.capability` or `user.mime_type`.
21    pub name: String,
22    /// Raw value bytes. May be arbitrary binary (a capability struct) or UTF-8
23    /// text (`user.*` attributes often are) — the caller decides how to render.
24    pub value: Vec<u8>,
25}
26
27impl Xattr {
28    /// Render the value for display: as text if it is valid, printable UTF-8,
29    /// otherwise as a `0x`-prefixed hex string. Lets a browse view show
30    /// `user.mime_type = text/plain` but `security.capability = 0x0100...`.
31    pub fn value_display(&self) -> String {
32        if let Ok(s) = std::str::from_utf8(&self.value) {
33            if !s.is_empty() && s.chars().all(|c| !c.is_control() || c == '\n' || c == '\t') {
34                return s.to_string();
35            }
36        }
37        format!("0x{}", hex_encode(&self.value))
38    }
39}
40
41/// Lowercase hex, no separators.
42pub fn hex_encode(bytes: &[u8]) -> String {
43    let mut s = String::with_capacity(bytes.len() * 2);
44    for b in bytes {
45        s.push(char::from_digit((b >> 4) as u32, 16).unwrap());
46        s.push(char::from_digit((b & 0xf) as u32, 16).unwrap());
47    }
48    s
49}
50
51/// Parse a value written by a user: `0x`-prefixed hex is decoded to raw bytes,
52/// anything else is taken as its UTF-8 bytes. The inverse of
53/// [`Xattr::value_display`] for round-tripping through an editor.
54pub fn parse_value(input: &str) -> Result<Vec<u8>, String> {
55    let trimmed = input.trim();
56    if let Some(hex) = trimmed
57        .strip_prefix("0x")
58        .or_else(|| trimmed.strip_prefix("0X"))
59    {
60        let hex: String = hex.chars().filter(|c| !c.is_whitespace()).collect();
61        if !hex.len().is_multiple_of(2) {
62            return Err("hex value must have an even number of digits".into());
63        }
64        let mut out = Vec::with_capacity(hex.len() / 2);
65        let bytes = hex.as_bytes();
66        let mut i = 0;
67        while i < bytes.len() {
68            let hi = (bytes[i] as char)
69                .to_digit(16)
70                .ok_or_else(|| format!("invalid hex digit {:?}", bytes[i] as char))?;
71            let lo = (bytes[i + 1] as char)
72                .to_digit(16)
73                .ok_or_else(|| format!("invalid hex digit {:?}", bytes[i + 1] as char))?;
74            out.push(((hi << 4) | lo) as u8);
75            i += 2;
76        }
77        Ok(out)
78    } else {
79        Ok(input.as_bytes().to_vec())
80    }
81}
82
83/// A recognized xattr namespace prefix. A name without one of these cannot be
84/// represented on disk, so an editor rejects it up front.
85pub const NAMESPACES: &[&str] = &["user.", "trusted.", "security.", "system."];
86
87/// True when `name` starts with a representable namespace prefix.
88pub fn has_valid_namespace(name: &str) -> bool {
89    NAMESPACES.iter().any(|p| name.starts_with(p))
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn text_values_display_as_text_binary_as_hex() {
98        let t = Xattr {
99            name: "user.mime_type".into(),
100            value: b"text/plain".to_vec(),
101        };
102        assert_eq!(t.value_display(), "text/plain");
103        let b = Xattr {
104            name: "security.capability".into(),
105            value: vec![1, 0, 0, 2, 0xff],
106        };
107        assert_eq!(b.value_display(), "0x01000002ff");
108    }
109
110    #[test]
111    fn parse_round_trips_hex_and_text() {
112        assert_eq!(parse_value("0x0100ff").unwrap(), vec![1, 0, 0xff]);
113        assert_eq!(parse_value("0x01 00 ff").unwrap(), vec![1, 0, 0xff]);
114        assert_eq!(parse_value("text/plain").unwrap(), b"text/plain".to_vec());
115        assert!(parse_value("0x123").is_err(), "odd hex length rejected");
116        assert!(parse_value("0xzz").is_err(), "bad hex digit rejected");
117    }
118
119    #[test]
120    fn namespace_validation() {
121        assert!(has_valid_namespace("security.capability"));
122        assert!(has_valid_namespace("user.foo"));
123        assert!(!has_valid_namespace("bogus.foo"));
124        assert!(!has_valid_namespace("capability"));
125    }
126}