Skip to main content

irontide_core/
lengths.rs

1#![allow(
2    clippy::cast_possible_truncation,
3    clippy::cast_precision_loss,
4    clippy::cast_possible_wrap,
5    clippy::cast_sign_loss,
6    reason = "M175: piece arithmetic — narrowing casts bounded by `num_pieces: u32` invariant established in `Lengths::new`"
7)]
8
9/// Piece and chunk arithmetic for `BitTorrent` downloads.
10///
11/// Manages the mapping between:
12/// - **Pieces**: fixed-size blocks verified by SHA1 (except possibly the last piece)
13/// - **Chunks**: sub-piece blocks requested from peers (typically 16 KiB)
14/// - **Files**: the actual files on disk that pieces map across
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Lengths {
17    /// Total size of all files in bytes.
18    total_length: u64,
19    /// Size of each piece in bytes (last piece may be smaller).
20    piece_length: u64,
21    /// Size of each chunk/block in bytes (typically 16384).
22    chunk_size: u32,
23    /// Pre-computed number of pieces.
24    num_pieces: u32,
25    /// Pre-computed size of the last piece.
26    last_piece_size: u32,
27}
28
29/// Default chunk size (16 KiB) — standard in `BitTorrent`.
30pub const DEFAULT_CHUNK_SIZE: u32 = 16384;
31
32impl Lengths {
33    /// Create a new Lengths calculator.
34    ///
35    /// # Panics
36    /// Panics if `piece_length` or `chunk_size` is 0.
37    #[must_use]
38    pub fn new(total_length: u64, piece_length: u64, chunk_size: u32) -> Self {
39        assert!(piece_length > 0, "piece_length must be > 0");
40        assert!(chunk_size > 0, "chunk_size must be > 0");
41
42        let num_pieces = if total_length == 0 {
43            0
44        } else {
45            total_length.div_ceil(piece_length) as u32
46        };
47
48        let last_piece_size = if num_pieces == 0 {
49            0
50        } else {
51            let remainder = total_length % piece_length;
52            if remainder == 0 {
53                piece_length as u32
54            } else {
55                remainder as u32
56            }
57        };
58
59        Self {
60            total_length,
61            piece_length,
62            chunk_size,
63            num_pieces,
64            last_piece_size,
65        }
66    }
67
68    /// Total size of all content.
69    #[must_use]
70    pub fn total_length(&self) -> u64 {
71        self.total_length
72    }
73
74    /// Standard piece length.
75    #[must_use]
76    pub fn piece_length(&self) -> u64 {
77        self.piece_length
78    }
79
80    /// Chunk/block size.
81    #[must_use]
82    pub fn chunk_size(&self) -> u32 {
83        self.chunk_size
84    }
85
86    /// Total number of pieces.
87    #[must_use]
88    pub fn num_pieces(&self) -> u32 {
89        self.num_pieces
90    }
91
92    /// Actual length of a specific piece (last piece may be shorter).
93    #[inline]
94    #[must_use]
95    pub fn piece_size(&self, piece_index: u32) -> u32 {
96        if piece_index >= self.num_pieces {
97            0
98        } else if piece_index == self.num_pieces - 1 {
99            self.last_piece_size
100        } else {
101            self.piece_length as u32
102        }
103    }
104
105    /// Number of chunks in a specific piece.
106    #[inline]
107    #[must_use]
108    pub fn chunks_in_piece(&self, piece_index: u32) -> u32 {
109        let piece_size = u64::from(self.piece_size(piece_index));
110        if piece_size == 0 {
111            return 0;
112        }
113        piece_size.div_ceil(u64::from(self.chunk_size)) as u32
114    }
115
116    /// Offset and length of a specific chunk within a piece.
117    ///
118    /// Returns `(offset_within_piece, chunk_length)`.
119    #[inline]
120    #[must_use]
121    pub fn chunk_info(&self, piece_index: u32, chunk_index: u32) -> Option<(u32, u32)> {
122        let piece_size = self.piece_size(piece_index);
123        if piece_size == 0 {
124            return None;
125        }
126
127        let offset = chunk_index * self.chunk_size;
128        if offset >= piece_size {
129            return None;
130        }
131
132        let remaining = piece_size - offset;
133        let len = remaining.min(self.chunk_size);
134        Some((offset, len))
135    }
136
137    /// Absolute byte offset for the start of a piece.
138    #[must_use]
139    pub fn piece_offset(&self, piece_index: u32) -> u64 {
140        u64::from(piece_index) * self.piece_length
141    }
142
143    /// Map an absolute byte offset to a piece index.
144    ///
145    /// Returns `None` when `byte_offset >= total_length`.
146    ///
147    /// Prefer this over the ad-hoc `(byte / piece_length) as u32` form: M132's
148    /// in-flight underflow is the cautionary precedent for narrowing casts on
149    /// hot paths. Routing all byte→piece conversions through one bounded
150    /// function keeps that bug class in one place.
151    #[inline]
152    #[allow(
153        clippy::cast_possible_truncation,
154        reason = "byte_offset < total_length, total_length / piece_length ≤ num_pieces (u32)"
155    )]
156    #[must_use]
157    pub fn piece_index_for_byte(&self, byte_offset: u64) -> Option<u32> {
158        if byte_offset >= self.total_length {
159            return None;
160        }
161        Some((byte_offset / self.piece_length) as u32)
162    }
163
164    /// Map an absolute byte offset to `(piece_index, offset_within_piece)`.
165    ///
166    /// Sibling of [`Self::piece_index_for_byte`] for callers that also need
167    /// the offset within the piece (e.g. disk reads at a sub-piece position).
168    /// Returns `None` when `byte_offset >= total_length`.
169    #[inline]
170    #[allow(
171        clippy::cast_possible_truncation,
172        reason = "byte_offset < total_length: piece_index bounded by num_pieces (u32); offset_in_piece bounded by piece_length (u32 by construction in `Lengths::new`)"
173    )]
174    #[must_use]
175    pub fn byte_to_piece_with_offset(&self, byte_offset: u64) -> Option<(u32, u32)> {
176        if byte_offset >= self.total_length {
177            return None;
178        }
179        let piece_index = (byte_offset / self.piece_length) as u32;
180        let offset_in_piece = (byte_offset % self.piece_length) as u32;
181        Some((piece_index, offset_in_piece))
182    }
183
184    /// Given file boundaries, determine which pieces a file spans.
185    /// Returns `(first_piece, last_piece)` inclusive.
186    #[must_use]
187    pub fn file_pieces(&self, file_offset: u64, file_length: u64) -> Option<(u32, u32)> {
188        if file_length == 0 || file_offset >= self.total_length {
189            return None;
190        }
191        let first = (file_offset / self.piece_length) as u32;
192        let last_byte = file_offset + file_length - 1;
193        let last = (last_byte.min(self.total_length - 1) / self.piece_length) as u32;
194        Some((first, last))
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    fn make_lengths() -> Lengths {
203        // 1 MiB total, 256 KiB pieces, 16 KiB chunks
204        Lengths::new(1_048_576, 262_144, 16_384)
205    }
206
207    #[test]
208    fn num_pieces_exact_division() {
209        let l = make_lengths();
210        assert_eq!(l.num_pieces(), 4); // 1 MiB / 256 KiB = 4
211    }
212
213    #[test]
214    fn num_pieces_with_remainder() {
215        let l = Lengths::new(1_000_000, 262_144, 16_384);
216        assert_eq!(l.num_pieces(), 4); // ceil(1000000 / 262144) = 4
217    }
218
219    #[test]
220    fn piece_size_regular() {
221        let l = make_lengths();
222        assert_eq!(l.piece_size(0), 262_144);
223        assert_eq!(l.piece_size(1), 262_144);
224        assert_eq!(l.piece_size(3), 262_144); // last piece, exact division
225    }
226
227    #[test]
228    fn piece_size_last_piece_shorter() {
229        let l = Lengths::new(1_000_000, 262_144, 16_384);
230        assert_eq!(l.piece_size(0), 262_144);
231        assert_eq!(l.piece_size(3), 1_000_000 - 3 * 262_144); // 213568
232    }
233
234    #[test]
235    fn piece_size_out_of_bounds() {
236        let l = make_lengths();
237        assert_eq!(l.piece_size(4), 0);
238        assert_eq!(l.piece_size(100), 0);
239    }
240
241    #[test]
242    fn chunks_in_piece() {
243        let l = make_lengths();
244        assert_eq!(l.chunks_in_piece(0), 16); // 262144 / 16384 = 16
245    }
246
247    #[test]
248    fn chunks_in_last_piece() {
249        let l = Lengths::new(1_000_000, 262_144, 16_384);
250        let last_piece_size = 1_000_000 - 3 * 262_144; // 213568
251        let expected_chunks = (last_piece_size + 16383) / 16384; // 14
252        assert_eq!(l.chunks_in_piece(3), expected_chunks as u32);
253    }
254
255    #[test]
256    fn chunk_info_regular() {
257        let l = make_lengths();
258        assert_eq!(l.chunk_info(0, 0), Some((0, 16384)));
259        assert_eq!(l.chunk_info(0, 1), Some((16384, 16384)));
260        assert_eq!(l.chunk_info(0, 15), Some((15 * 16384, 16384)));
261    }
262
263    #[test]
264    fn chunk_info_last_chunk_shorter() {
265        // 100000 byte total, 50000 byte pieces, 16384 chunks
266        let l = Lengths::new(100_000, 50_000, 16_384);
267        // Piece 0: 50000 bytes, chunks: 0..16384, 16384..32768, 32768..49152 (16384), 49152..50000 (848)
268        assert_eq!(l.chunk_info(0, 3), Some((49152, 848)));
269    }
270
271    #[test]
272    fn chunk_info_out_of_bounds() {
273        let l = make_lengths();
274        assert_eq!(l.chunk_info(0, 16), None); // only 16 chunks (0..15)
275        assert_eq!(l.chunk_info(4, 0), None); // piece doesn't exist
276    }
277
278    #[test]
279    fn piece_offset() {
280        let l = make_lengths();
281        assert_eq!(l.piece_offset(0), 0);
282        assert_eq!(l.piece_offset(1), 262_144);
283        assert_eq!(l.piece_offset(3), 786_432);
284    }
285
286    #[test]
287    fn byte_to_piece_with_offset_basic() {
288        let l = make_lengths();
289        assert_eq!(l.byte_to_piece_with_offset(0), Some((0, 0)));
290        assert_eq!(l.byte_to_piece_with_offset(262_143), Some((0, 262_143)));
291        assert_eq!(l.byte_to_piece_with_offset(262_144), Some((1, 0)));
292        assert_eq!(l.byte_to_piece_with_offset(1_048_575), Some((3, 262_143)));
293        assert_eq!(l.byte_to_piece_with_offset(1_048_576), None); // past end
294    }
295
296    #[test]
297    fn piece_index_for_byte_at_zero() {
298        let l = make_lengths();
299        assert_eq!(l.piece_index_for_byte(0), Some(0));
300    }
301
302    #[test]
303    fn piece_index_for_byte_at_piece_boundary() {
304        let l = make_lengths();
305        // First byte of piece 1 — exactly at piece_length.
306        assert_eq!(l.piece_index_for_byte(262_144), Some(1));
307        // Last byte of piece 0 — one before piece_length.
308        assert_eq!(l.piece_index_for_byte(262_143), Some(0));
309    }
310
311    #[test]
312    fn piece_index_for_byte_at_total_length_returns_none() {
313        // Boundary regression: total_length is exclusive — exactly at it must
314        // return None, not Some(num_pieces) which would index past the bitmap.
315        let l = make_lengths();
316        assert_eq!(l.piece_index_for_byte(1_048_576), None);
317        assert_eq!(l.piece_index_for_byte(u64::MAX), None);
318    }
319
320    #[test]
321    fn piece_index_for_byte_matches_byte_to_piece_with_offset() {
322        // Sibling consistency: indices must agree across the full range.
323        let l = make_lengths();
324        for byte in [0, 1, 262_143, 262_144, 524_287, 524_288, 1_048_575] {
325            let single = l.piece_index_for_byte(byte);
326            let pair = l.byte_to_piece_with_offset(byte).map(|(p, _)| p);
327            assert_eq!(single, pair, "disagreement at byte {byte}");
328        }
329        // Past-end agreement.
330        assert_eq!(l.piece_index_for_byte(1_048_576), None);
331        assert_eq!(l.byte_to_piece_with_offset(1_048_576), None);
332    }
333
334    #[test]
335    fn piece_index_for_byte_uneven_last_piece() {
336        // 1 MB total - 1 byte, 256 KiB pieces — last piece is short.
337        let l = Lengths::new(1_048_575, 262_144, 16_384);
338        assert_eq!(l.piece_index_for_byte(1_048_574), Some(3));
339        assert_eq!(l.piece_index_for_byte(1_048_575), None);
340    }
341
342    #[test]
343    fn file_pieces_spanning() {
344        let l = make_lengths();
345        // File starting at 100000, length 500000 — spans pieces 0..2
346        assert_eq!(l.file_pieces(100_000, 500_000), Some((0, 2)));
347    }
348
349    #[test]
350    fn file_pieces_single_piece() {
351        let l = make_lengths();
352        // File entirely within piece 1
353        assert_eq!(l.file_pieces(262_144, 100), Some((1, 1)));
354    }
355
356    #[test]
357    fn file_pieces_entire_torrent() {
358        let l = make_lengths();
359        assert_eq!(l.file_pieces(0, 1_048_576), Some((0, 3)));
360    }
361
362    #[test]
363    fn zero_length_torrent() {
364        let l = Lengths::new(0, 262_144, 16_384);
365        assert_eq!(l.num_pieces(), 0);
366    }
367
368    #[test]
369    fn tiny_torrent() {
370        let l = Lengths::new(1, 262_144, 16_384);
371        assert_eq!(l.num_pieces(), 1);
372        assert_eq!(l.piece_size(0), 1);
373        assert_eq!(l.chunks_in_piece(0), 1);
374        assert_eq!(l.chunk_info(0, 0), Some((0, 1)));
375    }
376}