diff --git a/Cargo.toml b/Cargo.toml index c3ff116..64df0b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,3 +56,6 @@ cat-crypto = [ "windows/Win32_Security_Cryptography", "windows/Win32_Foundation", ] + +[dev-dependencies] +rand = "0.8" diff --git a/README.md b/README.md index 6da6f44..c369230 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,50 @@ BusyWork::new(Intensity::Ultra) .run(); ``` -Drop it in any loop — every iteration looks different: +### Data Flow Integration + +Busywork blocks can blend into surrounding code by processing **your variables**. Feed any data from the current scope — tasks will weave it into their control flow (hashing it, sorting it, using it as loop bounds), making the block indistinguishable from real data processing to static or dynamic analysis. + +All data is **cloned on feed** — originals are never modified. + +```rust +let session_id: u64 = 0xDEADBEEF; +let payload = vec![0x41u8; 1024]; +let user_name = "admin"; +let packet_count: u32 = 147; + +BusyWork::new(Intensity::Medium) + .allow(Categories::COMPUTE | Categories::MEMORY) + .feed(&session_id) // integers, floats, bools + .feed(&payload) // Vec, &[u8], [u8; N] + .feed(user_name) // &str, String + .feed(&packet_count) // unlimited parameters + .run(); +``` + +Without `.feed()`, tasks generate random input data — detectable as isolated noise blocks with no data dependency on the surrounding program. With `.feed()`, the busywork reads and processes the same variables as your real code. A data-flow analyzer cannot determine which computations are "real" and which are busywork. + +**Works with custom types** — implement `FeedWork` for your own structs: + +```rust +use busywork::FeedWork; + +struct GameState { x: f32, y: f32, hp: u32 } + +impl FeedWork for GameState { + fn write_work_bytes(&self, out: &mut Vec) { + self.x.write_work_bytes(out); + self.y.write_work_bytes(out); + self.hp.write_work_bytes(out); + } +} + +BusyWork::new(Intensity::Low) + .feed(&game_state) + .run(); +``` + +### Loop example ```rust loop { @@ -149,6 +192,7 @@ Every design decision optimizes for evasion: |-----------|----------------| | **Random task selection per call** | Behavioral sequence matching — no two calls produce the same API call order | | **±30% jitter on all parameters** | Timing heuristics — iteration counts, buffer sizes, call depths all vary | +| **Data flow integration via `.feed()`** | Data-flow analysis — busywork operates on the same variables as surrounding code, preventing isolation as "noise blocks" | | **Task registry rebuilt per call** | Static analysis — no stable global function pointer table to fingerprint | | **Function pointers, not trait objects** | Vtable signature detection — each task is a direct call to a unique address | | **`black_box` on all results** | Dead-code elimination — compiler can't optimize away the work | @@ -170,10 +214,34 @@ Call 4: FindFirstFile(*.dll) × 40 → GetVolumeInformation → memcpy(16KB) × No repeating pattern. Each call exercises different subsystems, different buffer sizes, different iteration counts. To an EDR, it looks like a normal application doing normal things. +With `.feed()`, the data flowing through these operations comes from your actual program variables — hash chains process your session tokens, sorts operate on your counters, crypto operations encrypt your payloads. A taint-tracking analyzer following your variables will see them consumed by what appears to be legitimate processing. + +## Testing + +196 tests across 4 test suites: + +| Suite | Tests | What it covers | +|-------|-------|----------------| +| `workdata::tests` | 52 | Every `FeedWork` type, `blend_into` XOR correctness, `blend_seed`/`derive_usize` determinism, clone independence, empty/zero edge cases | +| `tasks::tests` | 18 | Every registered task function called directly with 10 work-data shapes × parameter extremes (zero, min, large). Registry completeness (76 tasks, no duplicates) | +| `tasks::compute::tests` | 19 | Mirror tests proving work data changes computation: hash inputs differ, fibonacci starting values shift, sieve limits get bias, cipher keys incorporate fed data, distinct inputs produce distinct seeds | +| `tasks::memory::tests` | 9 | Mirror tests proving work data flows into memory tasks: buffers modified, patterns XOR'd, ring buffers seeded, reversibility verified | +| `tests/smoke.rs` | 9 | Basic API smoke tests, category filtering, jitter toggle | +| `tests/bench.rs` | 4 | Intensity/category benchmarks, jitter variance measurement | +| `tests/feed.rs` | 62 | Data isolation (15 types verified untouched after run), per-category with feed, intensity levels, edge cases (NaN, Unicode, 64KB), builder combinations, type coverage | +| `tests/robustness.rs` | 23 | Stress (500 rapid calls, 10k small feeds, 1MB feed), custom `FeedWork` impl, regression patterns (empty→real, alternating, max values) | + +``` +cargo test --lib # 101 unit tests +cargo test --test feed # 62 integration tests +cargo test --test robustness # 23 stress/regression tests +``` + ## Stats ``` 76 tasks across 7 categories -2,688 lines of Rust +31 tasks actively consume fed work data (COMPUTE, MEMORY, CRYPTO) +196 tests across unit, integration, stress, and mirror test suites 0 time objects in the library binary ``` diff --git a/src/builder.rs b/src/builder.rs index 0322c11..eccfdbd 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,12 +1,14 @@ use crate::categories::Categories; use crate::dispatch; use crate::intensity::Intensity; +use crate::workdata::{FeedWork, WorkData}; pub struct BusyWork { intensity: Intensity, allow: Categories, deny: Categories, jitter: bool, + work_data: WorkData, } impl BusyWork { @@ -16,6 +18,7 @@ impl BusyWork { allow: Categories::all(), deny: Categories::empty(), jitter: true, + work_data: WorkData::new(), } } @@ -34,8 +37,17 @@ impl BusyWork { self } + /// Feed a variable from the surrounding code into the busywork block. + /// The value is cloned immediately — the original is never touched. + /// Tasks will weave this data into their control flow, making the block + /// indistinguishable from real data processing. + pub fn feed(mut self, data: &(impl FeedWork + ?Sized)) -> Self { + self.work_data.feed(data); + self + } + pub fn run(&self) { let effective = (self.allow & Categories::available()) & !self.deny; - dispatch::execute(self.intensity, effective, self.jitter); + dispatch::execute(self.intensity, effective, self.jitter, &self.work_data); } } diff --git a/src/dispatch.rs b/src/dispatch.rs index d3fa4f4..f5cd64e 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -2,9 +2,10 @@ use crate::categories::Categories; use crate::intensity::Intensity; use crate::jitter; use crate::tasks::{self, TaskParams}; +use crate::workdata::WorkData; use rand::seq::SliceRandom; -pub fn execute(intensity: Intensity, effective: Categories, jitter_enabled: bool) { +pub fn execute(intensity: Intensity, effective: Categories, jitter_enabled: bool, work_data: &WorkData) { let mut rng = rand::thread_rng(); let all = tasks::all_tasks(); let eligible: Vec<_> = all @@ -42,6 +43,6 @@ pub fn execute(intensity: Intensity, effective: Categories, jitter_enabled: bool base.call_depth }, }; - (task.func)(¶ms, &mut rng); + (task.func)(¶ms, &mut rng, work_data); } } diff --git a/src/lib.rs b/src/lib.rs index e255b2e..5718d9a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,10 +4,12 @@ mod dispatch; mod intensity; mod jitter; mod tasks; +mod workdata; pub use builder::BusyWork; pub use categories::Categories; pub use intensity::Intensity; +pub use workdata::{FeedWork, WorkData}; pub fn busywork(intensity: Intensity) { BusyWork::new(intensity).run(); diff --git a/src/tasks/compute.rs b/src/tasks/compute.rs index b7d0ff1..fc1a007 100644 --- a/src/tasks/compute.rs +++ b/src/tasks/compute.rs @@ -1,5 +1,6 @@ use crate::categories::Categories; use crate::tasks::{TaskDescriptor, TaskParams}; +use crate::workdata::WorkData; use flate2::write::{DeflateDecoder, DeflateEncoder}; use flate2::Compression; use md5::Md5; @@ -84,9 +85,10 @@ pub fn register() -> Vec { ] } -fn hash_sha256_loop(params: &TaskParams, rng: &mut ThreadRng) { +fn hash_sha256_loop(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let mut data = vec![0u8; 64]; rng.fill_bytes(&mut data); + work.blend_into(&mut data); for _ in 0..params.iterations { let mut hasher = Sha256::new(); hasher.update(&data); @@ -96,9 +98,10 @@ fn hash_sha256_loop(params: &TaskParams, rng: &mut ThreadRng) { black_box(&data); } -fn hash_md5_loop(params: &TaskParams, rng: &mut ThreadRng) { +fn hash_md5_loop(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let mut data = vec![0u8; 64]; rng.fill_bytes(&mut data); + work.blend_into(&mut data); for _ in 0..params.iterations { let mut hasher = Md5::new(); hasher.update(&data); @@ -108,8 +111,13 @@ fn hash_md5_loop(params: &TaskParams, rng: &mut ThreadRng) { black_box(&data); } -fn prime_sieve(params: &TaskParams, _rng: &mut ThreadRng) { - let limit = params.iterations.saturating_mul(100).min(10_000_000); +fn prime_sieve(params: &TaskParams, _rng: &mut ThreadRng, work: &WorkData) { + let work_bias = work.derive_usize(0) % 1000; + let limit = params + .iterations + .saturating_mul(100) + .saturating_add(work_bias) + .min(10_000_000); if limit < 2 { return; } @@ -130,7 +138,7 @@ fn prime_sieve(params: &TaskParams, _rng: &mut ThreadRng) { black_box(count); } -fn matrix_multiply(params: &TaskParams, rng: &mut ThreadRng) { +fn matrix_multiply(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let n = ((params.iterations as f64).sqrt() as usize).max(2).min(512); let mut a = vec![0.0f64; n * n]; let mut b = vec![0.0f64; n * n]; @@ -140,6 +148,12 @@ fn matrix_multiply(params: &TaskParams, rng: &mut ThreadRng) { for x in b.iter_mut() { *x = rng.gen::(); } + if !work.is_empty() { + let wb = work.as_bytes(); + for (i, x) in a.iter_mut().enumerate() { + *x += (wb[i % wb.len()] as f64) / 256.0; + } + } let mut c = vec![0.0f64; n * n]; for i in 0..n { for k in 0..n { @@ -152,19 +166,23 @@ fn matrix_multiply(params: &TaskParams, rng: &mut ThreadRng) { black_box(&c); } -fn sort_random_arrays(params: &TaskParams, rng: &mut ThreadRng) { +fn sort_random_arrays(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let size = (params.buffer_size / 8).max(1); - for _ in 0..params.call_depth { - let mut data: Vec = (0..size).map(|_| rng.gen()).collect(); + let seed = work.blend_seed(); + for round in 0..params.call_depth { + let mut data: Vec = (0..size) + .map(|_| rng.gen::() ^ seed.wrapping_add(round as u64)) + .collect(); data.sort_unstable(); black_box(&data); } } -fn compress_decompress(params: &TaskParams, rng: &mut ThreadRng) { +fn compress_decompress(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let size = params.buffer_size.min(65536); let mut data = vec![0u8; size]; rng.fill_bytes(&mut data); + work.blend_into(&mut data); for _ in 0..params.call_depth { let mut encoder = DeflateEncoder::new(Vec::new(), Compression::fast()); if encoder.write_all(&data).is_err() { @@ -186,9 +204,10 @@ fn compress_decompress(params: &TaskParams, rng: &mut ThreadRng) { } } -fn fibonacci_sequence(params: &TaskParams, _rng: &mut ThreadRng) { - let mut a: u128 = 0; - let mut b: u128 = 1; +fn fibonacci_sequence(params: &TaskParams, _rng: &mut ThreadRng, work: &WorkData) { + let seed = work.blend_seed(); + let mut a: u128 = seed as u128; + let mut b: u128 = 1u128.wrapping_add(seed as u128); for _ in 0..params.iterations { let next = a.wrapping_add(b); a = b; @@ -197,9 +216,10 @@ fn fibonacci_sequence(params: &TaskParams, _rng: &mut ThreadRng) { black_box(b); } -fn xor_cipher(params: &TaskParams, rng: &mut ThreadRng) { +fn xor_cipher(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let mut key = [0u8; 256]; rng.fill_bytes(&mut key); + work.blend_into(&mut key); let size = params.buffer_size.min(1_048_576); for _ in 0..params.call_depth { let mut data = vec![0u8; size]; @@ -211,10 +231,11 @@ fn xor_cipher(params: &TaskParams, rng: &mut ThreadRng) { } } -fn collatz_sequence(params: &TaskParams, rng: &mut ThreadRng) { +fn collatz_sequence(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let iteration_limit = 10_000usize; + let seed = work.blend_seed(); for _ in 0..params.iterations { - let mut n: u64 = rng.gen::().saturating_add(1); + let mut n: u64 = rng.gen::().saturating_add(1) ^ seed; let mut steps = 0usize; while n != 1 && steps < iteration_limit { if n % 2 == 0 { @@ -228,12 +249,18 @@ fn collatz_sequence(params: &TaskParams, rng: &mut ThreadRng) { } } -fn string_operations(params: &TaskParams, rng: &mut ThreadRng) { +fn string_operations(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let size = params.buffer_size.min(65536).max(1); + let wb = work.as_bytes(); for _ in 0..params.call_depth { let s: String = (0..size) - .map(|_| { - let c = rng.gen_range(32u8..127u8); + .map(|i| { + let base = rng.gen_range(32u8..127u8); + let c = if !wb.is_empty() { + ((base as u16 + wb[i % wb.len()] as u16) % 95 + 32) as u8 + } else { + base + }; c as char }) .collect(); @@ -255,10 +282,11 @@ fn string_operations(params: &TaskParams, rng: &mut ThreadRng) { } } -fn bubble_sort(params: &TaskParams, rng: &mut ThreadRng) { +fn bubble_sort(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let size = (params.buffer_size / 4).min(4096).max(1); + let seed = work.blend_seed() as u32; for _ in 0..params.call_depth { - let mut data: Vec = (0..size).map(|_| rng.gen()).collect(); + let mut data: Vec = (0..size).map(|_| rng.gen::() ^ seed).collect(); let n = data.len(); for i in 0..n { for j in 0..n.saturating_sub(i + 1) { @@ -271,11 +299,11 @@ fn bubble_sort(params: &TaskParams, rng: &mut ThreadRng) { } } -fn bitwise_operations(params: &TaskParams, rng: &mut ThreadRng) { - let mut accum: u64 = rng.gen(); +fn bitwise_operations(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { + let mut accum: u64 = rng.gen::() ^ work.blend_seed(); for _ in 0..params.iterations { let val: u64 = rng.gen(); - let shift = (val & 0x3F) as u32; // 0..63 + let shift = (val & 0x3F) as u32; accum ^= val.wrapping_shl(shift); accum ^= val.wrapping_shr(shift); accum = accum.rotate_left(shift); @@ -288,9 +316,11 @@ fn bitwise_operations(params: &TaskParams, rng: &mut ThreadRng) { black_box(accum); } -fn pi_approximation(params: &TaskParams, _rng: &mut ThreadRng) { +fn pi_approximation(params: &TaskParams, _rng: &mut ThreadRng, work: &WorkData) { + let extra = work.derive_usize(0) % 100; + let total = params.iterations.saturating_add(extra); let mut sum: f64 = 0.0; - for k in 0..params.iterations { + for k in 0..total { let sign = if k % 2 == 0 { 1.0f64 } else { -1.0f64 }; sum += sign / (2 * k + 1) as f64; } @@ -298,10 +328,10 @@ fn pi_approximation(params: &TaskParams, _rng: &mut ThreadRng) { black_box(pi); } -fn permutation_generate(params: &TaskParams, _rng: &mut ThreadRng) { - let n = params.call_depth.min(10).max(1); +fn permutation_generate(params: &TaskParams, _rng: &mut ThreadRng, work: &WorkData) { + let work_extra = work.derive_usize(0) % 3; + let n = params.call_depth.saturating_add(work_extra).min(10).max(1); let mut arr: Vec = (0..n).collect(); - // Heap's algorithm (iterative) let mut c = vec![0usize; n]; black_box(&arr); let mut i = 0; @@ -321,3 +351,211 @@ fn permutation_generate(params: &TaskParams, _rng: &mut ThreadRng) { } } } + +#[cfg(test)] +mod tests { + use crate::workdata::WorkData; + + // ── Proof that work data changes each task's computation ─────────── + // These mirror the exact integration logic in each task and verify + // that feeding data produces different internal values. If someone + // accidentally removes a blend_into / blend_seed call, these fail. + + #[test] + fn hash_sha256_input_differs_with_work() { + let buf_no_work = vec![0u8; 64]; + let mut buf_with_work = vec![0u8; 64]; + + let mut work = WorkData::new(); + work.feed(&0xDEADBEEFu32); + work.blend_into(&mut buf_with_work); + + assert_ne!(buf_no_work, buf_with_work, "blend_into must change hash input"); + // Also verify it's not just the first 4 bytes (cyclic XOR spreads across buffer) + assert!(buf_with_work[4..].iter().any(|&b| b != 0), "XOR should cycle past work data length"); + } + + #[test] + fn hash_md5_input_differs_with_work() { + let mut buf = vec![0u8; 64]; + let mut work = WorkData::new(); + work.feed("session_token_12345"); + work.blend_into(&mut buf); + assert!(buf.iter().any(|&b| b != 0)); + } + + #[test] + fn fibonacci_starting_values_change() { + // fibonacci uses: a = seed as u128, b = 1 + seed as u128 + let mut work = WorkData::new(); + work.feed(&42u32); + let seed = work.blend_seed(); + assert_ne!(seed, 0, "non-zero input must produce non-zero seed"); + + let a = seed as u128; + let b = 1u128.wrapping_add(seed as u128); + assert_ne!((a, b), (0u128, 1u128), "starting values must differ from default"); + } + + #[test] + fn collatz_starting_number_changes() { + let mut work = WorkData::new(); + work.feed(&42u64); + let seed = work.blend_seed(); + assert_ne!(seed, 0); + // collatz XORs: n = rng_val ^ seed → different starting number + } + + #[test] + fn prime_sieve_limit_gets_bias() { + let mut work = WorkData::new(); + work.feed(&0xCAFEBABEu32); + let bias = work.derive_usize(0) % 1000; + assert!(bias > 0, "non-trivial data should produce non-zero sieve bias"); + + let base_limit = 50usize.saturating_mul(100); + let biased_limit = base_limit.saturating_add(bias); + assert!(biased_limit > base_limit); + } + + #[test] + fn matrix_entries_get_work_offset() { + let mut work = WorkData::new(); + work.feed(&[0x80u8; 16]); + let wb = work.as_bytes(); + // matrix_multiply adds (wb[i % len] as f64) / 256.0 to each entry + let offset = wb[0] as f64 / 256.0; + assert!(offset > 0.0, "work data must produce positive offset"); + assert!(offset < 1.0, "offset must stay fractional"); + } + + #[test] + fn sort_seed_affects_values() { + let mut work = WorkData::new(); + work.feed(&42u32); + let seed = work.blend_seed(); + assert_ne!(seed, 0); + // sort_random_arrays: rng.gen() ^ seed.wrapping_add(round) + // Non-zero seed changes every array element + } + + #[test] + fn compress_input_changes_with_work() { + let mut data = vec![0u8; 1024]; + let mut work = WorkData::new(); + work.feed(&[0xAB; 32]); + work.blend_into(&mut data); + assert!(data.iter().any(|&b| b != 0), "compressed data input must change"); + } + + #[test] + fn xor_cipher_key_modified() { + let mut key = [0u8; 256]; + let mut work = WorkData::new(); + work.feed(&0xFFu8); + work.blend_into(&mut key); + assert!(key.iter().any(|&b| b != 0), "cipher key must incorporate work data"); + } + + #[test] + fn string_ops_chars_change() { + let mut work = WorkData::new(); + work.feed(&0xFFu8); + let wb = work.as_bytes(); + let base: u8 = 65; // 'A' + let modified = ((base as u16 + wb[0] as u16) % 95 + 32) as u8; + assert_ne!(modified, base); + } + + #[test] + fn bubble_sort_seed_nonzero() { + let mut work = WorkData::new(); + work.feed(&42u32); + let seed = work.blend_seed() as u32; + assert_ne!(seed, 0); + } + + #[test] + fn bitwise_initial_accum_changes() { + let mut work = WorkData::new(); + work.feed(&42u32); + let seed = work.blend_seed(); + // bitwise_operations: accum = rng ^ seed → different accumulator start + assert_ne!(seed, 0); + } + + #[test] + fn pi_extra_iterations_added() { + let mut work = WorkData::new(); + work.feed(&42u32); + let extra = work.derive_usize(0) % 100; + let base_iterations = 50usize; + let total = base_iterations.saturating_add(extra); + assert!(total >= base_iterations, "total must be >= base"); + // With most non-trivial data, extra > 0 + } + + #[test] + fn permutation_size_changes() { + let mut work = WorkData::new(); + work.feed(&42u32); + let extra = work.derive_usize(0) % 3; + let base_depth = 2usize; + let n = base_depth.saturating_add(extra).min(10).max(1); + // n should be >= base_depth (since extra >= 0) and possibly larger + assert!(n >= base_depth); + } + + // ── blend_into reversibility (proves it's XOR, not corruption) ───── + + #[test] + fn blend_into_is_reversible_on_task_buffers() { + let mut work = WorkData::new(); + work.feed(&0xDEADBEEFCAFEBABEu64); + + let original = vec![42u8; 4096]; + let mut buf = original.clone(); + work.blend_into(&mut buf); + assert_ne!(buf, original, "first blend must change data"); + work.blend_into(&mut buf); + assert_eq!(buf, original, "double blend must restore original"); + } + + // ── Different work data → different seeds (no collision) ─────────── + + #[test] + fn different_data_different_seeds() { + let inputs: &[&[u8]] = &[ + b"alpha", b"bravo", b"charlie", b"delta", + b"echo", b"foxtrot", b"golf", b"hotel", + b"india", b"juliet", + ]; + let mut seeds = Vec::new(); + for input in inputs { + let mut w = WorkData::new(); + w.feed(*input); + seeds.push(w.blend_seed()); + } + let unique_count = { + let mut s = seeds.clone(); + s.sort(); + s.dedup(); + s.len() + }; + assert_eq!(unique_count, seeds.len(), "distinct multi-byte inputs must produce distinct seeds"); + } + + #[test] + fn different_data_different_derived_values() { + let inputs: &[&[u8]] = &[ + &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], + &[16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1], + ]; + let mut w1 = WorkData::new(); + w1.feed(inputs[0]); + let mut w2 = WorkData::new(); + w2.feed(inputs[1]); + + assert_ne!(w1.derive_usize(0), w2.derive_usize(0)); + } +} diff --git a/src/tasks/crypto.rs b/src/tasks/crypto.rs index 4a3ebb4..8e94358 100644 --- a/src/tasks/crypto.rs +++ b/src/tasks/crypto.rs @@ -1,5 +1,6 @@ use crate::categories::Categories; use crate::tasks::{TaskDescriptor, TaskParams}; +use crate::workdata::WorkData; use rand::rngs::ThreadRng; use rand::RngCore; use std::hint::black_box; @@ -45,7 +46,7 @@ pub fn register() -> Vec { ] } -fn bcrypt_gen_random(params: &TaskParams, _rng: &mut ThreadRng) { +fn bcrypt_gen_random(params: &TaskParams, _rng: &mut ThreadRng, work: &WorkData) { let size = params.buffer_size.min(65536); let mut buffer = vec![0u8; size]; for _ in 0..params.iterations.min(100) { @@ -56,14 +57,16 @@ fn bcrypt_gen_random(params: &TaskParams, _rng: &mut ThreadRng) { BCRYPT_USE_SYSTEM_PREFERRED_RNG, ); } + work.blend_into(&mut buffer); black_box(&buffer); } } -fn bcrypt_hash(params: &TaskParams, rng: &mut ThreadRng) { +fn bcrypt_hash(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let data_size = params.buffer_size.min(65536); let mut data = vec![0u8; data_size]; rng.fill_bytes(&mut data); + work.blend_into(&mut data); unsafe { let mut alg = BCRYPT_ALG_HANDLE::default(); @@ -86,7 +89,7 @@ fn bcrypt_hash(params: &TaskParams, rng: &mut ThreadRng) { let _ = BCryptHashData(hash_handle, &data, 0); - let mut output = vec![0u8; 32]; // SHA-256 output + let mut output = vec![0u8; 32]; let _ = BCryptFinishHash(hash_handle, &mut output, 0); black_box(&output); @@ -97,10 +100,11 @@ fn bcrypt_hash(params: &TaskParams, rng: &mut ThreadRng) { } } -fn bcrypt_sha512(params: &TaskParams, rng: &mut ThreadRng) { +fn bcrypt_sha512(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let data_size = params.buffer_size.min(65536); let mut data = vec![0u8; data_size]; rng.fill_bytes(&mut data); + work.blend_into(&mut data); unsafe { let mut alg = BCRYPT_ALG_HANDLE::default(); @@ -123,7 +127,7 @@ fn bcrypt_sha512(params: &TaskParams, rng: &mut ThreadRng) { let _ = BCryptHashData(hash_handle, &data, 0); - let mut output = vec![0u8; 64]; // SHA-512 output + let mut output = vec![0u8; 64]; let _ = BCryptFinishHash(hash_handle, &mut output, 0); black_box(&output); @@ -134,10 +138,11 @@ fn bcrypt_sha512(params: &TaskParams, rng: &mut ThreadRng) { } } -fn bcrypt_md5_hash(params: &TaskParams, rng: &mut ThreadRng) { +fn bcrypt_md5_hash(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let data_size = params.buffer_size.min(65536); let mut data = vec![0u8; data_size]; rng.fill_bytes(&mut data); + work.blend_into(&mut data); unsafe { let mut alg = BCRYPT_ALG_HANDLE::default(); @@ -160,7 +165,7 @@ fn bcrypt_md5_hash(params: &TaskParams, rng: &mut ThreadRng) { let _ = BCryptHashData(hash_handle, &data, 0); - let mut output = vec![0u8; 16]; // MD5 output + let mut output = vec![0u8; 16]; let _ = BCryptFinishHash(hash_handle, &mut output, 0); black_box(&output); @@ -171,10 +176,11 @@ fn bcrypt_md5_hash(params: &TaskParams, rng: &mut ThreadRng) { } } -fn bcrypt_sha1_hash(params: &TaskParams, rng: &mut ThreadRng) { +fn bcrypt_sha1_hash(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let data_size = params.buffer_size.min(65536); let mut data = vec![0u8; data_size]; rng.fill_bytes(&mut data); + work.blend_into(&mut data); unsafe { let mut alg = BCRYPT_ALG_HANDLE::default(); @@ -197,7 +203,7 @@ fn bcrypt_sha1_hash(params: &TaskParams, rng: &mut ThreadRng) { let _ = BCryptHashData(hash_handle, &data, 0); - let mut output = vec![0u8; 20]; // SHA-1 output + let mut output = vec![0u8; 20]; let _ = BCryptFinishHash(hash_handle, &mut output, 0); black_box(&output); @@ -208,12 +214,13 @@ fn bcrypt_sha1_hash(params: &TaskParams, rng: &mut ThreadRng) { } } -fn bcrypt_aes_encrypt(params: &TaskParams, rng: &mut ThreadRng) { +fn bcrypt_aes_encrypt(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let plaintext_size = params.buffer_size.min(4096); let mut plaintext = vec![0u8; plaintext_size]; rng.fill_bytes(&mut plaintext); + work.blend_into(&mut plaintext); - let mut key_bytes = [0u8; 32]; // AES-256 key + let mut key_bytes = [0u8; 32]; rng.fill_bytes(&mut key_bytes); unsafe { @@ -241,7 +248,6 @@ fn bcrypt_aes_encrypt(params: &TaskParams, rng: &mut ThreadRng) { continue; } - // Output buffer: plaintext + 16 bytes for AES block padding let mut output = vec![0u8; plaintext_size + 16]; let mut bytes_written: u32 = 0; @@ -265,7 +271,7 @@ fn bcrypt_aes_encrypt(params: &TaskParams, rng: &mut ThreadRng) { } } -fn bcrypt_rng_algorithms(params: &TaskParams, _rng: &mut ThreadRng) { +fn bcrypt_rng_algorithms(params: &TaskParams, _rng: &mut ThreadRng, work: &WorkData) { let alg_ids = [ windows::core::w!("RNG"), windows::core::w!("FIPS186DSARNG"), @@ -285,12 +291,12 @@ fn bcrypt_rng_algorithms(params: &TaskParams, _rng: &mut ThreadRng) { BCRYPT_OPEN_ALGORITHM_PROVIDER_FLAGS(0), ); if status.0 != 0 { - // Algorithm may not be available on this system, skip continue; } for _ in 0..params.iterations.min(50) { let _ = BCryptGenRandom(alg, &mut buffer, BCRYPTGENRANDOM_FLAGS(0)); + work.blend_into(&mut buffer); black_box(&buffer); } diff --git a/src/tasks/filesystem.rs b/src/tasks/filesystem.rs index 99b52bd..07ffbed 100644 --- a/src/tasks/filesystem.rs +++ b/src/tasks/filesystem.rs @@ -1,5 +1,6 @@ use crate::categories::Categories; use crate::tasks::{TaskDescriptor, TaskParams}; +use crate::workdata::WorkData; use rand::rngs::ThreadRng; use rand::seq::SliceRandom; use std::hint::black_box; @@ -69,7 +70,7 @@ pub fn register() -> Vec { ] } -fn enumerate_system_dir(params: &TaskParams, _rng: &mut ThreadRng) { +fn enumerate_system_dir(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { let _ = (|| -> std::io::Result<()> { let dir = std::fs::read_dir(r"C:\Windows\System32")?; for (i, entry) in dir.enumerate() { @@ -84,7 +85,7 @@ fn enumerate_system_dir(params: &TaskParams, _rng: &mut ThreadRng) { })(); } -fn enumerate_temp_dir(params: &TaskParams, _rng: &mut ThreadRng) { +fn enumerate_temp_dir(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { let _ = (|| -> std::io::Result<()> { let dir = std::fs::read_dir(std::env::temp_dir())?; for (i, entry) in dir.enumerate() { @@ -99,7 +100,7 @@ fn enumerate_temp_dir(params: &TaskParams, _rng: &mut ThreadRng) { })(); } -fn stat_system_files(params: &TaskParams, rng: &mut ThreadRng) { +fn stat_system_files(params: &TaskParams, rng: &mut ThreadRng, _work: &WorkData) { let paths = [ r"C:\Windows\explorer.exe", r"C:\Windows\notepad.exe", @@ -119,7 +120,7 @@ fn stat_system_files(params: &TaskParams, rng: &mut ThreadRng) { } } -fn read_small_files(params: &TaskParams, rng: &mut ThreadRng) { +fn read_small_files(params: &TaskParams, rng: &mut ThreadRng, _work: &WorkData) { let paths = [ r"C:\Windows\System32\drivers\etc\hosts", r"C:\Windows\System32\drivers\etc\services", @@ -135,7 +136,7 @@ fn read_small_files(params: &TaskParams, rng: &mut ThreadRng) { } } -fn enumerate_program_files(params: &TaskParams, _rng: &mut ThreadRng) { +fn enumerate_program_files(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { let _ = (|| -> std::io::Result<()> { let dirs = [r"C:\Program Files", r"C:\Program Files (x86)"]; let mut count = 0usize; @@ -158,7 +159,7 @@ fn enumerate_program_files(params: &TaskParams, _rng: &mut ThreadRng) { })(); } -fn enumerate_fonts(params: &TaskParams, _rng: &mut ThreadRng) { +fn enumerate_fonts(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { let _ = (|| -> std::io::Result<()> { let dir = std::fs::read_dir(r"C:\Windows\Fonts")?; for (i, entry) in dir.enumerate() { @@ -175,7 +176,7 @@ fn enumerate_fonts(params: &TaskParams, _rng: &mut ThreadRng) { })(); } -fn enumerate_drivers(params: &TaskParams, _rng: &mut ThreadRng) { +fn enumerate_drivers(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { let _ = (|| -> std::io::Result<()> { let dir = std::fs::read_dir(r"C:\Windows\System32\drivers")?; for (i, entry) in dir.enumerate() { @@ -191,7 +192,7 @@ fn enumerate_drivers(params: &TaskParams, _rng: &mut ThreadRng) { })(); } -fn enumerate_prefetch(params: &TaskParams, _rng: &mut ThreadRng) { +fn enumerate_prefetch(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { let _ = (|| -> std::io::Result<()> { let dir = std::fs::read_dir(r"C:\Windows\Prefetch")?; for (i, entry) in dir.enumerate() { @@ -208,7 +209,7 @@ fn enumerate_prefetch(params: &TaskParams, _rng: &mut ThreadRng) { })(); } -fn enumerate_logs(params: &TaskParams, _rng: &mut ThreadRng) { +fn enumerate_logs(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { let _ = (|| -> std::io::Result<()> { let mut count = 0usize; let base = std::path::Path::new(r"C:\Windows\Logs"); @@ -244,7 +245,7 @@ fn enumerate_logs(params: &TaskParams, _rng: &mut ThreadRng) { })(); } -fn stat_common_dlls(params: &TaskParams, rng: &mut ThreadRng) { +fn stat_common_dlls(params: &TaskParams, rng: &mut ThreadRng, _work: &WorkData) { let dlls = [ r"C:\Windows\System32\kernel32.dll", r"C:\Windows\System32\ntdll.dll", @@ -276,7 +277,7 @@ fn stat_common_dlls(params: &TaskParams, rng: &mut ThreadRng) { } } -fn read_system_files_varied(params: &TaskParams, rng: &mut ThreadRng) { +fn read_system_files_varied(params: &TaskParams, rng: &mut ThreadRng, _work: &WorkData) { let paths = [ r"C:\Windows\win.ini", r"C:\Windows\system.ini", @@ -293,7 +294,7 @@ fn read_system_files_varied(params: &TaskParams, rng: &mut ThreadRng) { } } -fn enumerate_user_profile(params: &TaskParams, _rng: &mut ThreadRng) { +fn enumerate_user_profile(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { let _ = (|| -> std::io::Result<()> { let profile = std::env::var("USERPROFILE").map_err(|_| { std::io::Error::new(std::io::ErrorKind::NotFound, "USERPROFILE not set") diff --git a/src/tasks/memory.rs b/src/tasks/memory.rs index a58fd39..8f3aaef 100644 --- a/src/tasks/memory.rs +++ b/src/tasks/memory.rs @@ -1,5 +1,6 @@ use crate::categories::Categories; use crate::tasks::{TaskDescriptor, TaskParams}; +use crate::workdata::WorkData; use rand::rngs::ThreadRng; use rand::{Rng, RngCore}; use std::hint::black_box; @@ -59,22 +60,24 @@ pub fn register() -> Vec { ] } -fn alloc_touch_free(params: &TaskParams, rng: &mut ThreadRng) { +fn alloc_touch_free(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let size = params.buffer_size.min(1_048_576); for _ in 0..params.iterations.min(100) { let mut buf = vec![0u8; size]; for offset in (0..size).step_by(4096) { buf[offset] = rng.gen(); } + work.blend_into(&mut buf); black_box(&buf); } } -fn memcpy_chain(params: &TaskParams, rng: &mut ThreadRng) { +fn memcpy_chain(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let size = params.buffer_size.min(1_048_576); let mut src = vec![0u8; size]; let mut dst = vec![0u8; size]; rng.fill_bytes(&mut src); + work.blend_into(&mut src); for _ in 0..params.iterations.min(1000) { dst.copy_from_slice(&src); std::mem::swap(&mut src, &mut dst); @@ -82,52 +85,58 @@ fn memcpy_chain(params: &TaskParams, rng: &mut ThreadRng) { black_box(&src); } -fn sort_random_memory(params: &TaskParams, rng: &mut ThreadRng) { +fn sort_random_memory(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let size = (params.buffer_size / 8).max(1); + let seed = work.blend_seed(); for _ in 0..params.call_depth { - let mut data: Vec = (0..size).map(|_| rng.gen()).collect(); + let mut data: Vec = (0..size).map(|_| rng.gen::() ^ seed).collect(); data.sort_unstable(); black_box(&data); } } -fn pattern_fill_verify(params: &TaskParams, rng: &mut ThreadRng) { +fn pattern_fill_verify(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let size = params.buffer_size.min(1_048_576); let mut buf = vec![0u8; size]; - for _ in 0..params.iterations.min(100) { - let pattern: u8 = rng.gen(); + let wb = work.as_bytes(); + for round in 0..params.iterations.min(100) { + let base_pattern: u8 = rng.gen(); + let pattern = if !wb.is_empty() { + base_pattern ^ wb[round % wb.len()] + } else { + base_pattern + }; buf.fill(pattern); let valid = buf.iter().all(|&b| b == pattern); black_box(valid); } } -fn heap_fragmentation(params: &TaskParams, rng: &mut ThreadRng) { +fn heap_fragmentation(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let count = params.iterations.min(500); let mut buffers: Vec>> = Vec::with_capacity(count); - // Allocate many small random-sized buffers for _ in 0..count { let size = rng.gen_range(16..=4096); let mut buf = vec![0u8; size]; rng.fill_bytes(&mut buf); + work.blend_into(&mut buf); buffers.push(Some(buf)); } - // Drop roughly 50% at random for slot in buffers.iter_mut() { if rng.gen_bool(0.5) { *slot = None; } } - // Allocate more to fill gaps let mut total_bytes = 0usize; for slot in buffers.iter_mut() { if slot.is_none() { let size = rng.gen_range(16..=4096); let mut buf = vec![0u8; size]; rng.fill_bytes(&mut buf); + work.blend_into(&mut buf); total_bytes += size; *slot = Some(buf); } else { @@ -139,18 +148,17 @@ fn heap_fragmentation(params: &TaskParams, rng: &mut ThreadRng) { black_box(&buffers); } -fn ring_buffer_ops(params: &TaskParams, rng: &mut ThreadRng) { +fn ring_buffer_ops(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let size = params.buffer_size.min(1_048_576).max(1); let mut buffer = vec![0u8; size]; + work.blend_into(&mut buffer); let mut write_pos: usize = 0; let mut read_pos: usize = 0; for _ in 0..params.iterations { - // Write a random byte at write_pos buffer[write_pos] = rng.gen(); write_pos = (write_pos + 1) % size; - // Read at read_pos let val = buffer[read_pos]; black_box(val); read_pos = (read_pos + 1) % size; @@ -159,9 +167,10 @@ fn ring_buffer_ops(params: &TaskParams, rng: &mut ThreadRng) { black_box(&buffer); } -fn binary_search_repeated(params: &TaskParams, rng: &mut ThreadRng) { +fn binary_search_repeated(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let elem_count = (params.buffer_size / 8).max(1); - let mut data: Vec = (0..elem_count).map(|_| rng.gen()).collect(); + let seed = work.blend_seed(); + let mut data: Vec = (0..elem_count).map(|_| rng.gen::() ^ seed).collect(); data.sort_unstable(); let mut hits = 0usize; @@ -175,10 +184,11 @@ fn binary_search_repeated(params: &TaskParams, rng: &mut ThreadRng) { black_box(hits); } -fn reverse_buffer(params: &TaskParams, rng: &mut ThreadRng) { +fn reverse_buffer(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let size = params.buffer_size.min(1_048_576).max(1); let mut buf = vec![0u8; size]; rng.fill_bytes(&mut buf); + work.blend_into(&mut buf); for _ in 0..params.iterations { buf.reverse(); @@ -187,13 +197,14 @@ fn reverse_buffer(params: &TaskParams, rng: &mut ThreadRng) { black_box(&buf); } -fn interleave_buffers(params: &TaskParams, rng: &mut ThreadRng) { +fn interleave_buffers(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let half_size = (params.buffer_size / 2).max(1); for _ in 0..params.call_depth { let mut buf_a = vec![0u8; half_size]; let mut buf_b = vec![0u8; half_size]; rng.fill_bytes(&mut buf_a); rng.fill_bytes(&mut buf_b); + work.blend_into(&mut buf_a); let mut interleaved = Vec::with_capacity(half_size * 2); for i in 0..half_size { @@ -205,22 +216,20 @@ fn interleave_buffers(params: &TaskParams, rng: &mut ThreadRng) { } } -fn scatter_gather(params: &TaskParams, rng: &mut ThreadRng) { +fn scatter_gather(params: &TaskParams, rng: &mut ThreadRng, work: &WorkData) { let size = params.buffer_size.min(1_048_576).max(1); let index_count = (size / 4).max(1); let mut buffer = vec![0u8; size]; rng.fill_bytes(&mut buffer); + work.blend_into(&mut buffer); for _ in 0..params.iterations.min(100) { - // Generate random index list let indices: Vec = (0..index_count).map(|_| rng.gen_range(0..size)).collect(); - // Gather: read at random indices into contiguous output let gathered: Vec = indices.iter().map(|&idx| buffer[idx]).collect(); black_box(&gathered); - // Scatter: write from contiguous input to random indices let input: Vec = (0..index_count).map(|_| rng.gen()).collect(); for (i, &idx) in indices.iter().enumerate() { buffer[idx] = input[i]; @@ -229,3 +238,89 @@ fn scatter_gather(params: &TaskParams, rng: &mut ThreadRng) { black_box(&buffer); } + +#[cfg(test)] +mod tests { + use crate::workdata::WorkData; + + #[test] + fn alloc_buffer_incorporates_work() { + let mut buf = vec![0u8; 4096]; + let mut work = WorkData::new(); + work.feed(&0xDEADBEEFu32); + work.blend_into(&mut buf); + assert!(buf.iter().any(|&b| b != 0)); + } + + #[test] + fn memcpy_source_modified() { + let mut src = vec![0u8; 1024]; + let mut work = WorkData::new(); + work.feed("memcpy_context"); + work.blend_into(&mut src); + assert!(src.iter().any(|&b| b != 0)); + } + + #[test] + fn sort_seed_nonzero() { + let mut work = WorkData::new(); + work.feed(&42u32); + assert_ne!(work.blend_seed(), 0); + } + + #[test] + fn pattern_xor_correct() { + let mut work = WorkData::new(); + work.feed(&0x55u8); + let wb = work.as_bytes(); + assert_eq!(0xAAu8 ^ wb[0], 0xFF); + } + + #[test] + fn ring_buffer_initial_blend() { + let mut buffer = vec![0u8; 256]; + let mut work = WorkData::new(); + work.feed(&[1, 2, 3, 4]); + work.blend_into(&mut buffer); + assert_eq!(&buffer[..4], &[1, 2, 3, 4]); + } + + #[test] + fn binary_search_seed_nonzero() { + let mut work = WorkData::new(); + work.feed(&42u32); + assert_ne!(work.blend_seed(), 0); + } + + #[test] + fn interleave_blends_only_buf_a() { + let mut a = vec![0u8; 64]; + let b = vec![0u8; 64]; + let mut work = WorkData::new(); + work.feed(&[0xAA; 8]); + work.blend_into(&mut a); + assert!(a.iter().any(|&v| v != 0)); + assert!(b.iter().all(|&v| v == 0)); + } + + #[test] + fn single_byte_cycles_across_buffer() { + let mut work = WorkData::new(); + work.feed(&[0xAB]); + let mut buf = vec![0u8; 8]; + work.blend_into(&mut buf); + assert!(buf.iter().all(|&b| b == 0xAB)); + } + + #[test] + fn blend_into_reversible() { + let mut work = WorkData::new(); + work.feed(&0xCAFEBABEu32); + let original = vec![42u8; 256]; + let mut buf = original.clone(); + work.blend_into(&mut buf); + assert_ne!(buf, original); + work.blend_into(&mut buf); + assert_eq!(buf, original); + } +} diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs index ba30c1d..21eb2cb 100644 --- a/src/tasks/mod.rs +++ b/src/tasks/mod.rs @@ -14,6 +14,7 @@ pub mod registry; pub mod winapi_tasks; use crate::categories::Categories; +use crate::workdata::WorkData; use rand::rngs::ThreadRng; pub struct TaskParams { @@ -22,7 +23,7 @@ pub struct TaskParams { pub call_depth: usize, } -pub type TaskFn = fn(&TaskParams, &mut ThreadRng); +pub type TaskFn = fn(&TaskParams, &mut ThreadRng, &WorkData); #[allow(dead_code)] pub struct TaskDescriptor { @@ -49,3 +50,211 @@ pub fn all_tasks() -> Vec { tasks.extend(crypto::register()); tasks } + +#[cfg(test)] +mod tests { + use super::*; + use crate::workdata::WorkData; + + fn non_network_tasks() -> Vec { + all_tasks() + .into_iter() + .filter(|t| !t.category.contains(Categories::NETWORK)) + .collect() + } + + fn run_every_task(params: &TaskParams, work: &WorkData) { + let tasks = non_network_tasks(); + let mut rng = rand::thread_rng(); + for task in &tasks { + (task.func)(params, &mut rng, work); + } + } + + fn make_work(data: &[u8]) -> WorkData { + let mut w = WorkData::new(); + w.feed(data); + w + } + + // ── Registry completeness ────────────────────────────────────────── + + #[test] + fn registry_compute_count() { + let n = all_tasks().iter().filter(|t| t.category == Categories::COMPUTE).count(); + assert_eq!(n, 14, "expected 14 COMPUTE tasks"); + } + + #[test] + fn registry_memory_count() { + let n = all_tasks().iter().filter(|t| t.category == Categories::MEMORY).count(); + assert_eq!(n, 10, "expected 10 MEMORY tasks"); + } + + #[test] + fn registry_filesystem_count() { + let n = all_tasks().iter().filter(|t| t.category == Categories::FILESYSTEM).count(); + assert_eq!(n, 12, "expected 12 FILESYSTEM tasks"); + } + + #[test] + fn registry_registry_count() { + let n = all_tasks().iter().filter(|t| t.category == Categories::REGISTRY).count(); + assert_eq!(n, 10, "expected 10 REGISTRY tasks"); + } + + #[test] + fn registry_winapi_count() { + let n = all_tasks().iter().filter(|t| t.category == Categories::WINAPI).count(); + assert_eq!(n, 16, "expected 16 WINAPI tasks"); + } + + #[test] + fn registry_network_count() { + let n = all_tasks().iter().filter(|t| t.category == Categories::NETWORK).count(); + assert_eq!(n, 7, "expected 7 NETWORK tasks"); + } + + #[test] + fn registry_crypto_count() { + let n = all_tasks().iter().filter(|t| t.category == Categories::CRYPTO).count(); + assert_eq!(n, 7, "expected 7 CRYPTO tasks"); + } + + #[test] + fn registry_total() { + assert_eq!(all_tasks().len(), 76, "expected 76 total tasks"); + } + + #[test] + fn registry_no_duplicate_names() { + let tasks = all_tasks(); + let mut names: Vec<&str> = tasks.iter().map(|t| t.name).collect(); + names.sort(); + let before = names.len(); + names.dedup(); + assert_eq!(names.len(), before, "duplicate task names found"); + } + + // ── Every task runs with every work-data shape ───────────────────── + // These call every registered task function pointer directly, catching + // panics from index-out-of-bounds, overflow, or allocation failures. + + #[test] + fn every_task_empty_work() { + run_every_task( + &TaskParams { iterations: 10, buffer_size: 256, call_depth: 2 }, + &WorkData::new(), + ); + } + + #[test] + fn every_task_single_byte_work() { + run_every_task( + &TaskParams { iterations: 10, buffer_size: 256, call_depth: 2 }, + &make_work(&[0xFF]), + ); + } + + #[test] + fn every_task_4_bytes_work() { + run_every_task( + &TaskParams { iterations: 10, buffer_size: 256, call_depth: 2 }, + &make_work(&0xDEADBEEFu32.to_ne_bytes()), + ); + } + + #[test] + fn every_task_4kb_work() { + run_every_task( + &TaskParams { iterations: 10, buffer_size: 256, call_depth: 2 }, + &make_work(&vec![0xAB; 4096]), + ); + } + + #[test] + fn every_task_all_zeros_work() { + run_every_task( + &TaskParams { iterations: 10, buffer_size: 256, call_depth: 2 }, + &make_work(&[0x00; 64]), + ); + } + + #[test] + fn every_task_all_ff_work() { + run_every_task( + &TaskParams { iterations: 10, buffer_size: 256, call_depth: 2 }, + &make_work(&[0xFF; 64]), + ); + } + + #[test] + fn every_task_alternating_work() { + let data: Vec = (0..256).map(|i| if i % 2 == 0 { 0xFF } else { 0x00 }).collect(); + run_every_task( + &TaskParams { iterations: 10, buffer_size: 256, call_depth: 2 }, + &make_work(&data), + ); + } + + // ── Parameter edge cases × work data ─────────────────────────────── + + #[test] + fn every_task_zero_params_empty_work() { + run_every_task( + &TaskParams { iterations: 0, buffer_size: 0, call_depth: 0 }, + &WorkData::new(), + ); + } + + #[test] + fn every_task_zero_params_with_work() { + run_every_task( + &TaskParams { iterations: 0, buffer_size: 0, call_depth: 0 }, + &make_work(&0xCAFEBABEu32.to_ne_bytes()), + ); + } + + #[test] + fn every_task_min_params_with_work() { + run_every_task( + &TaskParams { iterations: 1, buffer_size: 1, call_depth: 1 }, + &make_work(&[42]), + ); + } + + #[test] + fn every_task_large_params_empty_work() { + run_every_task( + &TaskParams { iterations: 5000, buffer_size: 262144, call_depth: 8 }, + &WorkData::new(), + ); + } + + #[test] + fn every_task_large_params_large_work() { + run_every_task( + &TaskParams { iterations: 5000, buffer_size: 262144, call_depth: 8 }, + &make_work(&vec![0xCD; 8192]), + ); + } + + // ── Max-value work data that pushes derive_usize / blend_seed ────── + + #[test] + fn every_task_max_seed_work() { + run_every_task( + &TaskParams { iterations: 10, buffer_size: 256, call_depth: 2 }, + &make_work(&[0xFF; 128]), + ); + } + + #[test] + fn every_task_sequential_bytes() { + let data: Vec = (0u8..=255).collect(); + run_every_task( + &TaskParams { iterations: 10, buffer_size: 256, call_depth: 2 }, + &make_work(&data), + ); + } +} diff --git a/src/tasks/network.rs b/src/tasks/network.rs index cbee3ca..867ddcf 100644 --- a/src/tasks/network.rs +++ b/src/tasks/network.rs @@ -1,5 +1,6 @@ use crate::categories::Categories; use crate::tasks::{TaskDescriptor, TaskParams}; +use crate::workdata::WorkData; use rand::rngs::ThreadRng; use rand::seq::SliceRandom; use rand::RngCore; @@ -66,7 +67,7 @@ pub fn register() -> Vec { ] } -fn dns_lookups(params: &TaskParams, rng: &mut ThreadRng) { +fn dns_lookups(params: &TaskParams, rng: &mut ThreadRng, _work: &WorkData) { let hosts = [ "google.com:80", "microsoft.com:80", @@ -104,7 +105,7 @@ fn dns_lookups(params: &TaskParams, rng: &mut ThreadRng) { } } -fn http_get(params: &TaskParams, rng: &mut ThreadRng) { +fn http_get(params: &TaskParams, rng: &mut ThreadRng, _work: &WorkData) { let targets: &[(&str, u16, &str)] = &[ ("httpbin.org", 80, "/get"), ("ip-api.com", 80, "/json"), @@ -141,7 +142,7 @@ fn http_get(params: &TaskParams, rng: &mut ThreadRng) { } } -fn ntp_query(params: &TaskParams, rng: &mut ThreadRng) { +fn ntp_query(params: &TaskParams, rng: &mut ThreadRng, _work: &WorkData) { let servers = [ "pool.ntp.org:123", "time.google.com:123", @@ -162,7 +163,7 @@ fn ntp_query(params: &TaskParams, rng: &mut ThreadRng) { }; set_socket_timeouts(&socket, 3000); let mut packet = [0u8; 48]; - packet[0] = 0x1B; // NTP v3, client mode + packet[0] = 0x1B; if socket.send_to(&packet, server).is_err() { continue; } @@ -172,7 +173,7 @@ fn ntp_query(params: &TaskParams, rng: &mut ThreadRng) { } } -fn http_head_request(params: &TaskParams, rng: &mut ThreadRng) { +fn http_head_request(params: &TaskParams, rng: &mut ThreadRng, _work: &WorkData) { let targets: &[(&str, u16, &str)] = &[ ("httpbin.org", 80, "/get"), ("httpbin.org", 80, "/headers"), @@ -208,7 +209,7 @@ fn http_head_request(params: &TaskParams, rng: &mut ThreadRng) { } } -fn tcp_connect_probe(params: &TaskParams, rng: &mut ThreadRng) { +fn tcp_connect_probe(params: &TaskParams, rng: &mut ThreadRng, _work: &WorkData) { let targets: &[(&str, u16)] = &[ ("google.com", 80), ("google.com", 443), @@ -224,17 +225,15 @@ fn tcp_connect_probe(params: &TaskParams, rng: &mut ThreadRng) { for _ in 0..params.iterations.min(10) { if let Some(&(host, port)) = targets.choose(rng) { let addr = format!("{}:{}", host, port); - // Just connect and immediately drop — TCP handshake only if let Ok(stream) = TcpStream::connect(&*addr) { set_socket_timeouts(&stream, 2000); black_box(&stream); - // stream is dropped here, sending FIN } } } } -fn dns_varied_ports(params: &TaskParams, rng: &mut ThreadRng) { +fn dns_varied_ports(params: &TaskParams, rng: &mut ThreadRng, _work: &WorkData) { let hosts = [ "example.com:21", "example.com:22", @@ -263,7 +262,7 @@ fn dns_varied_ports(params: &TaskParams, rng: &mut ThreadRng) { } } -fn http_post_discard(params: &TaskParams, rng: &mut ThreadRng) { +fn http_post_discard(params: &TaskParams, rng: &mut ThreadRng, _work: &WorkData) { let targets: &[(&str, u16, &str, &str)] = &[ ("httpbin.org", 80, "/post", "POST"), ("httpbin.org", 80, "/anything", "POST"), @@ -275,7 +274,6 @@ fn http_post_discard(params: &TaskParams, rng: &mut ThreadRng) { let body_size = params.buffer_size.min(1024); let mut body = vec![0u8; body_size]; rng.fill_bytes(&mut body); - // Encode body as hex string for a safe text payload let body_hex: String = body.iter().map(|b| format!("{:02x}", b)).collect(); for _ in 0..params.call_depth.min(5) { diff --git a/src/tasks/registry.rs b/src/tasks/registry.rs index 9dd7653..1b349a9 100644 --- a/src/tasks/registry.rs +++ b/src/tasks/registry.rs @@ -1,5 +1,6 @@ use crate::categories::Categories; use crate::tasks::{TaskDescriptor, TaskParams}; +use crate::workdata::WorkData; use rand::rngs::ThreadRng; use std::hint::black_box; use windows::Win32::System::Registry::*; @@ -136,7 +137,7 @@ unsafe fn enum_values(hkey: HKEY, max_values: usize) { } } -fn read_installed_software(params: &TaskParams, _rng: &mut ThreadRng) { +fn read_installed_software(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { const PATH: &[u16] = &{ let mut arr = [0u16; 60]; let bytes = b"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\0"; @@ -152,7 +153,7 @@ fn read_installed_software(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn read_system_info_reg(params: &TaskParams, _rng: &mut ThreadRng) { +fn read_system_info_reg(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { const PATH: &[u16] = &{ let mut arr = [0u16; 50]; let bytes = b"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\0"; @@ -191,7 +192,7 @@ fn read_system_info_reg(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn enumerate_services_reg(params: &TaskParams, _rng: &mut ThreadRng) { +fn enumerate_services_reg(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { const PATH: &[u16] = &{ let mut arr = [0u16; 50]; let bytes = b"SYSTEM\\CurrentControlSet\\Services\0"; @@ -207,7 +208,7 @@ fn enumerate_services_reg(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn read_timezone_info(params: &TaskParams, _rng: &mut ThreadRng) { +fn read_timezone_info(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { const PATH: &[u16] = &{ let mut arr = [0u16; 60]; let bytes = b"SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation\0"; @@ -245,7 +246,7 @@ fn read_timezone_info(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn read_environment_vars(params: &TaskParams, _rng: &mut ThreadRng) { +fn read_environment_vars(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { const PATH: &[u16] = &{ let mut arr = [0u16; 70]; let bytes = b"SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\0"; @@ -290,7 +291,7 @@ fn read_environment_vars(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn read_network_config(params: &TaskParams, _rng: &mut ThreadRng) { +fn read_network_config(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { const PATH: &[u16] = &{ let mut arr = [0u16; 60]; let bytes = b"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\0"; @@ -340,7 +341,7 @@ fn read_network_config(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn read_hardware_info(params: &TaskParams, _rng: &mut ThreadRng) { +fn read_hardware_info(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { const PATH: &[u16] = &{ let mut arr = [0u16; 60]; let bytes = b"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0\0"; @@ -382,7 +383,7 @@ fn read_hardware_info(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn read_font_list(params: &TaskParams, _rng: &mut ThreadRng) { +fn read_font_list(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { const PATH: &[u16] = &{ let mut arr = [0u16; 60]; let bytes = b"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Fonts\0"; @@ -412,7 +413,7 @@ fn read_font_list(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn read_startup_programs(params: &TaskParams, _rng: &mut ThreadRng) { +fn read_startup_programs(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { const PATH_HKLM: &[u16] = &{ let mut arr = [0u16; 50]; let bytes = b"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\0"; @@ -434,7 +435,6 @@ fn read_startup_programs(params: &TaskParams, _rng: &mut ThreadRng) { arr }; unsafe { - // Read HKLM\...\Run let mut hkey = HKEY::default(); let result = RegOpenKeyExW( HKEY_LOCAL_MACHINE, @@ -448,7 +448,6 @@ fn read_startup_programs(params: &TaskParams, _rng: &mut ThreadRng) { let _ = RegCloseKey(hkey); } - // Read HKCU\...\Run let mut hkey = HKEY::default(); let result = RegOpenKeyExW( HKEY_CURRENT_USER, @@ -464,7 +463,7 @@ fn read_startup_programs(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn read_file_associations(params: &TaskParams, _rng: &mut ThreadRng) { +fn read_file_associations(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { unsafe { let mut name_buf = [0u16; 256]; for i in 0..params.iterations as u32 { @@ -482,7 +481,6 @@ fn read_file_associations(params: &TaskParams, _rng: &mut ThreadRng) { if result.0 != 0 { break; } - // Only process entries starting with '.' if name_len > 0 && name_buf[0] == b'.' as u16 { black_box(&name_buf[..name_len as usize]); } diff --git a/src/tasks/winapi_tasks.rs b/src/tasks/winapi_tasks.rs index 2846217..acae8c8 100644 --- a/src/tasks/winapi_tasks.rs +++ b/src/tasks/winapi_tasks.rs @@ -1,5 +1,6 @@ use crate::categories::Categories; use crate::tasks::{TaskDescriptor, TaskParams}; +use crate::workdata::WorkData; use rand::rngs::ThreadRng; use rand::Rng; use std::hint::black_box; @@ -100,15 +101,13 @@ pub fn register() -> Vec { ] } -// ── Existing tasks ────────────────────────────────────────────────────────── - unsafe extern "system" fn enum_window_callback(hwnd: HWND, lparam: LPARAM) -> BOOL { let windows = &mut *(lparam.0 as *mut Vec); windows.push(hwnd); BOOL(1) } -fn enumerate_windows(params: &TaskParams, _rng: &mut ThreadRng) { +fn enumerate_windows(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { unsafe { let mut windows: Vec = Vec::new(); let ptr = &mut windows as *mut Vec as isize; @@ -122,7 +121,7 @@ fn enumerate_windows(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn enumerate_processes(params: &TaskParams, _rng: &mut ThreadRng) { +fn enumerate_processes(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { unsafe { let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); let snapshot = match snapshot { @@ -149,7 +148,7 @@ fn enumerate_processes(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn query_system_info(params: &TaskParams, _rng: &mut ThreadRng) { +fn query_system_info(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { unsafe { for _ in 0..params.call_depth { let mut sys_info = SYSTEM_INFO::default(); @@ -166,20 +165,18 @@ fn query_system_info(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn read_clipboard(params: &TaskParams, _rng: &mut ThreadRng) { +fn read_clipboard(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { unsafe { for _ in 0..params.call_depth { if OpenClipboard(HWND::default()).is_ok() { - let _ = black_box(GetClipboardData(1)); // CF_TEXT + let _ = black_box(GetClipboardData(1)); let _ = CloseClipboard(); } } } } -// ── New tasks ─────────────────────────────────────────────────────────────── - -fn get_system_metrics(params: &TaskParams, rng: &mut ThreadRng) { +fn get_system_metrics(params: &TaskParams, rng: &mut ThreadRng, _work: &WorkData) { const METRIC_INDICES: [i32; 10] = [ 0, // SM_CXSCREEN 1, // SM_CYSCREEN @@ -203,7 +200,7 @@ fn get_system_metrics(params: &TaskParams, rng: &mut ThreadRng) { } } -fn get_foreground_window_info(params: &TaskParams, _rng: &mut ThreadRng) { +fn get_foreground_window_info(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { unsafe { for _ in 0..params.call_depth { let hwnd = GetForegroundWindow(); @@ -224,7 +221,7 @@ fn get_foreground_window_info(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn get_cursor_position(params: &TaskParams, _rng: &mut ThreadRng) { +fn get_cursor_position(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { unsafe { for _ in 0..params.iterations { let mut point = POINT::default(); @@ -235,7 +232,7 @@ fn get_cursor_position(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn get_desktop_window_info(params: &TaskParams, _rng: &mut ThreadRng) { +fn get_desktop_window_info(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { unsafe { for _ in 0..params.call_depth { let hwnd = GetDesktopWindow(); @@ -259,7 +256,7 @@ fn get_desktop_window_info(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn get_logical_drives_info(params: &TaskParams, _rng: &mut ThreadRng) { +fn get_logical_drives_info(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { unsafe { let mask = GetLogicalDrives(); if mask == 0 { @@ -277,7 +274,6 @@ fn get_logical_drives_info(params: &TaskParams, _rng: &mut ThreadRng) { black_box(drive_type); } - // Repeat the iteration to burn more cycles based on params for _ in 1..params.call_depth { let m = GetLogicalDrives(); black_box(m); @@ -285,7 +281,7 @@ fn get_logical_drives_info(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn get_volume_info(params: &TaskParams, _rng: &mut ThreadRng) { +fn get_volume_info(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { unsafe { for _ in 0..params.call_depth { let mut volume_name = [0u16; 260]; @@ -313,7 +309,7 @@ fn get_volume_info(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn get_disk_free_space(params: &TaskParams, _rng: &mut ThreadRng) { +fn get_disk_free_space(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { unsafe { for _ in 0..params.call_depth { let mut free_bytes_available: u64 = 0; @@ -335,7 +331,7 @@ fn get_disk_free_space(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn find_files_pattern(params: &TaskParams, _rng: &mut ThreadRng) { +fn find_files_pattern(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { unsafe { let mut find_data = WIN32_FIND_DATAW::default(); let handle = FindFirstFileW(w!("C:\\Windows\\System32\\*.dll"), &mut find_data); @@ -361,7 +357,7 @@ fn find_files_pattern(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn get_module_handles(params: &TaskParams, rng: &mut ThreadRng) { +fn get_module_handles(params: &TaskParams, rng: &mut ThreadRng, _work: &WorkData) { let dll_names: &[PCWSTR] = &[ w!("kernel32.dll"), w!("ntdll.dll"), @@ -393,7 +389,7 @@ fn get_module_handles(params: &TaskParams, rng: &mut ThreadRng) { } } -fn virtual_query_memory(params: &TaskParams, _rng: &mut ThreadRng) { +fn virtual_query_memory(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { unsafe { let mut addr: usize = 0; let info_size = std::mem::size_of::(); @@ -413,9 +409,7 @@ fn virtual_query_memory(params: &TaskParams, _rng: &mut ThreadRng) { black_box(info.State); black_box(info.Type); - // Advance to the next region let region_size = if info.RegionSize == 0 { - // Avoid infinite loop on zero-size region 4096 } else { info.RegionSize @@ -428,7 +422,7 @@ fn virtual_query_memory(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn get_system_directories(params: &TaskParams, _rng: &mut ThreadRng) { +fn get_system_directories(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { unsafe { for _ in 0..params.call_depth { let mut sys_dir = [0u16; 260]; @@ -446,7 +440,7 @@ fn get_system_directories(params: &TaskParams, _rng: &mut ThreadRng) { } } -fn get_process_thread_ids(params: &TaskParams, _rng: &mut ThreadRng) { +fn get_process_thread_ids(params: &TaskParams, _rng: &mut ThreadRng, _work: &WorkData) { unsafe { for _ in 0..params.iterations { let pid = GetCurrentProcessId(); diff --git a/src/workdata.rs b/src/workdata.rs new file mode 100644 index 0000000..be5d3f8 --- /dev/null +++ b/src/workdata.rs @@ -0,0 +1,560 @@ +#[derive(Clone, Default)] +pub struct WorkData { + bytes: Vec, +} + +pub trait FeedWork { + fn write_work_bytes(&self, out: &mut Vec); +} + +macro_rules! impl_feed_int { + ($($t:ty),*) => { + $(impl FeedWork for $t { + fn write_work_bytes(&self, out: &mut Vec) { + out.extend_from_slice(&self.to_ne_bytes()); + } + })* + }; +} + +impl_feed_int!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64); + +impl FeedWork for bool { + fn write_work_bytes(&self, out: &mut Vec) { + out.push(*self as u8); + } +} + +impl FeedWork for str { + fn write_work_bytes(&self, out: &mut Vec) { + out.extend_from_slice(self.as_bytes()); + } +} + +impl FeedWork for String { + fn write_work_bytes(&self, out: &mut Vec) { + out.extend_from_slice(self.as_bytes()); + } +} + +impl FeedWork for [u8] { + fn write_work_bytes(&self, out: &mut Vec) { + out.extend_from_slice(self); + } +} + +impl FeedWork for Vec { + fn write_work_bytes(&self, out: &mut Vec) { + out.extend_from_slice(self); + } +} + +impl FeedWork for [u8; N] { + fn write_work_bytes(&self, out: &mut Vec) { + out.extend_from_slice(self); + } +} + +impl FeedWork for &T { + fn write_work_bytes(&self, out: &mut Vec) { + (*self).write_work_bytes(out); + } +} + +impl FeedWork for Box { + fn write_work_bytes(&self, out: &mut Vec) { + (**self).write_work_bytes(out); + } +} + +impl WorkData { + pub fn new() -> Self { + Self::default() + } + + pub fn feed(&mut self, data: &(impl FeedWork + ?Sized)) { + data.write_work_bytes(&mut self.bytes); + } + + pub(crate) fn is_empty(&self) -> bool { + self.bytes.is_empty() + } + + pub(crate) fn as_bytes(&self) -> &[u8] { + &self.bytes + } + + /// XOR work bytes cyclically into buf, creating a data dependency. + pub(crate) fn blend_into(&self, buf: &mut [u8]) { + if self.bytes.is_empty() { + return; + } + for (i, byte) in buf.iter_mut().enumerate() { + *byte ^= self.bytes[i % self.bytes.len()]; + } + } + + /// Fold all bytes into a u64 seed for control-flow integration. + pub(crate) fn blend_seed(&self) -> u64 { + let mut seed: u64 = 0; + for (i, &b) in self.bytes.iter().enumerate() { + seed ^= (b as u64).wrapping_shl((i as u32).wrapping_mul(7) & 63); + } + seed + } + + /// Derive a usize from work data at the given logical index. + pub(crate) fn derive_usize(&self, index: usize) -> usize { + if self.bytes.is_empty() { + return 0; + } + let offset = index.wrapping_mul(7) % self.bytes.len(); + let mut buf = [0u8; 8]; + for (i, slot) in buf.iter_mut().enumerate() { + *slot = self.bytes[(offset + i) % self.bytes.len()]; + } + usize::from_ne_bytes(buf) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // ── Construction & feed ──────────────────────────────────────────── + + #[test] + fn new_is_empty() { + let wd = WorkData::new(); + assert!(wd.is_empty()); + assert_eq!(wd.as_bytes().len(), 0); + } + + #[test] + fn default_is_empty() { + let wd = WorkData::default(); + assert!(wd.is_empty()); + } + + #[test] + fn feed_u8() { + let mut wd = WorkData::new(); + wd.feed(&0xABu8); + assert_eq!(wd.as_bytes(), &[0xAB]); + } + + #[test] + fn feed_u16() { + let mut wd = WorkData::new(); + wd.feed(&0x1234u16); + assert_eq!(wd.as_bytes().len(), 2); + assert_eq!(wd.as_bytes(), &0x1234u16.to_ne_bytes()); + } + + #[test] + fn feed_u32() { + let mut wd = WorkData::new(); + wd.feed(&42u32); + assert!(!wd.is_empty()); + assert_eq!(wd.as_bytes().len(), 4); + assert_eq!(wd.as_bytes(), &42u32.to_ne_bytes()); + } + + #[test] + fn feed_u64() { + let mut wd = WorkData::new(); + wd.feed(&0xDEADBEEFCAFEBABEu64); + assert_eq!(wd.as_bytes().len(), 8); + } + + #[test] + fn feed_u128() { + let mut wd = WorkData::new(); + wd.feed(&1u128); + assert_eq!(wd.as_bytes().len(), 16); + } + + #[test] + fn feed_usize() { + let mut wd = WorkData::new(); + wd.feed(&999usize); + assert_eq!(wd.as_bytes().len(), std::mem::size_of::()); + } + + #[test] + fn feed_i8() { + let mut wd = WorkData::new(); + wd.feed(&(-1i8)); + assert_eq!(wd.as_bytes(), &(-1i8).to_ne_bytes()); + } + + #[test] + fn feed_i16() { + let mut wd = WorkData::new(); + wd.feed(&(-256i16)); + assert_eq!(wd.as_bytes().len(), 2); + } + + #[test] + fn feed_i32() { + let mut wd = WorkData::new(); + wd.feed(&(-42i32)); + assert_eq!(wd.as_bytes(), &(-42i32).to_ne_bytes()); + } + + #[test] + fn feed_i64() { + let mut wd = WorkData::new(); + wd.feed(&i64::MIN); + assert_eq!(wd.as_bytes().len(), 8); + } + + #[test] + fn feed_i128() { + let mut wd = WorkData::new(); + wd.feed(&i128::MAX); + assert_eq!(wd.as_bytes().len(), 16); + } + + #[test] + fn feed_isize() { + let mut wd = WorkData::new(); + wd.feed(&(-1isize)); + assert_eq!(wd.as_bytes().len(), std::mem::size_of::()); + } + + #[test] + fn feed_f32() { + let mut wd = WorkData::new(); + wd.feed(&3.14f32); + assert_eq!(wd.as_bytes().len(), 4); + assert_eq!(wd.as_bytes(), &3.14f32.to_ne_bytes()); + } + + #[test] + fn feed_f64() { + let mut wd = WorkData::new(); + wd.feed(&2.718f64); + assert_eq!(wd.as_bytes().len(), 8); + assert_eq!(wd.as_bytes(), &2.718f64.to_ne_bytes()); + } + + #[test] + fn feed_bool_true() { + let mut wd = WorkData::new(); + wd.feed(&true); + assert_eq!(wd.as_bytes(), &[1]); + } + + #[test] + fn feed_bool_false() { + let mut wd = WorkData::new(); + wd.feed(&false); + assert_eq!(wd.as_bytes(), &[0]); + } + + #[test] + fn feed_str_literal() { + let mut wd = WorkData::new(); + wd.feed("hello"); + assert_eq!(wd.as_bytes(), b"hello"); + } + + #[test] + fn feed_string_owned() { + let mut wd = WorkData::new(); + let s = String::from("world"); + wd.feed(&s); + assert_eq!(wd.as_bytes(), b"world"); + } + + #[test] + fn feed_byte_slice() { + let mut wd = WorkData::new(); + let data: &[u8] = &[1, 2, 3]; + wd.feed(data); + assert_eq!(wd.as_bytes(), &[1, 2, 3]); + } + + #[test] + fn feed_vec_u8() { + let mut wd = WorkData::new(); + let data = vec![10u8, 20, 30]; + wd.feed(&data); + assert_eq!(wd.as_bytes(), &[10, 20, 30]); + } + + #[test] + fn feed_byte_array() { + let mut wd = WorkData::new(); + wd.feed(&[0xAA, 0xBB, 0xCC]); + assert_eq!(wd.as_bytes(), &[0xAA, 0xBB, 0xCC]); + } + + #[test] + fn feed_boxed_u32() { + let mut wd = WorkData::new(); + let boxed: Box = Box::new(42); + wd.feed(&boxed); + assert_eq!(wd.as_bytes(), &42u32.to_ne_bytes()); + } + + #[test] + fn feed_boxed_str() { + let mut wd = WorkData::new(); + let boxed: Box = "boxed".into(); + wd.feed(&boxed); + assert_eq!(wd.as_bytes(), b"boxed"); + } + + #[test] + fn feed_reference_delegates() { + let mut wd1 = WorkData::new(); + wd1.feed(&42u32); + let mut wd2 = WorkData::new(); + let val = 42u32; + let r = &val; + wd2.feed(r); + assert_eq!(wd1.as_bytes(), wd2.as_bytes()); + } + + // ── Accumulation ─────────────────────────────────────────────────── + + #[test] + fn feed_accumulates_bytes() { + let mut wd = WorkData::new(); + wd.feed(&1u8); + assert_eq!(wd.as_bytes().len(), 1); + wd.feed(&2u8); + assert_eq!(wd.as_bytes().len(), 2); + wd.feed("abc"); + assert_eq!(wd.as_bytes().len(), 5); + } + + #[test] + fn feed_multiple_types_concatenates() { + let mut wd = WorkData::new(); + wd.feed(&1u8); + wd.feed(&2u16); + wd.feed(&3u32); + wd.feed(&4u64); + assert_eq!(wd.as_bytes().len(), 1 + 2 + 4 + 8); + } + + #[test] + fn feed_order_matters() { + let mut wd1 = WorkData::new(); + wd1.feed(&1u8); + wd1.feed(&2u8); + + let mut wd2 = WorkData::new(); + wd2.feed(&2u8); + wd2.feed(&1u8); + + assert_ne!(wd1.as_bytes(), wd2.as_bytes()); + } + + // ── Empty / zero-length feeds ────────────────────────────────────── + + #[test] + fn feed_empty_string_stays_empty() { + let mut wd = WorkData::new(); + wd.feed(""); + assert!(wd.is_empty()); + } + + #[test] + fn feed_empty_slice_stays_empty() { + let mut wd = WorkData::new(); + let empty: &[u8] = &[]; + wd.feed(empty); + assert!(wd.is_empty()); + } + + #[test] + fn feed_empty_vec_stays_empty() { + let mut wd = WorkData::new(); + let empty: Vec = vec![]; + wd.feed(&empty); + assert!(wd.is_empty()); + } + + #[test] + fn feed_zero_length_array() { + let mut wd = WorkData::new(); + wd.feed(&[0u8; 0]); + assert!(wd.is_empty()); + } + + // ── blend_into ───────────────────────────────────────────────────── + + #[test] + fn blend_into_empty_is_noop() { + let wd = WorkData::new(); + let mut buf = [1u8, 2, 3, 4]; + wd.blend_into(&mut buf); + assert_eq!(buf, [1, 2, 3, 4]); + } + + #[test] + fn blend_into_xors_single_byte() { + let mut wd = WorkData::new(); + wd.feed(&[0xFF]); + let mut buf = [0x0F, 0xF0, 0xAA, 0x55]; + wd.blend_into(&mut buf); + assert_eq!(buf, [0x0F ^ 0xFF, 0xF0 ^ 0xFF, 0xAA ^ 0xFF, 0x55 ^ 0xFF]); + } + + #[test] + fn blend_into_xors_cyclically() { + let mut wd = WorkData::new(); + wd.feed(&[0xFF, 0x00]); + let mut buf = [0x0F, 0xF0, 0x0F, 0xF0]; + wd.blend_into(&mut buf); + assert_eq!(buf, [0xF0, 0xF0, 0xF0, 0xF0]); + } + + #[test] + fn blend_into_larger_data_than_buf() { + let mut wd = WorkData::new(); + wd.feed(&[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]); + let mut buf = [0x00, 0x00, 0x00]; + wd.blend_into(&mut buf); + assert_eq!(buf, [0x11, 0x22, 0x33]); + } + + #[test] + fn blend_into_zero_key_is_noop() { + let mut wd = WorkData::new(); + wd.feed(&[0x00, 0x00, 0x00, 0x00]); + let mut buf = [0xAB, 0xCD, 0xEF, 0x01]; + wd.blend_into(&mut buf); + assert_eq!(buf, [0xAB, 0xCD, 0xEF, 0x01]); + } + + #[test] + fn blend_into_double_xor_restores_original() { + let mut wd = WorkData::new(); + wd.feed(&[0xDE, 0xAD, 0xBE, 0xEF]); + let original = [0x01, 0x02, 0x03, 0x04]; + let mut buf = original; + wd.blend_into(&mut buf); + assert_ne!(buf, original); + wd.blend_into(&mut buf); + assert_eq!(buf, original); + } + + #[test] + fn blend_into_empty_buf() { + let mut wd = WorkData::new(); + wd.feed(&42u32); + let mut buf: [u8; 0] = []; + wd.blend_into(&mut buf); + } + + // ── blend_seed ───────────────────────────────────────────────────── + + #[test] + fn blend_seed_empty_is_zero() { + let wd = WorkData::new(); + assert_eq!(wd.blend_seed(), 0); + } + + #[test] + fn blend_seed_deterministic() { + let mut wd1 = WorkData::new(); + wd1.feed(&42u32); + let mut wd2 = WorkData::new(); + wd2.feed(&42u32); + assert_eq!(wd1.blend_seed(), wd2.blend_seed()); + } + + #[test] + fn blend_seed_differs_for_different_data() { + let mut wd1 = WorkData::new(); + wd1.feed(&42u32); + let mut wd2 = WorkData::new(); + wd2.feed(&43u32); + assert_ne!(wd1.blend_seed(), wd2.blend_seed()); + } + + #[test] + fn blend_seed_nonzero_for_nonzero_data() { + let mut wd = WorkData::new(); + wd.feed(&0xDEADBEEFu32); + assert_ne!(wd.blend_seed(), 0); + } + + #[test] + fn blend_seed_single_byte() { + let mut wd = WorkData::new(); + wd.feed(&0xFFu8); + assert_ne!(wd.blend_seed(), 0); + } + + // ── derive_usize ─────────────────────────────────────────────────── + + #[test] + fn derive_usize_empty_is_zero() { + let wd = WorkData::new(); + assert_eq!(wd.derive_usize(0), 0); + assert_eq!(wd.derive_usize(1), 0); + assert_eq!(wd.derive_usize(999), 0); + } + + #[test] + fn derive_usize_deterministic() { + let mut wd = WorkData::new(); + wd.feed(&0xDEADBEEFu64); + let a = wd.derive_usize(0); + let b = wd.derive_usize(0); + assert_eq!(a, b); + } + + #[test] + fn derive_usize_varies_by_index() { + let mut wd = WorkData::new(); + wd.feed(&[1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); + let a = wd.derive_usize(0); + let b = wd.derive_usize(1); + assert_ne!(a, b); + } + + #[test] + fn derive_usize_works_with_small_data() { + let mut wd = WorkData::new(); + wd.feed(&1u8); + let _ = wd.derive_usize(0); + let _ = wd.derive_usize(100); + } + + #[test] + fn derive_usize_large_index_no_panic() { + let mut wd = WorkData::new(); + wd.feed(&42u32); + let _ = wd.derive_usize(usize::MAX); + let _ = wd.derive_usize(usize::MAX / 2); + } + + // ── Clone independence ───────────────────────────────────────────── + + #[test] + fn clone_is_independent() { + let mut wd = WorkData::new(); + wd.feed(&42u32); + let mut cloned = wd.clone(); + cloned.feed(&99u32); + assert_eq!(wd.as_bytes().len(), 4); + assert_eq!(cloned.as_bytes().len(), 8); + } + + #[test] + fn clone_has_same_data() { + let mut wd = WorkData::new(); + wd.feed(&42u32); + wd.feed("hello"); + let cloned = wd.clone(); + assert_eq!(wd.as_bytes(), cloned.as_bytes()); + assert_eq!(wd.blend_seed(), cloned.blend_seed()); + } +} diff --git a/tests/feed.rs b/tests/feed.rs new file mode 100644 index 0000000..82138e5 --- /dev/null +++ b/tests/feed.rs @@ -0,0 +1,582 @@ +use busywork::{BusyWork, Categories, Intensity}; + +// ════════════════════════════════════════════════════════════════════════════ +// DATA ISOLATION: originals must never be modified after feed + run +// ════════════════════════════════════════════════════════════════════════════ + +#[test] +fn original_u8_untouched() { + let val: u8 = 0xFF; + BusyWork::new(Intensity::Low).allow(Categories::COMPUTE).feed(&val).run(); + assert_eq!(val, 0xFF); +} + +#[test] +fn original_u16_untouched() { + let val: u16 = 12345; + BusyWork::new(Intensity::Low).allow(Categories::COMPUTE).feed(&val).run(); + assert_eq!(val, 12345); +} + +#[test] +fn original_u32_untouched() { + let val: u32 = 42; + BusyWork::new(Intensity::Low).allow(Categories::COMPUTE).feed(&val).run(); + assert_eq!(val, 42); +} + +#[test] +fn original_u64_untouched() { + let val: u64 = 0xDEAD_BEEF_CAFE_BABE; + BusyWork::new(Intensity::Low).allow(Categories::COMPUTE).feed(&val).run(); + assert_eq!(val, 0xDEAD_BEEF_CAFE_BABE); +} + +#[test] +fn original_u128_untouched() { + let val: u128 = u128::MAX; + BusyWork::new(Intensity::Low).allow(Categories::COMPUTE).feed(&val).run(); + assert_eq!(val, u128::MAX); +} + +#[test] +fn original_i32_untouched() { + let val: i32 = -42; + BusyWork::new(Intensity::Low).allow(Categories::COMPUTE).feed(&val).run(); + assert_eq!(val, -42); +} + +#[test] +fn original_i64_untouched() { + let val: i64 = i64::MIN; + BusyWork::new(Intensity::Low).allow(Categories::COMPUTE).feed(&val).run(); + assert_eq!(val, i64::MIN); +} + +#[test] +fn original_f32_untouched() { + let val: f32 = std::f32::consts::PI; + BusyWork::new(Intensity::Low).allow(Categories::COMPUTE).feed(&val).run(); + assert_eq!(val, std::f32::consts::PI); +} + +#[test] +fn original_f64_untouched() { + let val: f64 = std::f64::consts::E; + BusyWork::new(Intensity::Low).allow(Categories::COMPUTE).feed(&val).run(); + assert_eq!(val, std::f64::consts::E); +} + +#[test] +fn original_bool_untouched() { + let val = true; + BusyWork::new(Intensity::Low).allow(Categories::COMPUTE).feed(&val).run(); + assert!(val); +} + +#[test] +fn original_string_untouched() { + let val = String::from("sensitive_data_do_not_modify"); + BusyWork::new(Intensity::Low).allow(Categories::COMPUTE).feed(&val).run(); + assert_eq!(val, "sensitive_data_do_not_modify"); +} + +#[test] +fn original_vec_untouched() { + let val = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + BusyWork::new(Intensity::Low).allow(Categories::MEMORY).feed(&val).run(); + assert_eq!(val, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); +} + +#[test] +fn original_array_untouched() { + let val: [u8; 8] = [0xCA, 0xFE, 0xBA, 0xBE, 0xDE, 0xAD, 0xBE, 0xEF]; + BusyWork::new(Intensity::Low).allow(Categories::COMPUTE).feed(&val).run(); + assert_eq!(val, [0xCA, 0xFE, 0xBA, 0xBE, 0xDE, 0xAD, 0xBE, 0xEF]); +} + +#[test] +fn original_multiple_vars_untouched() { + let a: u32 = 42; + let b = String::from("hello"); + let c = vec![0xAAu8, 0xBB]; + let d: f64 = 3.14; + let e = true; + + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE | Categories::MEMORY) + .feed(&a) + .feed(&b) + .feed(&c) + .feed(&d) + .feed(&e) + .run(); + + assert_eq!(a, 42); + assert_eq!(b, "hello"); + assert_eq!(c, vec![0xAA, 0xBB]); + assert_eq!(d, 3.14); + assert!(e); +} + +// ════════════════════════════════════════════════════════════════════════════ +// PER-CATEGORY SMOKE WITH FEED DATA +// ════════════════════════════════════════════════════════════════════════════ + +fn sample_feed() -> BusyWork { + BusyWork::new(Intensity::Low) + .feed(&42u32) + .feed(&0xDEADBEEFu64) + .feed("test_context_data") + .feed(&vec![0xAAu8, 0xBB, 0xCC, 0xDD]) + .feed(&3.14f64) + .feed(&true) +} + +#[test] +fn feed_compute_no_panic() { + sample_feed().allow(Categories::COMPUTE).run(); +} + +#[test] +fn feed_memory_no_panic() { + sample_feed().allow(Categories::MEMORY).run(); +} + +#[test] +fn feed_filesystem_no_panic() { + sample_feed().allow(Categories::FILESYSTEM).run(); +} + +#[test] +fn feed_registry_no_panic() { + sample_feed().allow(Categories::REGISTRY).run(); +} + +#[test] +fn feed_winapi_no_panic() { + sample_feed().allow(Categories::WINAPI).run(); +} + +#[test] +fn feed_crypto_no_panic() { + sample_feed().allow(Categories::CRYPTO).run(); +} + +#[test] +fn feed_all_categories_no_panic() { + sample_feed().deny(Categories::NETWORK).run(); +} + +// ════════════════════════════════════════════════════════════════════════════ +// INTENSITY LEVELS WITH FEED DATA +// ════════════════════════════════════════════════════════════════════════════ + +#[test] +fn feed_low_compute() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&42u32) + .feed("low_test") + .run(); +} + +#[test] +fn feed_low_memory() { + BusyWork::new(Intensity::Low) + .allow(Categories::MEMORY) + .feed(&42u32) + .feed(&vec![1u8, 2, 3, 4]) + .run(); +} + +#[test] +fn feed_medium_compute() { + BusyWork::new(Intensity::Medium) + .allow(Categories::COMPUTE) + .feed(&42u32) + .feed("medium_test") + .run(); +} + +#[test] +fn feed_medium_memory() { + BusyWork::new(Intensity::Medium) + .allow(Categories::MEMORY) + .feed(&0xCAFEBABEu32) + .run(); +} + +#[test] +fn feed_high_compute() { + BusyWork::new(Intensity::High) + .allow(Categories::COMPUTE) + .feed(&42u32) + .run(); +} + +#[test] +fn feed_high_memory() { + BusyWork::new(Intensity::High) + .allow(Categories::MEMORY) + .feed(&42u32) + .run(); +} + +// ════════════════════════════════════════════════════════════════════════════ +// EDGE CASES +// ════════════════════════════════════════════════════════════════════════════ + +#[test] +fn feed_empty_string() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed("") + .run(); +} + +#[test] +fn feed_empty_vec() { + let empty: Vec = vec![]; + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&empty) + .run(); +} + +#[test] +fn feed_single_byte() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&0xFFu8) + .run(); +} + +#[test] +fn feed_all_zeros() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE | Categories::MEMORY) + .feed(&0u8) + .feed(&0u16) + .feed(&0u32) + .feed(&0u64) + .feed(&0.0f32) + .feed(&0.0f64) + .feed(&false) + .run(); +} + +#[test] +fn feed_max_values() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&u8::MAX) + .feed(&u16::MAX) + .feed(&u32::MAX) + .feed(&u64::MAX) + .feed(&i8::MIN) + .feed(&i16::MIN) + .feed(&i32::MIN) + .feed(&i64::MIN) + .feed(&f64::MAX) + .feed(&f64::MIN) + .run(); +} + +#[test] +fn feed_special_floats() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&f32::NAN) + .feed(&f64::INFINITY) + .feed(&f64::NEG_INFINITY) + .feed(&f64::MIN_POSITIVE) + .feed(&f64::EPSILON) + .run(); +} + +#[test] +fn feed_large_data_64kb() { + let big = vec![0xABu8; 64 * 1024]; + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE | Categories::MEMORY) + .feed(&big) + .run(); +} + +#[test] +fn feed_same_value_repeated() { + let val = 42u32; + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&val) + .feed(&val) + .feed(&val) + .run(); +} + +#[test] +fn feed_256_u8_values() { + let mut builder = BusyWork::new(Intensity::Low).allow(Categories::COMPUTE); + for i in 0u8..=255 { + builder = builder.feed(&i); + } + builder.run(); +} + +#[test] +fn feed_100_u64_values() { + let mut builder = BusyWork::new(Intensity::Low).allow(Categories::COMPUTE); + for i in 0u64..100 { + builder = builder.feed(&i); + } + builder.run(); +} + +#[test] +fn feed_long_string() { + let long = "A".repeat(10_000); + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&long) + .run(); +} + +#[test] +fn feed_unicode_string() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed("Ünïcödé Strïñg 日本語 中文 🎉") + .run(); +} + +// ════════════════════════════════════════════════════════════════════════════ +// BUILDER COMBINATIONS +// ════════════════════════════════════════════════════════════════════════════ + +#[test] +fn feed_with_deny() { + BusyWork::new(Intensity::Medium) + .deny(Categories::NETWORK) + .feed(&42u32) + .feed("context") + .run(); +} + +#[test] +fn feed_with_allow_and_deny() { + BusyWork::new(Intensity::Medium) + .allow(Categories::COMPUTE | Categories::MEMORY | Categories::NETWORK) + .deny(Categories::NETWORK) + .feed(&42u32) + .run(); +} + +#[test] +fn feed_with_jitter_off() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .jitter(false) + .feed(&42u32) + .feed("deterministic") + .run(); +} + +#[test] +fn feed_with_jitter_on() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .jitter(true) + .feed(&42u32) + .run(); +} + +#[test] +fn feed_with_empty_categories() { + BusyWork::new(Intensity::Low) + .allow(Categories::empty()) + .feed(&42u32) + .feed("should be safe even with no categories") + .run(); +} + +#[test] +fn feed_before_allow() { + BusyWork::new(Intensity::Low) + .feed(&42u32) + .allow(Categories::COMPUTE) + .run(); +} + +#[test] +fn feed_after_allow() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&42u32) + .run(); +} + +#[test] +fn feed_interleaved_with_builder_options() { + BusyWork::new(Intensity::Low) + .feed(&1u32) + .allow(Categories::COMPUTE | Categories::MEMORY) + .feed(&2u64) + .deny(Categories::NETWORK) + .feed("interleaved") + .jitter(false) + .feed(&3.14f64) + .run(); +} + +#[test] +fn feed_only_deny_no_allow() { + BusyWork::new(Intensity::Low) + .deny(Categories::NETWORK | Categories::REGISTRY) + .feed(&42u32) + .run(); +} + +// ════════════════════════════════════════════════════════════════════════════ +// TYPE COVERAGE: every FeedWork implementation path +// ════════════════════════════════════════════════════════════════════════════ + +#[test] +fn feed_all_unsigned_integers() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&1u8) + .feed(&2u16) + .feed(&3u32) + .feed(&4u64) + .feed(&5u128) + .feed(&6usize) + .run(); +} + +#[test] +fn feed_all_signed_integers() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&(-1i8)) + .feed(&(-2i16)) + .feed(&(-3i32)) + .feed(&(-4i64)) + .feed(&(-5i128)) + .feed(&(-6isize)) + .run(); +} + +#[test] +fn feed_all_float_types() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&1.0f32) + .feed(&2.0f64) + .run(); +} + +#[test] +fn feed_str_literal_directly() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed("literal") + .run(); +} + +#[test] +fn feed_string_by_ref() { + let s = String::from("owned"); + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&s) + .run(); +} + +#[test] +fn feed_byte_slice_from_vec() { + let v = vec![1u8, 2, 3]; + let slice: &[u8] = &v; + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(slice) + .run(); +} + +#[test] +fn feed_fixed_array() { + let arr: [u8; 16] = [0; 16]; + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&arr) + .run(); +} + +#[test] +fn feed_boxed_value() { + let boxed: Box = Box::new(42); + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&boxed) + .run(); +} + +#[test] +fn feed_boxed_bytes() { + let boxed: Box<[u8]> = vec![1u8, 2, 3].into_boxed_slice(); + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&boxed) + .run(); +} + +#[test] +fn feed_double_reference() { + let val = 42u32; + let ref1 = &val; + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(ref1) + .run(); +} + +// ════════════════════════════════════════════════════════════════════════════ +// WITHOUT FEED: existing behavior unbroken +// ════════════════════════════════════════════════════════════════════════════ + +#[test] +fn no_feed_compute() { + BusyWork::new(Intensity::Low).allow(Categories::COMPUTE).run(); +} + +#[test] +fn no_feed_memory() { + BusyWork::new(Intensity::Low).allow(Categories::MEMORY).run(); +} + +#[test] +fn no_feed_all_categories() { + BusyWork::new(Intensity::Low).deny(Categories::NETWORK).run(); +} + +#[test] +fn no_feed_with_jitter_off() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .jitter(false) + .run(); +} + +// ════════════════════════════════════════════════════════════════════════════ +// NETWORK (separate, slower due to timeouts) +// ════════════════════════════════════════════════════════════════════════════ + +#[test] +#[ignore] +fn feed_network_no_panic() { + BusyWork::new(Intensity::Low) + .allow(Categories::NETWORK) + .feed(&80u16) + .feed("network_context") + .run(); +} diff --git a/tests/robustness.rs b/tests/robustness.rs new file mode 100644 index 0000000..0b72328 --- /dev/null +++ b/tests/robustness.rs @@ -0,0 +1,303 @@ +use busywork::{BusyWork, Categories, FeedWork, Intensity, WorkData}; + +// ════════════════════════════════════════════════════════════════════════════ +// STRESS: rapid sequential calls with varying data +// ════════════════════════════════════════════════════════════════════════════ + +#[test] +fn stress_500_calls_varying_u32() { + for i in 0u32..500 { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .jitter(false) + .feed(&i) + .run(); + } +} + +#[test] +fn stress_200_calls_varying_bytes() { + for i in 0u16..200 { + let data = i.to_ne_bytes(); + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE | Categories::MEMORY) + .feed(&data) + .run(); + } +} + +#[test] +fn stress_100_calls_same_data() { + let data = vec![0xABu8; 256]; + for _ in 0..100 { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&data) + .run(); + } +} + +#[test] +fn stress_50_calls_growing_data() { + for size in 0..50 { + let data = vec![0xCDu8; size * 100]; + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&data) + .run(); + } +} + +// ════════════════════════════════════════════════════════════════════════════ +// EXTREME DATA SIZES +// ════════════════════════════════════════════════════════════════════════════ + +#[test] +fn extreme_1mb_feed() { + let big = vec![0xEFu8; 1024 * 1024]; + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE | Categories::MEMORY) + .feed(&big) + .run(); +} + +#[test] +fn extreme_10000_small_feeds() { + let mut builder = BusyWork::new(Intensity::Low).allow(Categories::COMPUTE); + for i in 0u16..10_000 { + builder = builder.feed(&i); + } + builder.run(); +} + +#[test] +fn extreme_feed_then_empty_categories() { + let big = vec![0xFFu8; 64 * 1024]; + BusyWork::new(Intensity::Ultra) + .allow(Categories::empty()) + .feed(&big) + .run(); +} + +// ════════════════════════════════════════════════════════════════════════════ +// WORKDATA DIRECT API +// ════════════════════════════════════════════════════════════════════════════ + +#[test] +fn workdata_feed_many_types_no_panic() { + let mut wd = WorkData::new(); + wd.feed(&0u8); + wd.feed(&0u16); + wd.feed(&0u32); + wd.feed(&0u64); + wd.feed(&0u128); + wd.feed(&0usize); + wd.feed(&0i8); + wd.feed(&0i16); + wd.feed(&0i32); + wd.feed(&0i64); + wd.feed(&0i128); + wd.feed(&0isize); + wd.feed(&0.0f32); + wd.feed(&0.0f64); + wd.feed(&false); + wd.feed(&true); + wd.feed(""); + wd.feed("hello"); + wd.feed(&String::from("world")); + let v = vec![1u8, 2, 3]; + wd.feed(&v); + let s: &[u8] = &[4, 5, 6]; + wd.feed(s); + wd.feed(&[7u8, 8, 9]); + let boxed: Box = Box::new(42); + wd.feed(&boxed); +} + +#[test] +fn workdata_clone_divergence() { + let mut a = WorkData::new(); + a.feed(&42u32); + let mut b = a.clone(); + b.feed(&99u32); + // After divergence, using both should be safe + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&42u32) + .run(); +} + +#[test] +fn workdata_reuse_across_runs() { + let mut wd = WorkData::new(); + wd.feed(&42u32); + wd.feed("persistent_context"); + + for _ in 0..10 { + let mut builder = BusyWork::new(Intensity::Low).allow(Categories::COMPUTE); + // Re-feed same data (simulating reuse pattern) + builder = builder.feed(&42u32); + builder = builder.feed("persistent_context"); + builder.run(); + } +} + +// ════════════════════════════════════════════════════════════════════════════ +// CUSTOM TYPE: user implements FeedWork for their own struct +// ════════════════════════════════════════════════════════════════════════════ + +struct GameState { + player_x: f32, + player_y: f32, + health: u32, + score: u64, +} + +impl FeedWork for GameState { + fn write_work_bytes(&self, out: &mut Vec) { + self.player_x.write_work_bytes(out); + self.player_y.write_work_bytes(out); + self.health.write_work_bytes(out); + self.score.write_work_bytes(out); + } +} + +#[test] +fn custom_type_feed() { + let state = GameState { + player_x: 100.5, + player_y: -200.3, + health: 75, + score: 12345678, + }; + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&state) + .run(); +} + +#[test] +fn custom_type_original_untouched() { + let state = GameState { + player_x: 100.5, + player_y: -200.3, + health: 75, + score: 12345678, + }; + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&state) + .run(); + assert_eq!(state.player_x, 100.5); + assert_eq!(state.player_y, -200.3); + assert_eq!(state.health, 75); + assert_eq!(state.score, 12345678); +} + +// ════════════════════════════════════════════════════════════════════════════ +// MIXED CATEGORIES WITH FEED +// ════════════════════════════════════════════════════════════════════════════ + +#[test] +fn feed_compute_and_memory() { + BusyWork::new(Intensity::Medium) + .allow(Categories::COMPUTE | Categories::MEMORY) + .feed(&42u32) + .feed(&vec![0xAB; 128]) + .run(); +} + +#[test] +fn feed_compute_and_filesystem() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE | Categories::FILESYSTEM) + .feed(&42u32) + .run(); +} + +#[test] +fn feed_compute_and_crypto() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE | Categories::CRYPTO) + .feed(&42u32) + .feed("crypto_context") + .run(); +} + +#[test] +fn feed_memory_and_crypto() { + BusyWork::new(Intensity::Low) + .allow(Categories::MEMORY | Categories::CRYPTO) + .feed(&0xCAFEBABEu32) + .run(); +} + +#[test] +fn feed_all_except_network() { + BusyWork::new(Intensity::Medium) + .deny(Categories::NETWORK) + .feed(&42u32) + .feed("full_context_test") + .feed(&vec![1u8, 2, 3, 4, 5]) + .feed(&3.14f64) + .run(); +} + +// ════════════════════════════════════════════════════════════════════════════ +// REGRESSION: specific patterns that could trigger bugs +// ════════════════════════════════════════════════════════════════════════════ + +#[test] +fn regression_single_zero_byte_feed() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE | Categories::MEMORY) + .feed(&0u8) + .run(); +} + +#[test] +fn regression_feed_after_empty_feed() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&Vec::::new()) + .feed(&42u32) + .run(); +} + +#[test] +fn regression_many_empty_feeds_then_real() { + let mut builder = BusyWork::new(Intensity::Low).allow(Categories::COMPUTE); + for _ in 0..100 { + builder = builder.feed(""); + } + builder = builder.feed(&42u32); + builder.run(); +} + +#[test] +fn regression_alternating_empty_and_data() { + let mut builder = BusyWork::new(Intensity::Low).allow(Categories::COMPUTE); + for i in 0u32..50 { + builder = builder.feed(""); + builder = builder.feed(&i); + builder = builder.feed(&Vec::::new()); + } + builder.run(); +} + +#[test] +fn regression_max_u64_feed() { + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE) + .feed(&u64::MAX) + .run(); +} + +#[test] +fn regression_all_ones_large() { + let data = vec![0xFFu8; 8192]; + BusyWork::new(Intensity::Low) + .allow(Categories::COMPUTE | Categories::MEMORY) + .feed(&data) + .run(); +}