From 6df45e024463e0d10cd391e8758f797692a6f762 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Mon, 3 Feb 2020 04:39:48 -0800 Subject: [PATCH] Restrict piece length - Must be greater than zero - Must be a power of two (but can override with `--allow uneven-piece-length` - Must be greater than 16KiB (but can override with `--allow small-piece-length` - Must be less than u32 max type: changed --- src/bytes.rs | 6 ++ src/common.rs | 4 +- src/env.rs | 11 ++++ src/error.rs | 20 +++++- src/lint.rs | 70 ++++++++++++++++++++ src/main.rs | 1 + src/torrent/create.rs | 147 +++++++++++++++++++++++++++++++++++++++--- 7 files changed, 246 insertions(+), 13 deletions(-) create mode 100644 src/lint.rs diff --git a/src/bytes.rs b/src/bytes.rs index a4c6ed4..e8814ac 100644 --- a/src/bytes.rs +++ b/src/bytes.rs @@ -12,6 +12,12 @@ const YI: u128 = ZI << 10; #[derive(Debug, PartialEq, Copy, Clone)] pub(crate) struct Bytes(pub(crate) u128); +impl Bytes { + pub(crate) fn is_power_of_two(self) -> bool { + self.0 == 0 || self.0 & (self.0 - 1) == 0 + } +} + fn float_to_int(x: f64) -> u128 { #![allow( clippy::as_conversions, diff --git a/src/common.rs b/src/common.rs index 726f297..ba30d6b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -2,7 +2,7 @@ pub(crate) use std::{ borrow::Cow, cmp::{Ordering, Reverse}, - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, BTreeSet, HashMap}, convert::{Infallible, TryInto}, env, ffi::{OsStr, OsString}, @@ -44,7 +44,7 @@ pub(crate) use crate::{ // structs and enums pub(crate) use crate::{ bytes::Bytes, env::Env, error::Error, file_info::FileInfo, hasher::Hasher, info::Info, - metainfo::Metainfo, mode::Mode, opt::Opt, platform::Platform, style::Style, + lint::Lint, metainfo::Metainfo, mode::Mode, opt::Opt, platform::Platform, style::Style, subcommand::Subcommand, torrent::Torrent, use_color::UseColor, }; diff --git a/src/env.rs b/src/env.rs index 355412b..bea3d36 100644 --- a/src/env.rs +++ b/src/env.rs @@ -91,6 +91,17 @@ impl Env { self.err_style.message().suffix(), ) .ok(); + + if let Some(lint) = error.lint() { + writeln!( + &mut self.err, + "{}: This check can be disabled with `--allow {}`.", + self.err_style.message().paint("note"), + lint.name() + ) + .ok(); + } + Err(EXIT_FAILURE) } } else { diff --git a/src/error.rs b/src/error.rs index 2993bed..245493b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -37,7 +37,13 @@ pub(crate) enum Error { bytes, Bytes(u32::max_value().into()) ))] - PieceLength { bytes: Bytes }, + PieceLengthTooLarge { bytes: Bytes }, + #[snafu(display("Piece length `{}` is not an even power of two", bytes))] + PieceLengthUneven { bytes: Bytes }, + #[snafu(display("Piece length must be at least 16 KiB"))] + 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))] @@ -51,6 +57,18 @@ pub(crate) enum Error { feature ))] Unstable { feature: &'static str }, + #[snafu(display("Unknown lint: {}", text))] + LintUnknown { text: String }, +} + +impl Error { + pub(crate) fn lint(&self) -> Option { + match self { + Self::PieceLengthUneven { .. } => Some(Lint::UnevenPieceLength), + Self::PieceLengthSmall { .. } => Some(Lint::SmallPieceLength), + _ => None, + } + } } impl From for Error { diff --git a/src/lint.rs b/src/lint.rs new file mode 100644 index 0000000..0bbe06c --- /dev/null +++ b/src/lint.rs @@ -0,0 +1,70 @@ +use crate::common::*; + +#[derive(Eq, PartialEq, Debug, Copy, Clone, Ord, PartialOrd)] +pub(crate) enum Lint { + UnevenPieceLength, + SmallPieceLength, +} + +const UNEVEN_PIECE_LENGTH: &str = "uneven-piece-length"; +const SMALL_PIECE_LENGTH: &str = "small-piece-length"; + +impl Lint { + pub(crate) fn name(self) -> &'static str { + match self { + Self::UnevenPieceLength => UNEVEN_PIECE_LENGTH, + Self::SmallPieceLength => SMALL_PIECE_LENGTH, + } + } +} + +impl FromStr for Lint { + type Err = Error; + + fn from_str(text: &str) -> Result { + match text.replace('_', "-").to_lowercase().as_str() { + UNEVEN_PIECE_LENGTH => Ok(Self::UnevenPieceLength), + SMALL_PIECE_LENGTH => Ok(Self::SmallPieceLength), + _ => Err(Error::LintUnknown { + text: text.to_string(), + }), + } + } +} + +impl Display for Lint { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}", self.name()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_str_ok() { + assert_eq!( + Lint::UnevenPieceLength, + "uneven_piece_length".parse().unwrap() + ); + + assert_eq!( + Lint::UnevenPieceLength, + "uneven-piece-length".parse().unwrap() + ); + + assert_eq!( + Lint::UnevenPieceLength, + "UNEVEN-piece-length".parse().unwrap() + ); + } + + #[test] + fn from_str_err() { + assert_matches!( + "foo".parse::(), + Err(Error::LintUnknown { text }) if text == "foo" + ); + } +} diff --git a/src/main.rs b/src/main.rs index 4a2d6a0..7e77559 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,6 +52,7 @@ mod hasher; mod info; mod into_u64; mod into_usize; +mod lint; mod metainfo; mod mode; mod opt; diff --git a/src/torrent/create.rs b/src/torrent/create.rs index a6c1072..0a6b594 100644 --- a/src/torrent/create.rs +++ b/src/torrent/create.rs @@ -15,6 +15,13 @@ pub(crate) struct Create { long_help = "Use `ANNOUNCE` as the primary tracker announce URL. To supply multiple announce URLs, also use `--announce-tier`." )] announce: Url, + #[structopt( + name = "ALLOW", + long = "allow", + help = "Use `ANNOUNCE` as the primary tracker announce URL.", + long_help = "Use `ANNOUNCE` as the primary tracker announce URL. To supply multiple announce URLs, also use `--announce-tier`." + )] + allowed_lints: Vec, #[structopt( long = "announce-tier", name = "ANNOUNCE-TIER", @@ -98,15 +105,57 @@ Note: Many BitTorrent clients do not implement the behavior described in BEP 12. private: bool, } +struct Linter { + allowed: BTreeSet, +} + +impl Linter { + fn new() -> Linter { + Linter { + allowed: BTreeSet::new(), + } + } + + fn allow(&mut self, allowed: impl IntoIterator) { + self.allowed.extend(allowed) + } + + fn is_allowed(&self, lint: Lint) -> bool { + self.allowed.contains(&lint) + } + + fn is_denied(&self, lint: Lint) -> bool { + !self.is_allowed(lint) + } +} + 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 { + let mut linter = Linter::new(); + linter.allow(self.allowed_lints.iter().cloned()); + + if linter.is_denied(Lint::UnevenPieceLength) && !self.piece_length.is_power_of_two() { + return Err(Error::PieceLengthUneven { bytes: self.piece_length, - })?; + }); + } + + let piece_length: u32 = + self + .piece_length + .0 + .try_into() + .map_err(|_| Error::PieceLengthTooLarge { + bytes: self.piece_length, + })?; + + if piece_length == 0 { + return Err(Error::PieceLengthZero); + } + + if linter.is_denied(Lint::SmallPieceLength) && piece_length < 16 * 1024 { + return Err(Error::PieceLengthSmall); + } let input = env.resolve(&self.input); @@ -405,14 +454,14 @@ mod tests { "--announce", "http://bar", "--piece-length", - "1", + "64KiB", ]); 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::(&bytes).unwrap(); - assert_eq!(metainfo.info.piece_length, 1); + assert_eq!(metainfo.info.piece_length, 64 * 1024); } #[test] @@ -441,7 +490,7 @@ mod tests { "--announce", "http://bar", "--piece-length", - "1", + "16KiB", ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); @@ -459,7 +508,7 @@ mod tests { "--announce", "http://bar", "--piece-length", - "1", + "32KiB", ]); let dir = env.resolve("foo"); fs::create_dir(&dir).unwrap(); @@ -588,6 +637,8 @@ mod tests { "http://bar", "--piece-length", "1", + "--allow", + "small-piece-length", ]); let contents = "bar"; fs::write(env.resolve("foo"), contents).unwrap(); @@ -732,4 +783,80 @@ mod tests { panic!("Failed to read `opened.txt`."); } + + #[test] + fn uneven_piece_length() { + let mut env = environment(&[ + "--input", + "foo", + "--announce", + "http://bar", + "--piece-length", + "17KiB", + ]); + assert_matches!( + env.run(), + Err(Error::PieceLengthUneven { bytes }) if bytes.0 == 17 * 1024 + ); + } + + #[test] + fn uneven_piece_length_allow() { + let mut env = environment(&[ + "--input", + "foo", + "--announce", + "http://bar", + "--piece-length", + "17KiB", + "--allow", + "uneven-piece-length", + ]); + let dir = env.resolve("foo"); + fs::create_dir(&dir).unwrap(); + env.run().unwrap(); + } + + #[test] + fn zero_piece_length() { + let mut env = environment(&[ + "--input", + "foo", + "--announce", + "http://bar", + "--piece-length", + "0", + ]); + assert_matches!(env.run(), Err(Error::PieceLengthZero)); + } + + #[test] + fn small_piece_length() { + let mut env = environment(&[ + "--input", + "foo", + "--announce", + "http://bar", + "--piece-length", + "8KiB", + ]); + assert_matches!(env.run(), Err(Error::PieceLengthSmall)); + } + + #[test] + fn small_piece_length_allow() { + let mut env = environment(&[ + "--input", + "foo", + "--announce", + "http://bar", + "--piece-length", + "8KiB", + "--allow", + "small-piece-length", + ]); + let dir = env.resolve("foo"); + fs::create_dir(&dir).unwrap(); + env.run().unwrap(); + } }