Skip to main content

hara_native/file/
legacy.rs

1//! Migration adapter from the synchronous `FileProvider` boundary.
2//!
3//! The adapter intentionally performs the legacy operation when its future is
4//! polled. Native disk migration must replace this with an I/O-scheduler-backed
5//! implementation before `FileProvider` is removed.
6
7use super::{
8    FilesystemCallContext, FilesystemDescriptor, FilesystemEntry, FilesystemEntryPage,
9    FilesystemFuture, FilesystemMutation, FilesystemMutationContext, FilesystemPageRequest,
10    IFilesystem, SynchronousFileProvider,
11};
12use crate::file::{CopyOptions, DeleteOptions, FileError, MkdirOptions, MoveOptions, WriteOptions};
13use std::cell::Cell;
14use std::rc::Rc;
15
16#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
17impl SynchronousFileProvider for crate::file::NativeFileProvider {}
18impl SynchronousFileProvider for crate::file::MemoryFileProvider {}
19impl SynchronousFileProvider for crate::file::UnsupportedFileProvider {}
20
21pub(super) struct LegacyFilesystem<P> {
22    provider: P,
23    descriptor: FilesystemDescriptor,
24    closed: Rc<Cell<bool>>,
25}
26
27impl<P> LegacyFilesystem<P> {
28    pub fn new(provider: P, descriptor: FilesystemDescriptor) -> Self {
29        Self {
30            provider,
31            descriptor,
32            closed: Rc::new(Cell::new(false)),
33        }
34    }
35
36    fn before_call(&self, context: &FilesystemCallContext) -> Result<(), FileError> {
37        if self.closed.get() {
38            return Err(FileError::Io("filesystem provider is closed".into()));
39        }
40        context.check()
41    }
42
43    fn require_no_revision_expectation(
44        mutation: &FilesystemMutationContext,
45    ) -> Result<(), FileError> {
46        if mutation.required() {
47            Err(FileError::Unsupported)
48        } else {
49            Ok(())
50        }
51    }
52}
53
54impl<P: SynchronousFileProvider> IFilesystem for LegacyFilesystem<P> {
55    fn descriptor(&self) -> FilesystemDescriptor {
56        self.descriptor.clone()
57    }
58
59    fn stat<'a>(
60        &'a self,
61        context: FilesystemCallContext,
62        path: String,
63    ) -> FilesystemFuture<'a, FilesystemEntry> {
64        Box::pin(async move {
65            self.before_call(&context)?;
66            self.provider.stat_entry(&path).map(FilesystemEntry::from)
67        })
68    }
69
70    fn read<'a>(
71        &'a self,
72        context: FilesystemCallContext,
73        path: String,
74    ) -> FilesystemFuture<'a, Vec<u8>> {
75        Box::pin(async move {
76            self.before_call(&context)?;
77            self.provider.read_bytes(&path)
78        })
79    }
80
81    fn write<'a>(
82        &'a self,
83        context: FilesystemCallContext,
84        path: String,
85        bytes: Vec<u8>,
86        options: WriteOptions,
87        mutation: FilesystemMutationContext,
88    ) -> FilesystemFuture<'a, FilesystemMutation> {
89        Box::pin(async move {
90            self.before_call(&context)?;
91            Self::require_no_revision_expectation(&mutation)?;
92            self.provider
93                .write_bytes(&path, bytes, options)
94                .map(FilesystemMutation::path)
95        })
96    }
97
98    fn entries_page<'a>(
99        &'a self,
100        context: FilesystemCallContext,
101        path: String,
102        request: FilesystemPageRequest,
103    ) -> FilesystemFuture<'a, FilesystemEntryPage> {
104        Box::pin(async move {
105            self.before_call(&context)?;
106            if request.limit == 0 {
107                return Err(FileError::InvalidPath(
108                    "filesystem page limit must be positive".into(),
109                ));
110            }
111            let start = request
112                .token
113                .as_deref()
114                .unwrap_or("0")
115                .parse::<usize>()
116                .map_err(|_| FileError::InvalidPath("invalid filesystem page token".into()))?;
117            let mut entries = self.provider.entries_values(&path)?;
118            entries.sort_by(|left, right| left.path.cmp(&right.path));
119            if start > entries.len() {
120                return Err(FileError::InvalidPath(
121                    "filesystem page token is outside the directory".into(),
122                ));
123            }
124            let end = start.saturating_add(request.limit).min(entries.len());
125            let next_token = (end < entries.len()).then(|| end.to_string());
126            Ok(FilesystemEntryPage {
127                entries: entries[start..end]
128                    .iter()
129                    .cloned()
130                    .map(FilesystemEntry::from)
131                    .collect(),
132                next_token,
133            })
134        })
135    }
136
137    fn mkdir<'a>(
138        &'a self,
139        context: FilesystemCallContext,
140        path: String,
141        options: MkdirOptions,
142        mutation: FilesystemMutationContext,
143    ) -> FilesystemFuture<'a, FilesystemMutation> {
144        Box::pin(async move {
145            self.before_call(&context)?;
146            Self::require_no_revision_expectation(&mutation)?;
147            self.provider
148                .mkdir_path(&path, options)
149                .map(FilesystemMutation::path)
150        })
151    }
152
153    fn delete<'a>(
154        &'a self,
155        context: FilesystemCallContext,
156        path: String,
157        options: DeleteOptions,
158        mutation: FilesystemMutationContext,
159    ) -> FilesystemFuture<'a, FilesystemMutation> {
160        Box::pin(async move {
161            self.before_call(&context)?;
162            Self::require_no_revision_expectation(&mutation)?;
163            self.provider
164                .delete_path(&path, options)
165                .map(FilesystemMutation::path)
166        })
167    }
168
169    fn copy<'a>(
170        &'a self,
171        context: FilesystemCallContext,
172        source: String,
173        target: String,
174        options: CopyOptions,
175        mutation: FilesystemMutationContext,
176    ) -> FilesystemFuture<'a, FilesystemMutation> {
177        Box::pin(async move {
178            self.before_call(&context)?;
179            Self::require_no_revision_expectation(&mutation)?;
180            self.provider
181                .copy_path(&source, &target, options)
182                .map(FilesystemMutation::path)
183        })
184    }
185
186    fn move_entry<'a>(
187        &'a self,
188        context: FilesystemCallContext,
189        source: String,
190        target: String,
191        options: MoveOptions,
192        mutation: FilesystemMutationContext,
193    ) -> FilesystemFuture<'a, FilesystemMutation> {
194        Box::pin(async move {
195            self.before_call(&context)?;
196            Self::require_no_revision_expectation(&mutation)?;
197            self.provider
198                .move_path(&source, &target, options)
199                .map(FilesystemMutation::path)
200        })
201    }
202
203    fn close<'a>(&'a self, context: FilesystemCallContext) -> FilesystemFuture<'a, ()> {
204        Box::pin(async move {
205            context.check()?;
206            self.closed.set(true);
207            Ok(())
208        })
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn close_marks_the_legacy_adapter_closed() {
218        let filesystem = LegacyFilesystem::new(
219            crate::file::MemoryFileProvider::new("/"),
220            FilesystemDescriptor::legacy("memory", "memory fixture"),
221        );
222        assert!(!filesystem.closed.get());
223        filesystem.closed.set(true);
224        assert!(matches!(
225            filesystem.before_call(&FilesystemCallContext::default()),
226            Err(FileError::Io(_))
227        ));
228    }
229}