aster_forge_cloud_files_windows/
identity.rs1use std::{fmt, mem::size_of};
4
5use aster_forge_cloud_files_core::{
6 CloudItemId, CloudItemKey, CloudNamespaceId, CloudRootId, CloudScope,
7};
8
9use crate::{Result, WindowsCloudFilesError};
10
11pub const CFAPI_FILE_IDENTITY_MAX_BYTES: usize = 4096;
13
14const MAGIC: &[u8; 4] = b"AFCF";
15const FORMAT_VERSION: u8 = 1;
16const HEADER_LEN: usize = MAGIC.len() + 1 + 3 * size_of::<u32>();
17
18#[derive(Clone, PartialEq, Eq, Hash)]
20pub struct WindowsFileIdentity(Vec<u8>);
21
22impl WindowsFileIdentity {
23 pub fn encode(key: &CloudItemKey) -> Result<Self> {
29 let fields = [
30 key.scope().namespace_id().as_str().as_bytes(),
31 key.scope().root_id().as_str().as_bytes(),
32 key.item_id().as_str().as_bytes(),
33 ];
34 let payload_len = fields
35 .iter()
36 .try_fold(0usize, |total, field| total.checked_add(field.len()))
37 .ok_or(WindowsCloudFilesError::FileIdentityTooLarge {
38 actual: usize::MAX,
39 maximum: CFAPI_FILE_IDENTITY_MAX_BYTES,
40 })?;
41 let total_len = HEADER_LEN.checked_add(payload_len).ok_or(
42 WindowsCloudFilesError::FileIdentityTooLarge {
43 actual: usize::MAX,
44 maximum: CFAPI_FILE_IDENTITY_MAX_BYTES,
45 },
46 )?;
47 validate_size(total_len)?;
48
49 let mut encoded = Vec::with_capacity(total_len);
50 encoded.extend_from_slice(MAGIC);
51 encoded.push(FORMAT_VERSION);
52 for field in fields {
53 let len = u32::try_from(field.len()).map_err(|_| {
54 WindowsCloudFilesError::FileIdentityTooLarge {
55 actual: field.len(),
56 maximum: CFAPI_FILE_IDENTITY_MAX_BYTES,
57 }
58 })?;
59 encoded.extend_from_slice(&len.to_le_bytes());
60 }
61 for field in fields {
62 encoded.extend_from_slice(field);
63 }
64 Ok(Self(encoded))
65 }
66
67 pub fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
73 validate_size(bytes.len())?;
74 decode_bytes(&bytes)?;
75 Ok(Self(bytes))
76 }
77
78 pub fn decode(&self) -> Result<CloudItemKey> {
84 decode_bytes(&self.0)
85 }
86
87 #[must_use]
89 pub fn as_bytes(&self) -> &[u8] {
90 &self.0
91 }
92
93 #[must_use]
95 pub fn len(&self) -> usize {
96 self.0.len()
97 }
98
99 #[must_use]
101 pub fn is_empty(&self) -> bool {
102 self.0.is_empty()
103 }
104
105 #[must_use]
107 pub fn into_bytes(self) -> Vec<u8> {
108 self.0
109 }
110}
111
112impl fmt::Debug for WindowsFileIdentity {
113 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
114 formatter
115 .debug_struct("WindowsFileIdentity")
116 .field("format_version", &FORMAT_VERSION)
117 .field("byte_len", &self.0.len())
118 .finish()
119 }
120}
121
122fn validate_size(actual: usize) -> Result<()> {
123 if actual > CFAPI_FILE_IDENTITY_MAX_BYTES {
124 return Err(WindowsCloudFilesError::FileIdentityTooLarge {
125 actual,
126 maximum: CFAPI_FILE_IDENTITY_MAX_BYTES,
127 });
128 }
129 Ok(())
130}
131
132fn decode_bytes(bytes: &[u8]) -> Result<CloudItemKey> {
133 if bytes.len() < HEADER_LEN {
134 return Err(invalid("identity envelope is truncated"));
135 }
136 if bytes.get(..MAGIC.len()) != Some(MAGIC.as_slice()) {
137 return Err(invalid("identity envelope magic does not match"));
138 }
139 if bytes[MAGIC.len()] != FORMAT_VERSION {
140 return Err(invalid("identity envelope version is unsupported"));
141 }
142
143 let lengths_start = MAGIC.len() + 1;
144 let mut lengths = [0usize; 3];
145 for (index, length) in lengths.iter_mut().enumerate() {
146 let start = lengths_start + index * size_of::<u32>();
147 let end = start + size_of::<u32>();
148 let raw: [u8; 4] = bytes[start..end]
149 .try_into()
150 .map_err(|_| invalid("identity length table is truncated"))?;
151 *length = usize::try_from(u32::from_le_bytes(raw))
152 .map_err(|_| invalid("identity field length cannot be represented"))?;
153 }
154
155 let expected_len = lengths.iter().try_fold(HEADER_LEN, |total, length| {
156 total
157 .checked_add(*length)
158 .ok_or_else(|| invalid("identity field lengths overflow"))
159 })?;
160 if expected_len != bytes.len() {
161 return Err(invalid("identity field lengths do not match the envelope"));
162 }
163
164 let mut offset = HEADER_LEN;
165 let mut fields = Vec::with_capacity(lengths.len());
166 for length in lengths {
167 let end = offset + length;
168 let value = std::str::from_utf8(&bytes[offset..end])
169 .map_err(|_| invalid("identity field is not UTF-8"))?;
170 fields.push(value.to_owned());
171 offset = end;
172 }
173
174 let mut fields = fields.into_iter();
175 let namespace = fields
176 .next()
177 .ok_or_else(|| invalid("identity namespace is missing"))?;
178 let root = fields
179 .next()
180 .ok_or_else(|| invalid("identity root is missing"))?;
181 let item = fields
182 .next()
183 .ok_or_else(|| invalid("identity item is missing"))?;
184 Ok(CloudItemKey::new(
185 CloudScope::new(CloudNamespaceId::new(namespace)?, CloudRootId::new(root)?),
186 CloudItemId::new(item)?,
187 ))
188}
189
190const fn invalid(reason: &'static str) -> WindowsCloudFilesError {
191 WindowsCloudFilesError::InvalidFileIdentity { reason }
192}