shadow_crypt_core/v3/
metadata.rs1use std::time::{Duration, SystemTime, UNIX_EPOCH};
23
24use crate::{
25 errors::{FileError, HeaderError},
26 file::{ContentKind, FileMetadata},
27 memory::{SecureBytes, SecureString},
28};
29
30const FLAG_MTIME: u8 = 0b0000_0001;
31const FLAG_MODE: u8 = 0b0000_0010;
32const FLAG_ARCHIVE: u8 = 0b0000_0100;
33
34const MAX_ENVELOPE_LEN: usize = u16::MAX as usize - 16;
37
38pub fn serialize(metadata: &FileMetadata) -> Result<SecureBytes, HeaderError> {
42 let filename = metadata.filename().as_str().as_bytes();
43 if u16::try_from(filename.len()).is_err() {
44 return Err(HeaderError::MetadataTooLong);
45 }
46
47 let mtime = metadata.mtime().map(systemtime_to_parts);
48
49 let mut flags = 0u8;
50 if mtime.is_some() {
51 flags |= FLAG_MTIME;
52 }
53 if metadata.mode().is_some() {
54 flags |= FLAG_MODE;
55 }
56 if metadata.kind() == ContentKind::Archive {
57 flags |= FLAG_ARCHIVE;
58 }
59
60 let mut envelope = Vec::with_capacity(1 + 2 + filename.len() + 12 + 4);
61 envelope.push(flags);
62 envelope.extend_from_slice(&(filename.len() as u16).to_le_bytes());
63 envelope.extend_from_slice(filename);
64 if let Some((secs, nanos)) = mtime {
65 envelope.extend_from_slice(&secs.to_le_bytes());
66 envelope.extend_from_slice(&nanos.to_le_bytes());
67 }
68 if let Some(mode) = metadata.mode() {
69 envelope.extend_from_slice(&mode.to_le_bytes());
70 }
71
72 if envelope.len() > MAX_ENVELOPE_LEN {
73 return Err(HeaderError::MetadataTooLong);
74 }
75 Ok(SecureBytes::new(envelope))
76}
77
78pub fn parse(envelope: &[u8]) -> Result<FileMetadata, FileError> {
82 let err = || FileError::InvalidMetadata;
83
84 let (&flags, rest) = envelope.split_first().ok_or_else(err)?;
85 if flags & !(FLAG_MTIME | FLAG_MODE | FLAG_ARCHIVE) != 0 {
86 return Err(err());
87 }
88
89 let (len_bytes, rest) = rest.split_at_checked(2).ok_or_else(err)?;
90 let filename_len = u16::from_le_bytes(len_bytes.try_into().map_err(|_| err())?) as usize;
91 let (filename_bytes, rest) = rest.split_at_checked(filename_len).ok_or_else(err)?;
92 let filename = std::str::from_utf8(filename_bytes).map_err(|_| err())?;
93
94 let (mtime, rest) = if flags & FLAG_MTIME != 0 {
95 let (secs_bytes, rest) = rest.split_at_checked(8).ok_or_else(err)?;
96 let (nanos_bytes, rest) = rest.split_at_checked(4).ok_or_else(err)?;
97 let secs = i64::from_le_bytes(secs_bytes.try_into().map_err(|_| err())?);
98 let nanos = u32::from_le_bytes(nanos_bytes.try_into().map_err(|_| err())?);
99 if nanos >= 1_000_000_000 {
100 return Err(err());
101 }
102 (
103 Some(parts_to_systemtime(secs, nanos).ok_or_else(err)?),
104 rest,
105 )
106 } else {
107 (None, rest)
108 };
109
110 let (mode, rest) = if flags & FLAG_MODE != 0 {
111 let (mode_bytes, rest) = rest.split_at_checked(4).ok_or_else(err)?;
112 let mode = u32::from_le_bytes(mode_bytes.try_into().map_err(|_| err())?);
113 (Some(mode), rest)
114 } else {
115 (None, rest)
116 };
117
118 if !rest.is_empty() {
119 return Err(err());
120 }
121
122 let metadata = FileMetadata::new(SecureString::new(filename.to_string()), mtime, mode);
123 Ok(if flags & FLAG_ARCHIVE != 0 {
124 metadata.into_archive()
125 } else {
126 metadata
127 })
128}
129
130fn systemtime_to_parts(t: SystemTime) -> (i64, u32) {
136 let (secs, nanos): (i128, u32) = match t.duration_since(UNIX_EPOCH) {
137 Ok(d) => (d.as_secs().into(), d.subsec_nanos()),
138 Err(e) => {
139 let d = e.duration();
140 let (secs, nanos) = (i128::from(d.as_secs()), d.subsec_nanos());
141 if nanos == 0 {
142 (-secs, 0)
143 } else {
144 (-(secs + 1), 1_000_000_000 - nanos)
145 }
146 }
147 };
148 (secs.clamp(i64::MIN.into(), i64::MAX.into()) as i64, nanos)
149}
150
151fn parts_to_systemtime(secs: i64, nanos: u32) -> Option<SystemTime> {
152 if secs >= 0 {
153 UNIX_EPOCH.checked_add(Duration::new(secs as u64, nanos))
154 } else if nanos == 0 {
155 UNIX_EPOCH.checked_sub(Duration::from_secs(secs.unsigned_abs()))
156 } else {
157 UNIX_EPOCH.checked_sub(Duration::new(
158 (secs + 1).unsigned_abs(),
159 1_000_000_000 - nanos,
160 ))
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 fn meta(filename: &str, mtime: Option<SystemTime>, mode: Option<u32>) -> FileMetadata {
169 FileMetadata::new(SecureString::new(filename.to_string()), mtime, mode)
170 }
171
172 fn round_trip(metadata: &FileMetadata) -> FileMetadata {
173 parse(serialize(metadata).unwrap().as_slice()).unwrap()
174 }
175
176 #[test]
177 fn round_trip_all_fields() {
178 let mtime = UNIX_EPOCH + Duration::new(1_700_000_000, 123_456_789);
179 let parsed = round_trip(&meta("café.txt", Some(mtime), Some(0o644)));
180
181 assert_eq!(parsed.filename().as_str(), "café.txt");
182 assert_eq!(parsed.mtime(), Some(mtime));
183 assert_eq!(parsed.mode(), Some(0o644));
184 }
185
186 #[test]
187 fn round_trip_filename_only() {
188 let parsed = round_trip(&meta("a.txt", None, None));
189 assert_eq!(parsed.filename().as_str(), "a.txt");
190 assert_eq!(parsed.mtime(), None);
191 assert_eq!(parsed.mode(), None);
192 assert_eq!(parsed.kind(), crate::file::ContentKind::File);
193 }
194
195 #[test]
196 fn round_trip_archive_kind() {
197 let archive_meta = meta("photos", None, Some(0o755)).into_archive();
198 let parsed = round_trip(&archive_meta);
199 assert_eq!(parsed.filename().as_str(), "photos");
200 assert_eq!(parsed.kind(), crate::file::ContentKind::Archive);
201 }
202
203 #[test]
206 fn round_trip_extreme_mtimes() {
207 for secs in [i64::MIN, i64::MIN + 1, i64::MAX] {
208 let mut envelope = vec![1u8]; envelope.extend_from_slice(&1u16.to_le_bytes());
210 envelope.push(b'f');
211 envelope.extend_from_slice(&secs.to_le_bytes());
212 envelope.extend_from_slice(&0u32.to_le_bytes());
213
214 if let Ok(parsed) = parse(&envelope) {
215 let reparsed = parse(serialize(&parsed).unwrap().as_slice()).unwrap();
216 assert_eq!(reparsed.mtime(), parsed.mtime(), "secs {secs}");
217 }
218 }
219 }
220
221 #[test]
222 fn round_trip_pre_epoch_mtime() {
223 let mtime = UNIX_EPOCH - Duration::new(100, 250_000_000);
224 let parsed = round_trip(&meta("old.txt", Some(mtime), None));
225 assert_eq!(parsed.mtime(), Some(mtime));
226 }
227
228 #[test]
229 fn unknown_flags_rejected() {
230 let mut envelope = serialize(&meta("a", None, None))
231 .unwrap()
232 .as_slice()
233 .to_vec();
234 envelope[0] |= 0b1000_0000;
235 assert!(parse(&envelope).is_err());
236 }
237
238 #[test]
239 fn trailing_bytes_rejected() {
240 let mut envelope = serialize(&meta("a", None, None))
241 .unwrap()
242 .as_slice()
243 .to_vec();
244 envelope.push(0);
245 assert!(parse(&envelope).is_err());
246 }
247
248 #[test]
249 fn truncated_envelope_rejected() {
250 let envelope = serialize(&meta("abcdef", None, Some(0o600)))
251 .unwrap()
252 .as_slice()
253 .to_vec();
254 for len in 0..envelope.len() {
255 assert!(parse(&envelope[..len]).is_err(), "accepted prefix {len}");
256 }
257 }
258
259 #[test]
260 fn invalid_utf8_filename_rejected() {
261 let envelope = [0u8, 2, 0, 0xff, 0xfe];
263 assert!(parse(&envelope).is_err());
264 }
265
266 #[test]
267 fn oversized_filename_rejected() {
268 let long = "x".repeat(u16::MAX as usize + 1);
269 assert!(matches!(
270 serialize(&meta(&long, None, None)),
271 Err(HeaderError::MetadataTooLong)
272 ));
273 }
274}