name = "compact_str"
description = """
ETNA workload for the Rust `compact_str` crate (CompactString, an inline+heap
SSO string). Each variant re-introduces one historical bug-fix into a fresh
patched branch and pairs it with a framework-neutral property, four PBT
adapters, and a deterministic witness test.
"""
language = "rust"
strategies = ["proptest", "quickcheck", "crabcheck", "hegel"]
crate = "compact_str"
base_commit = "a9f7181fcc2b1c8d0a765e5aead17f0403447144"

[[tasks]]
mutations = ["try_reserve_overflow_silent_ae5f2bc_1"]

[tasks.source]
repo = "https://github.com/ParkMyCar/compact_str"
commits = ["ae5f2bc02261c7b28b8448e1e26e9b6eaf0191e0"]
commit_subjects = ["Check for overflow in `reserve()`"]
summary = "`Repr::reserve(additional)` computed `len + additional` with wrapping `usize` arithmetic, so an additive overflow produced a tiny capacity that pretended to fit. The fix replaces the addition with `checked_add` and propagates the overflow as `ReserveError`, letting `try_reserve` return `Err` instead of silently under-allocating."

[tasks.injection]
kind = "patch"
patch = "patches/try_reserve_overflow_silent_ae5f2bc_1.patch"
files = ["compact_str/src/repr/mod.rs"]
locations = [{ file = "compact_str/src/repr/mod.rs", line = 224, symbol = "Repr::reserve" }]

[tasks.bug]
short_name = "try_reserve_overflow_silent"
invariant = "`CompactString::try_reserve(additional)` must return `Err(ReserveError)` whenever `self.len() + additional` would overflow `usize`, rather than silently wrapping the capacity computation. Downstream, the wrapped value lets `reserve` claim a tiny allocation is sufficient and return `Ok(())`, which means any subsequent write operation keyed on the requested size walks past the allocation."
how_triggered = "the pre-ae5f2bc body computes `let needed_capacity = len + additional;`. In release builds `usize` addition wraps silently, so `needed_capacity` becomes a small value and the function returns `Ok(())`. The fix replaces the expression with `len.checked_add(additional).ok_or(ReserveError(()))?`. `case_from_nonempty_max` uses `len=1, additional=usize::MAX` → the wrap produces `0`, which trivially fits. `case_from_16char_near_max` uses `len=16, additional=usize::MAX-8` → wrap produces `7`, same story. The property function discards inputs that do not overflow, so framework-generated cases only ever exercise the true overflow branch."

[[tasks.tasks]]
property = "TryReserveOverflowReturnsErr"
witnesses = [
  { test_fn = "witness_try_reserve_overflow_returns_err_case_from_nonempty_max" },
  { test_fn = "witness_try_reserve_overflow_returns_err_case_from_16char_near_max" },
]

[[tasks]]
mutations = ["retain_leaves_stale_len_on_panic_042b64a_1"]

[tasks.source]
repo = "https://github.com/ParkMyCar/compact_str"
commits = ["042b64a8a2ff225e9485d7676a53a503ba467d6f"]
commit_subjects = ["fix: retain not set len if predicate panics (#413)"]
prs = [413]
summary = "`CompactString::retain` only called `self.set_len(dest_idx)` at the end of the loop. If the user predicate panicked, the unwind bypassed that call and left the string reporting its original length and bytes, diverging from `std::string::String::retain`'s unwind-safety contract. The fix wraps the loop in a `SetLenOnDrop` guard."

[tasks.injection]
kind = "patch"
patch = "patches/retain_leaves_stale_len_on_panic_042b64a_1.patch"
files = ["compact_str/src/lib.rs"]
locations = [{ file = "compact_str/src/lib.rs", line = 1217, symbol = "CompactString::retain" }]

[tasks.bug]
short_name = "retain_leaves_stale_len_on_panic"
invariant = "if the predicate passed to `CompactString::retain` panics, the string's observable state after the unwind must match `std::string::String::retain`'s guarantee: the bytes prior to the panic are retained (and the length reflects exactly those bytes), and the trailing bytes beyond the panic point are dropped from both view and length. Any other outcome leaves the CompactString holding stale length / bytes that can be re-exposed on a subsequent operation."
how_triggered = """
the pre-042b64a body tracks `dest_idx`/`src_idx` as locals and only calls `self.set_len(dest_idx)` as the last statement of the function. When `predicate` panics, the unwind skips the `set_len` call entirely, so the CompactString keeps its original length and its original bytes past the retained prefix. The fix wraps the loop in a `SetLenOnDrop` guard that runs `set_len` during unwind. `case_panic_mid_ascii` panics after two retained ASCII chars (expected post-condition: `len=2`, first two bytes intact, third byte unreachable; buggy: `len=6`, original "abcdef" still visible). `case_panic_first_char` panics before any retention (expected: `len=0`; buggy: `len=6`).
"""

[[tasks.tasks]]
property = "RetainPanicPreservesUtf8"
witnesses = [
  { test_fn = "witness_retain_panic_preserves_utf8_case_panic_mid_ascii" },
  { test_fn = "witness_retain_panic_preserves_utf8_case_panic_first_char" },
]
