1use std::num::NonZeroUsize;
4
5use crate::{CloudFilesCoreError, Result};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub struct Alignment(u64);
10
11impl Alignment {
12 pub const ONE: Self = Self(1);
14
15 #[must_use]
17 pub const fn new(bytes: u64) -> Option<Self> {
18 if bytes == 0 { None } else { Some(Self(bytes)) }
19 }
20
21 #[must_use]
23 pub const fn get(self) -> u64 {
24 self.0
25 }
26
27 pub fn intersection(self, other: Self) -> Result<Self> {
33 let divisor = greatest_common_divisor(self.0, other.0);
34 let left = self.0 / divisor;
35 let Some(bytes) = left.checked_mul(other.0) else {
36 return Err(CloudFilesCoreError::AlignmentIntersectionOverflow {
37 left: self.0,
38 right: other.0,
39 });
40 };
41 Ok(Self(bytes))
42 }
43}
44
45const fn greatest_common_divisor(mut left: u64, mut right: u64) -> u64 {
46 while right != 0 {
47 let remainder = left % right;
48 left = right;
49 right = remainder;
50 }
51 left
52}
53
54#[expect(
56 clippy::struct_excessive_bools,
57 reason = "identity guarantees are independent capabilities rather than mutually exclusive state"
58)]
59#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
60pub struct IdentityCapabilities {
61 pub stable_item_ids: bool,
63 pub path_independent_item_ids: bool,
65 pub same_root_move_preserves_identity: bool,
67 pub cross_root_move_preserves_identity: bool,
69}
70
71impl IdentityCapabilities {
72 #[must_use]
74 pub const fn intersection(self, other: Self) -> Self {
75 Self {
76 stable_item_ids: self.stable_item_ids && other.stable_item_ids,
77 path_independent_item_ids: self.path_independent_item_ids
78 && other.path_independent_item_ids,
79 same_root_move_preserves_identity: self.same_root_move_preserves_identity
80 && other.same_root_move_preserves_identity,
81 cross_root_move_preserves_identity: self.cross_root_move_preserves_identity
82 && other.cross_root_move_preserves_identity,
83 }
84 }
85
86 pub fn validate_core_requirements(self) -> Result<()> {
92 if !self.stable_item_ids {
93 return Err(CloudFilesCoreError::MissingRequiredCapability {
94 capability: "stable_item_ids",
95 });
96 }
97 if !self.path_independent_item_ids {
98 return Err(CloudFilesCoreError::MissingRequiredCapability {
99 capability: "path_independent_item_ids",
100 });
101 }
102 if !self.same_root_move_preserves_identity {
103 return Err(CloudFilesCoreError::MissingRequiredCapability {
104 capability: "same_root_move_preserves_identity",
105 });
106 }
107 Ok(())
108 }
109}
110
111#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
113pub struct RevisionCapabilities {
114 pub separate_metadata_and_content: bool,
116 pub conditional_metadata_mutation: bool,
118 pub conditional_content_access: bool,
120}
121
122impl RevisionCapabilities {
123 #[must_use]
125 pub const fn intersection(self, other: Self) -> Self {
126 Self {
127 separate_metadata_and_content: self.separate_metadata_and_content
128 && other.separate_metadata_and_content,
129 conditional_metadata_mutation: self.conditional_metadata_mutation
130 && other.conditional_metadata_mutation,
131 conditional_content_access: self.conditional_content_access
132 && other.conditional_content_access,
133 }
134 }
135}
136
137#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
139pub struct EnumerationCapabilities {
140 pub paged: bool,
142 pub stable_snapshot: bool,
144}
145
146impl EnumerationCapabilities {
147 #[must_use]
149 pub const fn intersection(self, other: Self) -> Self {
150 Self {
151 paged: self.paged && other.paged,
152 stable_snapshot: self.stable_snapshot && other.stable_snapshot,
153 }
154 }
155}
156
157#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
159pub struct AnchoredChangeCapabilities {
160 pub cursor_reset: bool,
162 pub tombstones: bool,
164}
165
166impl AnchoredChangeCapabilities {
167 const fn intersection(self, other: Self) -> Self {
168 Self {
169 cursor_reset: self.cursor_reset && other.cursor_reset,
170 tombstones: self.tombstones && other.tombstones,
171 }
172 }
173}
174
175#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
177pub struct ChangeCapabilities {
178 pub anchored: Option<AnchoredChangeCapabilities>,
180 pub snapshot_diff: bool,
182 pub external_invalidation: bool,
184}
185
186impl ChangeCapabilities {
187 #[must_use]
189 pub const fn intersection(self, other: Self) -> Self {
190 let anchored = match (self.anchored, other.anchored) {
191 (Some(left), Some(right)) => Some(left.intersection(right)),
192 _ => None,
193 };
194 Self {
195 anchored,
196 snapshot_diff: self.snapshot_diff && other.snapshot_diff,
197 external_invalidation: self.external_invalidation && other.external_invalidation,
198 }
199 }
200}
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub struct RangeHydrationCapabilities {
205 pub alignment: Alignment,
207 pub partial_cancel: bool,
209}
210
211impl RangeHydrationCapabilities {
212 pub fn intersection(self, other: Self) -> Result<Self> {
218 Ok(Self {
219 alignment: self.alignment.intersection(other.alignment)?,
220 partial_cancel: self.partial_cancel && other.partial_cancel,
221 })
222 }
223}
224
225#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
227pub struct HydrationCapabilities {
228 pub whole_file: bool,
230 pub range: Option<RangeHydrationCapabilities>,
232 pub progressive_range: Option<RangeHydrationCapabilities>,
234 pub incremental_from_existing: bool,
236}
237
238impl HydrationCapabilities {
239 pub fn intersection(self, other: Self) -> Result<Self> {
245 let range = match (self.range, other.range) {
246 (Some(left), Some(right)) => Some(left.intersection(right)?),
247 _ => None,
248 };
249 let progressive_range = match (self.progressive_range, other.progressive_range) {
250 (Some(left), Some(right)) => Some(left.intersection(right)?),
251 _ => None,
252 };
253 Ok(Self {
254 whole_file: self.whole_file && other.whole_file,
255 range,
256 progressive_range,
257 incremental_from_existing: self.incremental_from_existing
258 && other.incremental_from_existing,
259 })
260 }
261}
262
263#[expect(
265 clippy::struct_excessive_bools,
266 reason = "mutation support is an independent capability set rather than mutually exclusive state"
267)]
268#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
269pub struct MutationCapabilities {
270 pub create: bool,
272 pub modify_metadata: bool,
274 pub modify_content: bool,
276 pub delete: bool,
278 pub atomic_move: bool,
280 pub cross_root_move: bool,
282 pub resumable_upload: bool,
284}
285
286impl MutationCapabilities {
287 #[must_use]
289 pub const fn intersection(self, other: Self) -> Self {
290 Self {
291 create: self.create && other.create,
292 modify_metadata: self.modify_metadata && other.modify_metadata,
293 modify_content: self.modify_content && other.modify_content,
294 delete: self.delete && other.delete,
295 atomic_move: self.atomic_move && other.atomic_move,
296 cross_root_move: self.cross_root_move && other.cross_root_move,
297 resumable_upload: self.resumable_upload && other.resumable_upload,
298 }
299 }
300}
301
302#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
304pub struct ContentStorageModes {
305 pub platform_managed: bool,
307 pub provider_managed: bool,
309 pub hybrid: bool,
311}
312
313impl ContentStorageModes {
314 #[must_use]
316 pub const fn intersection(self, other: Self) -> Self {
317 Self {
318 platform_managed: self.platform_managed && other.platform_managed,
319 provider_managed: self.provider_managed && other.provider_managed,
320 hybrid: self.hybrid && other.hybrid,
321 }
322 }
323
324 #[must_use]
326 pub const fn is_supported(self) -> bool {
327 self.platform_managed || self.provider_managed || self.hybrid
328 }
329}
330
331#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
333pub struct MaterializationCapabilities {
334 pub storage_modes: ContentStorageModes,
336}
337
338impl MaterializationCapabilities {
339 #[must_use]
341 pub const fn intersection(self, other: Self) -> Self {
342 Self {
343 storage_modes: self.storage_modes.intersection(other.storage_modes),
344 }
345 }
346}
347
348#[expect(
350 clippy::struct_excessive_bools,
351 reason = "eviction guarantees are independent capabilities rather than mutually exclusive state"
352)]
353#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
354pub struct EvictionCapabilities {
355 pub supported: bool,
357 pub pinning: bool,
359 pub dirty_guard: bool,
361 pub open_lease_guard: bool,
363}
364
365impl EvictionCapabilities {
366 #[must_use]
368 pub const fn intersection(self, other: Self) -> Self {
369 let supported = self.supported && other.supported;
370 Self {
371 supported,
372 pinning: supported && self.pinning && other.pinning,
373 dirty_guard: supported && self.dirty_guard && other.dirty_guard,
374 open_lease_guard: supported && self.open_lease_guard && other.open_lease_guard,
375 }
376 }
377}
378
379#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
381pub enum CancellationLevel {
382 #[default]
384 None,
385 BestEffortRequest,
387 ExactRequest,
389 RangeSubset,
391}
392
393impl CancellationLevel {
394 #[must_use]
396 pub fn intersection(self, other: Self) -> Self {
397 std::cmp::min(self, other)
398 }
399}
400
401#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
403pub struct NamingCapabilities {
404 pub case_sensitive: bool,
406 pub case_preserving: bool,
408 pub normalization_preserving: bool,
410}
411
412impl NamingCapabilities {
413 #[must_use]
415 pub const fn intersection(self, other: Self) -> Self {
416 Self {
417 case_sensitive: self.case_sensitive && other.case_sensitive,
418 case_preserving: self.case_preserving && other.case_preserving,
419 normalization_preserving: self.normalization_preserving
420 && other.normalization_preserving,
421 }
422 }
423}
424
425#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
427pub struct CloudFilesLimits {
428 pub native_identity_max_bytes: Option<NonZeroUsize>,
430 pub max_transfer_chunk: Option<NonZeroUsize>,
432 pub max_in_flight_requests: Option<NonZeroUsize>,
434 pub directory_page_size_hint: Option<NonZeroUsize>,
436 pub name_max_encoded_bytes: Option<NonZeroUsize>,
438}
439
440impl CloudFilesLimits {
441 #[must_use]
443 pub fn intersection(self, other: Self) -> Self {
444 Self {
445 native_identity_max_bytes: minimum_optional_limit(
446 self.native_identity_max_bytes,
447 other.native_identity_max_bytes,
448 ),
449 max_transfer_chunk: minimum_optional_limit(
450 self.max_transfer_chunk,
451 other.max_transfer_chunk,
452 ),
453 max_in_flight_requests: minimum_optional_limit(
454 self.max_in_flight_requests,
455 other.max_in_flight_requests,
456 ),
457 directory_page_size_hint: minimum_optional_limit(
458 self.directory_page_size_hint,
459 other.directory_page_size_hint,
460 ),
461 name_max_encoded_bytes: minimum_optional_limit(
462 self.name_max_encoded_bytes,
463 other.name_max_encoded_bytes,
464 ),
465 }
466 }
467
468 pub fn validate_native_identity(self, encoded: &[u8]) -> Result<()> {
474 let Some(max_bytes) = self.native_identity_max_bytes else {
475 return Ok(());
476 };
477 if encoded.len() > max_bytes.get() {
478 return Err(CloudFilesCoreError::NativeIdentityTooLarge {
479 actual_bytes: encoded.len(),
480 max_bytes: max_bytes.get(),
481 });
482 }
483 Ok(())
484 }
485}
486
487fn minimum_optional_limit(
488 left: Option<NonZeroUsize>,
489 right: Option<NonZeroUsize>,
490) -> Option<NonZeroUsize> {
491 match (left, right) {
492 (Some(left), Some(right)) => Some(std::cmp::min(left, right)),
493 (Some(limit), None) | (None, Some(limit)) => Some(limit),
494 (None, None) => None,
495 }
496}
497
498#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
500pub struct CloudFilesCapabilities {
501 pub identity: IdentityCapabilities,
503 pub revisions: RevisionCapabilities,
505 pub enumeration: EnumerationCapabilities,
507 pub changes: ChangeCapabilities,
509 pub hydration: HydrationCapabilities,
511 pub mutations: MutationCapabilities,
513 pub materialization: MaterializationCapabilities,
515 pub eviction: EvictionCapabilities,
517 pub cancellation: CancellationLevel,
519 pub naming: NamingCapabilities,
521 pub limits: CloudFilesLimits,
523}
524
525impl CloudFilesCapabilities {
526 #[must_use]
532 pub const fn unconstrained() -> Self {
533 let unconstrained_range = RangeHydrationCapabilities {
534 alignment: Alignment::ONE,
535 partial_cancel: true,
536 };
537 Self {
538 identity: IdentityCapabilities {
539 stable_item_ids: true,
540 path_independent_item_ids: true,
541 same_root_move_preserves_identity: true,
542 cross_root_move_preserves_identity: true,
543 },
544 revisions: RevisionCapabilities {
545 separate_metadata_and_content: true,
546 conditional_metadata_mutation: true,
547 conditional_content_access: true,
548 },
549 enumeration: EnumerationCapabilities {
550 paged: true,
551 stable_snapshot: true,
552 },
553 changes: ChangeCapabilities {
554 anchored: Some(AnchoredChangeCapabilities {
555 cursor_reset: true,
556 tombstones: true,
557 }),
558 snapshot_diff: true,
559 external_invalidation: true,
560 },
561 hydration: HydrationCapabilities {
562 whole_file: true,
563 range: Some(unconstrained_range),
564 progressive_range: Some(unconstrained_range),
565 incremental_from_existing: true,
566 },
567 mutations: MutationCapabilities {
568 create: true,
569 modify_metadata: true,
570 modify_content: true,
571 delete: true,
572 atomic_move: true,
573 cross_root_move: true,
574 resumable_upload: true,
575 },
576 materialization: MaterializationCapabilities {
577 storage_modes: ContentStorageModes {
578 platform_managed: true,
579 provider_managed: true,
580 hybrid: true,
581 },
582 },
583 eviction: EvictionCapabilities {
584 supported: true,
585 pinning: true,
586 dirty_guard: true,
587 open_lease_guard: true,
588 },
589 cancellation: CancellationLevel::RangeSubset,
590 naming: NamingCapabilities {
591 case_sensitive: true,
592 case_preserving: true,
593 normalization_preserving: true,
594 },
595 limits: CloudFilesLimits {
596 native_identity_max_bytes: None,
597 max_transfer_chunk: None,
598 max_in_flight_requests: None,
599 directory_page_size_hint: None,
600 name_max_encoded_bytes: None,
601 },
602 }
603 }
604
605 pub fn intersection(self, other: Self) -> Result<Self> {
614 Ok(Self {
615 identity: self.identity.intersection(other.identity),
616 revisions: self.revisions.intersection(other.revisions),
617 enumeration: self.enumeration.intersection(other.enumeration),
618 changes: self.changes.intersection(other.changes),
619 hydration: self.hydration.intersection(other.hydration)?,
620 mutations: self.mutations.intersection(other.mutations),
621 materialization: self.materialization.intersection(other.materialization),
622 eviction: self.eviction.intersection(other.eviction),
623 cancellation: self.cancellation.intersection(other.cancellation),
624 naming: self.naming.intersection(other.naming),
625 limits: self.limits.intersection(other.limits),
626 })
627 }
628
629 pub fn validate_core_requirements(self) -> Result<()> {
635 self.identity.validate_core_requirements()
636 }
637}