Skip to main content

mkit_server/
download.rs

1//! `DownloadPack` chunking (SPEC-TRANSPORT-CONNECT ยง6.2).
2//!
3//! The canonical copy of the chunk loops in `mkit-transport-connect` 0.4's
4//! `pack.rs` (`chunk_download`) and `mkit serve`'s `download_chunks`; `vcs-worker`
5//! adopted this plan in WP-M0-17 (planner decision Q17). The old copies are
6//! gone: `mkit serve`'s in WP-M0-13, `mkit-transport-connect`'s with its
7//! server in WP-M0-15.
8
9/// Largest `PackChunk.data` a server sends: well below the 1 MiB frame limit
10/// of the ssh/enc framing, and a manageable Connect message size.
11pub const DOWNLOAD_CHUNK_MAX: usize = 800 * 1024;
12
13/// One `PackChunk` of a download: `len` bytes at `offset`.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct ChunkSpan {
16    /// Byte offset of the chunk in the pack.
17    pub offset: u64,
18    /// Chunk length in bytes.
19    pub len: usize,
20    /// Whether this is the final chunk (`PackChunk.last`).
21    pub last: bool,
22}
23
24/// Split a `total`-byte pack into contiguous chunks of at most `max` bytes
25/// (a `max` of 0 counts as 1). Only the final span is `last`. An empty pack
26/// yields exactly one `{offset: 0, len: 0, last: true}` span, so the client
27/// always sees a terminator.
28pub fn chunk_plan(total: u64, max: usize) -> impl Iterator<Item = ChunkSpan> {
29    let step = u64::try_from(max.max(1)).unwrap_or(u64::MAX);
30    let mut offset = 0u64;
31    let mut done = false;
32    std::iter::from_fn(move || {
33        if done {
34            return None;
35        }
36        let len = (total - offset).min(step);
37        offset += len;
38        done = offset == total;
39        Some(ChunkSpan {
40            offset: offset - len,
41            // `len <= step`, which came from a `usize`.
42            len: usize::try_from(len).unwrap_or(usize::MAX),
43            last: done,
44        })
45    })
46}
47
48#[cfg(test)]
49mod tests {
50    use proptest::prelude::*;
51
52    use super::*;
53
54    fn plan(total: u64, max: usize) -> Vec<ChunkSpan> {
55        chunk_plan(total, max).collect()
56    }
57
58    fn span(offset: u64, len: usize, last: bool) -> ChunkSpan {
59        ChunkSpan { offset, len, last }
60    }
61
62    #[test]
63    fn chunk_plan_empty_is_single_last() {
64        assert_eq!(plan(0, DOWNLOAD_CHUNK_MAX), [span(0, 0, true)]);
65    }
66
67    #[test]
68    fn chunk_plan_exact_multiple() {
69        let max = DOWNLOAD_CHUNK_MAX;
70        let total = 2 * max as u64;
71        assert_eq!(
72            plan(total, max),
73            [span(0, max, false), span(max as u64, max, true)]
74        );
75        assert_eq!(plan(4, 4), [span(0, 4, true)]);
76    }
77
78    #[test]
79    fn chunk_plan_remainder() {
80        assert_eq!(
81            plan(10, 4),
82            [span(0, 4, false), span(4, 4, false), span(8, 2, true)]
83        );
84        assert_eq!(
85            plan(3, 0),
86            [span(0, 1, false), span(1, 1, false), span(2, 1, true)]
87        );
88    }
89
90    proptest! {
91        #[test]
92        fn chunk_plan_spans_are_contiguous(total in 0u64..100_000, max in 1usize..5_000) {
93            let spans = plan(total, max);
94            let mut next = 0u64;
95            for (i, s) in spans.iter().enumerate() {
96                prop_assert_eq!(s.offset, next);
97                prop_assert!(s.len <= max);
98                prop_assert_eq!(s.last, i + 1 == spans.len());
99                if total > 0 {
100                    prop_assert!(s.len > 0);
101                }
102                next += s.len as u64;
103            }
104            prop_assert_eq!(next, total);
105        }
106    }
107}