diff --git a/CHANGELOG.md b/CHANGELOG.md index aa6d4f3..abdc300 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,42 +9,49 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Added -- storage.Prefixed: New wrapper that scopes every operation, predicate, - Range, and Watch call under a given namespace prefix. Nested Prefixed - wrappers are flattened at construction. +### Changed + +### Fixed + +## [v1.3.0] - 2026-05-05 + +This release introduces a new transaction-aware integrity API +(`Codec[T]`, `Store[T]`, and `Tx`), a `Prefixed` storage wrapper for +namespace scoping, a `LayeredNamer` with per-category key layout, and +additional typed marshallers. It also bumps dependencies to address +vulnerabilities reported by govulncheck. + +### Added + +- storage.Prefixed: wrapper that scopes every storage operation, + predicate, Range, and Watch call under a given namespace prefix. + Nested wrappers are flattened automatically (#67). - namer.LayeredNamer: namespace-agnostic namer that places each key category under its own top-level location segment - (`//`, `//`, - `//`). The constructor validates segments - (non-empty, slash-free, unique across categories) and `ParseKey` - parses keys back to `(name, KeyType, property)` unambiguously. -- integrity.Codec: schema-only `Codec[T]` (no storage handle) and - `CodecBuilder[T]` with a fluent value-receiver API mirroring - `TypedBuilder[T]`. `Build()` returns `(*Codec[T], error)` and + (`//`, `/hash//...`, + `/sig//...`). Segments are validated at construction + and `ParseKey` parses keys back to `(name, KeyType, property)` + unambiguously (#68). +- integrity: new schema-driven API for integrity-protected storage. + `Codec[T]` describes the value layout independently of any storage + handle and is built via the fluent `CodecBuilder[T]`, which validates location-override keys eagerly so typos like `WithHashLocation("sah256", …)` are no longer silently ignored. -- integrity.Tx: transaction primitives `Tx`, `Branch`, `Branchable`, - `GetFuture[T]`, `RangeFuture[T]`, `Response`, and sentinel errors - `ErrBranchNotFired`, `ErrTxNotCommitted`, `ErrTxAlreadyCommitted`. - `Codec[T]` gains `TxGet`, `TxPut`, `TxDelete`, `TxRange`, and - `BindPredicate` for use inside transactions. -- integrity.Store: `Store[T]` (codec bound to a storage handle) with - `Get`/`Put`/`Delete`/`Range`/`Watch` implemented as thin `Tx` - wrappers, and `Codec[T].Bind` for cheap binding. Multi-codec - transactions lower to a single storage call. + `Store[T]` binds a codec to a storage handle and exposes + `Get`/`Put`/`Delete`/`Range`/`Watch`. Conditional multi-key updates + are available through `Tx` with `Branch` and typed futures; + multi-codec transactions are lowered to a single storage call + (#69, #70, #71). - marshaller: `TypedJSONMarshaller[T]` for `encoding/json`-based - marshalling and `TypedBytesMarshaller` passthrough - `TypedMarshaller[[]byte]` for values that are already serialized or - stored as opaque blobs. - -### Changed + marshalling and `TypedBytesMarshaller` passthrough for values + already stored as opaque bytes (#73). ### Fixed -- Bump google.golang.org/grpc to v1.79.3 to fix GO-2026-4762 - (authorization bypass via missing leading slash in :path). -- Bump go.opentelemetry.io/otel/sdk to v1.40.0 to fix GO-2026-4394 - (arbitrary code execution via PATH hijacking). +- Bumped `google.golang.org/grpc` to `v1.79.3` (GO-2026-4762: + authorization bypass via missing leading slash in `:path`) and + `go.opentelemetry.io/otel/sdk` to `v1.40.0` (GO-2026-4394: arbitrary + code execution via PATH hijacking) (#75). ## [v1.2.0] - 2026-04-29 @@ -138,6 +145,7 @@ The release introduces the initial version of the library. ### Fixed +[v1.3.0]: https://github.com/tarantool/go-storage/releases/tag/v1.3.0 [v1.2.0]: https://github.com/tarantool/go-storage/releases/tag/v1.2.0 [v1.1.2]: https://github.com/tarantool/go-storage/releases/tag/v1.1.2 [v1.1.1]: https://github.com/tarantool/go-storage/releases/tag/v1.1.1 diff --git a/README.md b/README.md index 1f7437c..be8d76d 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,10 @@ safety are critical. - Real-time Watch: Monitor changes to keys and prefixes - Conditional Execution: Value and version-based predicates for safe updates - Data Integrity: Built-in signing and verification of stored data +- Schema-Driven Integrity API: `Codec[T]` / `Store[T]` for type-safe values + plus multi-key atomic transactions via `Tx` +- Namespace Scoping: `Prefixed` wrapper that scopes every operation under a + key prefix - Key‑Value Operations: Get, Put, Delete with prefix support - Range Queries: Efficient scanning of keys with filters - Extensible Drivers: Easy to add new storage backends @@ -208,6 +212,23 @@ The core `Storage` interface (`storage.Storage`) provides high‑level methods: - `Tx(ctx) tx.Tx` – create a transaction builder - `Range(ctx, opts) ([]kv.KeyValue, error)` – range query with prefix/limit +#### Namespace Scoping with `Prefixed` + +`storage.Prefixed(prefix, inner)` returns a `Storage` that transparently +prepends `prefix` to every operation, predicate, Range, and Watch call, and +strips it back from any keys returned to the caller. Nested wrappers are +flattened at construction (`Prefixed("/a", Prefixed("/b", base))` is +equivalent to `Prefixed("/a/b", base)`). + +```go +scoped := storage.Prefixed("/ns", storage.NewStorage(driver)) + +// Caller writes /cfg/version; the driver actually stores /ns/cfg/version. +_, err := scoped.Tx(ctx).Then( + operation.Put([]byte("/cfg/version"), []byte("1.0.0")), +).Commit() +``` + #### Transaction Builder The `tx.Tx` interface enables conditional transactions: @@ -346,6 +367,97 @@ The `integrity.Typed` builder also accepts custom marshallers (default is YAML), custom namers, and separate signer/verifier instances for asymmetric setups. +#### Schema-Driven Integrity API (`Codec`, `Store`, `Tx`) + +Alongside `integrity.Typed`, the package exposes a schema-first API split +into three pieces: + +- `integrity.Codec[T]` describes the on-disk layout (object location, + hashers, signers, marshaller) without binding to any storage handle. It is + built via the fluent `CodecBuilder[T]`, which validates location-override + keys eagerly so typos like `WithHashLocation("sah256", …)` fail at + `Build()` instead of being silently ignored. +- `integrity.Store[T]` is a codec bound to a `storage.Storage` and exposes + the familiar `Get` / `Put` / `Delete` / `Range` / `Watch` methods. +- `integrity.Tx` accumulates `TxGet` / `TxPut` / `TxDelete` / `TxRange` + calls from one or more codecs and commits them atomically through a + single storage call. Reads return typed futures (`GetFuture[T]`, + `RangeFuture[T]`) whose `Result()` is populated after `Commit`. + +##### Codec and Store + +```go +codec, err := integrity.NewCodecBuilder[MyConfig](). + WithObjectLocation("config"). + WithHasher(hasher.NewSHA256Hasher()). + Build() +if err != nil { + log.Fatal(err) +} + +store := codec.Bind(baseStorage) + +if err := store.Put(ctx, "app/settings", MyConfig{...}); err != nil { + log.Fatal(err) +} + +res, err := store.Get(ctx, "app/settings") +if err != nil { + log.Fatal(err) +} +cfg := res.Value.Unwrap() +``` + +##### Multi-Key Transactions + +`Tx` batches reads and writes — across multiple codecs if needed — into one +atomic storage call. `If` predicates are routed by `Then` / `Else`; futures +attached to the branch that did not fire return `ErrBranchNotFired`. + +```go +txn := integrity.NewTx(baseStorage) + +pred, _ := codec.ValueEqual(MyConfig{...}) +bound, _ := codec.BindPredicate("app/settings", pred) +txn.If(bound) + +newFut := codec.TxGet(txn.Then(), "app/new-settings") +_ = codec.TxPut(txn.Then(), "app/settings", MyConfig{...}) + +resp, err := txn.Commit(ctx) +if err != nil { + log.Fatal(err) +} +if !resp.Succeeded { + // The Then branch did not fire; newFut.Result() returns + // integrity.ErrBranchNotFired. +} +``` + +##### Layered Key Layout + +The new API uses `namer.LayeredNamer` by default, which places each key +category under its own top-level location segment: + +``` +// (value) +/hash/// (one per hasher) +/sig/// (one per signer) +``` + +`namer.CompactSingleHash()` and `namer.CompactSingleSig()` drop the +per-hasher / per-signer segment when exactly one is configured. `ParseKey` +parses a raw key back to `(name, KeyType, property)` unambiguously. + +##### Marshallers + +Beyond the default YAML marshaller, the `marshaller` package now ships: + +- `TypedJSONMarshaller[T]` — `encoding/json`-based marshalling for any + Go type. +- `TypedBytesMarshaller` — passthrough `TypedMarshaller[[]byte]` for values + that are already serialized or stored as opaque blobs. + ### Examples Comprehensive examples are available in the driver packages: