Skip to main content

dope_core/io/fs/
mod.rs

1use std::{io, marker, mem, os::fd, path, rc};
2
3use crate::{
4    backend,
5    driver::route::{self, kind},
6    io::transfer,
7    platform,
8};
9
10pub(crate) mod raw;
11
12pub struct RawMetadata {
13    pub len: u64,
14    pub modified: Option<(i64, u32)>,
15    pub regular: bool,
16}
17
18pub struct Directory {
19    inner: rc::Rc<fd::OwnedFd>,
20    _thread: o3::ThreadBound,
21}
22
23impl Directory {
24    pub fn open(path: impl AsRef<path::Path>) -> io::Result<Self> {
25        let directory = <backend::Backend as platform::Filesystem>::open_directory(path.as_ref())?;
26        Ok(Self {
27            inner: rc::Rc::new(directory),
28            _thread: o3::ThreadBound::NEW,
29        })
30    }
31
32    pub fn relative(&self, path: &str) -> io::Result<OpenPath> {
33        validate_relative(path)?;
34        let path = c_path(path)?;
35        Ok(OpenPath {
36            path,
37            directory: rc::Rc::clone(&self.inner),
38        })
39    }
40}
41
42pub struct OpenPath {
43    pub(crate) path: std::ffi::CString,
44    pub(crate) directory: rc::Rc<fd::OwnedFd>,
45}
46
47pub struct Native(marker::PhantomData<fn()>);
48
49/// The durability boundary requested for a file sync operation.
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub enum Sync {
52    /// Persist file data and the metadata required to retrieve it.
53    Data,
54    /// Persist all file data and metadata.
55    All,
56}
57
58#[allow(private_bounds)]
59pub trait Mode: raw::Mode {}
60
61impl<F: raw::Mode> Mode for F {}
62
63#[repr(transparent)]
64pub struct Metadata<F: Mode> {
65    raw: <F as raw::Mode>::Metadata,
66    mode: marker::PhantomData<fn() -> F>,
67}
68
69/// A native file operation borrowing every descriptor and output it captures.
70///
71/// Raw tokens cannot enter the safe file submission boundary:
72///
73/// ```compile_fail
74/// use std::{mem::MaybeUninit, os::fd::BorrowedFd};
75/// use dope_core::{
76///     driver::route::{KeyTag, Token},
77///     io::fs::{Native, Submission},
78/// };
79///
80/// fn submit_raw(fd: BorrowedFd<'_>, buffer: &mut [MaybeUninit<u8>], token: Token) {
81///     let _ = Submission::<Native, KeyTag<1>>::read_uninit(fd, buffer, 0, token);
82/// }
83/// ```
84///
85/// ```compile_fail
86/// use std::{mem::MaybeUninit, os::fd::AsFd};
87/// use dope_core::{
88///     driver::{Reference, route::{Epoch, KeyTag, SlotIndex}},
89///     io::fs::{Native, Submission},
90/// };
91///
92/// fn free_buffer_while_borrowed<'d>(driver: Reference<'d>) {
93///     let target = driver.targets::<KeyTag<1>>().bind(SlotIndex::ZERO, Epoch::INITIAL);
94///     let file = std::fs::File::open("data").unwrap();
95///     let mut buffer = vec![MaybeUninit::<u8>::uninit(); 8];
96///     let submission = Submission::<Native, KeyTag<1>>::read_uninit(
97///         file.as_fd(), &mut buffer, 0, target,
98///     ).unwrap();
99///     drop(buffer);
100///     drop(submission);
101/// }
102/// ```
103pub struct Submission<'a, 'd, F, Tag>
104where
105    F: Mode,
106    Tag: route::Tag,
107{
108    pub(in crate::io::fs) raw: <F as raw::Mode>::Raw,
109    pub(in crate::io::fs) target: route::Operation<'d, Tag>,
110    borrow: marker::PhantomData<&'a mut ()>,
111    mode: marker::PhantomData<fn() -> F>,
112}
113
114pub(crate) struct OpenState<F: Mode> {
115    path: OpenPath,
116    beneath: <F as raw::Mode>::Beneath,
117    mode: marker::PhantomData<fn() -> F>,
118}
119
120impl<F: Mode> Metadata<F> {
121    #[doc(hidden)]
122    pub fn parse(self) -> io::Result<RawMetadata> {
123        let metadata = <F as raw::Mode>::parse(&self.raw)?;
124        if metadata
125            .modified
126            .is_some_and(|(_, nanos)| nanos >= 1_000_000_000)
127        {
128            return Err(io::Error::new(
129                io::ErrorKind::InvalidData,
130                "dope: stat returned an invalid modification nanosecond",
131            ));
132        }
133        Ok(metadata)
134    }
135}
136
137impl<'a, 'd, F, Tag> Submission<'a, 'd, F, Tag>
138where
139    F: Mode,
140    Tag: route::Tag,
141{
142    fn bind(raw: <F as raw::Mode>::Raw, target: route::Operation<'d, Tag>) -> Self {
143        Self {
144            raw,
145            target,
146            borrow: marker::PhantomData,
147            mode: marker::PhantomData,
148        }
149    }
150
151    pub fn read_uninit(
152        fd: fd::BorrowedFd<'a>,
153        buffer: &'a mut [mem::MaybeUninit<u8>],
154        offset: u64,
155        target: route::Target<'d, Tag>,
156    ) -> io::Result<Self> {
157        let len = transfer::Len::try_io(buffer.len())?;
158        Ok(Self::bind(
159            <F as raw::Mode>::read(fd, buffer, len, offset),
160            target.operation(kind::READ),
161        ))
162    }
163
164    pub fn read(
165        fd: fd::BorrowedFd<'a>,
166        buffer: &'a mut [u8],
167        offset: u64,
168        target: route::Target<'d, Tag>,
169    ) -> io::Result<Self> {
170        let len = transfer::Len::try_io(buffer.len())?;
171        Ok(Self::bind(
172            <F as raw::Mode>::read_initialized(fd, buffer, len, offset),
173            target.operation(kind::READ),
174        ))
175    }
176
177    #[doc(hidden)]
178    pub fn write(
179        fd: fd::BorrowedFd<'a>,
180        buffer: &'a [u8],
181        offset: u64,
182        target: route::Target<'d, Tag>,
183    ) -> io::Result<Self> {
184        let len = transfer::Len::try_io(buffer.len())?;
185        Ok(Self::bind(
186            <F as raw::Mode>::write(fd, buffer, len, offset),
187            target.operation(kind::WRITE),
188        ))
189    }
190
191    pub fn stat_fd(
192        fd: fd::BorrowedFd<'a>,
193        output: &'a mut mem::MaybeUninit<Metadata<F>>,
194        target: route::Target<'d, Tag>,
195    ) -> Self {
196        Self::bind(
197            <F as raw::Mode>::stat(fd, output),
198            target.operation(kind::STAT),
199        )
200    }
201
202    /// Submit a durability barrier for all writes completed before this operation.
203    pub fn sync(fd: fd::BorrowedFd<'a>, mode: Sync, target: route::Target<'d, Tag>) -> Self {
204        Self::bind(
205            <F as raw::Mode>::sync(fd, mode),
206            target.operation(kind::SYNC),
207        )
208    }
209}
210
211impl<F: Mode> OpenState<F> {
212    pub(crate) fn new(path: OpenPath) -> Self {
213        Self {
214            path,
215            beneath: <F as raw::Mode>::confined_regular_open(),
216            mode: marker::PhantomData,
217        }
218    }
219
220    pub(crate) fn submission<'d, Tag: route::Tag>(
221        &self,
222        target: route::Target<'d, Tag>,
223    ) -> Submission<'_, 'd, F, Tag> {
224        Submission::bind(
225            <F as raw::Mode>::open(
226                &self.beneath,
227                fd::AsFd::as_fd(self.path.directory.as_ref()),
228                self.path.path.as_c_str(),
229            ),
230            target.operation(kind::OPEN),
231        )
232    }
233}
234
235#[doc(hidden)]
236pub struct OpenRequest<F: Mode> {
237    state: OpenState<F>,
238}
239
240impl<F: Mode> OpenRequest<F> {
241    pub fn submission<'d, Tag: route::Tag>(
242        &self,
243        target: route::Target<'d, Tag>,
244    ) -> Submission<'_, 'd, F, Tag> {
245        self.state.submission(target)
246    }
247}
248
249impl OpenPath {
250    #[doc(hidden)]
251    pub fn regular_request<F: Mode>(self) -> OpenRequest<F> {
252        OpenRequest {
253            state: OpenState::new(self),
254        }
255    }
256}
257
258fn c_path(path: &str) -> io::Result<std::ffi::CString> {
259    std::ffi::CString::new(path)
260        .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "dope: path has interior nul"))
261}
262
263fn validate_relative(path: &str) -> io::Result<()> {
264    const COMPONENT_MAX: usize = 255;
265
266    let invalid = || {
267        io::Error::new(
268            io::ErrorKind::InvalidInput,
269            "dope: path is not a confined relative path",
270        )
271    };
272
273    if path.is_empty() || path.starts_with('/') {
274        return Err(invalid());
275    }
276
277    let mut meaningful = false;
278    let mut oversized_component = false;
279    for component in path.split('/') {
280        if component == ".." {
281            return Err(invalid());
282        }
283        meaningful |= !component.is_empty() && component != ".";
284        oversized_component |= component.len() > COMPONENT_MAX;
285    }
286
287    if !meaningful {
288        return Err(invalid());
289    }
290    if path.len() >= libc::PATH_MAX as usize || oversized_component {
291        return Err(io::Error::new(
292            io::ErrorKind::InvalidInput,
293            "dope: confined path exceeds the platform limit",
294        ));
295    }
296    Ok(())
297}