Support adding DHT bootstrap nodes to created torrents

The --dht-node flag can be used to add DHT bootstrap nodes to new torrents.

This is the only piece of metainfo-related functionality in BEP 5, so we can mark BEP
5 as implemented.

type: added
This commit is contained in:
Casey Rodarmor 2020-02-14 02:16:19 -08:00
parent 6549850dac
commit 165a7ea444
No known key found for this signature in database
GPG Key ID: 556186B153EC6FE0
10 changed files with 338 additions and 63 deletions

View File

@ -81,7 +81,7 @@ at any time.
| [02](http://bittorrent.org/beps/bep_0002.html) | :heavy_minus_sign: | Sample reStructured Text BEP Template | | [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 | | [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 | | [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 | | [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 | | [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 | | [08](http://bittorrent.org/beps/bep_0008.html) | :heavy_minus_sign: | Tracker Peer Obfuscation |

View File

@ -11,7 +11,7 @@ pub(crate) use std::{
hash::Hash, hash::Hash,
io::{self, Read, Write}, io::{self, Read, Write},
iter::{self, Sum}, iter::{self, Sum},
num::{ParseFloatError, TryFromIntError}, num::{ParseFloatError, ParseIntError, TryFromIntError},
ops::{AddAssign, Div, DivAssign, Mul, MulAssign, SubAssign}, ops::{AddAssign, Div, DivAssign, Mul, MulAssign, SubAssign},
path::{self, Path, PathBuf}, path::{self, Path, PathBuf},
process::{self, Command, ExitStatus}, process::{self, Command, ExitStatus},
@ -26,7 +26,7 @@ pub(crate) use chrono::{TimeZone, Utc};
pub(crate) use globset::{Glob, GlobMatcher}; pub(crate) use globset::{Glob, GlobMatcher};
pub(crate) use libc::EXIT_FAILURE; pub(crate) use libc::EXIT_FAILURE;
pub(crate) use regex::{Regex, RegexSet}; 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_hex::SerHex;
pub(crate) use serde_with::rust::unwrap_or_skip; pub(crate) use serde_with::rust::unwrap_or_skip;
pub(crate) use sha1::Sha1; pub(crate) use sha1::Sha1;
@ -37,7 +37,7 @@ pub(crate) use structopt::{
StructOpt, StructOpt,
}; };
pub(crate) use unicode_width::UnicodeWidthStr; pub(crate) use unicode_width::UnicodeWidthStr;
pub(crate) use url::Url; pub(crate) use url::{Host, Url};
pub(crate) use walkdir::WalkDir; pub(crate) use walkdir::WalkDir;
// modules // modules
@ -53,7 +53,7 @@ pub(crate) use crate::{
pub(crate) use crate::{ pub(crate) use crate::{
bytes::Bytes, env::Env, error::Error, file_info::FileInfo, file_path::FilePath, 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, 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, piece_length_picker::PieceLengthPicker, platform::Platform, status::Status, style::Style,
table::Table, target::Target, torrent_summary::TorrentSummary, use_color::UseColor, table::Table, target::Target, torrent_summary::TorrentSummary, use_color::UseColor,
verifier::Verifier, walker::Walker, verifier::Verifier, walker::Walker,

View File

@ -29,14 +29,25 @@ pub(crate) enum Error {
CommandInvoke { command: String, source: io::Error }, CommandInvoke { command: String, source: io::Error },
#[snafu(display("Command `{}` returned bad exit status: {}", command, status))] #[snafu(display("Command `{}` returned bad exit status: {}", command, status))]
CommandStatus { command: String, status: ExitStatus }, CommandStatus { command: String, status: ExitStatus },
#[snafu(display("Filename was not valid unicode: {}", filename.to_string_lossy()))] #[snafu(display("Filename was not valid unicode: {}", filename.display()))]
FilenameDecode { filename: OsString }, FilenameDecode { filename: PathBuf },
#[snafu(display("Path had no file name: {}", path.display()))] #[snafu(display("Path had no file name: {}", path.display()))]
FilenameExtract { path: PathBuf }, FilenameExtract { path: PathBuf },
#[snafu(display("I/O error at `{}`: {}", path.display(), source))] #[snafu(display("I/O error at `{}`: {}", path.display(), source))]
Filesystem { source: io::Error, path: PathBuf }, Filesystem { source: io::Error, path: PathBuf },
#[snafu(display("Invalid glob: {}", source))] #[snafu(display("Invalid glob: {}", source))]
GlobParse { source: globset::Error }, 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(",")))] #[snafu(display("Failed to find opener utility, please install one of {}", tried.join(",")))]
OpenerMissing { tried: &'static [&'static str] }, OpenerMissing { tried: &'static [&'static str] },
#[snafu(display( #[snafu(display(
@ -104,8 +115,6 @@ pub(crate) enum Error {
feature feature
))] ))]
Unstable { feature: &'static str }, Unstable { feature: &'static str },
#[snafu(display("Unknown lint: {}", text))]
LintUnknown { text: String },
#[snafu(display("Torrent verification failed: {}", status))] #[snafu(display("Torrent verification failed: {}", status))]
Verify { status: Status }, Verify { status: Status },
} }

View File

@ -72,6 +72,7 @@ mod linter;
mod md5_digest; mod md5_digest;
mod metainfo; mod metainfo;
mod mode; mod mode;
mod node;
mod opt; mod opt;
mod path_ext; mod path_ext;
mod piece_length_picker; mod piece_length_picker;

View File

@ -3,8 +3,8 @@ use crate::common::*;
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
pub(crate) struct Metainfo { pub(crate) struct Metainfo {
pub(crate) announce: String, pub(crate) announce: String,
#[serde(rename = "announce-list")]
#[serde( #[serde(
rename = "announce-list",
skip_serializing_if = "Option::is_none", skip_serializing_if = "Option::is_none",
default, default,
with = "unwrap_or_skip" with = "unwrap_or_skip"
@ -16,15 +16,15 @@ pub(crate) struct Metainfo {
with = "unwrap_or_skip" with = "unwrap_or_skip"
)] )]
pub(crate) comment: Option<String>, pub(crate) comment: Option<String>,
#[serde(rename = "created by")]
#[serde( #[serde(
rename = "created by",
skip_serializing_if = "Option::is_none", skip_serializing_if = "Option::is_none",
default, default,
with = "unwrap_or_skip" with = "unwrap_or_skip"
)] )]
pub(crate) created_by: Option<String>, pub(crate) created_by: Option<String>,
#[serde(rename = "creation date")]
#[serde( #[serde(
rename = "creation date",
skip_serializing_if = "Option::is_none", skip_serializing_if = "Option::is_none",
default, default,
with = "unwrap_or_skip" with = "unwrap_or_skip"
@ -37,6 +37,12 @@ pub(crate) struct Metainfo {
)] )]
pub(crate) encoding: Option<String>, pub(crate) encoding: Option<String>,
pub(crate) info: Info, pub(crate) info: Info,
#[serde(
skip_serializing_if = "Option::is_none",
default,
with = "unwrap_or_skip"
)]
pub(crate) nodes: Option<Vec<Node>>,
} }
impl Metainfo { impl Metainfo {
@ -103,6 +109,7 @@ mod tests {
created_by: Some("created by".into()), created_by: Some("created by".into()),
creation_date: Some(1), creation_date: Some(1),
encoding: Some("UTF-8".into()), encoding: Some("UTF-8".into()),
nodes: Some(vec!["x:12".parse().unwrap(), "1.1.1.1:16".parse().unwrap()]),
info: Info { info: Info {
private: Some(true), private: Some(true),
piece_length: Bytes(16 * 1024), piece_length: Bytes(16 * 1024),
@ -130,6 +137,7 @@ mod tests {
let value = Metainfo { let value = Metainfo {
announce: "announce".into(), announce: "announce".into(),
announce_list: Some(vec![vec!["announce".into(), "b".into()], vec!["c".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()), comment: Some("comment".into()),
created_by: Some("created by".into()), created_by: Some("created by".into()),
creation_date: Some(1), creation_date: Some(1),

148
src/node.rs Normal file
View File

@ -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<Self, Self::Err> {
let socket_address_re = Regex::new(
r"(?x)
^
(?P<host>.*?)
:
(?P<port>\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::<u16>().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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Tuple::from(self).serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Node {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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::<Node>(&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",
);
}
}

View File

@ -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." long_help = "Include `COMMENT` in generated `.torrent` file. Stored under `comment` key of top-level metainfo dictionary."
)] )]
comment: Option<String>, comment: Option<String>,
#[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<Node>,
#[structopt( #[structopt(
name = "FOLLOW-SYMLINKS", name = "FOLLOW-SYMLINKS",
long = "follow-symlinks", long = "follow-symlinks",
@ -197,7 +207,7 @@ impl Create {
None => filename None => filename
.to_str() .to_str()
.ok_or_else(|| Error::FilenameDecode { .ok_or_else(|| Error::FilenameDecode {
filename: filename.to_os_string(), filename: PathBuf::from(filename),
})? })?
.to_owned(), .to_owned(),
}; };
@ -255,6 +265,11 @@ impl Create {
} else { } else {
Some(announce_list) Some(announce_list)
}, },
nodes: if self.dht_nodes.is_empty() {
None
} else {
Some(self.dht_nodes)
},
creation_date, creation_date,
created_by, created_by,
info, info,
@ -370,7 +385,7 @@ mod tests {
} }
}; };
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_eq!(metainfo.info.private, None); assert_eq!(metainfo.info.private, None);
} }
@ -383,7 +398,7 @@ mod tests {
} }
}; };
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_eq!(metainfo.info.private, Some(true)); assert_eq!(metainfo.info.private, Some(true));
} }
@ -407,7 +422,7 @@ mod tests {
} }
}; };
env.run().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, "http://bar/");
assert!(metainfo.announce_list.is_none()); assert!(metainfo.announce_list.is_none());
} }
@ -426,7 +441,7 @@ mod tests {
} }
}; };
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_eq!( assert_eq!(
metainfo.announce, metainfo.announce,
"udp://tracker.opentrackr.org:1337/announce" "udp://tracker.opentrackr.org:1337/announce"
@ -439,7 +454,7 @@ mod tests {
let mut env = environment(&["--input", "foo", "--announce", "wss://tracker.btorrent.xyz"]); let mut env = environment(&["--input", "foo", "--announce", "wss://tracker.btorrent.xyz"]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().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_eq!(metainfo.announce, "wss://tracker.btorrent.xyz/");
assert!(metainfo.announce_list.is_none()); assert!(metainfo.announce_list.is_none());
} }
@ -456,7 +471,7 @@ mod tests {
]); ]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().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, "http://bar/");
assert_eq!( assert_eq!(
metainfo.announce_list, metainfo.announce_list,
@ -478,7 +493,7 @@ mod tests {
]); ]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().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, "http://bar/");
assert_eq!( assert_eq!(
metainfo.announce_list, metainfo.announce_list,
@ -494,7 +509,7 @@ mod tests {
let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); let mut env = environment(&["--input", "foo", "--announce", "http://bar"]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_eq!(metainfo.comment, None); assert_eq!(metainfo.comment, None);
} }
@ -510,7 +525,7 @@ mod tests {
]); ]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().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!"); assert_eq!(metainfo.comment.unwrap(), "Hello, world!");
} }
@ -519,7 +534,7 @@ mod tests {
let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); let mut env = environment(&["--input", "foo", "--announce", "http://bar"]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().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))); assert_eq!(metainfo.info.piece_length, Bytes::from(16 * 2u32.pow(10)));
} }
@ -535,7 +550,7 @@ mod tests {
]); ]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().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)); assert_eq!(metainfo.info.piece_length, Bytes(64 * 1024));
} }
@ -551,7 +566,7 @@ mod tests {
]); ]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().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)); assert_eq!(metainfo.info.piece_length, Bytes(512 * 1024));
} }
@ -567,7 +582,7 @@ mod tests {
]); ]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_eq!(metainfo.info.name, "foo"); assert_eq!(metainfo.info.name, "foo");
} }
@ -585,7 +600,7 @@ mod tests {
fs::create_dir(&dir).unwrap(); fs::create_dir(&dir).unwrap();
fs::write(dir.join("bar"), "").unwrap(); fs::write(dir.join("bar"), "").unwrap();
env.run().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"); assert_eq!(metainfo.info.name, "bar");
} }
@ -601,7 +616,7 @@ mod tests {
]); ]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
env.load_torrent("x.torrent"); env.load_metainfo("x.torrent");
} }
#[test] #[test]
@ -609,7 +624,7 @@ mod tests {
let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); let mut env = environment(&["--input", "foo", "--announce", "http://bar"]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().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); assert_eq!(metainfo.created_by.unwrap(), consts::CREATED_BY_DEFAULT);
} }
@ -624,7 +639,7 @@ mod tests {
]); ]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_eq!(metainfo.created_by, None); assert_eq!(metainfo.created_by, None);
} }
@ -633,7 +648,7 @@ mod tests {
let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); let mut env = environment(&["--input", "foo", "--announce", "http://bar"]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().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())); assert_eq!(metainfo.encoding, Some("UTF-8".into()));
} }
@ -646,7 +661,7 @@ mod tests {
.unwrap() .unwrap()
.as_secs(); .as_secs();
env.run().unwrap(); 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);
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(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_eq!(metainfo.creation_date, None); assert_eq!(metainfo.creation_date, None);
} }
@ -672,7 +687,7 @@ mod tests {
let contents = "bar"; let contents = "bar";
fs::write(env.resolve("foo"), contents).unwrap(); fs::write(env.resolve("foo"), contents).unwrap();
env.run().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.pieces, Sha1::from(contents).digest().bytes());
assert_eq!( assert_eq!(
metainfo.info.mode, metainfo.info.mode,
@ -698,7 +713,7 @@ mod tests {
let contents = "bar"; let contents = "bar";
fs::write(env.resolve("foo"), contents).unwrap(); fs::write(env.resolve("foo"), contents).unwrap();
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
let pieces = Sha1::from("b") let pieces = Sha1::from("b")
.digest() .digest()
.bytes() .bytes()
@ -724,7 +739,7 @@ mod tests {
let contents = ""; let contents = "";
fs::write(env.resolve("foo"), contents).unwrap(); fs::write(env.resolve("foo"), contents).unwrap();
env.run().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.pieces.len(), 0);
assert_eq!( assert_eq!(
metainfo.info.mode, metainfo.info.mode,
@ -741,7 +756,7 @@ mod tests {
let dir = env.resolve("foo"); let dir = env.resolve("foo");
fs::create_dir(&dir).unwrap(); fs::create_dir(&dir).unwrap();
env.run().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.pieces.len(), 0);
assert_eq!(metainfo.info.mode, Mode::Multiple { files: Vec::new() }) assert_eq!(metainfo.info.mode, Mode::Multiple { files: Vec::new() })
} }
@ -755,7 +770,7 @@ mod tests {
let contents = "bar"; let contents = "bar";
fs::write(file, contents).unwrap(); fs::write(file, contents).unwrap();
env.run().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.pieces, Sha1::from(contents).digest().bytes());
match metainfo.info.mode { match metainfo.info.mode {
Mode::Multiple { files } => { Mode::Multiple { files } => {
@ -781,7 +796,7 @@ mod tests {
let contents = "bar"; let contents = "bar";
fs::write(file, contents).unwrap(); fs::write(file, contents).unwrap();
env.run().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.pieces, Sha1::from(contents).digest().bytes());
match metainfo.info.mode { match metainfo.info.mode {
Mode::Multiple { files } => { Mode::Multiple { files } => {
@ -807,7 +822,7 @@ mod tests {
fs::write(dir.join("x"), "xyz").unwrap(); fs::write(dir.join("x"), "xyz").unwrap();
fs::write(dir.join("h"), "hij").unwrap(); fs::write(dir.join("h"), "hij").unwrap();
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_eq!( assert_eq!(
metainfo.info.pieces, metainfo.info.pieces,
Sha1::from("abchijxyz").digest().bytes() Sha1::from("abchijxyz").digest().bytes()
@ -924,7 +939,7 @@ mod tests {
let dir = env.resolve("foo"); let dir = env.resolve("foo");
fs::create_dir(&dir).unwrap(); fs::create_dir(&dir).unwrap();
env.run().unwrap(); env.run().unwrap();
env.load_torrent("foo.torrent"); env.load_metainfo("foo.torrent");
} }
#[test] #[test]
@ -972,7 +987,7 @@ mod tests {
let dir = env.resolve("foo"); let dir = env.resolve("foo");
fs::create_dir(&dir).unwrap(); fs::create_dir(&dir).unwrap();
env.run().unwrap(); env.run().unwrap();
env.load_torrent("foo.torrent"); env.load_metainfo("foo.torrent");
} }
#[test] #[test]
@ -1053,7 +1068,7 @@ Content Size 9 bytes
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
fs::write(env.resolve("foo.torrent"), "foo").unwrap(); fs::write(env.resolve("foo.torrent"), "foo").unwrap();
env.run().unwrap(); env.run().unwrap();
env.load_torrent("foo.torrent"); env.load_metainfo("foo.torrent");
} }
#[test] #[test]
@ -1064,7 +1079,7 @@ Content Size 9 bytes
fs::write(dir.join("Thumbs.db"), "abc").unwrap(); fs::write(dir.join("Thumbs.db"), "abc").unwrap();
fs::write(dir.join("Desktop.ini"), "abc").unwrap(); fs::write(dir.join("Desktop.ini"), "abc").unwrap();
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.is_empty() 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("Thumbs.db"), "abc").unwrap();
fs::write(dir.join("Desktop.ini"), "abc").unwrap(); fs::write(dir.join("Desktop.ini"), "abc").unwrap();
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.len() == 2 Mode::Multiple { files } if files.len() == 2
@ -1121,7 +1136,7 @@ Content Size 9 bytes
.unwrap(); .unwrap();
} }
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.len() == 0 Mode::Multiple { files } if files.len() == 0
@ -1142,7 +1157,7 @@ Content Size 9 bytes
fs::create_dir(&dir).unwrap(); fs::create_dir(&dir).unwrap();
fs::write(dir.join(".hidden"), "abc").unwrap(); fs::write(dir.join(".hidden"), "abc").unwrap();
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.len() == 1 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"]); let mut env = environment(&["--input", "foo", "--announce", "http://bar", "--md5sum"]);
populate_symlinks(&env); populate_symlinks(&env);
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.is_empty() Mode::Multiple { files } if files.is_empty()
@ -1206,7 +1221,7 @@ Content Size 9 bytes
]); ]);
populate_symlinks(&env); populate_symlinks(&env);
env.run().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("barbaz").digest().bytes()); assert_eq!(metainfo.info.pieces, Sha1::from("barbaz").digest().bytes());
match metainfo.info.mode { match metainfo.info.mode {
Mode::Multiple { files } => { Mode::Multiple { files } => {
@ -1253,7 +1268,7 @@ Content Size 9 bytes
env.create_dir("foo/.bar"); env.create_dir("foo/.bar");
env.create_file("foo/.bar/baz", "baz"); env.create_file("foo/.bar/baz", "baz");
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.is_empty() Mode::Multiple { files } if files.is_empty()
@ -1286,7 +1301,7 @@ Content Size 9 bytes
.unwrap(); .unwrap();
} }
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.is_empty() 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/b", "b");
env.create_file("foo/c", "c"); env.create_file("foo/c", "c");
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.len() == 2 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/b", "b");
env.create_file("foo/c", "c"); env.create_file("foo/c", "c");
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.len() == 3 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/b", "b");
env.create_file("foo/c", "c"); env.create_file("foo/c", "c");
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.len() == 2 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/b", "b");
env.create_file("foo/c", "c"); env.create_file("foo/c", "c");
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.is_empty() 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/b", "b");
env.create_file("foo/c", "c"); env.create_file("foo/c", "c");
env.run().unwrap(); env.run().unwrap();
let metainfo = env.load_torrent("foo.torrent"); let metainfo = env.load_metainfo("foo.torrent");
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.len() == 1 Mode::Multiple { files } if files.len() == 1
); );
assert_eq!(metainfo.info.pieces, Sha1::from("a").digest().bytes()); 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(),
]),
);
}
} }

View File

@ -36,6 +36,11 @@ mod tests {
let metainfo = Metainfo { let metainfo = Metainfo {
announce: "announce".into(), announce: "announce".into(),
announce_list: Some(vec![vec!["announce".into(), "b".into()], vec!["c".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()), comment: Some("comment".into()),
created_by: Some("created by".into()), created_by: Some("created by".into()),
creation_date: Some(1), creation_date: Some(1),
@ -74,12 +79,15 @@ mod tests {
Created By created by Created By created by
Source source Source source
Info Hash b7595205a46491b3e8686e10b28efe7144d066cc Info Hash b7595205a46491b3e8686e10b28efe7144d066cc
Torrent Size 252 bytes Torrent Size 319 bytes
Content Size 20 bytes Content Size 20 bytes
Private yes Private yes
Trackers Tier 1: announce Trackers Tier 1: announce
b b
Tier 2: c Tier 2: c
DHT Nodes x:12
1.1.1.1:16
[2001:db8:85a3::8a2e:370]:7334
Piece Size 16 KiB Piece Size 16 KiB
Piece Count 1 Piece Count 1
File Count 1 File Count 1
@ -108,10 +116,11 @@ Created\t1970-01-01 00:00:01 UTC
Created By\tcreated by Created By\tcreated by
Source\tsource Source\tsource
Info Hash\tb7595205a46491b3e8686e10b28efe7144d066cc Info Hash\tb7595205a46491b3e8686e10b28efe7144d066cc
Torrent Size\t252 Torrent Size\t319
Content Size\t20 Content Size\t20
Private\tyes Private\tyes
Trackers\tannounce\tb\tc Trackers\tannounce\tb\tc
DHT Nodes\tx:12\t1.1.1.1:16\t[2001:db8:85a3::8a2e:370]:7334
Piece Size\t16384 Piece Size\t16384
Piece Count\t1 Piece Count\t1
File Count\t1 File Count\t1
@ -129,6 +138,11 @@ Files\tfoo
announce_list: Some(vec![vec!["x".into()], vec!["y".into()], vec!["z".into()]]), announce_list: Some(vec![vec!["x".into()], vec!["y".into()], vec!["z".into()]]),
comment: Some("comment".into()), comment: Some("comment".into()),
created_by: Some("created by".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), creation_date: Some(1),
encoding: Some("UTF-8".into()), encoding: Some("UTF-8".into()),
info: Info { info: Info {
@ -165,13 +179,16 @@ Files\tfoo
Created By created by Created By created by
Source source Source source
Info Hash b7595205a46491b3e8686e10b28efe7144d066cc Info Hash b7595205a46491b3e8686e10b28efe7144d066cc
Torrent Size 240 bytes Torrent Size 307 bytes
Content Size 20 bytes Content Size 20 bytes
Private yes Private yes
Trackers a Trackers a
x x
y y
z z
DHT Nodes x:12
1.1.1.1:16
[2001:db8:85a3::8a2e:370]:7334
Piece Size 16 KiB Piece Size 16 KiB
Piece Count 1 Piece Count 1
File Count 1 File Count 1
@ -200,10 +217,11 @@ Created\t1970-01-01 00:00:01 UTC
Created By\tcreated by Created By\tcreated by
Source\tsource Source\tsource
Info Hash\tb7595205a46491b3e8686e10b28efe7144d066cc Info Hash\tb7595205a46491b3e8686e10b28efe7144d066cc
Torrent Size\t240 Torrent Size\t307
Content Size\t20 Content Size\t20
Private\tyes Private\tyes
Trackers\ta\tx\ty\tz 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 Size\t16384
Piece Count\t1 Piece Count\t1
File Count\t1 File Count\t1
@ -220,6 +238,11 @@ Files\tfoo
announce: "a".into(), announce: "a".into(),
announce_list: Some(vec![vec!["b".into()], vec!["c".into()], vec!["a".into()]]), announce_list: Some(vec![vec!["b".into()], vec!["c".into()], vec!["a".into()]]),
comment: Some("comment".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()), created_by: Some("created by".into()),
creation_date: Some(1), creation_date: Some(1),
encoding: Some("UTF-8".into()), encoding: Some("UTF-8".into()),
@ -257,12 +280,15 @@ Files\tfoo
Created By created by Created By created by
Source source Source source
Info Hash b7595205a46491b3e8686e10b28efe7144d066cc Info Hash b7595205a46491b3e8686e10b28efe7144d066cc
Torrent Size 240 bytes Torrent Size 307 bytes
Content Size 20 bytes Content Size 20 bytes
Private yes Private yes
Trackers b Trackers b
c c
a a
DHT Nodes x:12
1.1.1.1:16
[2001:db8:85a3::8a2e:370]:7334
Piece Size 16 KiB Piece Size 16 KiB
Piece Count 1 Piece Count 1
File Count 1 File Count 1
@ -291,10 +317,11 @@ Created\t1970-01-01 00:00:01 UTC
Created By\tcreated by Created By\tcreated by
Source\tsource Source\tsource
Info Hash\tb7595205a46491b3e8686e10b28efe7144d066cc Info Hash\tb7595205a46491b3e8686e10b28efe7144d066cc
Torrent Size\t240 Torrent Size\t307
Content Size\t20 Content Size\t20
Private\tyes Private\tyes
Trackers\tb\tc\ta Trackers\tb\tc\ta
DHT Nodes\tx:12\t1.1.1.1:16\t[2001:db8:85a3::8a2e:370]:7334
Piece Size\t16384 Piece Size\t16384
Piece Count\t1 Piece Count\t1
File Count\t1 File Count\t1

View File

@ -31,7 +31,7 @@ impl TestEnv {
fs::write(self.env.resolve(path), bytes.as_ref()).unwrap(); fs::write(self.env.resolve(path), bytes.as_ref()).unwrap();
} }
pub(crate) fn load_torrent(&self, filename: impl AsRef<Path>) -> Metainfo { pub(crate) fn load_metainfo(&self, filename: impl AsRef<Path>) -> Metainfo {
Metainfo::load(self.env.resolve(filename.as_ref())).unwrap() Metainfo::load(self.env.resolve(filename.as_ref())).unwrap()
} }
} }

View File

@ -142,6 +142,16 @@ impl TorrentSummary {
None => table.row("Tracker", &self.metainfo.announce), 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::<Vec<String>>(),
);
}
table.size("Piece Size", self.metainfo.info.piece_length); table.size("Piece Size", self.metainfo.info.piece_length);
table.row("Piece Count", self.metainfo.info.pieces.len() / 20); table.row("Piece Count", self.metainfo.info.pieces.len() / 20);