1use crate::chunk::{self, Hash};
12use crate::error::{Error, Result};
13use crate::format;
14use reed_solomon_erasure::galois_8::ReedSolomon;
15use serde::{Deserialize, Serialize};
16use serde_bytes::ByteBuf;
17use std::collections::BTreeMap;
18use std::fs::File;
19use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
20use std::path::{Path, PathBuf};
21
22pub const VOLUME_MAGIC: [u8; 4] = *b"TSV\x1A";
23pub const TRAILER_MAGIC: [u8; 4] = *b"VST\x1A";
24pub const VERSION: u16 = 1;
25pub const HEADER_LEN: usize = 64;
26pub const TRAILER_LEN: usize = 56;
27pub const MIN_PIECE_SIZE: u32 = 64 << 10;
28pub const MAX_PIECE_SIZE: u32 = 64 << 20;
29pub const DEFAULT_PIECE_SIZE: u32 = 1 << 20;
30pub const MAX_VOLUMES: usize = 256;
31pub const MAX_DESCRIPTOR: u64 = 64 << 20;
33pub const MAX_PIECES: u32 = 2_000_000;
36pub const MAX_ARCHIVE_NAME: usize = 255;
38pub const EXTENSION: &str = "tsrv";
39
40#[derive(Clone, Debug, Serialize, Deserialize)]
42pub struct VolumeSet {
43 pub v: u16,
44 pub set_id: ByteBuf,
45 pub archive_name: String,
46 pub archive_size: u64,
47 pub archive_b3: ByteBuf,
49 pub piece_size: u32,
50 pub data: u16,
51 pub parity: u16,
52 pub pieces: u32,
54 pub stripes: u32,
56 pub piece_hashes: Vec<ByteBuf>,
57 pub parity_hashes: Vec<ByteBuf>,
59 pub root: ByteBuf,
61 pub generator: String,
62}
63
64impl VolumeSet {
65 pub fn total(&self) -> usize {
66 self.data as usize + self.parity as usize
67 }
68
69 pub fn is_parity(&self, index: usize) -> bool {
70 index >= self.data as usize
71 }
72
73 pub fn volume_pieces(&self, index: usize) -> u64 {
75 if self.is_parity(index) {
76 self.stripes as u64
77 } else {
78 let (p, n) = (self.pieces as u64, self.data as u64);
79 let v = index as u64;
80 if v >= p {
81 0
82 } else {
83 (p - v).div_ceil(n)
84 }
85 }
86 }
87
88 pub fn payload_len(&self, index: usize) -> u64 {
89 self.volume_pieces(index) * self.piece_size as u64
90 }
91
92 pub fn volume_name(&self, index: usize) -> String {
97 let width = if self.total() > 99 { 3 } else { 2 };
98 let stem = match crate::paths::normalize(&self.archive_name) {
99 Ok(n) if !n.contains('/') && n.len() <= MAX_ARCHIVE_NAME => n,
100 _ => format!("set-{}", hex::encode(&self.set_id[..8])),
101 };
102 format!("{stem}.v{:0width$}.{EXTENSION}", index + 1, width = width)
103 }
104
105 pub fn validate(&self) -> Result<()> {
106 let bad = |m: &str| Error::Corrupt(format!("volume descriptor: {m}"));
107 if self.v != VERSION {
108 return Err(Error::Version(self.v));
109 }
110 if self.data == 0 || self.total() > MAX_VOLUMES {
111 return Err(bad("volume counts out of range"));
112 }
113 if !(MIN_PIECE_SIZE..=MAX_PIECE_SIZE).contains(&self.piece_size) {
114 return Err(bad("piece size out of range"));
115 }
116 if self.set_id.len() != 32 || self.archive_b3.len() != 32 || self.root.len() != 32 {
117 return Err(bad("hash lengths"));
118 }
119 let pieces = self.archive_size.div_ceil(self.piece_size as u64);
120 if pieces > MAX_PIECES as u64 {
121 return Err(Error::Limit(format!("volume descriptor: {pieces} pieces exceed the limit of {MAX_PIECES}")));
122 }
123 if pieces != self.pieces as u64 || pieces.div_ceil(self.data as u64) != self.stripes as u64 {
124 return Err(bad("piece or stripe count does not match the archive size"));
125 }
126 if self.piece_hashes.len() != self.pieces as usize || self.parity_hashes.len() != self.stripes as usize * self.parity as usize {
127 return Err(bad("hash list lengths"));
128 }
129 if self.piece_hashes.iter().chain(self.parity_hashes.iter()).any(|h| h.len() != 32) {
130 return Err(bad("hash length"));
131 }
132 if self.archive_name.is_empty() || self.archive_name.len() > MAX_ARCHIVE_NAME || self.archive_name.contains(['/', '\\']) || self.archive_name.chars().any(|c| c.is_control()) {
133 return Err(bad("archive name"));
134 }
135 Ok(())
136 }
137
138 pub(crate) fn piece_hash(&self, index: u32) -> [u8; 32] {
139 let mut h = [0u8; 32];
140 h.copy_from_slice(&self.piece_hashes[index as usize]);
141 h
142 }
143
144 pub(crate) fn parity_hash(&self, stripe: u32, j: usize) -> [u8; 32] {
145 let mut h = [0u8; 32];
146 h.copy_from_slice(&self.parity_hashes[stripe as usize * self.parity as usize + j]);
147 h
148 }
149}
150
151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
153pub struct VolumeHeader {
154 pub index: u16,
155 pub data: u16,
156 pub parity: u16,
157 pub piece_size: u32,
158 pub archive_size: u64,
159 pub set_id: [u8; 32],
160 pub payload_len: u64,
161}
162
163pub(crate) fn encode_header(h: &VolumeHeader) -> [u8; HEADER_LEN] {
164 let mut b = [0u8; HEADER_LEN];
165 b[0..4].copy_from_slice(&VOLUME_MAGIC);
166 b[4..6].copy_from_slice(&VERSION.to_le_bytes());
167 b[6..8].copy_from_slice(&h.index.to_le_bytes());
168 b[8..10].copy_from_slice(&h.data.to_le_bytes());
169 b[10..12].copy_from_slice(&h.parity.to_le_bytes());
170 b[12..16].copy_from_slice(&h.piece_size.to_le_bytes());
171 b[16..24].copy_from_slice(&h.archive_size.to_le_bytes());
172 b[24..56].copy_from_slice(&h.set_id);
173 b[56..64].copy_from_slice(&h.payload_len.to_le_bytes());
174 b
175}
176
177pub(crate) fn decode_header(b: &[u8]) -> Result<VolumeHeader> {
178 if b.len() < HEADER_LEN || b[0..4] != VOLUME_MAGIC {
179 return Err(Error::BadMagic);
180 }
181 let version = u16::from_le_bytes([b[4], b[5]]);
182 if version != VERSION {
183 return Err(Error::Version(version));
184 }
185 let mut set_id = [0u8; 32];
186 set_id.copy_from_slice(&b[24..56]);
187 Ok(VolumeHeader {
188 index: u16::from_le_bytes([b[6], b[7]]),
189 data: u16::from_le_bytes([b[8], b[9]]),
190 parity: u16::from_le_bytes([b[10], b[11]]),
191 piece_size: u32::from_le_bytes(b[12..16].try_into().unwrap()),
192 archive_size: u64::from_le_bytes(b[16..24].try_into().unwrap()),
193 set_id,
194 payload_len: u64::from_le_bytes(b[56..64].try_into().unwrap()),
195 })
196}
197
198pub(crate) fn encode_trailer(desc_off: u64, desc_len: u64, desc_hash: &Hash, index: u16) -> [u8; TRAILER_LEN] {
199 let mut b = [0u8; TRAILER_LEN];
200 b[0..8].copy_from_slice(&desc_off.to_le_bytes());
201 b[8..16].copy_from_slice(&desc_len.to_le_bytes());
202 b[16..48].copy_from_slice(desc_hash);
203 b[48..50].copy_from_slice(&index.to_le_bytes());
204 b[52..56].copy_from_slice(&TRAILER_MAGIC);
205 b
206}
207
208fn decode_trailer(b: &[u8]) -> Result<(u64, u64, Hash, u16)> {
210 if b.len() != TRAILER_LEN || b[52..56] != TRAILER_MAGIC {
211 return Err(Error::Corrupt("volume trailer".into()));
212 }
213 let mut h = [0u8; 32];
214 h.copy_from_slice(&b[16..48]);
215 Ok((u64::from_le_bytes(b[0..8].try_into().unwrap()), u64::from_le_bytes(b[8..16].try_into().unwrap()), h, u16::from_le_bytes([b[48], b[49]])))
216}
217
218pub(crate) fn set_id_for(archive_b3: &Hash, archive_size: u64, piece_size: u32, data: u16, parity: u16) -> Hash {
219 let mut h = blake3::Hasher::new_derive_key("tsaur volume set v1");
220 h.update(archive_b3);
221 h.update(&archive_size.to_le_bytes());
222 h.update(&piece_size.to_le_bytes());
223 h.update(&data.to_le_bytes());
224 h.update(&parity.to_le_bytes());
225 *h.finalize().as_bytes()
226}
227
228fn read_full<R: Read>(r: &mut R, buf: &mut [u8]) -> Result<usize> {
229 let mut got = 0;
230 while got < buf.len() {
231 let n = r.read(&mut buf[got..])?;
232 if n == 0 {
233 break;
234 }
235 got += n;
236 }
237 Ok(got)
238}
239
240fn hash_file(path: &Path) -> Result<(Hash, u64)> {
241 let mut f = BufReader::with_capacity(1 << 20, File::open(path)?);
242 let mut hasher = blake3::Hasher::new();
243 let mut buf = vec![0u8; 1 << 20];
244 let mut total = 0u64;
245 loop {
246 let n = f.read(&mut buf)?;
247 if n == 0 {
248 break;
249 }
250 hasher.update(&buf[..n]);
251 total += n as u64;
252 }
253 Ok((*hasher.finalize().as_bytes(), total))
254}
255
256fn rs_new(k: usize, m: usize) -> Result<Option<ReedSolomon>> {
257 if m == 0 {
258 return Ok(None);
259 }
260 ReedSolomon::new(k, m).map(Some).map_err(|e| Error::Invalid(format!("reed-solomon: {e:?}")))
261}
262
263pub struct SplitOptions {
266 pub data: u16,
267 pub parity: u16,
268 pub piece_size: Option<u32>,
270 pub outputs: Vec<PathBuf>,
272}
273
274pub const ADAPTIVE_PIECES_PER_VOLUME: u64 = 16;
276pub const ADAPTIVE_MAX_PIECE_SIZE: u32 = 1 << 20;
278pub const ADAPTIVE_MAX_PIECES: u64 = 1_000_000;
280
281pub fn adaptive_piece_size(archive_size: u64, data: u16) -> u32 {
287 let per_volume = archive_size.div_ceil(ADAPTIVE_PIECES_PER_VOLUME * data.max(1) as u64);
288 let mut size = per_volume.max(MIN_PIECE_SIZE as u64).next_power_of_two().min(ADAPTIVE_MAX_PIECE_SIZE as u64);
289 while archive_size.div_ceil(size) > ADAPTIVE_MAX_PIECES && size < MAX_PIECE_SIZE as u64 {
290 size *= 2;
291 }
292 size as u32
293}
294
295#[derive(Clone, Debug, Serialize)]
296pub struct VolumeInfo {
297 pub index: usize,
298 pub kind: &'static str,
299 pub path: PathBuf,
300 pub bytes: u64,
301}
302
303#[derive(Clone, Debug, Serialize)]
304pub struct SplitReport {
305 pub set_id: String,
306 pub descriptor_b3: String,
308 pub archive_b3: String,
309 pub archive_size: u64,
310 pub piece_size: u32,
311 pub data: u16,
312 pub parity: u16,
313 pub pieces: u32,
314 pub stripes: u32,
315 pub piece_size_adaptive: bool,
317 pub volumes: Vec<VolumeInfo>,
318 pub max_per_location: usize,
320 pub placement_warning: bool,
322}
323
324pub fn placement(outputs: &[PathBuf], index: usize) -> &Path {
326 &outputs[index % outputs.len()]
327}
328
329pub fn split(archive: &Path, opts: &SplitOptions) -> Result<SplitReport> {
330 let (n, m) = (opts.data as usize, opts.parity as usize);
331 if n == 0 || n + m > MAX_VOLUMES {
332 return Err(Error::Invalid(format!("data volumes must be 1..{MAX_VOLUMES} and data + parity at most {MAX_VOLUMES}")));
333 }
334 if let Some(ps) = opts.piece_size {
335 if !(MIN_PIECE_SIZE..=MAX_PIECE_SIZE).contains(&ps) {
336 return Err(Error::Invalid(format!("piece size must be between {MIN_PIECE_SIZE} and {MAX_PIECE_SIZE} bytes")));
337 }
338 }
339 if opts.outputs.is_empty() {
340 return Err(Error::Invalid("at least one output directory is required".into()));
341 }
342 for d in &opts.outputs {
343 if !d.is_dir() {
344 return Err(Error::Missing(format!("output directory {} does not exist", d.display())));
345 }
346 }
347 let archive_name = archive.file_name().map(|s| s.to_string_lossy().to_string()).filter(|s| !s.is_empty()).ok_or_else(|| Error::Invalid("archive path has no file name".into()))?;
348 let (archive_b3, archive_size) = hash_file(archive)?;
349 let piece_size = opts.piece_size.unwrap_or_else(|| adaptive_piece_size(archive_size, opts.data));
350 let ps = piece_size as usize;
351 let pieces_u64 = archive_size.div_ceil(ps as u64);
352 let pieces = u32::try_from(pieces_u64).ok().filter(|&p| p <= MAX_PIECES).ok_or_else(|| Error::Limit(format!("{pieces_u64} pieces exceed the limit of {MAX_PIECES}: use a larger piece size")))?;
353 let stripes = pieces_u64.div_ceil(n as u64) as u32;
354 let set_id = set_id_for(&archive_b3, archive_size, piece_size, opts.data, opts.parity);
355 let mut set = VolumeSet {
356 v: VERSION,
357 set_id: ByteBuf::from(set_id.to_vec()),
358 archive_name,
359 archive_size,
360 archive_b3: ByteBuf::from(archive_b3.to_vec()),
361 piece_size,
362 data: opts.data,
363 parity: opts.parity,
364 pieces,
365 stripes,
366 piece_hashes: Vec::with_capacity(pieces as usize),
367 parity_hashes: Vec::with_capacity(stripes as usize * m),
368 root: ByteBuf::from(vec![0u8; 32]),
369 generator: crate::WRITER.to_string(),
370 };
371 if MAX_DESCRIPTOR < 32 * (pieces as u64 + stripes as u64 * m as u64) {
372 return Err(Error::Limit("descriptor too large: use a larger piece size".into()));
373 }
374
375 let mut writers: Vec<BufWriter<File>> = Vec::with_capacity(n + m);
377 let mut paths = Vec::with_capacity(n + m);
378 for vi in 0..n + m {
379 let path = placement(&opts.outputs, vi).join(set.volume_name(vi));
380 let mut w = BufWriter::with_capacity(1 << 20, File::create(&path)?);
381 let header = VolumeHeader { index: vi as u16, data: opts.data, parity: opts.parity, piece_size, archive_size, set_id, payload_len: set.payload_len(vi) };
382 w.write_all(&encode_header(&header))?;
383 writers.push(w);
384 paths.push(path);
385 }
386
387 let rs = rs_new(n, m)?;
389 let mut input = BufReader::with_capacity(1 << 20, File::open(archive)?);
390 let mut hashes: Vec<Hash> = Vec::with_capacity(pieces as usize);
391 let mut shards: Vec<Vec<u8>> = (0..n + m).map(|_| vec![0u8; ps]).collect();
392 let mut read_total = 0u64;
393 for s in 0..stripes {
394 for v in 0..n {
395 let pi = s as u64 * n as u64 + v as u64;
396 let shard = &mut shards[v];
397 if pi < pieces as u64 {
398 let got = read_full(&mut input, shard)?;
399 shard[got..].fill(0);
400 read_total += got as u64;
401 hashes.push(chunk::hash(shard));
402 writers[v].write_all(shard)?;
403 } else {
404 shard.fill(0); }
406 }
407 if let Some(rs) = &rs {
408 rs.encode(&mut shards).map_err(|e| Error::Invalid(format!("reed-solomon encode: {e:?}")))?;
409 for j in 0..m {
410 set.parity_hashes.push(ByteBuf::from(chunk::hash(&shards[n + j]).to_vec()));
411 writers[n + j].write_all(&shards[n + j])?;
412 }
413 }
414 }
415 if read_total != archive_size {
416 return Err(Error::Io(std::io::Error::other("archive changed size while it was being split")));
417 }
418 set.root = ByteBuf::from(chunk::merkle_root(&hashes).to_vec());
419 set.piece_hashes = hashes.iter().map(|h| ByteBuf::from(h.to_vec())).collect();
420 set.validate()?;
421
422 let desc = format::cbor_encode(&set)?;
424 let desc_hash = chunk::hash(&desc);
425 let mut volumes = Vec::with_capacity(n + m);
426 for (vi, mut w) in writers.into_iter().enumerate() {
427 let desc_off = HEADER_LEN as u64 + set.payload_len(vi);
428 w.write_all(&desc)?;
429 w.write_all(&encode_trailer(desc_off, desc.len() as u64, &desc_hash, vi as u16))?;
430 w.flush()?;
431 let bytes = desc_off + desc.len() as u64 + TRAILER_LEN as u64;
432 volumes.push(VolumeInfo { index: vi, kind: if vi < n { "data" } else { "parity" }, path: paths[vi].clone(), bytes });
433 }
434 let mut per_location: BTreeMap<PathBuf, usize> = BTreeMap::new();
435 for vi in 0..n + m {
436 *per_location.entry(placement(&opts.outputs, vi).to_path_buf()).or_insert(0) += 1;
437 }
438 let max_per_location = per_location.values().copied().max().unwrap_or(0);
439 Ok(SplitReport {
440 set_id: hex::encode(set_id),
441 descriptor_b3: hex::encode(desc_hash),
442 archive_b3: hex::encode(archive_b3),
443 archive_size,
444 piece_size,
445 piece_size_adaptive: opts.piece_size.is_none(),
446 data: opts.data,
447 parity: opts.parity,
448 pieces,
449 stripes,
450 volumes,
451 max_per_location,
452 placement_warning: max_per_location > m,
453 })
454}
455
456#[derive(Clone, Debug, Serialize)]
460pub struct FoundVolume {
461 pub path: PathBuf,
462 pub index: usize,
463 pub kind: &'static str,
464 pub header_ok: bool,
465 pub descriptor_ok: bool,
466 pub size_ok: bool,
468 pub bad_pieces: Vec<u32>,
470 pub verified: bool,
471}
472
473impl FoundVolume {
474 pub fn usable(&self) -> bool {
475 self.size_ok && self.bad_pieces.is_empty()
476 }
477}
478
479#[derive(Clone, Debug, Serialize)]
480pub struct SetStatus {
481 pub set_id: String,
482 pub descriptor_b3: String,
484 pub archive_name: String,
485 pub archive_size: u64,
486 pub archive_b3: String,
487 pub piece_size: u32,
488 pub data: u16,
489 pub parity: u16,
490 pub pieces: u32,
491 pub stripes: u32,
492 pub volumes: Vec<Option<FoundVolume>>,
494 pub missing: Vec<usize>,
495 pub stripes_short: u32,
497 pub reconstructible: bool,
498 pub extra: Vec<(PathBuf, String)>,
500 #[serde(skip)]
501 pub set: VolumeSet,
502}
503
504struct Probe {
505 path: PathBuf,
506 header: Option<VolumeHeader>,
507 trailer_index: Option<u16>,
508 set_id: Option<[u8; 32]>,
509 descriptor: Option<VolumeSet>,
510 file_len: u64,
511 error: Option<String>,
512}
513
514fn probe(path: &Path) -> Probe {
515 let mut p = Probe { path: path.to_path_buf(), header: None, trailer_index: None, set_id: None, descriptor: None, file_len: 0, error: None };
516 let mut f = match File::open(path) {
517 Ok(f) => f,
518 Err(e) => {
519 p.error = Some(e.to_string());
520 return p;
521 }
522 };
523 p.file_len = f.metadata().map(|m| m.len()).unwrap_or(0);
524 let mut hb = [0u8; HEADER_LEN];
525 if read_full(&mut f, &mut hb).map(|n| n == HEADER_LEN).unwrap_or(false) {
526 match decode_header(&hb) {
527 Ok(h) => {
528 p.set_id = Some(h.set_id);
529 p.header = Some(h);
530 }
531 Err(Error::Version(v)) => {
534 p.error = Some(format!("volume format version {v} is not supported by this build (version {VERSION})"));
535 return p;
536 }
537 Err(_) => {}
538 }
539 }
540 if p.file_len >= (HEADER_LEN + TRAILER_LEN) as u64 {
541 let mut tb = [0u8; TRAILER_LEN];
542 let ok = f.seek(SeekFrom::Start(p.file_len - TRAILER_LEN as u64)).is_ok() && read_full(&mut f, &mut tb).map(|n| n == TRAILER_LEN).unwrap_or(false);
543 if ok {
544 if let Ok((off, len, hash, index)) = decode_trailer(&tb) {
545 p.trailer_index = Some(index);
546 if len <= MAX_DESCRIPTOR && off.checked_add(len).is_some_and(|e| e + TRAILER_LEN as u64 == p.file_len) {
547 let mut db = vec![0u8; len as usize];
548 if f.seek(SeekFrom::Start(off)).is_ok() && read_full(&mut f, &mut db).map(|n| n as u64 == len).unwrap_or(false) && chunk::hash(&db) == hash {
549 if let Ok(set) = format::cbor_decode::<VolumeSet>(&db) {
550 if set.validate().is_ok() {
551 let mut id = [0u8; 32];
552 id.copy_from_slice(&set.set_id);
553 if p.header.is_some_and(|h| h.set_id != id) {
555 p.header = None;
556 }
557 p.set_id = Some(id);
558 p.descriptor = Some(set);
559 }
560 }
561 }
562 }
563 }
564 }
565 }
566 if p.header.is_none() && p.descriptor.is_none() {
567 p.error = Some("neither the header nor the trailer of this file is a valid T-saur volume".into());
568 }
569 p
570}
571
572pub fn expand(paths: &[PathBuf]) -> Result<Vec<PathBuf>> {
574 let mut out = Vec::new();
575 for p in paths {
576 if p.is_dir() {
577 let mut found: Vec<PathBuf> = std::fs::read_dir(p)?.filter_map(|e| e.ok().map(|e| e.path())).filter(|f| f.is_file() && f.extension().is_some_and(|x| x == EXTENSION)).collect();
578 found.sort();
579 out.extend(found);
580 } else {
581 out.push(p.clone());
582 }
583 }
584 Ok(out)
585}
586
587pub fn inspect(paths: &[PathBuf], verify: bool) -> Result<Vec<SetStatus>> {
589 let files = expand(paths)?;
590 let probes: Vec<Probe> = files.iter().map(|f| probe(f)).collect();
591 let mut groups: BTreeMap<[u8; 32], Vec<usize>> = BTreeMap::new();
592 let mut unknown: Vec<(PathBuf, String)> = Vec::new();
593 for (i, p) in probes.iter().enumerate() {
594 match p.set_id {
595 Some(id) => groups.entry(id).or_default().push(i),
596 None => unknown.push((p.path.clone(), p.error.clone().unwrap_or_else(|| "unrecognised".into()))),
597 }
598 }
599 let mut out = Vec::new();
600 for (id, members) in groups {
601 let descriptor_b3 = members.iter().find_map(|&i| probes[i].descriptor.as_ref().map(|d| hex::encode(chunk::hash(&format::cbor_encode(d).unwrap_or_default())))).unwrap_or_default();
603 let Some(set) = members.iter().find_map(|&i| probes[i].descriptor.clone()) else {
604 for &i in &members {
605 unknown.push((probes[i].path.clone(), "no member of this set has an intact descriptor".into()));
606 }
607 continue;
608 };
609 let mut volumes: Vec<Option<FoundVolume>> = vec![None; set.total()];
610 let mut extra = Vec::new();
611 for &i in &members {
612 let p = &probes[i];
613 let index = match (p.header.map(|h| h.index as usize), p.trailer_index.map(|t| t as usize)) {
614 (Some(h), Some(t)) if h != t => {
615 extra.push((p.path.clone(), "header and trailer disagree on the volume index".into()));
616 continue;
617 }
618 (Some(h), _) => h,
619 (None, Some(t)) => t,
620 (None, None) => {
621 extra.push((p.path.clone(), "volume index unknown".into()));
622 continue;
623 }
624 };
625 if index >= set.total() {
626 extra.push((p.path.clone(), format!("volume index {index} out of range")));
627 continue;
628 }
629 let header_ok = p
630 .header
631 .is_some_and(|h| h.data == set.data && h.parity == set.parity && h.piece_size == set.piece_size && h.archive_size == set.archive_size && h.payload_len == set.payload_len(index));
632 let expected_len = HEADER_LEN as u64 + set.payload_len(index);
633 let size_ok = p.file_len >= expected_len + TRAILER_LEN as u64;
634 let mut fv = FoundVolume {
635 path: p.path.clone(),
636 index,
637 kind: if set.is_parity(index) { "parity" } else { "data" },
638 header_ok,
639 descriptor_ok: p.descriptor.is_some(),
640 size_ok,
641 bad_pieces: Vec::new(),
642 verified: false,
643 };
644 if verify && size_ok {
645 fv.bad_pieces = verify_volume(&set, &p.path, index)?;
646 fv.verified = true;
647 }
648 match &volumes[index] {
649 Some(existing) if existing.usable() || !fv.usable() => extra.push((p.path.clone(), format!("duplicate of volume {}", index + 1))),
650 _ => volumes[index] = Some(fv),
651 }
652 }
653 let missing: Vec<usize> = (0..set.total()).filter(|&i| volumes[i].is_none()).collect();
654 let stripes_short = short_stripes(&set, &volumes);
655 out.push(SetStatus {
656 set_id: hex::encode(id),
657 descriptor_b3,
658 archive_name: set.archive_name.clone(),
659 archive_size: set.archive_size,
660 archive_b3: hex::encode(&set.archive_b3),
661 piece_size: set.piece_size,
662 data: set.data,
663 parity: set.parity,
664 pieces: set.pieces,
665 stripes: set.stripes,
666 volumes,
667 missing,
668 stripes_short,
669 reconstructible: stripes_short == 0,
670 extra,
671 set,
672 });
673 }
674 if out.is_empty() {
675 let detail = unknown.iter().map(|(p, e)| format!("{}: {e}", p.display())).collect::<Vec<_>>().join("; ");
676 return Err(Error::Missing(if detail.is_empty() { "no volume files found".into() } else { format!("no usable volume set among the inputs ({detail})") }));
677 }
678 for s in &mut out {
680 s.extra.extend(unknown.iter().cloned());
681 }
682 Ok(out)
683}
684
685fn short_stripes(set: &VolumeSet, volumes: &[Option<FoundVolume>]) -> u32 {
687 let n = set.data as usize;
688 let mut short = 0u32;
689 for s in 0..set.stripes {
690 let mut available = 0usize;
691 for (vi, vol) in volumes.iter().enumerate() {
692 let virtual_piece = !set.is_parity(vi) && (s as u64 * n as u64 + vi as u64) >= set.pieces as u64;
693 if virtual_piece {
694 available += 1;
695 continue;
696 }
697 if let Some(v) = vol {
698 if v.size_ok && !v.bad_pieces.contains(&s) {
699 available += 1;
700 }
701 }
702 }
703 if available < n {
704 short += 1;
705 }
706 }
707 short
708}
709
710pub fn verify_volume(set: &VolumeSet, path: &Path, index: usize) -> Result<Vec<u32>> {
712 let ps = set.piece_size as usize;
713 let mut f = BufReader::with_capacity(ps.min(1 << 20), File::open(path)?);
714 f.seek(SeekFrom::Start(HEADER_LEN as u64))?;
715 let mut buf = vec![0u8; ps];
716 let mut bad = Vec::new();
717 let count = set.volume_pieces(index);
718 for s in 0..count {
719 let got = read_full(&mut f, &mut buf)?;
720 let expected = if set.is_parity(index) { set.parity_hash(s as u32, index - set.data as usize) } else { set.piece_hash((s * set.data as u64 + index as u64) as u32) };
721 if got != ps || chunk::hash(&buf) != expected {
722 bad.push(s as u32);
723 }
724 }
725 Ok(bad)
726}
727
728fn open_volumes(status: &SetStatus) -> Result<Vec<Option<File>>> {
732 let mut files = Vec::with_capacity(status.set.total());
733 for v in &status.volumes {
734 files.push(match v {
735 Some(fv) if fv.size_ok => Some(File::open(&fv.path)?),
736 _ => None,
737 });
738 }
739 Ok(files)
740}
741
742fn read_stripe(set: &VolumeSet, files: &mut [Option<File>], s: u32, buf: &mut [Vec<u8>]) -> Result<Vec<Option<Vec<u8>>>> {
745 let (n, ps) = (set.data as usize, set.piece_size as usize);
746 let mut shards: Vec<Option<Vec<u8>>> = Vec::with_capacity(set.total());
747 for vi in 0..set.total() {
748 let virtual_piece = !set.is_parity(vi) && (s as u64 * n as u64 + vi as u64) >= set.pieces as u64;
749 if virtual_piece {
750 shards.push(Some(vec![0u8; ps]));
751 continue;
752 }
753 let expected = if set.is_parity(vi) { set.parity_hash(s, vi - n) } else { set.piece_hash((s as u64 * n as u64 + vi as u64) as u32) };
754 let shard = match &mut files[vi] {
755 Some(f) => {
756 let off = HEADER_LEN as u64 + s as u64 * ps as u64;
757 let b = &mut buf[vi];
758 let ok = f.seek(SeekFrom::Start(off)).is_ok() && read_full(f, b).map(|g| g == ps).unwrap_or(false) && chunk::hash(b) == expected;
759 if ok {
760 Some(b.clone())
761 } else {
762 None
763 }
764 }
765 None => None,
766 };
767 shards.push(shard);
768 }
769 Ok(shards)
770}
771
772fn rebuild_stripe(set: &VolumeSet, rs: &Option<ReedSolomon>, s: u32, shards: &mut [Option<Vec<u8>>], data_only: bool) -> Result<u32> {
775 let n = set.data as usize;
776 let available = shards.iter().filter(|x| x.is_some()).count();
777 if available < n {
778 let missing: Vec<String> = shards.iter().enumerate().filter(|(_, x)| x.is_none()).map(|(i, _)| set.volume_name(i)).collect();
779 return Err(Error::Missing(format!("stripe {s}: only {available} of the {n} pieces needed are intact; unavailable: {}", missing.join(", "))));
780 }
781 let wanted = if data_only { &shards[..n] } else { &shards[..] };
782 let rebuilt = wanted.iter().filter(|x| x.is_none()).count() as u32;
783 if rebuilt > 0 {
784 let rs = rs.as_ref().ok_or_else(|| Error::Missing(format!("stripe {s}: a piece is missing and the set has no parity")))?;
785 let res = if data_only { rs.reconstruct_data(shards) } else { rs.reconstruct(shards) };
786 res.map_err(|e| Error::Corrupt(format!("reed-solomon reconstruct: {e:?}")))?;
787 }
788 Ok(rebuilt)
789}
790
791#[derive(Clone, Debug, Serialize)]
792pub struct JoinReport {
793 pub set_id: String,
794 pub archive_name: String,
795 pub archive_size: u64,
796 pub output: PathBuf,
797 pub volumes_used: Vec<usize>,
798 pub volumes_missing: Vec<usize>,
799 pub pieces_rebuilt: u32,
800 pub archive_b3: String,
801 pub archive_hash_ok: bool,
802}
803
804pub fn join(paths: &[PathBuf], out: &Path) -> Result<JoinReport> {
808 let mut sets = inspect(paths, false)?;
809 if sets.len() > 1 {
810 return Err(Error::Invalid(format!("the inputs hold {} different volume sets: give the volumes of one set", sets.len())));
811 }
812 let status = sets.remove(0);
813 let set = status.set.clone();
814 let mut files = open_volumes(&status)?;
815 let rs = rs_new(set.data as usize, set.parity as usize)?;
816 let ps = set.piece_size as usize;
817 let mut buf: Vec<Vec<u8>> = (0..set.total()).map(|_| vec![0u8; ps]).collect();
818 let tmp = out.with_extension(format!("{}.tsr-partial", out.extension().map(|x| x.to_string_lossy().to_string()).unwrap_or_default()));
819 let result = (|| -> Result<u32> {
820 let mut w = BufWriter::with_capacity(1 << 20, File::create(&tmp)?);
821 let mut hasher = blake3::Hasher::new();
822 let mut written = 0u64;
823 let mut rebuilt = 0u32;
824 for s in 0..set.stripes {
825 let mut shards = read_stripe(&set, &mut files, s, &mut buf)?;
826 rebuilt += rebuild_stripe(&set, &rs, s, &mut shards, true)?;
827 for (v, shard) in shards.iter().take(set.data as usize).enumerate() {
828 let pi = s as u64 * set.data as u64 + v as u64;
829 if pi >= set.pieces as u64 {
830 break;
831 }
832 let piece = shard.as_ref().ok_or_else(|| Error::Corrupt("reed-solomon left a shard empty".into()))?;
833 let take = (set.archive_size - written).min(ps as u64) as usize;
834 w.write_all(&piece[..take])?;
835 hasher.update(&piece[..take]);
836 written += take as u64;
837 }
838 }
839 w.flush()?;
840 if written != set.archive_size || hasher.finalize().as_bytes()[..] != set.archive_b3[..] {
841 return Err(Error::HashMismatch("rebuilt archive does not match the hash recorded at split time".into()));
842 }
843 Ok(rebuilt)
844 })();
845 match result {
846 Ok(rebuilt) => {
847 if out.exists() {
848 std::fs::remove_file(out)?;
849 }
850 std::fs::rename(&tmp, out)?;
851 Ok(JoinReport {
852 set_id: status.set_id.clone(),
853 archive_name: set.archive_name.clone(),
854 archive_size: set.archive_size,
855 output: out.to_path_buf(),
856 volumes_used: (0..set.total()).filter(|&i| files[i].is_some()).collect(),
857 volumes_missing: status.missing.clone(),
858 pieces_rebuilt: rebuilt,
859 archive_b3: hex::encode(&set.archive_b3),
860 archive_hash_ok: true,
861 })
862 }
863 Err(e) => {
864 let _ = std::fs::remove_file(&tmp);
865 Err(e)
866 }
867 }
868}
869
870#[derive(Clone, Debug, Serialize)]
871pub struct RepairReport {
872 pub set_id: String,
873 pub rebuilt: Vec<VolumeInfo>,
874 pub pieces_rebuilt: u32,
875}
876
877pub fn repair(paths: &[PathBuf], out_dir: Option<&Path>) -> Result<RepairReport> {
881 let mut sets = inspect(paths, true)?;
882 if sets.len() > 1 {
883 return Err(Error::Invalid(format!("the inputs hold {} different volume sets: give the volumes of one set", sets.len())));
884 }
885 let status = sets.remove(0);
886 let set = status.set.clone();
887 let targets: Vec<(usize, PathBuf)> = (0..set.total())
888 .filter_map(|vi| match &status.volumes[vi] {
889 Some(fv) if fv.usable() && fv.header_ok && fv.descriptor_ok => None,
890 Some(fv) => Some((vi, fv.path.clone())),
891 None => {
892 let dir = out_dir.map(Path::to_path_buf).or_else(|| status.volumes.iter().flatten().next().map(|f| f.path.parent().map(Path::to_path_buf).unwrap_or_default()));
893 dir.map(|d| (vi, d.join(set.volume_name(vi))))
894 }
895 })
896 .collect();
897 if targets.is_empty() {
898 return Ok(RepairReport { set_id: status.set_id.clone(), rebuilt: Vec::new(), pieces_rebuilt: 0 });
899 }
900 if status.stripes_short > 0 {
901 return Err(Error::Missing(format!("{} stripe(s) cannot be rebuilt with the volumes at hand", status.stripes_short)));
902 }
903 let mut files = open_volumes(&status)?;
904 let rs = rs_new(set.data as usize, set.parity as usize)?;
906 let ps = set.piece_size as usize;
907 let mut buf: Vec<Vec<u8>> = (0..set.total()).map(|_| vec![0u8; ps]).collect();
908 let desc = format::cbor_encode(&set)?;
909 let desc_hash = chunk::hash(&desc);
910 let mut set_id = [0u8; 32];
911 set_id.copy_from_slice(&set.set_id);
912 let tmp_paths: Vec<PathBuf> = targets.iter().map(|(_, p)| p.with_extension(format!("{EXTENSION}.tsr-partial"))).collect();
913 let result = (|| -> Result<u32> {
914 let mut writers: Vec<BufWriter<File>> = Vec::with_capacity(targets.len());
915 for ((vi, _), tmp) in targets.iter().zip(tmp_paths.iter()) {
916 let mut w = BufWriter::with_capacity(1 << 20, File::create(tmp)?);
917 let header = VolumeHeader { index: *vi as u16, data: set.data, parity: set.parity, piece_size: set.piece_size, archive_size: set.archive_size, set_id, payload_len: set.payload_len(*vi) };
918 w.write_all(&encode_header(&header))?;
919 writers.push(w);
920 }
921 let mut rebuilt = 0u32;
922 for s in 0..set.stripes {
923 let mut shards = read_stripe(&set, &mut files, s, &mut buf)?;
924 rebuilt += rebuild_stripe(&set, &rs, s, &mut shards, false)?;
925 for ((vi, _), w) in targets.iter().zip(writers.iter_mut()) {
926 if s as u64 >= set.volume_pieces(*vi) {
927 continue;
928 }
929 let piece = shards[*vi].as_ref().ok_or_else(|| Error::Corrupt("reed-solomon left a shard empty".into()))?;
930 w.write_all(piece)?;
931 }
932 }
933 for ((vi, _), mut w) in targets.iter().zip(writers) {
934 w.write_all(&desc)?;
935 w.write_all(&encode_trailer(HEADER_LEN as u64 + set.payload_len(*vi), desc.len() as u64, &desc_hash, *vi as u16))?;
936 w.flush()?;
937 }
938 Ok(rebuilt)
939 })();
940 match result {
941 Ok(pieces_rebuilt) => {
942 drop(files);
943 let mut rebuilt = Vec::new();
944 for ((vi, path), tmp) in targets.iter().zip(tmp_paths.iter()) {
945 if path.exists() {
946 std::fs::remove_file(path)?;
947 }
948 std::fs::rename(tmp, path)?;
949 let bytes = std::fs::metadata(path)?.len();
950 rebuilt.push(VolumeInfo { index: *vi, kind: if set.is_parity(*vi) { "parity" } else { "data" }, path: path.clone(), bytes });
951 }
952 Ok(RepairReport { set_id: status.set_id.clone(), rebuilt, pieces_rebuilt })
953 }
954 Err(e) => {
955 for tmp in &tmp_paths {
956 let _ = std::fs::remove_file(tmp);
957 }
958 Err(e)
959 }
960 }
961}
962
963#[cfg(test)]
964mod tests {
965 use super::*;
966
967 #[test]
968 fn adaptive_piece_size_boundaries() {
969 assert_eq!(adaptive_piece_size(0, 4), MIN_PIECE_SIZE);
971 assert_eq!(adaptive_piece_size(1, 1), MIN_PIECE_SIZE);
972 assert_eq!(adaptive_piece_size(956_731, 4), 64 << 10);
974 assert_eq!(adaptive_piece_size(4 * 16 * 65_536, 4), 64 << 10);
976 assert_eq!(adaptive_piece_size(4 * 16 * 65_536 + 1, 4), 128 << 10);
977 assert_eq!(adaptive_piece_size(4 * 16 * 200_000, 4), 256 << 10);
979 assert_eq!(adaptive_piece_size(64 << 20, 4), 1 << 20);
981 assert_eq!(adaptive_piece_size(10 << 30, 4), 1 << 20);
982 assert_eq!(adaptive_piece_size(1_500_000u64 << 20, 4), 2 << 20);
984 assert_eq!(adaptive_piece_size(u64::MAX / 4, 4), MAX_PIECE_SIZE);
985 assert!(adaptive_piece_size(64 << 20, 1) >= adaptive_piece_size(64 << 20, 8));
987 }
988}