1use std::time::Duration;
11use std::time::Instant;
12
13use crate::directory::ListFilter;
14use crate::error::FsError;
15use crate::error::FsErrorKind;
16use crate::error::FsOperation;
17use crate::error::FsResult;
18use crate::metadata::SymlinkPolicy;
19use crate::path::PathSemantics;
20use crate::path::RelativePath;
21
22#[non_exhaustive]
44#[derive(Clone, Debug, Eq, PartialEq, Default)]
45pub struct ListOptions {
46 recursive: bool,
48 symlink_policy: Option<SymlinkPolicy>,
50 include_metadata: bool,
52 page_size: Option<usize>,
54 filter: Option<ListFilter>,
61 max_depth: Option<usize>,
63 max_entries: Option<usize>,
65 deadline: Option<Duration>,
67}
68
69impl ListOptions {
70 #[inline]
72 #[must_use]
73 pub const fn with_recursive(mut self, recursive: bool) -> Self {
74 self.recursive = recursive;
75 self
76 }
77
78 #[inline]
80 #[must_use]
81 pub const fn recursive(&self) -> bool {
82 self.recursive
83 }
84
85 #[inline]
87 #[must_use]
88 pub const fn with_symlink_policy(mut self, policy: SymlinkPolicy) -> Self {
89 self.symlink_policy = Some(policy);
90 self
91 }
92
93 #[inline]
95 #[must_use]
96 pub const fn symlink_policy_override(&self) -> Option<SymlinkPolicy> {
97 self.symlink_policy
98 }
99
100 #[inline]
102 #[must_use]
103 pub const fn with_include_metadata(mut self, include: bool) -> Self {
104 self.include_metadata = include;
105 self
106 }
107
108 #[inline]
110 #[must_use]
111 pub const fn include_metadata(&self) -> bool {
112 self.include_metadata
113 }
114
115 #[inline]
117 #[must_use]
118 pub const fn with_page_size(mut self, page_size: Option<usize>) -> Self {
119 self.page_size = page_size;
120 self
121 }
122
123 #[inline]
125 #[must_use]
126 pub const fn page_size(&self) -> Option<usize> {
127 self.page_size
128 }
129
130 #[inline]
132 #[must_use]
133 pub fn with_prefix(mut self, prefix: Option<String>) -> Self {
134 self.filter = prefix.map(ListFilter::Subtree);
135 self
136 }
137
138 #[inline]
144 #[must_use]
145 pub fn prefix(&self) -> Option<&str> {
146 match self.filter.as_ref() {
147 Some(ListFilter::Subtree(prefix)) => Some(prefix),
148 _ => None,
149 }
150 }
151
152 #[must_use]
154 pub fn with_filter(mut self, filter: Option<ListFilter>) -> Self {
155 self.filter = filter;
156 self
157 }
158
159 #[must_use]
164 pub fn filter(&self) -> Option<&ListFilter> {
165 self.filter.as_ref()
166 }
167
168 #[must_use]
170 pub fn object_keys() -> Self {
171 Self {
172 recursive: true,
173 filter: Some(ListFilter::LiteralPrefix(String::new())),
174 ..Self::default()
175 }
176 }
177
178 #[inline]
180 #[must_use]
181 pub const fn with_max_depth(mut self, max_depth: Option<usize>) -> Self {
182 self.max_depth = max_depth;
183 self
184 }
185
186 #[inline]
188 #[must_use]
189 pub const fn max_depth(&self) -> Option<usize> {
190 self.max_depth
191 }
192
193 #[inline]
195 #[must_use]
196 pub const fn with_max_entries(mut self, max_entries: Option<usize>) -> Self {
197 self.max_entries = max_entries;
198 self
199 }
200
201 #[inline]
203 #[must_use]
204 pub const fn max_entries(&self) -> Option<usize> {
205 self.max_entries
206 }
207
208 #[inline]
210 #[must_use]
211 pub const fn with_deadline(mut self, deadline: Option<Duration>) -> Self {
212 self.deadline = deadline;
213 self
214 }
215
216 #[inline]
218 #[must_use]
219 pub const fn deadline(&self) -> Option<Duration> {
220 self.deadline
221 }
222
223 pub fn validate(&self) -> FsResult<()> {
230 if self.page_size == Some(0) {
231 return Err(FsError::new(
232 FsErrorKind::InvalidOptions,
233 FsOperation::List,
234 "list page size must be greater than zero",
235 ));
236 }
237 self.validate_common()?;
238 if let Some(ListFilter::Subtree(prefix)) = self.filter.as_ref() {
239 let parsed = RelativePath::parse(prefix).map_err(|_| {
240 FsError::new(
241 FsErrorKind::InvalidOptions,
242 FsOperation::List,
243 "list subtree must be a canonical relative path",
244 )
245 })?;
246 if parsed.as_str() != prefix {
247 return Err(FsError::new(
248 FsErrorKind::InvalidOptions,
249 FsOperation::List,
250 "list subtree must be a canonical relative path",
251 ));
252 }
253 }
254 if self
255 .deadline
256 .is_some_and(|deadline| Instant::now().checked_add(deadline).is_none())
257 {
258 return Err(FsError::new(
259 FsErrorKind::InvalidOptions,
260 FsOperation::List,
261 "list deadline exceeds the platform monotonic-clock range",
262 ));
263 }
264 Ok(())
265 }
266
267 pub fn validate_for(&self, semantics: PathSemantics) -> FsResult<()> {
276 self.validate_common()?;
277 self.validate_filter(semantics)
278 }
279
280 fn validate_common(&self) -> FsResult<()> {
282 if self.page_size == Some(0) {
283 return Err(FsError::new(
284 FsErrorKind::InvalidOptions,
285 FsOperation::List,
286 "list page size must be greater than zero",
287 ));
288 }
289 if self
290 .deadline
291 .is_some_and(|deadline| Instant::now().checked_add(deadline).is_none())
292 {
293 return Err(FsError::new(
294 FsErrorKind::InvalidOptions,
295 FsOperation::List,
296 "list deadline exceeds the platform monotonic-clock range",
297 ));
298 }
299 Ok(())
300 }
301
302 fn validate_filter(&self, semantics: PathSemantics) -> FsResult<()> {
304 match (semantics, self.filter.as_ref()) {
305 (PathSemantics::Hierarchical, Some(ListFilter::Subtree(prefix))) => {
306 let parsed = RelativePath::parse(prefix).map_err(|_| {
307 FsError::new(
308 FsErrorKind::InvalidOptions,
309 FsOperation::List,
310 "list subtree must be a canonical relative path",
311 )
312 })?;
313 if parsed.as_str() != prefix {
314 return Err(FsError::new(
315 FsErrorKind::InvalidOptions,
316 FsOperation::List,
317 "list subtree must be a canonical relative path",
318 ));
319 }
320 }
321 (PathSemantics::Hierarchical, Some(ListFilter::LiteralPrefix(_))) => {
322 return Err(FsError::new(
323 FsErrorKind::InvalidOptions,
324 FsOperation::List,
325 "literal prefix requires flat path semantics",
326 ));
327 }
328 (PathSemantics::ObjectKey | PathSemantics::ProviderSpecific, Some(ListFilter::Subtree(_))) => {
329 return Err(FsError::new(
330 FsErrorKind::InvalidOptions,
331 FsOperation::List,
332 "subtree filter requires hierarchical path semantics",
333 ));
334 }
335 (PathSemantics::ObjectKey | PathSemantics::ProviderSpecific, Some(ListFilter::LiteralPrefix(prefix)))
336 if prefix.contains('\0') =>
337 {
338 return Err(FsError::new(
339 FsErrorKind::InvalidOptions,
340 FsOperation::List,
341 "literal prefix contains NUL",
342 ));
343 }
344 _ => {}
345 }
346 if matches!(semantics, PathSemantics::ObjectKey | PathSemantics::ProviderSpecific)
347 && (!self.recursive || self.max_depth.is_some())
348 {
349 return Err(FsError::new(
350 FsErrorKind::InvalidOptions,
351 FsOperation::List,
352 "flat listing requires recursive traversal without max_depth",
353 ));
354 }
355 Ok(())
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use std::hint::black_box;
362 use std::time::Duration;
363
364 use super::ListOptions;
365 use crate::directory::ListFilter;
366 use crate::metadata::SymlinkPolicy;
367
368 #[test]
369 fn option_accessors_are_executed_at_runtime() {
370 let constructor: fn() -> ListOptions = black_box(Default::default);
371 let with_recursive: fn(ListOptions, bool) -> ListOptions = black_box(ListOptions::with_recursive);
372 let recursive: fn(&ListOptions) -> bool = black_box(ListOptions::recursive);
373 let with_symlink_policy: fn(ListOptions, SymlinkPolicy) -> ListOptions =
374 black_box(ListOptions::with_symlink_policy);
375 let symlink_policy_override: fn(&ListOptions) -> Option<SymlinkPolicy> =
376 black_box(ListOptions::symlink_policy_override);
377 let with_include_metadata: fn(ListOptions, bool) -> ListOptions = black_box(ListOptions::with_include_metadata);
378 let include_metadata: fn(&ListOptions) -> bool = black_box(ListOptions::include_metadata);
379 let with_page_size: fn(ListOptions, Option<usize>) -> ListOptions = black_box(ListOptions::with_page_size);
380 let page_size: fn(&ListOptions) -> Option<usize> = black_box(ListOptions::page_size);
381 let with_prefix: fn(ListOptions, Option<String>) -> ListOptions = black_box(ListOptions::with_prefix);
382 let prefix: for<'a> fn(&'a ListOptions) -> Option<&'a str> = black_box(ListOptions::prefix);
383 let with_filter: fn(ListOptions, Option<ListFilter>) -> ListOptions = black_box(ListOptions::with_filter);
384 let filter: for<'a> fn(&'a ListOptions) -> Option<&'a ListFilter> = black_box(ListOptions::filter);
385 let object_keys: fn() -> ListOptions = black_box(ListOptions::object_keys);
386 let with_max_depth: fn(ListOptions, Option<usize>) -> ListOptions = black_box(ListOptions::with_max_depth);
387 let max_depth: fn(&ListOptions) -> Option<usize> = black_box(ListOptions::max_depth);
388 let with_max_entries: fn(ListOptions, Option<usize>) -> ListOptions = black_box(ListOptions::with_max_entries);
389 let max_entries: fn(&ListOptions) -> Option<usize> = black_box(ListOptions::max_entries);
390 let with_deadline: fn(ListOptions, Option<Duration>) -> ListOptions = black_box(ListOptions::with_deadline);
391 let deadline: fn(&ListOptions) -> Option<Duration> = black_box(ListOptions::deadline);
392
393 let options = with_deadline(
394 with_max_entries(
395 with_max_depth(
396 with_filter(
397 with_prefix(
398 with_page_size(
399 with_include_metadata(
400 with_symlink_policy(with_recursive(constructor(), true), SymlinkPolicy::Reject),
401 true,
402 ),
403 Some(20),
404 ),
405 Some("reports".to_owned()),
406 ),
407 Some(ListFilter::Subtree("reports".to_owned())),
408 ),
409 Some(2),
410 ),
411 Some(10),
412 ),
413 Some(Duration::from_secs(1)),
414 );
415
416 assert!(recursive(&options));
417 assert_eq!(Some(SymlinkPolicy::Reject), symlink_policy_override(&options));
418 assert!(include_metadata(&options));
419 assert_eq!(Some(20), page_size(&options));
420 assert_eq!(Some("reports"), prefix(&options));
421 assert!(matches!(filter(&options), Some(ListFilter::Subtree(value)) if value == "reports"));
422 assert!(recursive(&object_keys()));
423 assert_eq!(Some(2), max_depth(&options));
424 assert_eq!(Some(10), max_entries(&options));
425 assert_eq!(Some(Duration::from_secs(1)), deadline(&options));
426 }
427}