1use crate::crypto::CipherId;
8use crate::error::{Error, Result};
9use crate::util::{Cursor, Writer};
10
11pub const MAGIC_FILE: [u8; 8] = *b"OMYFILE\x01";
13pub const MAGIC_SHARD: [u8; 8] = *b"OMYSHRD\x01";
15
16pub const VERSION_MAJOR: u16 = 1;
18pub const VERSION_MINOR: u16 = 0;
20
21pub const FIXED_HEADER_LEN: usize = 96;
23pub const SLOT_LEN: usize = 48;
25pub const SLOT_COUNT: usize = 8;
27pub const SLOT_COUNT_U8: u8 = 8;
29
30const _: () = assert!(SLOT_COUNT == SLOT_COUNT_U8 as usize, "SLOT_COUNT 与其 u8 形式不一致");
32pub const SLOT_AREA_LEN: usize = SLOT_LEN * SLOT_COUNT;
34pub const SLOT_AREA_OFFSET: usize = FIXED_HEADER_LEN;
36pub const TLV_AREA_OFFSET: usize = SLOT_AREA_OFFSET + SLOT_AREA_LEN;
38pub const HEADER_MAC_LEN: usize = 32;
40
41pub const SHARD_HEADER_LEN: usize = 56;
43
44pub const MIN_CHUNK_SIZE: u32 = 64 * 1024;
46pub const MAX_CHUNK_SIZE: u32 = 16 * 1024 * 1024;
48pub const DEFAULT_CHUNK_SIZE: u32 = 256 * 1024;
50pub const MOBILE_MAX_CHUNK_SIZE: u32 = 1024 * 1024;
52
53pub const FILENAME_BUCKET: usize = 64;
55
56pub const HEADER_LEN_FIELD_OFFSET: usize = 12;
64
65const _: () = assert!(
67 HEADER_LEN_FIELD_OFFSET == MAGIC_FILE.len() + 2 + 2,
68 "HEADER_LEN_FIELD_OFFSET 必须等于 magic + version_major + version_minor 的长度"
69);
70
71pub mod parse_limits {
92 pub const MAX_HEADER_LEN: u32 = 64 * 1024 * 1024;
94 pub const MAX_TLV_LEN: u32 = 32 * 1024 * 1024;
96 pub const MAX_TLV_ENTRIES: usize = 4096;
98 pub const MAX_INDEX_ENTRIES: u32 = 16 * 1024 * 1024;
100 pub const MAX_PARSE_CHUNK_SIZE: u32 = 64 * 1024 * 1024;
105 pub const MAX_FOLDER_ENTRIES: usize = 10_000_000;
107}
108
109pub const KDF_ARGON2ID: u8 = 1;
111pub const COMPRESS_NONE: u8 = 0;
113pub const COMPRESS_ZSTD: u8 = 1;
115
116pub mod flags {
118 pub const FILENAME_ENCRYPTED: u32 = 1 << 0;
120 pub const EXT_PRESERVED: u32 = 1 << 1;
122 pub const COMPRESSED: u32 = 1 << 2;
124 pub const SHARDED: u32 = 1 << 3;
126 pub const HAS_THUMBNAIL: u32 = 1 << 4;
128 pub const CONTAINER: u32 = 1 << 5;
130 pub const DISGUISED: u32 = 1 << 6;
132 pub const TRANSCODED: u32 = 1 << 7;
134 pub const META_NORMALIZED: u32 = 1 << 8;
136 pub const SIZE_PADDED: u32 = 1 << 9;
138 pub const SLOT_DIRECTORY: u32 = 1 << 10;
147
148 pub const KNOWN_MASK: u32 = FILENAME_ENCRYPTED
150 | EXT_PRESERVED
151 | COMPRESSED
152 | SHARDED
153 | HAS_THUMBNAIL
154 | CONTAINER
155 | DISGUISED
156 | TRANSCODED
157 | META_NORMALIZED
158 | SIZE_PADDED
159 | SLOT_DIRECTORY;
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct FixedHeader {
165 pub version_major: u16,
167 pub version_minor: u16,
169 pub header_len: u32,
171 pub file_uuid: [u8; 16],
173 pub vault_salt: [u8; 16],
175 pub flags: u32,
177 pub cipher_id: CipherId,
179 pub kdf_id: u8,
181 pub compress_id: u8,
183 pub slot_count: u8,
185 pub argon2_m_kib: u32,
187 pub argon2_t: u32,
189 pub argon2_p: u32,
191 pub chunk_size: u32,
193 pub plaintext_size: u64,
195 pub base_nonce: [u8; 7],
197 pub chunk_version: u8,
199 pub tlv_len: u32,
201}
202
203impl FixedHeader {
204 pub fn parse(data: &[u8]) -> Result<Self> {
216 if data.len() < FIXED_HEADER_LEN {
217 return Err(Error::Truncated {
218 context: "fixed header",
219 need: FIXED_HEADER_LEN,
220 got: data.len(),
221 });
222 }
223 let mut c = Cursor::new(data, "fixed header");
224
225 let magic: [u8; 8] = c.take_array()?;
226 if magic != MAGIC_FILE {
227 return Err(Error::BadMagic { got: magic });
228 }
229
230 let version_major = c.u16le()?;
231 let version_minor = c.u16le()?;
232 if version_major > VERSION_MAJOR {
233 return Err(Error::UnsupportedVersion {
234 found_major: version_major,
235 found_minor: version_minor,
236 supported_major: VERSION_MAJOR,
237 });
238 }
239
240 let header_len = c.u32le()?;
241 let file_uuid: [u8; 16] = c.take_array()?;
242 let vault_salt: [u8; 16] = c.take_array()?;
243 let flags = c.u32le()?;
244 let cipher_id = CipherId::from_u8(c.u8()?)?;
245 let kdf_id = c.u8()?;
246 let compress_id = c.u8()?;
247 let slot_count = c.u8()?;
248 let argon2_m_kib = c.u32le()?;
249 let argon2_t = c.u32le()?;
250 let argon2_p = c.u32le()?;
251 let chunk_size = c.u32le()?;
252 let plaintext_size = c.u64le()?;
253 let base_nonce: [u8; 7] = c.take_array()?;
254 let chunk_version = c.u8()?;
255 let tlv_len = c.u32le()?;
256 let _reserved = c.u32le()?; debug_assert_eq!(c.position(), FIXED_HEADER_LEN, "fixed header 布局与规范不符");
259
260 let h = Self {
261 version_major,
262 version_minor,
263 header_len,
264 file_uuid,
265 vault_salt,
266 flags,
267 cipher_id,
268 kdf_id,
269 compress_id,
270 slot_count,
271 argon2_m_kib,
272 argon2_t,
273 argon2_p,
274 chunk_size,
275 plaintext_size,
276 base_nonce,
277 chunk_version,
278 tlv_len,
279 };
280 h.validate()?;
281 Ok(h)
282 }
283
284 pub fn validate(&self) -> Result<()> {
293 if self.slot_count as usize != SLOT_COUNT {
294 return Err(Error::MalformedHeader { reason: "slot_count must be 8" });
295 }
296 if self.kdf_id != KDF_ARGON2ID {
297 return Err(Error::MalformedHeader { reason: "unknown kdf_id" });
298 }
299 if self.compress_id != COMPRESS_NONE && self.compress_id != COMPRESS_ZSTD {
300 return Err(Error::MalformedHeader { reason: "unknown compress_id" });
301 }
302 if self.chunk_version != 0 {
303 return Err(Error::MalformedHeader { reason: "chunk_version must be 0 in v1" });
304 }
305
306 if self.header_len > parse_limits::MAX_HEADER_LEN {
308 return Err(Error::MalformedHeader {
309 reason: "header_len exceeds the 64 MiB parse limit",
310 });
311 }
312 if self.tlv_len > parse_limits::MAX_TLV_LEN {
313 return Err(Error::MalformedHeader {
314 reason: "tlv_len exceeds the 32 MiB parse limit",
315 });
316 }
317 if self.chunk_size == 0 {
324 return Err(Error::MalformedHeader { reason: "chunk_size must not be zero" });
325 }
326 if self.chunk_size > parse_limits::MAX_PARSE_CHUNK_SIZE {
327 return Err(Error::MalformedHeader {
328 reason: "chunk_size exceeds the 64 MiB parse limit",
329 });
330 }
331
332 let expect_len = TLV_AREA_OFFSET
334 .checked_add(self.tlv_len as usize)
335 .and_then(|v| v.checked_add(HEADER_MAC_LEN))
336 .ok_or(Error::MalformedHeader { reason: "tlv_len overflows header_len" })?;
337 if (self.header_len as usize) != expect_len {
338 return Err(Error::MalformedHeader {
339 reason: "header_len does not match 96 + 384 + tlv_len + 32",
340 });
341 }
342
343 let n = self.n_chunks();
346 if n > u64::from(u32::MAX) {
347 return Err(Error::MalformedHeader {
348 reason: "chunk count exceeds u32; nonce space would be exhausted",
349 });
350 }
351
352 let compressed_flag = self.flags & flags::COMPRESSED != 0;
354 if compressed_flag != (self.compress_id == COMPRESS_ZSTD) {
355 return Err(Error::MalformedHeader {
356 reason: "COMPRESSED flag contradicts compress_id",
357 });
358 }
359 Ok(())
360 }
361
362 pub fn to_bytes(&self) -> Result<[u8; FIXED_HEADER_LEN]> {
368 self.validate()?;
369 let mut w = Writer::with_capacity(FIXED_HEADER_LEN);
370 w.bytes(&MAGIC_FILE)
371 .u16le(self.version_major)
372 .u16le(self.version_minor)
373 .u32le(self.header_len)
374 .bytes(&self.file_uuid)
375 .bytes(&self.vault_salt)
376 .u32le(self.flags)
377 .u8(self.cipher_id as u8)
378 .u8(self.kdf_id)
379 .u8(self.compress_id)
380 .u8(self.slot_count)
381 .u32le(self.argon2_m_kib)
382 .u32le(self.argon2_t)
383 .u32le(self.argon2_p)
384 .u32le(self.chunk_size)
385 .u64le(self.plaintext_size)
386 .bytes(&self.base_nonce)
387 .u8(self.chunk_version)
388 .u32le(self.tlv_len)
389 .u32le(0); let v = w.into_vec();
392 let mut out = [0u8; FIXED_HEADER_LEN];
393 let src = v.get(..FIXED_HEADER_LEN).ok_or(Error::MalformedHeader {
394 reason: "serialized fixed header shorter than 96 bytes",
395 })?;
396 out.copy_from_slice(src);
397 Ok(out)
398 }
399
400 #[must_use]
402 pub const fn n_chunks(&self) -> u64 {
403 let cs = self.chunk_size as u64;
404 if cs == 0 {
405 return 1;
406 }
407 let n = self.plaintext_size.div_ceil(cs);
408 if n == 0 { 1 } else { n }
409 }
410
411 #[must_use]
413 pub const fn has_flag(&self, flag: u32) -> bool {
414 self.flags & flag != 0
415 }
416
417 #[must_use]
422 pub const fn argon2_params(&self) -> crate::crypto::Argon2Params {
423 crate::crypto::Argon2Params {
424 m_kib: self.argon2_m_kib,
425 t: self.argon2_t,
426 p: self.argon2_p,
427 }
428 }
429
430 pub fn chunk_ct_range(&self, index: u64) -> Result<(u64, u64)> {
444 if self.has_flag(flags::COMPRESSED) {
445 return Err(Error::MalformedHeader {
446 reason: "chunk_ct_range is invalid for compressed payloads; use the index table",
447 });
448 }
449 let total = self.n_chunks();
450 if index >= total {
451 return Err(Error::ChunkOutOfRange { index, total });
452 }
453 let cs = u64::from(self.chunk_size);
454 let tag = crate::crypto::TAG_LEN as u64;
455
456 let stride = cs
457 .checked_add(tag)
458 .ok_or(Error::MalformedHeader { reason: "chunk stride overflow" })?;
459 let ct_offset = u64::from(self.header_len)
460 .checked_add(index.checked_mul(stride).ok_or(Error::MalformedHeader {
461 reason: "chunk offset overflow",
462 })?)
463 .ok_or(Error::MalformedHeader { reason: "chunk offset overflow" })?;
464
465 let consumed = index
466 .checked_mul(cs)
467 .ok_or(Error::MalformedHeader { reason: "chunk offset overflow" })?;
468 let plain_len = self.plaintext_size.saturating_sub(consumed).min(cs);
469 let ct_len = plain_len
470 .checked_add(tag)
471 .ok_or(Error::MalformedHeader { reason: "chunk length overflow" })?;
472
473 Ok((ct_offset, ct_len))
474 }
475
476 #[must_use]
481 pub const fn chunk_index_of(&self, plain_offset: u64) -> u64 {
482 match plain_offset.checked_div(self.chunk_size as u64) {
483 Some(v) => v,
484 None => 0,
485 }
486 }
487}
488
489pub const fn validate_chunk_size(chunk_size: u32) -> Result<()> {
504 if chunk_size < MIN_CHUNK_SIZE || chunk_size > MAX_CHUNK_SIZE {
505 return Err(Error::InvalidChunkSize {
506 got: chunk_size,
507 min: MIN_CHUNK_SIZE,
508 max: MAX_CHUNK_SIZE,
509 });
510 }
511 Ok(())
512}