librqbit_core/
torrent_metainfo.rs

1use anyhow::Context;
2use bencode::BencodeDeserializer;
3use buffers::{ByteBuf, ByteBufOwned};
4use bytes::Bytes;
5use clone_to_owned::CloneToOwned;
6use itertools::Either;
7use serde::{Deserialize, Serialize};
8use std::{iter::once, path::PathBuf};
9use tracing::debug;
10
11use crate::{hash_id::Id20, lengths::Lengths};
12
13pub type TorrentMetaV1Borrowed<'a> = TorrentMetaV1<ByteBuf<'a>>;
14pub type TorrentMetaV1Owned = TorrentMetaV1<ByteBufOwned>;
15
16pub struct ParsedTorrent<BufType> {
17    /// The parsed torrent.
18    pub meta: TorrentMetaV1<BufType>,
19
20    /// The raw bytes of the torrent's "info" dict.
21    pub info_bytes: BufType,
22}
23
24/// Parse torrent metainfo from bytes (includes additional fields).
25pub fn torrent_from_bytes_ext<'de, BufType: Deserialize<'de> + From<&'de [u8]>>(
26    buf: &'de [u8],
27) -> anyhow::Result<ParsedTorrent<BufType>> {
28    let mut de = BencodeDeserializer::new_from_buf(buf);
29    de.is_torrent_info = true;
30    let mut t = TorrentMetaV1::deserialize(&mut de)?;
31    let (digest, info_bytes) = match (de.torrent_info_digest, de.torrent_info_bytes) {
32        (Some(digest), Some(info_bytes)) => (digest, info_bytes),
33        (o1, o2) => anyhow::bail!(
34            "programming error: digest.is_some()={}, info_bytes.is_some()={}. Probably one of bencode/sha1* features isn't enabled.",
35            o1.is_some(),
36            o2.is_some()
37        ),
38    };
39    t.info_hash = Id20::new(digest);
40    Ok(ParsedTorrent {
41        meta: t,
42        info_bytes: BufType::from(info_bytes),
43    })
44}
45
46/// Parse torrent metainfo from bytes.
47pub fn torrent_from_bytes<'de, BufType: Deserialize<'de> + From<&'de [u8]>>(
48    buf: &'de [u8],
49) -> anyhow::Result<TorrentMetaV1<BufType>> {
50    torrent_from_bytes_ext(buf).map(|r| r.meta)
51}
52
53fn is_false(b: &bool) -> bool {
54    !*b
55}
56
57/// A parsed .torrent file.
58#[derive(Serialize, Deserialize, Debug, Clone)]
59pub struct TorrentMetaV1<BufType> {
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub announce: Option<BufType>,
62    #[serde(
63        rename = "announce-list",
64        default = "Vec::new",
65        skip_serializing_if = "Vec::is_empty"
66    )]
67    pub announce_list: Vec<Vec<BufType>>,
68    pub info: TorrentMetaV1Info<BufType>,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub comment: Option<BufType>,
71    #[serde(rename = "created by", skip_serializing_if = "Option::is_none")]
72    pub created_by: Option<BufType>,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub encoding: Option<BufType>,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub publisher: Option<BufType>,
77    #[serde(rename = "publisher-url", skip_serializing_if = "Option::is_none")]
78    pub publisher_url: Option<BufType>,
79    #[serde(rename = "creation date", skip_serializing_if = "Option::is_none")]
80    pub creation_date: Option<usize>,
81
82    #[serde(skip)]
83    pub info_hash: Id20,
84}
85
86impl<BufType> TorrentMetaV1<BufType> {
87    pub fn iter_announce(&self) -> impl Iterator<Item = &BufType> {
88        if self.announce_list.iter().flatten().next().is_some() {
89            return itertools::Either::Left(self.announce_list.iter().flatten());
90        }
91        itertools::Either::Right(self.announce.iter())
92    }
93}
94
95/// Main torrent information, shared by .torrent files and magnet link contents.
96#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
97pub struct TorrentMetaV1Info<BufType> {
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub name: Option<BufType>,
100    pub pieces: BufType,
101    #[serde(rename = "piece length")]
102    pub piece_length: u32,
103
104    // Single-file mode
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub length: Option<u64>,
107    #[serde(default = "none", skip_serializing_if = "Option::is_none")]
108    pub attr: Option<BufType>,
109    #[serde(default = "none", skip_serializing_if = "Option::is_none")]
110    pub sha1: Option<BufType>,
111    #[serde(
112        default = "none",
113        rename = "symlink path",
114        skip_serializing_if = "Option::is_none"
115    )]
116    pub symlink_path: Option<Vec<BufType>>,
117
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub md5sum: Option<BufType>,
120
121    // Multi-file mode
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub files: Option<Vec<TorrentMetaV1File<BufType>>>,
124
125    #[serde(skip_serializing_if = "is_false", default)]
126    pub private: bool,
127}
128
129#[derive(Clone, Copy)]
130pub enum FileIteratorName<'a, BufType> {
131    Single(Option<&'a BufType>),
132    Tree(&'a [BufType]),
133}
134
135impl<BufType> std::fmt::Debug for FileIteratorName<'_, BufType>
136where
137    BufType: AsRef<[u8]>,
138{
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        match self.to_string() {
141            Ok(s) => write!(f, "{s:?}"),
142            Err(e) => write!(f, "<{e:?}>"),
143        }
144    }
145}
146
147impl<'a, BufType> FileIteratorName<'a, BufType>
148where
149    BufType: AsRef<[u8]>,
150{
151    pub fn to_vec(&self) -> anyhow::Result<Vec<String>> {
152        self.iter_components()
153            .map(|c| c.map(|s| s.to_owned()))
154            .collect()
155    }
156
157    pub fn to_string(&self) -> anyhow::Result<String> {
158        let mut buf = String::new();
159        for (idx, bit) in self.iter_components().enumerate() {
160            let bit = bit?;
161            if idx > 0 {
162                buf.push(std::path::MAIN_SEPARATOR);
163            }
164            buf.push_str(bit)
165        }
166        Ok(buf)
167    }
168    pub fn to_pathbuf(&self) -> anyhow::Result<PathBuf> {
169        let mut buf = PathBuf::new();
170        for bit in self.iter_components() {
171            let bit = bit?;
172            buf.push(bit)
173        }
174        Ok(buf)
175    }
176    pub fn iter_components(&self) -> impl Iterator<Item = anyhow::Result<&'a str>> {
177        let it = match self {
178            FileIteratorName::Single(None) => return Either::Left(once(Ok("torrent-content"))),
179            FileIteratorName::Single(Some(name)) => Either::Left(once((*name).as_ref())),
180            FileIteratorName::Tree(t) => Either::Right(t.iter().map(|bb| bb.as_ref())),
181        };
182        Either::Right(it.map(|part: &'a [u8]| {
183            let bit = std::str::from_utf8(part).context("cannot decode filename bit as UTF-8")?;
184            if bit == ".." {
185                anyhow::bail!("path traversal detected, \"..\" in filename bit {:?}", bit);
186            }
187            if bit.contains('/') || bit.contains('\\') {
188                anyhow::bail!("suspicios separator in filename bit {:?}", bit);
189            }
190            Ok(bit)
191        }))
192    }
193}
194
195#[derive(Serialize, Deserialize, Default, Debug, Clone, Copy)]
196pub struct FileDetailsAttrs {
197    pub symlink: bool,
198    pub hidden: bool,
199    pub padding: bool,
200    pub executable: bool,
201}
202
203pub struct FileDetails<'a, BufType> {
204    pub filename: FileIteratorName<'a, BufType>,
205    pub len: u64,
206
207    // bep-47
208    attr: Option<&'a BufType>,
209    pub sha1: Option<&'a BufType>,
210    pub symlink_path: Option<&'a [BufType]>,
211}
212
213impl<BufType> FileDetails<'_, BufType>
214where
215    BufType: AsRef<[u8]>,
216{
217    pub fn attrs(&self) -> FileDetailsAttrs {
218        let attrs = match self.attr {
219            Some(attrs) => attrs,
220            None => return FileDetailsAttrs::default(),
221        };
222        let mut result = FileDetailsAttrs::default();
223        for byte in attrs.as_ref().iter().copied() {
224            match byte {
225                b'l' => result.symlink = true,
226                b'h' => result.hidden = true,
227                b'p' => result.padding = true,
228                b'x' => result.executable = true,
229                other => debug!(attr = other, "unknown file attribute"),
230            }
231        }
232        result
233    }
234}
235
236pub struct FileDetailsExt<'a, BufType> {
237    pub details: FileDetails<'a, BufType>,
238    // absolute offset in torrent if it was a flat blob of bytes
239    pub offset: u64,
240
241    // the pieces that contain this file
242    pub pieces: std::ops::Range<u32>,
243}
244
245impl<BufType> FileDetailsExt<'_, BufType> {
246    pub fn pieces_usize(&self) -> std::ops::Range<usize> {
247        self.pieces.start as usize..self.pieces.end as usize
248    }
249}
250
251impl<BufType: AsRef<[u8]>> TorrentMetaV1Info<BufType> {
252    pub fn get_hash(&self, piece: u32) -> Option<&[u8]> {
253        let start = piece as usize * 20;
254        let end = start + 20;
255        let expected_hash = self.pieces.as_ref().get(start..end)?;
256        Some(expected_hash)
257    }
258
259    pub fn compare_hash(&self, piece: u32, hash: [u8; 20]) -> Option<bool> {
260        let start = piece as usize * 20;
261        let end = start + 20;
262        let expected_hash = self.pieces.as_ref().get(start..end)?;
263        Some(expected_hash == hash)
264    }
265
266    #[inline(never)]
267    pub fn iter_file_details(
268        &self,
269    ) -> anyhow::Result<impl Iterator<Item = FileDetails<'_, BufType>>> {
270        match (self.length, self.files.as_ref()) {
271            // Single-file
272            (Some(length), None) => Ok(Either::Left(once(FileDetails {
273                filename: FileIteratorName::Single(self.name.as_ref()),
274                len: length,
275                attr: self.attr.as_ref(),
276                sha1: self.sha1.as_ref(),
277                symlink_path: self.symlink_path.as_deref(),
278            }))),
279
280            // Multi-file
281            (None, Some(files)) => {
282                if files.is_empty() {
283                    anyhow::bail!("expected multi-file torrent to have at least one file")
284                }
285                Ok(Either::Right(files.iter().map(|f| FileDetails {
286                    filename: FileIteratorName::Tree(&f.path),
287                    len: f.length,
288                    attr: f.attr.as_ref(),
289                    sha1: f.sha1.as_ref(),
290                    symlink_path: f.symlink_path.as_deref(),
291                })))
292            }
293            _ => anyhow::bail!("torrent can't be both in single and multi-file mode"),
294        }
295    }
296
297    pub fn iter_file_lengths(&self) -> anyhow::Result<impl Iterator<Item = u64> + '_> {
298        Ok(self.iter_file_details()?.map(|d| d.len))
299    }
300
301    // NOTE: lenghts MUST be construced with Lenghts::from_torrent, otherwise
302    // the yielded results will be garbage.
303    pub fn iter_file_details_ext<'a>(
304        &'a self,
305        lengths: &'a Lengths,
306    ) -> anyhow::Result<impl Iterator<Item = FileDetailsExt<'a, BufType>> + 'a> {
307        Ok(self.iter_file_details()?.scan(0u64, |acc_offset, details| {
308            let offset = *acc_offset;
309            *acc_offset += details.len;
310            Some(FileDetailsExt {
311                pieces: lengths.iter_pieces_within_offset(offset, details.len),
312                details,
313                offset,
314            })
315        }))
316    }
317}
318
319const fn none<T>() -> Option<T> {
320    None
321}
322
323#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
324pub struct TorrentMetaV1File<BufType> {
325    pub length: u64,
326    pub path: Vec<BufType>,
327
328    #[serde(default = "none", skip_serializing_if = "Option::is_none")]
329    pub attr: Option<BufType>,
330    #[serde(default = "none", skip_serializing_if = "Option::is_none")]
331    pub sha1: Option<BufType>,
332    #[serde(
333        default = "none",
334        rename = "symlink path",
335        skip_serializing_if = "Option::is_none"
336    )]
337    pub symlink_path: Option<Vec<BufType>>,
338}
339
340impl<BufType> TorrentMetaV1File<BufType>
341where
342    BufType: AsRef<[u8]>,
343{
344    pub fn full_path(&self, parent: &mut PathBuf) -> anyhow::Result<()> {
345        for p in self.path.iter() {
346            let bit = std::str::from_utf8(p.as_ref())?;
347            parent.push(bit);
348        }
349        Ok(())
350    }
351}
352
353impl<BufType> CloneToOwned for TorrentMetaV1File<BufType>
354where
355    BufType: CloneToOwned,
356{
357    type Target = TorrentMetaV1File<<BufType as CloneToOwned>::Target>;
358
359    fn clone_to_owned(&self, within_buffer: Option<&Bytes>) -> Self::Target {
360        TorrentMetaV1File {
361            length: self.length,
362            path: self.path.clone_to_owned(within_buffer),
363            attr: self.attr.clone_to_owned(within_buffer),
364            sha1: self.sha1.clone_to_owned(within_buffer),
365            symlink_path: self.symlink_path.clone_to_owned(within_buffer),
366        }
367    }
368}
369
370impl<BufType> CloneToOwned for TorrentMetaV1Info<BufType>
371where
372    BufType: CloneToOwned,
373{
374    type Target = TorrentMetaV1Info<<BufType as CloneToOwned>::Target>;
375
376    fn clone_to_owned(&self, within_buffer: Option<&Bytes>) -> Self::Target {
377        TorrentMetaV1Info {
378            name: self.name.clone_to_owned(within_buffer),
379            pieces: self.pieces.clone_to_owned(within_buffer),
380            piece_length: self.piece_length,
381            length: self.length,
382            md5sum: self.md5sum.clone_to_owned(within_buffer),
383            files: self.files.clone_to_owned(within_buffer),
384            attr: self.attr.clone_to_owned(within_buffer),
385            sha1: self.sha1.clone_to_owned(within_buffer),
386            symlink_path: self.symlink_path.clone_to_owned(within_buffer),
387            private: self.private,
388        }
389    }
390}
391
392impl<BufType> CloneToOwned for TorrentMetaV1<BufType>
393where
394    BufType: CloneToOwned,
395{
396    type Target = TorrentMetaV1<<BufType as CloneToOwned>::Target>;
397
398    fn clone_to_owned(&self, within_buffer: Option<&Bytes>) -> Self::Target {
399        TorrentMetaV1 {
400            announce: self.announce.clone_to_owned(within_buffer),
401            announce_list: self.announce_list.clone_to_owned(within_buffer),
402            info: self.info.clone_to_owned(within_buffer),
403            comment: self.comment.clone_to_owned(within_buffer),
404            created_by: self.created_by.clone_to_owned(within_buffer),
405            encoding: self.encoding.clone_to_owned(within_buffer),
406            publisher: self.publisher.clone_to_owned(within_buffer),
407            publisher_url: self.publisher_url.clone_to_owned(within_buffer),
408            creation_date: self.creation_date,
409            info_hash: self.info_hash,
410        }
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use bencode::BencodeValue;
417
418    use super::*;
419
420    const TORRENT_BYTES: &[u8] =
421        include_bytes!("../../librqbit/resources/ubuntu-21.04-desktop-amd64.iso.torrent");
422
423    #[test]
424    fn test_deserialize_torrent_owned() {
425        let torrent: TorrentMetaV1Owned = torrent_from_bytes(TORRENT_BYTES).unwrap();
426        dbg!(torrent);
427    }
428
429    #[test]
430    fn test_deserialize_torrent_borrowed() {
431        let torrent: TorrentMetaV1Borrowed = torrent_from_bytes(TORRENT_BYTES).unwrap();
432        dbg!(torrent);
433    }
434
435    #[test]
436    fn test_deserialize_torrent_with_info_hash() {
437        let torrent: TorrentMetaV1Borrowed = torrent_from_bytes(TORRENT_BYTES).unwrap();
438        assert_eq!(
439            torrent.info_hash.as_string(),
440            "64a980abe6e448226bb930ba061592e44c3781a1"
441        );
442    }
443
444    #[test]
445    fn test_serialize_then_deserialize_bencode() {
446        let torrent: TorrentMetaV1Info<ByteBuf> = torrent_from_bytes(TORRENT_BYTES).unwrap().info;
447        let mut writer = Vec::new();
448        bencode::bencode_serialize_to_writer(&torrent, &mut writer).unwrap();
449        let deserialized = TorrentMetaV1Info::<ByteBuf>::deserialize(
450            &mut BencodeDeserializer::new_from_buf(&writer),
451        )
452        .unwrap();
453
454        assert_eq!(torrent, deserialized);
455    }
456
457    #[test]
458    fn test_private_serialize_deserialize() {
459        for private in [false, true] {
460            let info: TorrentMetaV1Info<ByteBufOwned> = TorrentMetaV1Info {
461                private,
462                ..Default::default()
463            };
464            let mut buf = Vec::new();
465            bencode::bencode_serialize_to_writer(&info, &mut buf).unwrap();
466
467            let deserialized = TorrentMetaV1Info::<ByteBuf>::deserialize(
468                &mut BencodeDeserializer::new_from_buf(&buf),
469            )
470            .unwrap();
471            assert_eq!(info.private, deserialized.private);
472
473            let deserialized_dyn = ::bencode::dyn_from_bytes::<ByteBuf>(&buf).unwrap();
474            let hm = match deserialized_dyn {
475                bencode::BencodeValue::Dict(hm) => hm,
476                _ => panic!("expected dict"),
477            };
478            match (private, hm.get(&ByteBuf(b"private"))) {
479                (true, Some(BencodeValue::Integer(1))) => {}
480                (false, None) => {}
481                (_, v) => {
482                    panic!("unexpected value for \"private\": {v:?}")
483                }
484            }
485        }
486    }
487
488    #[test]
489    fn test_private_real_torrent() {
490        let buf = include_bytes!("resources/test/private.torrent");
491        let torrent: TorrentMetaV1Borrowed = torrent_from_bytes(buf).unwrap();
492        assert!(torrent.info.private);
493    }
494}