Skip to main content

vfsi_core/
write.rs

1use crate::{Fd, VfFile, VfOffset};
2use std::path::Path;
3
4#[bitfields::bitfield(u8)]
5struct NativeWriteFlags {
6    creation: bool,
7    truncate: bool,
8    #[bits(6)]
9    _reserved: u8,
10}
11
12/// A native write with owned or borrowed target and payload storage.
13/// Positioning is always [`VfOffset`], with packed create/truncate flags.
14/// The fixed policy types let constructors infer storage without annotations.
15///
16/// ```
17/// use vfsi_core::{VfFile, VfOffset, WriteOp};
18/// let current = WriteOp::new(VfFile::from_fd(7), VfOffset::Cur, vec![1, 2]);
19/// let positional = WriteOp::at(VfFile::from_fd(7), 11, vec![3, 4]);
20/// assert_eq!(current.offset(), VfOffset::Cur);
21/// assert_eq!(positional.offset(), VfOffset::At(11));
22/// assert_eq!(positional.borrowed().data(), &[3, 4]);
23/// ```
24pub type WriteOp<T = VfFile, D = Vec<u8>> = WriteRequest<T, D, VfOffset, u8>;
25
26/// One write operation, independent of how its target and payload are stored.
27///
28/// Native adapters may own a `VfFile` and `Vec<u8>` while preparing a batch;
29/// execution borrows both through [`WriteRequest::borrowed`]. The portable API uses
30/// an opaque wrapper over this storage with a borrowed application handle and an absolute `u64`
31/// offset. Borrowing for dispatch never clones a handle or copies payload bytes.
32///
33/// The final storage parameter is `u8` for native path flags and `()` for
34/// portable/retained-handle requests, so positional operations pay no space
35/// overhead for path-only creation and truncation flags.
36#[derive(Clone, Copy)]
37pub struct WriteRequest<T, D, O, P> {
38    file: T,
39    offset: O,
40    data: D,
41    flags: P,
42}
43
44impl<T, D, O, P: Default> WriteRequest<T, D, O, P> {
45    /// Prepare an operation without I/O; the offset type determines positioning.
46    pub fn new(file: T, offset: O, data: D) -> Self {
47        Self {
48            file,
49            offset,
50            data,
51            flags: P::default(),
52        }
53    }
54}
55
56impl<T, D> WriteRequest<T, D, VfOffset, u8> {
57    /// Whether the native path write creates a missing file.
58    pub fn creates(&self) -> bool {
59        NativeWriteFlags::from_bits(self.flags).creation()
60    }
61    /// Whether the native path write truncates before writing.
62    pub fn truncates(&self) -> bool {
63        NativeWriteFlags::from_bits(self.flags).truncate()
64    }
65    /// Create a missing native path when executing this operation.
66    pub fn with_creation(mut self) -> Self {
67        let mut flags = NativeWriteFlags::from_bits(self.flags);
68        flags.set_creation(true);
69        self.flags = flags.into_bits();
70        self
71    }
72    /// Truncate a native path before writing, fused into the native write.
73    pub fn with_truncate(mut self) -> Self {
74        let mut flags = NativeWriteFlags::from_bits(self.flags);
75        flags.set_truncate(true);
76        self.flags = flags.into_bits();
77        self
78    }
79}
80
81impl<T, D: AsRef<[u8]>, O: Copy, P: Copy> WriteRequest<T, D, O, P> {
82    /// Borrow preparation storage for execution, preserving all options.
83    pub fn borrowed(&self) -> WriteRequest<&T, &[u8], O, P> {
84        WriteRequest {
85            file: &self.file,
86            offset: self.offset,
87            data: self.data.as_ref(),
88            flags: self.flags,
89        }
90    }
91}
92
93impl<T, D, O: Copy, P> WriteRequest<T, D, O, P> {
94    /// Positioning policy, or an absolute offset for portable operations.
95    pub fn offset(&self) -> O {
96        self.offset
97    }
98}
99
100impl<D, O, P> WriteRequest<VfFile, D, O, P> {
101    /// Target storage owned while preparing a native request.
102    pub fn file(&self) -> &VfFile {
103        &self.file
104    }
105}
106impl<T, O, P> WriteRequest<T, Vec<u8>, O, P> {
107    /// Payload storage owned while preparing a native request.
108    pub fn data(&self) -> &[u8] {
109        &self.data
110    }
111}
112
113impl<'a, T, O, P> WriteRequest<&'a T, &'a [u8], O, P> {
114    /// Borrowed target. Constructing an operation never opens or closes it.
115    pub fn file(&self) -> &'a T {
116        self.file
117    }
118    /// Borrowed payload; ownership remains with the caller.
119    pub fn data(&self) -> &'a [u8] {
120        self.data
121    }
122}
123
124impl<T, D, O: From<u64>, P: Default> WriteRequest<T, D, O, P> {
125    /// Prepare an absolute-offset write; validation happens at dispatch.
126    /// Portable writes do not change the handle's cursor. Native append
127    /// descriptors retain the backend's append positioning semantics.
128    ///
129    /// ```no_run
130    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp, WriteOptions};
131    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
132    /// let file = fs.create("/output")?;
133    /// let result = fs.vwrite(&[WriteOp::at(&file, 0, b"hello")], WriteOptions::new().write_all(true));
134    /// let close = fs.close_files(vec![file]);
135    /// result?;
136    /// close?;
137    /// # Ok(())
138    /// # }
139    /// ```
140    pub fn at(file: T, offset: u64, data: D) -> Self {
141        Self::new(file, offset.into(), data)
142    }
143}
144
145impl WriteOp {
146    pub fn from_path(path: &str, offset: VfOffset, data: Vec<u8>) -> Self {
147        Self::new(VfFile::from_path(path), offset, data)
148    }
149    pub fn from_os_path(path: &Path, offset: VfOffset, data: Vec<u8>) -> Self {
150        Self::new(VfFile::from_os_path(path), offset, data)
151    }
152    pub fn from_fd(fd: Fd, offset: VfOffset, data: Vec<u8>) -> Self {
153        Self::new(VfFile::from_fd(fd), offset, data)
154    }
155}
156
157impl<T, D: AsRef<[u8]>, O: std::fmt::Debug, P> std::fmt::Debug for WriteRequest<T, D, O, P> {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        f.debug_struct("WriteOp")
160            .field("offset", &self.offset)
161            .field("length", &self.data.as_ref().len())
162            .finish_non_exhaustive()
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn owned_storage_is_borrowed_without_copying_or_losing_native_options() {
172        let file = VfFile::from_os_path(Path::new("/output"));
173        for offset in [VfOffset::At(7), VfOffset::Cur, VfOffset::End] {
174            for (create, truncate) in [(false, false), (true, false), (false, true), (true, true)] {
175                let mut op = WriteOp::new(file.clone(), offset, vec![0x5a; 1024]);
176                if create {
177                    op = op.with_creation();
178                }
179                if truncate {
180                    op = op.with_truncate();
181                }
182                let native: WriteOp<&VfFile, &[u8]> = op.borrowed();
183                assert!(std::ptr::eq(native.file(), op.file()));
184                assert_eq!(native.data().as_ptr(), op.data().as_ptr());
185                assert_eq!(native.data().len(), op.data().len());
186                assert_eq!(native.offset(), offset);
187                assert_eq!(native.creates(), create);
188                assert_eq!(native.truncates(), truncate);
189                // Preparing multiple dispatch vectors does not consume storage.
190                assert_eq!(op.borrowed().data().as_ptr(), native.data().as_ptr());
191            }
192        }
193        let empty = WriteOp::from_fd(7, VfOffset::Cur, Vec::new());
194        assert!(empty.borrowed().data().is_empty());
195    }
196
197    #[test]
198    fn portable_operations_are_copyable_without_handle_traits_or_extra_flags_storage() {
199        struct NonClone;
200        let file = NonClone;
201        let payload = b"payload";
202        let op = crate::api::WriteOp::at(&file, 11, payload);
203        let copy = op;
204        assert!(std::ptr::eq(op.file(), copy.file()));
205        assert_eq!(op.data().as_ptr(), payload.as_ptr());
206        assert_eq!(copy.data().as_ptr(), payload.as_ptr());
207        assert_eq!(copy.offset(), 11);
208        assert_eq!(
209            std::mem::size_of_val(&op),
210            std::mem::size_of::<(&NonClone, u64, &[u8])>()
211        );
212        assert_eq!(std::mem::size_of::<NativeWriteFlags>(), 1);
213        // Handle requests also carry no path-only flags, even with native offsets.
214        type HandleOp<'a> = WriteRequest<&'a NonClone, &'a [u8], VfOffset, ()>;
215        assert_eq!(
216            std::mem::size_of::<HandleOp<'_>>(),
217            std::mem::size_of::<(&NonClone, VfOffset, &[u8])>()
218        );
219        assert!(!format!("{op:?}").is_empty());
220    }
221}