Skip to content

Polishing 0.2#3

Merged
tgockel merged 19 commits into
trunkfrom
polishing-0.2
Apr 29, 2026
Merged

Polishing 0.2#3
tgockel merged 19 commits into
trunkfrom
polishing-0.2

Conversation

@tgockel
Copy link
Copy Markdown
Owner

@tgockel tgockel commented Apr 29, 2026

No description provided.

tgockel added 19 commits April 29, 2026 10:46
- Replace `x.len() == 0` with `x.is_empty()` in test.rs
- Qualify the `[WriteBuf]` intra-doc link in `Truncated`'s docs as
  `[WriteBuf](crate::WriteBuf)` so rustdoc resolves it from inside
  the `truncated` module

This makes the workspace clean under `-D warnings` so we can start
enforcing it in CI.
The Rust crate-level attribute syntax is `#![no_std]`, not `!#[no_std]`.
- Add `RUSTDOCFLAGS: -D warnings` to the docs step and switch to
  `cargo doc --no-deps` so we only validate this crate's docs.
- Pass `--all-targets --all-features -- -D warnings` to clippy so
  any future warning fails the build.
- Bump `actions/checkout@v3` to `@v4`.
Centralize lint configuration in `Cargo.toml` so individual files no
longer need crate-level `#![deny(...)]` attributes. This required a few
minor changes:

- `#[must_use]` on the `WriteBuf` accessors and `Truncated::get`.
- `#[allow(clippy::used_underscore_items, reason = "...")]` on the
  three public methods that delegate to the `_finish_with` /
  `_write` helpers, since the leading underscore is intentional
  to disambiguate them from the public API.
- `#[allow(clippy::doc_link_with_quotes, reason = "...")]` on the
  README include, because the readme uses real Markdown links
  with quoted Unicode names like `["U+200D" "Zero Width Joiner"]`
  that clippy mistakes for malformed intra-doc links.
- Restructured the doc-comments on `WriteBuf::finish`,
  `finish_with`, `finish_with_or`, and the `fmt::Write` impl to
  use `# Errors` (the convention) for the `Err` case.
- Replaced the test-only `pub` helpers with `fn` (private),
  restricted `utf8_char_width` to `pub(crate)`, dropped
  `.iter()` on `for ... in TEST_CASES`, swapped
  `..input.len() + 1` for `..=input.len()`, and elided the
  redundant lifetime in `impl fmt::Write for WriteBuf<'_>`.
- Add `documentation = "https://docs.rs/fmtbuf"`. While docs.rs
  is the de facto default, an explicit field shows up in
  tooling and crates.io listings.
- Bump the maintenance badge from `experimental` to
  `passively-maintained`. The crate is feature-stable for the
  0.2.0 release; new versions will be bug fixes and small
  improvements.
These all come for free since the inner `&'a str` already implements
them. They make `Truncated` usable in tests (assert_eq! on results),
hash maps and sets, and pattern-match guards without callers having to
unwrap the inner string first.
The inherent accessors on `Truncated` now mirror the methods on
`TruncatedResultExt`, so callers see the same vocabulary whether they
are holding a `Truncated` directly or a `Result<&str, Truncated<'_>>`:

    match writer.finish_with("...") {
        Ok(s) => s,
        Err(e) => {
            eprintln!("truncated at {} bytes", e.written_len());
            e.written()
        }
    }

`get` was a poor name in retrospect: `Truncated` is an error type
that *carries* the validly-written string, not a string-like value.
Lets users `dbg!()` and log a `WriteBuf`. The implementation prints
the position, total capacity (`target.len()`), reserve, truncated
flag, and the validly-written portion as a `&str`. The raw target
bytes are deliberately not printed: per the type docs, the bytes
beyond `position` may be uninitialized sentinels that are not
meaningful to the caller.
4 new tests:

- `zero_length_buffer_does_not_panic`: exercises every public entry
  point on a `WriteBuf` constructed with a zero-length slice. Each
  must report truncation rather than panicking.
- `reserve_larger_than_buffer_through_finish_with`: pins down the
  observable behavior when `reserve > target.len()` -- writes are
  rejected, but `finish_with` and `finish_with_or` still place the
  truncated suffix (using the documented "last bytes that start at
  a valid UTF-8 boundary" rule when the suffix is too long).
- `truncation_flag_survives_subsequent_writes`: after a write hits
  truncation, a second `write_str` must immediately `Err` and leave
  `position` unchanged; `finish_with_or` then uses the truncated
  suffix.
- `debug_format_describes_state` (std-only, since `format!` needs
  `alloc`): asserts the new `WriteBuf` `Debug` impl shows the
  user-visible state and never the raw target bytes (which can be
  uninitialized sentinels).
The new `tests/proptest.rs` exercises the public API with random inputs
and buffer sizes 512 times, checking these properties:

- `finish` never panics and produces valid UTF-8 (the type system
  already enforces this, but assert it directly via
  `core::str::from_utf8` on the underlying bytes).
- `finish` results are always a prefix of the input string, and the
  entire input round-trips when `is_truncated()` is false.
- All length accessors agree: `written().len()`,
  `written_bytes().len()`, `position()`, and (when the result is `Err`)
  `Truncated::written_len()`.
- `finish_with` never panics, produces valid UTF-8, and the output never
  exceeds the buffer size.
- `finish_with` `Ok` results always end with the requested suffix.
- After a sequence of `write_str` calls of arbitrary lengths,
  `position()` matches `written().len()` and the bytes form valid
  UTF-8.
Adds an `[Unreleased]` placeholder so post-0.2.0 work has a place
to land, and expands the `## [0.2.0]` Added section to cover the
items added during the release-readiness pass:

- `Truncated` derives `Clone`, `Copy`, `PartialEq`, `Eq`, `Hash`.
- `Truncated::written()` (renamed from the never-released `get`)
  and `Truncated::written_len()` for parity with
  `TruncatedResultExt`.
- `fmt::Debug` for `WriteBuf`.
Catches the case where a contributor inadvertently uses a feature
newer than the declared MSRV. The job reads the `rust-version` field
out of `Cargo.toml`, installs that exact toolchain, and builds the
crate against both feature configurations (`default` and
`--no-default-features`).

`cargo build` rather than `cargo test` here, because dev-dependencies
(proptest, clap) may bump their MSRVs ahead of ours. Library MSRV
applies to consumers, not test tooling.
The fuzz harness exercises both `finish` and `finish_with` /
`finish_with_or` against arbitrary `(buf_len, reserve, input,
finish_with, truncate_with)` tuples, asserting that:

- The written portion is always valid UTF-8.
- The written byte length never exceeds the original buffer size.
- No panic under any input

Run on demand with:

    cargo +nightly fuzz run finish_with -- -max_total_time=60
Gate every PR that modifies `src/`, `fuzz/`, or `Cargo.toml` with a
short fuzz session. PR-time is the right place for this -- after a
bug-introducing change merges, fuzz only confirms it broke; before
merge, fuzz can block it. `push: trunk` is kept as a backstop for
direct pushes that bypass review, and `workflow_dispatch` is wired
up for ad-hoc longer runs (e.g. before tagging a release).

The corpus is cached under a shared prefix so PRs and trunk runs
all build on whatever corpus the most recent run left behind.
Per-run `key` (`github.run_id`) ensures every run writes a fresh
entry even when the prefix matches a previous one. Crash inputs
under `fuzz/artifacts/` are uploaded on failure for triage.

No `schedule:` -- the fuzzer offers no signal when the code hasn't
changed, so a periodic run would just burn CI minutes. Path
filtering keeps docs-only and unrelated PRs from paying the cost.
Both modules currently contain zero `unsafe`. Any future edit that
tries to add `unsafe` to either module fails the build rather than
slipping in unnoticed.
The previous wording said "there is only one `unsafe` block".
Strictly there are three `unsafe { ... }` blocks in `src/lib.rs`,
all calling the single helper `from_utf8_expect`. The
user-facing claim ("one bypass") was still true, but the wording
no longer matched the code.

The new text describes what is actually there: one `unsafe fn`
that performs the bypass, an invariant upheld by `core::fmt::Write`
and verified in debug builds, and `#![forbid(unsafe_code)]` on
the sibling modules so the unsafe surface cannot grow accidentally.
After the MSRV CI job was added, a contributor with a recent
toolchain can write code that compiles locally but fails the
1.81 build on CI. Mention the `rust-version` field in
`Cargo.toml` as the source of truth and call out that CI enforces
it, so the failure mode is documented rather than surprising.
Three small accessors that round out the inspection and reuse
surface of `WriteBuf`:

- `capacity` returns the size of the target buffer (`target.len()`),
  the constant counterpart to `position`.
- `remaining` returns the number of bytes still available for
  `write_str`. Truncation-aware: returns `0` when the buffer is
  marked truncated, since further writes via `core::fmt::Write`
  would error immediately. Saturates when `reserve > capacity`
  rather than underflowing.
- `clear` resets `position` and the `truncated` flag so the buffer
  can be reused for a new formatting attempt. The configured
  `reserve` is preserved (it's a config setting, not per-write
  state).
The existing `finish_with` target exercises the suffix-placement
path (one `write_str` then `finish_with*`). The new
`write_sequence` target focuses on the orthogonal axis: a random
sequence of `write_str`, `clear`, and `set_reserve` calls,
asserting type-wide invariants after every operation. This
catches state-corruption bugs that only surface when truncation
hits midstream and the caller continues writing or reconfigures
the reserve, which the suffix target cannot reach.

Invariants enforced after each op:

- Once truncated, every subsequent `write_str` must `Err`.
- `capacity()` is fixed to the buffer length.
- `position()` matches `written().len()` and `written_bytes().len()`.
- `written_bytes()` is always valid UTF-8.
- `remaining()` matches the public formula
  `truncated ? 0 : capacity - position - reserve` (saturating).
- `clear()` resets position and the truncated flag.

CI: switched the `fuzz.yml` workflow to a matrix over
`[finish_with, write_sequence]` so both run in parallel on PRs
that touch fuzz-relevant code. Per-target corpus caching
(`fuzz/corpus/<target>`) means the targets do not share state.
The default per-target time is 150 s; previously it was 300 s
total for the single target.
@tgockel tgockel merged commit 970605e into trunk Apr 29, 2026
4 checks passed
@tgockel tgockel deleted the polishing-0.2 branch April 29, 2026 19:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant