#![allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
reason = "M175: piece arithmetic — narrowing casts bounded by `num_pieces: u32` invariant established in `Lengths::new`"
)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Lengths {
total_length: u64,
piece_length: u64,
chunk_size: u32,
num_pieces: u32,
last_piece_size: u32,
}
pub const DEFAULT_CHUNK_SIZE: u32 = 16384;
impl Lengths {
#[must_use]
pub fn new(total_length: u64, piece_length: u64, chunk_size: u32) -> Self {
assert!(piece_length > 0, "piece_length must be > 0");
assert!(chunk_size > 0, "chunk_size must be > 0");
let num_pieces = if total_length == 0 {
0
} else {
total_length.div_ceil(piece_length) as u32
};
let last_piece_size = if num_pieces == 0 {
0
} else {
let remainder = total_length % piece_length;
if remainder == 0 {
piece_length as u32
} else {
remainder as u32
}
};
Self {
total_length,
piece_length,
chunk_size,
num_pieces,
last_piece_size,
}
}
#[must_use]
pub fn total_length(&self) -> u64 {
self.total_length
}
#[must_use]
pub fn piece_length(&self) -> u64 {
self.piece_length
}
#[must_use]
pub fn chunk_size(&self) -> u32 {
self.chunk_size
}
#[must_use]
pub fn num_pieces(&self) -> u32 {
self.num_pieces
}
#[inline]
#[must_use]
pub fn piece_size(&self, piece_index: u32) -> u32 {
if piece_index >= self.num_pieces {
0
} else if piece_index == self.num_pieces - 1 {
self.last_piece_size
} else {
self.piece_length as u32
}
}
#[inline]
#[must_use]
pub fn chunks_in_piece(&self, piece_index: u32) -> u32 {
let piece_size = u64::from(self.piece_size(piece_index));
if piece_size == 0 {
return 0;
}
piece_size.div_ceil(u64::from(self.chunk_size)) as u32
}
#[inline]
#[must_use]
pub fn chunk_info(&self, piece_index: u32, chunk_index: u32) -> Option<(u32, u32)> {
let piece_size = self.piece_size(piece_index);
if piece_size == 0 {
return None;
}
let offset = chunk_index * self.chunk_size;
if offset >= piece_size {
return None;
}
let remaining = piece_size - offset;
let len = remaining.min(self.chunk_size);
Some((offset, len))
}
#[must_use]
pub fn piece_offset(&self, piece_index: u32) -> u64 {
u64::from(piece_index) * self.piece_length
}
#[inline]
#[allow(
clippy::cast_possible_truncation,
reason = "byte_offset < total_length, total_length / piece_length ≤ num_pieces (u32)"
)]
#[must_use]
pub fn piece_index_for_byte(&self, byte_offset: u64) -> Option<u32> {
if byte_offset >= self.total_length {
return None;
}
Some((byte_offset / self.piece_length) as u32)
}
#[inline]
#[allow(
clippy::cast_possible_truncation,
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`)"
)]
#[must_use]
pub fn byte_to_piece_with_offset(&self, byte_offset: u64) -> Option<(u32, u32)> {
if byte_offset >= self.total_length {
return None;
}
let piece_index = (byte_offset / self.piece_length) as u32;
let offset_in_piece = (byte_offset % self.piece_length) as u32;
Some((piece_index, offset_in_piece))
}
#[must_use]
pub fn file_pieces(&self, file_offset: u64, file_length: u64) -> Option<(u32, u32)> {
if file_length == 0 || file_offset >= self.total_length {
return None;
}
let first = (file_offset / self.piece_length) as u32;
let last_byte = file_offset + file_length - 1;
let last = (last_byte.min(self.total_length - 1) / self.piece_length) as u32;
Some((first, last))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_lengths() -> Lengths {
Lengths::new(1_048_576, 262_144, 16_384)
}
#[test]
fn num_pieces_exact_division() {
let l = make_lengths();
assert_eq!(l.num_pieces(), 4); }
#[test]
fn num_pieces_with_remainder() {
let l = Lengths::new(1_000_000, 262_144, 16_384);
assert_eq!(l.num_pieces(), 4); }
#[test]
fn piece_size_regular() {
let l = make_lengths();
assert_eq!(l.piece_size(0), 262_144);
assert_eq!(l.piece_size(1), 262_144);
assert_eq!(l.piece_size(3), 262_144); }
#[test]
fn piece_size_last_piece_shorter() {
let l = Lengths::new(1_000_000, 262_144, 16_384);
assert_eq!(l.piece_size(0), 262_144);
assert_eq!(l.piece_size(3), 1_000_000 - 3 * 262_144); }
#[test]
fn piece_size_out_of_bounds() {
let l = make_lengths();
assert_eq!(l.piece_size(4), 0);
assert_eq!(l.piece_size(100), 0);
}
#[test]
fn chunks_in_piece() {
let l = make_lengths();
assert_eq!(l.chunks_in_piece(0), 16); }
#[test]
fn chunks_in_last_piece() {
let l = Lengths::new(1_000_000, 262_144, 16_384);
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);
}
#[test]
fn chunk_info_regular() {
let l = make_lengths();
assert_eq!(l.chunk_info(0, 0), Some((0, 16384)));
assert_eq!(l.chunk_info(0, 1), Some((16384, 16384)));
assert_eq!(l.chunk_info(0, 15), Some((15 * 16384, 16384)));
}
#[test]
fn chunk_info_last_chunk_shorter() {
let l = Lengths::new(100_000, 50_000, 16_384);
assert_eq!(l.chunk_info(0, 3), Some((49152, 848)));
}
#[test]
fn chunk_info_out_of_bounds() {
let l = make_lengths();
assert_eq!(l.chunk_info(0, 16), None); assert_eq!(l.chunk_info(4, 0), None); }
#[test]
fn piece_offset() {
let l = make_lengths();
assert_eq!(l.piece_offset(0), 0);
assert_eq!(l.piece_offset(1), 262_144);
assert_eq!(l.piece_offset(3), 786_432);
}
#[test]
fn byte_to_piece_with_offset_basic() {
let l = make_lengths();
assert_eq!(l.byte_to_piece_with_offset(0), Some((0, 0)));
assert_eq!(l.byte_to_piece_with_offset(262_143), Some((0, 262_143)));
assert_eq!(l.byte_to_piece_with_offset(262_144), Some((1, 0)));
assert_eq!(l.byte_to_piece_with_offset(1_048_575), Some((3, 262_143)));
assert_eq!(l.byte_to_piece_with_offset(1_048_576), None); }
#[test]
fn piece_index_for_byte_at_zero() {
let l = make_lengths();
assert_eq!(l.piece_index_for_byte(0), Some(0));
}
#[test]
fn piece_index_for_byte_at_piece_boundary() {
let l = make_lengths();
assert_eq!(l.piece_index_for_byte(262_144), Some(1));
assert_eq!(l.piece_index_for_byte(262_143), Some(0));
}
#[test]
fn piece_index_for_byte_at_total_length_returns_none() {
let l = make_lengths();
assert_eq!(l.piece_index_for_byte(1_048_576), None);
assert_eq!(l.piece_index_for_byte(u64::MAX), None);
}
#[test]
fn piece_index_for_byte_matches_byte_to_piece_with_offset() {
let l = make_lengths();
for byte in [0, 1, 262_143, 262_144, 524_287, 524_288, 1_048_575] {
let single = l.piece_index_for_byte(byte);
let pair = l.byte_to_piece_with_offset(byte).map(|(p, _)| p);
assert_eq!(single, pair, "disagreement at byte {byte}");
}
assert_eq!(l.piece_index_for_byte(1_048_576), None);
assert_eq!(l.byte_to_piece_with_offset(1_048_576), None);
}
#[test]
fn piece_index_for_byte_uneven_last_piece() {
let l = Lengths::new(1_048_575, 262_144, 16_384);
assert_eq!(l.piece_index_for_byte(1_048_574), Some(3));
assert_eq!(l.piece_index_for_byte(1_048_575), None);
}
#[test]
fn file_pieces_spanning() {
let l = make_lengths();
assert_eq!(l.file_pieces(100_000, 500_000), Some((0, 2)));
}
#[test]
fn file_pieces_single_piece() {
let l = make_lengths();
assert_eq!(l.file_pieces(262_144, 100), Some((1, 1)));
}
#[test]
fn file_pieces_entire_torrent() {
let l = make_lengths();
assert_eq!(l.file_pieces(0, 1_048_576), Some((0, 3)));
}
#[test]
fn zero_length_torrent() {
let l = Lengths::new(0, 262_144, 16_384);
assert_eq!(l.num_pieces(), 0);
}
#[test]
fn tiny_torrent() {
let l = Lengths::new(1, 262_144, 16_384);
assert_eq!(l.num_pieces(), 1);
assert_eq!(l.piece_size(0), 1);
assert_eq!(l.chunks_in_piece(0), 1);
assert_eq!(l.chunk_info(0, 0), Some((0, 1)));
}
}