Skip to main content

vfsi_core/api/
metadata.rs

1//! Options shared by scalar convenience and vector metadata queries.
2
3#[bitfields::bitfield(u8)]
4#[derive(PartialEq, Eq)]
5struct AttrsFlags {
6    #[bits(default = true)]
7    follow: bool,
8    #[bits(7)]
9    _reserved: u8,
10}
11
12/// Select metadata fields and whether to follow the final symlink.
13/// Ancestor symlinks use ordinary backend path-resolution semantics.
14/// ```compile_fail
15/// use vfsi_core::api::AttrsOptions;
16/// AttrsOptions::new().with_follow_symlinks(false);
17/// ```
18#[derive(Clone, Copy, Debug)]
19#[must_use = "configuration and requests have no effect until used"]
20pub struct AttrsOptions {
21    fields: crate::api::Attributes,
22    flags: AttrsFlags,
23}
24impl Default for AttrsOptions {
25    fn default() -> Self {
26        Self {
27            fields: crate::api::Attributes::stat(),
28            flags: AttrsFlags::new(),
29        }
30    }
31}
32impl AttrsOptions {
33    /// Standard stat fields, following the final symlink.
34    pub fn new() -> Self {
35        Self::default()
36    }
37    /// Select requested attributes. Object type may additionally be fetched.
38    pub fn with_fields(mut self, fields: crate::api::Attributes) -> Self {
39        self.fields = fields;
40        self
41    }
42    /// Follow the final symlink when true; otherwise describe the link itself.
43    pub fn with_follow_final_symlink(mut self, follow: bool) -> Self {
44        self.flags.set_follow(follow);
45        self
46    }
47    /// Requested attributes; unavailable optional attributes remain absent.
48    pub fn fields(self) -> crate::api::Attributes {
49        self.fields
50    }
51    /// Whether to follow the final component if it is a symlink.
52    pub fn follow_final_symlink(self) -> bool {
53        self.flags.follow()
54    }
55}
56
57/// One attribute update for a path or opened object.
58///
59/// Paths can be supplied directly. Use [`super::Target`] for handles
60/// or mixed path/handle batches. Unspecified fields are unchanged; construction
61/// performs no I/O. Final symlinks are followed by default.
62#[derive(Clone, Copy)]
63#[must_use = "configuration and requests have no effect until used"]
64pub struct SetAttrsOp<T> {
65    target: T,
66    permissions: crate::Permissions,
67    uid: u32,
68    gid: u32,
69    len: u64,
70    accessed: Option<std::time::SystemTime>,
71    modified: Option<std::time::SystemTime>,
72    flags: AttrsFlags,
73    present: u8,
74}
75
76impl<T> SetAttrsOp<T> {
77    /// Prepare an update, following the final symlink for path targets.
78    pub fn new(target: T) -> Self {
79        Self {
80            target,
81            permissions: crate::Permissions::from_mode(0),
82            uid: 0,
83            gid: 0,
84            len: 0,
85            accessed: None,
86            modified: None,
87            flags: AttrsFlags::new(),
88            present: 0,
89        }
90    }
91
92    /// Follow the final path symlink when true, or modify the link itself when
93    /// false. Opened objects retain their identity regardless of this setting.
94    pub fn with_follow_final_symlink(mut self, follow: bool) -> Self {
95        self.flags.set_follow(follow);
96        self
97    }
98
99    /// The path or opened-object operand.
100    pub fn target(&self) -> &T {
101        &self.target
102    }
103
104    /// Change permissions, leaving other unspecified attributes unchanged.
105    pub fn permissions(mut self, permissions: crate::Permissions) -> Self {
106        self.permissions = permissions;
107        self.present |= 1 << 0;
108        self
109    }
110
111    /// Change the owner. `u32::MAX` is reserved and rejected before dispatch.
112    pub fn uid(mut self, uid: u32) -> Self {
113        self.uid = uid;
114        self.present |= 1 << 1;
115        self
116    }
117
118    /// Change the group. `u32::MAX` is reserved and rejected before dispatch.
119    pub fn gid(mut self, gid: u32) -> Self {
120        self.gid = gid;
121        self.present |= 1 << 2;
122        self
123    }
124
125    /// Truncate or extend the file. Zero explicitly truncates it to an empty file.
126    pub fn len(mut self, len: u64) -> Self {
127        self.len = len;
128        self.present |= 1 << 3;
129        self
130    }
131
132    /// Set the access timestamp.
133    pub fn accessed(mut self, accessed: std::time::SystemTime) -> Self {
134        self.accessed = Some(accessed);
135        self
136    }
137
138    /// Set the modification timestamp.
139    pub fn modified(mut self, modified: std::time::SystemTime) -> Self {
140        self.modified = Some(modified);
141        self
142    }
143
144    /// Reuse all requested changes and the symlink policy with another target.
145    /// This constructs an operation without I/O or cloning the original target.
146    pub fn with_target<U>(&self, target: U) -> SetAttrsOp<U> {
147        SetAttrsOp {
148            target,
149            permissions: self.permissions,
150            uid: self.uid,
151            gid: self.gid,
152            len: self.len,
153            accessed: self.accessed,
154            modified: self.modified,
155            flags: self.flags,
156            present: self.present,
157        }
158    }
159
160    /// Requested permission change, or `None` to leave permissions unchanged.
161    pub fn requested_permissions(&self) -> Option<crate::Permissions> {
162        (self.present & (1 << 0) != 0).then_some(self.permissions)
163    }
164    /// Requested owner change, or `None` to leave the owner unchanged.
165    pub fn requested_uid(&self) -> Option<u32> {
166        (self.present & (1 << 1) != 0).then_some(self.uid)
167    }
168    /// Requested group change, or `None` to leave the group unchanged.
169    pub fn requested_gid(&self) -> Option<u32> {
170        (self.present & (1 << 2) != 0).then_some(self.gid)
171    }
172    /// Requested length, including zero, or `None` to leave the length unchanged.
173    pub fn requested_len(&self) -> Option<u64> {
174        (self.present & (1 << 3) != 0).then_some(self.len)
175    }
176    /// Requested access timestamp, or `None` to leave it unchanged.
177    pub fn requested_accessed(&self) -> Option<std::time::SystemTime> {
178        self.accessed
179    }
180    /// Requested modification timestamp, or `None` to leave it unchanged.
181    pub fn requested_modified(&self) -> Option<std::time::SystemTime> {
182        self.modified
183    }
184
185    /// Whether the final path symlink is followed.
186    pub fn follow_final_symlink(&self) -> bool {
187        self.flags.follow()
188    }
189}
190
191impl<T: std::fmt::Debug> std::fmt::Debug for SetAttrsOp<T> {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        f.debug_struct("SetAttrsOp")
194            .field("target", self.target())
195            .field("permissions", &self.requested_permissions())
196            .field("uid", &self.requested_uid())
197            .field("gid", &self.requested_gid())
198            .field("len", &self.requested_len())
199            .field("accessed", &self.requested_accessed())
200            .field("modified", &self.requested_modified())
201            .field("flags", &self.flags)
202            .finish()
203    }
204}
205impl<'a, F> SetAttrsOp<super::Target<'a, F>> {
206    /// Prepare an update for an opened object without borrowing its pathname.
207    /// The handle remains owned by the caller and is checked at dispatch.
208    ///
209    /// ```no_run
210    /// use vfsi_core::api::{SetAttrsOp, Vfsi};
211    /// # fn example<F: Vfsi>(fs: &F, file: &F::File) -> vfsi_core::api::Result<()> {
212    /// fs.vsetattrs(&[SetAttrsOp::file(file).len(1024)])?;
213    /// # Ok(())
214    /// # }
215    /// ```
216    pub fn file(file: &'a F) -> Self {
217        Self::new(super::Target::File(file))
218    }
219}
220
221#[cfg(test)]
222mod option_layout_tests {
223    use super::*;
224    #[test]
225    fn retargeting_preserves_all_changes_without_cloning_the_target() {
226        struct NonClone;
227        let permissions = crate::Permissions::from_mode(0o640);
228        let accessed = std::time::UNIX_EPOCH + std::time::Duration::from_secs(123);
229        let modified = std::time::UNIX_EPOCH - std::time::Duration::from_secs(456);
230        let empty = SetAttrsOp::new(NonClone);
231        assert!(empty.follow_final_symlink());
232        assert_eq!(empty.requested_permissions(), None);
233        assert_eq!(empty.requested_len(), None);
234        assert_eq!(empty.requested_uid(), None);
235        assert_eq!(empty.requested_gid(), None);
236        assert_eq!(empty.requested_accessed(), None);
237        assert_eq!(empty.requested_modified(), None);
238        let op = empty
239            .permissions(permissions)
240            .len(0)
241            .uid(42)
242            .gid(43)
243            .accessed(accessed)
244            .modified(modified)
245            .with_follow_final_symlink(false);
246        for path in ["/first", "/second"] {
247            let rebound = op.with_target(path);
248            assert_eq!(*rebound.target(), path);
249            assert_eq!(rebound.requested_permissions(), Some(permissions));
250            assert_eq!(rebound.requested_len(), Some(0));
251            assert_eq!(rebound.requested_uid(), Some(42));
252            assert_eq!(rebound.requested_gid(), Some(43));
253            assert_eq!(rebound.requested_accessed(), Some(accessed));
254            assert_eq!(rebound.requested_modified(), Some(modified));
255            assert!(!rebound.follow_final_symlink());
256        }
257        let replaced = op
258            .with_target("/third")
259            .len(10)
260            .uid(44)
261            .with_follow_final_symlink(true);
262        assert_eq!(replaced.requested_len(), Some(10));
263        assert_eq!(replaced.requested_uid(), Some(44));
264        assert!(replaced.follow_final_symlink());
265        assert_eq!(op.requested_len(), Some(0));
266        assert_eq!(op.requested_uid(), Some(42));
267        assert!(!op.follow_final_symlink());
268    }
269    #[test]
270    fn packed_follow_flag_preserves_selected_fields() {
271        assert_eq!(std::mem::size_of::<AttrsFlags>(), 1);
272        assert!(AttrsOptions::default().follow_final_symlink());
273        let op = SetAttrsOp::new("/file").len(0).uid(42);
274        assert_eq!(*op.target(), "/file");
275        assert!(op.follow_final_symlink());
276        let op = op.with_follow_final_symlink(false);
277        assert!(!op.follow_final_symlink());
278        assert_eq!(op.requested_len(), Some(0));
279        assert_eq!(op.requested_uid(), Some(42));
280        let fields = crate::api::Attributes::SIZE | crate::api::Attributes::CHANGE;
281        let options = AttrsOptions::new().with_fields(fields);
282        for follow in [false, true, false] {
283            let changed = options.with_follow_final_symlink(follow);
284            assert_eq!(changed.follow_final_symlink(), follow);
285            assert_eq!(changed.fields(), fields);
286            assert!(options.follow_final_symlink());
287        }
288    }
289}
290
291#[cfg(test)]
292mod compact_update_tests {
293    use super::*;
294    #[test]
295    fn integer_presence_is_independent_and_keeps_zero_and_maximum_values() {
296        if cfg!(target_pointer_width = "64") {
297            assert_eq!(std::mem::size_of::<SetAttrsOp<&std::path::Path>>(), 72);
298        }
299        for bits in 0u8..16 {
300            for maximum in [false, true] {
301                let integer = if maximum { u32::MAX } else { 0 };
302                let length = if maximum { u64::MAX } else { 0 };
303                let permissions = crate::Permissions::from_mode(integer);
304                let mut op = SetAttrsOp::new("/file");
305                if bits & 1 != 0 {
306                    op = op.permissions(permissions);
307                }
308                if bits & 2 != 0 {
309                    op = op.uid(integer);
310                }
311                if bits & 4 != 0 {
312                    op = op.gid(integer);
313                }
314                if bits & 8 != 0 {
315                    op = op.len(length);
316                }
317                for op in [
318                    op,
319                    op.with_target("/other").with_follow_final_symlink(false),
320                ] {
321                    assert_eq!(
322                        op.requested_permissions(),
323                        (bits & 1 != 0).then_some(permissions)
324                    );
325                    assert_eq!(op.requested_uid(), (bits & 2 != 0).then_some(integer));
326                    assert_eq!(op.requested_gid(), (bits & 4 != 0).then_some(integer));
327                    assert_eq!(op.requested_len(), (bits & 8 != 0).then_some(length));
328                    assert_eq!(op.requested_accessed(), None);
329                    assert_eq!(op.requested_modified(), None);
330                }
331            }
332        }
333    }
334}