diff --git a/README.md b/README.md index e3ae94a..3d9bcf6 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ at any time. | [02](http://bittorrent.org/beps/bep_0002.html) | :heavy_minus_sign: | Sample reStructured Text BEP Template | | [03](http://bittorrent.org/beps/bep_0003.html) | :white_check_mark: | The BitTorrent Protocol Specification | | [04](http://bittorrent.org/beps/bep_0004.html) | :heavy_minus_sign: | Assigned Numbers | -| [05](http://bittorrent.org/beps/bep_0005.html) | [:x:](https://github.com/casey/intermodal/issues/90) | DHT Protocol | +| [05](http://bittorrent.org/beps/bep_0005.html) | :white_check_mark: | DHT Protocol | | [06](http://bittorrent.org/beps/bep_0006.html) | :heavy_minus_sign: | Fast Extension | | [07](http://bittorrent.org/beps/bep_0007.html) | :heavy_minus_sign: | IPv6 Tracker Extension | | [08](http://bittorrent.org/beps/bep_0008.html) | :heavy_minus_sign: | Tracker Peer Obfuscation | diff --git a/src/common.rs b/src/common.rs index 8773c80..7f76268 100644 --- a/src/common.rs +++ b/src/common.rs @@ -11,7 +11,7 @@ pub(crate) use std::{ hash::Hash, io::{self, Read, Write}, iter::{self, Sum}, - num::{ParseFloatError, TryFromIntError}, + num::{ParseFloatError, ParseIntError, TryFromIntError}, ops::{AddAssign, Div, DivAssign, Mul, MulAssign, SubAssign}, path::{self, Path, PathBuf}, process::{self, Command, ExitStatus}, @@ -26,7 +26,7 @@ pub(crate) use chrono::{TimeZone, Utc}; pub(crate) use globset::{Glob, GlobMatcher}; pub(crate) use libc::EXIT_FAILURE; pub(crate) use regex::{Regex, RegexSet}; -pub(crate) use serde::{Deserialize, Serialize}; +pub(crate) use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; pub(crate) use serde_hex::SerHex; pub(crate) use serde_with::rust::unwrap_or_skip; pub(crate) use sha1::Sha1; @@ -37,7 +37,7 @@ pub(crate) use structopt::{ StructOpt, }; pub(crate) use unicode_width::UnicodeWidthStr; -pub(crate) use url::Url; +pub(crate) use url::{Host, Url}; pub(crate) use walkdir::WalkDir; // modules @@ -53,7 +53,7 @@ pub(crate) use crate::{ pub(crate) use crate::{ bytes::Bytes, env::Env, error::Error, file_info::FileInfo, file_path::FilePath, file_status::FileStatus, files::Files, hasher::Hasher, info::Info, lint::Lint, linter::Linter, - md5_digest::Md5Digest, metainfo::Metainfo, mode::Mode, opt::Opt, + md5_digest::Md5Digest, metainfo::Metainfo, mode::Mode, node::Node, opt::Opt, piece_length_picker::PieceLengthPicker, platform::Platform, status::Status, style::Style, table::Table, target::Target, torrent_summary::TorrentSummary, use_color::UseColor, verifier::Verifier, walker::Walker, diff --git a/src/error.rs b/src/error.rs index 9e78aee..44c20f3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -29,14 +29,25 @@ pub(crate) enum Error { CommandInvoke { command: String, source: io::Error }, #[snafu(display("Command `{}` returned bad exit status: {}", command, status))] CommandStatus { command: String, status: ExitStatus }, - #[snafu(display("Filename was not valid unicode: {}", filename.to_string_lossy()))] - FilenameDecode { filename: OsString }, + #[snafu(display("Filename was not valid unicode: {}", filename.display()))] + FilenameDecode { filename: PathBuf }, #[snafu(display("Path had no file name: {}", path.display()))] FilenameExtract { path: PathBuf }, #[snafu(display("I/O error at `{}`: {}", path.display(), source))] Filesystem { source: io::Error, path: PathBuf }, #[snafu(display("Invalid glob: {}", source))] GlobParse { source: globset::Error }, + #[snafu(display("Unknown lint: {}", text))] + LintUnknown { text: String }, + #[snafu(display("DHT node port missing: {}", text))] + NodeParsePortMissing { text: String }, + #[snafu(display("Failed to parse DHT node host `{}`: {}", text, source))] + NodeParseHost { + text: String, + source: url::ParseError, + }, + #[snafu(display("Failed to parse DHT node port `{}`: {}", text, source))] + NodeParsePort { text: String, source: ParseIntError }, #[snafu(display("Failed to find opener utility, please install one of {}", tried.join(",")))] OpenerMissing { tried: &'static [&'static str] }, #[snafu(display( @@ -104,8 +115,6 @@ pub(crate) enum Error { feature ))] Unstable { feature: &'static str }, - #[snafu(display("Unknown lint: {}", text))] - LintUnknown { text: String }, #[snafu(display("Torrent verification failed: {}", status))] Verify { status: Status }, } diff --git a/src/main.rs b/src/main.rs index 969c04a..6872a23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,6 +72,7 @@ mod linter; mod md5_digest; mod metainfo; mod mode; +mod node; mod opt; mod path_ext; mod piece_length_picker; diff --git a/src/metainfo.rs b/src/metainfo.rs index ff405be..a53489d 100644 --- a/src/metainfo.rs +++ b/src/metainfo.rs @@ -3,8 +3,8 @@ use crate::common::*; #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub(crate) struct Metainfo { pub(crate) announce: String, - #[serde(rename = "announce-list")] #[serde( + rename = "announce-list", skip_serializing_if = "Option::is_none", default, with = "unwrap_or_skip" @@ -16,15 +16,15 @@ pub(crate) struct Metainfo { with = "unwrap_or_skip" )] pub(crate) comment: Option, - #[serde(rename = "created by")] #[serde( + rename = "created by", skip_serializing_if = "Option::is_none", default, with = "unwrap_or_skip" )] pub(crate) created_by: Option, - #[serde(rename = "creation date")] #[serde( + rename = "creation date", skip_serializing_if = "Option::is_none", default, with = "unwrap_or_skip" @@ -37,6 +37,12 @@ pub(crate) struct Metainfo { )] pub(crate) encoding: Option, pub(crate) info: Info, + #[serde( + skip_serializing_if = "Option::is_none", + default, + with = "unwrap_or_skip" + )] + pub(crate) nodes: Option>, } impl Metainfo { @@ -103,6 +109,7 @@ mod tests { created_by: Some("created by".into()), creation_date: Some(1), encoding: Some("UTF-8".into()), + nodes: Some(vec!["x:12".parse().unwrap(), "1.1.1.1:16".parse().unwrap()]), info: Info { private: Some(true), piece_length: Bytes(16 * 1024), @@ -130,6 +137,7 @@ mod tests { let value = Metainfo { announce: "announce".into(), announce_list: Some(vec![vec!["announce".into(), "b".into()], vec!["c".into()]]), + nodes: Some(vec!["x:12".parse().unwrap(), "1.1.1.1:16".parse().unwrap()]), comment: Some("comment".into()), created_by: Some("created by".into()), creation_date: Some(1), diff --git a/src/node.rs b/src/node.rs new file mode 100644 index 0000000..b266747 --- /dev/null +++ b/src/node.rs @@ -0,0 +1,148 @@ +use crate::common::*; + +#[derive(Debug, PartialEq, Clone)] +pub(crate) struct Node { + host: Host, + port: u16, +} + +impl FromStr for Node { + type Err = Error; + + fn from_str(text: &str) -> Result { + let socket_address_re = Regex::new( + r"(?x) + ^ + (?P.*?) + : + (?P\d+?) + $ + ", + ) + .unwrap(); + + if let Some(captures) = socket_address_re.captures(text) { + let host_text = captures.name("host").unwrap().as_str(); + let port_text = captures.name("port").unwrap().as_str(); + + let host = Host::parse(&host_text).context(error::NodeParseHost { + text: text.to_owned(), + })?; + + let port = port_text.parse::().context(error::NodeParsePort { + text: text.to_owned(), + })?; + + Ok(Self { host, port }) + } else { + Err(Error::NodeParsePortMissing { + text: text.to_owned(), + }) + } + } +} + +impl Display for Node { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}:{}", self.host, self.port) + } +} + +#[derive(Serialize, Deserialize)] +struct Tuple(String, u16); + +impl From<&Node> for Tuple { + fn from(node: &Node) -> Self { + let host = match &node.host { + Host::Domain(domain) => domain.to_string(), + Host::Ipv4(ipv4) => ipv4.to_string(), + Host::Ipv6(ipv6) => ipv6.to_string(), + }; + Self(host, node.port) + } +} + +impl Serialize for Node { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + Tuple::from(self).serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Node { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let tuple = Tuple::deserialize(deserializer)?; + + let host = if tuple.0.contains(':') { + Host::parse(&format!("[{}]", tuple.0)) + } else { + Host::parse(&tuple.0) + } + .map_err(|error| D::Error::custom(format!("Failed to parse node host: {}", error)))?; + + Ok(Node { + host, + port: tuple.1, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::net::{Ipv4Addr, Ipv6Addr}; + + fn case(host: Host, port: u16, text: &str, bencode: &str) { + let node = Node { host, port }; + let parsed: Node = text.parse().expect(&format!("Failed to parse {}", text)); + assert_eq!(parsed, node); + let ser = bendy::serde::to_bytes(&node).unwrap(); + assert_eq!( + ser, + bencode.as_bytes(), + "Unexpected serialization: {} != {}", + String::from_utf8_lossy(&ser), + bencode, + ); + let de = bendy::serde::from_bytes::(&ser).unwrap(); + assert_eq!(de, node); + } + + #[test] + fn test_domain() { + case( + Host::Domain("imdl.com".to_owned()), + 12, + "imdl.com:12", + "l8:imdl.comi12ee", + ); + } + + #[test] + fn test_ipv4() { + case( + Host::Ipv4(Ipv4Addr::new(1, 2, 3, 4)), + 100, + "1.2.3.4:100", + "l7:1.2.3.4i100ee", + ); + } + + #[test] + fn test_ipv6() { + case( + Host::Ipv6(Ipv6Addr::new( + 0x1234, 0x5678, 0x9ABC, 0xDEF0, 0x1234, 0x5678, 0x9ABC, 0xDEF0, + )), + 65000, + "[1234:5678:9abc:def0:1234:5678:9abc:def0]:65000", + "l39:1234:5678:9abc:def0:1234:5678:9abc:def0i65000ee", + ); + } +} diff --git a/src/opt/torrent/create.rs b/src/opt/torrent/create.rs index 4c96556..c0f8b21 100644 --- a/src/opt/torrent/create.rs +++ b/src/opt/torrent/create.rs @@ -43,6 +43,16 @@ Note: Many BitTorrent clients do not implement the behavior described in BEP 12. long_help = "Include `COMMENT` in generated `.torrent` file. Stored under `comment` key of top-level metainfo dictionary." )] comment: Option, + #[structopt( + name = "NODE", + long = "dht-node", + help = "Add DHT bootstrap node `NODE` to torrent. `NODE` should be in the form `HOST:PORT`.", + long_help = "Add DHT bootstrap node `NODE` to torrent. `NODE` should be in the form `HOST:PORT`, where `HOST` is a domain name, an IPv4 address, or an IPv6 address surrounded by brackets. May be given more than once to add multiple bootstrap nodes. Examples: + `--dht-node router.example.com:1337` + `--dht-node 203.0.113.0:2290` + `--dht-node [2001:db8:4275:7920:6269:7463:6f69:6e21]:8832`" + )] + dht_nodes: Vec, #[structopt( name = "FOLLOW-SYMLINKS", long = "follow-symlinks", @@ -197,7 +207,7 @@ impl Create { None => filename .to_str() .ok_or_else(|| Error::FilenameDecode { - filename: filename.to_os_string(), + filename: PathBuf::from(filename), })? .to_owned(), }; @@ -255,6 +265,11 @@ impl Create { } else { Some(announce_list) }, + nodes: if self.dht_nodes.is_empty() { + None + } else { + Some(self.dht_nodes) + }, creation_date, created_by, info, @@ -370,7 +385,7 @@ mod tests { } }; env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.private, None); } @@ -383,7 +398,7 @@ mod tests { } }; env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.private, Some(true)); } @@ -407,7 +422,7 @@ mod tests { } }; env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.announce, "http://bar/"); assert!(metainfo.announce_list.is_none()); } @@ -426,7 +441,7 @@ mod tests { } }; env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!( metainfo.announce, "udp://tracker.opentrackr.org:1337/announce" @@ -439,7 +454,7 @@ mod tests { let mut env = environment(&["--input", "foo", "--announce", "wss://tracker.btorrent.xyz"]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.announce, "wss://tracker.btorrent.xyz/"); assert!(metainfo.announce_list.is_none()); } @@ -456,7 +471,7 @@ mod tests { ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.announce, "http://bar/"); assert_eq!( metainfo.announce_list, @@ -478,7 +493,7 @@ mod tests { ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.announce, "http://bar/"); assert_eq!( metainfo.announce_list, @@ -494,7 +509,7 @@ mod tests { let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.comment, None); } @@ -510,7 +525,7 @@ mod tests { ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.comment.unwrap(), "Hello, world!"); } @@ -519,7 +534,7 @@ mod tests { let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.piece_length, Bytes::from(16 * 2u32.pow(10))); } @@ -535,7 +550,7 @@ mod tests { ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.piece_length, Bytes(64 * 1024)); } @@ -551,7 +566,7 @@ mod tests { ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.piece_length, Bytes(512 * 1024)); } @@ -567,7 +582,7 @@ mod tests { ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.name, "foo"); } @@ -585,7 +600,7 @@ mod tests { fs::create_dir(&dir).unwrap(); fs::write(dir.join("bar"), "").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo/bar.torrent"); + let metainfo = env.load_metainfo("foo/bar.torrent"); assert_eq!(metainfo.info.name, "bar"); } @@ -601,7 +616,7 @@ mod tests { ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - env.load_torrent("x.torrent"); + env.load_metainfo("x.torrent"); } #[test] @@ -609,7 +624,7 @@ mod tests { let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.created_by.unwrap(), consts::CREATED_BY_DEFAULT); } @@ -624,7 +639,7 @@ mod tests { ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.created_by, None); } @@ -633,7 +648,7 @@ mod tests { let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.encoding, Some("UTF-8".into())); } @@ -646,7 +661,7 @@ mod tests { .unwrap() .as_secs(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert!(metainfo.creation_date.unwrap() < now + 10); assert!(metainfo.creation_date.unwrap() > now - 10); } @@ -662,7 +677,7 @@ mod tests { ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.creation_date, None); } @@ -672,7 +687,7 @@ mod tests { let contents = "bar"; fs::write(env.resolve("foo"), contents).unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes()); assert_eq!( metainfo.info.mode, @@ -698,7 +713,7 @@ mod tests { let contents = "bar"; fs::write(env.resolve("foo"), contents).unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); let pieces = Sha1::from("b") .digest() .bytes() @@ -724,7 +739,7 @@ mod tests { let contents = ""; fs::write(env.resolve("foo"), contents).unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.pieces.len(), 0); assert_eq!( metainfo.info.mode, @@ -741,7 +756,7 @@ mod tests { let dir = env.resolve("foo"); fs::create_dir(&dir).unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.pieces.len(), 0); assert_eq!(metainfo.info.mode, Mode::Multiple { files: Vec::new() }) } @@ -755,7 +770,7 @@ mod tests { let contents = "bar"; fs::write(file, contents).unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes()); match metainfo.info.mode { Mode::Multiple { files } => { @@ -781,7 +796,7 @@ mod tests { let contents = "bar"; fs::write(file, contents).unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes()); match metainfo.info.mode { Mode::Multiple { files } => { @@ -807,7 +822,7 @@ mod tests { fs::write(dir.join("x"), "xyz").unwrap(); fs::write(dir.join("h"), "hij").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!( metainfo.info.pieces, Sha1::from("abchijxyz").digest().bytes() @@ -924,7 +939,7 @@ mod tests { let dir = env.resolve("foo"); fs::create_dir(&dir).unwrap(); env.run().unwrap(); - env.load_torrent("foo.torrent"); + env.load_metainfo("foo.torrent"); } #[test] @@ -972,7 +987,7 @@ mod tests { let dir = env.resolve("foo"); fs::create_dir(&dir).unwrap(); env.run().unwrap(); - env.load_torrent("foo.torrent"); + env.load_metainfo("foo.torrent"); } #[test] @@ -1053,7 +1068,7 @@ Content Size 9 bytes fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo.torrent"), "foo").unwrap(); env.run().unwrap(); - env.load_torrent("foo.torrent"); + env.load_metainfo("foo.torrent"); } #[test] @@ -1064,7 +1079,7 @@ Content Size 9 bytes fs::write(dir.join("Thumbs.db"), "abc").unwrap(); fs::write(dir.join("Desktop.ini"), "abc").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.is_empty() @@ -1086,7 +1101,7 @@ Content Size 9 bytes fs::write(dir.join("Thumbs.db"), "abc").unwrap(); fs::write(dir.join("Desktop.ini"), "abc").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.len() == 2 @@ -1121,7 +1136,7 @@ Content Size 9 bytes .unwrap(); } env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.len() == 0 @@ -1142,7 +1157,7 @@ Content Size 9 bytes fs::create_dir(&dir).unwrap(); fs::write(dir.join(".hidden"), "abc").unwrap(); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.len() == 1 @@ -1185,7 +1200,7 @@ Content Size 9 bytes let mut env = environment(&["--input", "foo", "--announce", "http://bar", "--md5sum"]); populate_symlinks(&env); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.is_empty() @@ -1206,7 +1221,7 @@ Content Size 9 bytes ]); populate_symlinks(&env); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_eq!(metainfo.info.pieces, Sha1::from("barbaz").digest().bytes()); match metainfo.info.mode { Mode::Multiple { files } => { @@ -1253,7 +1268,7 @@ Content Size 9 bytes env.create_dir("foo/.bar"); env.create_file("foo/.bar/baz", "baz"); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.is_empty() @@ -1286,7 +1301,7 @@ Content Size 9 bytes .unwrap(); } env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.is_empty() @@ -1302,7 +1317,7 @@ Content Size 9 bytes env.create_file("foo/b", "b"); env.create_file("foo/c", "c"); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.len() == 2 @@ -1318,7 +1333,7 @@ Content Size 9 bytes env.create_file("foo/b", "b"); env.create_file("foo/c", "c"); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.len() == 3 @@ -1341,7 +1356,7 @@ Content Size 9 bytes env.create_file("foo/b", "b"); env.create_file("foo/c", "c"); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.len() == 2 @@ -1357,7 +1372,7 @@ Content Size 9 bytes env.create_file("foo/b", "b"); env.create_file("foo/c", "c"); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.is_empty() @@ -1384,11 +1399,68 @@ Content Size 9 bytes env.create_file("foo/b", "b"); env.create_file("foo/c", "c"); env.run().unwrap(); - let metainfo = env.load_torrent("foo.torrent"); + let metainfo = env.load_metainfo("foo.torrent"); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.len() == 1 ); assert_eq!(metainfo.info.pieces, Sha1::from("a").digest().bytes()); } + + #[test] + fn nodes_default() { + let mut env = env! { + args: ["--input", "foo", "--announce", "http://bar"], + tree: { + foo: "", + } + }; + env.run().unwrap(); + let metainfo = env.load_metainfo("foo.torrent"); + assert!(metainfo.nodes.is_none()); + } + + #[test] + fn nodes_invalid() { + let mut env = env! { + args: ["--input", "foo", "--announce", "http://bar", "--dht-node", "blah"], + tree: { + foo: "", + }, + }; + assert_matches!(env.run(), Err(Error::Clap { .. })); + } + + #[test] + fn nodes_valid() { + let mut env = env! { + args: [ + "--input", + "foo", + "--announce", + "http://bar", + "--dht-node", + "router.example.com:1337", + "--dht-node", + "203.0.113.0:2290", + "--dht-node", + "[2001:db8:4275:7920:6269:7463:6f69:6e21]:8832", + ], + tree: { + foo: "", + }, + }; + env.run().unwrap(); + let metainfo = env.load_metainfo("foo.torrent"); + assert_eq!( + metainfo.nodes, + Some(vec![ + "router.example.com:1337".parse().unwrap(), + "203.0.113.0:2290".parse().unwrap(), + "[2001:db8:4275:7920:6269:7463:6f69:6e21]:8832" + .parse() + .unwrap(), + ]), + ); + } } diff --git a/src/opt/torrent/show.rs b/src/opt/torrent/show.rs index b26e152..d872d92 100644 --- a/src/opt/torrent/show.rs +++ b/src/opt/torrent/show.rs @@ -36,6 +36,11 @@ mod tests { let metainfo = Metainfo { announce: "announce".into(), announce_list: Some(vec![vec!["announce".into(), "b".into()], vec!["c".into()]]), + nodes: Some(vec![ + "x:12".parse().unwrap(), + "1.1.1.1:16".parse().unwrap(), + "[2001:0db8:85a3::0000:8a2e:0370]:7334".parse().unwrap(), + ]), comment: Some("comment".into()), created_by: Some("created by".into()), creation_date: Some(1), @@ -74,12 +79,15 @@ mod tests { Created By created by Source source Info Hash b7595205a46491b3e8686e10b28efe7144d066cc -Torrent Size 252 bytes +Torrent Size 319 bytes Content Size 20 bytes Private yes Trackers Tier 1: announce b Tier 2: c + DHT Nodes x:12 + 1.1.1.1:16 + [2001:db8:85a3::8a2e:370]:7334 Piece Size 16 KiB Piece Count 1 File Count 1 @@ -108,10 +116,11 @@ Created\t1970-01-01 00:00:01 UTC Created By\tcreated by Source\tsource Info Hash\tb7595205a46491b3e8686e10b28efe7144d066cc -Torrent Size\t252 +Torrent Size\t319 Content Size\t20 Private\tyes Trackers\tannounce\tb\tc +DHT Nodes\tx:12\t1.1.1.1:16\t[2001:db8:85a3::8a2e:370]:7334 Piece Size\t16384 Piece Count\t1 File Count\t1 @@ -129,6 +138,11 @@ Files\tfoo announce_list: Some(vec![vec!["x".into()], vec!["y".into()], vec!["z".into()]]), comment: Some("comment".into()), created_by: Some("created by".into()), + nodes: Some(vec![ + "x:12".parse().unwrap(), + "1.1.1.1:16".parse().unwrap(), + "[2001:0db8:85a3::0000:8a2e:0370]:7334".parse().unwrap(), + ]), creation_date: Some(1), encoding: Some("UTF-8".into()), info: Info { @@ -165,13 +179,16 @@ Files\tfoo Created By created by Source source Info Hash b7595205a46491b3e8686e10b28efe7144d066cc -Torrent Size 240 bytes +Torrent Size 307 bytes Content Size 20 bytes Private yes Trackers a x y z + DHT Nodes x:12 + 1.1.1.1:16 + [2001:db8:85a3::8a2e:370]:7334 Piece Size 16 KiB Piece Count 1 File Count 1 @@ -200,10 +217,11 @@ Created\t1970-01-01 00:00:01 UTC Created By\tcreated by Source\tsource Info Hash\tb7595205a46491b3e8686e10b28efe7144d066cc -Torrent Size\t240 +Torrent Size\t307 Content Size\t20 Private\tyes Trackers\ta\tx\ty\tz +DHT Nodes\tx:12\t1.1.1.1:16\t[2001:db8:85a3::8a2e:370]:7334 Piece Size\t16384 Piece Count\t1 File Count\t1 @@ -220,6 +238,11 @@ Files\tfoo announce: "a".into(), announce_list: Some(vec![vec!["b".into()], vec!["c".into()], vec!["a".into()]]), comment: Some("comment".into()), + nodes: Some(vec![ + "x:12".parse().unwrap(), + "1.1.1.1:16".parse().unwrap(), + "[2001:0db8:85a3::8a2e:0370]:7334".parse().unwrap(), + ]), created_by: Some("created by".into()), creation_date: Some(1), encoding: Some("UTF-8".into()), @@ -257,12 +280,15 @@ Files\tfoo Created By created by Source source Info Hash b7595205a46491b3e8686e10b28efe7144d066cc -Torrent Size 240 bytes +Torrent Size 307 bytes Content Size 20 bytes Private yes Trackers b c a + DHT Nodes x:12 + 1.1.1.1:16 + [2001:db8:85a3::8a2e:370]:7334 Piece Size 16 KiB Piece Count 1 File Count 1 @@ -291,10 +317,11 @@ Created\t1970-01-01 00:00:01 UTC Created By\tcreated by Source\tsource Info Hash\tb7595205a46491b3e8686e10b28efe7144d066cc -Torrent Size\t240 +Torrent Size\t307 Content Size\t20 Private\tyes Trackers\tb\tc\ta +DHT Nodes\tx:12\t1.1.1.1:16\t[2001:db8:85a3::8a2e:370]:7334 Piece Size\t16384 Piece Count\t1 File Count\t1 diff --git a/src/test_env.rs b/src/test_env.rs index 81895af..b447c6d 100644 --- a/src/test_env.rs +++ b/src/test_env.rs @@ -31,7 +31,7 @@ impl TestEnv { fs::write(self.env.resolve(path), bytes.as_ref()).unwrap(); } - pub(crate) fn load_torrent(&self, filename: impl AsRef) -> Metainfo { + pub(crate) fn load_metainfo(&self, filename: impl AsRef) -> Metainfo { Metainfo::load(self.env.resolve(filename.as_ref())).unwrap() } } diff --git a/src/torrent_summary.rs b/src/torrent_summary.rs index 23f9691..d9f4fe9 100644 --- a/src/torrent_summary.rs +++ b/src/torrent_summary.rs @@ -142,6 +142,16 @@ impl TorrentSummary { None => table.row("Tracker", &self.metainfo.announce), } + if let Some(nodes) = &self.metainfo.nodes { + table.list( + "DHT Nodes", + nodes + .iter() + .map(ToString::to_string) + .collect::>(), + ); + } + table.size("Piece Size", self.metainfo.info.piece_length); table.row("Piece Count", self.metainfo.info.pieces.len() / 20);