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#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Lengths {
17 total_length: u64,
19 piece_length: u64,
21 chunk_size: u32,
23 num_pieces: u32,
25 last_piece_size: u32,
27}
28
29pub const DEFAULT_CHUNK_SIZE: u32 = 16384;
31
32impl Lengths {
33 #[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 #[must_use]
70 pub fn total_length(&self) -> u64 {
71 self.total_length
72 }
73
74 #[must_use]
76 pub fn piece_length(&self) -> u64 {
77 self.piece_length
78 }
79
80 #[must_use]
82 pub fn chunk_size(&self) -> u32 {
83 self.chunk_size
84 }
85
86 #[must_use]
88 pub fn num_pieces(&self) -> u32 {
89 self.num_pieces
90 }
91
92 #[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 #[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 #[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 #[must_use]
139 pub fn piece_offset(&self, piece_index: u32) -> u64 {
140 u64::from(piece_index) * self.piece_length
141 }
142
143 #[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 #[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 #[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 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); }
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); }
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); }
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); }
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); }
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; let expected_chunks = (last_piece_size + 16383) / 16384; 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 let l = Lengths::new(100_000, 50_000, 16_384);
267 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); assert_eq!(l.chunk_info(4, 0), None); }
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); }
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 assert_eq!(l.piece_index_for_byte(262_144), Some(1));
307 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 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 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 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 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 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 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}