{"id":51130134,"url":"https://github.com/williamwutq/bllist","last_synced_at":"2026-06-25T11:30:48.974Z","repository":{"id":352581622,"uuid":"1214876042","full_name":"williamwutq/bllist","owner":"williamwutq","description":"Durable, crash-safe, checksummed block-based linked list allocators stored in a single file","archived":false,"fork":false,"pushed_at":"2026-06-16T05:55:02.000Z","size":386,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-16T07:50:09.401Z","etag":null,"topics":["data","data-storage","data-structure","database","file-based","linkedlist"],"latest_commit_sha":null,"homepage":"https://crates.io/crates/bllist","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/williamwutq.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-19T07:01:19.000Z","updated_at":"2026-06-16T05:55:06.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/williamwutq/bllist","commit_stats":null,"previous_names":["williamwutq/bllist"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/williamwutq/bllist","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/williamwutq%2Fbllist","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/williamwutq%2Fbllist/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/williamwutq%2Fbllist/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/williamwutq%2Fbllist/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/williamwutq","download_url":"https://codeload.github.com/williamwutq/bllist/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/williamwutq%2Fbllist/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34773841,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-25T02:00:05.521Z","response_time":101,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["data","data-storage","data-structure","database","file-based","linkedlist"],"created_at":"2026-06-25T11:30:48.335Z","updated_at":"2026-06-25T11:30:48.963Z","avatar_url":"https://github.com/williamwutq.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# bllist\n\nDurable, crash-safe, checksummed block-based linked list allocators stored in a single file.\n\n`bllist` builds on [`bstack`](https://crates.io/crates/bstack) to provide persistent linked lists backed by fixed-size or variable-size blocks. Every block carries a CRC32 checksum; writes flush durably to disk before returning; and the file survives unclean shutdowns through automatic orphan recovery on the next open.\n\n[![Crates.io](https://img.shields.io/crates/v/bllist)](https://crates.io/crates/bllist)\n[![Docs.rs](https://img.shields.io/docsrs/bllist)](https://docs.rs/bllist)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)\n\n---\n\n## Features\n\n- **Four allocator types** — singly-linked (`FixedBlockList`, `DynamicBlockList`) and doubly-linked (`FixedDblList`, `DynamicDblList`) variants for fixed and variable-size records\n- **Bidirectional iteration** — `FixedDblIter` and `DynDblIter` implement `DoubleEndedIterator`, enabling forward, reverse (`.rev()`), and interleaved front/back traversal; the singly-linked variants provide forward-only `FixedIter` / `DynIter`\n- **O(1) push/pop at both ends** — doubly-linked types store a tail pointer and support `push_back` / `pop_back` in addition to `push_front` / `pop_front`\n- **CRC32 integrity** — every block is checksummed; corruption is detected on read\n- **Crash safety** — all mutations are durable before returning; orphaned blocks are recovered on the next `open`; doubly-linked lists also rebuild the tail pointer on recovery\n- **Power-of-two block sizes** — dynamic blocks have a total on-disk footprint that is always a power of two, enabling splitting and coalescing\n- **Splitting** — when a free block is larger than needed (within `MAX_SPLIT` = 3 levels), it is halved and the spare half returned to its bin, reducing wasted space\n- **Coalescing on open** — adjacent free blocks whose combined size is a power of two are merged into a single larger block, fighting long-term fragmentation\n- **Tail-block shrink** — freeing the last block in the file pops it from the BStack instead of adding it to the free list, keeping the file compact in sequential push/pop workloads\n- **Zero-copy reads** — `read_into`, `pop_front_into`, and `pop_back_into` fill a caller-supplied buffer directly from the file\n- **Async I/O** — `AsyncFixedBlockList` and `AsyncDynamicBlockList` wrappers offload every blocking call to Tokio's thread pool; enable with `features = [\"async\"]`\n- **Thread-safe** — `Send + Sync`; concurrent reads are efficient via `pread` on Unix/Windows\n- **Valid BStack files** — all list types produce valid `bstack` files; the BStack header and crash-recovery semantics are inherited for free\n- **Cross-type protection** — all four list types use distinct file magics (`\"BLLS\"`, `\"BLLD\"`, `\"BLDF\"`, `\"BLDD\"`) and cannot open each other's files\n\n---\n\n## Quick start\n\nAdd to `Cargo.toml`:\n\n```toml\n[dependencies]\nbllist = \"0.2\"\n\n# Enable async wrappers (requires a Tokio runtime):\n# bllist = { version = \"0.2\", features = [\"async\"] }\n```\n\n### Fixed-size blocks\n\n```rust\nuse bllist::FixedBlockList;\n\nfn main() -\u003e Result\u003c(), bllist::Error\u003e {\n    // 52 bytes of payload per block (64 bytes total on disk).\n    let list = FixedBlockList::\u003c52\u003e::open(\"data.blls\")?;\n\n    list.push_front(b\"hello\")?;\n    list.push_front(b\"world\")?;\n\n    while let Some(data) = list.pop_front()? {\n        println!(\"{}\", String::from_utf8_lossy(\u0026data));\n    }\n    // prints \"world\", then \"hello\"\n\n    Ok(())\n}\n```\n\n### Variable-size blocks\n\n```rust\nuse bllist::DynamicBlockList;\n\nfn main() -\u003e Result\u003c(), bllist::Error\u003e {\n    // The total on-disk block size (header + payload) is a power of two.\n    // A 5-byte push occupies 32 bytes on disk (5+20=25 → 32, bin 5).\n    let list = DynamicBlockList::open(\"data.blld\")?;\n\n    list.push_front(b\"short\")?;\n    list.push_front(b\"a somewhat longer record\")?;\n\n    while let Some(data) = list.pop_front()? {\n        println!(\"{}\", String::from_utf8_lossy(\u0026data));\n    }\n\n    Ok(())\n}\n```\n\n### Doubly-linked fixed-size blocks\n\n```rust\nuse bllist::FixedDblList;\n\nfn main() -\u003e Result\u003c(), bllist::Error\u003e {\n    // 44 bytes of payload per block (64 bytes total on disk).\n    let list = FixedDblList::\u003c44\u003e::open(\"data.bldf\")?;\n\n    // Queue: push to back, pop from front (FIFO).\n    list.push_back(b\"task-1\")?;\n    list.push_back(b\"task-2\")?;\n    list.push_back(b\"task-3\")?;\n\n    while let Some(data) = list.pop_front()? {\n        println!(\"{}\", String::from_utf8_lossy(\u0026data));\n    }\n    // prints \"task-1\", \"task-2\", \"task-3\"\n\n    // Or iterate in reverse using DoubleEndedIterator:\n    list.push_back(b\"a\")?;\n    list.push_back(b\"b\")?;\n    list.push_back(b\"c\")?;\n    for item in list.iter()?.rev() {\n        println!(\"{}\", String::from_utf8_lossy(\u0026item?));\n    }\n    // prints \"c\", \"b\", \"a\"\n\n    Ok(())\n}\n```\n\n### Doubly-linked variable-size blocks\n\n```rust\nuse bllist::DynamicDblList;\n\nfn main() -\u003e Result\u003c(), bllist::Error\u003e {\n    // block_size_for(size) = smallest power-of-two ≥ size+28 (28-byte header).\n    let list = DynamicDblList::open(\"data.bldd\")?;\n\n    list.push_back(b\"short\")?;\n    list.push_back(b\"a somewhat longer record\")?;\n\n    // Bidirectional iteration:\n    let mut it = list.iter()?;\n    println!(\"{}\", String::from_utf8_lossy(\u0026it.next().unwrap()?));      // \"short\"\n    println!(\"{}\", String::from_utf8_lossy(\u0026it.next_back().unwrap()?)); // \"a somewhat longer record\"\n\n    Ok(())\n}\n```\n\n### Async (Tokio)\n\nEnable the `async` feature and use `AsyncFixedBlockList` / `AsyncDynamicBlockList`:\n\n```rust\nuse bllist::AsyncDynamicBlockList;\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c(), bllist::Error\u003e {\n    let list = AsyncDynamicBlockList::open(\"data.blld\").await?;\n\n    list.push_front(b\"short record\").await?;\n    list.push_front(b\"a somewhat longer record\").await?;\n\n    while let Some(data) = list.pop_front().await? {\n        println!(\"{}\", String::from_utf8_lossy(\u0026data));\n    }\n\n    Ok(())\n}\n```\n\nThe wrappers are `Clone` (cheap `Arc` increment) and share the same underlying\nfile handle, so you can hand copies to multiple tasks without reopening the file.\n\n---\n\n## API overview\n\n### `FixedBlockList\u003cPAYLOAD_CAPACITY\u003e`\n\n`PAYLOAD_CAPACITY` is the number of **payload bytes** per block. Each block occupies `PAYLOAD_CAPACITY + 12` bytes on disk. `PAYLOAD_CAPACITY` must be `\u003e 0`; a value of `0` is rejected at compile time.\n\n| Method                                 | Description                                  |\n|----------------------------------------|----------------------------------------------|\n| `open(path)`                           | Open or create; performs crash recovery      |\n| `push_front(data)`                     | Allocate, write, and prepend to the list     |\n| `pop_front()` → `Option\u003cVec\u003cu8\u003e\u003e`      | Unlink head, read payload, free block        |\n| `pop_front_into(buf)` → `bool`         | Zero-copy pop into caller buffer             |\n| `alloc()` → `BlockRef`                 | Allocate a raw block (from free list or new) |\n| `free(block)`                          | Return a block to the free list              |\n| `write(block, data)`                   | Write payload, preserve next pointer         |\n| `read(block)` → `Vec\u003cu8\u003e`              | Read and checksum-verify payload             |\n| `read_into(block, buf)`                | Zero-copy read into caller buffer            |\n| `set_next(block, next)`                | Update next pointer, preserve payload        |\n| `get_next(block)` → `Option\u003cBlockRef\u003e` | Read next pointer (no CRC check)             |\n| `root()` → `Option\u003cBlockRef\u003e`          | Current head of the active list              |\n| `iter()` → `FixedIter\u003c'_\u003e`             | Forward iterator from head to tail           |\n| `payload_capacity()`                   | `PAYLOAD_CAPACITY`                           |\n\n### `BlockRef`\n\nA `Copy` handle encoding a block's logical byte offset in the BStack file. Treat it like a typed index; never forge offsets manually.\n\n### `DynamicBlockList`\n\nBlocks may hold any payload up to 2^31 − 20 bytes.  The **total on-disk footprint** of a block (20-byte header + payload) is always a power of two, with a minimum of 32 bytes (bin 5, 12-byte payload).  Allocation first checks the exact power-of-two bin, then searches up to `MAX_SPLIT` = 3 bins higher and splits if found, and finally extends the file.  On open, adjacent free blocks whose combined size is a power of two are coalesced.\n\n| Method                                    | Description                                                                  |\n|-------------------------------------------|------------------------------------------------------------------------------|\n| `open(path)`                              | Open or create; validates header; coalesces free blocks and recovers orphans |\n| `push_front(data)`                        | Allocate, write, and prepend to the list                                     |\n| `pop_front()` → `Option\u003cVec\u003cu8\u003e\u003e`         | Unlink head, read payload, free block                                        |\n| `pop_front_into(buf)` → `bool`            | Zero-copy pop into caller buffer                                             |\n| `alloc(size)` → `DynBlockRef`             | Allocate block; splits from larger bin or extends file                       |\n| `free(block)`                             | Return a block to its bin                                                    |\n| `write(block, data)`                      | Write payload, update `data_len`                                             |\n| `read(block)` → `Vec\u003cu8\u003e`                 | Read `data_len` bytes, checksum-verify                                       |\n| `read_into(block, buf)`                   | Zero-copy read into caller buffer                                            |\n| `set_next(block, next)`                   | Update next pointer, preserve payload                                        |\n| `get_next(block)` → `Option\u003cDynBlockRef\u003e` | Read next pointer (no CRC check)                                             |\n| `root()` → `Option\u003cDynBlockRef\u003e`          | Current head of the active list                                              |\n| `capacity(block)` → `usize`               | Payload capacity = `block_size − 20`                                         |\n| `data_len(block)` → `usize`               | Bytes last written to this block                                             |\n| `data_start(block)` → `u64`               | Logical offset of first payload byte (validates offset)                      |\n| `data_end(block)` → `u64`                 | Logical offset past the last written byte (reads `data_len`)                 |\n| `bstack()` → `\u0026BStack`                    | Underlying file handle for raw read-only streaming                           |\n| `iter()` → `DynIter\u003c'_\u003e`                  | Forward iterator from head to tail                                           |\n| `block_size_for(size)` → `usize`          | Smallest power-of-two total size ≥ `size + 20` (min 32)                      |\n\n### `DynBlockRef`\n\nA `Copy` handle encoding a dynamic block's logical byte offset. Analogous to `BlockRef` but for `DynamicBlockList`.\n\n| Method                 | Description                                                            |\n|------------------------|------------------------------------------------------------------------|\n| `data_start()` → `u64` | Logical offset of the first payload byte (`self.0 + 20`); pure, no I/O |\n\n### `FixedIter\u003c'a, PAYLOAD_CAPACITY\u003e`\n\nA forward iterator over the active list of a `FixedBlockList`.  Obtained via\n`list.iter()?`.  Each item is `Result\u003cVec\u003cu8\u003e, Error\u003e`; the `Vec` is always\n`PAYLOAD_CAPACITY` bytes long (zero-padded past the last write).  CRC is\nverified on every step; the iterator stops after the first error.\n\n### `DynIter\u003c'a\u003e`\n\nA forward iterator over the active list of a `DynamicBlockList`.  Obtained\nvia `list.iter()?`.  Each item is `Result\u003cVec\u003cu8\u003e, Error\u003e` containing exactly\nthe bytes last written to that block.  CRC is verified on every step; the\niterator stops after the first error.\n\n---\n\n### `FixedDblList\u003cPAYLOAD_CAPACITY\u003e`\n\n`PAYLOAD_CAPACITY` must be `\u003e 0`.  Each block occupies `PAYLOAD_CAPACITY + 20`\nbytes on disk (4-byte CRC + 8-byte `prev` + 8-byte `next` + payload).  The\nfile header (32 bytes) stores `root`, `tail`, and `free_head`.\n\n| Method                                    | Description                                        |\n|-------------------------------------------|----------------------------------------------------|\n| `open(path)`                              | Open or create; recovers orphans and rebuilds tail |\n| `push_front(data)`                        | Allocate, write, and prepend to the list           |\n| `push_back(data)`                         | Allocate, write, and append to the list            |\n| `pop_front()` → `Option\u003cVec\u003cu8\u003e\u003e`         | Unlink head, read payload, free block              |\n| `pop_back()` → `Option\u003cVec\u003cu8\u003e\u003e`          | Unlink tail, read payload, free block              |\n| `pop_front_into(buf)` → `bool`            | Zero-copy pop from head into caller buffer         |\n| `pop_back_into(buf)` → `bool`             | Zero-copy pop from tail into caller buffer         |\n| `alloc()` → `BlockDblRef`                 | Allocate a raw block                               |\n| `free(block)`                             | Return a block to the free list                    |\n| `write(block, data)`                      | Write payload, preserve prev/next                  |\n| `read(block)` → `Vec\u003cu8\u003e`                 | Read and checksum-verify payload                   |\n| `read_into(block, buf)`                   | Zero-copy read into caller buffer                  |\n| `set_next(block, next)`                   | Update next pointer, preserve payload and prev     |\n| `set_prev(block, prev)`                   | Update prev pointer, preserve payload and next     |\n| `get_next(block)` → `Option\u003cBlockDblRef\u003e` | Read next pointer (no CRC check)                   |\n| `get_prev(block)` → `Option\u003cBlockDblRef\u003e` | Read prev pointer (no CRC check)                   |\n| `root()` → `Option\u003cBlockDblRef\u003e`          | Current head of the active list                    |\n| `tail()` → `Option\u003cBlockDblRef\u003e`          | Current tail of the active list                    |\n| `iter()` → `FixedDblIter\u003c'_\u003e`             | Double-ended iterator (forward and backward)       |\n| `payload_capacity()`                      | `PAYLOAD_CAPACITY`                                 |\n\n### `BlockDblRef`\n\nA `Copy` handle encoding a `FixedDblList` block's logical byte offset.  Same\n`Display` / `LowerHex` / `UpperHex` / `From` traits as `BlockRef`.\n\n### `FixedDblIter\u003c'a, PAYLOAD_CAPACITY\u003e`\n\nA double-ended iterator over the active list of a `FixedDblList`.  Obtained\nvia `list.iter()?`.  Implements both `Iterator` (forward, head→tail) and\n`DoubleEndedIterator` (backward, tail→head).  When both cursors converge on the\nsame block, it is yielded exactly once.  CRC is verified on every item; the\niterator terminates on the first error from either end.\n\n---\n\n### `DynamicDblList`\n\nSame bin-based allocator as `DynamicBlockList` with `prev` pointers added.\nThe block header is 28 bytes (CRC + prev + next + block\\_size + data\\_len);\n`block_size_for(size)` returns the smallest power-of-two ≥ `size + 28` (min 32,\npayload capacity = 4 bytes for bin 5).\n\n| Method                                       | Description                                                |\n|----------------------------------------------|------------------------------------------------------------|\n| `open(path)`                                 | Open or create; coalesces, recovers orphans, rebuilds tail |\n| `push_front(data)`                           | Allocate, write, and prepend                               |\n| `push_back(data)`                            | Allocate, write, and append                                |\n| `pop_front()` → `Option\u003cVec\u003cu8\u003e\u003e`            | Unlink head, read payload, free block                      |\n| `pop_back()` → `Option\u003cVec\u003cu8\u003e\u003e`             | Unlink tail, read payload, free block                      |\n| `pop_front_into(buf)` → `bool`               | Zero-copy pop from head                                    |\n| `pop_back_into(buf)` → `bool`                | Zero-copy pop from tail                                    |\n| `alloc(size)` → `DynBlockDblRef`             | Allocate block; splits or extends file                     |\n| `free(block)`                                | Return block to its bin                                    |\n| `write(block, data)`                         | Write payload, update `data_len`                           |\n| `read(block)` → `Vec\u003cu8\u003e`                    | Read `data_len` bytes, checksum-verify                     |\n| `read_into(block, buf)`                      | Zero-copy read into caller buffer                          |\n| `set_next(block, next)`                      | Update next pointer, preserve all other fields             |\n| `set_prev(block, prev)`                      | Update prev pointer, preserve all other fields             |\n| `get_next(block)` → `Option\u003cDynBlockDblRef\u003e` | Read next pointer (no CRC check)                           |\n| `get_prev(block)` → `Option\u003cDynBlockDblRef\u003e` | Read prev pointer (no CRC check)                           |\n| `root()` → `Option\u003cDynBlockDblRef\u003e`          | Current head of the active list                            |\n| `tail()` → `Option\u003cDynBlockDblRef\u003e`          | Current tail of the active list                            |\n| `capacity(block)` → `usize`                  | Payload capacity = `block_size − 28`                       |\n| `data_len(block)` → `usize`                  | Bytes last written to this block                           |\n| `data_start(block)` → `u64`                  | Logical offset of first payload byte (validates offset)    |\n| `data_end(block)` → `u64`                    | Logical offset past the last written byte                  |\n| `bstack()` → `\u0026BStack`                       | Underlying file handle for raw read-only streaming         |\n| `iter()` → `DynDblIter\u003c'_\u003e`                  | Double-ended iterator (forward and backward)               |\n| `block_size_for(size)` → `usize`             | Smallest power-of-two total size ≥ `size + 28` (min 32)    |\n\n### `DynBlockDblRef`\n\nA `Copy` handle encoding a `DynamicDblList` block's logical byte offset.\n\n| Method                 | Description                                                            |\n|------------------------|------------------------------------------------------------------------|\n| `data_start()` → `u64` | Logical offset of the first payload byte (`self.0 + 28`); pure, no I/O |\n\n### `DynDblIter\u003c'a\u003e`\n\nA double-ended iterator over the active list of a `DynamicDblList`.  Obtained\nvia `list.iter()?`.  Implements `Iterator` and `DoubleEndedIterator` with the\nsame convergence semantics as `FixedDblIter`.\n\n### `AsyncFixedBlockList\u003cPAYLOAD_CAPACITY\u003e` *(feature `async`)*\n\nAn async, `Clone`-able wrapper around `FixedBlockList`. Each method runs on\nTokio's blocking-thread pool via `spawn_blocking`.  Data inputs accept any\n`impl AsRef\u003c[u8]\u003e + Send + 'static` (e.g. `Vec\u003cu8\u003e`, `Box\u003c[u8]\u003e`, `\u0026'static [u8]`).\n\n\u003e **No async iterator** — `spawn_blocking` requires `'static` closures, so a\n\u003e streaming async iterator cannot hold a borrowed `\u0026'a` reference to the inner\n\u003e list across await points.  Use `list.inner().iter()?` to iterate\n\u003e synchronously, or collect into a `Vec` inside one `spawn_blocking` block.\n\n| Method                                       | Description                                |\n|----------------------------------------------|--------------------------------------------|\n| `open(path).await`                           | Open or create on a blocking thread        |\n| `push_front(data).await`                     | Allocate, write, prepend to list           |\n| `pop_front().await` → `Option\u003cVec\u003cu8\u003e\u003e`      | Unlink head, read payload, free block      |\n| `alloc().await` → `BlockRef`                 | Allocate a raw block                       |\n| `free(block).await`                          | Return block to free list                  |\n| `write(block, data).await`                   | Write payload, preserve next pointer       |\n| `read(block).await` → `Vec\u003cu8\u003e`              | Read and checksum-verify payload           |\n| `set_next(block, next).await`                | Update next pointer                        |\n| `get_next(block).await` → `Option\u003cBlockRef\u003e` | Read next pointer (no CRC check)           |\n| `root().await` → `Option\u003cBlockRef\u003e`          | Current head of the active list            |\n| `payload_capacity()`                         | `PAYLOAD_CAPACITY` (no I/O)                |\n| `inner()` → `\u0026FixedBlockList\u003cN\u003e`             | Underlying sync handle for streaming reads |\n\n### `AsyncDynamicBlockList` *(feature `async`)*\n\nAn async, `Clone`-able wrapper around `DynamicBlockList`. Same `spawn_blocking`\napproach as `AsyncFixedBlockList`.\n\n| Method                                          | Description                                             |\n|-------------------------------------------------|---------------------------------------------------------|\n| `open(path).await`                              | Open or create on a blocking thread                     |\n| `push_front(data).await`                        | Allocate, write, prepend to list                        |\n| `pop_front().await` → `Option\u003cVec\u003cu8\u003e\u003e`         | Unlink head, read payload, free block                   |\n| `alloc(size).await` → `DynBlockRef`             | Allocate block; splits or extends file                  |\n| `free(block).await`                             | Return block to its bin                                 |\n| `write(block, data).await`                      | Write payload, update `data_len`                        |\n| `read(block).await` → `Vec\u003cu8\u003e`                 | Read `data_len` bytes, checksum-verify                  |\n| `set_next(block, next).await`                   | Update next pointer                                     |\n| `get_next(block).await` → `Option\u003cDynBlockRef\u003e` | Read next pointer (no CRC check)                        |\n| `root().await` → `Option\u003cDynBlockRef\u003e`          | Current head of the active list                         |\n| `capacity(block).await` → `usize`               | Payload capacity = `block_size − 20`                    |\n| `data_len(block).await` → `usize`               | Bytes last written to this block                        |\n| `data_end(block).await` → `u64`                 | Logical offset past the last written byte               |\n| `block_size_for(size)`                          | Smallest power-of-two total size ≥ `size + 20` (no I/O) |\n| `inner()` → `\u0026DynamicBlockList`                 | Underlying sync handle for streaming reads              |\n\n\u003e **No async iterator** — `spawn_blocking` requires `'static` closures, so a\n\u003e streaming async iterator cannot hold a borrowed `\u0026'a` reference to the inner\n\u003e list across await points.  Use `list.inner().iter()?` to iterate\n\u003e synchronously, or collect into a `Vec` inside one `spawn_blocking` block.\n\n---\n\n## Traversal and iteration\n\nAll four list types expose an iterator via `.iter()?`.  The singly-linked types\nreturn a **forward-only** iterator; the doubly-linked types return a\n**double-ended** iterator.\n\n```rust\nuse bllist::FixedBlockList;\n\nlet list = FixedBlockList::\u003c52\u003e::open(\"data.blls\")?;\nfor item in list.iter()? {\n    let payload = item?;          // Vec\u003cu8\u003e, always 52 bytes\n    println!(\"{}\", String::from_utf8_lossy(\u0026payload));\n}\n```\n\n```rust\nuse bllist::DynamicBlockList;\n\nlet list = DynamicBlockList::open(\"data.blld\")?;\nfor item in list.iter()? {\n    let payload = item?;          // Vec\u003cu8\u003e, exactly data_len bytes\n    println!(\"{}\", String::from_utf8_lossy(\u0026payload));\n}\n```\n\n```rust\nuse bllist::FixedDblList;\n\nlet list = FixedDblList::\u003c44\u003e::open(\"data.bldf\")?;\n\n// Forward (head → tail):\nfor item in list.iter()? {\n    println!(\"{}\", String::from_utf8_lossy(\u0026item?));\n}\n\n// Backward (tail → head) via DoubleEndedIterator:\nfor item in list.iter()?.rev() {\n    println!(\"{}\", String::from_utf8_lossy(\u0026item?));\n}\n\n// Alternating from both ends simultaneously:\nlet mut it = list.iter()?;\nwhile let Some(front) = it.next() {\n    println!(\"front: {}\", String::from_utf8_lossy(\u0026front?));\n    if let Some(back) = it.next_back() {\n        println!(\"back:  {}\", String::from_utf8_lossy(\u0026back?));\n    }\n}\n```\n\n`iter()` reads the current `root` (and `tail` for doubly-linked lists) once to\nseed the iterator; each subsequent `next()` / `next_back()` call issues one\nfile read and CRC verification.  The iterator holds a shared `\u0026` reference to\nthe list, preventing mutation while iteration is in progress.\n\nFor the doubly-linked iterators, both cursors advance toward each other.  When\nthey converge on the same block, that block is yielded exactly once (from\nwhichever side calls next), then both cursors are set to `None`.  The iterator\nterminates on the first error from either end.\n\n`DoubleEndedIterator` is **not** implemented for `FixedIter` and `DynIter` —\nthe singly-linked list types do not store a tail pointer, so backward traversal\nwould require collecting all elements first.  Use `FixedDblList` or\n`DynamicDblList` when bidirectional traversal is needed.\n\n---\n\n## Streaming reads\n\n`read()` and `read_into()` verify the CRC on every call and either allocate a\n`Vec\u003cu8\u003e` or copy into a caller buffer.  For large payloads — or when you need\nto hand a byte range to another layer (e.g. `sendfile`, a scatter-gather\nbuffer, or an async runtime) — `DynamicBlockList` exposes three building\nblocks that let you issue a single raw read:\n\n| Building block                        | I/O cost                          | Returns                               |\n|---------------------------------------|-----------------------------------|---------------------------------------|\n| `block.data_start()`                  | none (pure arithmetic)            | start of payload as `u64`             |\n| `list.data_end(block)?`               | 1 × 4-byte read (`data_len`)      | one-past-end of written data as `u64` |\n| `list.bstack().get_into(start, buf)?` | 1 × `pread` of your chosen length | fills `buf` from the file             |\n\n```rust\nuse bllist::DynamicBlockList;\n\nlet list = DynamicBlockList::open(\"data.blld\")?;\n// … obtain `block` from push_front / pop_front / root traversal …\n\n// Compute the byte range with no file I/O.\nlet start: u64 = block.data_start();\nlet end:   u64 = list.data_end(block)?;  // one 4-byte read\n\n// Single pread into a caller-owned buffer — no CRC, no Vec allocation.\nlet mut buf = vec![0u8; (end - start) as usize];\nlist.bstack().get_into(start, \u0026mut buf)?;\n\n// Or fill a sub-range of an existing buffer:\nlist.bstack().get_into(start, \u0026mut frame[offset..])?;\n```\n\n\u003e **Only read-only BStack operations are safe**: `get`, `get_into`, `peek`,\n\u003e `len`.  Never call `push`, `pop`, or `set` on the handle returned by\n\u003e `bstack()` — doing so can silently corrupt the list structure.  Use `read()`\n\u003e or `read_into()` when CRC verification matters.\n\n---\n\n## File layouts\n\n### `FixedBlockList` files (`\"BLLS\"`)\n\n```\n┌──────────────────────────┬───────────────────────────────────────────┐\n│  BStack header (16 B)    │  bllist header (24 B, logical offset 0)   │\n│  \"BSTK\" magic + clen     │  \"BLLS\" + version + root + free_head      │\n├──────────────────────────┴───────────────────────────────────────────┤\n│  Block 0  (PAYLOAD_CAPACITY+12 bytes, logical offset 24)             │\n│  checksum(4) │ next(8) │ payload(PAYLOAD_CAPACITY)                   │\n├──────────────────────────────────────────────────────────────────────┤\n│  Block 1  (PAYLOAD_CAPACITY+12 bytes, logical offset 24+PC+12)  …    │\n└──────────────────────────────────────────────────────────────────────┘\n```\n\n- The **bllist header** stores the root block offset and single free-list-head offset.\n- The **block checksum** covers bytes `[4..PAYLOAD_CAPACITY+12]` (next pointer + full payload field). Payload bytes beyond the last `write` are guaranteed to be zero.\n- The **free list** is an embedded singly-linked list using the `next` field of freed blocks.\n\n### `DynamicBlockList` files (`\"BLLD\"`)\n\n```\n┌──────────────────────────┬───────────────────────────────────────────────┐\n│  BStack header (16 B)    │  bllist-dynamic header (272 B, logical off 0) │\n│  \"BSTK\" magic + clen     │  \"BLLD\" + version + root + bin_heads[32]      │\n├──────────────────────────┴───────────────────────────────────────────────┤\n│  Block (total size = 2^k bytes, k ≥ 5)                                   │\n│  checksum(4) │ next(8) │ block_size(4) │ data_len(4) │ payload(bs-20 B)  │\n├──────────────────────────────────────────────────────────────────────────┤\n│  Block …                                                                 │\n└──────────────────────────────────────────────────────────────────────────┘\n```\n\n- The **bllist-dynamic header** stores the root offset and 32 bin free-list heads. Bin *k* holds free blocks whose **total on-disk size** equals 2^*k* bytes (bins 0–4 are always empty; minimum is bin 5 = 32 bytes).\n- The **`block_size` field** stores the total on-disk size of the block (header + payload), always a power of two (≥ 32). Payload capacity = `block_size − 20`.\n- The **block checksum** covers bytes `[4..block_size]` (next + block_size + data_len + full payload field, including zero-padded tail).\n- `data_len` records how many bytes were written; bytes beyond it are guaranteed to be zero.\n\n### `FixedDblList` files (`\"BLDF\"`)\n\n```\n┌──────────────────────────┬──────────────────────────────────────────────────────────┐\n│  BStack header (16 B)    │  bllist-dbl-fixed header (32 B, logical offset 0)        │\n│  \"BSTK\" magic + clen     │  \"BLDF\" + version + root + tail + free_head              │\n├──────────────────────────┴──────────────────────────────────────────────────────────┤\n│  Block 0  (PAYLOAD_CAPACITY+20 bytes, logical offset 32)                            │\n│  checksum(4) │ prev(8) │ next(8) │ payload(PAYLOAD_CAPACITY)                        │\n├─────────────────────────────────────────────────────────────────────────────────────┤\n│  Block 1  …                                                                         │\n└─────────────────────────────────────────────────────────────────────────────────────┘\n```\n\n- The **header** stores `root`, `tail`, and `free_head`; `tail` enables O(1) `push_back` and `pop_back`.\n- The **block checksum** covers bytes `[4..PAYLOAD_CAPACITY+20]` (prev + next + full payload).\n- The **free list** is singly-linked via the `next` field of freed blocks (`prev` is zeroed on free).\n\n### `DynamicDblList` files (`\"BLDD\"`)\n\n```\n┌──────────────────────────┬──────────────────────────────────────────────────────────────┐\n│  BStack header (16 B)    │  bllist-dbl-dynamic header (280 B, logical off 0)            │\n│  \"BSTK\" magic + clen     │  \"BLDD\" + version + root + tail + bin_heads[32]              │\n├──────────────────────────┴──────────────────────────────────────────────────────────────┤\n│  Block (total size = 2^k bytes, k ≥ 5)                                                  │\n│  checksum(4) │ prev(8) │ next(8) │ block_size(4) │ data_len(4) │ payload(bs-28 B)      │\n├──────────────────────────────────────────────────────────────────────────────────────────┤\n│  Block …                                                                                │\n└──────────────────────────────────────────────────────────────────────────────────────────┘\n```\n\n- The **header** stores `root`, `tail`, and 32 bin free-list heads.\n- Block layout adds `prev` before `next`; payload capacity = `block_size − 28`. Minimum block is 32 bytes (bin 5, 4-byte payload).\n- `block_size_for(size)` returns the smallest power-of-two ≥ `size + 28` (min 32).\n- Free blocks are singly-linked via the `next` field (`prev` zeroed); bin allocator, splitting, and coalescing are identical to `DynamicBlockList`.\n\n---\n\n## Crash safety details\n\n`bllist` is designed around two principles:\n\n1. **Durable writes** — every `stack.set()` / `stack.push()` call issues `fsync` (or `F_FULLFSYNC` on macOS) before returning.\n2. **CRC-detected partial writes** — the checksum over the block header and payload detects any block that was partially overwritten before a crash.\n\nOn `open`, the file is scanned for *orphaned* blocks (allocated but not reachable from either the active list or any free list). They are silently reclaimed.\n\n| Crash point                                    | Effect                             | Recovery                           |\n|------------------------------------------------|------------------------------------|------------------------------------|\n| During `alloc` (file grow)                     | Block exists but is in no list     | Reclaimed as orphan on next `open` |\n| After `alloc`, before `push_front` links it    | Block written but root not updated | Reclaimed as orphan on next `open` |\n| After `pop_front` advances root, before `free` | Block exists but in no list        | Reclaimed as orphan on next `open` |\n\nNo data that was fully committed (root updated) is ever lost.\n\n---\n\n## Choosing the right type\n\n|                            | `FixedBlockList`        | `DynamicBlockList`                     | `FixedDblList`          | `DynamicDblList`                       |\n|----------------------------|-------------------------|----------------------------------------|-------------------------|----------------------------------------|\n| Record size                | Always the same         | Varies                                 | Always the same         | Varies                                 |\n| Links                      | Singly-linked           | Singly-linked                          | Doubly-linked           | Doubly-linked                          |\n| On-disk overhead per block | 12 bytes                | 20 bytes                               | 20 bytes                | 28 bytes                               |\n| Block size on disk         | `PAYLOAD_CAPACITY + 12` | Power of two ≥ `payload + 20` (min 32) | `PAYLOAD_CAPACITY + 20` | Power of two ≥ `payload + 28` (min 32) |\n| push/pop at both ends      | No                      | No                                     | Yes                     | Yes                                    |\n| Bidirectional iteration    | No                      | No                                     | Yes                     | Yes                                    |\n| Free list                  | Single flat list        | 32 power-of-two bins                   | Single flat list        | 32 power-of-two bins                   |\n| Splitting                  | No                      | Up to `MAX_SPLIT` = 3 levels           | No                      | Up to `MAX_SPLIT` = 3 levels           |\n| Coalescing on open         | No                      | Yes (adjacent same-power-of-two runs)  | No                      | Yes (adjacent same-power-of-two runs)  |\n| Tail-block shrink on free  | Yes                     | Yes                                    | Yes                     | Yes                                    |\n| Tail rebuilt on open       | N/A                     | N/A                                    | Yes                     | Yes                                    |\n| Orphan scan                | O(n) slot enumeration   | O(n) sequential scan + rebuild         | O(n) slot enumeration   | O(n) sequential scan + rebuild         |\n| File magic                 | `\"BLLS\"`                | `\"BLLD\"`                               | `\"BLDF\"`                | `\"BLDD\"`                               |\n| On-disk format version     | 1                       | 2                                      | 1                       | 1                                      |\n\n**Choose singly-linked** (`FixedBlockList` / `DynamicBlockList`) when you only\nneed a stack (push/pop from one end) or a forward-only iterator — they have\nlower per-block overhead.\n\n**Choose doubly-linked** (`FixedDblList` / `DynamicDblList`) when you need a\nqueue (push to one end, pop from the other), bidirectional iteration, or\nefficient access to the tail.\n\n### Choosing `PAYLOAD_CAPACITY` for fixed-size lists\n\n- Minimum: `1`\n- For `FixedBlockList` (`+12` overhead): `52` (64 bytes on disk), `116` (128 bytes on disk)\n- For `FixedDblList` (`+20` overhead): `44` (64 bytes on disk), `108` (128 bytes on disk)\n- `PAYLOAD_CAPACITY = 0` is rejected at compile time for both types\n\n---\n\n## Direct file access — use with extreme caution\n\nAll list types produce valid BStack files, so you can open them with\n`bstack::BStack::open` or inspect the raw bytes with any file tool.\n**Writing to the file outside of `bllist` is strongly discouraged.**\n`bllist` does not re-validate structural invariants on every operation, so\ndirect writes can silently corrupt the list in ways that are not caught until\nmuch later — or not caught at all.\n\nSpecific dangers:\n\n| Operation                          | Risk                                                                                                  |\n|------------------------------------|-------------------------------------------------------------------------------------------------------|\n| `BStack::push`                     | Appends raw bytes that are not a complete, aligned block; breaks slot enumeration and orphan recovery |\n| `BStack::pop`                      | May truncate a block mid-stream or destroy the list header                                            |\n| `BStack::set` at header offsets    | Overwrites root or free-list / bin-head pointers                                                      |\n| `BStack::set` inside a block       | Invalidates the block's CRC; `read` will return a checksum error                                      |\n| Raw file writes (`write(2)`, etc.) | Bypasses the advisory lock entirely; any of the above, plus potential torn writes                     |\n\n**The exclusive advisory lock** (`flock` on Unix, `LockFileEx` on Windows)\nheld by a live list prevents a second process from opening the same file\nthrough BStack simultaneously. It does **not** prevent raw file\ndescriptor access, so a process that opens the file without going through\nBStack can bypass the lock and cause corruption.\n\n**Safe read-only inspection** is possible: open the file with\n`bstack::BStack::open` and use only `get`, `peek`, and `len`. These calls do\nnot write to the file and will not disturb the list state. Mutating calls\n(`push`, `pop`, `set`) must not be used.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwilliamwutq%2Fbllist","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwilliamwutq%2Fbllist","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwilliamwutq%2Fbllist/lists"}