use std::{ops::Range, path::PathBuf};
use crate::{metainfo::Metainfo, FileIndex, PieceIndex};
/// Information about a torrent's file.
#[derive(Clone, Debug)]
pub struct FileInfo {
/// The file's relative path from the download directory.
pub path: PathBuf,
/// The file's length, in bytes.
pub len: u64,
/// The byte offset of the file within the torrent, when all files in
/// torrent are viewed as a single contiguous byte array. This is always
/// 0 for a single file torrent.
pub torrent_offset: u64,
}
impl FileInfo {
/// Returns a range that represents the file's first and one past the last
/// bytes' offsets in the torrent.
pub fn byte_range(&self) -> Range<u64> {
self.torrent_offset..self.torrent_end_offset()
}
/// Returns the file's one past the last byte's offset in the torrent.
pub fn torrent_end_offset(&self) -> u64 {
self.torrent_offset + self.len
}
/// Returns the slice in file that overlaps with the range starting at the
/// given offset.
///
/// # Arguments
///
/// * `torrent_offset` - A byte offset in the entire torrent.
/// * `len` - The length of the byte range, starting from the offset. This
/// may exceed the file length, in which case the returned file
/// length will be smaller.
///
/// # Panics
///
/// This will panic if `torrent_offset` is smaller than the file's offset in
/// torrent, or if it's past the last byte in file.
pub fn get_slice(&self, torrent_offset: u64, len: u64) -> FileSlice {
assert!(
torrent_offset >= self.torrent_offset,
"torrent offset must be larger than file offset",
);
let torrent_end_offset = self.torrent_end_offset();
assert!(
torrent_offset < torrent_end_offset,
"torrent offset must be smaller than file end offset",
);
FileSlice {
offset: torrent_offset - self.torrent_offset,
len: len.min(torrent_end_offset - torrent_offset),
}
}
}
/// Represents the location of a range of bytes within a file.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FileSlice {
/// The byte offset in file, relative to the file's start.
pub offset: u64,
/// The length of the slice, in bytes.
pub len: u64,
}
/// Information about a torrent's storage details, such as the piece count and
/// length, download length, etc.
#[derive(Clone, Debug)]
pub struct StorageInfo {
/// The number of pieces in the torrent.
pub piece_count: usize,
/// The nominal length of a piece.
pub piece_len: u32,
/// The length of the last piece in torrent, which may differ from the
/// normal piece length if the download size is not an exact multiple of the
/// piece length.
pub last_piece_len: u32,
/// The sum of the length of all files in the torrent.
// TODO: consider renaming to `torrent_len` to better reflect that the
// torrent may already be downloaded
pub download_len: u64,
/// The download destination directory of the torrent.
///
/// In case of single file downloads, this is the directory where the file
/// is downloaded, named as the torrent.
/// In case of archive downloads, this directory is the download directory
/// joined by the torrent's name. This is because in case of a torrent that
/// has multiple top-level entries, the downloaded files would be scattered
/// across the download directory, which is an annoyance we want to avoid.
/// E.g. downloading files into ~/Downloads/<torrent> instead of just
/// ~/Downloads.
pub download_dir: PathBuf,
/// All files in torrent.
pub files: Vec<FileInfo>,
}
impl StorageInfo {
/// Extracts storage related information from the torrent metainfo.
pub fn new(metainfo: &Metainfo, download_dir: PathBuf) -> Self {
let piece_count = metainfo.piece_count();
let download_len = metainfo.download_len();
let piece_len = metainfo.piece_len;
let last_piece_len =
download_len - piece_len as u64 * (piece_count - 1) as u64;
let last_piece_len = last_piece_len as u32;
// if this is an archive, download files into torrent's own dir
let download_dir = if metainfo.is_archive() {
download_dir.join(&metainfo.name)
} else {
download_dir
};
Self {
piece_count,
piece_len,
last_piece_len,
download_len,
download_dir,
files: metainfo.files.clone(),
}
}
/// Returns the zero-based indices of the files of torrent that intersect
/// with the piece.
///
/// # Panics
///
/// Panics if the piece index is invalid. Validation must happen at the
/// protocol level. The internals of the engine work on the assumption that
/// piece indices are valid.
pub fn files_intersecting_piece(
&self,
index: PieceIndex,
) -> Range<FileIndex> {
log::trace!("Returning files intersecting piece {}", index);
let piece_offset = index as u64 * self.piece_len as u64;
let piece_end = piece_offset + self.piece_len(index) as u64;
let files = self.files_intersecting_bytes(piece_offset..piece_end);
files
}
/// Returns the files that overlap with the given left-inclusive range of
/// bytes, where `bytes.start` is the offset and `bytes.end` is one past the
/// last byte offset.
pub fn files_intersecting_bytes(
&self,
byte_range: Range<u64>,
) -> Range<FileIndex> {
debug_assert_ne!(self.files.len(), 0);
if self.files.len() == 1 {
// when torrent only has one file, only that file can be returned
//
// TODO: consider whether to return an error, an empty range, panic,
// or do a noop if the range is invalid (outside the first or last
// byte offsets of our file)
0..1
} else {
// find the index of the first file that contains the first byte
// of the range
let first_matching_index = match self
.files
.iter()
.enumerate()
.find(|(_, file)| {
// check if the file's byte range contains the first
// byte of the range
file.byte_range().contains(&byte_range.start)
})
.map(|(index, _)| index)
{
Some(index) => index,
None => return 0..0,
};
// the resulting files
let mut file_range = first_matching_index..first_matching_index + 1;
// Find the the last file that contains the last byte of the
// range, starting at the file after the above found one.
//
// NOTE: the order of `enumerate` and `skip` matters as
// otherwise we'd be getting relative indices
for (index, file) in
self.files.iter().enumerate().skip(first_matching_index + 1)
{
// stop if file's first byte is not contained by the byte
// range (is at or past the end of the byte range we're
// looking for)
if !byte_range.contains(&file.torrent_offset) {
break;
}
// note that we need to add one to the end as this is
// a left-inclusive range, so we want the end (excluded) to
// be one past the actually included value
file_range.end = index + 1;
}
file_range
}
}
/// Returns the piece's absolute offset in the torrent.
pub fn torrent_piece_offset(&self, index: PieceIndex) -> u64 {
index as u64 * self.piece_len as u64
}
/// Returns the length of the piece at the given index.
///
/// # Panics
///
/// Panics if the piece index is invalid. Validation must happen at the
/// protocol level. The internals of the engine work on the assumption that
/// piece indices are valid.
#[allow(clippy::comparison_chain)]
pub fn piece_len(&self, index: PieceIndex) -> u32 {
assert!(index < self.piece_count, "piece index out of range");
if index == self.piece_count - 1 {
self.last_piece_len
} else {
self.piece_len
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_get_slice() {
let file = FileInfo {
// file doesn't need to exist as we're not doing any IO in this test
path: PathBuf::from("/tmp/does/not/exist"),
len: 500,
torrent_offset: 200,
};
assert_eq!(
file.get_slice(300, 1000),
FileSlice {
offset: 300 - 200,
len: 500 - (300 - 200),
},
"file slice for byte range longer than file should return \
at most file length long slice"
);
assert_eq!(
file.get_slice(300, 10),
FileSlice {
offset: 300 - 200,
len: 10,
},
"file slice for byte range smaller than file should return \
at most byte range long slice"
);
assert_eq!(
file.get_slice(200, 500),
FileSlice {
offset: 0,
len: 500,
},
"file slice for byte range equal to file length should return \
the full file slice"
);
}
#[test]
#[should_panic(expected = "torrent offset must be larger than file offset")]
fn test_file_get_slice_starting_before_file() {
let file = FileInfo {
// file doesn't need to exist as we're not doing any IO in this test
path: PathBuf::from("/tmp/does/not/exist"),
len: 500,
torrent_offset: 200,
};
// we can't query a file slace for a byte range starting before the file
file.get_slice(100, 400);
}
#[test]
#[should_panic(
expected = "torrent offset must be smaller than file end offset"
)]
fn test_file_get_slice_starting_after_file() {
let file = FileInfo {
// file doesn't need to exist as we're not doing any IO in this test
path: PathBuf::from("/tmp/does/not/exist"),
len: 500,
torrent_offset: 200,
};
// we can't query a file slace for a byte range starting before the file
file.get_slice(200 + 500, 400);
}
#[test]
fn test_files_intersecting_pieces() {
// single file
let piece_count = 4;
let piece_len = 4;
let last_piece_len = 2;
// 3 full length pieces; 1 smaller piece,
let download_len = 3 * 4 + 2;
let files = vec![FileInfo {
path: PathBuf::from("/bogus"),
torrent_offset: 0,
len: download_len,
}];
let info = StorageInfo {
piece_count,
piece_len,
last_piece_len,
download_len,
download_dir: PathBuf::from("/"),
files,
};
// all 4 pieces are in the same file
assert_eq!(info.files_intersecting_piece(0), 0..1);
assert_eq!(info.files_intersecting_piece(1), 0..1);
assert_eq!(info.files_intersecting_piece(2), 0..1);
assert_eq!(info.files_intersecting_piece(3), 0..1);
// multi-file
//
// pieces: (index:first byte offset)
// --------------------------------------------------------------------
// |0:0 |1:16 |2:32 |3:48 |4:64 |
// --------------------------------------------------------------------
// files: (index:first byte offset,last byte offset)
// --------------------------------------------------------------------
// |0:0,8 |1:9,19 |2:20,26|3:27,35 |4:36,47 |5:48,63 |6:64,71 |
// --------------------------------------------------------------------
let files = vec![
FileInfo {
path: PathBuf::from("/0"),
torrent_offset: 0,
len: 9,
},
FileInfo {
path: PathBuf::from("/1"),
torrent_offset: 9,
len: 11,
},
FileInfo {
path: PathBuf::from("/2"),
torrent_offset: 20,
len: 7,
},
FileInfo {
path: PathBuf::from("/3"),
torrent_offset: 27,
len: 9,
},
FileInfo {
path: PathBuf::from("/4"),
torrent_offset: 36,
len: 12,
},
FileInfo {
path: PathBuf::from("/5"),
torrent_offset: 48,
len: 16,
},
FileInfo {
path: PathBuf::from("/6"),
torrent_offset: 64,
len: 8,
},
];
let download_len: u64 = files.iter().map(|f| f.len).sum();
// sanity check that the offsets in the files above correctly follow
// each other and that they add up to the total download length
debug_assert_eq!(
files.iter().fold(0, |offset, file| {
debug_assert_eq!(offset, file.torrent_offset);
offset + file.len
}),
download_len,
);
let piece_count: usize = 5;
let piece_len: u32 = 16;
let last_piece_len: u32 = 8;
// sanity check that full piece lengths and last piece length equals the
// total download length
debug_assert_eq!(
(piece_count as u64 - 1) * piece_len as u64 + last_piece_len as u64,
download_len
);
let info = StorageInfo {
piece_count,
piece_len,
last_piece_len,
download_len,
download_dir: PathBuf::from("/"),
files,
};
// piece 0 intersects with files 0 and 1
assert_eq!(info.files_intersecting_piece(0), 0..2);
// piece 1 intersects with files 1, 2, 3
assert_eq!(info.files_intersecting_piece(1), 1..4);
// piece 2 intersects with files 3 and 4
assert_eq!(info.files_intersecting_piece(2), 3..5);
// piece 3 intersects with only file 5
assert_eq!(info.files_intersecting_piece(3), 5..6);
// last piece 4 intersects with only file 6
assert_eq!(info.files_intersecting_piece(4), 6..7);
}
#[test]
fn test_files_intersecting_bytes() {
let download_len = 12341234;
let files = vec![FileInfo {
path: PathBuf::from("/bogus"),
torrent_offset: 0,
len: download_len,
}];
let info = StorageInfo {
// arbitrary piece info (not used in this test)
piece_count: 4,
piece_len: 4,
last_piece_len: 2,
download_len,
download_dir: PathBuf::from("/"),
files,
};
assert_eq!(info.files_intersecting_bytes(0..0), 0..1);
assert_eq!(info.files_intersecting_bytes(0..1), 0..1);
assert_eq!(info.files_intersecting_bytes(0..12341234), 0..1);
// multi-file
let files = vec![
FileInfo {
path: PathBuf::from("/bogus0"),
torrent_offset: 0,
len: 4,
},
FileInfo {
path: PathBuf::from("/bogus1"),
torrent_offset: 4,
len: 9,
},
FileInfo {
path: PathBuf::from("/bogus2"),
torrent_offset: 13,
len: 3,
},
FileInfo {
path: PathBuf::from("/bogus3"),
torrent_offset: 16,
len: 10,
},
];
let download_len = files.iter().map(|f| f.len).sum();
let info = StorageInfo {
// arbitrary piece info (not used in this test)
piece_count: 4,
piece_len: 4,
last_piece_len: 2,
download_len,
download_dir: PathBuf::from("/"),
files,
};
// bytes only in the first file
assert_eq!(info.files_intersecting_bytes(0..4), 0..1);
// bytes intersecting two files
assert_eq!(info.files_intersecting_bytes(0..5), 0..2);
// bytes overlapping with two files
assert_eq!(info.files_intersecting_bytes(0..13), 0..2);
// bytes intersecting three files
assert_eq!(info.files_intersecting_bytes(0..15), 0..3);
// bytes intersecting all files
assert_eq!(info.files_intersecting_bytes(0..18), 0..4);
// bytes intersecting the last byte of the last file
assert_eq!(info.files_intersecting_bytes(25..26), 3..4);
// bytes overlapping with two files in the middle
assert_eq!(info.files_intersecting_bytes(4..16), 1..3);
// bytes intersecting only one byte of two files each, among the middle
// of all files
assert_eq!(info.files_intersecting_bytes(8..14), 1..3);
// bytes intersecting only one byte of one file, among the middle of all
// files
assert_eq!(info.files_intersecting_bytes(13..14), 2..3);
// bytes not intersecting any files
assert_eq!(info.files_intersecting_bytes(30..38), 0..0);
}
}