Accept --piece-length arguments with SI units
				
					
				
			Valid units include MiB, KiB, and GiB. type: changed
This commit is contained in:
		
							parent
							
								
									eb6556ae6a
								
							
						
					
					
						commit
						635692fdfa
					
				
							
								
								
									
										162
									
								
								src/bytes.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								src/bytes.rs
									
									
									
									
									
										Normal file
									
								
							@ -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");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -10,6 +10,7 @@ pub(crate) use std::{
 | 
				
			|||||||
  fs::{self, File},
 | 
					  fs::{self, File},
 | 
				
			||||||
  hash::Hash,
 | 
					  hash::Hash,
 | 
				
			||||||
  io::{self, Read, Write},
 | 
					  io::{self, Read, Write},
 | 
				
			||||||
 | 
					  num::ParseFloatError,
 | 
				
			||||||
  path::{Path, PathBuf},
 | 
					  path::{Path, PathBuf},
 | 
				
			||||||
  process::{self, Command, ExitStatus},
 | 
					  process::{self, Command, ExitStatus},
 | 
				
			||||||
  str::{self, FromStr},
 | 
					  str::{self, FromStr},
 | 
				
			||||||
@ -42,9 +43,9 @@ pub(crate) use crate::{
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// structs and enums
 | 
					// structs and enums
 | 
				
			||||||
pub(crate) use crate::{
 | 
					pub(crate) use crate::{
 | 
				
			||||||
  env::Env, error::Error, file_info::FileInfo, hasher::Hasher, info::Info, metainfo::Metainfo,
 | 
					  bytes::Bytes, env::Env, error::Error, file_info::FileInfo, hasher::Hasher, info::Info,
 | 
				
			||||||
  mode::Mode, opt::Opt, platform::Platform, style::Style, subcommand::Subcommand, torrent::Torrent,
 | 
					  metainfo::Metainfo, mode::Mode, opt::Opt, platform::Platform, style::Style,
 | 
				
			||||||
  use_color::UseColor,
 | 
					  subcommand::Subcommand, torrent::Torrent, use_color::UseColor,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// test modules
 | 
					// test modules
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										13
									
								
								src/error.rs
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								src/error.rs
									
									
									
									
									
								
							@ -11,6 +11,13 @@ pub(crate) enum Error {
 | 
				
			|||||||
  AnnounceUrlParse { source: url::ParseError },
 | 
					  AnnounceUrlParse { source: url::ParseError },
 | 
				
			||||||
  #[snafu(display("Failed to decode bencode: {}", source))]
 | 
					  #[snafu(display("Failed to decode bencode: {}", source))]
 | 
				
			||||||
  BencodeDecode { source: serde_bencode::Error },
 | 
					  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))]
 | 
					  #[snafu(display("{}", source))]
 | 
				
			||||||
  Clap { source: clap::Error },
 | 
					  Clap { source: clap::Error },
 | 
				
			||||||
  #[snafu(display("Failed to invoke command `{}`: {}", command, source,))]
 | 
					  #[snafu(display("Failed to invoke command `{}`: {}", command, source,))]
 | 
				
			||||||
@ -25,6 +32,12 @@ pub(crate) enum Error {
 | 
				
			|||||||
  Filesystem { source: io::Error, path: PathBuf },
 | 
					  Filesystem { source: io::Error, path: PathBuf },
 | 
				
			||||||
  #[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(
 | 
				
			||||||
 | 
					    "Piece length `{}` too large. The maximum supported piece length is {}.",
 | 
				
			||||||
 | 
					    bytes,
 | 
				
			||||||
 | 
					    Bytes(u32::max_value().into())
 | 
				
			||||||
 | 
					  ))]
 | 
				
			||||||
 | 
					  PieceLength { bytes: Bytes },
 | 
				
			||||||
  #[snafu(display("Serialization failed: {}", source))]
 | 
					  #[snafu(display("Serialization failed: {}", source))]
 | 
				
			||||||
  Serialize { source: serde_bencode::Error },
 | 
					  Serialize { source: serde_bencode::Error },
 | 
				
			||||||
  #[snafu(display("Failed to write to standard error: {}", source))]
 | 
					  #[snafu(display("Failed to write to standard error: {}", source))]
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,8 @@
 | 
				
			|||||||
#![allow(
 | 
					#![allow(
 | 
				
			||||||
  clippy::else_if_without_else,
 | 
					  clippy::else_if_without_else,
 | 
				
			||||||
  clippy::enum_glob_use,
 | 
					  clippy::enum_glob_use,
 | 
				
			||||||
 | 
					  clippy::float_arithmetic,
 | 
				
			||||||
 | 
					  clippy::float_cmp,
 | 
				
			||||||
  clippy::implicit_return,
 | 
					  clippy::implicit_return,
 | 
				
			||||||
  clippy::indexing_slicing,
 | 
					  clippy::indexing_slicing,
 | 
				
			||||||
  clippy::integer_arithmetic,
 | 
					  clippy::integer_arithmetic,
 | 
				
			||||||
@ -40,6 +42,7 @@ mod test_env;
 | 
				
			|||||||
mod capture;
 | 
					mod capture;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mod bencode;
 | 
					mod bencode;
 | 
				
			||||||
 | 
					mod bytes;
 | 
				
			||||||
mod common;
 | 
					mod common;
 | 
				
			||||||
mod consts;
 | 
					mod consts;
 | 
				
			||||||
mod env;
 | 
					mod env;
 | 
				
			||||||
 | 
				
			|||||||
@ -84,10 +84,11 @@ Note: Many BitTorrent clients do not implement the behavior described in BEP 12.
 | 
				
			|||||||
  #[structopt(
 | 
					  #[structopt(
 | 
				
			||||||
    name = "PIECE-LENGTH",
 | 
					    name = "PIECE-LENGTH",
 | 
				
			||||||
    long = "piece-length",
 | 
					    long = "piece-length",
 | 
				
			||||||
    default_value = "524288",
 | 
					    default_value = "512KiB",
 | 
				
			||||||
    help = "Set piece length to `PIECE-LENGTH` bytes."
 | 
					    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(
 | 
					  #[structopt(
 | 
				
			||||||
    name = "PRIVATE",
 | 
					    name = "PRIVATE",
 | 
				
			||||||
    long = "private",
 | 
					    long = "private",
 | 
				
			||||||
@ -99,6 +100,14 @@ Note: Many BitTorrent clients do not implement the behavior described in BEP 12.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
impl Create {
 | 
					impl Create {
 | 
				
			||||||
  pub(crate) fn run(self, env: &Env) -> Result<(), Error> {
 | 
					  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 input = env.resolve(&self.input);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let mut announce_list = Vec::new();
 | 
					    let mut announce_list = Vec::new();
 | 
				
			||||||
@ -157,10 +166,10 @@ impl Create {
 | 
				
			|||||||
      Some(String::from(consts::CREATED_BY_DEFAULT))
 | 
					      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 {
 | 
					    let info = Info {
 | 
				
			||||||
      piece_length: self.piece_length,
 | 
					      piece_length,
 | 
				
			||||||
      mode,
 | 
					      mode,
 | 
				
			||||||
      pieces,
 | 
					      pieces,
 | 
				
			||||||
      name,
 | 
					      name,
 | 
				
			||||||
@ -406,6 +415,24 @@ mod tests {
 | 
				
			|||||||
    assert_eq!(metainfo.info.piece_length, 1);
 | 
					    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]
 | 
					  #[test]
 | 
				
			||||||
  fn name() {
 | 
					  fn name() {
 | 
				
			||||||
    let mut env = environment(&[
 | 
					    let mut env = environment(&[
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user