Skip to content

Commit

Permalink
plug fuzzing corpus to tests
Browse files Browse the repository at this point in the history
  • Loading branch information
remi-dupre committed Feb 12, 2025
1 parent ff7ed59 commit ff734c7
Show file tree
Hide file tree
Showing 12 changed files with 183 additions and 111 deletions.
10 changes: 8 additions & 2 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,16 @@ jobs:
- { crate: ".", features: "log,auto-country" }
- { crate: ".", features: "log,auto-country,auto-timezone" }
- { crate: ".", features: "log,auto-timezone" }
- { crate: ".", features: "fuzzing" }
- { crate: "opening-hours-syntax", features: "log" }
defaults:
run:
working-directory: ${{ matrix.crate }}
steps:
- uses: actions/checkout@v4
with:
submodules: true
token: ${{ secrets.GITHUB_TOKEN }}
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
Expand Down Expand Up @@ -102,8 +106,10 @@ jobs:
image: xd009642/tarpaulin:develop-nightly
options: --security-opt seccomp=unconfined
steps:
- name: Checkout repository
uses: actions/checkout@v2
- uses: actions/checkout@v2
with:
submodules: true
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Python library
run: apt-get update && apt-get install -yy python3-dev && apt-get clean
- name: Generate code coverage
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ default = ["log"]
auto-country = ["dep:country-boundaries"]
auto-timezone = ["dep:chrono-tz", "dep:tzf-rs"]
log = ["opening-hours-syntax/log", "dep:log"]
fuzzing = ["auto-country", "auto-timezone", "dep:arbitrary"]

# Disable timeout behavior for performance tests. This is useful when tests
# need to be in slow environments or with high overhead, for example when
Expand All @@ -49,6 +50,9 @@ country-boundaries = { version = "1.2", optional = true }
chrono-tz = { version = "0.10", optional = true }
tzf-rs = { version = "0.4", default-features = false, optional = true }

# Feature: fuzzing
arbitrary = { version = "1", features = ["derive"], optional = true }

[build-dependencies]
chrono = "0.4"
compact-calendar = { path = "compact-calendar", version = "1.1.0" }
Expand All @@ -69,6 +73,9 @@ name = "benchmarks"
harness = false
path = "opening-hours/benches/benchmarks.rs"

[profile.test]
opt-level = 2 # fuzzing corpus is quite long to run through

[profile.dev.package.flate2]
opt-level = 3 # build script will run substantially faster for dev

Expand Down
6 changes: 5 additions & 1 deletion fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ version = "0.1.0"
authors = ["Rémi Dupré <remi@dupre.io>"]
edition = "2021"

[features]
default = ["fuzzing"]
fuzzing = []

[[bin]]
name = "fuzz_oh"
path = "fuzz_targets/fuzz_oh.rs"
Expand All @@ -15,4 +19,4 @@ cargo-fuzz = true
arbitrary = { version = "1", features = ["derive"] }
chrono = "0.4"
libfuzzer-sys = "0.4"
opening-hours = { path = "..", features = ["auto-country", "auto-timezone"] }
opening-hours = { path = "..", features = ["auto-country", "auto-timezone", "fuzzing"] }
2 changes: 1 addition & 1 deletion fuzz/corpus
Submodule corpus updated 851 files
111 changes: 5 additions & 106 deletions fuzz/fuzz_targets/fuzz_oh.rs
Original file line number Diff line number Diff line change
@@ -1,112 +1,11 @@
#![no_main]
use arbitrary::Arbitrary;
use chrono::{DateTime, Datelike};
use libfuzzer_sys::{fuzz_target, Corpus};

use std::fmt::Debug;

use opening_hours::localization::{Coordinates, Localize};
use opening_hours::{Context, OpeningHours};

#[derive(Arbitrary, Clone, Debug)]
pub enum CompareWith {
Stringified,
Normalized,
}

#[derive(Arbitrary, Clone, Debug)]
pub enum Operation {
DoubleNormalize,
Compare(CompareWith),
}

#[derive(Arbitrary, Clone)]
pub struct Data {
date_secs: i64,
oh: String,
coords: Option<[i16; 2]>,
operation: Operation,
}

impl Data {
fn coords_float(&self) -> Option<[f64; 2]> {
self.coords.map(|coords| {
[
90.0 * coords[0] as f64 / (i16::MAX as f64 + 1.0),
180.0 * coords[1] as f64 / (i16::MAX as f64 + 1.0),
]
})
}
}

impl Debug for Data {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut debug = f.debug_struct("Data");

if let Some(date) = DateTime::from_timestamp(self.date_secs, 0) {
debug.field("date", &date.naive_utc());
}

debug.field("operation", &self.operation);
debug.field("oh", &self.oh);

if let Some(coords) = &self.coords_float() {
debug.field("coords", coords);
}

debug.finish()
}
}
use opening_hours::fuzzing::{run_fuzz_oh, Data};

fuzz_target!(|data: Data| -> Corpus {
if data.oh.contains('=') {
// The fuzzer spends way too much time building comments.
return Corpus::Reject;
}

let Some(date) = DateTime::from_timestamp(data.date_secs, 0) else {
return Corpus::Reject;
};

let date = date.naive_utc();

if date.year() < 1900 || date.year() > 9999 {
return Corpus::Reject;
if run_fuzz_oh(data) {
Corpus::Keep
} else {
Corpus::Reject
}

let Ok(oh_1) = data.oh.parse::<OpeningHours>() else {
return Corpus::Reject;
};

match &data.operation {
Operation::DoubleNormalize => {
let normalized = oh_1.normalize();
assert_eq!(normalized, normalized.clone().normalize());
}
Operation::Compare(compare_with) => {
let oh_2 = match compare_with {
CompareWith::Normalized => oh_1.normalize(),
CompareWith::Stringified => oh_1.to_string().parse().unwrap_or_else(|err| {
eprintln!("[ERR] Initial Expression: {}", data.oh);
eprintln!("[ERR] Invalid stringified Expression: {oh_1}");
panic!("{err}")
}),
};

if let Some([lat, lon]) = data.coords_float() {
let ctx = Context::from_coords(Coordinates::new(lat, lon).unwrap());
let date = ctx.locale.datetime(date);
let oh_1 = oh_1.with_context(ctx.clone());
let oh_2 = oh_2.with_context(ctx.clone());

assert_eq!(oh_1.state(date), oh_2.state(date));
assert_eq!(oh_1.next_change(date), oh_2.next_change(date));
} else {
assert_eq!(oh_1.state(date), oh_2.state(date));
assert_eq!(oh_1.next_change(date), oh_2.next_change(date));
}
}
}

Corpus::Keep
});
4 changes: 3 additions & 1 deletion opening-hours/src/filter/date_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,9 @@ impl DateFilter for ds::WeekRange {
L: Localize,
{
let week = date.iso_week().week() as u8;
self.range.wrapping_contains(&week) && (week - self.range.start()) % self.step == 0
self.range.wrapping_contains(&week)
// TODO: what happens when week < range.start ?
&& week.saturating_sub(*self.range.start()) % self.step == 0
}

fn next_change_hint<L>(&self, date: NaiveDate, _ctx: &Context<L>) -> Option<NaiveDate>
Expand Down
110 changes: 110 additions & 0 deletions opening-hours/src/fuzzing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use arbitrary::Arbitrary;
use chrono::{DateTime, Datelike};

use std::fmt::Debug;

use crate::localization::{Coordinates, Localize};
use crate::{Context, OpeningHours};

#[derive(Arbitrary, Clone, Debug)]
pub enum CompareWith {
Stringified,
Normalized,
}

#[derive(Arbitrary, Clone, Debug)]
pub enum Operation {
DoubleNormalize,
Compare(CompareWith),
}

#[derive(Arbitrary, Clone)]
pub struct Data {
date_secs: i64,
oh: String,
coords: Option<[i16; 2]>,
operation: Operation,
}

impl Data {
fn coords_float(&self) -> Option<[f64; 2]> {
self.coords.map(|coords| {
[
90.0 * coords[0] as f64 / (i16::MAX as f64 + 1.0),
180.0 * coords[1] as f64 / (i16::MAX as f64 + 1.0),
]
})
}
}

impl Debug for Data {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut debug = f.debug_struct("Data");

if let Some(date) = DateTime::from_timestamp(self.date_secs, 0) {
debug.field("date", &date.naive_utc());
}

debug.field("operation", &self.operation);
debug.field("oh", &self.oh);

if let Some(coords) = &self.coords_float() {
debug.field("coords", coords);
}

debug.finish()
}
}

pub fn run_fuzz_oh(data: Data) -> bool {
if data.oh.contains('=') {
// The fuzzer spends way too much time building comments.
return false;
}

let Some(date) = DateTime::from_timestamp(data.date_secs, 0) else {
return false;
};

let date = date.naive_utc();

if date.year() < 1900 || date.year() > 9999 {
return false;
}

let Ok(oh_1) = data.oh.parse::<OpeningHours>() else {
return false;
};

match &data.operation {
Operation::DoubleNormalize => {
let normalized = oh_1.normalize();
assert_eq!(normalized, normalized.clone().normalize());
}
Operation::Compare(compare_with) => {
let oh_2 = match compare_with {
CompareWith::Normalized => oh_1.normalize(),
CompareWith::Stringified => oh_1.to_string().parse().unwrap_or_else(|err| {
eprintln!("[ERR] Initial Expression: {}", data.oh);
eprintln!("[ERR] Invalid stringified Expression: {oh_1}");
panic!("{err}")
}),
};

if let Some([lat, lon]) = data.coords_float() {
let ctx = Context::from_coords(Coordinates::new(lat, lon).unwrap());
let date = ctx.locale.datetime(date);
let oh_1 = oh_1.with_context(ctx.clone());
let oh_2 = oh_2.with_context(ctx.clone());

assert_eq!(oh_1.state(date), oh_2.state(date));
assert_eq!(oh_1.next_change(date), oh_2.next_change(date));
} else {
assert_eq!(oh_1.state(date), oh_2.state(date));
assert_eq!(oh_1.next_change(date), oh_2.next_change(date));
}
}
}

true
}
3 changes: 3 additions & 0 deletions opening-hours/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
pub mod schedule;
pub mod localization;

#[cfg(feature = "fuzzing")]
pub mod fuzzing;

mod context;
mod filter;
mod opening_hours;
Expand Down
29 changes: 29 additions & 0 deletions opening-hours/src/tests/fuzzing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use std::fs::File;
use std::io::Read;
use std::path::Path;

use arbitrary::{Arbitrary, Unstructured};

use crate::fuzzing::{run_fuzz_oh, Data};

#[test]
fn run_fuzz_corpus() {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("fuzz")
.join("corpus")
.join("fuzz_oh");

let dir = std::fs::read_dir(path).expect("could not open corpus directory");

for entry in dir {
let entry = entry.expect("failed to iter corpus directory");
eprintln!("Running fuzz corpus file {:?}", entry.path());
let mut file = File::open(entry.path()).expect("failed to open corpus example");
let mut bytes = Vec::new();
file.read_to_end(&mut bytes).expect("failed to read file");
let data = Data::arbitrary(&mut Unstructured::new(&bytes)).expect("could not parse corpus");
eprintln!("Input: {data:?}");
let output = run_fuzz_oh(data);
eprintln!("Output: {output:?}");
}
}
3 changes: 3 additions & 0 deletions opening-hours/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ mod week_selector;
mod weekday_selector;
mod year_selector;

#[cfg(feature = "fuzzing")]
mod fuzzing;

use criterion::black_box;
use std::sync::{Arc, Condvar, Mutex};
use std::thread;
Expand Down
8 changes: 8 additions & 0 deletions opening-hours/src/tests/week_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,11 @@ fn last_year_week() -> Result<(), Error> {
);
Ok(())
}

#[test]
fn outside_wrapping_range() -> Result<(), Error> {
let oh = OpeningHours::parse("2030 week52-01")?;
assert!(oh.next_change(datetime!("2024-06-01 12:00")).is_some());
assert!(oh.is_closed(datetime!("2024-06-01 12:00")));
Ok(())
}

0 comments on commit ff734c7

Please sign in to comment.