Skip to main content

omy_core/
shard.rs

1//! 分片格式:切分、合并、冗余头恢复(规范 §8)。
2//!
3//! # 冗余 header 的价值
4//!
5//! `shard_index > 0` 的分片可携带完整主 header 副本。这样即使第 0 片丢失,
6//! 任意一片仍能独立识别文件身份、读出加密文件名和缩略图,UI 不至于显示成
7//! 一堆无名碎片。
8//!
9//! 代价是每片增加 `header_len` 字节(典型 656 B 到 20 KB,取决于是否含缩略图)。
10
11use crate::error::{Error, Result};
12use crate::header::{MAGIC_SHARD, SHARD_HEADER_LEN};
13use crate::util::{Cursor, Writer};
14
15/// 分片头(56 字节)。
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ShardHeader {
18    /// 与主文件一致,用于归属判定。
19    pub file_uuid: [u8; 16],
20    /// 本片序号,从 0 开始。
21    pub shard_index: u32,
22    /// 总片数。
23    pub shard_total: u32,
24    /// 本片数据在原密文中的偏移。
25    pub data_offset: u64,
26    /// 本片数据长度。
27    pub data_len: u64,
28    /// 本片数据的 CRC-32(IEEE)。
29    pub crc32: u32,
30    /// 冗余主 header 长度,0 表示无。
31    pub redundant_header_len: u32,
32}
33
34impl ShardHeader {
35    /// 解析分片头。
36    ///
37    /// # Errors
38    ///
39    /// - [`Error::Truncated`]:输入不足 56 字节
40    /// - [`Error::BadMagic`]:magic 不是 `"OMYSHRD"` + 0x01
41    pub fn parse(data: &[u8]) -> Result<Self> {
42        if data.len() < SHARD_HEADER_LEN {
43            return Err(Error::Truncated {
44                context: "shard header",
45                need: SHARD_HEADER_LEN,
46                got: data.len(),
47            });
48        }
49        let mut c = Cursor::new(data, "shard header");
50        let magic: [u8; 8] = c.take_array()?;
51        if magic != MAGIC_SHARD {
52            return Err(Error::BadMagic { got: magic });
53        }
54        let h = Self {
55            file_uuid: c.take_array()?,
56            shard_index: c.u32le()?,
57            shard_total: c.u32le()?,
58            data_offset: c.u64le()?,
59            data_len: c.u64le()?,
60            crc32: c.u32le()?,
61            redundant_header_len: c.u32le()?,
62        };
63        debug_assert_eq!(c.position(), SHARD_HEADER_LEN, "分片头布局与规范不符");
64        Ok(h)
65    }
66
67    /// 序列化为 56 字节。
68    ///
69    /// # Errors
70    ///
71    /// 序列化结果长度异常时返回 [`Error::MalformedHeader`]。
72    pub fn to_bytes(&self) -> Result<[u8; SHARD_HEADER_LEN]> {
73        let mut w = Writer::with_capacity(SHARD_HEADER_LEN);
74        w.bytes(&MAGIC_SHARD)
75            .bytes(&self.file_uuid)
76            .u32le(self.shard_index)
77            .u32le(self.shard_total)
78            .u64le(self.data_offset)
79            .u64le(self.data_len)
80            .u32le(self.crc32)
81            .u32le(self.redundant_header_len);
82
83        let v = w.into_vec();
84        let mut out = [0u8; SHARD_HEADER_LEN];
85        let src = v.get(..SHARD_HEADER_LEN).ok_or(Error::MalformedHeader {
86            reason: "serialized shard header shorter than 56 bytes",
87        })?;
88        out.copy_from_slice(src);
89        Ok(out)
90    }
91
92    /// 本片数据段在分片文件内的起始偏移。
93    #[must_use]
94    pub const fn data_start(&self) -> usize {
95        SHARD_HEADER_LEN.saturating_add(self.redundant_header_len as usize)
96    }
97}
98
99/// 计算 CRC-32(IEEE)。
100#[must_use]
101pub fn crc32(data: &[u8]) -> u32 {
102    let mut h = crc32fast::Hasher::new();
103    h.update(data);
104    h.finalize()
105}
106
107/// 把完整密文切分为多个分片。
108///
109/// `redundant_header` 若提供,会附加到 `shard_index > 0` 的每一片中
110/// (第 0 片本身就含主 header,无需冗余)。
111///
112/// # Errors
113///
114/// [`Error::MalformedHeader`]:`shard_size` 为 0,或片数超过 `u32` 上限。
115pub fn split(
116    full_ciphertext: &[u8],
117    file_uuid: &[u8; 16],
118    shard_size: usize,
119    redundant_header: Option<&[u8]>,
120) -> Result<Vec<Vec<u8>>> {
121    if shard_size == 0 {
122        return Err(Error::MalformedHeader { reason: "shard_size must not be zero" });
123    }
124    let total = full_ciphertext.len().div_ceil(shard_size).max(1);
125    let total_u32 = u32::try_from(total)
126        .map_err(|_| Error::MalformedHeader { reason: "shard count exceeds u32" })?;
127
128    let mut shards = Vec::with_capacity(total);
129    for i in 0..total {
130        let start = i.saturating_mul(shard_size);
131        let end = start.saturating_add(shard_size).min(full_ciphertext.len());
132        let data = full_ciphertext.get(start..end).unwrap_or(&[]);
133
134        let i_u32 = u32::try_from(i)
135            .map_err(|_| Error::MalformedHeader { reason: "shard index exceeds u32" })?;
136        // 第 0 片已含主 header,不再附冗余副本
137        let redundant = if i_u32 == 0 { None } else { redundant_header };
138        let rlen = redundant.map_or(0, <[u8]>::len);
139
140        let sh = ShardHeader {
141            file_uuid: *file_uuid,
142            shard_index: i_u32,
143            shard_total: total_u32,
144            data_offset: start as u64,
145            data_len: data.len() as u64,
146            crc32: crc32(data),
147            redundant_header_len: u32::try_from(rlen).map_err(|_| Error::MalformedHeader {
148                reason: "redundant header exceeds u32",
149            })?,
150        };
151
152        let mut w = Writer::with_capacity(SHARD_HEADER_LEN.saturating_add(rlen).saturating_add(data.len()));
153        w.bytes(&sh.to_bytes()?);
154        if let Some(r) = redundant {
155            w.bytes(r);
156        }
157        w.bytes(data);
158        shards.push(w.into_vec());
159    }
160    Ok(shards)
161}
162
163/// 合并分片,还原完整密文。
164///
165/// 按规范 §8.3 的顺序校验:magic → uuid 一致性 → CRC → 排序 → 缺片检查 → 拼接。
166/// 输入可以是任意顺序。
167///
168/// # Errors
169///
170/// - [`Error::BadMagic`]:某片不是分片文件
171/// - [`Error::ShardUuidMismatch`]:某片属于其它文件
172/// - [`Error::ShardCrcMismatch`]:某片数据损坏
173/// - [`Error::MissingShards`]:缺少某些片,错误中列出具体序号
174pub fn merge(shards: &[Vec<u8>]) -> Result<Vec<u8>> {
175    if shards.is_empty() {
176        return Err(Error::MissingShards { missing: Vec::new(), total: 0 });
177    }
178
179    let mut parsed: Vec<(ShardHeader, &[u8])> = Vec::with_capacity(shards.len());
180    let mut expect_uuid: Option<[u8; 16]> = None;
181    let mut expect_total: Option<u32> = None;
182
183    for raw in shards {
184        let h = ShardHeader::parse(raw)?;
185
186        match expect_uuid {
187            None => expect_uuid = Some(h.file_uuid),
188            Some(u) if u != h.file_uuid => {
189                return Err(Error::ShardUuidMismatch { index: h.shard_index });
190            }
191            Some(_) => {}
192        }
193        if expect_total.is_none() {
194            expect_total = Some(h.shard_total);
195        }
196
197        let start = h.data_start();
198        let len = usize::try_from(h.data_len).map_err(|_| Error::MalformedHeader {
199            reason: "shard data_len exceeds addressable range",
200        })?;
201        let end = start.saturating_add(len);
202        let data = raw.get(start..end).ok_or(Error::Truncated {
203            context: "shard data",
204            need: end,
205            got: raw.len(),
206        })?;
207
208        let computed = crc32(data);
209        if computed != h.crc32 {
210            return Err(Error::ShardCrcMismatch {
211                index: h.shard_index,
212                expected: h.crc32,
213                computed,
214            });
215        }
216        parsed.push((h, data));
217    }
218
219    let total = expect_total.unwrap_or(0);
220    parsed.sort_by_key(|(h, _)| h.shard_index);
221
222    // 检查 0..total 无缺失
223    let present: Vec<u32> = parsed.iter().map(|(h, _)| h.shard_index).collect();
224    let missing: Vec<u32> = (0..total).filter(|i| !present.contains(i)).collect();
225    if !missing.is_empty() {
226        return Err(Error::MissingShards { missing, total });
227    }
228
229    let cap = parsed.iter().map(|(_, d)| d.len()).sum();
230    let mut out = Vec::with_capacity(cap);
231    for (_, d) in &parsed {
232        out.extend_from_slice(d);
233    }
234    Ok(out)
235}
236
237/// 从分片中恢复冗余主 header 副本。
238///
239/// 用于第 0 片丢失时仍能读出文件名与缩略图。
240///
241/// # Errors
242///
243/// - [`Error::BadMagic`]:不是分片文件
244/// - [`Error::MalformedHeader`]:该片不含冗余 header
245/// - [`Error::Truncated`]:数据不完整
246pub fn recover_header_from_shard(shard: &[u8]) -> Result<Vec<u8>> {
247    let h = ShardHeader::parse(shard)?;
248    if h.redundant_header_len == 0 {
249        return Err(Error::MalformedHeader {
250            reason: "this shard carries no redundant header",
251        });
252    }
253    let len = usize::try_from(h.redundant_header_len).map_err(|_| Error::MalformedHeader {
254        reason: "redundant_header_len exceeds addressable range",
255    })?;
256    let end = SHARD_HEADER_LEN.saturating_add(len);
257    let hdr = shard.get(SHARD_HEADER_LEN..end).ok_or(Error::Truncated {
258        context: "redundant header",
259        need: end,
260        got: shard.len(),
261    })?;
262    Ok(hdr.to_vec())
263}
264
265/// 分片集合的缺片情况,用于降级播放(规范 §8.4)。
266#[derive(Debug, Clone, PartialEq, Eq)]
267pub struct ShardCoverage {
268    /// 总片数。
269    pub total: u32,
270    /// 缺失的片序号。
271    pub missing: Vec<u32>,
272    /// 缺失区间在原密文中的 `(偏移, 长度)`,落在这些区间的读取必须失败。
273    pub holes: Vec<(u64, u64)>,
274}
275
276impl ShardCoverage {
277    /// 某个密文区间是否完整可读。
278    #[must_use]
279    pub fn is_readable(&self, offset: u64, length: u64) -> bool {
280        let end = offset.saturating_add(length);
281        !self.holes.iter().any(|&(ho, hl)| {
282            let hend = ho.saturating_add(hl);
283            offset < hend && ho < end
284        })
285    }
286}
287
288/// 分析分片集合的缺片情况。
289///
290/// # Errors
291///
292/// 解析分片头失败时返回错误。
293pub fn analyze_coverage(shards: &[Vec<u8>]) -> Result<ShardCoverage> {
294    let mut heads = Vec::with_capacity(shards.len());
295    for raw in shards {
296        heads.push(ShardHeader::parse(raw)?);
297    }
298    let total = heads.first().map_or(0, |h| h.shard_total);
299    let present: Vec<u32> = heads.iter().map(|h| h.shard_index).collect();
300    let missing: Vec<u32> = (0..total).filter(|i| !present.contains(i)).collect();
301
302    // 用已知片推断标准片大小:非末片的 data_len 即片大小
303    let shard_size = heads
304        .iter()
305        .filter(|h| h.shard_index.saturating_add(1) < h.shard_total)
306        .map(|h| h.data_len)
307        .max()
308        .or_else(|| heads.first().map(|h| h.data_len))
309        .unwrap_or(0);
310
311    let holes = missing
312        .iter()
313        .map(|&i| (u64::from(i).saturating_mul(shard_size), shard_size))
314        .collect();
315
316    Ok(ShardCoverage { total, missing, holes })
317}