1use super::*;
2
3#[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#[doc(hidden)]
30pub type BackendDirectoryPage = (
31 Vec<VfAttrs>,
32 Option<DirPageCursor>,
33 Vec<Option<DirPageCursor>>,
34);
35
36#[doc(hidden)]
38pub type DirectoryPage = (
39 crate::DirectoryListing,
40 Option<DirPageCursor>,
41 Vec<(std::path::PathBuf, DirPageCursor)>,
42);
43
44#[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 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 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 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}