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:
parent
85f02d9f29
commit
6df45e0244
|
@ -12,6 +12,12 @@ const YI: u128 = ZI << 10;
|
||||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||||
pub(crate) struct Bytes(pub(crate) u128);
|
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 {
|
fn float_to_int(x: f64) -> u128 {
|
||||||
#![allow(
|
#![allow(
|
||||||
clippy::as_conversions,
|
clippy::as_conversions,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
pub(crate) use std::{
|
pub(crate) use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
cmp::{Ordering, Reverse},
|
cmp::{Ordering, Reverse},
|
||||||
collections::{BTreeMap, HashMap},
|
collections::{BTreeMap, BTreeSet, HashMap},
|
||||||
convert::{Infallible, TryInto},
|
convert::{Infallible, TryInto},
|
||||||
env,
|
env,
|
||||||
ffi::{OsStr, OsString},
|
ffi::{OsStr, OsString},
|
||||||
|
@ -44,7 +44,7 @@ pub(crate) use crate::{
|
||||||
// structs and enums
|
// structs and enums
|
||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
bytes::Bytes, env::Env, error::Error, file_info::FileInfo, hasher::Hasher, info::Info,
|
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,
|
subcommand::Subcommand, torrent::Torrent, use_color::UseColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
11
src/env.rs
11
src/env.rs
|
@ -91,6 +91,17 @@ impl Env {
|
||||||
self.err_style.message().suffix(),
|
self.err_style.message().suffix(),
|
||||||
)
|
)
|
||||||
.ok();
|
.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)
|
Err(EXIT_FAILURE)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
20
src/error.rs
20
src/error.rs
|
@ -37,7 +37,13 @@ pub(crate) enum Error {
|
||||||
bytes,
|
bytes,
|
||||||
Bytes(u32::max_value().into())
|
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))]
|
#[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))]
|
||||||
|
@ -51,6 +57,18 @@ pub(crate) enum Error {
|
||||||
feature
|
feature
|
||||||
))]
|
))]
|
||||||
Unstable { feature: &'static str },
|
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 {
|
impl From<clap::Error> for Error {
|
||||||
|
|
70
src/lint.rs
Normal file
70
src/lint.rs
Normal 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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,6 +52,7 @@ mod hasher;
|
||||||
mod info;
|
mod info;
|
||||||
mod into_u64;
|
mod into_u64;
|
||||||
mod into_usize;
|
mod into_usize;
|
||||||
|
mod lint;
|
||||||
mod metainfo;
|
mod metainfo;
|
||||||
mod mode;
|
mod mode;
|
||||||
mod opt;
|
mod opt;
|
||||||
|
|
|
@ -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`."
|
long_help = "Use `ANNOUNCE` as the primary tracker announce URL. To supply multiple announce URLs, also use `--announce-tier`."
|
||||||
)]
|
)]
|
||||||
announce: Url,
|
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(
|
#[structopt(
|
||||||
long = "announce-tier",
|
long = "announce-tier",
|
||||||
name = "ANNOUNCE-TIER",
|
name = "ANNOUNCE-TIER",
|
||||||
|
@ -98,15 +105,57 @@ Note: Many BitTorrent clients do not implement the behavior described in BEP 12.
|
||||||
private: bool,
|
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 {
|
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
|
let mut linter = Linter::new();
|
||||||
.piece_length
|
linter.allow(self.allowed_lints.iter().cloned());
|
||||||
.0
|
|
||||||
.try_into()
|
if linter.is_denied(Lint::UnevenPieceLength) && !self.piece_length.is_power_of_two() {
|
||||||
.map_err(|_| Error::PieceLength {
|
return Err(Error::PieceLengthUneven {
|
||||||
bytes: self.piece_length,
|
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);
|
let input = env.resolve(&self.input);
|
||||||
|
|
||||||
|
@ -405,14 +454,14 @@ mod tests {
|
||||||
"--announce",
|
"--announce",
|
||||||
"http://bar",
|
"http://bar",
|
||||||
"--piece-length",
|
"--piece-length",
|
||||||
"1",
|
"64KiB",
|
||||||
]);
|
]);
|
||||||
fs::write(env.resolve("foo"), "").unwrap();
|
fs::write(env.resolve("foo"), "").unwrap();
|
||||||
env.run().unwrap();
|
env.run().unwrap();
|
||||||
let torrent = env.resolve("foo.torrent");
|
let torrent = env.resolve("foo.torrent");
|
||||||
let bytes = fs::read(torrent).unwrap();
|
let bytes = fs::read(torrent).unwrap();
|
||||||
let metainfo = serde_bencode::de::from_bytes::<Metainfo>(&bytes).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]
|
#[test]
|
||||||
|
@ -441,7 +490,7 @@ mod tests {
|
||||||
"--announce",
|
"--announce",
|
||||||
"http://bar",
|
"http://bar",
|
||||||
"--piece-length",
|
"--piece-length",
|
||||||
"1",
|
"16KiB",
|
||||||
]);
|
]);
|
||||||
fs::write(env.resolve("foo"), "").unwrap();
|
fs::write(env.resolve("foo"), "").unwrap();
|
||||||
env.run().unwrap();
|
env.run().unwrap();
|
||||||
|
@ -459,7 +508,7 @@ mod tests {
|
||||||
"--announce",
|
"--announce",
|
||||||
"http://bar",
|
"http://bar",
|
||||||
"--piece-length",
|
"--piece-length",
|
||||||
"1",
|
"32KiB",
|
||||||
]);
|
]);
|
||||||
let dir = env.resolve("foo");
|
let dir = env.resolve("foo");
|
||||||
fs::create_dir(&dir).unwrap();
|
fs::create_dir(&dir).unwrap();
|
||||||
|
@ -588,6 +637,8 @@ mod tests {
|
||||||
"http://bar",
|
"http://bar",
|
||||||
"--piece-length",
|
"--piece-length",
|
||||||
"1",
|
"1",
|
||||||
|
"--allow",
|
||||||
|
"small-piece-length",
|
||||||
]);
|
]);
|
||||||
let contents = "bar";
|
let contents = "bar";
|
||||||
fs::write(env.resolve("foo"), contents).unwrap();
|
fs::write(env.resolve("foo"), contents).unwrap();
|
||||||
|
@ -732,4 +783,80 @@ mod tests {
|
||||||
|
|
||||||
panic!("Failed to read `opened.txt`.");
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user