diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5389cafa..e0938bc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -230,3 +230,28 @@ jobs: env: RUSTDOCFLAGS: -D warnings run: cargo doc --no-deps --document-private-items --workspace + + fuzz_testing: + name: Compile Fuzz Test Targets + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install required packages (Ubuntu) + run: | + sudo apt-get update + sudo apt-get install g++ --yes + + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + + - name: Install Fuzzer + run: cargo install cargo-fuzz + working-directory: fuzz + + - name: Verify fuzz targets build + run: cargo check + working-directory: fuzz diff --git a/Cargo.lock b/Cargo.lock index 2feff124..fac37ce8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "bitflags" version = "2.9.1" @@ -85,6 +94,17 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -126,6 +146,7 @@ name = "globset" version = "0.4.16" dependencies = [ "aho-corasick", + "arbitrary", "bstr", "glob", "log", diff --git a/Cargo.toml b/Cargo.toml index f956a8e6..46fdf31a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ exclude = [ "/pkg/brew", "/benchsuite/", "/scripts/", + "/crates/fuzz", ] build = "build.rs" autotests = false diff --git a/ci/ubuntu-install-packages b/ci/ubuntu-install-packages index 5e7202c8..571ca747 100755 --- a/ci/ubuntu-install-packages +++ b/ci/ubuntu-install-packages @@ -11,4 +11,4 @@ if ! command -V sudo; then fi sudo apt-get update sudo apt-get install -y --no-install-recommends \ - zsh xz-utils liblz4-tool musl-tools brotli zstd + zsh xz-utils liblz4-tool musl-tools brotli zstd g++ diff --git a/crates/globset/Cargo.toml b/crates/globset/Cargo.toml index a3fd689c..afcdb4df 100644 --- a/crates/globset/Cargo.toml +++ b/crates/globset/Cargo.toml @@ -21,6 +21,7 @@ bench = false [dependencies] aho-corasick = "1.1.1" +arbitrary = { version = "1.3.2", optional = true, features = ["derive"] } bstr = { version = "1.6.2", default-features = false, features = ["std"] } log = { version = "0.4.20", optional = true } serde = { version = "1.0.188", optional = true } @@ -41,6 +42,7 @@ serde_json = "1.0.107" [features] default = ["log"] +arbitrary = ["dep:arbitrary"] # DEPRECATED. It is a no-op. SIMD is done automatically through runtime # dispatch. simd-accel = [] diff --git a/crates/globset/src/glob.rs b/crates/globset/src/glob.rs index c25e3f22..fdd6d16c 100644 --- a/crates/globset/src/glob.rs +++ b/crates/globset/src/glob.rs @@ -72,6 +72,7 @@ impl MatchStrategy { /// It cannot be used directly to match file paths, but it can be converted /// to a regular expression string or a matcher. #[derive(Clone, Debug, Eq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct Glob { glob: String, re: String, @@ -194,6 +195,7 @@ pub struct GlobBuilder<'a> { } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] struct GlobOptions { /// Whether to match case insensitively. case_insensitive: bool, @@ -220,6 +222,7 @@ impl GlobOptions { } #[derive(Clone, Debug, Default, Eq, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] struct Tokens(Vec); impl std::ops::Deref for Tokens { @@ -236,6 +239,7 @@ impl std::ops::DerefMut for Tokens { } #[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] enum Token { Literal(char), Any, diff --git a/crates/globset/src/lib.rs b/crates/globset/src/lib.rs index e804e12a..4531ac28 100644 --- a/crates/globset/src/lib.rs +++ b/crates/globset/src/lib.rs @@ -94,6 +94,19 @@ Standard Unix-style glob syntax is supported: A `GlobBuilder` can be used to prevent wildcards from matching path separators, or to enable case insensitive matching. + +# Crate Features + +This crate includes optional features that can be enabled if necessary. +These features are not required but may be useful depending on the use case. + +The following features are available: + +* **arbitrary** - + Enabling this feature introduces a public dependency on the + [`arbitrary`](https://crates.io/crates/arbitrary) + crate. Namely, it implements the `Arbitrary` trait from that crate for the + [`Glob`] type. This feature is disabled by default. */ #![deny(missing_docs)] diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 00000000..1a45eee7 --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 00000000..4f833f3d --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,188 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "bstr" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fuzz" +version = "0.0.1" +dependencies = [ + "globset", + "libfuzzer-sys", +] + +[[package]] +name = "globset" +version = "0.4.16" +dependencies = [ + "aho-corasick", + "arbitrary", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "jobserver" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +dependencies = [ + "libc", +] + +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-automata" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "serde" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 00000000..ed71db5d --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "fuzz" +version = "0.0.1" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +globset = { path = "../crates/globset", features = ["arbitrary"] } + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "fuzz_glob" +path = "fuzz_targets/fuzz_glob.rs" +test = false +doc = false diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 00000000..583e88d0 --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,52 @@ +# Fuzz Testing + +## Introduction + +Fuzz testing produces pseudo-random / arbitrary data that is used to find +stability issues within a code base. While Rust provides a strong type system, +this does not guarantee that an object will convert properly from one struct +to another. It is the responsibility of the developer to ensure that a struct +is converted properly. Fuzz testing will generate input within the domain of +each property. This arbitrary data can then be used to convert from ObjectA +to ObjectB and then back. This type of testing will help catch bugs that the +type system is not able to see. + +## Installation + +This crate relies on the `cargo-fuzz` component. To install this component, +run the following from the `fuzz` directory: + +```bash +cargo install cargo-fuzz +``` + +## Listing Targets + +Once installed, fuzz targets can be listed by running the following command: + +```bash +cargo fuzz list +``` + +This command will print out a list of all targets that can be tested. + +## Running Fuzz Tests + +To run a fuzz test, the target must be specified: + +```bash +cargo fuzz run +``` + +Note that the above will run the fuzz test indefinitely. Use the +`-max_total_time=` flag to specify how many seconds the test +should run for: + +```bash +cargo fuzz run -- -max_total_time=5 +``` + +The above command will run the fuzz test for five seconds. If the test +completes without error it will show how many tests were run successfully. +The test will abort and return a non-zero error code if it is able to produce +an error. The arbitrary input will be displayed in the event of a failure. diff --git a/fuzz/fuzz_targets/fuzz_glob.rs b/fuzz/fuzz_targets/fuzz_glob.rs new file mode 100644 index 00000000..900b0cd3 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_glob.rs @@ -0,0 +1,22 @@ +#![no_main] + +use std::str::FromStr; + +use globset::Glob; + +libfuzzer_sys::fuzz_target!(|glob_str: &str| { + let Ok(glob) = Glob::new(glob_str) else { + return; + }; + + let Ok(glob2) = Glob::from_str(glob_str) else { + return; + }; + + // Verify that a `Glob` constructed with `new` is the same as a `Glob`` constructed + // with `from_str`. + assert_eq!(glob, glob2); + + // Verify that `Glob::glob` produces the same string as the original. + assert_eq!(glob.glob(), glob_str); +});