Skip to main content

vfsi_sync/
traits.rs

1use super::*;
2
3/// Backend-owned continuation state for a paged directory visit.
4///
5/// The state is dropped automatically when the caller stops or an error
6/// occurs, so backends do not need an explicit cursor-close operation.
7#[doc(hidden)]
8pub struct DirPageCursor(Box<dyn std::any::Any + Send>);
9
10impl DirPageCursor {
11    pub fn new<T: std::any::Any + Send>(state: T) -> Self {
12        Self(Box::new(state))
13    }
14
15    pub fn is<T: std::any::Any + Send>(&self) -> bool {
16        self.0.is::<T>()
17    }
18
19    pub fn into_state<T: std::any::Any + Send>(self) -> VfResult<T> {
20        self.0
21            .downcast::<T>()
22            .map(|state| *state)
23            .map_err(|_| VfError::client(0, ERR_INVAL))
24    }
25}
26
27/// Backend page plus optional child seeds aligned with its entries.
28/// A seed starts a child's first page relative to its observed parent.
29#[doc(hidden)]
30pub type BackendDirectoryPage = (
31    Vec<VfAttrs>,
32    Option<DirPageCursor>,
33    Vec<Option<DirPageCursor>>,
34);
35
36/// Owned application page and traversal-scoped anchored child seeds.
37#[doc(hidden)]
38pub type DirectoryPage = (
39    crate::DirectoryListing,
40    Option<DirPageCursor>,
41    Vec<(std::path::PathBuf, DirPageCursor)>,
42);
43
44/// Resource limits for reading multiple complete files into memory.
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub struct ReadAllOptions {
47    max_total_bytes: usize,
48}
49
50impl ReadAllOptions {
51    pub const fn new() -> Self {
52        Self {
53            max_total_bytes: DEFAULT_READ_ALLV_MAX_TOTAL_BYTES,
54        }
55    }
56
57    /// Set the maximum combined size of all returned buffers.
58    pub const fn with_max_total_bytes(mut self, bytes: usize) -> Self {
59        self.max_total_bytes = bytes;
60        self
61    }
62
63    pub const fn max_total_bytes(self) -> usize {
64        self.max_total_bytes
65    }
66}
67
68impl Default for ReadAllOptions {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74fn contract_error(
75    operation: &str,
76    index: Option<usize>,
77    detail: impl std::fmt::Display,
78) -> VfError {
79    VfError::transport(
80        index,
81        format!("{operation} backend contract violation: {detail}"),
82    )
83}
84
85pub(crate) fn take_single_result<T>(operation: &str, mut results: Vec<T>) -> VfResult<T> {
86    if results.len() != 1 {
87        return Err(contract_error(
88            operation,
89            None,
90            format!("returned {} results for one request", results.len()),
91        ));
92    }
93    Ok(results.pop().expect("validated one result"))
94}
95
96pub(crate) fn validate_read_results(
97    operation: &str,
98    requests: &[ReadOp],
99    results: &[ReadResult],
100) -> VfResult<()> {
101    if results.len() != requests.len() {
102        return Err(contract_error(
103            operation,
104            None,
105            format!(
106                "returned {} results for {} requests",
107                results.len(),
108                requests.len()
109            ),
110        ));
111    }
112    for (index, (request, result)) in requests.iter().zip(results).enumerate() {
113        if result.file != request.file {
114            return Err(contract_error(
115                operation,
116                Some(index),
117                "result file does not match request",
118            ));
119        }
120        if result.data.len() > request.length {
121            return Err(contract_error(
122                operation,
123                Some(index),
124                format!(
125                    "returned {} bytes for a {}-byte read",
126                    result.data.len(),
127                    request.length
128                ),
129            ));
130        }
131        if let VfOffset::At(expected) = request.offset
132            && result.offset != expected
133        {
134            return Err(contract_error(
135                operation,
136                Some(index),
137                format!("result offset {} does not match {expected}", result.offset),
138            ));
139        }
140        if request.length != 0 && result.data.is_empty() && !result.eof {
141            return Err(contract_error(
142                operation,
143                Some(index),
144                "read made no progress without reporting EOF",
145            ));
146        }
147    }
148    Ok(())
149}
150
151pub(crate) fn validate_read_into_results(
152    operation: &str,
153    requests: &[ReadOp],
154    results: &[ReadIntoResult],
155) -> VfResult<()> {
156    if results.len() != requests.len() {
157        return Err(contract_error(
158            operation,
159            None,
160            format!(
161                "returned {} results for {} requests",
162                results.len(),
163                requests.len()
164            ),
165        ));
166    }
167    for (index, (request, result)) in requests.iter().zip(results).enumerate() {
168        if result.file != request.file {
169            return Err(contract_error(
170                operation,
171                Some(index),
172                "result file does not match request",
173            ));
174        }
175        if result.bytes_read > request.length {
176            return Err(contract_error(
177                operation,
178                Some(index),
179                format!(
180                    "returned {} bytes for a {}-byte read",
181                    result.bytes_read, request.length
182                ),
183            ));
184        }
185        if let VfOffset::At(expected) = request.offset
186            && result.offset != expected
187        {
188            return Err(contract_error(
189                operation,
190                Some(index),
191                format!("result offset {} does not match {expected}", result.offset),
192            ));
193        }
194        if request.length != 0 && result.bytes_read == 0 && !result.eof {
195            return Err(contract_error(
196                operation,
197                Some(index),
198                "read made no progress without reporting EOF",
199            ));
200        }
201    }
202    Ok(())
203}
204
205pub(crate) fn validate_write_results(
206    operation: &str,
207    requests: &[WriteOp<&FileOperand, &[u8]>],
208    results: &[WriteResult],
209) -> VfResult<()> {
210    if results.len() != requests.len() {
211        return Err(contract_error(
212            operation,
213            None,
214            format!(
215                "returned {} results for {} requests",
216                results.len(),
217                requests.len()
218            ),
219        ));
220    }
221    for (index, (request, result)) in requests.iter().zip(results).enumerate() {
222        if result.file != *request.file() {
223            return Err(contract_error(
224                operation,
225                Some(index),
226                "result file does not match request",
227            ));
228        }
229        if result.bytes_written > request.data().len() {
230            return Err(contract_error(
231                operation,
232                Some(index),
233                format!(
234                    "reported {} bytes written for a {}-byte write",
235                    result.bytes_written,
236                    request.data().len()
237                ),
238            ));
239        }
240        if let VfOffset::At(expected) = request.offset()
241            && result.offset != expected
242        {
243            return Err(contract_error(
244                operation,
245                Some(index),
246                format!("result offset {} does not match {expected}", result.offset),
247            ));
248        }
249    }
250    Ok(())
251}
252
253#[cfg(test)]
254mod contract_tests {
255    use super::*;
256
257    fn assert_contract_error(error: VfError, index: Option<usize>, detail: &str) {
258        assert_eq!(error.domain(), vfsi_core::ErrorDomain::Transport);
259        assert_eq!(error.index(), index);
260        assert_eq!(error.status(), None);
261        let message = error.to_string();
262        assert!(message.contains("backend contract violation"), "{message}");
263        assert!(message.contains(detail), "{message}");
264    }
265
266    fn request() -> ReadOp {
267        ReadOp::at(FileOperand::from_path("/file"), 0, 1)
268    }
269
270    fn result() -> ReadResult {
271        ReadResult {
272            file: FileOperand::from_path("/file"),
273            offset: 0,
274            data: vec![1],
275            eof: true,
276        }
277    }
278
279    #[test]
280    fn read_result_cardinality_is_checked_before_indexing() {
281        let requests = [request(), request()];
282        validate_read_results("test", &requests, &[result(), result()]).unwrap();
283        validate_read_results("test", &[], &[]).unwrap();
284        assert_contract_error(
285            validate_read_results("test", &requests, &[result()]).unwrap_err(),
286            None,
287            "returned 1 results for 2 requests",
288        );
289        assert_contract_error(
290            validate_read_results("test", &requests[..1], &[result(), result()]).unwrap_err(),
291            None,
292            "returned 2 results for 1 requests",
293        );
294    }
295
296    #[test]
297    fn read_result_identity_offset_progress_and_size_are_checked() {
298        let request = request();
299        validate_read_results("test", std::slice::from_ref(&request), &[result()]).unwrap();
300        let larger = ReadOp::at(FileOperand::from_path("/file"), 0, 4);
301        validate_read_results(
302            "test",
303            &[larger],
304            &[ReadResult {
305                eof: false,
306                ..result()
307            }],
308        )
309        .unwrap();
310        validate_read_results(
311            "test",
312            std::slice::from_ref(&request),
313            &[ReadResult {
314                data: Vec::new(),
315                eof: true,
316                ..result()
317            }],
318        )
319        .unwrap();
320        for malformed in [
321            ReadResult {
322                file: FileOperand::from_path("/other"),
323                ..result()
324            },
325            ReadResult {
326                offset: 1,
327                ..result()
328            },
329            ReadResult {
330                data: vec![1, 2],
331                ..result()
332            },
333            ReadResult {
334                data: Vec::new(),
335                eof: false,
336                ..result()
337            },
338        ] {
339            // Keep a valid prefix so attribution must identify the second
340            // request rather than always returning request zero.
341            assert_contract_error(
342                validate_read_results(
343                    "test",
344                    &[request.clone(), request.clone()],
345                    &[result(), malformed],
346                )
347                .unwrap_err(),
348                Some(1),
349                "test",
350            );
351        }
352    }
353
354    #[test]
355    fn write_result_identity_offset_size_and_cardinality_are_checked() {
356        let file = FileOperand::from_path("/file");
357        let request: WriteOp<&FileOperand, &[u8]> = WriteOp::new(&file, VfOffset::At(0), b"x");
358        let valid = WriteResult {
359            file: file.clone(),
360            offset: 0,
361            bytes_written: 1,
362            stable: true,
363        };
364        validate_write_results("test", &[request], std::slice::from_ref(&valid)).unwrap();
365        // Partial/zero progress is a valid backend result; vwrite_all_native owns
366        // the policy for completing it or rejecting a no-progress loop.
367        validate_write_results(
368            "test",
369            &[request],
370            &[WriteResult {
371                bytes_written: 0,
372                ..valid.clone()
373            }],
374        )
375        .unwrap();
376        validate_write_results("test", &[], &[]).unwrap();
377        assert_contract_error(
378            validate_write_results("test", &[request], &[]).unwrap_err(),
379            None,
380            "returned 0 results",
381        );
382        assert_contract_error(
383            validate_write_results("test", &[request], &[valid.clone(), valid.clone()])
384                .unwrap_err(),
385            None,
386            "returned 2 results",
387        );
388        for malformed in [
389            WriteResult {
390                file: FileOperand::from_path("/other"),
391                ..valid.clone()
392            },
393            WriteResult {
394                offset: 1,
395                ..valid.clone()
396            },
397            WriteResult {
398                bytes_written: 2,
399                ..valid.clone()
400            },
401        ] {
402            assert_contract_error(
403                validate_write_results("test", &[request, request], &[valid.clone(), malformed])
404                    .unwrap_err(),
405                Some(1),
406                "test",
407            );
408        }
409    }
410
411    #[test]
412    fn scalar_result_cardinality_is_checked_without_panicking() {
413        assert_contract_error(
414            take_single_result::<u8>("readlink", Vec::new()).unwrap_err(),
415            None,
416            "readlink",
417        );
418        assert_contract_error(
419            take_single_result("readlink", vec![1u8, 2]).unwrap_err(),
420            None,
421            "readlink",
422        );
423        assert_eq!(take_single_result("readlink", vec![7u8]).unwrap(), 7);
424    }
425}
426
427#[cfg(test)]
428mod walk_depth_tests {
429    use super::{DepthLimit, ListDirOptions};
430    #[test]
431    fn depth_limit_is_compact_and_finite_values_never_become_unlimited() {
432        const UNLIMITED: DepthLimit = DepthLimit::unlimited();
433        assert_eq!(std::mem::size_of::<DepthLimit>(), 2);
434        assert_eq!(std::mem::size_of::<Option<DepthLimit>>(), 2);
435        assert!(UNLIMITED.is_unlimited());
436        for depth in (0..=200).chain([201, 255, 256, 500, 65533]) {
437            let limit = DepthLimit::new(depth).unwrap();
438            assert!(!limit.is_unlimited());
439            assert_eq!(limit.get(), depth);
440        }
441        for depth in [65534, 65535, usize::MAX - 1, usize::MAX] {
442            assert_eq!(DepthLimit::new(depth), None);
443        }
444        assert_ne!(None, DepthLimit::new(0));
445        assert_ne!(None, Some(DepthLimit::unlimited()));
446    }
447    #[test]
448    fn depth_limits_normalize_without_wrapping_and_keep_other_options() {
449        const UNLIMITED: ListDirOptions = ListDirOptions::new()
450            .with_recursive(true)
451            .with_max_depth(DepthLimit::unlimited());
452        assert_eq!(UNLIMITED.max_depth_or(Default::default()).get(), usize::MAX);
453        for depth in 0..=200 {
454            let options = ListDirOptions::new()
455                .with_recursive(true)
456                .with_max_depth(if depth == usize::MAX {
457                    vfsi_core::api::DepthLimit::unlimited()
458                } else {
459                    vfsi_core::api::DepthLimit::new(depth).expect("finite depth")
460                })
461                .with_max_entries(7)
462                .with_max_path_bytes(11)
463                .with_truncate_at_max_depth(true);
464            assert_eq!(options.max_depth_or(Default::default()).get(), depth);
465            assert_eq!(
466                options.max_entries_or(vfsi_core::api::DEFAULT_DIRECTORY_MAX_ENTRIES),
467                7
468            );
469            assert_eq!(
470                options.max_path_bytes_or(vfsi_core::api::DEFAULT_DIRECTORY_MAX_PATH_BYTES),
471                11
472            );
473            assert!(options.truncate_at_max_depth());
474        }
475        for depth in [201, 254, 255, 256, 500, 65533, usize::MAX] {
476            let options =
477                ListDirOptions::new()
478                    .with_recursive(true)
479                    .with_max_depth(if depth == usize::MAX {
480                        vfsi_core::api::DepthLimit::unlimited()
481                    } else {
482                        vfsi_core::api::DepthLimit::new(depth).expect("finite depth")
483                    });
484            assert_eq!(options.max_depth_or(Default::default()).get(), depth);
485            assert_eq!(
486                options
487                    .with_max_depth(vfsi_core::api::DepthLimit::new(0).expect("finite depth"))
488                    .max_depth_or(Default::default())
489                    .get(),
490                0
491            );
492            assert_eq!(
493                options
494                    .with_max_depth(vfsi_core::api::DepthLimit::new(200).expect("finite depth"))
495                    .max_depth_or(Default::default())
496                    .get(),
497                200
498            );
499        }
500        assert_eq!(
501            ListDirOptions::unlimited()
502                .with_recursive(true)
503                .max_depth_or(Default::default())
504                .get(),
505            usize::MAX
506        );
507    }
508}