#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "M205: piece/file counts bounded by torrent metadata (u32 pieces, practical file counts)"
)]
use std::path::{Path, PathBuf};
pub struct ScanResult {
pub candidates: Vec<bool>,
pub candidate_count: u32,
pub total_pieces: u32,
pub files_found: u32,
pub files_total: u32,
}
#[must_use]
pub fn quick_file_scan(
file_infos: &[irontide_core::FileInfo],
piece_length: u64,
total_pieces: u32,
save_path: &Path,
) -> ScanResult {
if total_pieces == 0 || file_infos.is_empty() || piece_length == 0 {
return ScanResult {
candidates: vec![false; total_pieces as usize],
candidate_count: 0,
total_pieces,
files_found: 0,
files_total: file_infos.len() as u32,
};
}
let mut file_ok = Vec::with_capacity(file_infos.len());
let mut files_found = 0u32;
for fi in file_infos {
let rel_path: PathBuf = fi.path.iter().collect();
let full_path = save_path.join(&rel_path);
let ok = match std::fs::metadata(&full_path) {
Ok(m) => {
let size_ok = m.len() >= fi.length;
if size_ok {
files_found += 1;
}
size_ok
}
Err(_) => false,
};
file_ok.push(ok);
}
let mut candidates = vec![true; total_pieces as usize];
let mut offset = 0u64;
for (file_idx, fi) in file_infos.iter().enumerate() {
if !file_ok[file_idx] {
let first_piece = offset / piece_length;
let last_byte = offset + fi.length.saturating_sub(1);
let last_piece = if fi.length == 0 {
first_piece
} else {
last_byte / piece_length
};
for p in first_piece..=last_piece {
if (p as usize) < candidates.len() {
candidates[p as usize] = false;
}
}
}
offset += fi.length;
}
let candidate_count = candidates.iter().filter(|&&c| c).count() as u32;
ScanResult {
candidates,
candidate_count,
total_pieces,
files_found,
files_total: file_infos.len() as u32,
}
}
#[cfg(test)]
mod tests {
use super::*;
use irontide_core::FileInfo;
use std::fs;
fn make_file_info(path: &[&str], length: u64) -> FileInfo {
FileInfo {
path: path.iter().map(|s| (*s).to_string()).collect(),
length,
}
}
#[test]
fn empty_torrent() {
let result = quick_file_scan(&[], 256_000, 0, Path::new("/tmp"));
assert_eq!(result.candidate_count, 0);
assert_eq!(result.total_pieces, 0);
}
#[test]
fn all_files_present() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("test");
fs::create_dir_all(&sub).unwrap();
fs::write(sub.join("a.txt"), vec![0u8; 1000]).unwrap();
fs::write(sub.join("b.txt"), vec![0u8; 500]).unwrap();
let files = vec![
make_file_info(&["test", "a.txt"], 1000),
make_file_info(&["test", "b.txt"], 500),
];
let result = quick_file_scan(&files, 1000, 2, dir.path());
assert_eq!(result.files_found, 2);
assert_eq!(result.candidate_count, 2);
assert!(result.candidates[0]);
assert!(result.candidates[1]);
}
#[test]
fn missing_file_marks_pieces_not_candidate() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("test");
fs::create_dir_all(&sub).unwrap();
fs::write(sub.join("a.txt"), vec![0u8; 1000]).unwrap();
let files = vec![
make_file_info(&["test", "a.txt"], 1000),
make_file_info(&["test", "b.txt"], 500),
];
let result = quick_file_scan(&files, 1000, 2, dir.path());
assert_eq!(result.files_found, 1);
assert!(result.candidates[0]);
assert!(!result.candidates[1]);
}
#[test]
fn short_file_not_candidate() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("test");
fs::create_dir_all(&sub).unwrap();
fs::write(sub.join("a.txt"), vec![0u8; 500]).unwrap();
let files = vec![make_file_info(&["test", "a.txt"], 1000)];
let result = quick_file_scan(&files, 1000, 1, dir.path());
assert_eq!(result.files_found, 0);
assert!(!result.candidates[0]);
}
#[test]
fn multi_file_piece_boundary() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("test");
fs::create_dir_all(&sub).unwrap();
fs::write(sub.join("a.txt"), vec![0u8; 600]).unwrap();
fs::write(sub.join("b.txt"), vec![0u8; 400]).unwrap();
let files = vec![
make_file_info(&["test", "a.txt"], 600),
make_file_info(&["test", "b.txt"], 400),
];
let result = quick_file_scan(&files, 500, 2, dir.path());
assert_eq!(result.files_found, 2);
assert_eq!(result.candidate_count, 2);
}
#[test]
fn missing_middle_file_invalidates_spanning_pieces() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("test");
fs::create_dir_all(&sub).unwrap();
fs::write(sub.join("a.txt"), vec![0u8; 1000]).unwrap();
fs::write(sub.join("c.txt"), vec![0u8; 1000]).unwrap();
let files = vec![
make_file_info(&["test", "a.txt"], 1000),
make_file_info(&["test", "b.txt"], 1000),
make_file_info(&["test", "c.txt"], 1000),
];
let result = quick_file_scan(&files, 1000, 3, dir.path());
assert_eq!(result.files_found, 2);
assert!(result.candidates[0]);
assert!(!result.candidates[1]);
assert!(result.candidates[2]);
}
#[test]
fn zero_length_file_counted_as_found() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("test");
fs::create_dir_all(&sub).unwrap();
fs::write(sub.join("a.txt"), vec![0u8; 1000]).unwrap();
fs::write(sub.join("empty.txt"), b"").unwrap();
let files = vec![
make_file_info(&["test", "a.txt"], 1000),
make_file_info(&["test", "empty.txt"], 0),
];
let result = quick_file_scan(&files, 1000, 1, dir.path());
assert_eq!(result.files_found, 2);
assert!(result.candidates[0]);
}
#[test]
fn no_files_on_disk() {
let dir = tempfile::tempdir().unwrap();
let files = vec![
make_file_info(&["test", "a.txt"], 1000),
make_file_info(&["test", "b.txt"], 500),
];
let result = quick_file_scan(&files, 1000, 2, dir.path());
assert_eq!(result.files_found, 0);
assert_eq!(result.candidate_count, 0);
}
#[test]
fn single_file_torrent() {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("movie.mkv"), vec![0u8; 5000]).unwrap();
let files = vec![make_file_info(&["movie.mkv"], 5000)];
let result = quick_file_scan(&files, 1000, 5, dir.path());
assert_eq!(result.files_found, 1);
assert_eq!(result.candidate_count, 5);
}
#[test]
fn scan_result_counts() {
let dir = tempfile::tempdir().unwrap();
let files = vec![make_file_info(&["a"], 100), make_file_info(&["b"], 100)];
let result = quick_file_scan(&files, 100, 2, dir.path());
assert_eq!(result.total_pieces, 2);
assert_eq!(result.files_total, 2);
}
}