1#![allow(
2 clippy::cast_possible_truncation,
3 clippy::cast_possible_wrap,
4 clippy::cast_sign_loss,
5 reason = "M175: BEP 52 file tree — piece counts bounded by torrent size"
6)]
7
8use std::collections::BTreeMap;
16
17use irontide_bencode::BencodeValue;
18
19use crate::error::Error;
20use crate::hash::Id32;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct V2FileAttr {
25 pub length: u64,
27 pub pieces_root: Option<Id32>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum FileTreeNode {
34 File(V2FileAttr),
36 Directory(BTreeMap<String, Self>),
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct V2FileInfo {
43 pub path: Vec<String>,
45 pub attr: V2FileAttr,
47}
48
49impl FileTreeNode {
50 pub fn from_bencode(value: &BencodeValue) -> Result<Self, Error> {
60 let dict = value
61 .as_dict()
62 .ok_or_else(|| Error::InvalidTorrent("file tree node must be a dict".into()))?;
63
64 if let Some(attr_value) = dict.get(b"".as_ref()) {
66 let attr = parse_file_attr(attr_value)?;
67 return Ok(Self::File(attr));
68 }
69
70 let mut children = BTreeMap::new();
72 for (key, child_value) in dict {
73 let name = String::from_utf8(key.clone())
74 .map_err(|_| Error::InvalidTorrent("file tree key is not valid UTF-8".into()))?;
75 let child = Self::from_bencode(child_value)?;
76 children.insert(name, child);
77 }
78
79 Ok(Self::Directory(children))
80 }
81
82 #[must_use]
84 pub fn flatten(&self) -> Vec<V2FileInfo> {
85 let mut result = Vec::new();
86 self.flatten_into(&mut result, &mut Vec::new());
87 result
88 }
89
90 fn flatten_into(&self, result: &mut Vec<V2FileInfo>, path: &mut Vec<String>) {
91 match self {
92 Self::File(attr) => {
93 result.push(V2FileInfo {
94 path: path.clone(),
95 attr: attr.clone(),
96 });
97 }
98 Self::Directory(children) => {
99 for (name, child) in children {
100 path.push(name.clone());
101 child.flatten_into(result, path);
102 path.pop();
103 }
104 }
105 }
106 }
107
108 #[must_use]
112 pub fn to_bencode(&self) -> BencodeValue {
113 match self {
114 Self::File(attr) => {
115 let mut file_dict = BTreeMap::new();
116 file_dict.insert(
117 b"length".to_vec(),
118 BencodeValue::Integer(attr.length as i64),
119 );
120 if let Some(root) = &attr.pieces_root {
121 file_dict.insert(
122 b"pieces root".to_vec(),
123 BencodeValue::Bytes(root.as_bytes().to_vec()),
124 );
125 }
126 let mut node = BTreeMap::new();
128 node.insert(b"".to_vec(), BencodeValue::Dict(file_dict));
129 BencodeValue::Dict(node)
130 }
131 Self::Directory(children) => {
132 let mut dict = BTreeMap::new();
133 for (name, child) in children {
134 dict.insert(name.as_bytes().to_vec(), child.to_bencode());
135 }
136 BencodeValue::Dict(dict)
137 }
138 }
139 }
140}
141
142fn parse_file_attr(value: &BencodeValue) -> Result<V2FileAttr, Error> {
144 let dict = value
145 .as_dict()
146 .ok_or_else(|| Error::InvalidTorrent("file attr must be a dict".into()))?;
147
148 let length = dict
149 .get(b"length".as_ref())
150 .and_then(irontide_bencode::BencodeValue::as_int)
151 .ok_or_else(|| Error::InvalidTorrent("file attr missing 'length'".into()))?;
152
153 if length < 0 {
154 return Err(Error::InvalidTorrent(format!(
155 "file attr has negative length: {length}"
156 )));
157 }
158
159 let pieces_root = if let Some(root_val) = dict.get(b"pieces root".as_ref()) {
160 let bytes = root_val
161 .as_bytes_raw()
162 .ok_or_else(|| Error::InvalidTorrent("pieces root must be bytes".into()))?;
163 Some(Id32::from_bytes(bytes)?)
164 } else {
165 None
166 };
167
168 Ok(V2FileAttr {
169 length: length as u64,
170 pieces_root,
171 })
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 fn bdict(pairs: Vec<(&[u8], BencodeValue)>) -> BencodeValue {
180 let mut map = BTreeMap::new();
181 for (k, v) in pairs {
182 map.insert(k.to_vec(), v);
183 }
184 BencodeValue::Dict(map)
185 }
186
187 fn bint(v: i64) -> BencodeValue {
188 BencodeValue::Integer(v)
189 }
190
191 fn bbytes(v: &[u8]) -> BencodeValue {
192 BencodeValue::Bytes(v.to_vec())
193 }
194
195 #[test]
196 fn single_file() {
197 let root_hash = [0xABu8; 32];
199 let tree = bdict(vec![(
200 b"test.txt",
201 bdict(vec![(
202 b"",
203 bdict(vec![
204 (b"length", bint(1024)),
205 (b"pieces root", bbytes(&root_hash)),
206 ]),
207 )]),
208 )]);
209
210 let node = FileTreeNode::from_bencode(&tree).unwrap();
211 let files = node.flatten();
212 assert_eq!(files.len(), 1);
213 assert_eq!(files[0].path, vec!["test.txt"]);
214 assert_eq!(files[0].attr.length, 1024);
215 assert_eq!(files[0].attr.pieces_root, Some(Id32(root_hash)));
216 }
217
218 #[test]
219 fn nested_directory() {
220 let tree = bdict(vec![(
222 b"dir",
223 bdict(vec![(
224 b"subfile.dat",
225 bdict(vec![(b"", bdict(vec![(b"length", bint(512))]))]),
226 )]),
227 )]);
228
229 let node = FileTreeNode::from_bencode(&tree).unwrap();
230 let files = node.flatten();
231 assert_eq!(files.len(), 1);
232 assert_eq!(files[0].path, vec!["dir", "subfile.dat"]);
233 assert_eq!(files[0].attr.length, 512);
234 assert_eq!(files[0].attr.pieces_root, None);
235 }
236
237 #[test]
238 fn multiple_files_btreemap_ordering() {
239 let tree = bdict(vec![
241 (
242 b"beta.txt",
243 bdict(vec![(b"", bdict(vec![(b"length", bint(200))]))]),
244 ),
245 (
246 b"alpha.txt",
247 bdict(vec![(b"", bdict(vec![(b"length", bint(100))]))]),
248 ),
249 ]);
250
251 let node = FileTreeNode::from_bencode(&tree).unwrap();
252 let files = node.flatten();
253 assert_eq!(files.len(), 2);
254 assert_eq!(files[0].path, vec!["alpha.txt"]);
256 assert_eq!(files[1].path, vec!["beta.txt"]);
257 }
258
259 #[test]
260 fn reject_missing_length() {
261 let tree = bdict(vec![(
263 b"bad.txt",
264 bdict(vec![(
265 b"",
266 bdict(vec![(b"pieces root", bbytes(&[0u8; 32]))]),
267 )]),
268 )]);
269
270 assert!(FileTreeNode::from_bencode(&tree).is_err());
271 }
272
273 #[test]
274 fn reject_non_dict() {
275 let value = BencodeValue::Integer(42);
276 assert!(FileTreeNode::from_bencode(&value).is_err());
277 }
278
279 #[test]
280 fn empty_file_no_pieces_root() {
281 let tree = bdict(vec![(
283 b"empty.txt",
284 bdict(vec![(b"", bdict(vec![(b"length", bint(0))]))]),
285 )]);
286
287 let node = FileTreeNode::from_bencode(&tree).unwrap();
288 let files = node.flatten();
289 assert_eq!(files.len(), 1);
290 assert_eq!(files[0].attr.length, 0);
291 assert_eq!(files[0].attr.pieces_root, None);
292 }
293}