Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **C port of the `BStackChunk` chunked-view operations, as `bstack_slice_*` functions over a `bstack_slice_t` + `chunk_len` (no `bstack_chunk_t`; `s.len` must be a whole multiple of `chunk_len`).** Brings the mostly already-released Rust `BStackChunk` surface to C: geometry `bstack_slice_same_chunk_phase`, `adjacent_to`, `overlaps`, `merge`, `merge_adjacent` (the last four take `chunk_len == 0` for the plain-slice form); structural `split_at` (byte), `split_chunk_at` (chunk index), and the `bstack_slice_nth_chunk` macro (`get`, no bounds check); read-only `search` (bsearch-style), `select` (partition point), and `is_sorted` (no feature flag — O(log n) probing via `bstack_get_batched_gen` under `BSTACK_FEATURE_ATOMIC`, one whole-region `bstack_get` otherwise); and mutating `reverse_chunks`, `rotate_left`/`rotate_right`, `sort`/`sort_partial`, `partition`/`partition_partial` (select-nth), each one crash-atomic `bstack_process` call under `BSTACK_FEATURE_SET` + `BSTACK_FEATURE_ATOMIC`. Callbacks follow the ctx-free `qsort`/`bsearch` convention; `sort` uses `qsort` (not guaranteed stable, unlike Rust's `sort_by`) and `partition` is an in-place quickselect; the `_partial` pair is the bounded-memory out-of-core form for regions too large for one buffer, per-step crash-atomic and re-runnable rather than atomic as a whole.
- **`SegregatedBStackAllocator::coalesce` (`alloc` + `set` + `atomic`, Rust only): merge physically-adjacent free blocks.** Freed blocks only ever return to their own class list, so adjacent free blocks never combine and no oversized request can reuse a contiguous free run; `coalesce()` fuses them and rebuilds the free lists, returning the number of blocks merged into a neighbour. Unlike the `unsafe`, quiescence-required `recover`, it is **safe** — the whole scan-and-rewrite runs as one `BStack::inplace_gen` batch, requiring no allocator-level lock.
- **`SegregatedBStackAllocator::coalesce` (Rust) / `segregated_bstack_allocator_coalesce` (C), `alloc` + `set` + `atomic` / `BSTACK_FEATURE_SET` + `BSTACK_FEATURE_ATOMIC`: merge physically-adjacent free blocks.** Freed blocks only ever return to their own class list, so adjacent free blocks never combine and no oversized request can reuse a contiguous free run; `coalesce` fuses them and rebuilds the free lists, reporting the number of blocks merged into a neighbour. Unlike the quiescence-required `recover`, it takes no allocator-level lock and the whole scan-and-rewrite runs as one `inplace_gen` batch (in Rust it is therefore a **safe** method, where `recover` is `unsafe`).
- **`BStackGuardedSlice` gains a full I/O surface derived from whole-block `decode`/`encode` (`guarded` feature, Rust only; `set` for writes, `set` + `atomic` for in-place updates).** The transform hooks are now whole-block: `decode` maps the raw stored bytes to the apparent (decoded) bytes and `encode` the reverse — both default to identity (`Cow::Borrowed`, no allocation) — so a transforming guard (encryption, compression) implements only `len`, `raw_block`, `decode`, and `encode`. Reads: `read`/`read_into`/`read_range`/`read_range_into` plus the scan family `get`/`contains`/`starts_with`/`ends_with`/`find`/`rfind`/`position`/`rposition` (each reads and decodes the whole block). Whole-block writes (`set`): `write`/`copy_from_slice`/`zero`/`fill`/`fill_with`, which `encode` a full-block replacement. In-place read-modify-writes (`set` + `atomic`): `write_range`/`zero_range`/`process`/`copy_within`, each one crash-atomic `BStack::process_gen` that decodes, mutates, re-encodes, and writes under a single held lock, aborting with no write on a `decode`/`encode`/bounds error or an `encode` that changes the block length. Coordinate and `BStack` accessors `start`/`end`/`range`/`as_range`/`stack`, plus the `BStackGuardedSliceSubview` conveniences `head`/`tail`, round out parity with `BStackSlice`.
- **`BStackAtomicGuardedSlice` raw-level atomics: `swap`, `cas_on`, `cas_on_ne`, `cas_on_masked` (`guarded` + `set` + `atomic`, Rust only).** Each is a single crash-atomic operation on the raw block, delegating to the underlying `BStackSlice::swap`/`cas_on`/`cas_on_ne`/`cas_on_masked`. They operate on the **raw** stored bytes and bypass `decode`/`encode` (so `expected`/`new_bytes` are in raw form); for a pass-through guard the raw and apparent bytes coincide.

### Changed

Expand All @@ -30,6 +32,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`GhostTreeBstackAllocator` version bumped (`alloc` + `set`; Rust and C): magic `ALGT\x00\x01\x03\x00` → `ALGT\x00\x01\x04\x00`.** Marks the `align_up_len` / `algt_align_up_len` overflow fix below, so a file can be attributed to a writer that rejects an unalignable length rather than one that wraps it.
- **`BStack`'s `Hash` now hashes the raw fd (Unix) / handle (Windows) instead of the instance address (Rust only).** Same per-live-instance uniqueness, still consistent with the pointer-identity `PartialEq`, but stable when the value moves. Platforms that are neither Unix nor Windows keep the address hash. See `algos/EQUALITY.md`.
- **`FirstFitBStackAllocator` version bumped (`alloc` + `set`; Rust and C): magic `ALFF\x00\x01\x03\x00` → `ALFF\x00\x01\x04\x00`.** No layout change; the patch byte records the writer, so a file can be attributed to a build carrying this cycle's batched free-list writes: `add_to_free_list`'s probe-coalesce-prepend and the alloc/realloc carve each committing as one crash-atomic unit, with the on-disk sizes and free-list pointers validated before use.
- **`BStackGuardedSlice::write` now overwrites the whole apparent block instead of writing a `min(len, data.len())` prefix (`guarded` + `set`, Rust only).** It passes all of `data` through `encode` and writes the result — the whole-block replacement the codec model requires — then fires `on_write(0, data.len())`. A guard that relied on the old prefix-truncation now writes all of `data`, and errors if the encoded result exceeds the raw block.

### Deprecated

- **`BStackGuardedSlice::pre_read`, `post_read`, `pre_write`, `post_write` renamed; removal in 0.5.0 (`guarded` feature, Rust only).** `post_read` → `decode` and `pre_write` → `encode` (the whole-block transform pair); `pre_read` → `on_read` and `post_write` → `on_write` (the observe/deny hooks). The hook offset convention is unified to **relative to the start of the slice**: `post_write` was already relative, but `pre_read`'s offset was absolute within the `BStack`, so `on_read` receives a relative offset where `pre_read` received an absolute one. The new hooks bridge to the deprecated ones by default — the `on_read` bridge re-adds the slice start, so an implementor overriding only `pre_read` still receives the absolute offset it expects — so a guard overriding only the old hooks keeps working unchanged until removal.

### Fixed

Expand Down
48 changes: 48 additions & 0 deletions PLANNED.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,3 +395,51 @@ The flag is named for it. On a protected stack every checked call pays a relaxed

- **Named modes or an axis triple.** A `{ read, write, truncate }` triple of authorities is more expressive and no larger, at the cost of admitting nonsense (`read: none, write: any`). The enum is proposed because the curated eight are what callers want and a one-byte discriminant keeps the table compact.
- **What an allocator may do inside a `Prot` range.** Incomparability settles one direction — a guard holder cannot reach an `Alloc` range — but not the other. A caller may mark its own allocation `Prot` and then free it, leaving a mode over bytes the allocator is about to hand to someone else. Either `dealloc` resets the reclaimed range to `All`, which means an allocator overriding a mode it otherwise cannot touch, or the protection outlives the allocation and poisons the block for its next owner.

---

## `BStackGuardedUnit` and `BStackGuardedBuilder` — composable transform units for `guarded` (0.5.0)

**Feature flag:** `guarded`.
**Breaking change:** No

### Motivation

The whole-block codec model (`decode`/`encode`) already makes layering transforms — compress-then-encrypt, checksum-then-encode — a plain function composition: `decode = inner.decode ∘ outer.decode`, `encode = outer.encode ∘ inner.encode`. It works today with no new API: an implementor writes a guard whose `decode`/`encode` chain the layers by hand. That manual pattern is enough — this entry is convenience, not a missing capability.

What it removes is repetition. Every downstream that stacks transforms hand-rolls a bespoke guard, re-deriving the composition order each time and, in the naive form, allocating a fresh buffer at every layer.

A "middle" layer is not a guard: it binds no storage (`len`/`raw_block`) and has no side hooks — it is only a transform. Composition therefore operates on the transform half alone, over one shared storage binding. Making that first-class means naming the transform as its own unit and giving a builder that stacks units and attaches the result to a `BStackSlice`.

### Design

A **unit** is a single storage-agnostic transform — an `encode`/`decode` pair, with no `len`/`raw_block`/hooks:

```rust
pub trait BStackGuardedUnit {
/// inner (toward storage) bytes -> outer (toward caller) bytes
fn decode<'d>(&self, data: &'d [u8]) -> io::Result<Cow<'d, [u8]>>;
/// outer bytes -> inner bytes
fn encode<'d>(&self, data: &'d [u8]) -> io::Result<Cow<'d, [u8]>>;
}
```

A **builder** accumulates units from storage outward and binds them to a slice. It is monadic — each `then` returns a new builder carrying one more layer — so a pipeline reads in storage-to-caller order and the composition order stays implicit:

```rust
let guard = BStackGuardedBuilder::over(slice) // innermost = closest to storage
.then(Encrypt::new(key)) // raw <-> compressed
.then(Compress::default()) // compressed <-> plaintext
.build(); // : impl BStackGuardedSlice
// guard.decode = Compress.decode ∘ Encrypt.decode
// guard.encode = Encrypt.encode ∘ Compress.encode
```

`build()` yields a `BStackGuardedSlice` whose `len`/`raw_block` come from the bound slice, whose `decode`/`encode` fold the units in the two opposite orders, and whose `on_read`/`on_write` fire once for the whole stack.

### Open questions

- **Allocation between layers.** The naive fold allocates a `Vec` per unit (each `Cow::Owned`). Users that care run the layers through a reused scratch/ring buffer instead. The builder should offer a buffer-reusing fold — a scratch pair the fold ping-pongs between, or an in-place unit variant — without forcing every unit author to manage buffers. A length-changing unit (compression) complicates a fixed scratch.
- **Static vs dynamic stacks.** A tuple/HList builder monomorphizes and inlines the fold with zero per-layer dispatch but fixes the layer count at the type level; a `Vec<Box<dyn BStackGuardedUnit>>` allows runtime-assembled pipelines at the cost of a virtual call and a heap indirection per layer. Pick the static form as the default and let a `Box<dyn>` unit holding a `Vec` cover the dynamic case, or offer both.
- **Length bookkeeping.** For a length-changing stack `len()` (apparent) must be derived, and the atomic in-place methods (`write_range`/`process`/…) do not apply — they require `encode` to preserve the raw block length. The builder should surface whether the composed stack is length-preserving, so those methods are available exactly when every unit is.
- **Relationship to the deprecation question.** If `BStackTransaction` subsumes cross-boundary atomicity and `guarded` is reduced to byte transformation (see "`guarded` semantics under `BStackTransaction`"), the unit/builder *is* that reduced core — the transform surface without the storage/atomicity trait machinery. These entries should be resolved together.
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -885,19 +885,22 @@ The `alloc` feature adds typed region management over a `BStack` payload.

### Region handle design

The `alloc` feature provides four distinct handle types for different roles:
The `alloc` feature provides four distinct handle types for different roles; the `guarded` feature adds a fifth, `BStackGuardedSlice`, for transparent I/O interception:

| Type | Carries | Copy | I/O | Alloc ops |
|---------------------------|--------------|------|----------|-----------|
| `BStackRange` | nothing | yes | no | no |
| `BStackOwnedSlice<'a, A>` | `&'a A` | no | via view | yes |
| `BStackSlice<'a>` | `&'a BStack` | no | yes | no |
| `BStackChunk<'a>` | `&'a BStack` | no | yes | no |
| Type | Carries | Copy | I/O | Alloc ops |
|-----------------------------|--------------|------|----------|-----------|
| `BStackRange` | nothing | yes | no | no |
| `BStackOwnedSlice<'a, A>` | `&'a A` | no | via view | yes |
| `BStackSlice<'a>` | `&'a BStack` | no | yes | no |
| `BStackChunk<'a>` | `&'a BStack` | no | yes | no |
| `BStackGuardedSlice<'a, A>` | `&'a BStack` | no | yes | no |

`BStackOwnedSlice` is non-`Copy` and non-`Clone`: an allocation has exactly one owner. Obtaining an I/O view via `as_slice()` or `as_slice_mut()` ties the view's lifetime to the borrow of the owned slice, preventing it from outliving the handle. `BStackSlice` is non-`Copy` so that `write*(&mut self)` provides single-writer exclusivity; it is `Clone` for explicit second views.

`BStackChunk` sits at the same semantic position as `BStackSlice` — same `Carries`/`Copy`/`I/O`/`Alloc ops` columns, same non-`Copy`-but-`Clone` rationale — it is simply a `BStackSlice` with a fixed stride layered on top (see "`BStackChunk<'a>` — fixed-stride chunked view" below). It is not an iterator itself and has no allocator operations of its own.

`BStackGuardedSlice` also sits at the same semantic position as `BStackSlice` — same `Carries`/`Copy`/`I/O`/`Alloc ops` columns — but it is a **trait** (`guarded` feature), not a concrete type: an implementor wraps a `BStackSlice` and intercepts I/O through `encode`/`decode`/`on_read`/`on_write` hooks. It owns no region and has no allocator operations of its own; see the region-handle-types section above and the module docs.

### `BStackAllocator` trait

A trait for types that own a `BStack` and manage contiguous byte regions
Expand Down
Loading
Loading