Skip to main content

librqbit_core/
torrent_metainfo.rs

1use bencode::WithRawBytes;
2use buffers::{ByteBuf, ByteBufOwned};
3use bytes::Bytes;
4use clone_to_owned::CloneToOwned;
5use encoding_rs::Encoding;
6use itertools::Either;
7use serde_derive::{Deserialize, Serialize};
8use std::{borrow::Cow, collections::HashSet, iter::once, path::PathBuf};
9use tracing::debug;
10
11use crate::{Error, 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 info_hash).
25#[cfg(any(feature = "sha1-ring", feature = "sha1-crypto-hash"))]
26pub fn torrent_from_bytes<'de>(
27    buf: &'de [u8],
28) -> Result<TorrentMetaV1<ByteBuf<'de>>, bencode::DeserializeError> {
29    let mut t: TorrentMetaV1<ByteBuf<'_>> = bencode::from_bytes(buf)
30        .inspect_err(|e| tracing::trace!("error deserializing torrent: {e:#}"))
31        .map_err(|e| e.into_kind())?;
32
33    use sha1w::ISha1;
34
35    let mut digest = sha1w::Sha1::new();
36    digest.update(t.info.raw_bytes.as_ref());
37    t.info_hash = Id20::new(digest.finish());
38    Ok(t)
39}
40
41fn is_false(b: &bool) -> bool {
42    !*b
43}
44
45/// A parsed .torrent file.
46#[derive(Serialize, Deserialize, Debug, Clone)]
47pub struct TorrentMetaV1<BufType> {
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub announce: Option<BufType>,
50    #[serde(
51        rename = "announce-list",
52        default = "Vec::new",
53        skip_serializing_if = "Vec::is_empty"
54    )]
55    pub announce_list: Vec<Vec<BufType>>,
56    pub info: WithRawBytes<TorrentMetaV1Info<BufType>, BufType>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub comment: Option<BufType>,
59    #[serde(rename = "created by", skip_serializing_if = "Option::is_none")]
60    pub created_by: Option<BufType>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub encoding: Option<BufType>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub publisher: Option<BufType>,
65    #[serde(rename = "publisher-url", skip_serializing_if = "Option::is_none")]
66    pub publisher_url: Option<BufType>,
67    #[serde(rename = "creation date", skip_serializing_if = "Option::is_none")]
68    pub creation_date: Option<usize>,
69
70    #[serde(skip)]
71    pub info_hash: Id20,
72}
73
74impl<BufType> TorrentMetaV1<BufType> {
75    pub fn iter_announce(&self) -> impl Iterator<Item = &BufType> {
76        if self.announce_list.iter().flatten().next().is_some() {
77            return itertools::Either::Left(self.announce_list.iter().flatten());
78        }
79        itertools::Either::Right(self.announce.iter())
80    }
81}
82
83/// Main torrent information, shared by .torrent files and magnet link contents.
84#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
85pub struct TorrentMetaV1Info<BufType> {
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub name: Option<BufType>,
88    pub pieces: BufType,
89    #[serde(rename = "piece length")]
90    pub piece_length: u32,
91
92    // Single-file mode
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub length: Option<u64>,
95    #[serde(default = "none", skip_serializing_if = "Option::is_none")]
96    pub attr: Option<BufType>,
97    #[serde(default = "none", skip_serializing_if = "Option::is_none")]
98    pub sha1: Option<BufType>,
99    #[serde(
100        default = "none",
101        rename = "symlink path",
102        skip_serializing_if = "Option::is_none"
103    )]
104    pub symlink_path: Option<Vec<BufType>>,
105
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub md5sum: Option<BufType>,
108
109    // Multi-file mode
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub files: Option<Vec<TorrentMetaV1File<BufType>>>,
112
113    #[serde(skip_serializing_if = "is_false", default)]
114    pub private: bool,
115}
116
117#[derive(Clone, Copy)]
118pub struct FileIteratorName<'a, BufType> {
119    encoding: &'static Encoding,
120    data: FileIteratorNameData<'a, BufType>,
121}
122
123#[derive(Clone, Copy)]
124pub enum FileIteratorNameData<'a, BufType> {
125    Single(Option<&'a BufType>),
126    Tree(&'a [BufType]),
127}
128
129impl<BufType> std::fmt::Debug for FileIteratorName<'_, BufType>
130where
131    BufType: AsRef<[u8]>,
132{
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        <Self as std::fmt::Display>::fmt(self, f)
135    }
136}
137
138impl<BufType> std::fmt::Display for FileIteratorName<'_, BufType>
139where
140    BufType: AsRef<[u8]>,
141{
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        for (idx, bit) in self.iter_components().enumerate() {
144            if idx > 0 {
145                write!(f, "{}", std::path::MAIN_SEPARATOR)?;
146            }
147            write!(f, "{bit}")?;
148        }
149        Ok(())
150    }
151}
152
153impl<'a, BufType> FileIteratorName<'a, BufType>
154where
155    BufType: AsRef<[u8]>,
156{
157    /// Convert path components into a vector.
158    pub fn to_vec(&self) -> Vec<String> {
159        self.iter_components().map(|c| c.into_owned()).collect()
160    }
161
162    /// Convert path components into a PathBuf, for use with local FS.
163    pub fn to_pathbuf(&self) -> PathBuf {
164        let mut buf = PathBuf::new();
165        for bit in self.iter_components() {
166            buf.push(&*bit)
167        }
168        buf
169    }
170
171    /// Iterate path components as strings. Will decode using the detected encoding, replacing
172    /// unknown characters with a placeholder.
173    pub fn iter_components(&self) -> impl Iterator<Item = Cow<'a, str>> + use<'a, BufType> {
174        let encoding = self.encoding;
175        self.iter_components_bytes()
176            .map(move |part| encoding.decode(part).0)
177    }
178
179    /// Iterate path components as bytes.
180    pub fn iter_components_bytes(&self) -> impl Iterator<Item = &'a [u8]> + use<'a, BufType> {
181        let it = match self.data {
182            FileIteratorNameData::Single(None) => {
183                return Either::Left(once(&b"torrent-content"[..]));
184            }
185            FileIteratorNameData::Single(Some(name)) => Either::Left(once((*name).as_ref())),
186            FileIteratorNameData::Tree(t) => Either::Right(t.iter().map(|bb| bb.as_ref())),
187        };
188        Either::Right(it)
189    }
190}
191
192#[derive(Serialize, Deserialize, Default, Debug, Clone, Copy)]
193pub struct FileDetailsAttrs {
194    pub symlink: bool,
195    pub hidden: bool,
196    pub padding: bool,
197    pub executable: bool,
198}
199
200pub struct FileDetails<'a, BufType> {
201    pub filename: FileIteratorName<'a, BufType>,
202    pub len: u64,
203
204    // bep-47
205    attr: Option<&'a BufType>,
206    pub sha1: Option<&'a BufType>,
207    pub symlink_path: Option<&'a [BufType]>,
208}
209
210impl<BufType> FileDetails<'_, BufType>
211where
212    BufType: AsRef<[u8]>,
213{
214    pub fn attrs(&self) -> FileDetailsAttrs {
215        let attrs = match self.attr {
216            Some(attrs) => attrs,
217            None => return FileDetailsAttrs::default(),
218        };
219        let mut result = FileDetailsAttrs::default();
220        for byte in attrs.as_ref().iter().copied() {
221            match byte {
222                b'l' => result.symlink = true,
223                b'h' => result.hidden = true,
224                b'p' => result.padding = true,
225                b'x' => result.executable = true,
226                other => debug!(attr = other, "unknown file attribute"),
227            }
228        }
229        result
230    }
231}
232
233pub struct FileDetailsExt<'a, BufType> {
234    pub details: FileDetails<'a, BufType>,
235    // absolute offset in torrent if it was a flat blob of bytes
236    pub offset: u64,
237
238    // the pieces that contain this file
239    pub pieces: std::ops::Range<u32>,
240}
241
242impl<BufType> FileDetailsExt<'_, BufType> {
243    pub fn pieces_usize(&self) -> std::ops::Range<usize> {
244        self.pieces.start as usize..self.pieces.end as usize
245    }
246}
247
248#[derive(Clone, Debug)]
249pub struct ValidatedTorrentMetaV1Info<BufType> {
250    encoding: &'static Encoding,
251    lengths: Lengths,
252    info: TorrentMetaV1Info<BufType>,
253}
254
255impl<BufType: AsRef<[u8]>> ValidatedTorrentMetaV1Info<BufType> {
256    pub fn name(&self) -> Option<Cow<'_, str>> {
257        self.info
258            .name
259            .as_ref()
260            .map(|n| self.encoding.decode(n.as_ref()).0)
261            .filter(|n| !n.is_empty())
262    }
263
264    pub fn info(&self) -> &TorrentMetaV1Info<BufType> {
265        &self.info
266    }
267
268    pub fn lengths(&self) -> &Lengths {
269        &self.lengths
270    }
271
272    pub fn name_or_else<'a, DefaultT: Into<Cow<'a, str>>>(
273        &'a self,
274        default: impl Fn() -> DefaultT,
275    ) -> Cow<'a, str> {
276        self.name().unwrap_or_else(|| default().into())
277    }
278
279    /// Guaranteed to produce at least one file.
280    pub fn iter_file_details(&self) -> impl Iterator<Item = FileDetails<'_, BufType>> {
281        // .unwrap is ok here() as we checked errors at creation time.
282        self.info.iter_file_details_raw(self.encoding).unwrap()
283    }
284
285    pub fn iter_file_lengths(&self) -> impl Iterator<Item = u64> + '_ {
286        self.iter_file_details().map(|d| d.len)
287    }
288
289    // Iterate file details with additional computations for offsets.
290    pub fn iter_file_details_ext<'a>(
291        &'a self,
292    ) -> impl Iterator<Item = FileDetailsExt<'a, BufType>> + 'a {
293        self.iter_file_details()
294            .scan(0u64, move |acc_offset, details| {
295                let offset = *acc_offset;
296                *acc_offset += details.len;
297                Some(FileDetailsExt {
298                    pieces: self.lengths.iter_pieces_within_offset(offset, details.len),
299                    details,
300                    offset,
301                })
302            })
303    }
304}
305
306impl<BufType: AsRef<[u8]>> TorrentMetaV1Info<BufType> {
307    pub fn validate(self) -> crate::Result<ValidatedTorrentMetaV1Info<BufType>> {
308        let lengths = Lengths::from_torrent(&self)?;
309        let encoding = self.detect_encoding();
310        let validated = ValidatedTorrentMetaV1Info {
311            encoding,
312            lengths,
313            info: self,
314        };
315
316        // Ensure:
317        // - there's at least one file
318        // - each filename has at least one path component
319        // - there's no path traversal
320        // - all filenames are unique
321        let mut seen_files = 0;
322        for file in validated.info.iter_file_details_raw(encoding)? {
323            seen_files += 1;
324            let mut seen_a_bit = false;
325            for bit in file.filename.iter_components_bytes() {
326                seen_a_bit = true;
327                if bit == b".." {
328                    return Err(Error::BadTorrentPathTraversal);
329                }
330                use memchr::memchr;
331                if memchr(b'/', bit).is_some() || memchr(b'\\', bit).is_some() {
332                    return Err(Error::BadTorrentSeparatorInName);
333                }
334            }
335            if !seen_a_bit {
336                return Err(Error::BadTorrentFileNoName);
337            }
338        }
339        if seen_files == 0 {
340            return Err(Error::BadTorrentNoFiles);
341        }
342
343        let mut unique_filenames = HashSet::<PathBuf>::new();
344        for fd in validated.iter_file_details() {
345            let pb = fd.filename.to_pathbuf();
346            if pb.as_os_str().is_empty() {
347                return Err(Error::BadTorrentFileNoName);
348            }
349            unique_filenames.insert(pb);
350        }
351        if unique_filenames.len() != seen_files {
352            return Err(Error::BadTorrentDuplicateFilenames);
353        }
354
355        Ok(validated)
356    }
357
358    pub fn get_hash(&self, piece: u32) -> Option<&[u8]> {
359        let start = piece as usize * 20;
360        let end = start + 20;
361        let expected_hash = self.pieces.as_ref().get(start..end)?;
362        Some(expected_hash)
363    }
364
365    pub fn compare_hash(&self, piece: u32, hash: [u8; 20]) -> Option<bool> {
366        let start = piece as usize * 20;
367        let end = start + 20;
368        let expected_hash = self.pieces.as_ref().get(start..end)?;
369        Some(expected_hash == hash)
370    }
371
372    pub fn detect_encoding(&self) -> &'static Encoding {
373        let mut encdetect = chardetng::EncodingDetector::new(chardetng::Iso2022JpDetection::Deny);
374        if let Some(name) = self.name.as_ref() {
375            encdetect.feed(name.as_ref(), false);
376        }
377
378        for file in self.files.iter().flat_map(|f| f.iter()) {
379            for component in file.path.iter() {
380                encdetect.feed(component.as_ref(), false);
381            }
382        }
383
384        encdetect.guess(None, chardetng::Utf8Detection::Allow)
385    }
386
387    pub(crate) fn iter_file_details_raw(
388        &self,
389        encoding: &'static Encoding,
390    ) -> crate::Result<impl Iterator<Item = FileDetails<'_, BufType>>> {
391        match (self.length, self.files.as_ref()) {
392            // Single-file
393            (Some(length), None) => Ok(Either::Left(once(FileDetails {
394                filename: FileIteratorName {
395                    encoding,
396                    data: FileIteratorNameData::Single(self.name.as_ref()),
397                },
398                len: length,
399                attr: self.attr.as_ref(),
400                sha1: self.sha1.as_ref(),
401                symlink_path: self.symlink_path.as_deref(),
402            }))),
403
404            // Multi-file
405            (None, Some(files)) => {
406                if files.is_empty() {
407                    return Err(Error::BadTorrentMultiFileEmpty);
408                }
409                Ok(Either::Right(files.iter().map(move |f| FileDetails {
410                    filename: FileIteratorName {
411                        encoding,
412                        data: FileIteratorNameData::Tree(&f.path),
413                    },
414                    len: f.length,
415                    attr: f.attr.as_ref(),
416                    sha1: f.sha1.as_ref(),
417                    symlink_path: f.symlink_path.as_deref(),
418                })))
419            }
420            _ => Err(Error::BadTorrentBothSingleAndMultiFile),
421        }
422    }
423}
424
425const fn none<T>() -> Option<T> {
426    None
427}
428
429#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
430pub struct TorrentMetaV1File<BufType> {
431    pub length: u64,
432    pub path: Vec<BufType>,
433
434    #[serde(default = "none", skip_serializing_if = "Option::is_none")]
435    pub attr: Option<BufType>,
436    #[serde(default = "none", skip_serializing_if = "Option::is_none")]
437    pub sha1: Option<BufType>,
438    #[serde(
439        default = "none",
440        rename = "symlink path",
441        skip_serializing_if = "Option::is_none"
442    )]
443    pub symlink_path: Option<Vec<BufType>>,
444}
445
446impl<BufType> CloneToOwned for TorrentMetaV1File<BufType>
447where
448    BufType: CloneToOwned,
449{
450    type Target = TorrentMetaV1File<<BufType as CloneToOwned>::Target>;
451
452    fn clone_to_owned(&self, within_buffer: Option<&Bytes>) -> Self::Target {
453        TorrentMetaV1File {
454            length: self.length,
455            path: self.path.clone_to_owned(within_buffer),
456            attr: self.attr.clone_to_owned(within_buffer),
457            sha1: self.sha1.clone_to_owned(within_buffer),
458            symlink_path: self.symlink_path.clone_to_owned(within_buffer),
459        }
460    }
461}
462
463impl<BufType> CloneToOwned for TorrentMetaV1Info<BufType>
464where
465    BufType: CloneToOwned,
466{
467    type Target = TorrentMetaV1Info<<BufType as CloneToOwned>::Target>;
468
469    fn clone_to_owned(&self, within_buffer: Option<&Bytes>) -> Self::Target {
470        TorrentMetaV1Info {
471            name: self.name.clone_to_owned(within_buffer),
472            pieces: self.pieces.clone_to_owned(within_buffer),
473            piece_length: self.piece_length,
474            length: self.length,
475            md5sum: self.md5sum.clone_to_owned(within_buffer),
476            files: self.files.clone_to_owned(within_buffer),
477            attr: self.attr.clone_to_owned(within_buffer),
478            sha1: self.sha1.clone_to_owned(within_buffer),
479            symlink_path: self.symlink_path.clone_to_owned(within_buffer),
480            private: self.private,
481        }
482    }
483}
484
485impl<BufType> CloneToOwned for TorrentMetaV1<BufType>
486where
487    BufType: CloneToOwned,
488{
489    type Target = TorrentMetaV1<<BufType as CloneToOwned>::Target>;
490
491    fn clone_to_owned(&self, within_buffer: Option<&Bytes>) -> Self::Target {
492        TorrentMetaV1 {
493            announce: self.announce.clone_to_owned(within_buffer),
494            announce_list: self.announce_list.clone_to_owned(within_buffer),
495            info: self.info.clone_to_owned(within_buffer),
496            comment: self.comment.clone_to_owned(within_buffer),
497            created_by: self.created_by.clone_to_owned(within_buffer),
498            encoding: self.encoding.clone_to_owned(within_buffer),
499            publisher: self.publisher.clone_to_owned(within_buffer),
500            publisher_url: self.publisher_url.clone_to_owned(within_buffer),
501            creation_date: self.creation_date,
502            info_hash: self.info_hash,
503        }
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use bencode::{BencodeValue, from_bytes};
510
511    use super::*;
512
513    const TORRENT_BYTES: &[u8] =
514        include_bytes!("../../librqbit/resources/ubuntu-21.04-desktop-amd64.iso.torrent");
515
516    #[test]
517    fn test_deserialize_torrent_borrowed() {
518        let torrent: TorrentMetaV1Borrowed = from_bytes(TORRENT_BYTES).unwrap();
519        dbg!(torrent);
520    }
521
522    #[test]
523    #[cfg(any(feature = "sha1-ring", feature = "sha1-crypto-hash"))]
524    fn test_deserialize_torrent_with_info_hash() {
525        let torrent: TorrentMetaV1Borrowed = torrent_from_bytes(TORRENT_BYTES).unwrap();
526        assert_eq!(
527            torrent.info_hash.as_string(),
528            "64a980abe6e448226bb930ba061592e44c3781a1"
529        );
530    }
531
532    #[test]
533    fn test_serialize_then_deserialize_bencode() {
534        let torrent = from_bytes::<TorrentMetaV1<ByteBuf>>(TORRENT_BYTES)
535            .unwrap()
536            .info
537            .data;
538        let mut writer = Vec::new();
539        bencode::bencode_serialize_to_writer(&torrent, &mut writer).unwrap();
540        let deserialized = from_bytes::<TorrentMetaV1Info<ByteBuf>>(&writer).unwrap();
541
542        assert_eq!(torrent, deserialized);
543    }
544
545    #[test]
546    fn test_private_serialize_deserialize() {
547        for private in [false, true] {
548            let info: TorrentMetaV1Info<ByteBufOwned> = TorrentMetaV1Info {
549                private,
550                ..Default::default()
551            };
552            let mut buf = Vec::new();
553            bencode::bencode_serialize_to_writer(&info, &mut buf).unwrap();
554
555            let deserialized = from_bytes::<TorrentMetaV1Info<ByteBuf>>(&buf).unwrap();
556            assert_eq!(info.private, deserialized.private);
557
558            let deserialized_dyn = ::bencode::dyn_from_bytes::<ByteBuf>(&buf).unwrap();
559            let hm = match deserialized_dyn {
560                bencode::BencodeValue::Dict(hm) => hm,
561                _ => panic!("expected dict"),
562            };
563            match (private, hm.get(&ByteBuf(b"private"))) {
564                (true, Some(BencodeValue::Integer(1))) => {}
565                (false, None) => {}
566                (_, v) => {
567                    panic!("unexpected value for \"private\": {v:?}")
568                }
569            }
570        }
571    }
572
573    #[test]
574    #[cfg(any(feature = "sha1-ring", feature = "sha1-crypto-hash"))]
575    fn test_private_real_torrent() {
576        let buf = include_bytes!("resources/test/private.torrent");
577        let torrent: TorrentMetaV1Borrowed = from_bytes(buf).unwrap();
578        assert!(torrent.info.data.private);
579    }
580}