Skip to content

Commit

Permalink
Accept --piece-length arguments with SI units
Browse files Browse the repository at this point in the history
Valid units include MiB, KiB, and GiB.

type: changed
  • Loading branch information
casey committed Apr 8, 2020
1 parent eb6556a commit 635692f
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 8 deletions.
162 changes: 162 additions & 0 deletions src/bytes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
use crate::common::*;

const KI: u128 = 1 << 10;
const MI: u128 = KI << 10;
const GI: u128 = MI << 10;
const TI: u128 = GI << 10;
const PI: u128 = TI << 10;
const EI: u128 = PI << 10;
const ZI: u128 = EI << 10;
const YI: u128 = ZI << 10;

#[derive(Debug, PartialEq, Copy, Clone)]
pub(crate) struct Bytes(pub(crate) u128);

fn float_to_int(x: f64) -> u128 {
#![allow(
clippy::as_conversions,
clippy::cast_sign_loss,
clippy::cast_possible_truncation
)]
x as u128
}

fn int_to_float(x: u128) -> f64 {
#![allow(clippy::as_conversions, clippy::cast_precision_loss)]
x as f64
}

impl FromStr for Bytes {
type Err = Error;

fn from_str(text: &str) -> Result<Self, Self::Err> {
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_digit(c: &char) -> bool {
match c {
'0'..='9' | '.' => true,
_ => false,
}
}

let digits = text.chars().take_while(is_digit).collect::<String>();

let suffix = text.chars().skip_while(is_digit).collect::<String>();

let value = digits.parse::<f64>().map_err(|source| Error::ByteParse {
text: text.to_owned(),
source,
})?;

let multiple = match suffix.to_lowercase().as_str() {
"" | "b" | "byte" | "bytes" => 1,
"kib" => KI,
"mib" => MI,
"gib" => GI,
"tib" => TI,
"pib" => PI,
"eib" => EI,
"zib" => ZI,
"yib" => YI,
_ => {
return Err(Error::ByteSuffix {
text: text.to_owned(),
suffix: suffix.to_owned(),
})
}
};

Ok(Bytes(float_to_int(value * int_to_float(multiple))))
}
}

impl Display for Bytes {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
const DISPLAY_SUFFIXES: &[&str] = &["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];

let mut value = int_to_float(self.0);

let mut i = 0;

while value >= 1024.0 {
value /= 1024.0;
i += 1;
}

let suffix = if i == 0 {
if value == 1.0 {
"byte"
} else {
"bytes"
}
} else {
DISPLAY_SUFFIXES[i - 1]
};

let formatted = format!("{:.2}", value);
let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
write!(f, "{} {}", trimmed, suffix)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn ok() {
const CASES: &[(&str, u128)] = &[
("0", 0),
("0kib", 0),
("1", 1),
("1b", 1),
("1byte", 1),
("1bytes", 1),
("1kib", KI),
("1KiB", KI),
("12kib", 12 * KI),
("1.5mib", 1 * MI + 512 * KI),
("1yib", 1 * YI),
];

for (text, value) in CASES {
assert_eq!(
text.parse::<Bytes>().unwrap(),
Bytes(*value),
"text: {}",
text
);
}
}

#[test]
fn err() {
assert_matches!(
"100foo".parse::<Bytes>().unwrap_err(),
Error::ByteSuffix { text, suffix }
if text == "100foo" && suffix == "foo"
);

assert_matches!(
"1.0.0foo".parse::<Bytes>().unwrap_err(),
Error::ByteParse { .. }
);
}

#[test]
fn display() {
assert_eq!(Bytes(0).to_string(), "0 bytes");
assert_eq!(Bytes(1).to_string(), "1 byte");
assert_eq!(Bytes(2).to_string(), "2 bytes");
assert_eq!(Bytes(KI).to_string(), "1 KiB");
assert_eq!(Bytes(512 * KI).to_string(), "512 KiB");
assert_eq!(Bytes(MI).to_string(), "1 MiB");
assert_eq!(Bytes(MI + 512 * KI).to_string(), "1.5 MiB");
assert_eq!(Bytes(1024 * MI + 512 * MI).to_string(), "1.5 GiB");
assert_eq!(Bytes(GI).to_string(), "1 GiB");
assert_eq!(Bytes(TI).to_string(), "1 TiB");
assert_eq!(Bytes(PI).to_string(), "1 PiB");
assert_eq!(Bytes(EI).to_string(), "1 EiB");
assert_eq!(Bytes(ZI).to_string(), "1 ZiB");
assert_eq!(Bytes(YI).to_string(), "1 YiB");
}
}
7 changes: 4 additions & 3 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub(crate) use std::{
fs::{self, File},
hash::Hash,
io::{self, Read, Write},
num::ParseFloatError,
path::{Path, PathBuf},
process::{self, Command, ExitStatus},
str::{self, FromStr},
Expand Down Expand Up @@ -42,9 +43,9 @@ pub(crate) use crate::{

// structs and enums
pub(crate) use crate::{
env::Env, error::Error, file_info::FileInfo, hasher::Hasher, info::Info, metainfo::Metainfo,
mode::Mode, opt::Opt, platform::Platform, style::Style, subcommand::Subcommand, torrent::Torrent,
use_color::UseColor,
bytes::Bytes, env::Env, error::Error, file_info::FileInfo, hasher::Hasher, info::Info,
metainfo::Metainfo, mode::Mode, opt::Opt, platform::Platform, style::Style,
subcommand::Subcommand, torrent::Torrent, use_color::UseColor,
};

// test modules
Expand Down
13 changes: 13 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ pub(crate) enum Error {
AnnounceUrlParse { source: url::ParseError },
#[snafu(display("Failed to decode bencode: {}", source))]
BencodeDecode { source: serde_bencode::Error },
#[snafu(display("Failed to parse byte count `{}`: {}", text, source))]
ByteParse {
text: String,
source: ParseFloatError,
},
#[snafu(display("Failed to parse byte count `{}`, invalid suffix: `{}`", text, suffix))]
ByteSuffix { text: String, suffix: String },
#[snafu(display("{}", source))]
Clap { source: clap::Error },
#[snafu(display("Failed to invoke command `{}`: {}", command, source,))]
Expand All @@ -25,6 +32,12 @@ pub(crate) enum Error {
Filesystem { source: io::Error, path: PathBuf },
#[snafu(display("Failed to find opener utility, please install one of {}", tried.join(",")))]
OpenerMissing { tried: &'static [&'static str] },
#[snafu(display(
"Piece length `{}` too large. The maximum supported piece length is {}.",
bytes,
Bytes(u32::max_value().into())
))]
PieceLength { bytes: Bytes },
#[snafu(display("Serialization failed: {}", source))]
Serialize { source: serde_bencode::Error },
#[snafu(display("Failed to write to standard error: {}", source))]
Expand Down
3 changes: 3 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#![allow(
clippy::else_if_without_else,
clippy::enum_glob_use,
clippy::float_arithmetic,
clippy::float_cmp,
clippy::implicit_return,
clippy::indexing_slicing,
clippy::integer_arithmetic,
Expand Down Expand Up @@ -40,6 +42,7 @@ mod test_env;
mod capture;

mod bencode;
mod bytes;
mod common;
mod consts;
mod env;
Expand Down
37 changes: 32 additions & 5 deletions src/torrent/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,11 @@ Note: Many BitTorrent clients do not implement the behavior described in BEP 12.
#[structopt(
name = "PIECE-LENGTH",
long = "piece-length",
default_value = "524288",
help = "Set piece length to `PIECE-LENGTH` bytes."
default_value = "512KiB",
help = "Set piece length to `PIECE-LENGTH` bytes.",
long_help = "Set piece length to `PIECE-LENGTH` bytes. Accepts SI units, e.g. kib, mib, and gib."
)]
piece_length: u32,
piece_length: Bytes,
#[structopt(
name = "PRIVATE",
long = "private",
Expand All @@ -99,6 +100,14 @@ Note: Many BitTorrent clients do not implement the behavior described in BEP 12.

impl Create {
pub(crate) fn run(self, env: &Env) -> Result<(), Error> {
let piece_length: u32 = self
.piece_length
.0
.try_into()
.map_err(|_| Error::PieceLength {
bytes: self.piece_length,
})?;

let input = env.resolve(&self.input);

let mut announce_list = Vec::new();
Expand Down Expand Up @@ -157,10 +166,10 @@ impl Create {
Some(String::from(consts::CREATED_BY_DEFAULT))
};

let (mode, pieces) = Hasher::hash(&input, self.md5sum, self.piece_length)?;
let (mode, pieces) = Hasher::hash(&input, self.md5sum, piece_length)?;

let info = Info {
piece_length: self.piece_length,
piece_length,
mode,
pieces,
name,
Expand Down Expand Up @@ -406,6 +415,24 @@ mod tests {
assert_eq!(metainfo.info.piece_length, 1);
}

#[test]
fn si_piece_size() {
let mut env = environment(&[
"--input",
"foo",
"--announce",
"http://bar",
"--piece-length",
"0.5MiB",
]);
fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap();
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.piece_length, 512 * 1024);
}

#[test]
fn name() {
let mut env = environment(&[
Expand Down

0 comments on commit 635692f

Please sign in to comment.