Polishing 0.2#3
Merged
Merged
Conversation
- 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.