Skip to main content

zsync_rs/
control.rs

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    /// Parse a control file.
78    ///
79    /// The header section is read through a byte cap. It arrives from the
80    /// network, and both a single unterminated line and an endless run of
81    /// `URL:` lines would otherwise grow without bound, letting the origin
82    /// choose how much memory the client spends before any of it is
83    /// validated.
84    pub fn parse<R: Read>(reader: R) -> Result<Self, ParseError> {
85        /// Generous: real headers are a few hundred bytes, and even a long
86        /// mirror list is far below this.
87        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        // Sanity check: avoid massive allocations from malformed input.
199        // Each block needs (rsum_bytes + checksum_bytes) of data following the header.
200        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        // Reserve for what has arrived, not for what was claimed. The count
228        // is derived from a header a few bytes long, so sizing the buffer
229        // from it lets a tiny response reserve gigabytes before a single
230        // checksum is read. Growth is geometric, so a genuinely large file
231        // costs a handful of reallocations and nothing else.
232        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    /// Generate a control file by scanning an input file.
273    /// Blocksize is auto-calculated if `None`.
274    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    /// Write the control file to a writer.
336    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
402/// Calculate optimal hash lengths based on file size and blocksize.
403fn 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    /// A stream that never ends and never contains a newline, which is
429    /// what a hostile origin serves to make the client read forever.
430    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        // The cap is what makes this terminate at all. Which error comes
442        // out depends on where the cut lands: a line with no separator is
443        // reported as a bad header, a truncated section as an oversized
444        // one. Either is fine; reading forever is not.
445        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        // Individually valid lines, without end. The cap has to bound the
458        // section, not just a single line.
459        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        // File not aligned to blocksize
546        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}