aster_forge_cloud_files_windows/
registration.rs

1//! Owned sync-root registration model and policy validation.
2
3use std::{fmt, mem::size_of, ops::BitOr};
4
5use aster_forge_cloud_files_core::{CloudNamespaceId, CloudRootId, CloudScope};
6
7use crate::{Result, WindowsCloudFilesError, WindowsFileIdentity};
8
9/// Maximum `SyncRootIdentity` size accepted by `CfRegisterSyncRoot`.
10pub const CFAPI_SYNC_ROOT_IDENTITY_MAX_BYTES: usize = 64 * 1024;
11
12const MAGIC: &[u8; 4] = b"AFSR";
13const FORMAT_VERSION: u8 = 1;
14const HEADER_LEN: usize = MAGIC.len() + 1 + 2 * size_of::<u32>();
15const MAX_PROVIDER_TEXT_UTF16_UNITS: usize = 255;
16const PLACEHOLDER_MANAGEMENT_MIN_INTEGRATION: u32 = 0x310;
17const FULL_RESTART_HYDRATION_MIN_INTEGRATION: u32 = 0x500;
18
19/// Stable provider telemetry identity shared by every version and sync root of one provider.
20#[derive(Clone, Copy, PartialEq, Eq, Hash)]
21pub struct WindowsProviderId(u128);
22
23impl WindowsProviderId {
24    /// Creates a stable non-zero GUID value from its canonical `u128` representation.
25    /// # Errors
26    ///
27    /// Returns an error when validation fails or an underlying backend, store, or platform
28    /// operation fails.
29    pub fn new(value: u128) -> Result<Self> {
30        if value == 0 {
31            return Err(WindowsCloudFilesError::EmptyProviderId);
32        }
33        Ok(Self(value))
34    }
35
36    /// Returns the canonical GUID value.
37    #[must_use]
38    pub const fn as_u128(self) -> u128 {
39        self.0
40    }
41}
42
43impl fmt::Debug for WindowsProviderId {
44    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
45        write!(formatter, "WindowsProviderId({:032x})", self.0)
46    }
47}
48
49/// Owned, versioned CFAPI sync-root identity for one exact Forge namespace/root scope.
50#[derive(Clone, PartialEq, Eq, Hash)]
51pub struct WindowsSyncRootIdentity(Vec<u8>);
52
53impl WindowsSyncRootIdentity {
54    /// Encodes one product-neutral scope into the current Windows sync-root envelope.
55    /// # Errors
56    ///
57    /// Returns an error when validation fails or an underlying backend, store, or platform
58    /// operation fails.
59    pub fn encode(scope: &CloudScope) -> Result<Self> {
60        let fields = [
61            scope.namespace_id().as_str().as_bytes(),
62            scope.root_id().as_str().as_bytes(),
63        ];
64        let payload_len = fields
65            .iter()
66            .try_fold(0usize, |total, field| total.checked_add(field.len()))
67            .ok_or(WindowsCloudFilesError::SyncRootIdentityTooLarge {
68                actual: usize::MAX,
69                maximum: CFAPI_SYNC_ROOT_IDENTITY_MAX_BYTES,
70            })?;
71        let total_len = HEADER_LEN.checked_add(payload_len).ok_or(
72            WindowsCloudFilesError::SyncRootIdentityTooLarge {
73                actual: usize::MAX,
74                maximum: CFAPI_SYNC_ROOT_IDENTITY_MAX_BYTES,
75            },
76        )?;
77        validate_sync_root_identity_size(total_len)?;
78
79        let mut encoded = Vec::with_capacity(total_len);
80        encoded.extend_from_slice(MAGIC);
81        encoded.push(FORMAT_VERSION);
82        for field in fields {
83            let length = u32::try_from(field.len()).map_err(|_| {
84                WindowsCloudFilesError::SyncRootIdentityTooLarge {
85                    actual: field.len(),
86                    maximum: CFAPI_SYNC_ROOT_IDENTITY_MAX_BYTES,
87                }
88            })?;
89            encoded.extend_from_slice(&length.to_le_bytes());
90        }
91        for field in fields {
92            encoded.extend_from_slice(field);
93        }
94        Ok(Self(encoded))
95    }
96
97    /// Imports and validates a canonical sync-root identity envelope.
98    /// # Errors
99    ///
100    /// Returns an error when validation fails or an underlying backend, store, or platform
101    /// operation fails.
102    pub fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
103        validate_sync_root_identity_size(bytes.len())?;
104        decode_sync_root_identity(&bytes)?;
105        Ok(Self(bytes))
106    }
107
108    /// Decodes the product-neutral namespace/root scope.
109    /// # Errors
110    ///
111    /// Returns an error when validation fails or an underlying backend, store, or platform
112    /// operation fails.
113    pub fn decode(&self) -> Result<CloudScope> {
114        decode_sync_root_identity(&self.0)
115    }
116
117    /// Returns the exact bytes persisted by CFAPI.
118    #[must_use]
119    pub fn as_bytes(&self) -> &[u8] {
120        &self.0
121    }
122
123    /// Returns the encoded byte length.
124    #[must_use]
125    pub fn len(&self) -> usize {
126        self.0.len()
127    }
128
129    /// Returns whether the envelope is empty.
130    #[must_use]
131    pub fn is_empty(&self) -> bool {
132        self.0.is_empty()
133    }
134
135    /// Consumes the identity and returns its exact bytes.
136    #[must_use]
137    pub fn into_bytes(self) -> Vec<u8> {
138        self.0
139    }
140}
141
142impl fmt::Debug for WindowsSyncRootIdentity {
143    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
144        formatter
145            .debug_struct("WindowsSyncRootIdentity")
146            .field("format_version", &FORMAT_VERSION)
147            .field("byte_len", &self.0.len())
148            .finish()
149    }
150}
151
152/// Hydration behavior requested from CFAPI.
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum WindowsHydrationPolicyPrimary {
155    /// Complete user I/O after the requested range is available and continue in the background.
156    Progressive,
157    /// Hydrate the complete file before completing user I/O.
158    Full,
159    /// Keep every placeholder fully hydrated.
160    AlwaysFull,
161}
162
163/// Primary hydration behavior plus independently negotiated CFAPI modifiers.
164#[expect(
165    clippy::struct_excessive_bools,
166    reason = "these fields map independent native hydration policy modifiers"
167)]
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub struct WindowsHydrationPolicy {
170    /// Primary hydration behavior.
171    pub primary: WindowsHydrationPolicyPrimary,
172    /// Persist and validate provider bytes before completing user I/O.
173    pub validation_required: bool,
174    /// Permit the platform to serve provider bytes without storing them locally.
175    pub streaming_allowed: bool,
176    /// Permit native auto-dehydration of in-sync placeholders.
177    pub auto_dehydration_allowed: bool,
178    /// Permit full restart hydration used when file size changes during fetch.
179    pub allow_full_restart_hydration: bool,
180}
181
182impl WindowsHydrationPolicy {
183    /// Validates conflicts and platform-version gates.
184    /// # Errors
185    ///
186    /// Returns an error when validation fails or an underlying backend, store, or platform
187    /// operation fails.
188    pub fn validate(self, platform: WindowsPlatformVersion) -> Result<()> {
189        if self.validation_required && self.streaming_allowed {
190            return Err(invalid_policy(
191                "validation-required and streaming-allowed are mutually exclusive",
192            ));
193        }
194        if self.allow_full_restart_hydration
195            && platform.integration < FULL_RESTART_HYDRATION_MIN_INTEGRATION
196        {
197            return Err(WindowsCloudFilesError::UnsupportedPlatformIntegration {
198                feature: "allow-full-restart-hydration",
199                required: FULL_RESTART_HYDRATION_MIN_INTEGRATION,
200                actual: platform.integration,
201            });
202        }
203        Ok(())
204    }
205}
206
207/// Supported namespace population behavior.
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub enum WindowsPopulationPolicy {
210    /// Populate every child when an unpopulated directory is accessed.
211    Full,
212    /// Assume the complete namespace is already present locally.
213    AlwaysFull,
214}
215
216/// Supported CFAPI in-sync tracking bits.
217#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
218pub struct WindowsInSyncPolicy(u32);
219
220impl WindowsInSyncPolicy {
221    /// Track no optional metadata changes.
222    pub const NONE: Self = Self(0);
223    /// Track file creation time.
224    pub const FILE_CREATION_TIME: Self = Self(0x0000_0001);
225    /// Track file read-only attribute.
226    pub const FILE_READ_ONLY: Self = Self(0x0000_0002);
227    /// Track file hidden attribute.
228    pub const FILE_HIDDEN: Self = Self(0x0000_0004);
229    /// Track file system attribute.
230    pub const FILE_SYSTEM: Self = Self(0x0000_0008);
231    /// Track directory creation time.
232    pub const DIRECTORY_CREATION_TIME: Self = Self(0x0000_0010);
233    /// Track directory read-only attribute.
234    pub const DIRECTORY_READ_ONLY: Self = Self(0x0000_0020);
235    /// Track directory hidden attribute.
236    pub const DIRECTORY_HIDDEN: Self = Self(0x0000_0040);
237    /// Track directory system attribute.
238    pub const DIRECTORY_SYSTEM: Self = Self(0x0000_0080);
239    /// Track file last-write time.
240    pub const FILE_LAST_WRITE_TIME: Self = Self(0x0000_0100);
241    /// Track directory last-write time.
242    pub const DIRECTORY_LAST_WRITE_TIME: Self = Self(0x0000_0200);
243    /// Preserve in-sync state for sync-engine initiated modifications.
244    pub const PRESERVE_FOR_SYNC_ENGINE: Self = Self(0x8000_0000);
245
246    /// Returns the exact CFAPI bitset.
247    #[must_use]
248    pub const fn bits(self) -> u32 {
249        self.0
250    }
251
252    /// Returns whether all requested bits are present.
253    #[must_use]
254    pub const fn contains(self, other: Self) -> bool {
255        self.0 & other.0 == other.0
256    }
257}
258
259impl BitOr for WindowsInSyncPolicy {
260    type Output = Self;
261
262    fn bitor(self, other: Self) -> Self::Output {
263        Self(self.0 | other.0)
264    }
265}
266
267/// Whether placeholder hard links are permitted.
268#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
269pub enum WindowsHardLinkPolicy {
270    /// Keep the CFAPI default and reject placeholder hard links.
271    #[default]
272    Forbidden,
273    /// Permit supported hard-link scenarios.
274    Allowed,
275}
276
277/// Permissions granted to non-provider processes while the sync root is active.
278#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
279pub struct WindowsPlaceholderManagementPolicy {
280    /// Permit unrestricted placeholder creation.
281    pub create_unrestricted: bool,
282    /// Permit unrestricted conversion to placeholders.
283    pub convert_unrestricted: bool,
284    /// Permit unrestricted placeholder updates.
285    pub update_unrestricted: bool,
286}
287
288impl WindowsPlaceholderManagementPolicy {
289    /// Returns whether any integration-gated permission is requested.
290    #[must_use]
291    pub const fn is_unrestricted(self) -> bool {
292        self.create_unrestricted || self.convert_unrestricted || self.update_unrestricted
293    }
294}
295
296/// Complete CFAPI sync-root policy set.
297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298pub struct WindowsSyncRootPolicies {
299    /// Hydration behavior and modifiers.
300    pub hydration: WindowsHydrationPolicy,
301    /// Namespace population behavior.
302    pub population: WindowsPopulationPolicy,
303    /// Metadata changes that clear in-sync state.
304    pub in_sync: WindowsInSyncPolicy,
305    /// Placeholder hard-link behavior.
306    pub hard_links: WindowsHardLinkPolicy,
307    /// Non-provider placeholder-management permissions.
308    pub placeholder_management: WindowsPlaceholderManagementPolicy,
309}
310
311impl WindowsSyncRootPolicies {
312    /// Validates policy conflicts and platform integration gates.
313    /// # Errors
314    ///
315    /// Returns an error when validation fails or an underlying backend, store, or platform
316    /// operation fails.
317    pub fn validate(self, platform: WindowsPlatformVersion) -> Result<()> {
318        self.hydration.validate(platform)?;
319        if self.placeholder_management.is_unrestricted()
320            && platform.integration < PLACEHOLDER_MANAGEMENT_MIN_INTEGRATION
321        {
322            return Err(WindowsCloudFilesError::UnsupportedPlatformIntegration {
323                feature: "unrestricted-placeholder-management",
324                required: PLACEHOLDER_MANAGEMENT_MIN_INTEGRATION,
325                actual: platform.integration,
326            });
327        }
328        Ok(())
329    }
330}
331
332/// Persistent registration flags independent from windows-rs values.
333#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
334pub struct WindowsSyncRootRegistrationOptions {
335    /// Update an existing registration instead of creating a new claim.
336    pub update_existing: bool,
337    /// Disable on-demand population for the root directory itself.
338    pub disable_on_demand_population_on_root: bool,
339    /// Mark the root directory in-sync during registration.
340    pub mark_in_sync_on_root: bool,
341}
342
343/// Detected CFAPI platform version used for gated policy validation.
344#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
345pub struct WindowsPlatformVersion {
346    /// Windows platform build number.
347    pub build: u32,
348    /// Windows platform revision number.
349    pub revision: u32,
350    /// Cloud Files integration number.
351    pub integration: u32,
352}
353
354/// Owned persistent sync-root registration request.
355#[derive(Debug, Clone, PartialEq, Eq)]
356pub struct WindowsSyncRootRegistration {
357    provider_name: String,
358    provider_version: String,
359    provider_id: WindowsProviderId,
360    sync_root_identity: WindowsSyncRootIdentity,
361    root_file_identity: WindowsFileIdentity,
362    policies: WindowsSyncRootPolicies,
363    options: WindowsSyncRootRegistrationOptions,
364}
365
366impl WindowsSyncRootRegistration {
367    /// Creates a persistent registration request and validates all portable identity boundaries.
368    /// # Errors
369    ///
370    /// Returns an error when validation fails or an underlying backend, store, or platform
371    /// operation fails.
372    pub fn new(
373        provider_name: impl Into<String>,
374        provider_version: impl Into<String>,
375        provider_id: WindowsProviderId,
376        sync_root_identity: WindowsSyncRootIdentity,
377        root_file_identity: WindowsFileIdentity,
378        policies: WindowsSyncRootPolicies,
379        options: WindowsSyncRootRegistrationOptions,
380    ) -> Result<Self> {
381        let provider_name = provider_name.into();
382        let provider_version = provider_version.into();
383        validate_registration_text("provider name", &provider_name)?;
384        validate_registration_text("provider version", &provider_version)?;
385        if root_file_identity.decode()?.scope() != &sync_root_identity.decode()? {
386            return Err(WindowsCloudFilesError::RootFileIdentityScopeMismatch);
387        }
388        Ok(Self {
389            provider_name,
390            provider_version,
391            provider_id,
392            sync_root_identity,
393            root_file_identity,
394            policies,
395            options,
396        })
397    }
398
399    /// Validates policies against one detected Windows platform version.
400    /// # Errors
401    ///
402    /// Returns an error when validation fails or an underlying backend, store, or platform
403    /// operation fails.
404    pub fn validate_for_platform(&self, platform: WindowsPlatformVersion) -> Result<()> {
405        self.policies.validate(platform)
406    }
407
408    /// Returns the user-facing provider name.
409    #[must_use]
410    pub fn provider_name(&self) -> &str {
411        &self.provider_name
412    }
413
414    /// Returns the user-facing provider version.
415    #[must_use]
416    pub fn provider_version(&self) -> &str {
417        &self.provider_version
418    }
419
420    /// Returns the stable provider GUID.
421    #[must_use]
422    pub const fn provider_id(&self) -> WindowsProviderId {
423        self.provider_id
424    }
425
426    /// Returns the persistent sync-root identity.
427    #[must_use]
428    pub const fn sync_root_identity(&self) -> &WindowsSyncRootIdentity {
429        &self.sync_root_identity
430    }
431
432    /// Returns the required file identity used when callbacks target the root itself.
433    #[must_use]
434    pub const fn root_file_identity(&self) -> &WindowsFileIdentity {
435        &self.root_file_identity
436    }
437
438    /// Returns the requested sync-root policies.
439    #[must_use]
440    pub const fn policies(&self) -> WindowsSyncRootPolicies {
441        self.policies
442    }
443
444    /// Returns persistent registration flags.
445    #[must_use]
446    pub const fn options(&self) -> WindowsSyncRootRegistrationOptions {
447        self.options
448    }
449
450    /// Consumes the registration and returns every owned boundary value.
451    #[must_use]
452    pub fn into_parts(
453        self,
454    ) -> (
455        String,
456        String,
457        WindowsProviderId,
458        WindowsSyncRootIdentity,
459        WindowsFileIdentity,
460        WindowsSyncRootPolicies,
461        WindowsSyncRootRegistrationOptions,
462    ) {
463        (
464            self.provider_name,
465            self.provider_version,
466            self.provider_id,
467            self.sync_root_identity,
468            self.root_file_identity,
469            self.policies,
470            self.options,
471        )
472    }
473}
474
475fn validate_sync_root_identity_size(actual: usize) -> Result<()> {
476    if actual > CFAPI_SYNC_ROOT_IDENTITY_MAX_BYTES {
477        return Err(WindowsCloudFilesError::SyncRootIdentityTooLarge {
478            actual,
479            maximum: CFAPI_SYNC_ROOT_IDENTITY_MAX_BYTES,
480        });
481    }
482    Ok(())
483}
484
485fn decode_sync_root_identity(bytes: &[u8]) -> Result<CloudScope> {
486    if bytes.len() < HEADER_LEN {
487        return Err(invalid_sync_root_identity("identity envelope is truncated"));
488    }
489    if bytes.get(..MAGIC.len()) != Some(MAGIC.as_slice()) {
490        return Err(invalid_sync_root_identity(
491            "identity envelope magic does not match",
492        ));
493    }
494    if bytes[MAGIC.len()] != FORMAT_VERSION {
495        return Err(invalid_sync_root_identity(
496            "identity envelope version is unsupported",
497        ));
498    }
499
500    let lengths_start = MAGIC.len() + 1;
501    let mut lengths = [0usize; 2];
502    for (index, length) in lengths.iter_mut().enumerate() {
503        let start = lengths_start + index * size_of::<u32>();
504        let end = start + size_of::<u32>();
505        let raw: [u8; 4] = bytes[start..end]
506            .try_into()
507            .map_err(|_| invalid_sync_root_identity("identity length table is truncated"))?;
508        *length = usize::try_from(u32::from_le_bytes(raw)).map_err(|_| {
509            invalid_sync_root_identity("identity field length cannot be represented")
510        })?;
511    }
512    let expected_len = lengths.iter().try_fold(HEADER_LEN, |total, length| {
513        total
514            .checked_add(*length)
515            .ok_or_else(|| invalid_sync_root_identity("identity field lengths overflow"))
516    })?;
517    if expected_len != bytes.len() {
518        return Err(invalid_sync_root_identity(
519            "identity field lengths do not match the envelope",
520        ));
521    }
522
523    let namespace_end = HEADER_LEN + lengths[0];
524    let namespace = std::str::from_utf8(&bytes[HEADER_LEN..namespace_end])
525        .map_err(|_| invalid_sync_root_identity("identity field is not UTF-8"))?;
526    let root = std::str::from_utf8(&bytes[namespace_end..])
527        .map_err(|_| invalid_sync_root_identity("identity field is not UTF-8"))?;
528    Ok(CloudScope::new(
529        CloudNamespaceId::new(namespace)?,
530        CloudRootId::new(root)?,
531    ))
532}
533
534fn validate_registration_text(field: &'static str, value: &str) -> Result<()> {
535    if value.is_empty() {
536        return Err(invalid_registration_text(field, "value is empty"));
537    }
538    if value.encode_utf16().any(|unit| unit == 0) {
539        return Err(invalid_registration_text(field, "value contains a NUL"));
540    }
541    if value.encode_utf16().count() > MAX_PROVIDER_TEXT_UTF16_UNITS {
542        return Err(invalid_registration_text(
543            field,
544            "value exceeds 255 UTF-16 code units",
545        ));
546    }
547    Ok(())
548}
549
550const fn invalid_sync_root_identity(reason: &'static str) -> WindowsCloudFilesError {
551    WindowsCloudFilesError::InvalidSyncRootIdentity { reason }
552}
553
554const fn invalid_registration_text(
555    field: &'static str,
556    reason: &'static str,
557) -> WindowsCloudFilesError {
558    WindowsCloudFilesError::InvalidRegistrationString { field, reason }
559}
560
561const fn invalid_policy(reason: &'static str) -> WindowsCloudFilesError {
562    WindowsCloudFilesError::InvalidSyncRootPolicy { reason }
563}