1use std::io::{BufRead, Read, Seek, SeekFrom, Write};
2
3use crate::checksum::{calc_md4, calc_sha1_stream};
4use crate::rsum::{Rsum, calc_rsum_block};
5
6#[derive(Debug, thiserror::Error)]
7pub enum GenerateError {
8 #[error("IO error: {0}")]
9 Io(#[from] std::io::Error),
10 #[error("file is empty")]
11 EmptyFile,
12}
13
14#[derive(Debug, thiserror::Error)]
15pub enum WriteError {
16 #[error("IO error: {0}")]
17 Io(#[from] std::io::Error),
18}
19
20#[derive(Debug, thiserror::Error)]
21pub enum ParseError {
22 #[error("IO error: {0}")]
23 Io(#[from] std::io::Error),
24 #[error("Invalid header: {0}")]
25 InvalidHeader(String),
26 #[error("Missing required field: {0}")]
27 MissingField(String),
28 #[error("Invalid blocksize: {0}")]
29 InvalidBlocksize(String),
30 #[error("Invalid hash lengths: {0}")]
31 InvalidHashLengths(String),
32 #[error("Invalid length: {0}")]
33 InvalidLength(String),
34 #[error("Unexpected end of file")]
35 UnexpectedEof,
36 #[error("Header section exceeds {0} bytes")]
37 HeaderTooLarge(u64),
38}
39
40#[derive(Debug, Clone, Copy)]
41pub struct BlockChecksum {
42 pub rsum: Rsum,
43 pub checksum: [u8; 16],
44}
45
46#[derive(Debug, Clone)]
47pub struct ControlFile {
48 pub version: String,
49 pub filename: Option<String>,
50 pub mtime: Option<String>,
51 pub blocksize: usize,
52 pub length: u64,
53 pub hash_lengths: HashLengths,
54 pub urls: Vec<String>,
55 pub sha1: Option<String>,
56 pub block_checksums: Vec<BlockChecksum>,
57}
58
59#[derive(Debug, Clone, Copy)]
60pub struct HashLengths {
61 pub seq_matches: u8,
62 pub rsum_bytes: u8,
63 pub checksum_bytes: u8,
64}
65
66impl Default for HashLengths {
67 fn default() -> Self {
68 Self {
69 seq_matches: 1,
70 rsum_bytes: 4,
71 checksum_bytes: 16,
72 }
73 }
74}
75
76impl ControlFile {
77 pub fn parse<R: Read>(reader: R) -> Result<Self, ParseError> {
85 const MAX_HEADER_BYTES: u64 = 1 << 20;
88
89 let mut reader = std::io::BufReader::new(reader);
90 let mut line = String::new();
91
92 let mut version = String::new();
93 let mut filename = None;
94 let mut mtime = None;
95 let mut blocksize = None;
96 let mut length = None;
97 let mut hash_lengths = HashLengths::default();
98 let mut urls = Vec::new();
99 let mut sha1 = None;
100
101 let mut header = (&mut reader).take(MAX_HEADER_BYTES);
102 loop {
103 line.clear();
104 let bytes_read = header.read_line(&mut line)?;
105 if bytes_read == 0 {
106 if header.limit() == 0 {
107 return Err(ParseError::HeaderTooLarge(MAX_HEADER_BYTES));
108 }
109 return Err(ParseError::UnexpectedEof);
110 }
111
112 let trimmed = line.trim_end_matches(['\n', '\r', ' ']);
113 if trimmed.is_empty() {
114 break;
115 }
116
117 let Some((key, value)) = trimmed.split_once(':') else {
118 return Err(ParseError::InvalidHeader(trimmed.to_string()));
119 };
120
121 let value = value.trim_start_matches(' ');
122
123 match key {
124 "zsync" => {
125 version = value.to_string();
126 }
127 "Filename" => {
128 filename = Some(value.to_string());
129 }
130 "MTime" => {
131 mtime = Some(value.to_string());
132 }
133 "Blocksize" => {
134 let bs: usize = value
135 .parse()
136 .map_err(|_| ParseError::InvalidBlocksize(value.to_string()))?;
137 if bs == 0 || (bs & (bs - 1)) != 0 {
138 return Err(ParseError::InvalidBlocksize(value.to_string()));
139 }
140 blocksize = Some(bs);
141 }
142 "Length" => {
143 length = Some(
144 value
145 .parse()
146 .map_err(|_| ParseError::InvalidLength(value.to_string()))?,
147 );
148 }
149 "URL" => {
150 urls.push(value.to_string());
151 }
152 "Hash-Lengths" => {
153 let parts: Vec<&str> = value.split(',').collect();
154 if parts.len() != 3 {
155 return Err(ParseError::InvalidHashLengths(value.to_string()));
156 }
157 let seq_matches: u8 = parts[0]
158 .parse()
159 .map_err(|_| ParseError::InvalidHashLengths(value.to_string()))?;
160 let rsum_bytes: u8 = parts[1]
161 .parse()
162 .map_err(|_| ParseError::InvalidHashLengths(value.to_string()))?;
163 let checksum_bytes: u8 = parts[2]
164 .parse()
165 .map_err(|_| ParseError::InvalidHashLengths(value.to_string()))?;
166
167 if !(1..=2).contains(&seq_matches)
168 || !(1..=4).contains(&rsum_bytes)
169 || !(3..=16).contains(&checksum_bytes)
170 {
171 return Err(ParseError::InvalidHashLengths(value.to_string()));
172 }
173
174 hash_lengths = HashLengths {
175 seq_matches,
176 rsum_bytes,
177 checksum_bytes,
178 };
179 }
180 "SHA-1" => {
181 if value.len() != 40 {
182 return Err(ParseError::InvalidHeader(
183 "SHA-1 digest wrong length".to_string(),
184 ));
185 }
186 sha1 = Some(value.to_string());
187 }
188 _ => {}
189 }
190 }
191
192 let blocksize =
193 blocksize.ok_or_else(|| ParseError::MissingField("Blocksize".to_string()))?;
194 let length: u64 = length.ok_or_else(|| ParseError::MissingField("Length".to_string()))?;
195
196 let num_blocks = length.div_ceil(blocksize as u64) as usize;
197
198 const MAX_BLOCKS: usize = 64 * 1024 * 1024;
201 if num_blocks > MAX_BLOCKS {
202 return Err(ParseError::InvalidLength(format!(
203 "too many blocks: {num_blocks}"
204 )));
205 }
206
207 let block_checksums = Self::read_block_checksums(&mut reader, num_blocks, hash_lengths)?;
208
209 Ok(Self {
210 version,
211 filename,
212 mtime,
213 blocksize,
214 length,
215 hash_lengths,
216 urls,
217 sha1,
218 block_checksums,
219 })
220 }
221
222 fn read_block_checksums<R: BufRead>(
223 reader: &mut R,
224 num_blocks: usize,
225 hash_lengths: HashLengths,
226 ) -> Result<Vec<BlockChecksum>, ParseError> {
227 const MAX_PREALLOC: usize = 1 << 16;
233 let mut checksums = Vec::with_capacity(num_blocks.min(MAX_PREALLOC));
234 let entry_size = (hash_lengths.rsum_bytes + hash_lengths.checksum_bytes) as usize;
235 let mut buf = vec![0u8; entry_size];
236
237 for _ in 0..num_blocks {
238 reader.read_exact(&mut buf)?;
239
240 let rsum_bytes = hash_lengths.rsum_bytes as usize;
241 let (rsum_a, rsum_b) = match rsum_bytes {
242 1 => (0u16, u16::from(buf[0])),
243 2 => (0u16, u16::from_be_bytes([buf[0], buf[1]])),
244 3 => (u16::from(buf[0]), u16::from_be_bytes([buf[1], buf[2]])),
245 4 => (
246 u16::from_be_bytes([buf[0], buf[1]]),
247 u16::from_be_bytes([buf[2], buf[3]]),
248 ),
249 _ => (0, 0),
250 };
251
252 let mut checksum = [0u8; 16];
253 checksum[..hash_lengths.checksum_bytes as usize]
254 .copy_from_slice(&buf[rsum_bytes..entry_size]);
255
256 checksums.push(BlockChecksum {
257 rsum: Rsum {
258 a: rsum_a,
259 b: rsum_b,
260 },
261 checksum,
262 });
263 }
264
265 Ok(checksums)
266 }
267
268 pub fn num_blocks(&self) -> usize {
269 self.block_checksums.len()
270 }
271
272 pub fn generate<R: Read + Seek>(
275 reader: &mut R,
276 filename: &str,
277 url: &str,
278 blocksize: Option<usize>,
279 ) -> Result<Self, GenerateError> {
280 let file_length = reader.seek(SeekFrom::End(0))?;
281 if file_length == 0 {
282 return Err(GenerateError::EmptyFile);
283 }
284 reader.seek(SeekFrom::Start(0))?;
285
286 let blocksize = blocksize.unwrap_or_else(|| auto_blocksize(file_length));
287 let hash_lengths = calculate_hash_lengths(file_length, blocksize);
288 let num_blocks = file_length.div_ceil(blocksize as u64) as usize;
289
290 let mut block_checksums = Vec::with_capacity(num_blocks);
291 let mut buf = vec![0u8; blocksize];
292
293 for i in 0..num_blocks {
294 let is_last = i == num_blocks - 1;
295 let block_len = if is_last {
296 let rem = (file_length % blocksize as u64) as usize;
297 if rem == 0 { blocksize } else { rem }
298 } else {
299 blocksize
300 };
301
302 reader.read_exact(&mut buf[..block_len])?;
303 if block_len < blocksize {
304 buf[block_len..].fill(0);
305 }
306
307 let rsum = calc_rsum_block(&buf);
308 let checksum = calc_md4(&buf);
309 block_checksums.push(BlockChecksum { rsum, checksum });
310 }
311
312 reader.seek(SeekFrom::Start(0))?;
313 let sha1_bytes = calc_sha1_stream(reader)?;
314 let sha1 = sha1_bytes
315 .iter()
316 .fold(String::with_capacity(40), |mut s, b| {
317 use std::fmt::Write;
318 let _ = write!(s, "{b:02x}");
319 s
320 });
321
322 Ok(Self {
323 version: "0.6.2".to_string(),
324 filename: Some(filename.to_string()),
325 mtime: None,
326 blocksize,
327 length: file_length,
328 hash_lengths,
329 urls: vec![url.to_string()],
330 sha1: Some(sha1),
331 block_checksums,
332 })
333 }
334
335 pub fn write<W: Write>(&self, writer: &mut W) -> Result<(), WriteError> {
337 writeln!(writer, "zsync: {}", self.version)?;
338 if let Some(ref filename) = self.filename {
339 writeln!(writer, "Filename: {filename}")?;
340 }
341 if let Some(ref mtime) = self.mtime {
342 writeln!(writer, "MTime: {mtime}")?;
343 }
344 writeln!(writer, "Blocksize: {}", self.blocksize)?;
345 writeln!(writer, "Length: {}", self.length)?;
346 writeln!(
347 writer,
348 "Hash-Lengths: {},{},{}",
349 self.hash_lengths.seq_matches,
350 self.hash_lengths.rsum_bytes,
351 self.hash_lengths.checksum_bytes
352 )?;
353 for url in &self.urls {
354 writeln!(writer, "URL: {url}")?;
355 }
356 if let Some(ref sha1) = self.sha1 {
357 writeln!(writer, "SHA-1: {sha1}")?;
358 }
359 writeln!(writer)?;
360
361 let rsum_bytes = self.hash_lengths.rsum_bytes as usize;
362 let checksum_bytes = self.hash_lengths.checksum_bytes as usize;
363
364 for block in &self.block_checksums {
365 let rsum_be = rsum_to_bytes(block.rsum, rsum_bytes);
366 writer.write_all(&rsum_be)?;
367 writer.write_all(&block.checksum[..checksum_bytes])?;
368 }
369
370 Ok(())
371 }
372}
373
374fn rsum_to_bytes(rsum: Rsum, rsum_bytes: usize) -> Vec<u8> {
375 match rsum_bytes {
376 1 => vec![rsum.b as u8],
377 2 => rsum.b.to_be_bytes().to_vec(),
378 3 => {
379 let mut v = Vec::with_capacity(3);
380 v.push(rsum.a as u8);
381 v.extend_from_slice(&rsum.b.to_be_bytes());
382 v
383 }
384 4 => {
385 let mut v = Vec::with_capacity(4);
386 v.extend_from_slice(&rsum.a.to_be_bytes());
387 v.extend_from_slice(&rsum.b.to_be_bytes());
388 v
389 }
390 _ => vec![0; rsum_bytes],
391 }
392}
393
394fn auto_blocksize(file_length: u64) -> usize {
395 if file_length < 100_000_000 {
396 2048
397 } else {
398 4096
399 }
400}
401
402fn calculate_hash_lengths(file_length: u64, blocksize: usize) -> HashLengths {
404 let len = file_length as f64;
405 let bs = blocksize as f64;
406 let seq_matches: u8 = if file_length > blocksize as u64 { 2 } else { 1 };
407 let sm = f64::from(seq_matches);
408
409 let rsum_bytes = ((len.ln() + bs.ln()) / 2_f64.ln() - 8.6) / sm / 8.0;
410 let rsum_bytes = (rsum_bytes.ceil() as i32).clamp(2, 4);
411
412 let num_blocks = 1.0 + len / bs;
413 let calc1 = ((20.0 + len.log2() + num_blocks.log2()) / sm / 8.0).ceil();
414 let calc2 = (7.9 + (20.0 + num_blocks.log2())) / 8.0;
415 let checksum_bytes = (calc1.max(calc2) as i32).clamp(4, 16);
416
417 HashLengths {
418 seq_matches,
419 rsum_bytes: rsum_bytes as u8,
420 checksum_bytes: checksum_bytes as u8,
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427
428 struct Endless;
431
432 impl std::io::Read for Endless {
433 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
434 buf.fill(b'A');
435 Ok(buf.len())
436 }
437 }
438
439 #[test]
440 fn an_endless_header_is_refused_rather_than_buffered() {
441 let err = ControlFile::parse(Endless).expect_err("must not read forever");
446 assert!(
447 matches!(
448 err,
449 ParseError::HeaderTooLarge(_) | ParseError::InvalidHeader(_)
450 ),
451 "expected the header cap to stop parsing, got: {err}"
452 );
453 }
454
455 #[test]
456 fn an_endless_run_of_headers_is_refused() {
457 struct ManyUrls;
460 impl std::io::Read for ManyUrls {
461 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
462 let line = b"URL: http://example.invalid/f\n";
463 let mut written = 0;
464 while written + line.len() <= buf.len() {
465 buf[written..written + line.len()].copy_from_slice(line);
466 written += line.len();
467 }
468 Ok(written.max(1))
469 }
470 }
471 let err = ControlFile::parse(ManyUrls).expect_err("must not read forever");
472 assert!(
473 matches!(err, ParseError::HeaderTooLarge(_)),
474 "expected a header cap error, got: {err}"
475 );
476 }
477
478 #[test]
479 fn test_parse_minimal() {
480 let mut data = Vec::new();
481 data.extend_from_slice(
482 b"zsync: 0.6.2\nBlocksize: 2048\nLength: 2048\nHash-Lengths: 1,4,16\n\n",
483 );
484 data.extend_from_slice(&[0u8; 20]);
485 let result = ControlFile::parse(&data[..]);
486 assert!(result.is_ok());
487 let cf = result.unwrap();
488 assert_eq!(cf.blocksize, 2048);
489 assert_eq!(cf.length, 2048);
490 }
491
492 #[test]
493 fn test_parse_missing_blocksize() {
494 let data = b"zsync: 0.6.2\nLength: 4096\n\n";
495 let result = ControlFile::parse(&data[..]);
496 assert!(result.is_err());
497 }
498
499 #[test]
500 fn test_parse_invalid_blocksize() {
501 let data = b"zsync: 0.6.2\nBlocksize: 1000\nLength: 4096\n\n";
502 let result = ControlFile::parse(&data[..]);
503 assert!(result.is_err());
504 }
505
506 #[test]
507 fn test_generate_write_roundtrip() {
508 let file_data = vec![42u8; 4096];
509 let mut cursor = std::io::Cursor::new(&file_data);
510
511 let cf = ControlFile::generate(&mut cursor, "test.bin", "test.bin", Some(2048)).unwrap();
512 assert_eq!(cf.blocksize, 2048);
513 assert_eq!(cf.length, 4096);
514 assert_eq!(cf.block_checksums.len(), 2);
515 assert!(cf.sha1.is_some());
516
517 let mut buf = Vec::new();
518 cf.write(&mut buf).unwrap();
519 let parsed = ControlFile::parse(&buf[..]).unwrap();
520
521 assert_eq!(parsed.blocksize, cf.blocksize);
522 assert_eq!(parsed.length, cf.length);
523 assert_eq!(parsed.sha1, cf.sha1);
524 assert_eq!(parsed.block_checksums.len(), cf.block_checksums.len());
525 let rlen = cf.hash_lengths.rsum_bytes as usize;
526 let clen = cf.hash_lengths.checksum_bytes as usize;
527 for (a, b) in parsed.block_checksums.iter().zip(&cf.block_checksums) {
528 let a_bytes = rsum_to_bytes(a.rsum, rlen);
529 let b_bytes = rsum_to_bytes(b.rsum, rlen);
530 assert_eq!(a_bytes, b_bytes);
531 assert_eq!(a.checksum[..clen], b.checksum[..clen]);
532 }
533 }
534
535 #[test]
536 fn test_generate_empty_file() {
537 let file_data: Vec<u8> = vec![];
538 let mut cursor = std::io::Cursor::new(&file_data);
539 let result = ControlFile::generate(&mut cursor, "empty", "empty", None);
540 assert!(result.is_err());
541 }
542
543 #[test]
544 fn test_generate_partial_last_block() {
545 let file_data = vec![0xABu8; 3000];
547 let mut cursor = std::io::Cursor::new(&file_data);
548
549 let cf = ControlFile::generate(&mut cursor, "test.bin", "test.bin", Some(2048)).unwrap();
550 assert_eq!(cf.length, 3000);
551 assert_eq!(cf.block_checksums.len(), 2);
552 }
553
554 #[test]
555 fn test_auto_blocksize_small() {
556 assert_eq!(auto_blocksize(1024), 2048);
557 assert_eq!(auto_blocksize(99_999_999), 2048);
558 }
559
560 #[test]
561 fn test_auto_blocksize_large() {
562 assert_eq!(auto_blocksize(100_000_000), 4096);
563 assert_eq!(auto_blocksize(500_000_000), 4096);
564 }
565
566 #[test]
567 fn test_rsum_to_bytes() {
568 let rsum = Rsum {
569 a: 0x1234,
570 b: 0x5678,
571 };
572 assert_eq!(rsum_to_bytes(rsum, 4), vec![0x12, 0x34, 0x56, 0x78]);
573 assert_eq!(rsum_to_bytes(rsum, 2), vec![0x56, 0x78]);
574 assert_eq!(rsum_to_bytes(rsum, 1), vec![0x78]);
575 }
576}