aster_forge_cloud_files_windows/
placeholder.rs1use aster_forge_cloud_files_core::{CloudItem, CloudItemKind};
4
5use crate::{Result, WindowsCloudFilesError, WindowsFileIdentity};
6
7pub const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10;
9pub const FILE_ATTRIBUTE_NORMAL: u32 = 0x80;
11
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
14pub struct WindowsFileTimes {
15 pub creation: i64,
17 pub last_access: i64,
19 pub last_write: i64,
21 pub change: i64,
23}
24
25#[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 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 #[must_use]
65 pub const fn file_size(&self) -> i64 {
66 self.file_size
67 }
68
69 #[must_use]
71 pub const fn file_attributes(&self) -> u32 {
72 self.file_attributes
73 }
74
75 #[must_use]
77 pub const fn times(&self) -> WindowsFileTimes {
78 self.times
79 }
80}
81
82#[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 pub mark_in_sync: bool,
91 pub disable_on_demand_population: bool,
93 pub always_full: bool,
95 pub supersede: bool,
97}
98
99#[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 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 #[must_use]
134 pub fn relative_name(&self) -> &str {
135 &self.relative_name
136 }
137
138 #[must_use]
140 pub const fn metadata(&self) -> WindowsPlaceholderMetadata {
141 self.metadata
142 }
143
144 #[must_use]
146 pub const fn identity(&self) -> &WindowsFileIdentity {
147 &self.identity
148 }
149
150 #[must_use]
152 pub const fn options(&self) -> WindowsPlaceholderOptions {
153 self.options
154 }
155
156 #[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}