Add imdl torrent show

The `imdl torrent show` command displays information about on-disk
torrent files. The formatting of the command's output is copied from
torf, an excellent command-line torrent creator, editor, and viewer.

type: added
This commit is contained in:
Casey Rodarmor 2020-02-04 07:55:50 -08:00
parent 6df45e0244
commit 99a069a021
No known key found for this signature in database
GPG Key ID: 556186B153EC6FE0
17 changed files with 543 additions and 45 deletions

109
Cargo.lock generated
View File

@ -2,9 +2,9 @@
# It is not intended for manual editing.
[[package]]
name = "aho-corasick"
version = "0.7.7"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f56c476256dc249def911d6f7580b5fc7e875895b5d7ee88f5d602208035744"
checksum = "743ad5a418686aad3b87fd14c43badd828cf26e214a00f92a384291cf22e1811"
dependencies = [
"memchr",
]
@ -38,6 +38,12 @@ dependencies = [
"winapi 0.3.8",
]
[[package]]
name = "autocfg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
[[package]]
name = "bitflags"
version = "1.2.1"
@ -59,6 +65,17 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "chrono"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01"
dependencies = [
"num-integer",
"num-traits",
"time",
]
[[package]]
name = "clap"
version = "2.33.0"
@ -75,6 +92,22 @@ dependencies = [
"vec_map",
]
[[package]]
name = "ctor"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd8ce37ad4184ab2ce004c33bf6379185d3b1c95801cab51026bd271bf68eedc"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "difference"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
[[package]]
name = "doc-comment"
version = "0.3.1"
@ -155,9 +188,11 @@ version = "0.0.1"
dependencies = [
"ansi_term 0.12.1",
"atty",
"chrono",
"env_logger",
"libc",
"md5",
"pretty_assertions",
"regex",
"serde",
"serde_bencode",
@ -167,6 +202,7 @@ dependencies = [
"static_assertions",
"structopt",
"tempfile",
"unicode-width",
"url",
"walkdir",
]
@ -220,6 +256,34 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3197e20c7edb283f87c071ddfc7a2cca8f8e0b888c242959846a6fce03c72223"
[[package]]
name = "num-integer"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096"
dependencies = [
"autocfg",
]
[[package]]
name = "output_vt100"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
dependencies = [
"winapi 0.3.8",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
@ -233,10 +297,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
[[package]]
name = "proc-macro-error"
version = "0.4.6"
name = "pretty_assertions"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a427176b1223957a219780f6bd014fad03f59543ff7feb3854a6fb8e9626ac2b"
checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427"
dependencies = [
"ansi_term 0.11.0",
"ctor",
"difference",
"output_vt100",
]
[[package]]
name = "proc-macro-error"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875077759af22fa20b610ad4471d8155b321c89c3f2785526c9839b099be4e0a"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
@ -247,9 +323,9 @@ dependencies = [
[[package]]
name = "proc-macro-error-attr"
version = "0.4.6"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3e4daa0eae3f30db348ecfa5f942281173d441588ebdac871d730acbfc7f16"
checksum = "c5717d9fa2664351a01ed73ba5ef6df09c01a521cb42cb65a061432a826f3c7a"
dependencies = [
"proc-macro2",
"quote",
@ -471,9 +547,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "structopt"
version = "0.3.8"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df136b42d76b1fbea72e2ab3057343977b04b4a2e00836c3c7c0673829572713"
checksum = "a1bcbed7d48956fcbb5d80c6b95aedb553513de0a1b451ea92679d999c010e98"
dependencies = [
"clap",
"lazy_static",
@ -482,9 +558,9 @@ dependencies = [
[[package]]
name = "structopt-derive"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd50a87d2f7b8958055f3e73a963d78feaccca3836767a9069844e34b5b03c0a"
checksum = "095064aa1f5b94d14e635d0a5684cf140c43ae40a0fd990708d38f5d669e5f64"
dependencies = [
"heck",
"proc-macro-error",
@ -568,6 +644,17 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "time"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
dependencies = [
"libc",
"redox_syscall",
"winapi 0.3.8",
]
[[package]]
name = "unicode-bidi"
version = "0.3.4"

View File

@ -15,9 +15,11 @@ default-run = "imdl"
[dependencies]
ansi_term = "0.12"
atty = "0.2"
chrono = "0.4.1"
env_logger = "0.7"
libc = "0.2"
md5 = "0.7"
pretty_assertions = "0.6"
regex = "1"
serde_bencode = "0.2"
serde_bytes = "0.11"
@ -25,6 +27,7 @@ sha1 = "0.6"
snafu = "0.6"
static_assertions = "1"
tempfile = "3"
unicode-width = "0.1"
url = "2"
walkdir = "2.1"

View File

@ -14,6 +14,7 @@
- [References](#references)
- [Alternatives & Prior Art](#alternatives--prior-art)
- [BitTorrent](#bittorrent)
- [Acknowledgments](#acknowledgments)
## General
@ -161,3 +162,9 @@ at any time.
| https://wiki.theory.org/index.php/Main_Page | Wiki with lots of information about all aspects of the BitTorrent protocol and implementations. |
| https://archive.org/details/2014_torrent_archive_organized) | Massive 158 GiB archive containing 5.5 million torrents, assembled in 2014. |
| https://github.com/internetarchive/dweb-transport | Github repository hosting The Internet Archive's distributed web and BitTorrent-related software. |
## Acknowledgments
The formatting of `imdl torrent show` is entirely copied from
[torf](https://github.com/rndusr/torf-cli), an excellent command-line torrent
creator, editor, and viewer.

View File

@ -2,4 +2,4 @@
set -euxo pipefail
! grep --color -REn 'FIXME|TODO|XXX' src
! grep --color -REni 'FIXME|TODO|XXX' src

View File

@ -15,15 +15,13 @@ impl<'buffer> Value<'buffer> {
Parser::parse(buffer)
}
#[cfg(test)]
fn encode(&self) -> Vec<u8> {
pub(crate) fn encode(&self) -> Vec<u8> {
let mut buffer = Vec::new();
self.encode_into(&mut buffer);
buffer
}
#[cfg(test)]
fn encode_into(&self, buffer: &mut Vec<u8>) {
pub(crate) fn encode_into(&self, buffer: &mut Vec<u8>) {
match self {
Self::Int(value) => {
buffer.push(b'i');
@ -49,7 +47,6 @@ impl<'buffer> Value<'buffer> {
}
}
#[cfg(test)]
fn encode_str(buffer: &mut Vec<u8>, contents: &[u8]) {
buffer.extend_from_slice(contents.len().to_string().as_bytes());
buffer.push(b':');

View File

@ -13,6 +13,10 @@ const YI: u128 = ZI << 10;
pub(crate) struct Bytes(pub(crate) u128);
impl Bytes {
pub(crate) fn from(bytes: impl Into<u128>) -> Bytes {
Bytes(bytes.into())
}
pub(crate) fn is_power_of_two(self) -> bool {
self.0 == 0 || self.0 & (self.0 - 1) == 0
}

View File

@ -19,6 +19,7 @@ pub(crate) use std::{
};
// dependencies
pub(crate) use chrono::{TimeZone, Utc};
pub(crate) use libc::EXIT_FAILURE;
pub(crate) use regex::{Regex, RegexSet};
pub(crate) use serde::{Deserialize, Serialize};
@ -29,6 +30,7 @@ pub(crate) use structopt::{
clap::{AppSettings, ArgSettings},
StructOpt,
};
pub(crate) use unicode_width::UnicodeWidthStr;
pub(crate) use url::Url;
pub(crate) use walkdir::WalkDir;
@ -45,13 +47,10 @@ pub(crate) use crate::{
pub(crate) use crate::{
bytes::Bytes, env::Env, error::Error, file_info::FileInfo, hasher::Hasher, info::Info,
lint::Lint, metainfo::Metainfo, mode::Mode, opt::Opt, platform::Platform, style::Style,
subcommand::Subcommand, torrent::Torrent, use_color::UseColor,
subcommand::Subcommand, table::Table, torrent::Torrent, torrent_summary::TorrentSummary,
use_color::UseColor,
};
// test modules
#[cfg(test)]
pub(crate) use crate::testing;
// test stdlib types
#[cfg(test)]
pub(crate) use std::{
@ -63,6 +62,10 @@ pub(crate) use std::{
time::{Duration, Instant},
};
// test modules
#[cfg(test)]
pub(crate) use crate::testing;
// test structs and enums
#[cfg(test)]
pub(crate) use crate::{capture::Capture, test_env::TestEnv};

View File

@ -9,8 +9,13 @@ pub(crate) enum Error {
AnnounceEmpty,
#[snafu(display("Failed to parse announce URL: {}", source))]
AnnounceUrlParse { source: url::ParseError },
#[snafu(display("Failed to decode bencode: {}", source))]
BencodeDecode { source: serde_bencode::Error },
#[snafu(display("Failed to deserialize torrent metainfo from `{}`: {}", path.display(), source))]
MetainfoLoad {
source: serde_bencode::Error,
path: PathBuf,
},
#[snafu(display("Failed to serialize torrent metainfo: {}", source))]
MetainfoSerialize { source: serde_bencode::Error },
#[snafu(display("Failed to parse byte count `{}`: {}", text, source))]
ByteParse {
text: String,
@ -44,8 +49,6 @@ pub(crate) enum Error {
PieceLengthSmall,
#[snafu(display("Piece length cannot be zero"))]
PieceLengthZero,
#[snafu(display("Serialization failed: {}", source))]
Serialize { source: serde_bencode::Error },
#[snafu(display("Failed to write to standard error: {}", source))]
Stderr { source: io::Error },
#[snafu(display("Failed to write to standard output: {}", source))]
@ -77,12 +80,6 @@ impl From<clap::Error> for Error {
}
}
impl From<serde_bencode::Error> for Error {
fn from(source: serde_bencode::Error) -> Self {
Self::Serialize { source }
}
}
impl From<SystemTimeError> for Error {
fn from(source: SystemTimeError) -> Self {
Self::SystemTime { source }

View File

@ -2,7 +2,7 @@ use crate::common::*;
#[derive(Deserialize, Serialize)]
pub struct Info {
pub private: u8,
pub private: Option<u8>,
#[serde(rename = "piece length")]
pub piece_length: u32,
pub name: String,

View File

@ -7,11 +7,15 @@
clippy::implicit_return,
clippy::indexing_slicing,
clippy::integer_arithmetic,
clippy::integer_division,
clippy::large_enum_variant,
clippy::missing_docs_in_private_items,
clippy::needless_pass_by_value,
clippy::option_map_unwrap_or_else,
clippy::option_unwrap_used,
clippy::result_expect_used,
clippy::result_unwrap_used,
clippy::shadow_reuse,
clippy::unreachable,
clippy::wildcard_enum_match_arm
)]
@ -62,7 +66,9 @@ mod platform_interface;
mod reckoner;
mod style;
mod subcommand;
mod table;
mod torrent;
mod torrent_summary;
mod use_color;
fn main() {

View File

@ -3,13 +3,33 @@ use crate::common::*;
#[derive(Deserialize, Serialize)]
pub struct Metainfo {
pub announce: String,
#[serde(rename = "announce list")]
#[serde(rename = "announce-list")]
pub announce_list: Option<Vec<Vec<String>>>,
pub comment: Option<String>,
#[serde(rename = "created by")]
pub created_by: Option<String>,
#[serde(rename = "creation date")]
pub creation_date: Option<u64>,
pub encoding: String,
pub encoding: Option<String>,
pub info: Info,
}
impl Metainfo {
pub(crate) fn _load(path: impl AsRef<Path>) -> Result<Metainfo, Error> {
let path = path.as_ref();
let bytes = fs::read(path).context(error::Filesystem { path })?;
Self::deserialize(path, &bytes)
}
pub(crate) fn dump(&self, path: impl AsRef<Path>) -> Result<(), Error> {
let path = path.as_ref();
let bytes = serde_bencode::ser::to_bytes(&self).context(error::MetainfoSerialize)?;
fs::write(path, &bytes).context(error::Filesystem { path })?;
Ok(())
}
pub(crate) fn deserialize(path: impl AsRef<Path>, bytes: &[u8]) -> Result<Metainfo, Error> {
let path = path.as_ref();
serde_bencode::de::from_bytes(&bytes).context(error::MetainfoLoad { path })
}
}

View File

@ -6,3 +6,12 @@ pub enum Mode {
Single { length: u64, md5sum: Option<String> },
Multiple { files: Vec<FileInfo> },
}
impl Mode {
pub(crate) fn total_size(&self) -> Bytes {
match self {
Self::Single { length, .. } => Bytes::from(*length),
Self::Multiple { files } => Bytes::from(files.iter().map(|file| file.length).sum::<u64>()),
}
}
}

172
src/table.rs Normal file
View File

@ -0,0 +1,172 @@
use crate::common::*;
pub(crate) struct Table {
rows: Vec<(&'static str, Value)>,
}
impl Table {
pub(crate) fn new() -> Self {
Self { rows: Vec::new() }
}
pub(crate) fn row(&mut self, name: &'static str, value: impl ToString) {
self.rows.push((name, Value::Scalar(value.to_string())));
}
pub(crate) fn tiers(
&mut self,
name: &'static str,
tiers: impl IntoIterator<Item = (impl ToString, impl IntoIterator<Item = impl ToString>)>,
) {
self.rows.push((
name,
Value::Tiers(
tiers
.into_iter()
.map(|(name, values)| {
(
name.to_string(),
values.into_iter().map(|value| value.to_string()).collect(),
)
})
.collect(),
),
));
}
fn rows(&self) -> &[(&'static str, Value)] {
&self.rows
}
pub(crate) fn name_width(&self) -> usize {
self
.rows()
.iter()
.map(|row| UnicodeWidthStr::width(row.0))
.max()
.unwrap_or(0)
}
pub(crate) fn write_human_readable(&self, out: &mut dyn Write) -> io::Result<()> {
fn padding(out: &mut dyn Write, n: usize) -> io::Result<()> {
write!(out, "{:width$}", "", width = n)
}
let name_width = self.name_width();
for (name, value) in self.rows() {
write!(out, "{:>width$}", name, width = name_width)?;
match value {
Value::Scalar(value) => writeln!(out, " {}", value)?,
Value::Tiers(tiers) => {
let tier_name_width = tiers
.iter()
.map(|(name, _values)| UnicodeWidthStr::width(name.as_str()))
.max()
.unwrap_or(0);
for (i, (name, values)) in tiers.iter().enumerate() {
if i > 0 {
padding(out, name_width)?;
}
write!(
out,
" {:width$}",
format!("{}:", name).as_str(),
width = tier_name_width + 1
)?;
for (i, value) in values.iter().enumerate() {
if i > 0 {
padding(out, name_width + 2 + tier_name_width + 1)?;
}
writeln!(out, " {}", value)?;
}
}
}
}
}
Ok(())
}
}
enum Value {
Scalar(String),
Tiers(Vec<(String, Vec<String>)>),
}
#[cfg(test)]
mod tests {
use super::*;
fn human_readable(table: Table, want: &str) {
let mut cursor = Cursor::new(Vec::new());
table.write_human_readable(&mut cursor).unwrap();
let have = String::from_utf8(cursor.into_inner()).unwrap();
if have != want {
panic!("have != want:\nHAVE:\n{}\nWANT:\n{}", have, want);
}
}
#[test]
fn single_row() {
let mut table = Table::new();
table.row("Foo", "bar");
human_readable(table, "Foo bar\n");
}
#[test]
fn multiple_rows() {
let mut table = Table::new();
table.row("Foo", "bar");
table.row("X", "y");
human_readable(table, "Foo bar\n X y\n");
}
#[test]
fn tiers_aligned() {
let mut table = Table::new();
table.tiers("Foo", vec![("Bar", &["a", "b"]), ("Baz", &["x", "y"])]);
human_readable(
table,
"\
Foo Bar: a
b
Baz: x
y
",
);
}
#[test]
fn tiers_unaligned() {
let mut table = Table::new();
table.tiers(
"First",
vec![("Some", &["the", "thing"]), ("Other", &["about", "that"])],
);
table.tiers(
"Second",
vec![
("Row", &["the", "thing"]),
("More Stuff", &["about", "that"]),
],
);
human_readable(
table,
" First Some: the
thing
Other: about
that
Second Row: the
thing
More Stuff: about
that
",
);
}
}

View File

@ -1,6 +1,7 @@
use crate::common::*;
mod create;
mod show;
mod stats;
#[derive(StructOpt)]
@ -12,6 +13,7 @@ mod stats;
pub(crate) enum Torrent {
Create(torrent::create::Create),
Stats(torrent::stats::Stats),
Show(torrent::show::Show),
}
impl Torrent {
@ -19,6 +21,7 @@ impl Torrent {
match self {
Self::Create(create) => create.run(env),
Self::Stats(stats) => stats.run(env, unstable),
Self::Show(show) => show.run(env),
}
}
}

View File

@ -197,7 +197,7 @@ impl Create {
input.parent().unwrap().join(torrent_name)
});
let private = if self.private { 1 } else { 0 };
let private = if self.private { Some(1) } else { None };
let creation_date = if self.no_creation_date {
None
@ -227,7 +227,7 @@ impl Create {
let metainfo = Metainfo {
comment: self.comment,
encoding: consts::ENCODING_UTF8.to_string(),
encoding: Some(consts::ENCODING_UTF8.to_string()),
announce: self.announce.to_string(),
announce_list: if announce_list.is_empty() {
None
@ -239,9 +239,7 @@ impl Create {
info,
};
let bytes = serde_bencode::ser::to_bytes(&metainfo)?;
fs::write(&output, bytes).context(error::Filesystem { path: &output })?;
metainfo.dump(&output)?;
if self.open {
Platform::open(&output)?;
@ -255,8 +253,6 @@ impl Create {
mod tests {
use super::*;
use crate::test_env::TestEnv;
fn environment(args: &[&str]) -> TestEnv {
testing::env(["torrent", "create"].iter().chain(args).cloned())
}
@ -292,7 +288,7 @@ mod tests {
let torrent = env.resolve("foo.torrent");
let bytes = fs::read(torrent).unwrap();
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).unwrap();
assert_eq!(metainfo.info.private, 0);
assert_eq!(metainfo.info.private, None);
}
#[test]
@ -303,7 +299,7 @@ mod tests {
let torrent = env.resolve("foo.torrent");
let bytes = fs::read(torrent).unwrap();
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).unwrap();
assert_eq!(metainfo.info.private, 1);
assert_eq!(metainfo.info.private, Some(1));
}
#[test]
@ -573,7 +569,7 @@ mod tests {
let torrent = env.resolve("foo.torrent");
let bytes = fs::read(torrent).unwrap();
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).unwrap();
assert_eq!(metainfo.encoding, "UTF-8");
assert_eq!(metainfo.encoding, Some("UTF-8".into()));
}
#[test]

90
src/torrent/show.rs Normal file
View File

@ -0,0 +1,90 @@
use crate::common::*;
#[derive(StructOpt)]
#[structopt(
help_message(consts::HELP_MESSAGE),
version_message(consts::VERSION_MESSAGE),
about("Display information about a `.torrent` file.")
)]
pub(crate) struct Show {
#[structopt(
name = "TORRENT",
long = "input",
help = "Show information about `TORRENT`."
)]
input: PathBuf,
}
impl Show {
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
let summary = TorrentSummary::load(&env.resolve(self.input))?;
let table = summary.table();
table
.write_human_readable(&mut env.out)
.context(error::Stdout)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn output() {
let mut env = testing::env(
["torrent", "show", "--input", "foo.torrent"]
.iter()
.cloned(),
);
let metainfo = Metainfo {
announce: "announce".into(),
announce_list: Some(vec![vec!["announce".into(), "b".into()], vec!["c".into()]]),
comment: Some("comment".into()),
created_by: Some("created by".into()),
creation_date: Some(1),
encoding: Some("UTF-8".into()),
info: Info {
private: Some(1),
piece_length: 16 * 1024,
name: "foo".into(),
pieces: vec![
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
],
mode: Mode::Single {
length: 20,
md5sum: None,
},
},
};
let path = env.resolve("foo.torrent");
metainfo.dump(path).unwrap();
env.run().unwrap();
let have = env.out();
let want = " Name foo
Comment comment
Created 1970-01-01 00:00:01 UTC
Info Hash bd68a8a5ab377e37e8cdbfd37b670408c59a009f
Torrent Size 236 bytes
Content Size 20 bytes
Private yes
Trackers Main: announce
Tier 1: announce
b
Tier 2: c
Piece Size 16 KiB
Piece Count 1
File Count 1
";
assert_eq!(have, want);
}
}

104
src/torrent_summary.rs Normal file
View File

@ -0,0 +1,104 @@
use crate::common::*;
pub(crate) struct TorrentSummary {
metainfo: Metainfo,
infohash: sha1::Digest,
size: Bytes,
}
impl TorrentSummary {
pub(crate) fn load(path: &Path) -> Result<Self, Error> {
let bytes = fs::read(path).context(error::Filesystem { path })?;
let metainfo = Metainfo::deserialize(path, &bytes)?;
let value = bencode::Value::decode(&bytes).unwrap();
let infohash = if let bencode::Value::Dict(items) = value {
let info = items
.iter()
.find(|(key, _value)| key == b"info")
.unwrap()
.1
.encode();
Sha1::from(info).digest()
} else {
unreachable!()
};
let metadata = path.metadata().context(error::Filesystem { path })?;
Ok(Self {
size: Bytes(metadata.len().into()),
infohash,
metainfo,
})
}
pub(crate) fn table(&self) -> Table {
let mut table = Table::new();
table.row("Name", &self.metainfo.info.name);
if let Some(comment) = &self.metainfo.comment {
table.row("Comment", comment);
}
if let Some(creation_date) = self.metainfo.creation_date {
#[allow(clippy::as_conversions)]
table.row(
"Created",
Utc.timestamp(
creation_date
.min(i64::max_value() as u64)
.try_into()
.unwrap(),
0,
),
);
}
table.row("Info Hash", self.infohash);
table.row("Torrent Size", self.size);
table.row("Content Size", self.metainfo.info.mode.total_size());
table.row(
"Private",
if self.metainfo.info.private.unwrap_or(0) == 1 {
"yes"
} else {
"no"
},
);
match &self.metainfo.announce_list {
Some(tiers) => {
let mut value = Vec::new();
value.push(("Main".to_owned(), vec![self.metainfo.announce.clone()]));
for (i, tier) in tiers.iter().enumerate() {
value.push((format!("Tier {}", i + 1), tier.clone()));
}
table.tiers("Trackers", value);
}
None => table.row("Tracker", &self.metainfo.announce),
}
table.row("Piece Size", Bytes::from(self.metainfo.info.piece_length));
table.row("Piece Count", self.metainfo.info.pieces.len() / 20);
table.row(
"File Count",
match &self.metainfo.info.mode {
Mode::Single { .. } => 1,
Mode::Multiple { files } => files.len(),
},
);
table
}
}