1#![allow(
2 clippy::cast_possible_truncation,
3 clippy::cast_sign_loss,
4 reason = "M205: piece/file counts bounded by torrent metadata (u32 pieces, practical file counts)"
5)]
6
7use std::path::{Path, PathBuf};
17
18pub struct ScanResult {
20 pub candidates: Vec<bool>,
22 pub candidate_count: u32,
24 pub total_pieces: u32,
26 pub files_found: u32,
28 pub files_total: u32,
30}
31
32#[must_use]
34pub fn quick_file_scan(
35 file_infos: &[irontide_core::FileInfo],
36 piece_length: u64,
37 total_pieces: u32,
38 save_path: &Path,
39) -> ScanResult {
40 if total_pieces == 0 || file_infos.is_empty() || piece_length == 0 {
41 return ScanResult {
42 candidates: vec![false; total_pieces as usize],
43 candidate_count: 0,
44 total_pieces,
45 files_found: 0,
46 files_total: file_infos.len() as u32,
47 };
48 }
49
50 let mut file_ok = Vec::with_capacity(file_infos.len());
51 let mut files_found = 0u32;
52
53 for fi in file_infos {
54 let rel_path: PathBuf = fi.path.iter().collect();
55 let full_path = save_path.join(&rel_path);
56 let ok = match std::fs::metadata(&full_path) {
57 Ok(m) => {
58 let size_ok = m.len() >= fi.length;
59 if size_ok {
60 files_found += 1;
61 }
62 size_ok
63 }
64 Err(_) => false,
65 };
66 file_ok.push(ok);
67 }
68
69 let mut candidates = vec![true; total_pieces as usize];
70 let mut offset = 0u64;
71
72 for (file_idx, fi) in file_infos.iter().enumerate() {
73 if !file_ok[file_idx] {
74 let first_piece = offset / piece_length;
75 let last_byte = offset + fi.length.saturating_sub(1);
76 let last_piece = if fi.length == 0 {
77 first_piece
78 } else {
79 last_byte / piece_length
80 };
81
82 for p in first_piece..=last_piece {
83 if (p as usize) < candidates.len() {
84 candidates[p as usize] = false;
85 }
86 }
87 }
88 offset += fi.length;
89 }
90
91 let candidate_count = candidates.iter().filter(|&&c| c).count() as u32;
92
93 ScanResult {
94 candidates,
95 candidate_count,
96 total_pieces,
97 files_found,
98 files_total: file_infos.len() as u32,
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use irontide_core::FileInfo;
106 use std::fs;
107
108 fn make_file_info(path: &[&str], length: u64) -> FileInfo {
109 FileInfo {
110 path: path.iter().map(|s| (*s).to_string()).collect(),
111 length,
112 }
113 }
114
115 #[test]
116 fn empty_torrent() {
117 let result = quick_file_scan(&[], 256_000, 0, Path::new("/tmp"));
118 assert_eq!(result.candidate_count, 0);
119 assert_eq!(result.total_pieces, 0);
120 }
121
122 #[test]
123 fn all_files_present() {
124 let dir = tempfile::tempdir().unwrap();
125 let sub = dir.path().join("test");
126 fs::create_dir_all(&sub).unwrap();
127 fs::write(sub.join("a.txt"), vec![0u8; 1000]).unwrap();
128 fs::write(sub.join("b.txt"), vec![0u8; 500]).unwrap();
129
130 let files = vec![
131 make_file_info(&["test", "a.txt"], 1000),
132 make_file_info(&["test", "b.txt"], 500),
133 ];
134
135 let result = quick_file_scan(&files, 1000, 2, dir.path());
136 assert_eq!(result.files_found, 2);
137 assert_eq!(result.candidate_count, 2);
138 assert!(result.candidates[0]);
139 assert!(result.candidates[1]);
140 }
141
142 #[test]
143 fn missing_file_marks_pieces_not_candidate() {
144 let dir = tempfile::tempdir().unwrap();
145 let sub = dir.path().join("test");
146 fs::create_dir_all(&sub).unwrap();
147 fs::write(sub.join("a.txt"), vec![0u8; 1000]).unwrap();
148
149 let files = vec![
150 make_file_info(&["test", "a.txt"], 1000),
151 make_file_info(&["test", "b.txt"], 500),
152 ];
153
154 let result = quick_file_scan(&files, 1000, 2, dir.path());
155 assert_eq!(result.files_found, 1);
156 assert!(result.candidates[0]);
157 assert!(!result.candidates[1]);
158 }
159
160 #[test]
161 fn short_file_not_candidate() {
162 let dir = tempfile::tempdir().unwrap();
163 let sub = dir.path().join("test");
164 fs::create_dir_all(&sub).unwrap();
165 fs::write(sub.join("a.txt"), vec![0u8; 500]).unwrap();
166
167 let files = vec![make_file_info(&["test", "a.txt"], 1000)];
168
169 let result = quick_file_scan(&files, 1000, 1, dir.path());
170 assert_eq!(result.files_found, 0);
171 assert!(!result.candidates[0]);
172 }
173
174 #[test]
175 fn multi_file_piece_boundary() {
176 let dir = tempfile::tempdir().unwrap();
177 let sub = dir.path().join("test");
178 fs::create_dir_all(&sub).unwrap();
179 fs::write(sub.join("a.txt"), vec![0u8; 600]).unwrap();
180 fs::write(sub.join("b.txt"), vec![0u8; 400]).unwrap();
181
182 let files = vec![
183 make_file_info(&["test", "a.txt"], 600),
184 make_file_info(&["test", "b.txt"], 400),
185 ];
186
187 let result = quick_file_scan(&files, 500, 2, dir.path());
188 assert_eq!(result.files_found, 2);
189 assert_eq!(result.candidate_count, 2);
190 }
191
192 #[test]
193 fn missing_middle_file_invalidates_spanning_pieces() {
194 let dir = tempfile::tempdir().unwrap();
195 let sub = dir.path().join("test");
196 fs::create_dir_all(&sub).unwrap();
197 fs::write(sub.join("a.txt"), vec![0u8; 1000]).unwrap();
198 fs::write(sub.join("c.txt"), vec![0u8; 1000]).unwrap();
199
200 let files = vec![
201 make_file_info(&["test", "a.txt"], 1000),
202 make_file_info(&["test", "b.txt"], 1000),
203 make_file_info(&["test", "c.txt"], 1000),
204 ];
205
206 let result = quick_file_scan(&files, 1000, 3, dir.path());
207 assert_eq!(result.files_found, 2);
208 assert!(result.candidates[0]);
209 assert!(!result.candidates[1]);
210 assert!(result.candidates[2]);
211 }
212
213 #[test]
214 fn zero_length_file_counted_as_found() {
215 let dir = tempfile::tempdir().unwrap();
216 let sub = dir.path().join("test");
217 fs::create_dir_all(&sub).unwrap();
218 fs::write(sub.join("a.txt"), vec![0u8; 1000]).unwrap();
219 fs::write(sub.join("empty.txt"), b"").unwrap();
220
221 let files = vec![
222 make_file_info(&["test", "a.txt"], 1000),
223 make_file_info(&["test", "empty.txt"], 0),
224 ];
225
226 let result = quick_file_scan(&files, 1000, 1, dir.path());
227 assert_eq!(result.files_found, 2);
228 assert!(result.candidates[0]);
229 }
230
231 #[test]
232 fn no_files_on_disk() {
233 let dir = tempfile::tempdir().unwrap();
234 let files = vec![
235 make_file_info(&["test", "a.txt"], 1000),
236 make_file_info(&["test", "b.txt"], 500),
237 ];
238
239 let result = quick_file_scan(&files, 1000, 2, dir.path());
240 assert_eq!(result.files_found, 0);
241 assert_eq!(result.candidate_count, 0);
242 }
243
244 #[test]
245 fn single_file_torrent() {
246 let dir = tempfile::tempdir().unwrap();
247 fs::write(dir.path().join("movie.mkv"), vec![0u8; 5000]).unwrap();
248
249 let files = vec![make_file_info(&["movie.mkv"], 5000)];
250
251 let result = quick_file_scan(&files, 1000, 5, dir.path());
252 assert_eq!(result.files_found, 1);
253 assert_eq!(result.candidate_count, 5);
254 }
255
256 #[test]
257 fn scan_result_counts() {
258 let dir = tempfile::tempdir().unwrap();
259 let files = vec![make_file_info(&["a"], 100), make_file_info(&["b"], 100)];
260 let result = quick_file_scan(&files, 100, 2, dir.path());
261 assert_eq!(result.total_pieces, 2);
262 assert_eq!(result.files_total, 2);
263 }
264}