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
This commit is contained in:
Casey Rodarmor 2020-02-03 04:39:48 -08:00
parent 85f02d9f29
commit 6df45e0244
No known key found for this signature in database
GPG Key ID: 556186B153EC6FE0
7 changed files with 246 additions and 13 deletions

View File

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

View File

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

View File

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

View File

@ -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<Lint> {
match self {
Self::PieceLengthUneven { .. } => Some(Lint::UnevenPieceLength),
Self::PieceLengthSmall { .. } => Some(Lint::SmallPieceLength),
_ => None,
}
}
}
impl From<clap::Error> for Error {

70
src/lint.rs Normal file
View File

@ -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<Self, Self::Err> {
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::<Lint>(),
Err(Error::LintUnknown { text }) if text == "foo"
);
}
}

View File

@ -52,6 +52,7 @@ mod hasher;
mod info;
mod into_u64;
mod into_usize;
mod lint;
mod metainfo;
mod mode;
mod opt;

View File

@ -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<Lint>,
#[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<Lint>,
}
impl Linter {
fn new() -> Linter {
Linter {
allowed: BTreeSet::new(),
}
}
fn allow(&mut self, allowed: impl IntoIterator<Item = Lint>) {
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::<Metainfo>(&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();
}
}