aster_forge_cloud_files_windows/
placeholder.rs

1//! Owned placeholder creation inputs independent from temporary native pointers.
2
3use aster_forge_cloud_files_core::{CloudItem, CloudItemKind};
4
5use crate::{Result, WindowsCloudFilesError, WindowsFileIdentity};
6
7/// Windows directory file-attribute bit.
8pub const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10;
9/// Windows normal-file attribute bit.
10pub const FILE_ATTRIBUTE_NORMAL: u32 = 0x80;
11
12/// Windows file times expressed as signed 100-nanosecond intervals since the Windows epoch.
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
14pub struct WindowsFileTimes {
15    /// Creation time.
16    pub creation: i64,
17    /// Last access time.
18    pub last_access: i64,
19    /// Last write time.
20    pub last_write: i64,
21    /// Metadata change time.
22    pub change: i64,
23}
24
25/// Owned CFAPI filesystem metadata for one placeholder.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct WindowsPlaceholderMetadata {
28    file_size: i64,
29    file_attributes: u32,
30    times: WindowsFileTimes,
31}
32
33impl WindowsPlaceholderMetadata {
34    /// Maps one core item into minimal Windows filesystem metadata.
35    /// # Errors
36    ///
37    /// Returns an error when validation fails or an underlying backend, store, or platform
38    /// operation fails.
39    pub fn from_item(item: &CloudItem, times: WindowsFileTimes) -> Result<Self> {
40        let (file_size, file_attributes) = match item.kind() {
41            CloudItemKind::File => {
42                let content =
43                    item.content()
44                        .ok_or(WindowsCloudFilesError::InvalidPlaceholderMetadata {
45                            reason: "file item is missing content metadata",
46                        })?;
47                let file_size = i64::try_from(content.size()).map_err(|_| {
48                    WindowsCloudFilesError::FileSizeTooLarge {
49                        size: content.size(),
50                    }
51                })?;
52                (file_size, FILE_ATTRIBUTE_NORMAL)
53            }
54            CloudItemKind::Directory => (0, FILE_ATTRIBUTE_DIRECTORY),
55        };
56        Ok(Self {
57            file_size,
58            file_attributes,
59            times,
60        })
61    }
62
63    /// Returns the signed CFAPI file size.
64    #[must_use]
65    pub const fn file_size(&self) -> i64 {
66        self.file_size
67    }
68
69    /// Returns Windows file-attribute bits.
70    #[must_use]
71    pub const fn file_attributes(&self) -> u32 {
72        self.file_attributes
73    }
74
75    /// Returns all Windows file times.
76    #[must_use]
77    pub const fn times(&self) -> WindowsFileTimes {
78        self.times
79    }
80}
81
82/// CFAPI placeholder creation options kept independent from windows-rs types.
83#[expect(
84    clippy::struct_excessive_bools,
85    reason = "these fields map independent CFAPI placeholder creation flags"
86)]
87#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
88pub struct WindowsPlaceholderOptions {
89    /// Mark the placeholder in-sync during creation.
90    pub mark_in_sync: bool,
91    /// Disable on-demand directory population for this placeholder.
92    pub disable_on_demand_population: bool,
93    /// Require the placeholder to remain fully hydrated.
94    pub always_full: bool,
95    /// Replace an existing placeholder with the same relative name.
96    pub supersede: bool,
97}
98
99/// Owned input for one `CfCreatePlaceholders` entry.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct WindowsPlaceholder {
102    relative_name: String,
103    metadata: WindowsPlaceholderMetadata,
104    identity: WindowsFileIdentity,
105    options: WindowsPlaceholderOptions,
106}
107
108impl WindowsPlaceholder {
109    /// Creates an owned placeholder request and verifies identity/item binding.
110    /// # Errors
111    ///
112    /// Returns an error when validation fails or an underlying backend, store, or platform
113    /// operation fails.
114    pub fn from_item(
115        item: &CloudItem,
116        identity: WindowsFileIdentity,
117        times: WindowsFileTimes,
118        options: WindowsPlaceholderOptions,
119    ) -> Result<Self> {
120        validate_relative_name(item.name())?;
121        if identity.decode()? != *item.key() {
122            return Err(WindowsCloudFilesError::IdentityItemMismatch);
123        }
124        Ok(Self {
125            relative_name: item.name().to_owned(),
126            metadata: WindowsPlaceholderMetadata::from_item(item, times)?,
127            identity,
128            options,
129        })
130    }
131
132    /// Returns the validated single-component Windows relative name.
133    #[must_use]
134    pub fn relative_name(&self) -> &str {
135        &self.relative_name
136    }
137
138    /// Returns the owned filesystem metadata.
139    #[must_use]
140    pub const fn metadata(&self) -> WindowsPlaceholderMetadata {
141        self.metadata
142    }
143
144    /// Returns the stable native identity bytes.
145    #[must_use]
146    pub const fn identity(&self) -> &WindowsFileIdentity {
147        &self.identity
148    }
149
150    /// Returns creation options.
151    #[must_use]
152    pub const fn options(&self) -> WindowsPlaceholderOptions {
153        self.options
154    }
155
156    /// Consumes the request and returns all owned parts.
157    #[must_use]
158    pub fn into_parts(
159        self,
160    ) -> (
161        String,
162        WindowsPlaceholderMetadata,
163        WindowsFileIdentity,
164        WindowsPlaceholderOptions,
165    ) {
166        (
167            self.relative_name,
168            self.metadata,
169            self.identity,
170            self.options,
171        )
172    }
173}
174
175fn validate_relative_name(name: &str) -> Result<()> {
176    if name.is_empty() {
177        return Err(invalid_name("name is empty"));
178    }
179    if name == "." || name == ".." {
180        return Err(invalid_name("dot path components are reserved"));
181    }
182    if name.ends_with([' ', '.']) {
183        return Err(invalid_name("name ends with a space or period"));
184    }
185    if name.chars().any(|character| {
186        character <= '\u{1f}'
187            || matches!(
188                character,
189                '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*'
190            )
191    }) {
192        return Err(invalid_name("name contains a Windows-reserved character"));
193    }
194    let stem = name.split('.').next().unwrap_or(name);
195    if is_reserved_device_name(stem) {
196        return Err(invalid_name("name uses a reserved Windows device name"));
197    }
198    Ok(())
199}
200
201fn is_reserved_device_name(stem: &str) -> bool {
202    let upper = stem.to_ascii_uppercase();
203    matches!(upper.as_str(), "CON" | "PRN" | "AUX" | "NUL")
204        || upper
205            .strip_prefix("COM")
206            .or_else(|| upper.strip_prefix("LPT"))
207            .is_some_and(|suffix| {
208                matches!(
209                    suffix,
210                    "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "¹" | "²" | "³"
211                )
212            })
213}
214
215const fn invalid_name(reason: &'static str) -> WindowsCloudFilesError {
216    WindowsCloudFilesError::InvalidPlaceholderName { reason }
217}