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 |
| [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 |

View File

@ -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,

View File

@ -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 },
}

View File

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

View File

@ -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<String>,
#[serde(rename = "created by")]
#[serde(
rename = "created by",
skip_serializing_if = "Option::is_none",
default,
with = "unwrap_or_skip"
)]
pub(crate) created_by: Option<String>,
#[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<String>,
pub(crate) info: Info,
#[serde(
skip_serializing_if = "Option::is_none",
default,
with = "unwrap_or_skip"
)]
pub(crate) nodes: Option<Vec<Node>>,
}
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),

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."
)]
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(
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(),
]),
);
}
}

View File

@ -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

View File

@ -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<Path>) -> Metainfo {
pub(crate) fn load_metainfo(&self, filename: impl AsRef<Path>) -> Metainfo {
Metainfo::load(self.env.resolve(filename.as_ref())).unwrap()
}
}

View File

@ -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::<Vec<String>>(),
);
}
table.size("Piece Size", self.metainfo.info.piece_length);
table.row("Piece Count", self.metainfo.info.pieces.len() / 20);