diff --git a/.github/actions/build_and_test/action.yml b/.github/actions/build_and_test/action.yml new file mode 100644 index 0000000..8dcbc12 --- /dev/null +++ b/.github/actions/build_and_test/action.yml @@ -0,0 +1,28 @@ +name: Build and Test +description: Run build and test steps for the project + +runs: + using: composite + steps: + - name: Install Toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: "1.88" + components: rustfmt + + - name: Cargo Format + uses: actions-rs/cargo@v1 + with: + command: fmt + args: -- --check + + - name: Build + shell: bash + run: cargo build + + - name: Test + uses: actions-rs/cargo@v1 + with: + command: test + args: -- --test-threads=1 + diff --git a/.github/actions/fuzz_tests/action.yml b/.github/actions/fuzz_tests/action.yml new file mode 100644 index 0000000..95c7b09 --- /dev/null +++ b/.github/actions/fuzz_tests/action.yml @@ -0,0 +1,30 @@ +name: Smoke-Test Fuzz Targets +description: Run fuzz tests on the project + +inputs: + fuzz_target: + description: 'The fuzz target to run' + required: true + fuzz_time: + description: 'Maximum time in seconds to run fuzzing' + required: false + default: '180' + cargo_fuzz_version: + description: 'Version of cargo-fuzz to install' + required: false + default: '0.13.0' + +runs: + using: composite + steps: + - name: Install cargo-fuzz + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-fuzz --version ${{ inputs.cargo_fuzz_version }} + + - name: Run Fuzz Tests + shell: bash + working-directory: fuzz + run: cargo fuzz run ${{ inputs.fuzz_target }} --release -- -max_total_time=${{ inputs.fuzz_time }} + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f690705 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: [pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build_and_test: + name: Build and Test + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run Build and Test + uses: ./.github/actions/build_and_test + + fuzz_tests: + name: Fuzz Tests (${{ matrix.fuzz_target }}) + runs-on: ubuntu-latest + strategy: + matrix: + fuzz_target: + - fuzz_json_de + # Add more fuzz targets here as needed + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run Fuzz Tests + uses: ./.github/actions/fuzz_tests + with: + fuzz_target: ${{ matrix.fuzz_target }} + diff --git a/.github/workflows/toolchain.yml b/.github/workflows/toolchain.yml deleted file mode 100644 index 43690e9..0000000 --- a/.github/workflows/toolchain.yml +++ /dev/null @@ -1,175 +0,0 @@ -on: [push, pull_request] - -name: CI - -jobs: - check: - name: Check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - uses: actions-rs/cargo@v1 - with: - command: check - args: --all-features - - fmt: - name: Rustfmt - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - run: rustup component add rustfmt - - uses: actions-rs/cargo@v1 - with: - command: fmt - args: -- --check - - clippy: - name: Clippy - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - run: rustup component add clippy - - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --all-features -- -D warnings - - test: - name: Test - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - uses: actions-rs/cargo@v1 - with: - command: test - args: -- --test-threads=1 - - test_thread_unsafe: - name: Test unsafe thread - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - uses: actions-rs/cargo@v1 - with: - command: test - args: --no-default-features -- --test-threads=1 - - test_miri: - name: Test (Miri) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: nightly - override: true - components: miri - - uses: actions-rs/cargo@v1 - with: - command: miri - args: test - env: - MIRIFLAGS: "-Zmiri-disable-isolation" - - test_miri_32bit_linux: - name: Test (Miri i686-unknown-linux-gnu) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: nightly - override: true - components: miri - target: i686-unknown-linux-gnu - - uses: actions-rs/cargo@v1 - with: - command: miri - args: test --target i686-unknown-linux-gnu - env: - MIRIFLAGS: "-Zmiri-disable-isolation" - - test_miri_32bit_windows: - name: Test (Miri i686-pc-windows-msvc) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: nightly - override: true - components: miri - target: i686-pc-windows-msvc - - uses: actions-rs/cargo@v1 - with: - command: miri - args: test --target i686-pc-windows-msvc - env: - MIRIFLAGS: "-Zmiri-disable-isolation" - - test_nightly: - name: Test (Nightly) - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: nightly - override: true - components: llvm-tools-preview - - uses: actions-rs/cargo@v1 - with: - command: install - args: cargo-binutils rustfilt - - name: Build tests - run: | - cargo rustc --message-format=json --tests -- -Zinstrument-coverage | jq -r '.executable | strings' > executables.txt - - name: Run tests - run: | - while read p; do - exec "$p" --test-threads=1 - done lcov.info - - name: Upload coverage report - uses: codecov/codecov-action@v1 - with: - fail_ci_if_error: true - - uses: actions/upload-artifact@v2 - with: - name: Upload coverage artifact - path: lcov.info diff --git a/Cargo.toml b/Cargo.toml index 4c93328..9332c82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = ["fuzz"] + [package] name = "ijson" version = "0.1.3" @@ -14,12 +17,17 @@ default = [] tracing = ["mockalloc/tracing"] thread_safe = [] +[workspace.dependencies] +serde = "1.0.117" +serde_json = "1.0.59" + + [dependencies] hashbrown = "0.13.2" dashmap = { version = "5.4", features = ["raw-api"] } lazy_static = "1.4.0" -serde = "1.0.117" -serde_json = "1.0.59" +serde = { workspace = true } +serde_json = { workspace = true } ctor = { version = "0.1.16", optional = true } paste = "1.0.15" half = "2.0.0" diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..1a45eee --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..09cd0b9 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "ijson-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +arbitrary = { version = "1.3", features = ["derive"] } +serde = { workspace = true } +serde_json = { workspace = true } + +[dependencies.ijson] +path = ".." + +[[bin]] +name = "fuzz_json_de" +path = "fuzz_targets/fuzz_json_de.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/fuzz_json_de.rs b/fuzz/fuzz_targets/fuzz_json_de.rs new file mode 100644 index 0000000..1592e59 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_json_de.rs @@ -0,0 +1,54 @@ +#![no_main] + +use arbitrary::Arbitrary; +use ijson::IValue; +use libfuzzer_sys::fuzz_target; +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Arbitrary, Debug)] +enum JsonValue { + Null, + Bool(bool), + Number(f64), + String(String), + Array(Vec), + Object(HashMap), +} + +impl JsonValue { + fn to_json_string(&self) -> String { + match self { + JsonValue::Null => "null".to_string(), + JsonValue::Bool(b) => b.to_string(), + JsonValue::Number(n) => { + if n.is_finite() { + n.to_string() + } else { + "0".to_string() + } + } + JsonValue::String(s) => format!("\"{}\"", s), + JsonValue::Array(arr) => { + let items: Vec = arr.iter().map(|v| v.to_json_string()).collect(); + format!("[{}]", items.join(",")) + } + JsonValue::Object(obj) => { + let items: Vec = obj + .iter() + .map(|(k, v)| { + let key = k.clone(); + format!("\"{}\":{}", key, v.to_json_string()) + }) + .collect(); + format!("{{{}}}", items.join(",")) + } + } + } +} + +fuzz_target!(|value: JsonValue| { + let json_string = value.to_json_string(); + let mut deserializer = serde_json::Deserializer::from_str(&json_string); + let _ = IValue::deserialize(&mut deserializer); +}); diff --git a/fuzz/rust-toolchain.toml b/fuzz/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/fuzz/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly"