Skip to content

Commit

Permalink
Add optional Arbitrary impls
Browse files Browse the repository at this point in the history
This enables us to use structure-aware fuzzing for more efficient
coverage of serialize/parse roundtripping.

https://rust-fuzz.github.io/book/cargo-fuzz/structure-aware-fuzzing.html
  • Loading branch information
apasel422 committed Feb 20, 2025
1 parent 13e254e commit d9c5865
Show file tree
Hide file tree
Showing 14 changed files with 142 additions and 36 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ keywords = ["http-header", "structured-header", ]
exclude = ["tests/**", ".github/*"]

[dependencies]
arbitrary = { version = "1.4.1", optional = true, features = ["derive"] }
base64 = "0.22.1"
indexmap = "2"
ref-cast = "1.0.23"
Expand All @@ -26,3 +27,6 @@ base32 = "0.5.1"
[[bench]]
name = "bench"
harness = false

[features]
arbitrary = ["dep:arbitrary", "indexmap/arbitrary"]
22 changes: 22 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ libfuzzer-sys = "0.4"

[dependencies.sfv]
path = ".."
features = ["arbitrary"]

[[bin]]
name = "parse_dictionary"
Expand All @@ -33,3 +34,24 @@ path = "fuzz_targets/parse_item.rs"
test = false
doc = false
bench = false

[[bin]]
name = "roundtrip_item"
path = "fuzz_targets/roundtrip_item.rs"
test = false
doc = false
bench = false

[[bin]]
name = "roundtrip_list"
path = "fuzz_targets/roundtrip_list.rs"
test = false
doc = false
bench = false

[[bin]]
name = "roundtrip_dictionary"
path = "fuzz_targets/roundtrip_dictionary.rs"
test = false
doc = false
bench = false
13 changes: 1 addition & 12 deletions fuzz/fuzz_targets/parse_dictionary.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
#![no_main]

use libfuzzer_sys::fuzz_target;
use sfv::SerializeValue as _;

fuzz_target!(|data: &[u8]| {
if let Ok(dict) = sfv::Parser::from_bytes(data).parse_dictionary() {
let serialized = dict.serialize_value();
if dict.is_empty() {
assert!(serialized.is_err());
} else {
assert_eq!(
sfv::Parser::from_bytes(serialized.unwrap().as_bytes()).parse_dictionary(),
Ok(dict)
);
}
}
let _ = sfv::Parser::from_bytes(data).parse_dictionary();
});
9 changes: 1 addition & 8 deletions fuzz/fuzz_targets/parse_item.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
#![no_main]

use libfuzzer_sys::fuzz_target;
use sfv::SerializeValue as _;

fuzz_target!(|data: &[u8]| {
if let Ok(item) = sfv::Parser::from_bytes(data).parse_item() {
let serialized = item.serialize_value().unwrap();
assert_eq!(
sfv::Parser::from_bytes(serialized.as_bytes()).parse_item(),
Ok(item)
);
}
let _ = sfv::Parser::from_bytes(data).parse_item();
});
13 changes: 1 addition & 12 deletions fuzz/fuzz_targets/parse_list.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
#![no_main]

use libfuzzer_sys::fuzz_target;
use sfv::SerializeValue as _;

fuzz_target!(|data: &[u8]| {
if let Ok(list) = sfv::Parser::from_bytes(data).parse_list() {
let serialized = list.serialize_value();
if list.is_empty() {
assert!(serialized.is_err());
} else {
assert_eq!(
sfv::Parser::from_bytes(serialized.unwrap().as_bytes()).parse_list(),
Ok(list)
);
}
}
let _ = sfv::Parser::from_bytes(data).parse_list();
});
16 changes: 16 additions & 0 deletions fuzz/fuzz_targets/roundtrip_dictionary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#![no_main]

use libfuzzer_sys::fuzz_target;
use sfv::SerializeValue as _;

fuzz_target!(|dict: sfv::Dictionary| {
let serialized = dict.serialize_value();
if dict.is_empty() {
assert!(serialized.is_err());
} else {
assert_eq!(
sfv::Parser::from_bytes(serialized.unwrap().as_bytes()).parse_dictionary(),
Ok(dict)
);
}
});
12 changes: 12 additions & 0 deletions fuzz/fuzz_targets/roundtrip_item.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#![no_main]

use libfuzzer_sys::fuzz_target;
use sfv::SerializeValue as _;

fuzz_target!(|item: sfv::Item| {
let serialized = item.serialize_value().unwrap();
assert_eq!(
sfv::Parser::from_bytes(serialized.as_bytes()).parse_item(),
Ok(item)
);
});
16 changes: 16 additions & 0 deletions fuzz/fuzz_targets/roundtrip_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#![no_main]

use libfuzzer_sys::fuzz_target;
use sfv::SerializeValue as _;

fuzz_target!(|list: sfv::List| {
let serialized = list.serialize_value();
if list.is_empty() {
assert!(serialized.is_err());
} else {
assert_eq!(
sfv::Parser::from_bytes(serialized.unwrap().as_bytes()).parse_list(),
Ok(list)
);
}
});
1 change: 1 addition & 0 deletions src/decimal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::fmt;
///
/// [decimal]: <https://httpwg.org/specs/rfc8941.html#decimal>
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Decimal(Integer);

impl Decimal {
Expand Down
17 changes: 13 additions & 4 deletions src/integer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@ use crate::{BareItem, RefBareItem};
use std::convert::{TryFrom, TryInto};
use std::fmt;

const RANGE_I64: std::ops::RangeInclusive<i64> = -999_999_999_999_999..=999_999_999_999_999;

/// A structured field value [integer].
///
/// [integer]: <https://httpwg.org/specs/rfc8941.html#integer>
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Integer(i64);
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Integer(
#[cfg_attr(
feature = "arbitrary",
arbitrary(with = |u: &mut arbitrary::Unstructured| u.int_in_range(RANGE_I64))
)]
i64,
);

impl Integer {
/// The minimum value for a parsed or serialized integer: `-999_999_999_999_999`.
pub const MIN: Self = Self(-999_999_999_999_999);
pub const MIN: Self = Self(*RANGE_I64.start());

/// The maximum value for a parsed or serialized integer: `999_999_999_999_999`.
pub const MAX: Self = Self(999_999_999_999_999);
pub const MAX: Self = Self(*RANGE_I64.end());

/// `0`.
///
Expand Down Expand Up @@ -86,7 +95,7 @@ macro_rules! impl_conversion {

fn try_from(v: $t) -> Result<Integer, OutOfRangeError> {
match i64::try_from(v) {
Ok(v) if (Integer::MIN.0..=Integer::MAX.0).contains(&v) => Ok(Integer(v)),
Ok(v) if RANGE_I64.contains(&v) => Ok(Integer(v)),
_ => Err(OutOfRangeError),
}
}
Expand Down
22 changes: 22 additions & 0 deletions src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,25 @@ impl Borrow<str> for KeyRef {
self.as_str()
}
}

#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for &'a KeyRef {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
KeyRef::from_str(<&str>::arbitrary(u)?).map_err(|_| arbitrary::Error::IncorrectFormat)
}

fn size_hint(_depth: usize) -> (usize, Option<usize>) {
(1, None)
}
}

#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for Key {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
<&KeyRef>::arbitrary(u).map(ToOwned::to_owned)
}

fn size_hint(_depth: usize) -> (usize, Option<usize>) {
(1, None)
}
}
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ type SFVResult<T> = std::result::Result<T, &'static str>;
// bare-item = sf-integer / sf-decimal / sf-string / sf-token
// / sf-binary / sf-boolean
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Item {
/// Value of `Item`.
pub bare_item: BareItem,
Expand Down Expand Up @@ -258,6 +259,7 @@ pub type Parameters = IndexMap<Key, BareItem>;

/// Represents a member of `List` or `Dictionary` structured field value.
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub enum ListEntry {
/// Member of `Item` type.
Item(Item),
Expand All @@ -281,6 +283,7 @@ impl From<InnerList> for ListEntry {
// inner-list = "(" *SP [ sf-item *( 1*SP sf-item ) *SP ] ")"
// parameters
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct InnerList {
/// `Items` that `InnerList` contains. Can be empty.
pub items: Vec<Item>,
Expand All @@ -305,6 +308,7 @@ impl InnerList {

/// `BareItem` type is used to construct `Items` or `Parameters` values.
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub enum BareItem {
/// Decimal number
// sf-decimal = ["-"] 1*12DIGIT "." 1*3DIGIT
Expand Down Expand Up @@ -457,6 +461,7 @@ pub(crate) enum Num {

/// Similar to `BareItem`, but used to serialize values via `RefItemSerializer`, `RefListSerializer`, `RefDictSerializer`.
#[derive(Debug, PartialEq, Clone, Copy)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub enum RefBareItem<'a> {
Integer(Integer),
Decimal(Decimal),
Expand Down
14 changes: 14 additions & 0 deletions src/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,17 @@ impl Borrow<str> for StringRef {
self.as_str()
}
}

#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for &'a StringRef {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
StringRef::from_str(<&str>::arbitrary(u)?).map_err(|_| arbitrary::Error::IncorrectFormat)
}
}

#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for String {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
<&StringRef>::arbitrary(u).map(ToOwned::to_owned)
}
}
14 changes: 14 additions & 0 deletions src/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,17 @@ impl Borrow<str> for TokenRef {
self.as_str()
}
}

#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for &'a TokenRef {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
TokenRef::from_str(<&str>::arbitrary(u)?).map_err(|_| arbitrary::Error::IncorrectFormat)
}
}

#[cfg(feature = "arbitrary")]
impl<'a> arbitrary::Arbitrary<'a> for Token {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
<&TokenRef>::arbitrary(u).map(ToOwned::to_owned)
}
}

0 comments on commit d9c5865

Please sign in to comment.