1use crate::chunk::{self, Hash};
11use crate::error::{Error, Result};
12use crate::format;
13use reed_solomon_erasure::galois_8::ReedSolomon;
14use serde::{Deserialize, Serialize};
15use serde_bytes::ByteBuf;
16use std::fs::{File, OpenOptions};
17use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
18use std::path::{Path, PathBuf};
19
20pub const MAX_DATA_PER_STRIPE: usize = 200;
21pub const MAX_STRIPE_BYTES: usize = 64 << 20;
24pub const MIN_PIECE_SIZE: u32 = 1024;
25pub const MAX_PIECE_SIZE: u32 = 16 << 20;
26
27#[derive(Clone, Debug, Serialize, Deserialize)]
28pub struct StripeMeta {
29 pub first: u32,
30 pub k: u32,
31 pub m: u32,
32 pub par_off: u64,
34}
35
36#[derive(Clone, Debug, Serialize, Deserialize)]
37pub struct PiecesMeta {
38 pub file: String,
39 pub size: u64,
40 pub piece_size: u32,
41 pub pieces: Vec<ByteBuf>,
42 pub parity: Vec<ByteBuf>,
43 pub stripes: Vec<StripeMeta>,
44 pub root: ByteBuf,
45}
46
47pub fn sidecar_paths(archive: &Path) -> (PathBuf, PathBuf) {
48 let s = archive.to_string_lossy();
49 (PathBuf::from(format!("{s}.pieces")), PathBuf::from(format!("{s}.par")))
50}
51
52fn stripe_k(piece_size: usize) -> usize {
53 (MAX_STRIPE_BYTES / piece_size).clamp(2, MAX_DATA_PER_STRIPE)
54}
55
56fn check_piece_size(piece_size: u32) -> Result<usize> {
57 if !(MIN_PIECE_SIZE..=MAX_PIECE_SIZE).contains(&piece_size) {
58 return Err(Error::Invalid(format!("piece size must be between {MIN_PIECE_SIZE} and {MAX_PIECE_SIZE} bytes")));
59 }
60 Ok(piece_size as usize)
61}
62
63fn read_full<R: Read>(r: &mut R, buf: &mut [u8]) -> Result<usize> {
65 let mut got = 0;
66 while got < buf.len() {
67 let n = r.read(&mut buf[got..])?;
68 if n == 0 {
69 break;
70 }
71 got += n;
72 }
73 Ok(got)
74}
75
76fn read_at(f: &mut File, off: u64, buf: &mut [u8]) -> Result<usize> {
78 f.seek(SeekFrom::Start(off))?;
79 let got = read_full(f, buf)?;
80 buf[got..].fill(0);
81 Ok(got)
82}
83
84fn rs_new(k: usize, m: usize) -> Result<ReedSolomon> {
85 ReedSolomon::new(k, m).map_err(|e| Error::Invalid(format!("reed-solomon: {e:?}")))
86}
87
88pub fn write_sidecars(archive: &Path, piece_size: u32, parity_ratio: f64) -> Result<PiecesMeta> {
89 let ps = check_piece_size(piece_size)?;
90 if !(0.0..=1.0).contains(&parity_ratio) {
91 return Err(Error::Invalid("parity ratio must be between 0 and 1".into()));
92 }
93 let mut input = BufReader::with_capacity(ps.min(1 << 20), File::open(archive)?);
94 let size = input.get_ref().metadata()?.len();
95 let n_pieces = usize::try_from(size.div_ceil(ps as u64)).map_err(|_| Error::Limit("too many pieces".into()))?;
96 if n_pieces > u32::MAX as usize {
97 return Err(Error::Limit("too many pieces".into()));
98 }
99 let k_max = stripe_k(ps);
100 let (pieces_path, par_path) = sidecar_paths(archive);
101 let mut par = BufWriter::new(File::create(&par_path)?);
102 let mut hashes: Vec<Hash> = Vec::with_capacity(n_pieces);
103 let mut parity_hashes: Vec<Hash> = Vec::new();
104 let mut stripes = Vec::new();
105 let mut par_off = 0u64;
106 let mut first = 0usize;
107 while first < n_pieces {
108 let k = (n_pieces - first).min(k_max);
109 let m = ((k as f64 * parity_ratio).round() as usize).max(1);
110 let mut shards: Vec<Vec<u8>> = Vec::with_capacity(k + m);
111 for _ in 0..k {
112 let mut buf = vec![0u8; ps];
113 read_full(&mut input, &mut buf)?;
114 hashes.push(chunk::hash(&buf));
115 shards.push(buf);
116 }
117 shards.extend((0..m).map(|_| vec![0u8; ps]));
118 rs_new(k, m)?.encode(&mut shards).map_err(|e| Error::Invalid(format!("reed-solomon encode: {e:?}")))?;
119 for p in &shards[k..] {
120 parity_hashes.push(chunk::hash(p));
121 par.write_all(p)?;
122 }
123 stripes.push(StripeMeta { first: first as u32, k: k as u32, m: m as u32, par_off });
124 par_off += (m * ps) as u64;
125 first += k;
126 }
127 par.flush()?;
128 let meta = PiecesMeta {
129 file: archive.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default(),
130 size,
131 piece_size,
132 pieces: hashes.iter().map(|h| ByteBuf::from(h.to_vec())).collect(),
133 parity: parity_hashes.iter().map(|h| ByteBuf::from(h.to_vec())).collect(),
134 stripes,
135 root: ByteBuf::from(chunk::merkle_root(&hashes).to_vec()),
136 };
137 std::fs::write(pieces_path, format::cbor_encode(&meta)?)?;
138 Ok(meta)
139}
140
141pub fn load_meta(archive: &Path) -> Result<PiecesMeta> {
142 let (pieces_path, _) = sidecar_paths(archive);
143 let bytes = std::fs::read(&pieces_path).map_err(|_| Error::Missing(format!("{} not found", pieces_path.display())))?;
144 let meta: PiecesMeta = format::cbor_decode(&bytes)?;
145 check_piece_size(meta.piece_size).map_err(|_| Error::Corrupt("pieces sidecar: piece size out of range".into()))?;
146 let n = meta.pieces.len();
147 let expected = meta.size.div_ceil(meta.piece_size as u64);
148 if n as u64 != expected {
149 return Err(Error::Corrupt("pieces sidecar: piece count does not match the size".into()));
150 }
151 let mut par_count = 0usize;
152 let mut next = 0u64;
153 for st in &meta.stripes {
154 if st.first as u64 != next || st.k == 0 || st.first as u64 + st.k as u64 > n as u64 || st.k as usize + st.m as usize > 256 {
155 return Err(Error::Corrupt("pieces sidecar: inconsistent stripe table".into()));
156 }
157 next += st.k as u64;
158 par_count += st.m as usize;
159 }
160 if next != n as u64 || par_count != meta.parity.len() || meta.pieces.iter().chain(meta.parity.iter()).any(|h| h.len() != 32) {
161 return Err(Error::Corrupt("pieces sidecar: inconsistent stripe table".into()));
162 }
163 Ok(meta)
164}
165
166pub fn verify(archive: &Path) -> Result<(Vec<u32>, PiecesMeta)> {
170 let meta = load_meta(archive)?;
171 let ps = meta.piece_size as usize;
172 let mut bad = Vec::new();
173 match File::open(archive) {
174 Ok(f) => {
175 let actual = f.metadata()?.len();
176 let mut r = BufReader::with_capacity(ps.min(1 << 20), f);
177 let mut buf = vec![0u8; ps];
178 for (i, h) in meta.pieces.iter().enumerate() {
179 let got = read_full(&mut r, &mut buf)?;
180 buf[got..].fill(0);
181 if chunk::hash(&buf)[..] != h[..] {
182 bad.push(i as u32);
183 }
184 }
185 if actual != meta.size && !meta.pieces.is_empty() {
186 let last = meta.pieces.len() as u32 - 1;
187 if !bad.contains(&last) {
188 bad.push(last);
189 }
190 }
191 }
192 Err(_) => bad = (0..meta.pieces.len() as u32).collect(),
193 }
194 Ok((bad, meta))
195}
196
197pub fn recover(archive: &Path) -> Result<(u32, bool)> {
201 let (bad, meta) = verify(archive)?;
202 if bad.is_empty() {
203 return Ok((0, true));
204 }
205 let ps = meta.piece_size as usize;
206 let (_, par_path) = sidecar_paths(archive);
207 let mut par = File::open(&par_path).map_err(|_| Error::Missing(format!("{} not found", par_path.display())))?;
208 let mut file = OpenOptions::new().read(true).write(true).create(true).truncate(false).open(archive)?;
209 if file.metadata()?.len() != meta.size {
210 file.set_len(meta.size)?;
211 }
212 let mut repaired = 0u32;
213 let mut par_index = 0usize;
214 let mut all_ok = true;
215 for st in &meta.stripes {
216 let (k, m, first) = (st.k as usize, st.m as usize, st.first as usize);
217 let stripe_bad: Vec<usize> = bad.iter().map(|&b| b as usize).filter(|&b| b >= first && b < first + k).collect();
218 if stripe_bad.is_empty() {
219 par_index += m;
220 continue;
221 }
222 let mut shards: Vec<Option<Vec<u8>>> = Vec::with_capacity(k + m);
223 for i in first..first + k {
224 if stripe_bad.contains(&i) {
225 shards.push(None);
226 } else {
227 let mut buf = vec![0u8; ps];
228 read_at(&mut file, (i * ps) as u64, &mut buf)?;
229 shards.push(Some(buf));
230 }
231 }
232 for j in 0..m {
233 let mut buf = vec![0u8; ps];
234 let got = read_at(&mut par, st.par_off + (j * ps) as u64, &mut buf)?;
235 let ok = got == ps && meta.parity.get(par_index + j).is_some_and(|h| h[..] == chunk::hash(&buf)[..]);
236 shards.push(if ok { Some(buf) } else { None });
237 }
238 par_index += m;
239 let available = shards.iter().filter(|s| s.is_some()).count();
240 if available < k {
241 all_ok = false;
242 continue;
243 }
244 rs_new(k, m)?.reconstruct(&mut shards).map_err(|e| Error::Corrupt(format!("reed-solomon reconstruct: {e:?}")))?;
245 for &i in &stripe_bad {
246 let piece = shards[i - first].as_ref().ok_or_else(|| Error::Corrupt("reed-solomon left a shard empty".into()))?;
247 let start = (i * ps) as u64;
248 let end = (start + ps as u64).min(meta.size);
249 file.seek(SeekFrom::Start(start))?;
250 file.write_all(&piece[..(end - start) as usize])?;
251 repaired += 1;
252 }
253 }
254 file.flush()?;
255 drop(file);
256 let (bad_after, _) = verify(archive)?;
257 Ok((repaired, all_ok && bad_after.is_empty()))
258}