From b0f449b6aedb78f185f8c7ecb451391b40de11e3 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Tue, 11 Feb 2020 03:08:57 -0800 Subject: [PATCH] Drop `serde_bencode` in favor of `bendy` For now depend on my branch on Github, until serde support and the value type land in the main repo. type: reform --- Cargo.lock | 106 ++++++++++- Cargo.toml | 8 + src/bencode.rs | 378 -------------------------------------- src/common.rs | 11 +- src/error.rs | 4 +- src/file_info.rs | 4 +- src/info.rs | 5 +- src/inner.rs | 52 ++++++ src/main.rs | 2 +- src/metainfo.rs | 47 ++++- src/mode.rs | 13 +- src/opt/torrent/create.rs | 131 +++++-------- src/opt/torrent/stats.rs | 107 ++++++++--- src/torrent_summary.rs | 9 +- 14 files changed, 362 insertions(+), 515 deletions(-) delete mode 100644 src/bencode.rs create mode 100644 src/inner.rs diff --git a/Cargo.lock b/Cargo.lock index 6a22305..a20389b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,38 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" +[[package]] +name = "backtrace" +version = "0.3.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f80256bc78f67e7df7e36d77366f636ed976895d91fe2ab9efa3973e8fe8c4f" +dependencies = [ + "backtrace-sys", + "cfg-if", + "libc", + "rustc-demangle", +] + +[[package]] +name = "backtrace-sys" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6575f128516de27e3ce99689419835fce9643a9b215a14d2b5b685be018491" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "bendy" +version = "0.2.2" +source = "git+https://github.com/casey/bendy.git?branch=value#8009217c7c753e0c3e1b6685bd9e9f980e5d38de" +dependencies = [ + "failure", + "serde", + "serde_bytes 0.11.3", +] + [[package]] name = "bitflags" version = "1.2.1" @@ -68,6 +100,12 @@ dependencies = [ "ppv-lite86", ] +[[package]] +name = "cc" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" + [[package]] name = "cfg-if" version = "0.1.10" @@ -136,6 +174,28 @@ dependencies = [ "termcolor", ] +[[package]] +name = "failure" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8273f13c977665c5db7eb2b99ae520952fe5ac831ae4cd09d80c4c7042b5ed9" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bc225b78e0391e4b8683440bf2e63c2deeeb2ce5189eab46e2b68c6d3725d08" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "fnv" version = "1.0.6" @@ -216,6 +276,7 @@ version = "0.0.1" dependencies = [ "ansi_term 0.12.1", "atty", + "bendy", "chrono", "env_logger", "globset", @@ -226,10 +287,12 @@ dependencies = [ "serde", "serde_bencode", "serde_bytes 0.11.3", + "serde_with", "sha1", "snafu", "static_assertions", "structopt", + "syn", "tempfile", "unicode-width", "url", @@ -461,6 +524,12 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "rustc-demangle" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" + [[package]] name = "rustversion" version = "1.0.2" @@ -492,9 +561,9 @@ dependencies = [ [[package]] name = "serde_bencode" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b79ce11369638af2fea08e00390316304c5b5e1b7a53d58ace925152b657443" +checksum = "315c49c11b6b10acc209df75b757ee70957b911ecd0e29bcbf2b735ebd580d45" dependencies = [ "serde", "serde_bytes 0.10.5", @@ -529,6 +598,27 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_with" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d3d595d64120bbbc70b7f6d5ae63298b62a3d9f373ec2f56acf5365ca8a444" +dependencies = [ + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4070d2c9b9d258465ad1d82aabb985b84cd9a3afa94da25ece5a9938ba5f1606" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.6.0" @@ -620,6 +710,18 @@ dependencies = [ "syn", ] +[[package]] +name = "synstructure" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "tempfile" version = "3.1.0" diff --git a/Cargo.toml b/Cargo.toml index 40dae1b..f46d854 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,14 +24,22 @@ pretty_assertions = "0.6" regex = "1" serde_bencode = "0.2" serde_bytes = "0.11" +serde_with = "1.4" sha1 = "0.6" snafu = "0.6" static_assertions = "1" +syn = "1.0.14" tempfile = "3" unicode-width = "0.1" url = "2" walkdir = "2.1" +[dependencies.bendy] +version = "0.2.2" +git = "https://github.com/casey/bendy.git" +branch = "value" +features = ["serde"] + [dependencies.serde] version = "1.0.103" features = ["derive"] diff --git a/src/bencode.rs b/src/bencode.rs deleted file mode 100644 index ce47d99..0000000 --- a/src/bencode.rs +++ /dev/null @@ -1,378 +0,0 @@ -use crate::common::*; - -use self::Error::*; - -#[derive(Clone)] -pub(crate) enum Value<'buffer> { - Int(&'buffer str), - List(Vec>), - Dict(Vec<(&'buffer [u8], Value<'buffer>)>), - Str(&'buffer [u8]), -} - -impl<'buffer> Value<'buffer> { - pub(crate) fn decode(buffer: &'buffer [u8]) -> Result, Error> { - Parser::parse(buffer) - } - - pub(crate) fn encode(&self) -> Vec { - let mut buffer = Vec::new(); - self.encode_into(&mut buffer); - buffer - } - - pub(crate) fn encode_into(&self, buffer: &mut Vec) { - match self { - Self::Int(value) => { - buffer.push(b'i'); - buffer.extend_from_slice(value.as_bytes()); - buffer.push(b'e'); - } - Self::List(values) => { - buffer.push(b'l'); - for value in values { - value.encode_into(buffer); - } - buffer.push(b'e'); - } - Self::Dict(items) => { - buffer.push(b'd'); - for (key, value) in items { - Self::encode_str(buffer, key); - value.encode_into(buffer); - } - buffer.push(b'e'); - } - Self::Str(contents) => Self::encode_str(buffer, contents), - } - } - - fn encode_str(buffer: &mut Vec, contents: &[u8]) { - buffer.extend_from_slice(contents.len().to_string().as_bytes()); - buffer.push(b':'); - buffer.extend_from_slice(contents); - } - - fn fmt_str(f: &mut Formatter, contents: &[u8]) -> fmt::Result { - if let Ok(text) = str::from_utf8(contents) { - write!(f, "\"{}\"", text) - } else { - write!(f, "<")?; - for byte in contents { - write!(f, "{:X}", byte)?; - } - write!(f, ">") - } - } -} - -impl<'buffer> Display for Value<'buffer> { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Int(value) => write!(f, "{}", value), - Self::List(values) => { - write!(f, "[")?; - for (i, value) in values.iter().enumerate() { - if i > 0 { - write!(f, ", ")?; - } - write!(f, "{}", value)?; - } - write!(f, "]") - } - Self::Dict(items) => { - write!(f, "{{")?; - for (i, (key, value)) in items.iter().enumerate() { - if i > 0 { - write!(f, ", ")?; - } - Value::fmt_str(f, key)?; - write!(f, ": {}", value)?; - } - write!(f, "}}") - } - Self::Str(contents) => Value::fmt_str(f, contents), - } - } -} - -#[derive(Debug, PartialEq)] -pub(crate) enum Error { - TrailingData { start: usize }, - UnexpectedEndOfBuffer, - UnexpectedByte { found: u8 }, - UnsortedKey, - DuplicateKey, - EmptyInteger, - NegativeZero, - LeadingZero, -} - -pub(crate) struct Parser<'buffer> { - index: usize, - buffer: &'buffer [u8], -} - -impl<'buffer> Parser<'buffer> { - pub(crate) fn parse(buffer: &'buffer [u8]) -> Result, Error> { - let parser = Parser { index: 0, buffer }; - - Ok(parser.root()?) - } - - fn root(mut self) -> Result, Error> { - let root = self.value()?; - - if self.index != self.buffer.len() { - return Err(TrailingData { start: self.index }); - } - - Ok(root) - } - - fn value(&mut self) -> Result, Error> { - match self.next()? { - b'i' => self.int(), - b'l' => self.list(), - b'd' => self.dict(), - b'0'..=b'9' => self.string(), - found => Err(UnexpectedByte { found }), - } - } - - fn extract_digits(&mut self) -> Result<&'buffer [u8], Error> { - let start = self.index; - - while let b'0'..=b'9' = self.next()? { - self.advance()?; - } - - Ok(&self.buffer[start..self.index]) - } - - fn parse_digits(digits: &[u8]) -> Result { - if digits.is_empty() { - return Err(EmptyInteger); - } - - if digits == b"0" { - return Ok(0); - } - - let mut i = 0; - - for digit in digits { - let value: u64 = (digit - b'0').into(); - - if value == 0 && i == 0 { - return Err(LeadingZero); - } - - i = i * 10 + value; - } - - Ok(i) - } - - fn int(&mut self) -> Result, Error> { - self.expect(b'i')?; - - let start = self.index; - - let negative = self.accept(b'-')?; - - let digits = self.extract_digits()?; - - let end = self.index; - - self.expect(b'e')?; - - let value = Self::parse_digits(digits)?; - - if value == 0 && negative { - return Err(NegativeZero); - } - - Ok(Value::Int( - str::from_utf8(&self.buffer[start..end]).unwrap(), - )) - } - - fn list(&mut self) -> Result, Error> { - self.expect(b'l')?; - - let mut values = Vec::new(); - - while self.next()? != b'e' { - values.push(self.value()?); - } - - self.expect(b'e')?; - - Ok(Value::List(values)) - } - - fn dict(&mut self) -> Result, Error> { - self.expect(b'd')?; - - let mut values: Vec<(&[u8], Value)> = Vec::new(); - - while self.next()? != b'e' { - let key = self.key()?; - - if let Some((last_key, _)) = values.last() { - match key.cmp(last_key) { - Ordering::Equal => return Err(DuplicateKey), - Ordering::Less => return Err(UnsortedKey), - Ordering::Greater => {} - } - } - - let value = self.value()?; - - values.push((key, value)); - } - - self.expect(b'e')?; - - Ok(Value::Dict(values)) - } - - fn string(&mut self) -> Result, Error> { - Ok(Value::Str(self.key()?)) - } - - fn key(&mut self) -> Result<&'buffer [u8], Error> { - let digits = self.extract_digits()?; - - self.expect(b':')?; - - let len = Self::parse_digits(digits)?; - - let start = self.index; - for _ in 0..len { - self.advance()?; - } - - Ok(&self.buffer[start..self.index]) - } - - fn next(&self) -> Result { - self - .buffer - .get(self.index) - .cloned() - .ok_or(UnexpectedEndOfBuffer) - } - - fn advance(&mut self) -> Result<(), Error> { - if self.index == self.buffer.len() { - Err(UnexpectedEndOfBuffer) - } else { - self.index += 1; - Ok(()) - } - } - - fn expect(&mut self, expected: u8) -> Result<(), Error> { - let found = self.next()?; - - if found != expected { - return Err(UnexpectedByte { found }); - } - - self.advance()?; - - Ok(()) - } - - fn accept(&mut self, acceptable: u8) -> Result { - if self.next()? == acceptable { - self.advance()?; - Ok(true) - } else { - Ok(false) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn err(input: impl AsRef<[u8]>, expected: Error) { - let buffer = input.as_ref(); - let text = String::from_utf8_lossy(buffer); - match Parser::parse(buffer) { - Ok(_) => panic!( - "Input `{}` passed validation, expected: {:?}", - text, expected, - ), - Err(error) => assert_eq!(error, expected, "Unexpected error for input `{}`", text), - } - } - - fn ok(input: impl AsRef<[u8]>) { - let buffer = input.as_ref(); - match Value::decode(buffer) { - Err(_) => { - panic!( - "Input failed to validate: `{}`", - String::from_utf8_lossy(buffer) - ); - } - Ok(value) => { - let round_trip = value.encode(); - assert_eq!(round_trip, buffer); - } - } - } - - #[test] - fn misc() { - err("", UnexpectedEndOfBuffer); - err("i20efoo", TrailingData { start: 4 }); - err("defoo", TrailingData { start: 2 }); - err("lefoo", TrailingData { start: 2 }); - err("1:afoo", TrailingData { start: 3 }); - } - - #[test] - fn int() { - err("ie", EmptyInteger); - err("i-0e", NegativeZero); - err("i00e", LeadingZero); - err("iae", UnexpectedByte { found: b'a' }); - ok("i0e"); - ok("i-100e"); - } - - #[test] - fn list() { - ok("le"); - ok("llelelee"); - ok("li20ee"); - ok("li20edelee"); - } - - #[test] - fn dict() { - ok("de"); - ok("d0:0:e"); - err("di0elee", UnexpectedByte { found: b'i' }); - err("d0:i0ei0ei0ee", UnexpectedByte { found: b'i' }); - err("d0:e", UnexpectedByte { found: b'e' }); - err("d0:de0:dee", DuplicateKey); - err("d1:ade0:dee", UnsortedKey); - err("d1:ade0:dee", UnsortedKey); - ok("d1:ade1:bde1:cdee"); - } - - #[test] - fn string() { - ok("0:"); - ok("5:hello"); - err("1:", UnexpectedEndOfBuffer); - err("2:a", UnexpectedEndOfBuffer); - } -} diff --git a/src/common.rs b/src/common.rs index 49f1892..c5860d9 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,7 +1,7 @@ // stdlib types pub(crate) use std::{ borrow::Cow, - cmp::{Ordering, Reverse}, + cmp::Reverse, collections::{BTreeMap, BTreeSet, HashMap}, convert::{Infallible, TryInto}, env, @@ -20,11 +20,13 @@ pub(crate) use std::{ }; // dependencies +pub(crate) use bendy::{decoding::FromBencode, encoding::ToBencode, value::Value}; pub(crate) use chrono::{TimeZone, Utc}; pub(crate) use globset::{Glob, GlobMatcher}; pub(crate) use libc::EXIT_FAILURE; pub(crate) use regex::{Regex, RegexSet}; -pub(crate) use serde::{Deserialize, Serialize}; +pub(crate) use serde::{Deserialize, Deserializer, Serialize, Serializer}; +pub(crate) use serde_with::skip_serializing_none; pub(crate) use sha1::Sha1; pub(crate) use snafu::{ResultExt, Snafu}; pub(crate) use static_assertions::const_assert; @@ -37,7 +39,7 @@ pub(crate) use url::Url; pub(crate) use walkdir::WalkDir; // modules -pub(crate) use crate::{bencode, consts, error, use_color}; +pub(crate) use crate::{consts, error, inner, use_color}; // traits pub(crate) use crate::{ @@ -70,3 +72,6 @@ pub(crate) use crate::testing; // test structs and enums #[cfg(test)] pub(crate) use crate::{capture::Capture, test_env::TestEnv, test_env_builder::TestEnvBuilder}; + +// type aliases +pub(crate) type Result = std::result::Result; diff --git a/src/error.rs b/src/error.rs index 29af2fc..ba2acc1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,11 +11,11 @@ pub(crate) enum Error { AnnounceUrlParse { source: url::ParseError }, #[snafu(display("Failed to deserialize torrent metainfo from `{}`: {}", path.display(), source))] MetainfoLoad { - source: serde_bencode::Error, + source: bendy::serde::Error, path: PathBuf, }, #[snafu(display("Failed to serialize torrent metainfo: {}", source))] - MetainfoSerialize { source: serde_bencode::Error }, + MetainfoSerialize { source: bendy::serde::Error }, #[snafu(display("Failed to parse byte count `{}`: {}", text, source))] ByteParse { text: String, diff --git a/src/file_info.rs b/src/file_info.rs index bc9f4c9..7831c86 100644 --- a/src/file_info.rs +++ b/src/file_info.rs @@ -1,8 +1,10 @@ use crate::common::*; +#[skip_serializing_none] #[derive(Deserialize, Serialize, Debug, PartialEq)] pub(crate) struct FileInfo { pub(crate) length: u64, - pub(crate) md5sum: Option, pub(crate) path: FilePath, + #[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] + pub(crate) md5sum: Option, } diff --git a/src/info.rs b/src/info.rs index 4345e15..4bbc7ce 100644 --- a/src/info.rs +++ b/src/info.rs @@ -1,11 +1,14 @@ use crate::common::*; -#[derive(Deserialize, Serialize)] +#[skip_serializing_none] +#[derive(Deserialize, Serialize, Debug, PartialEq)] pub(crate) struct Info { + #[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] pub(crate) private: Option, #[serde(rename = "piece length")] pub(crate) piece_length: u32, pub(crate) name: String, + #[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] pub(crate) source: Option, #[serde(with = "serde_bytes")] pub(crate) pieces: Vec, diff --git a/src/inner.rs b/src/inner.rs new file mode 100644 index 0000000..511f866 --- /dev/null +++ b/src/inner.rs @@ -0,0 +1,52 @@ +use crate::common::*; + +pub(crate) fn serialize(value: &Option, serializer: S) -> Result +where + S: Serializer, + T: Serialize, +{ + value.as_ref().unwrap().serialize(serializer) +} + +pub(crate) fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: Deserialize<'de>, +{ + Ok(Some(T::deserialize(deserializer)?)) +} + +#[cfg(test)] +mod tests { + use super::*; + + use serde::{de::DeserializeOwned, Deserialize, Serialize}; + + use std::fmt::Debug; + + fn case(value: T, expected: impl AsRef<[u8]>) + where + T: Serialize + DeserializeOwned + PartialEq + Debug, + { + let serialized = bendy::serde::ser::to_bytes(&value).unwrap(); + assert_eq!(serialized, expected.as_ref()); + + let deserialized = bendy::serde::de::from_bytes(&serialized).unwrap(); + assert_eq!(value, deserialized); + } + + #[test] + fn serialize() { + #[derive(Deserialize, Serialize, Debug, PartialEq)] + struct Foo { + #[serde(skip_serializing_if = "Option::is_none", default, with = "super")] + pub(crate) bar: Option, + } + + let none = Foo { bar: None }; + case(none, b"de"); + + let some = Foo { bar: Some(1) }; + case(some, b"d3:bari1ee"); + } +} diff --git a/src/main.rs b/src/main.rs index fde9e31..31fcebc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,7 +54,6 @@ mod test_env_builder; #[cfg(test)] mod capture; -mod bencode; mod bytes; mod common; mod consts; @@ -65,6 +64,7 @@ mod file_path; mod files; mod hasher; mod info; +mod inner; mod into_u64; mod into_usize; mod lint; diff --git a/src/metainfo.rs b/src/metainfo.rs index ca87bab..471fd5a 100644 --- a/src/metainfo.rs +++ b/src/metainfo.rs @@ -1,21 +1,28 @@ use crate::common::*; -#[derive(Deserialize, Serialize)] +#[skip_serializing_none] +#[derive(Deserialize, Serialize, Debug, PartialEq)] pub(crate) struct Metainfo { pub(crate) announce: String, #[serde(rename = "announce-list")] + #[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] pub(crate) announce_list: Option>>, + #[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] pub(crate) comment: Option, #[serde(rename = "created by")] + #[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] pub(crate) created_by: Option, #[serde(rename = "creation date")] + #[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] pub(crate) creation_date: Option, + #[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] pub(crate) encoding: Option, pub(crate) info: Info, } impl Metainfo { - pub(crate) fn _load(path: impl AsRef) -> Result { + #[cfg(test)] + pub(crate) fn load(path: impl AsRef) -> Result { let path = path.as_ref(); let bytes = fs::read(path).context(error::Filesystem { path })?; Self::deserialize(path, &bytes) @@ -24,17 +31,45 @@ impl Metainfo { #[cfg(test)] pub(crate) fn dump(&self, path: impl AsRef) -> Result<(), Error> { let path = path.as_ref(); - let bytes = serde_bencode::ser::to_bytes(&self).context(error::MetainfoSerialize)?; - fs::write(path, &bytes).context(error::Filesystem { path })?; + let bendy = bendy::serde::ser::to_bytes(&self).context(error::MetainfoSerialize)?; + let serde_bencode = serde_bencode::ser::to_bytes(&self).unwrap(); + if bendy != serde_bencode { + panic!( + "Serialize bendy != serde_bencode:\n{}\n{}", + String::from_utf8_lossy(&bendy), + String::from_utf8_lossy(&serde_bencode) + ); + } + fs::write(path, &bendy).context(error::Filesystem { path })?; Ok(()) } pub(crate) fn deserialize(path: impl AsRef, bytes: &[u8]) -> Result { let path = path.as_ref(); - serde_bencode::de::from_bytes(&bytes).context(error::MetainfoLoad { path }) + let bendy = bendy::serde::de::from_bytes(&bytes).context(error::MetainfoLoad { path })?; + let serde_bencode = serde_bencode::de::from_bytes(&bytes).unwrap(); + assert_eq!(bendy, serde_bencode); + Ok(bendy) } pub(crate) fn serialize(&self) -> Result, Error> { - serde_bencode::ser::to_bytes(&self).context(error::MetainfoSerialize) + let bendy = bendy::serde::ser::to_bytes(&self).context(error::MetainfoSerialize)?; + let serde_bencode = serde_bencode::ser::to_bytes(&self).unwrap(); + if bendy != serde_bencode { + panic!( + "Serialize bendy != serde_bencode:\n{}\n{}", + String::from_utf8_lossy(&bendy), + String::from_utf8_lossy(&serde_bencode) + ); + } + Ok(bendy) + } + + #[cfg(test)] + pub(crate) fn from_bytes(bytes: &[u8]) -> Metainfo { + let bendy = bendy::serde::de::from_bytes(bytes).unwrap(); + let serde_bencode = serde_bencode::de::from_bytes(bytes).unwrap(); + assert_eq!(bendy, serde_bencode); + bendy } } diff --git a/src/mode.rs b/src/mode.rs index d345a1e..6baf045 100644 --- a/src/mode.rs +++ b/src/mode.rs @@ -1,10 +1,17 @@ use crate::common::*; -#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[skip_serializing_none] #[serde(untagged)] +#[derive(Deserialize, Serialize, Debug, PartialEq)] pub(crate) enum Mode { - Single { length: u64, md5sum: Option }, - Multiple { files: Vec }, + Single { + length: u64, + #[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] + md5sum: Option, + }, + Multiple { + files: Vec, + }, } impl Mode { diff --git a/src/opt/torrent/create.rs b/src/opt/torrent/create.rs index fd4773c..e243f13 100644 --- a/src/opt/torrent/create.rs +++ b/src/opt/torrent/create.rs @@ -321,8 +321,8 @@ mod tests { env.run().unwrap(); let torrent = env.resolve("foo.torrent"); let bytes = fs::read(torrent).unwrap(); - let value = bencode::Value::decode(&bytes).unwrap(); - assert!(matches!(value, bencode::Value::Dict(_))); + let value = Value::from_bencode(&bytes).unwrap(); + assert!(matches!(value, Value::Dict(_))); } #[test] @@ -331,8 +331,7 @@ mod tests { 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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.info.private, None); } @@ -342,8 +341,7 @@ mod tests { 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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.info.private, Some(1)); } @@ -360,8 +358,7 @@ mod tests { 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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.announce, "http://bar/"); assert!(metainfo.announce_list.is_none()); } @@ -377,8 +374,7 @@ mod tests { 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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!( metainfo.announce, "udp://tracker.opentrackr.org:1337/announce" @@ -392,8 +388,7 @@ mod tests { 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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.announce, "wss://tracker.btorrent.xyz/"); assert!(metainfo.announce_list.is_none()); } @@ -411,8 +406,7 @@ mod tests { 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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.announce, "http://bar/"); assert_eq!( metainfo.announce_list, @@ -435,8 +429,7 @@ mod tests { 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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.announce, "http://bar/"); assert_eq!( metainfo.announce_list, @@ -453,8 +446,7 @@ mod tests { 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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.comment, None); } @@ -471,8 +463,7 @@ mod tests { 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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.comment.unwrap(), "Hello, world!"); } @@ -482,8 +473,7 @@ mod tests { 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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.info.piece_length, 16 * 2u32.pow(10)); } @@ -500,8 +490,7 @@ mod tests { 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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.info.piece_length, 64 * 1024); } @@ -518,8 +507,7 @@ mod tests { 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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.info.piece_length, 512 * 1024); } @@ -536,8 +524,7 @@ mod tests { 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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.info.name, "foo"); } @@ -556,8 +543,7 @@ mod tests { fs::write(dir.join("bar"), "").unwrap(); env.run().unwrap(); let torrent = dir.join("bar.torrent"); - let bytes = fs::read(torrent).unwrap(); - let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.info.name, "bar"); } @@ -574,8 +560,7 @@ mod tests { fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); let torrent = env.resolve("x.torrent"); - let bytes = fs::read(torrent).unwrap(); - serde_bencode::de::from_bytes::(&bytes).unwrap(); + Metainfo::load(torrent).unwrap(); } #[test] @@ -584,8 +569,7 @@ mod tests { 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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.created_by.unwrap(), consts::CREATED_BY_DEFAULT); } @@ -601,8 +585,7 @@ mod tests { 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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.created_by, None); } @@ -612,8 +595,7 @@ mod tests { 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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.encoding, Some("UTF-8".into())); } @@ -627,8 +609,7 @@ mod tests { .as_secs(); env.run().unwrap(); let torrent = env.resolve("foo.torrent"); - let bytes = fs::read(torrent).unwrap(); - let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + let metainfo = Metainfo::load(torrent).unwrap(); assert!(metainfo.creation_date.unwrap() < now + 10); assert!(metainfo.creation_date.unwrap() > now - 10); } @@ -645,8 +626,7 @@ mod tests { 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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.creation_date, None); } @@ -657,8 +637,7 @@ mod tests { fs::write(env.resolve("foo"), contents).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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes()); assert_eq!( metainfo.info.mode, @@ -685,8 +664,7 @@ mod tests { fs::write(env.resolve("foo"), contents).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(); + let metainfo = Metainfo::load(torrent).unwrap(); let pieces = Sha1::from("b") .digest() .bytes() @@ -713,8 +691,7 @@ mod tests { fs::write(env.resolve("foo"), contents).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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.info.pieces.len(), 0); assert_eq!( metainfo.info.mode, @@ -732,8 +709,7 @@ mod tests { fs::create_dir(&dir).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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.info.pieces.len(), 0); assert_eq!(metainfo.info.mode, Mode::Multiple { files: Vec::new() }) } @@ -748,8 +724,7 @@ mod tests { fs::write(file, contents).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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes()); match metainfo.info.mode { Mode::Multiple { files } => { @@ -776,8 +751,7 @@ mod tests { fs::write(file, contents).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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes()); match metainfo.info.mode { Mode::Multiple { files } => { @@ -804,8 +778,7 @@ mod tests { fs::write(dir.join("h"), "hij").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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!( metainfo.info.pieces, Sha1::from("abchijxyz").digest().bytes() @@ -1028,8 +1001,7 @@ Content Size 9 bytes fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); let bytes = env.out_bytes(); - let value = bencode::Value::decode(&bytes).unwrap(); - assert!(matches!(value, bencode::Value::Dict(_))); + Metainfo::from_bytes(&bytes); } #[test] @@ -1052,8 +1024,8 @@ Content Size 9 bytes env.run().unwrap(); let torrent = env.resolve("foo.torrent"); let bytes = fs::read(torrent).unwrap(); - let value = bencode::Value::decode(&bytes).unwrap(); - assert!(matches!(value, bencode::Value::Dict(_))); + let value = Value::from_bencode(&bytes).unwrap(); + assert!(matches!(value, Value::Dict(_))); } #[test] @@ -1065,8 +1037,7 @@ Content Size 9 bytes fs::write(dir.join("Desktop.ini"), "abc").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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.is_empty() @@ -1089,8 +1060,7 @@ Content Size 9 bytes fs::write(dir.join("Desktop.ini"), "abc").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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.len() == 2 @@ -1126,8 +1096,7 @@ Content Size 9 bytes } env.run().unwrap(); let torrent = env.resolve("foo.torrent"); - let bytes = fs::read(torrent).unwrap(); - let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.len() == 0 @@ -1149,8 +1118,7 @@ Content Size 9 bytes fs::write(dir.join(".hidden"), "abc").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(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.len() == 1 @@ -1194,8 +1162,7 @@ Content Size 9 bytes populate_symlinks(&env); env.run().unwrap(); let torrent = env.resolve("foo.torrent"); - let bytes = fs::read(torrent).unwrap(); - let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.is_empty() @@ -1217,8 +1184,7 @@ Content Size 9 bytes populate_symlinks(&env); env.run().unwrap(); let torrent = env.resolve("foo.torrent"); - let bytes = fs::read(torrent).unwrap(); - let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.info.pieces, Sha1::from("barbaz").digest().bytes()); match metainfo.info.mode { Mode::Multiple { files } => { @@ -1266,8 +1232,7 @@ Content Size 9 bytes env.create_file("foo/.bar/baz", "baz"); env.run().unwrap(); let torrent = env.resolve("foo.torrent"); - let bytes = fs::read(torrent).unwrap(); - let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.is_empty() @@ -1301,8 +1266,7 @@ Content Size 9 bytes } env.run().unwrap(); let torrent = env.resolve("foo.torrent"); - let bytes = fs::read(torrent).unwrap(); - let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.is_empty() @@ -1319,8 +1283,7 @@ Content Size 9 bytes env.create_file("foo/c", "c"); env.run().unwrap(); let torrent = env.resolve("foo.torrent"); - let bytes = fs::read(torrent).unwrap(); - let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.len() == 2 @@ -1337,8 +1300,7 @@ Content Size 9 bytes env.create_file("foo/c", "c"); env.run().unwrap(); let torrent = env.resolve("foo.torrent"); - let bytes = fs::read(torrent).unwrap(); - let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.len() == 3 @@ -1362,8 +1324,7 @@ Content Size 9 bytes env.create_file("foo/c", "c"); env.run().unwrap(); let torrent = env.resolve("foo.torrent"); - let bytes = fs::read(torrent).unwrap(); - let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.len() == 2 @@ -1380,8 +1341,7 @@ Content Size 9 bytes env.create_file("foo/c", "c"); env.run().unwrap(); let torrent = env.resolve("foo.torrent"); - let bytes = fs::read(torrent).unwrap(); - let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.is_empty() @@ -1409,8 +1369,7 @@ Content Size 9 bytes env.create_file("foo/c", "c"); env.run().unwrap(); let torrent = env.resolve("foo.torrent"); - let bytes = fs::read(torrent).unwrap(); - let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + let metainfo = Metainfo::load(torrent).unwrap(); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.len() == 1 diff --git a/src/opt/torrent/stats.rs b/src/opt/torrent/stats.rs index eda6c85..6104633 100644 --- a/src/opt/torrent/stats.rs +++ b/src/opt/torrent/stats.rs @@ -160,55 +160,106 @@ impl Extractor { return; }; - let value = if let Ok(value) = bencode::Value::decode(&contents) { - value + if let Ok(value) = Value::from_bencode(&contents) { + self.extract(&value); + if self.print { + eprintln!("{}:\n{}", path.display(), Self::pretty_print(&value)); + } } else { self.bencode_decode_errors += 1; - return; - }; - - if self.print { - eprintln!("{}:\n{}", path.display(), value); } - - self.extract(&value); } - fn extract(&mut self, value: &bencode::Value) { - use bencode::Value::*; - + fn extract(&mut self, value: &Value) { let matches = self.regex_set.matches(&self.current_path); for i in matches.iter() { let pattern = &self.regex_set.patterns()[i]; if let Some(values) = self.values.get_mut(pattern) { - values.push(value.to_string()); + values.push(Self::pretty_print(value)); } else { - self.values.insert(pattern.clone(), vec![value.to_string()]); + self + .values + .insert(pattern.clone(), vec![Self::pretty_print(value)]); } } let starting_length = self.current_path.len(); - if let Dict(items) = value { - for (key, value) in items { - match String::from_utf8_lossy(key) { - Cow::Borrowed(s) => self.current_path.push_str(s), - Cow::Owned(s) => self.current_path.push_str(&s), + match value { + Value::List(list) => { + if self.current_path.pop().is_some() { + self.current_path.push('*'); + } + for value in list { + self.extract(value); } - self.paths.increment_ref(&self.current_path); - self.current_path.push('/'); - self.extract(value); self.current_path.truncate(starting_length); } - } else if let List(values) = value { - if self.current_path.pop().is_some() { - self.current_path.push('*'); + Value::Dict(dict) => { + for (key, value) in dict { + match String::from_utf8_lossy(key) { + Cow::Borrowed(s) => self.current_path.push_str(s), + Cow::Owned(s) => self.current_path.push_str(&s), + } + self.paths.increment_ref(&self.current_path); + self.current_path.push('/'); + self.extract(value); + self.current_path.truncate(starting_length); + } } - for value in values { - self.extract(value); + Value::Integer(_) | Value::Bytes(_) => {} + } + } + + fn pretty_print(value: &Value) -> String { + let mut buffer = String::new(); + Self::pretty_print_inner(value, &mut buffer); + buffer + } + + fn pretty_print_inner(value: &Value, buffer: &mut String) { + match value { + Value::List(list) => { + buffer.push('['); + for (i, value) in list.iter().enumerate() { + if i > 0 { + buffer.push_str(", "); + } + Self::pretty_print_inner(value, buffer); + } + buffer.push(']'); } - self.current_path.truncate(starting_length); + Value::Dict(dict) => { + buffer.push('{'); + for (i, (key, value)) in dict.iter().enumerate() { + if i > 0 { + buffer.push_str(", "); + } + Self::pretty_print_string(key, buffer); + buffer.push_str(": "); + Self::pretty_print_inner(value, buffer); + } + buffer.push('}'); + } + Value::Integer(integer) => buffer.push_str(&integer.to_string()), + Value::Bytes(bytes) => { + Self::pretty_print_string(bytes, buffer); + } + } + } + + fn pretty_print_string(string: &[u8], buffer: &mut String) { + if let Ok(text) = str::from_utf8(string) { + buffer.push('\"'); + buffer.push_str(text); + buffer.push('\"'); + } else { + buffer.push('<'); + for byte in string { + buffer.push_str(&format!("{:X}", byte)); + } + buffer.push('>'); } } } diff --git a/src/torrent_summary.rs b/src/torrent_summary.rs index df5729a..40c0047 100644 --- a/src/torrent_summary.rs +++ b/src/torrent_summary.rs @@ -8,15 +8,16 @@ pub(crate) struct TorrentSummary { impl TorrentSummary { fn new(bytes: &[u8], metainfo: Metainfo) -> Result { - let value = bencode::Value::decode(&bytes).unwrap(); + let value = Value::from_bencode(&bytes).unwrap(); - let infohash = if let bencode::Value::Dict(items) = value { + let infohash = if let Value::Dict(items) = value { let info = items .iter() - .find(|(key, _value)| key == b"info") + .find(|pair: &(&Vec, &Value)| pair.0 == b"info") .unwrap() .1 - .encode(); + .to_bencode() + .unwrap(); Sha1::from(info).digest() } else { unreachable!()