rdest/
metainfo.rs

1// Copyright 2020 Mateusz Janda.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use crate::bcodec::bencoder::BEncoder;
10use crate::bcodec::bvalue::BValue;
11use crate::bcodec::raw_finder::RawFinder;
12use crate::constants::{HASH_SIZE, PIECE_LENGTH};
13use crate::hashmap;
14use crate::Error;
15use crate::{BDecoder, DeepFinder};
16use sha1;
17use std::collections::HashMap;
18use std::convert::{TryFrom, TryInto};
19use std::fs;
20use std::path::{Path, PathBuf};
21
22/// Metainfo file (also known as .torrent; see [BEP3](https://www.bittorrent.org/beps/bep_0003.html#metainfo%20files))
23/// describe all data required to find download file/files from peer-to-peer network.
24#[derive(PartialEq, Clone, Debug)]
25pub struct Metainfo {
26    announce: String,
27    name: String,
28    piece_length: u64,
29    pieces: Vec<[u8; HASH_SIZE]>,
30    files: Vec<File>,
31    info_hash: [u8; HASH_SIZE],
32}
33
34/// File description in metainfo (.torrent) file.
35#[derive(PartialEq, Clone, Debug)]
36pub struct File {
37    /// Total file length
38    pub length: u64,
39    /// File name (or path if file is placed in folders)
40    pub path: String,
41}
42
43pub struct PiecePos {
44    pub file_index: usize,
45    pub byte_index: usize,
46}
47
48impl Metainfo {
49    /// Create new torrent file.
50    ///
51    /// # Example
52    /// ```no_run
53    /// use rdest::Metainfo;
54    /// use std::path::Path;
55    ///
56    /// Metainfo::create_file(Path::new("my_file.dat"), &"http://127.0.0.1:8000".to_string()).unwrap();
57    /// ```
58    pub fn create_file(path: &Path, tracker_addr: &String) -> Result<(), Error> {
59        let metadata = match fs::metadata(path) {
60            Ok(metadata) => {
61                if metadata.is_dir() {
62                    return Err(Error::FileNotFound);
63                }
64                metadata
65            }
66            Err(_) => return Err(Error::FileNotFound),
67        };
68
69        let name = match path.file_name() {
70            Some(name) => match name.to_str() {
71                Some(name) => name.as_bytes().to_vec(),
72                None => return Err(Error::FileNotFound),
73            },
74            None => return Err(Error::FileNotFound),
75        };
76
77        let data = match fs::read(path) {
78            Ok(data) => data,
79            Err(_) => return Err(Error::FileNotFound),
80        };
81
82        let pieces = data
83            .chunks(PIECE_LENGTH)
84            .flat_map(|chunk| {
85                let mut hasher = sha1::Sha1::new();
86                hasher.update(chunk);
87                hasher.digest().bytes().as_ref().to_vec()
88            })
89            .collect::<Vec<u8>>();
90
91        let info = hashmap![
92            b"name".to_vec() => BValue::ByteStr(name),
93            b"piece length".to_vec() => BValue::Int(PIECE_LENGTH as i64),
94            b"pieces".to_vec() => BValue::ByteStr(pieces),
95            b"length".to_vec() => BValue::Int(metadata.len() as i64)
96        ];
97
98        let torrent = hashmap![
99            b"announce".to_vec() => BValue::ByteStr(tracker_addr.to_owned().into_bytes()),
100            b"info".to_vec() => BValue::Dict(info)
101        ];
102
103        let torrent_file = match path.file_name() {
104            Some(file_name) => {
105                let mut torrent_file = file_name.to_os_string();
106                torrent_file.push(".torrent");
107                torrent_file
108            }
109            None => return Err(Error::FileNotFound),
110        };
111
112        match fs::write(torrent_file, BEncoder::new().add_dict(&torrent).encode()) {
113            Ok(()) => Ok(()),
114            Err(_) => Err(Error::FileCannotWrite),
115        }
116    }
117
118    /// Read metainfo (.torrent) data from file.
119    ///
120    /// # Example
121    /// ```
122    /// use rdest::Metainfo;
123    /// use std::path::PathBuf;
124    ///
125    /// let path = PathBuf::from("ubuntu-20.04.2.0-desktop-amd64.iso.torrent");
126    /// let torrent = Metainfo::from_file(path.as_path()).unwrap();
127    /// ```
128    pub fn from_file(path: &Path) -> Result<Metainfo, Error> {
129        match &fs::read(path) {
130            Ok(val) => Self::from_bencode(val),
131            Err(_) => Err(Error::MetaFileNotFound),
132        }
133    }
134
135    /// Read metainfo (.torrent) data directly from [bencoded](https://en.wikipedia.org/wiki/Bencode) string.
136    ///
137    /// # Example
138    /// ```
139    /// use rdest::Metainfo;
140    ///
141    /// let torrent = Metainfo::from_bencode(b"d8:announce3:URL4:infod4:name4:NAME12:piece lengthi111e6:pieces20:AAAAABBBBBCCCCCDDDDD6:lengthi222eee").unwrap();
142    /// ```
143    pub fn from_bencode(data: &[u8]) -> Result<Metainfo, Error> {
144        let bvalues = BDecoder::from_array(data)?;
145
146        if bvalues.is_empty() {
147            return Err(Error::MetaBEncodeMissing);
148        }
149
150        let mut err = Err(Error::MetaDataMissing);
151        for val in bvalues {
152            match val {
153                BValue::Dict(dict) => match Self::parse(data, &dict) {
154                    Ok(torrent) => return Ok(torrent),
155                    Err(e) => err = Err(e),
156                },
157                _ => (),
158            }
159        }
160
161        err
162    }
163
164    fn parse(data: &[u8], dict: &HashMap<Vec<u8>, BValue>) -> Result<Metainfo, Error> {
165        let length = Self::find_length(dict);
166        let multi_files = Self::find_files(dict);
167
168        if length.is_some() && multi_files.is_some() {
169            return Err(Error::MetaLenAndFilesConflict);
170        } else if length.is_none() && multi_files.is_none() {
171            return Err(Error::MetaLenOrFilesMissing);
172        }
173
174        let name = Self::find_name(dict)?;
175        let files = match length {
176            Some(length) => vec![File {
177                length,
178                path: name.clone(),
179            }],
180            None => match multi_files {
181                Some(multi_files) => multi_files,
182                None => vec![],
183            },
184        };
185
186        let metainfo = Metainfo {
187            announce: Self::find_announce(dict)?,
188            name,
189            piece_length: Self::find_piece_length(dict)?,
190            pieces: Self::find_pieces(dict)?,
191            files,
192            info_hash: Self::calculate_hash(data)?,
193        };
194
195        Ok(metainfo)
196    }
197
198    /// Find value for "announce" key in pre-parsed dictionary (converted to HashMap).
199    pub fn find_announce(dict: &HashMap<Vec<u8>, BValue>) -> Result<String, Error> {
200        match dict.get(&b"announce".to_vec()) {
201            Some(BValue::ByteStr(val)) => {
202                String::from_utf8(val.to_vec()).or(Err(Error::MetaInvalidUtf8("announce")))
203            }
204            _ => Err(Error::MetaIncorrectOrMissing("announce")),
205        }
206    }
207
208    /// Find value for "info:name" key in pre-parsed dictionary (converted to HashMap).
209    pub fn find_name(dict: &HashMap<Vec<u8>, BValue>) -> Result<String, Error> {
210        match dict.get(&b"info".to_vec()) {
211            Some(BValue::Dict(info)) => match info.get(&b"name".to_vec()) {
212                Some(BValue::ByteStr(val)) => {
213                    String::from_utf8(val.to_vec()).or(Err(Error::MetaInvalidUtf8("name")))
214                }
215                _ => Err(Error::MetaIncorrectOrMissing("name")),
216            },
217            _ => Err(Error::MetaIncorrectOrMissing("info".into())),
218        }
219    }
220
221    /// Find value for "info:piece length" key in pre-parsed dictionary (converted to HashMap).
222    pub fn find_piece_length(dict: &HashMap<Vec<u8>, BValue>) -> Result<u64, Error> {
223        match dict.get(&b"info".to_vec()) {
224            Some(BValue::Dict(info)) => match info.get(&b"piece length".to_vec()) {
225                Some(BValue::Int(length)) => {
226                    u64::try_from(*length).or(Err(Error::MetaInvalidU64("piece length")))
227                }
228                _ => Err(Error::MetaIncorrectOrMissing("piece length")),
229            },
230            _ => Err(Error::MetaIncorrectOrMissing("info".into())),
231        }
232    }
233
234    /// Find value for "info:pieces" key in pre-parsed dictionary (converted to HashMap).
235    pub fn find_pieces(dict: &HashMap<Vec<u8>, BValue>) -> Result<Vec<[u8; HASH_SIZE]>, Error> {
236        match dict.get(&b"info".to_vec()) {
237            Some(BValue::Dict(info)) => match info.get(&b"pieces".to_vec()) {
238                Some(BValue::ByteStr(pieces)) => {
239                    if pieces.len() % HASH_SIZE != 0 {
240                        return Err(Error::MetaNotDivisible("pieces"));
241                    }
242                    Ok(pieces
243                        .chunks(HASH_SIZE)
244                        .map(|chunk| chunk.try_into().unwrap())
245                        .collect())
246                }
247                _ => Err(Error::MetaIncorrectOrMissing("pieces")),
248            },
249            _ => Err(Error::MetaIncorrectOrMissing("info".into())),
250        }
251    }
252
253    /// Find value for "info:length" key in pre-parsed dictionary (converted to HashMap).
254    pub fn find_length(dict: &HashMap<Vec<u8>, BValue>) -> Option<u64> {
255        match dict.get(&b"info".to_vec()) {
256            Some(BValue::Dict(info)) => match info.get(&b"length".to_vec()) {
257                Some(BValue::Int(length)) => u64::try_from(*length).ok(),
258                _ => None,
259            },
260            _ => None,
261        }
262    }
263
264    /// Find value for "info:files" key in pre-parsed dictionary (converted to HashMap).
265    pub fn find_files(dict: &HashMap<Vec<u8>, BValue>) -> Option<Vec<File>> {
266        match dict.get(&b"info".to_vec()) {
267            Some(BValue::Dict(info)) => match info.get(&b"files".to_vec()) {
268                Some(BValue::List(list)) => Some(Self::file_list(list)),
269                _ => None,
270            },
271            _ => None,
272        }
273    }
274
275    fn file_list(list: &Vec<BValue>) -> Vec<File> {
276        list.iter()
277            .filter_map(|elem| match elem {
278                BValue::Dict(dict) => Some(dict),
279                _ => None,
280            })
281            .filter_map(
282                |dict| match (dict.get(&b"length".to_vec()), dict.get(&b"path".to_vec())) {
283                    (Some(BValue::Int(length)), Some(BValue::ByteStr(path))) => {
284                        Some((length, path))
285                    }
286                    _ => None,
287                },
288            )
289            .filter_map(|(length, path)| {
290                match (u64::try_from(*length), String::from_utf8(path.to_vec())) {
291                    (Ok(l), Ok(p)) => Some(File { length: l, path: p }),
292                    _ => None,
293                }
294            })
295            .collect()
296    }
297
298    fn calculate_hash(data: &[u8]) -> Result<[u8; HASH_SIZE], Error> {
299        if let Some(info) = DeepFinder::find_first("4:info", data) {
300            let mut hasher = sha1::Sha1::new();
301            hasher.update(info.as_ref());
302            return Ok(hasher.digest().bytes());
303        }
304
305        Err(Error::InfoMissing)
306    }
307
308    /// Return URL of the tracker
309    pub fn tracker_url(&self) -> &String {
310        &self.announce
311    }
312
313    /// Return SHA-1 hash of specific piece.
314    pub fn piece(&self, piece_index: usize) -> &[u8; HASH_SIZE] {
315        &self.pieces[piece_index]
316    }
317
318    /// Return number of SHA-1 hashes.
319    pub fn pieces_num(&self) -> usize {
320        self.pieces.len()
321    }
322
323    /// Return length of specific piece.
324    pub fn piece_length(&self, piece_index: usize) -> usize {
325        if piece_index < self.pieces.len() - 1 {
326            return self.piece_length as usize;
327        }
328
329        let last = self.total_length() as usize % self.piece_length as usize;
330        if last != 0 {
331            return last;
332        }
333
334        return self.piece_length as usize;
335    }
336
337    /// Return length of all files described by torrent.
338    pub fn total_length(&self) -> u64 {
339        self.files.iter().map(|file| file.length).sum()
340    }
341
342    /// Return SHA-1 hash of info section.
343    pub fn info_hash(&self) -> &[u8; HASH_SIZE] {
344        &self.info_hash
345    }
346
347    /// Return vector with information which pieces contain which files.
348    pub fn file_piece_ranges(&self) -> Vec<(PathBuf, PiecePos, PiecePos)> {
349        let dir = match self.files.len() > 1 {
350            true => PathBuf::from(&self.name),
351            false => PathBuf::new(),
352        };
353
354        let mut ranges: Vec<(PathBuf, PiecePos, PiecePos)> = vec![];
355        let mut pos: usize = 0;
356
357        for File { length, path } in self.files.iter() {
358            ranges.push((
359                dir.join(path),
360                self.piece_pos(pos),
361                self.piece_pos(pos + *length as usize),
362            ));
363
364            pos += *length as usize;
365        }
366
367        ranges
368    }
369
370    fn piece_pos(&self, pos: usize) -> PiecePos {
371        PiecePos {
372            file_index: pos / self.piece_length as usize,
373            byte_index: pos % self.piece_length as usize,
374        }
375    }
376}