diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index a551409..e58ea13 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -20,6 +20,12 @@ jobs: -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local cmake --build /tmp/slotsboxmalloc/build -j sudo cmake --install /tmp/slotsboxmalloc/build + - name: Install kvspace ABI headers (version from deps.json) + run: | + ver=$(jq -r '.kvspace' deps.json) + git clone --depth 1 --branch "$ver" https://github.com/array2d/kvspace.git /tmp/kvspace + sudo mkdir -p /usr/local/include + sudo cp -R /tmp/kvspace/include/. /usr/local/include/ - name: 构建 run: | cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local \ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cf5d78..748124e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,3 +10,7 @@ jobs: run: ./ci/deps.sh - name: 构建 run: make + - name: Run C tests + run: make test + - name: Run Python wrapper tests + run: KVSPACE_C_SO=build/libkvspace-c.so python3 py/tests/test_wire.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 59de6f7..b320db0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,8 +30,10 @@ jobs: v="${RELEASE_TAG:-${GITHUB_REF_NAME}}" d="kvspace-c-abi-$v-linux-x86_64" mkdir -p "$d/include" "$d/lib" - # 头:本仓无公共头(内部头在 src/),权威 ABI 头在 kvspace 仓 → 递归整目录 - git clone --depth 1 https://github.com/array2d/kvspace.git /tmp/kvspace-hdr + # Copy authoritative ABI headers from the pinned kvspace tag in deps.json. + kvspace_ver=$(jq -r '.kvspace' deps.json) + test -n "$kvspace_ver" && test "$kvspace_ver" != null + git clone --depth 1 --branch "$kvspace_ver" https://github.com/array2d/kvspace.git /tmp/kvspace-hdr cp -R /tmp/kvspace-hdr/include/. "$d/include/" # 库:扫构建目录的所有 .so*/.dylib(-P 保符号链接) find build -maxdepth 3 \( -name 'lib*.so*' -o -name 'lib*.dylib' \) -exec cp -P {} "$d/lib/" \; diff --git a/.gitignore b/.gitignore index 5569c0b..8f48f55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build/ +build-test/ cmake-build-*/ *.o *.a diff --git a/CMakeLists.txt b/CMakeLists.txt index fea9d9b..4ed7a56 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,10 @@ if(KVSPACE_BUILD_TESTS) target_include_directories(test_codec_abi PRIVATE src) target_link_libraries(test_codec_abi PRIVATE kvspace-c) add_test(NAME codec_abi COMMAND test_codec_abi) + add_executable(test_abi_contract tests/test_abi_contract.c) + target_include_directories(test_abi_contract PRIVATE src) + target_link_libraries(test_abi_contract PRIVATE kvspace-c) + add_test(NAME abi_contract COMMAND test_abi_contract) add_executable(test_set_part_fastpath tests/test_set_part_fastpath.c) target_include_directories(test_set_part_fastpath PRIVATE src) target_link_libraries(test_set_part_fastpath PRIVATE kvspace-c) diff --git a/Makefile b/Makefile index bbf59ee..59ef6f6 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,13 @@ -.PHONY: build clean +.PHONY: build test clean build: @mkdir -p build && cd build && cmake -DCMAKE_BUILD_TYPE=Release .. && make -j$$(nproc) +# Tests need the kvspace ABI headers () on the include path. +test: + @mkdir -p build-test && cd build-test && \ + cmake -DCMAKE_BUILD_TYPE=Debug -DKVSPACE_BUILD_TESTS=ON .. && \ + make -j$$(nproc) && ctest --output-on-failure + clean: - rm -rf build + rm -rf build build-test diff --git a/README.md b/README.md index 8d70d79..6dd8b87 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,18 @@ Backend: `shm://` — single file-backed mmap block (ART-tree key index + slotsb ```bash make # → build/libkvspace-c.so +make test # → build-test/, runs ctest (needs the kvspace ABI headers) ``` -Dependencies: [`blockmalloc`](../blockmalloc), [`slotsboxmalloc`](../slotsboxmalloc) (header-only + `.so` dual-mode libraries). +Dependencies: [`blockmalloc`](../blockmalloc), [`slotsboxmalloc`](../slotsboxmalloc) (header-only, compiled into this library). +Tests additionally include the authoritative ABI header `` from [`kvspace`](../kvspace) (`include/`). +`ci/deps.sh` installs all three at the versions pinned in `deps.json`. ## API Two surfaces: -1. **Native C API** (`kvspaceShm*`) — `include/kvspace/kvspace.h`: +1. **Native C API** (`kvspaceShm*`) — `src/kvspace_shm.h` (internal header): `kvspaceShmOpen/Close`, `kvspaceShmGet/Set`, `kvspaceShmList`, `kvspaceShmDel/Deltree`, `kvspaceShmMkindex`, `kvspaceShmExtindex/Delextindex`, `kvspaceShmNotify/Watch`. 2. **durable C ABI** — byte-compatible with `kvspace-durable` (`src/durable_abi.c`): @@ -33,24 +36,30 @@ Two surfaces: ## XValue -Fixed 8-byte head wire format (`#566`), read/written by direct `memcpy` of the -`kvspaceXvalueHead` struct: +Fixed 8-byte head (`#566`) followed by the body and an attr region (`#571`), stored as one value under one key: ``` -[len:u48 le @0-5][storetype:u8 @6][ptr:u8 @7][body @8] +[len:u48 le @0-5][storetype:u8 @6][ptr:u8 @7][body @8][attr @8+len] +attr = [attr_len:u16 le][records] attr_len includes itself; empty attr = 2 +record = [tag:u8][len:u16 le][data] tag 1 = langtype (kindexpr); 2+ reserved +total = 8 + len + attr_len ``` - storetype selects short, slack, large, or extension storage; ptr marks pointer/ext. -- langtype (kindexpr) is not in the head — it lives in the `x‥langtype` attr key. -- The head is built with `kvspaceXvalueHeadNew*` constructors. +- langtype (kindexpr) is the tag-1 record of the attr region; no record = untyped. +- Values are built with the `kvspaceXvalueHeadNew*` constructors. `kvspaceSetValue` + rejects values whose attr region is missing or runs past the value. +- Store format version 6 (this wire). Stores written by older versions are refused at open; rebuild them. -## Tutorial +## Tests ```bash -python3 tutorial/test.py +make test # C tests (ctest) +KVSPACE_C_SO=build/libkvspace-c.so python3 py/tests/test_wire.py +(cd rust && cargo test) # links ../build ``` -Cases: `01_basic.c`, `02_cpp.cpp`, `03_python.py`, `04_rust.rs`, `05_integrity.c`, `06_multiprocess.c`. +Cross-language tutorials live in the [`kvspace`](../kvspace) repo (`tutorial-c/`). ## Language wrappers diff --git a/deps.json b/deps.json index 3a2eaaf..df07c4b 100644 --- a/deps.json +++ b/deps.json @@ -1,4 +1,5 @@ { "blockmalloc": "v0.1.5", + "kvspace": "v0.3.23", "slotsboxmalloc": "v0.1.7" } diff --git a/py/kvspace/__init__.py b/py/kvspace/__init__.py index ea741d2..86dc955 100644 --- a/py/kvspace/__init__.py +++ b/py/kvspace/__init__.py @@ -28,29 +28,31 @@ def _bind(fn, argtypes, restype): _bind(_lib.kvspaceShmSet, [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_int32], ctypes.c_int) _bind(_lib.kvspaceShmDel, [ctypes.c_void_p, ctypes.c_char_p], ctypes.c_int) _bind(_lib.kvspaceShmDeltree, [ctypes.c_void_p, ctypes.c_char_p], ctypes.c_int) -_bind(_lib.kvspaceShmMkindex, [ctypes.c_void_p, ctypes.c_char_p], ctypes.c_int) +_bind(_lib.kvspaceShmMkindex, [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint32], ctypes.c_int) _bind(_lib.kvspaceShmList, [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_bool, ctypes.c_int, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_int32)], ctypes.c_int) -_bind(_lib.kvspaceShmExtindex, [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p], ctypes.c_int) +_bind(_lib.kvspaceShmExtindex, [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, + ctypes.c_char_p, ctypes.c_uint32], ctypes.c_int) _bind(_lib.kvspaceShmDelextindex, [ctypes.c_void_p, ctypes.c_char_p], ctypes.c_int) _bind(_lib.kvspaceShmNotify, [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_int32], ctypes.c_int) _bind(_lib.kvspaceShmWatch, [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int32, ctypes.POINTER(ctypes.c_int32)], ctypes.POINTER(ctypes.c_uint8)) # ── 权威 head 构造器绑定(kvspace ABI,无 handle)────────────────── -# #566:head 固定 8 字节 [len:u48][storetype][ptr],body 偏移恒 8。直接 memcpy -# struct + body——构造走 C 构造器 kvspaceXvalueHeadNew*,读取本地 memcpy 固定头 + 字段访问。 +# #566/#571: Fixed 8-byte header [len:u48][storetype][ptr], body at offset 8, attr at +# 8+len ([attr_len:u16][records], record = [tag:u8][len:u16][data], tag 1 = langtype). +# Constructed via C kvspaceXvalueHeadNew*; parsed locally on read. _U8P = ctypes.POINTER(ctypes.c_uint8) _U8PP = ctypes.POINTER(_U8P) _U64P = ctypes.POINTER(ctypes.c_uint64) _bind(_lib.kvspaceXvalueHeadNewScalar, - [ctypes.c_uint8, _U8P, ctypes.c_uint32, _U8PP, _U64P], ctypes.c_int) + [ctypes.c_uint8, _U8P, ctypes.c_uint32, ctypes.c_char_p, _U8PP, _U64P], ctypes.c_int) _bind(_lib.kvspaceXvalueHeadNewSlack, - [_U8P, ctypes.c_uint64, ctypes.c_uint64, _U8PP, _U64P], ctypes.c_int) + [_U8P, ctypes.c_uint64, ctypes.c_uint64, ctypes.c_char_p, _U8PP, _U64P], ctypes.c_int) _bind(_lib.kvspaceXvalueHeadNewPtr, - [_U8P, ctypes.c_uint32, _U8PP, _U64P], ctypes.c_int) + [_U8P, ctypes.c_uint32, ctypes.c_char_p, _U8PP, _U64P], ctypes.c_int) _bind(_lib.kvspaceXvalueHeadNewExt, - [_U8P, ctypes.c_uint32, _U8PP, _U64P], ctypes.c_int) + [_U8P, ctypes.c_uint32, ctypes.c_char_p, _U8PP, _U64P], ctypes.c_int) ST_SHORT, ST_SLACK, ST_LARGE, ST_EXT = 0, 1, 2, 3 PTR_INLINE, PTR_PTR, PTR_EXT = 0, 1, 2 @@ -61,8 +63,11 @@ def _bind(fn, argtypes, restype): # ── XValue helpers ──────────────────────────────────────────────── -# 构造走 C 构造器(输出 frontend malloc,_take 拷出后 free);读取本地 -# memcpy 固定头 + 字段访问。 +# Construct via C constructors (_take copies and frees malloc buffer); parse header + attr locally. +# Empty langtype = untyped (empty attr). + +ATTR_TAG_LANGTYPE = 1 + def _take(out, total: ctypes.c_uint64) -> bytes: data = ctypes.string_at(out, total.value) @@ -74,61 +79,95 @@ def _new(fn, *args) -> bytes: out = _U8P() total = ctypes.c_uint64() if fn(*args, ctypes.byref(out), ctypes.byref(total)): - raise ValueError(f"{fn.__name__} 构造失败") + raise ValueError(f"{fn.__name__} failed") return _take(out, total) -def _xv_scalar(raw: bytes) -> bytes: - buf = (ctypes.c_uint8 * len(raw)).from_buffer_copy(raw) if raw else None - return _new(_lib.kvspaceXvalueHeadNewScalar, ST_SHORT, - ctypes.cast(buf, _U8P) if buf is not None else None, len(raw)) +def _u8buf(raw: bytes): + if not raw: + return None + return ctypes.cast((ctypes.c_uint8 * len(raw)).from_buffer_copy(raw), _U8P) -def _xv_slack(raw: bytes) -> bytes: - buf = (ctypes.c_uint8 * len(raw)).from_buffer_copy(raw) if raw else None - return _new(_lib.kvspaceXvalueHeadNewSlack, - ctypes.cast(buf, _U8P) if buf is not None else None, len(raw), len(raw)) +def _xv_scalar(raw: bytes, langtype: str) -> bytes: + return _new(_lib.kvspaceXvalueHeadNewScalar, ST_SHORT, _u8buf(raw), len(raw), + langtype.encode()) -def _xv_from_wire(data: Optional[bytes]) -> tuple[int, int, bytes]: - """读固定头 → (ref, storetype, body)。langtype 不在 head,kind 无从解析。""" - if not data or len(data) < HEAD_LEN: - return (0, 0, b"") +def _xv_slack(raw: bytes, langtype: str) -> bytes: + return _new(_lib.kvspaceXvalueHeadNewSlack, _u8buf(raw), len(raw), len(raw), + langtype.encode()) + + +def _split_wire(data: Optional[bytes]): + """Validate and split wire into (storetype, ptr, body, attr); returns None if malformed.""" + if not data or len(data) < HEAD_LEN + 2: + return None body_len = int.from_bytes(data[:6], "little") storetype, ptr = data[6], data[7] - if storetype > ST_EXT or ptr > PTR_EXT or HEAD_LEN + body_len != len(data): + if storetype > ST_EXT or ptr > PTR_EXT or HEAD_LEN + body_len + 2 > len(data): + return None + a = HEAD_LEN + body_len + attr_len = int.from_bytes(data[a:a + 2], "little") + if attr_len < 2 or a + attr_len != len(data): + return None + return storetype, ptr, data[HEAD_LEN:a], data[a:a + attr_len] + + +def _xv_from_wire(data: Optional[bytes]) -> tuple[int, int, bytes]: + """Read fixed header -> (ref, storetype, body) excluding attr; returns (0, 0, b"") if invalid.""" + parts = _split_wire(data) + if parts is None: return (0, 0, b"") + storetype, ptr, body, _ = parts ref = REF_PTR if ptr == PTR_PTR else REF_EXT if ptr == PTR_EXT else REF_INLINE - return (ref, storetype, data[HEAD_LEN:]) + return (ref, storetype, body) + + +def xv_langtype(data: Optional[bytes]) -> str: + """Read attr tag=1 (langtype) record; returns "" if untyped or invalid.""" + parts = _split_wire(data) + if parts is None: + return "" + attr = parts[3] + p = 2 + while p + 3 <= len(attr): + tag, rlen = attr[p], int.from_bytes(attr[p + 1:p + 3], "little") + if p + 3 + rlen > len(attr): + return "" + if tag == ATTR_TAG_LANGTYPE: + return attr[p + 3:p + 3 + rlen].decode() + p += 3 + rlen + return "" -def xv_int(v: int) -> bytes: - return _xv_scalar(struct.pack(" bytes: + return _xv_scalar(struct.pack(" bytes: - return _xv_scalar(struct.pack(" bytes: + return _xv_scalar(struct.pack(" bytes: - return _xv_slack(s.encode()) +def xv_str(s: str, langtype: str = "") -> bytes: + return _xv_slack(s.encode(), langtype) def xv_index() -> bytes: - """目录值(空 body 的 SHORT 头)。""" - return _xv_scalar(b"") + """Directory value (empty-body SHORT header, untyped).""" + return _xv_scalar(b"", "") -def xv_link(target: str) -> bytes: - """指针值(ptr 位):body=目标 key。langtype 由调用方写 x‥langtype attr key。""" - buf = (ctypes.c_uint8 * len(target)).from_buffer_copy(target.encode()) - return _new(_lib.kvspaceXvalueHeadNewPtr, buf, len(target)) +def xv_link(target: str, langtype: str = "") -> bytes: + """Pointer value (ptr=PTR): body=target key, langtype=target constraint ("" = unconstrained).""" + t = target.encode() + return _new(_lib.kvspaceXvalueHeadNewPtr, _u8buf(t), len(t), langtype.encode()) -def xv_ext(locator: str) -> bytes: - """外部定位符(EXT class):body=locator。langtype 由调用方写 x‥langtype attr key。""" - buf = (ctypes.c_uint8 * len(locator)).from_buffer_copy(locator.encode()) - return _new(_lib.kvspaceXvalueHeadNewExt, buf, len(locator)) +def xv_ext(locator: str, langtype: str = "") -> bytes: + """External locator (EXT class): body=locator.""" + loc = locator.encode() + return _new(_lib.kvspaceXvalueHeadNewExt, _u8buf(loc), len(loc), langtype.encode()) # ── KVSpace ───────────────────────────────────────────────────── @@ -177,22 +216,29 @@ def deltree(self, prefix: str): # ── Directory ──────────────────────────────────────────── - def mkindex(self, path: str): - _lib.kvspaceShmMkindex(self._kv, path.encode()) + def mkindex(self, path: str, capacity: int = 0): + _lib.kvspaceShmMkindex(self._kv, path.encode(), capacity) def list(self, prefix: str, resolve: bool = True) -> list[str]: out = ctypes.c_void_p() oc = ctypes.c_int32(0) _lib.kvspaceShmList(self._kv, prefix.encode(), False, 1 if resolve else 0, ctypes.byref(out), ctypes.byref(oc)) - if oc.value == 0: + if not out.value: return [] - ptrs = ctypes.cast(out, ctypes.POINTER(ctypes.c_char_p)) - return [ptrs[i].decode() for i in range(oc.value)] + # Copy and free the C-allocated name array. + ptrs = ctypes.cast(out, ctypes.POINTER(ctypes.c_void_p)) + names = [ctypes.string_at(ptrs[i]).decode() for i in range(oc.value)] + for i in range(oc.value): + _libc.free(ptrs[i]) + _libc.free(out) + return names # ── Link / ExtIndex ────────────────────────────────────── def extindex(self, path: str, extpath: str): - _lib.kvspaceShmExtindex(self._kv, path.encode(), extpath.encode()) + err = ctypes.create_string_buffer(256) + if _lib.kvspaceShmExtindex(self._kv, path.encode(), extpath.encode(), err, len(err)): + raise RuntimeError(err.value.decode() or f"extindex({path}, {extpath}) failed") def delextindex(self, path: str): _lib.kvspaceShmDelextindex(self._kv, path.encode()) diff --git a/py/tests/test_wire.py b/py/tests/test_wire.py index ade041c..000dbcb 100644 --- a/py/tests/test_wire.py +++ b/py/tests/test_wire.py @@ -1,48 +1,49 @@ #!/usr/bin/env python3 -"""wrapper ↔ C 构造器双向对拍 + ShmSet 读回。 - -#566:head 固定 8 字节 [len:u48][storetype][ptr],body 偏移恒 8。直接 memcpy -struct + body——C 侧权威是 kvspaceXvalueHeadNew* 构造器;wrapper 构造走同一构造器, -读取本地 memcpy 固定头 + 字段访问。断言 ref/storetype/body。 +"""wrapper <-> C constructor round-trip + ShmSet readback. +#566/#571: Fixed 8-byte header [len:u48][storetype][ptr], body at offset 8, attr at 8+len +([attr_len:u16][records], tag 1 = langtype), total = 8 + len + attr_len. +Constructed via kvspaceXvalueHeadNew*; parsed locally on read. """ import ctypes, os, struct, sys, tempfile from pathlib import Path - sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import kvspace from kvspace import (xv_int, xv_float, xv_str, xv_index, xv_link, xv_ext, - _xv_from_wire, KVSpace, ST_SHORT, ST_SLACK, ST_EXT, + xv_langtype, _xv_from_wire, KVSpace, ST_SHORT, ST_SLACK, ST_EXT, PTR_INLINE, PTR_PTR, PTR_EXT, HEAD_LEN) - _lib = ctypes.CDLL(kvspace._SO) _libc = ctypes.CDLL(None) _libc.free.argtypes = [ctypes.c_void_p] - _U8P = ctypes.POINTER(ctypes.c_uint8) _U8PP = ctypes.POINTER(_U8P) _U64P = ctypes.POINTER(ctypes.c_uint64) -_lib.kvspaceXvalueHeadNewScalar.argtypes = [ctypes.c_uint8, _U8P, ctypes.c_uint32, _U8PP, _U64P] +_CS = ctypes.c_char_p +_lib.kvspaceXvalueHeadNewScalar.argtypes = [ctypes.c_uint8, _U8P, ctypes.c_uint32, _CS, _U8PP, _U64P] _lib.kvspaceXvalueHeadNewScalar.restype = ctypes.c_int -_lib.kvspaceXvalueHeadNewSlack.argtypes = [_U8P, ctypes.c_uint64, ctypes.c_uint64, _U8PP, _U64P] +_lib.kvspaceXvalueHeadNewSlack.argtypes = [_U8P, ctypes.c_uint64, ctypes.c_uint64, _CS, _U8PP, _U64P] _lib.kvspaceXvalueHeadNewSlack.restype = ctypes.c_int -_lib.kvspaceXvalueHeadNewPtr.argtypes = [_U8P, ctypes.c_uint32, _U8PP, _U64P] +_lib.kvspaceXvalueHeadNewPtr.argtypes = [_U8P, ctypes.c_uint32, _CS, _U8PP, _U64P] _lib.kvspaceXvalueHeadNewPtr.restype = ctypes.c_int -_lib.kvspaceXvalueHeadNewExt.argtypes = [_U8P, ctypes.c_uint32, _U8PP, _U64P] +_lib.kvspaceXvalueHeadNewExt.argtypes = [_U8P, ctypes.c_uint32, _CS, _U8PP, _U64P] _lib.kvspaceXvalueHeadNewExt.restype = ctypes.c_int -def c_new(storetype, ptr, body): - """C 侧构造(独立 oracle):(storetype, ptr, body) → 固定头 + body。""" +def c_new(storetype, ptr, body, langtype=""): + """C constructor oracle: (storetype, ptr, body, langtype) -> full wire.""" buf = (ctypes.c_uint8 * len(body)).from_buffer_copy(body) if body else None rp = ctypes.cast(buf, _U8P) if buf is not None else None + lt = langtype.encode() out = _U8P() total = ctypes.c_uint64() if ptr == PTR_PTR: - rc = _lib.kvspaceXvalueHeadNewPtr(rp, len(body), ctypes.byref(out), ctypes.byref(total)) + rc = _lib.kvspaceXvalueHeadNewPtr(rp, len(body), lt, ctypes.byref(out), ctypes.byref(total)) elif ptr == PTR_EXT: - rc = _lib.kvspaceXvalueHeadNewExt(rp, len(body), ctypes.byref(out), ctypes.byref(total)) + rc = _lib.kvspaceXvalueHeadNewExt(rp, len(body), lt, ctypes.byref(out), ctypes.byref(total)) + elif storetype == ST_SLACK: + rc = _lib.kvspaceXvalueHeadNewSlack(rp, len(body), len(body), lt, + ctypes.byref(out), ctypes.byref(total)) else: - rc = _lib.kvspaceXvalueHeadNewScalar(storetype, rp, len(body), + rc = _lib.kvspaceXvalueHeadNewScalar(storetype, rp, len(body), lt, ctypes.byref(out), ctypes.byref(total)) assert rc == 0, f"c_new(st={storetype}, ptr={ptr}) rc={rc}" data = ctypes.string_at(out, total.value) @@ -51,13 +52,20 @@ def c_new(storetype, ptr, body): def c_peek(data): - """本地解析固定头 → (ref, storetype, body)。""" - assert len(data) >= HEAD_LEN, "短于 8 字节头" + """Independent wire parser -> (ref, storetype, body, langtype) with total length check.""" + assert len(data) >= HEAD_LEN + 2, "shorter than 8-byte header + empty attr" body_len = int.from_bytes(data[:6], "little") storetype, ptr = data[6], data[7] - assert HEAD_LEN + body_len == len(data), f"总长不匹配 {len(data)}" + a = HEAD_LEN + body_len + attr_len = int.from_bytes(data[a:a + 2], "little") + assert a + attr_len == len(data), f"total length mismatch {len(data)} != {a}+{attr_len}" + lt = "" + if attr_len > 2: + assert data[a + 2] == 1, "first attr record must be langtype" + rlen = int.from_bytes(data[a + 3:a + 5], "little") + lt = data[a + 5:a + 5 + rlen].decode() ref = 1 if ptr == PTR_PTR else 2 if ptr == PTR_EXT else 0 - return (ref, storetype, data[HEAD_LEN:]) + return (ref, storetype, data[HEAD_LEN:a], lt) _fails = 0 @@ -75,71 +83,49 @@ def check(name, got, want): def main(): - print("── wrapper 构造 → C 头解析 ──") - r, st, body = c_peek(xv_int(42)) - check("int64.ref", r, 0) - check("int64.storetype", st, ST_SHORT) - check("int64.body", body, struct.pack(" independent parse --") + check("int64", c_peek(xv_int(42, "int64")), (0, ST_SHORT, struct.pack(" wrapper read --") + v = c_new(ST_SHORT, PTR_INLINE, struct.pack(" i32; - // Head 构造(#566:head 固定 8 字节 [len:u48][storetype][ptr],直接 memcpy - // struct + body——构造器输出 frontend malloc,由调用方 free)。 + // Header constructors (#566/#571: fixed 8-byte header + body + attr region; + // empty string = untyped; malloc-allocated output freed by caller). pub fn kvspaceXvalueHeadNewScalar( storetype: u8, raw: *const u8, raw_len: u32, + langtype: *const c_char, out: *mut *mut u8, out_len: *mut u64, ) -> i32; @@ -64,18 +67,21 @@ mod ffi { data: *const u8, data_len: u64, cap: u64, + langtype: *const c_char, out: *mut *mut u8, out_len: *mut u64, ) -> i32; pub fn kvspaceXvalueHeadNewPtr( path: *const u8, path_len: u32, + langtype: *const c_char, out: *mut *mut u8, out_len: *mut u64, ) -> i32; pub fn kvspaceXvalueHeadNewExt( locator: *const u8, locator_len: u32, + langtype: *const c_char, out: *mut *mut u8, out_len: *mut u64, ) -> i32; @@ -86,9 +92,9 @@ mod ffi { // ── xvalue helpers ───────────────────────────────────────────── // -// #566:head 固定 8 字节 [len:u48][storetype][ptr],body 偏移恒 8。直接 memcpy -// struct + body——构造走 C 构造器 kvspaceXvalueHeadNew*,读取本地 memcpy 固定头 + 字段访问。 -// langtype 不在 head(x‥langtype attr key),固定头读取只给存储元数据。 +// #566/#571: Fixed 8-byte header [len:u48][storetype][ptr], body at offset 8, attr at 8+len +// ([attr_len:u16][records], tag 1 = langtype), total = 8 + len + attr_len. Constructed via C +// kvspaceXvalueHeadNew*; helpers here write untyped values (empty attr). pub mod xvalue { use super::ffi; @@ -101,8 +107,10 @@ pub mod xvalue { pub const PTR_PTR: u8 = 1; pub const PTR_EXT: u8 = 2; pub const HEAD_LEN: usize = 8; + /// Empty langtype (untyped, empty attr). + const NO_TYPE: *const c_char = b"\0".as_ptr() as *const c_char; - /// 取 C 构造器 malloc 输出(to_vec 后 free)。 + /// Copy and free malloc buffer returned by C constructor. fn take(out: *mut u8, total: u64) -> Vec { let bytes = unsafe { std::slice::from_raw_parts(out, total as usize).to_vec() }; unsafe { ffi::free(out as *mut std::ffi::c_void) }; @@ -114,7 +122,7 @@ pub mod xvalue { let mut total: u64 = 0; let raw = v.to_le_bytes(); let rc = unsafe { - ffi::kvspaceXvalueHeadNewScalar(ST_SHORT, raw.as_ptr(), 8, &mut out, &mut total) + ffi::kvspaceXvalueHeadNewScalar(ST_SHORT, raw.as_ptr(), 8, NO_TYPE, &mut out, &mut total) }; if rc != 0 { return Vec::new(); @@ -126,7 +134,7 @@ pub mod xvalue { let mut total: u64 = 0; let raw = v.to_le_bytes(); let rc = unsafe { - ffi::kvspaceXvalueHeadNewScalar(ST_SHORT, raw.as_ptr(), 8, &mut out, &mut total) + ffi::kvspaceXvalueHeadNewScalar(ST_SHORT, raw.as_ptr(), 8, NO_TYPE, &mut out, &mut total) }; if rc != 0 { return Vec::new(); @@ -141,6 +149,7 @@ pub mod xvalue { s.as_ptr(), s.len() as u64, s.len() as u64, + NO_TYPE, &mut out, &mut total, ) @@ -150,31 +159,31 @@ pub mod xvalue { } take(out, total) } - /// 目录值(空 body 的 SHORT 头)。 + /// Directory value (empty-body SHORT header). pub fn index() -> Vec { let mut out: *mut u8 = ptr::null_mut(); let mut total: u64 = 0; let rc = unsafe { - ffi::kvspaceXvalueHeadNewScalar(ST_SHORT, ptr::null(), 0, &mut out, &mut total) + ffi::kvspaceXvalueHeadNewScalar(ST_SHORT, ptr::null(), 0, NO_TYPE, &mut out, &mut total) }; if rc != 0 { return Vec::new(); } take(out, total) } - /// 指针值(ptr 位):body = 目标 key。langtype 由调用方写 x‥langtype attr key。 + /// Pointer value (ptr=PTR): body = target key. pub fn link(target: &str) -> Vec { let mut out: *mut u8 = ptr::null_mut(); let mut total: u64 = 0; let rc = unsafe { - ffi::kvspaceXvalueHeadNewPtr(target.as_ptr(), target.len() as u32, &mut out, &mut total) + ffi::kvspaceXvalueHeadNewPtr(target.as_ptr(), target.len() as u32, NO_TYPE, &mut out, &mut total) }; if rc != 0 { return Vec::new(); } take(out, total) } - /// 外部定位符(EXT class):body = locator。langtype 由调用方写 x‥langtype attr key。 + /// External locator (EXT class): body = locator. pub fn ext(locator: &str) -> Vec { let mut out: *mut u8 = ptr::null_mut(); let mut total: u64 = 0; @@ -182,6 +191,7 @@ pub mod xvalue { ffi::kvspaceXvalueHeadNewExt( locator.as_ptr(), locator.len() as u32, + NO_TYPE, &mut out, &mut total, ) @@ -192,20 +202,28 @@ pub mod xvalue { take(out, total) } - /// 读固定头 → (ref, storetype, body)。ref 由 ptr 位推导;body 借用 data。 - /// langtype 不在 head,kind 无从解析(读 x‥langtype attr key 是调用方的事)。 + /// Read fixed header -> (ref, storetype, body) excluding attr; returns (0, 0, &[]) if invalid. pub fn from_wire(data: &[u8]) -> (u8, u8, &[u8]) { - if data.len() < HEAD_LEN { + if data.len() < HEAD_LEN + 2 { return (0, 0, &[]); } - let len6 = &data[..6]; let mut body_len: u64 = 0; - for (i, b) in len6.iter().enumerate() { + for (i, b) in data[..6].iter().enumerate() { body_len |= (*b as u64) << (8 * i); } let storetype = data[6]; let ptr = data[7]; - if storetype > ST_EXT || ptr > PTR_EXT || HEAD_LEN + body_len as usize > data.len() { + let Ok(blen) = usize::try_from(body_len) else { + return (0, 0, &[]); + }; + let Some(a) = HEAD_LEN.checked_add(blen) else { + return (0, 0, &[]); + }; + if storetype > ST_EXT || ptr > PTR_EXT || a + 2 > data.len() { + return (0, 0, &[]); + } + let attr_len = u16::from_le_bytes([data[a], data[a + 1]]) as usize; + if attr_len < 2 || a + attr_len != data.len() { return (0, 0, &[]); } let r#ref = if ptr == PTR_PTR { @@ -215,7 +233,7 @@ pub mod xvalue { } else { 0 }; - (r#ref, storetype, &data[HEAD_LEN..]) + (r#ref, storetype, &data[HEAD_LEN..a]) } } @@ -301,7 +319,7 @@ impl KVSpace { let cp = CString::new(path).unwrap(); let ce = CString::new(extpath).unwrap(); unsafe { - ffi::kvspaceShmExtindex(self.ptr, cp.as_ptr(), ce.as_ptr()); + ffi::kvspaceShmExtindex(self.ptr, cp.as_ptr(), ce.as_ptr(), ptr::null_mut(), 0); } } } diff --git a/rust/tests/wire.rs b/rust/tests/wire.rs index ad389d5..a51f571 100644 --- a/rust/tests/wire.rs +++ b/rust/tests/wire.rs @@ -1,69 +1,94 @@ -//! Wrapper ↔ C 构造器双向对拍。 +//! Wrapper <-> C constructor round-trip tests. //! -//! #566:head 固定 8 字节 [len:u48][storetype][ptr],body 偏移恒 8。直接 memcpy -//! struct + body——C 侧权威是 kvspaceXvalueHeadNew* 构造器;Rust 侧 xvalue::* 走同一构造器, -//! from_wire 本地 memcpy 固定头 + 字段访问。断言 storetype/ref(ptr)/body 一致。 - +//! #566/#571: Fixed 8-byte header [len:u48][storetype][ptr], body at offset 8, attr at 8+len +//! ([attr_len:u16][records], tag 1 = langtype), total = 8 + len + attr_len. Constructed via C +//! kvspaceXvalueHeadNew*; parsed locally via xvalue::from_wire. use kvspace_c::xvalue; - +use std::os::raw::c_char; extern "C" { fn kvspaceXvalueHeadNewScalar( storetype: u8, raw: *const u8, raw_len: u32, + langtype: *const c_char, + out: *mut *mut u8, + out_len: *mut u64, + ) -> i32; + fn kvspaceXvalueHeadNewSlack( + data: *const u8, + data_len: u64, + cap: u64, + langtype: *const c_char, out: *mut *mut u8, out_len: *mut u64, ) -> i32; fn kvspaceXvalueHeadNewPtr( path: *const u8, path_len: u32, + langtype: *const c_char, out: *mut *mut u8, out_len: *mut u64, ) -> i32; fn kvspaceXvalueHeadNewExt( locator: *const u8, locator_len: u32, + langtype: *const c_char, out: *mut *mut u8, out_len: *mut u64, ) -> i32; fn free(p: *mut std::ffi::c_void); } - -/// C 侧构造(独立 oracle):(storetype, ptr, body) → 8 字节固定头 + body。 -fn c_new(storetype: u8, ptr: u8, body: &[u8]) -> Vec { +/// C constructor oracle: (storetype, ptr, body, langtype) -> full wire. +fn c_new(storetype: u8, ptr: u8, body: &[u8], langtype: &str) -> Vec { + let lt = std::ffi::CString::new(langtype).unwrap(); let mut out: *mut u8 = std::ptr::null_mut(); let mut total: u64 = 0; + let bp = if body.is_empty() { std::ptr::null() } else { body.as_ptr() }; let rc = match ptr { - xvalue::PTR_INLINE if body.is_empty() => unsafe { - kvspaceXvalueHeadNewScalar(storetype, std::ptr::null(), 0, &mut out, &mut total) + xvalue::PTR_INLINE if storetype == xvalue::ST_SLACK => unsafe { + kvspaceXvalueHeadNewSlack( + bp, + body.len() as u64, + body.len() as u64, + lt.as_ptr(), + &mut out, + &mut total, + ) }, xvalue::PTR_INLINE => unsafe { - kvspaceXvalueHeadNewScalar(storetype, body.as_ptr(), body.len() as u32, &mut out, &mut total) + kvspaceXvalueHeadNewScalar(storetype, bp, body.len() as u32, lt.as_ptr(), &mut out, &mut total) }, xvalue::PTR_PTR => unsafe { - kvspaceXvalueHeadNewPtr(body.as_ptr(), body.len() as u32, &mut out, &mut total) + kvspaceXvalueHeadNewPtr(bp, body.len() as u32, lt.as_ptr(), &mut out, &mut total) }, xvalue::PTR_EXT => unsafe { - kvspaceXvalueHeadNewExt(body.as_ptr(), body.len() as u32, &mut out, &mut total) + kvspaceXvalueHeadNewExt(bp, body.len() as u32, lt.as_ptr(), &mut out, &mut total) }, _ => panic!("bad ptr {ptr}"), }; - assert_eq!(rc, 0, "c_new(st={storetype}, ptr={ptr}) 失败"); + assert_eq!(rc, 0, "c_new(st={storetype}, ptr={ptr}) failed"); let v = unsafe { std::slice::from_raw_parts(out, total as usize).to_vec() }; unsafe { free(out as *mut std::ffi::c_void) }; v } - -/// 本地解析固定头 → (ref, storetype, body)。 -fn c_peek(data: &[u8]) -> (u8, u8, &[u8]) { - assert!(data.len() >= xvalue::HEAD_LEN, "短于 8 字节头"); +/// Independent wire parser -> (ref, storetype, body, langtype) with total length check. +fn c_peek(data: &[u8]) -> (u8, u8, Vec, String) { + assert!(data.len() >= xvalue::HEAD_LEN + 2, "shorter than 8-byte header + empty attr"); let mut body_len: u64 = 0; for (i, b) in data[..6].iter().enumerate() { body_len |= (*b as u64) << (8 * i); } let storetype = data[6]; let ptr = data[7]; - assert_eq!(xvalue::HEAD_LEN + body_len as usize, data.len(), "总长不匹配"); + let a = xvalue::HEAD_LEN + body_len as usize; + let attr_len = u16::from_le_bytes([data[a], data[a + 1]]) as usize; + assert_eq!(a + attr_len, data.len(), "total length mismatch"); + let mut lt = String::new(); + if attr_len > 2 { + assert_eq!(data[a + 2], 1, "first attr record must be langtype"); + let rlen = u16::from_le_bytes([data[a + 3], data[a + 4]]) as usize; + lt = String::from_utf8(data[a + 5..a + 5 + rlen].to_vec()).unwrap(); + } let r#ref = if ptr == xvalue::PTR_PTR { 1 } else if ptr == xvalue::PTR_EXT { @@ -71,9 +96,8 @@ fn c_peek(data: &[u8]) -> (u8, u8, &[u8]) { } else { 0 }; - (r#ref, storetype, &data[xvalue::HEAD_LEN..]) + (r#ref, storetype, data[xvalue::HEAD_LEN..a].to_vec(), lt) } - struct Checker { failures: usize, } @@ -87,88 +111,53 @@ impl Checker { } } } - #[test] fn wrapper_reads_c_peeks() { let mut c = Checker { failures: 0 }; - - let raw = 42i64.to_le_bytes(); - let enc = xvalue::int64(42); - let (r, st, body) = c_peek(&enc); - c.eq("int64.ref", r, 0); - c.eq("int64.storetype", st, 0); - c.eq("int64.body", body.to_vec(), raw.to_vec()); - - let enc = xvalue::float64(1.5); - let (r, st, body) = c_peek(&enc); - c.eq("float64.ref", r, 0); - c.eq("float64.storetype", st, 0); - c.eq("float64.body", body.to_vec(), 1.5f64.to_le_bytes().to_vec()); - + let none = String::new(); + c.eq("int64", c_peek(&xvalue::int64(42)), (0, 0, 42i64.to_le_bytes().to_vec(), none.clone())); + c.eq( + "float64", + c_peek(&xvalue::float64(1.5)), + (0, 0, 1.5f64.to_le_bytes().to_vec(), none.clone()), + ); let s = "héllo"; // 5 chars, 6 bytes - let enc = xvalue::string(s); - let (r, st, body) = c_peek(&enc); - c.eq("str.ref", r, 0); - c.eq("str.storetype", st, 1); // slack - c.eq("str.body", body.to_vec(), s.as_bytes().to_vec()); - - let enc = xvalue::index(); - let (r, st, body) = c_peek(&enc); - c.eq("index.ref", r, 0); - c.eq("index.storetype", st, 0); - c.eq("index.body", body.to_vec(), Vec::::new()); - - let enc = xvalue::link("/t/x"); - let (r, st, body) = c_peek(&enc); - c.eq("link.ref", r, 1); // ptr - c.eq("link.storetype", st, 1); // slack|ptr - c.eq("link.body", body.to_vec(), b"/t/x".to_vec()); - - let enc = xvalue::ext("s3://b/k"); - let (r, st, body) = c_peek(&enc); - c.eq("ext.ref", r, 2); // ext - c.eq("ext.storetype", st, 3); - c.eq("ext.body", body.to_vec(), b"s3://b/k".to_vec()); - - assert_eq!(c.failures, 0, "{} 项失败", c.failures); + c.eq("str", c_peek(&xvalue::string(s)), (0, 1, s.as_bytes().to_vec(), none.clone())); + c.eq("index", c_peek(&xvalue::index()), (0, 0, Vec::new(), none.clone())); + c.eq("link", c_peek(&xvalue::link("/t/x")), (1, 1, b"/t/x".to_vec(), none.clone())); + c.eq("ext", c_peek(&xvalue::ext("s3://b/k")), (2, 3, b"s3://b/k".to_vec(), none)); + assert_eq!(c.failures, 0, "{} failures", c.failures); } - #[test] fn c_new_wrapper_reads() { let mut c = Checker { failures: 0 }; - let raw = 7i64.to_le_bytes(); - let enc = c_new(xvalue::ST_SHORT, xvalue::PTR_INLINE, &raw); - let (r, st, body) = xvalue::from_wire(&enc); - c.eq("int64.ref", r, 0); - c.eq("int64.storetype", st, 0); - c.eq("int64.body", body.to_vec(), raw.to_vec()); - - let enc = c_new(xvalue::ST_SLACK, xvalue::PTR_INLINE, "héllo".as_bytes()); - let (r, st, body) = xvalue::from_wire(&enc); - c.eq("str.ref", r, 0); - c.eq("str.storetype", st, 1); - c.eq("str.body", body.to_vec(), "héllo".as_bytes().to_vec()); - - // 指针:ptr 位 1。 - let enc = c_new(xvalue::ST_SLACK, xvalue::PTR_PTR, b"/t/x"); - let (r, _, body) = xvalue::from_wire(&enc); - c.eq("ptr.ref", r, 1); - c.eq("ptr.body", body.to_vec(), b"/t/x".to_vec()); - - // 外部定位符:ptr 位 2。 - let enc = c_new(xvalue::ST_EXT, xvalue::PTR_EXT, b"s3://b/k"); - let (r, st, body) = xvalue::from_wire(&enc); - c.eq("ext.ref", r, 2); - c.eq("ext.storetype", st, 3); - c.eq("ext.body", body.to_vec(), b"s3://b/k".to_vec()); - - // 垃圾输入 → 空。 - let garbage = [0u8, 1, 2, 3]; - let (r, st, body) = xvalue::from_wire(&garbage); - c.eq("garbage.ref", r, 0); - c.eq("garbage.storetype", st, 0); - c.eq("garbage.body", body.to_vec(), Vec::::new()); - - assert_eq!(c.failures, 0, "{} 项失败", c.failures); + let enc = c_new(xvalue::ST_SHORT, xvalue::PTR_INLINE, &raw, "int64"); + c.eq("int64", xvalue::from_wire(&enc), (0, 0, &raw[..])); + let enc = c_new(xvalue::ST_SLACK, xvalue::PTR_INLINE, "héllo".as_bytes(), "char/utf8"); + c.eq("str", xvalue::from_wire(&enc), (0, 1, "héllo".as_bytes())); + // Pointer: ptr=1. + let enc = c_new(xvalue::ST_SLACK, xvalue::PTR_PTR, b"/t/x", "int64"); + c.eq("ptr", xvalue::from_wire(&enc), (1, 1, &b"/t/x"[..])); + // External locator: ptr=2. + let enc = c_new(xvalue::ST_EXT, xvalue::PTR_EXT, b"s3://b/k", ""); + c.eq("ext", xvalue::from_wire(&enc), (2, 3, &b"s3://b/k"[..])); + // Malformed / missing attr -> empty. + c.eq("garbage", xvalue::from_wire(&[0u8, 1, 2, 3]), (0, 0, &[][..])); + c.eq("no-attr", xvalue::from_wire(&[1u8, 0, 0, 0, 0, 0, 0, 0, 42]), (0, 0, &[][..])); + assert_eq!(c.failures, 0, "{} failures", c.failures); +} +#[test] +fn shm_roundtrip() { + let dir = std::env::temp_dir().join(format!("kvspace-c-rs-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("db"); + let kv = kvspace_c::KVSpace::open(path.to_str().unwrap(), 32768).unwrap(); + kv.mkindex("/t/", 0); + kv.set("/t/x", &xvalue::int64(42)); + let v = kv.get("/t/x").unwrap(); + assert_eq!(xvalue::from_wire(&v), (0, 0, &[42, 0, 0, 0, 0, 0, 0, 0][..])); + assert_eq!(kv.list("/t/"), vec!["x".to_string()]); + drop(kv); + let _ = std::fs::remove_dir_all(&dir); } diff --git a/src/durable_abi.c b/src/durable_abi.c index f9f95a7..cda6172 100644 --- a/src/durable_abi.c +++ b/src/durable_abi.c @@ -34,7 +34,7 @@ typedef struct { uint64_t body_cap; } kvspaceHead_t; -/* 固定 8 字节 head → 元数据(langtype 在 body 尾 @8+len,见 kvspaceGetHead)。 */ +/* Decode fixed 8-byte header into metadata (langtype is in attr at 8+len; see kvspaceGetHead). */ static int from_wire_head(const uint8_t *d, uint32_t total, kvspaceHead_t *out) { if (!out || !d || total < KVSPACE_XVALUE_HEAD_LEN) return 1; @@ -56,11 +56,14 @@ static int from_wire_head(const uint8_t *d, uint32_t total, kvspaceHead_t *out) } static int parse_shm_path(const char *dsn, char *out, size_t osz) { + if (!dsn) + return -1; const char *sep = strstr(dsn, "://"); - if (!sep || strncmp(dsn, "shm", (size_t)(sep - dsn)) != 0) + /* Scheme must be exactly "shm" (prefix matching would accept "s://" or "://"). */ + if (!sep || sep - dsn != 3 || strncmp(dsn, "shm", 3) != 0) return -1; - snprintf(out, osz, "%s", sep + 3); - return 0; + int n = snprintf(out, osz, "%s", sep + 3); + return n > 0 && (size_t)n < osz ? 0 : -1; /* reject empty or truncated path */ } void *kvspaceConnect(const char *dsn) { @@ -185,14 +188,12 @@ int kvspaceGetPart(void *h, const char *key, uint32_t offset, uint32_t len, int kvspaceSetPart(void *h, const char *key, uint32_t offset, const uint8_t *buf, uint32_t buf_len, char *err, uint32_t err_cap) { - int32_t total = 0; - uint8_t *d = kvspaceShmGet((kvspace_t *)h, key, 1, &total); - if (!d || total < 0 || offset + buf_len > (uint32_t)total) { + if (kvspaceShmSetPart((kvspace_t *)h, key, offset, buf, buf_len) != 0) { if (err && err_cap) - snprintf(err, err_cap, "kvspace: set-part out of range at %s", key); + snprintf(err, err_cap, "kvspace: set-part out of range at %s", + key ? key : ""); return 1; } - memcpy(d + offset, buf, buf_len); return 0; } @@ -264,85 +265,95 @@ int kvspaceListLen(void *h, const char *prefix, int expand_ext, int resolve, int kvspaceListAt(void *h, const char *prefix, int expand_ext, int resolve, int32_t idx, uint8_t *buf, uint32_t buf_cap, uint32_t *out_len) { - *out_len = 0; - char **names = NULL; - int32_t count = 0; - if (kvspaceShmList((kvspace_t *)h, prefix, expand_ext != 0, resolve, &names, - &count) != 0) - return -1; - int rc = -1; - if (idx >= 0 && idx < count) { - size_t l = strlen(names[idx]); - *out_len = (uint32_t)l; - if (buf && l <= buf_cap) { - memcpy(buf, names[idx], l); - rc = 0; - } - } - for (int32_t i = 0; i < count; i++) - free(names[i]); - free(names); - return rc; + return kvspaceShmListAt((kvspace_t *)h, prefix, expand_ext != 0, resolve, + idx, buf, buf_cap, out_len); } int kvspaceDel(void *h, const char *const *keys, uint32_t nkeys, char *err, uint32_t err_cap) { - (void)err; - (void)err_cap; - for (uint32_t i = 0; i < nkeys; i++) + if (!h || (!keys && nkeys > 0)) { + if (err && err_cap) + snprintf(err, err_cap, "kvspace: invalid arguments to del"); + return 1; + } + for (uint32_t i = 0; i < nkeys; i++) { + if (!keys[i]) { + if (err && err_cap) + snprintf(err, err_cap, "kvspace: null key in del at index %u", i); + return 1; + } + /* Missing keys and >=1024-byte keys are idempotent no-ops in KV del. */ kvspaceShmDel((kvspace_t *)h, keys[i]); + } return 0; } int kvspaceDelTree(void *h, const char *prefix, char *err, uint32_t err_cap) { - (void)err; - (void)err_cap; - return kvspaceShmDeltree((kvspace_t *)h, prefix); + int rc = kvspaceShmDeltree((kvspace_t *)h, prefix); + if (rc != 0 && err && err_cap) + snprintf(err, err_cap, "kvspace: deltree failed at %s", + prefix ? prefix : "(null)"); + return rc; } int kvspaceCp(void *h, const char *src, const char *dst, char *err, uint32_t err_cap) { - (void)err; - (void)err_cap; - return kvspaceShmCp((kvspace_t *)h, src, dst); + int rc = kvspaceShmCp((kvspace_t *)h, src, dst); + if (rc != 0 && err && err_cap) + snprintf(err, err_cap, "kvspace: cp failed from %s to %s", + src ? src : "(null)", dst ? dst : "(null)"); + return rc; } int kvspaceCpTree(void *h, const char *src, const char *dst, char *err, - uint32_t err_cap) { - (void)err; - (void)err_cap; - return kvspaceShmCptree((kvspace_t *)h, src, dst); + uint32_t err_cap) { + int rc = kvspaceShmCptree((kvspace_t *)h, src, dst); + if (rc != 0 && err && err_cap) + snprintf(err, err_cap, "kvspace: cptree failed from %s to %s", + src ? src : "(null)", dst ? dst : "(null)"); + return rc; } int kvspaceCpList(void *h, const char *src, const char *dst, char *err, - uint32_t err_cap) { - (void)err; - (void)err_cap; - return kvspaceShmCplist((kvspace_t *)h, src, dst); + uint32_t err_cap) { + int rc = kvspaceShmCplist((kvspace_t *)h, src, dst); + if (rc != 0 && err && err_cap) + snprintf(err, err_cap, "kvspace: cplist failed from %s to %s", + src ? src : "(null)", dst ? dst : "(null)"); + return rc; } int kvspaceMkindex(void *h, const char *path, uint32_t capacity, char *err, uint32_t err_cap) { - (void)err; - (void)err_cap; - return kvspaceShmMkindex((kvspace_t *)h, path, capacity); + int rc = kvspaceShmMkindex((kvspace_t *)h, path, capacity); + if (rc != 0 && err && err_cap) + snprintf(err, err_cap, "kvspace: mkindex failed at %s", + path ? path : "(null)"); + return rc; } int kvspaceMkindexExt(void *h, const char *path, const char *ext_path, char *err, uint32_t err_cap) { - return kvspaceShmExtindex((kvspace_t *)h, path, ext_path, err, err_cap); + int rc = kvspaceShmExtindex((kvspace_t *)h, path, ext_path, err, err_cap); + if (rc != 0 && err && err_cap && err[0] == '\0') + snprintf(err, err_cap, "kvspace: mkindex-ext failed at %s -> %s", + path ? path : "(null)", ext_path ? ext_path : "(null)"); + return rc; } int kvspaceRmindexExt(void *h, const char *path, char *err, uint32_t err_cap) { - (void)err; - (void)err_cap; - return kvspaceShmDelextindex((kvspace_t *)h, path); + int rc = kvspaceShmDelextindex((kvspace_t *)h, path); + if (rc != 0 && err && err_cap) + snprintf(err, err_cap, "kvspace: rmindex-ext failed at %s", + path ? path : "(null)"); + return rc; } int kvspaceClear(void *h, char *err, uint32_t err_cap) { - (void)err; - (void)err_cap; - return kvspaceShmDeltree((kvspace_t *)h, "/"); + int rc = kvspaceShmDeltree((kvspace_t *)h, "/"); + if (rc != 0 && err && err_cap) + snprintf(err, err_cap, "kvspace: clear failed"); + return rc; } /* New* 统一收口:New* 构造 head+body(malloc),total 超 u32 上限即失败。 */ @@ -416,6 +427,21 @@ int kvspaceNewFloat64(double v, const char *langtype, return rc == 0 ? finish(value, total, out, out_len) : 1; } +static int wire_is_none(const uint8_t *d, uint32_t len) { + if (!d || len == 0) + return 1; + if (len < KVSPACE_XVALUE_HEAD_LEN) + return 0; + const kvspaceXvalueHead *xh = (const kvspaceXvalueHead *)d; + if (xh->storetype != KVSPACE_XVALUE_ST_SHORT || + xh->ptr != KVSPACE_XVALUE_PTR_INLINE || + kvspaceXvalueHeadLen(xh) != 0) + return 0; + const uint8_t *lt = NULL; + uint16_t ltn = kvspaceXvalueHeadLangtypeBounded(d, len, <); + return ltn == 0 || (ltn == 4 && memcmp(lt, "None", 4) == 0); +} + int kvspaceWatch(void *h, const char *key, const uint8_t *target, uint32_t target_len, uint64_t tick_ns, uint8_t **out, uint32_t *out_len) { @@ -423,13 +449,20 @@ int kvspaceWatch(void *h, const char *key, const uint8_t *target, return 1; *out = NULL; *out_len = 0; + const int target_is_none = wire_is_none(target, target_len); struct timespec t0, tn; clock_gettime(CLOCK_MONOTONIC, &t0); for (;;) { int32_t len = 0; uint8_t *d = kvspaceShmGet((kvspace_t *)h, key, 1, &len); - if (d && (uint32_t)len == target_len && - memcmp(d, target, target_len) == 0) { + if (target_is_none) { + if (!d || len <= 0 || wire_is_none(d, (uint32_t)len)) { + *out = d; + *out_len = (d && len > 0) ? (uint32_t)len : 0; + return 0; + } + } else if (d && (uint32_t)len == target_len && + memcmp(d, target, target_len) == 0) { *out = d; *out_len = (uint32_t)len; return 0; diff --git a/src/kvspace.c b/src/kvspace.c index 24dabf9..aa2dbf4 100644 --- a/src/kvspace.c +++ b/src/kvspace.c @@ -162,6 +162,9 @@ typedef struct { uint64_t freed_bytes; /* 只由 leader 改 */ uint64_t garb_freeing; uint64_t round_ok, round_aborted, leak_boxes; + /* v6: Key-set generation. Incremented (release) whenever a key appears (insert/revive) + * or disappears (delete/tombstone) to invalidate per-handle List caches across processes. */ + uint64_t list_gen; uint32_t claim_seq; /* 30 位发号;耗尽 open 硬失败 */ p2_slot_t slots[P2_SLOT_N]; } kvspace_hdr_t; @@ -233,7 +236,23 @@ struct kvspace { uint8_t *sbo_data; watch_t watches[WATCH_TABLE_SZ]; pthread_mutex_t wlock; + /* Per-handle List cache (see list_cached): ListLen + indexed ListAt scans only once. + * Hits when (prefix, ex, resolve), resolved target, extindex mount, and hdr->list_gen + * all match, producing identical results to a fresh scan. */ + pthread_mutex_t lc_mtx; + char *lc_prefix; + char *lc_target; /* resolved target directory (equals prefix when resolve=0) */ + char *lc_ext; /* extindex mount source when ex=1; "" if unmounted */ + char **lc_names; + int32_t lc_count; + uint8_t lc_ex, lc_res; + uint64_t lc_gen; }; +/* Bump after key-set changes (insert/revive/delete) to invalidate List caches across processes. + * Ptr/@ext value changes do not bump this since list_cached re-checks target and mount each call. */ +static inline void list_gen_bump(kvspace_t *kv) { + __atomic_add_fetch(&kv->hdr->list_gen, 1ull, __ATOMIC_RELEASE); +} /* ---- shm region ---- */ static int region_map(shm_region_t *r, size_t size) { @@ -264,8 +283,6 @@ static int region_attach(shm_region_t *r, bool create, size_t size, return region_map(r, size); } -static void region_close(shm_region_t *r); /* fwd */ - static void region_close(shm_region_t *r) { if (r->base) munmap(r->base, r->reserve); @@ -273,11 +290,8 @@ static void region_close(shm_region_t *r) { close(r->fd); } -/* Pick up growth done by another process (mid-op growth: kvspace#11). */ - /* ── P2: 旁路 gen 表(node id → {struct_gen, value_gen})──────────── */ static void kv_sbo_free(kvspace_t *kv, uint64_t off); -static void p2_setstats_dump(void); static art_hdr_t *art_hdr(kvspace_t *kv, int32_t id); static int32_t art_child(kvspace_t *kv, void *n, uint8_t b); static int pfx_shared(const uint8_t *a, int al, const uint8_t *b, int bl); @@ -333,6 +347,7 @@ static int64_t gen_alloc_epoch(kvspace_t *kv) { static int p2_maint_start(kvspace_t *kv); /* 懒启动(阈值触发) */ static bool p2_claim_alive(kvspace_t *kv, uint32_t claim); static int kv_sync(kvspace_t *kv); +static void list_cache_drop(kvspace_t *kv); /* 阈值:生产固定 64MiB;测试构建可用 P2_TEST_GARB_THRESHOLD_MB 调低。 */ static uint64_t p2_garb_threshold(void) { @@ -430,17 +445,13 @@ static int32_t p2_node_at(kvspace_t *kv, const char *key) { int32_t nid = kv->hdr->art_root; while (nid >= 0) { art_hdr_t *h = art_hdr(kv, nid); - if (!h || h->type == ART_MOVED) { - if (getenv("P2_DEBUG")) { fprintf(stderr, "DBG node_at FAIL key=%s nid=%d type=%d d=%d/%d\n", key, nid, h ? (int)h->type : -1, d, klen); } + if (!h || h->type == ART_MOVED) return -1; - } if (h->prefix_len) { int s = pfx_shared(h->prefix, h->prefix_len, (const uint8_t *)key + d, klen - d); - if (s != h->prefix_len && d + s != klen) { - if (getenv("P2_DEBUG")) { fprintf(stderr, "DBG node_at PFX key=%s nid=%d d=%d s=%d plen=%d klen=%d\n", key, nid, d, s, (int)h->prefix_len, klen); } + if (s != h->prefix_len && d + s != klen) return -1; - } d += s; } /* 键可能正好结束在 prefix_len==0 的分支节点上(如 k1 落在根 N4 的子叶), @@ -450,7 +461,6 @@ static int32_t p2_node_at(kvspace_t *kv, const char *key) { nid = art_child(kv, h, (uint8_t)key[d]); d++; } - if (getenv("P2_DEBUG")) { fprintf(stderr, "DBG node_at EOF key=%s d=%d/%d\n", key, d, klen); } return -1; } @@ -533,16 +543,6 @@ static int p2_reg_claim(kvspace_t *kv) { __atomic_compare_exchange_n(&s->state, &v, P2_SLOT_FREE, false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE); } - if (getenv("P2_DEBUG")) { - int nf = 0, nm = 0; - for (int i = 0; i < P2_SLOT_N; i++) { - uint32_t v = __atomic_load_n(&kv->hdr->slots[i].state, __ATOMIC_ACQUIRE); - if (v == P2_SLOT_FREE) nf++; - else if (v == P2_SLOT_MINE) nm++; - } - fprintf(stderr, "DBG claim pid=%u start=%llu free=%d mine=%d\n", me, - (unsigned long long)st, nf, nm); - } for (int i = 0; i < P2_SLOT_N; i++) { p2_slot_t *s = &kv->hdr->slots[i]; uint32_t expect = P2_SLOT_FREE; @@ -624,21 +624,6 @@ static int gens_grow(kvspace_t *kv) { return 0; } - -/* ── P2: tombstone 延迟释放(粗粒度 epoch:队满或显式 flush 时统一 free)── */ - - -int kvspaceShmXvalueBodyView(kvspace_t *kv, const kvspaceRef_t *ref, - const char *key_fallback, kvspaceXvalueBodyView_t *out); -static int32_t art_id_of(kvspace_t *kv, const void *p); -static art_hdr_t *art_hdr(kvspace_t *kv, int32_t id); -static int32_t art_follow(kvspace_t *kv, int32_t id); -static int32_t ref_leaf(kvspace_t *kv, kvspaceRef_t *ref, const char *key); -static int32_t art_walk(kvspace_t *kv, int32_t nid, const uint8_t *key, int klen, - int d, int32_t *par, int *pard); -static int32_t art_child(kvspace_t *kv, void *n, uint8_t b); -static int pfx_shared(const uint8_t *a, int al, const uint8_t *b, int bl); - /* ── P2: 版本化 view token ─────────────────────────────────────── */ /* fork 检测不能每 op 调 getpid()(本机 glibc 不缓存,一次 ~150ns)。 * 用 pthread_atfork 的子进程钩子把全局世代 +1;热路径只比两个 u32。 */ @@ -669,6 +654,22 @@ static int head_of(const uint8_t *val, int32_t val_len, kvspaceXvalueHead *out) return 0; } +/* Write validation: head_of + complete attr region (attr_len >= 2 and within val_len). + * read_value derives total length from 8+len+attr_len without storing val_len, so + * values with an incomplete attr region would be unreadable if accepted. */ +static int value_ok(const uint8_t *val, int32_t val_len, + kvspaceXvalueHead *out) { + if (head_of(val, val_len, out) != 0) + return -1; + uint64_t l = kvspaceXvalueHeadLen(out); + if ((uint64_t)val_len < KVSPACE_XVALUE_HEAD_LEN + l + 2) + return -1; + uint64_t alen = kvspaceXvalueHeadAttrLen(val, l); + if (alen < 2 || alen > (uint64_t)val_len - KVSPACE_XVALUE_HEAD_LEN - l) + return -1; + return 0; +} + int kvspaceShmViewFromRef(kvspace_t *kv, const kvspaceRef_t *ref, const char *key_fallback, kvspaceViewToken_t *tok) { (void)key_fallback; @@ -728,17 +729,20 @@ int kvspaceShmResolvePrefix(kvspace_t *kv, const char *path, kvspaceRef_t *ref) art_hdr_t *h = art_hdr(kv, nid); if (!h || h->type == ART_MOVED) return -1; + int s = 0; if (h->prefix_len) { - int s = pfx_shared(h->prefix, h->prefix_len, - (const uint8_t *)path + d, klen - d); + s = pfx_shared(h->prefix, h->prefix_len, + (const uint8_t *)path + d, klen - d); if (s != h->prefix_len && d + s != klen) return -1; d += s; - if (d == klen) { - ref->block_id = (uint32_t)nid; - ref->depth = (uint32_t)s; /* 该节点前缀里被 path 覆盖的字节数 */ - return 0; - } + } + /* path may end at a node with prefix_len == 0 (e.g. "/vthread/1/" next to + * "/vthread/10/"); return here before reading path[klen]. */ + if (d == klen) { + ref->block_id = (uint32_t)nid; + ref->depth = (uint32_t)s; /* bytes of this node's prefix covered by path */ + return 0; } nid = art_child(kv, h, (uint8_t)path[d]); d++; @@ -802,14 +806,18 @@ int kvspaceShmViewCheck(kvspace_t *kv, const kvspaceViewToken_t *tok) { return 0; } -/* 热路径写:同尺寸就地写(8B inline 情形)。0=成功;1=需要回落(尺寸/版本不符)。 */ +/* Hot-path write: in-place same-length body write. Returns 0 on success, 1 to fall back. + * The attr region immediately follows body (at 8+len), so only writes matching the current + * header len are accepted; variable-length writes fall back to full-value write. */ int kvspaceShmViewWrite(kvspace_t *kv, kvspaceViewToken_t *tok, const uint8_t *data, uint32_t len) { if (!kv || P2_VIEW_GUARD(kv)) return -1; if (kvspaceShmViewCheck(kv, tok) != 0) return 1; - if (len > tok->body_cap) + kvspaceXvalueHead cur; + memcpy(&cur, tok->head, sizeof cur); + if ((uint64_t)len != kvspaceXvalueHeadLen(&cur) || len > tok->body_cap) return 1; kv_gens_t *g = gens_at(kv, (int32_t)tok->node_id); if (!g) @@ -851,6 +859,7 @@ int kvspaceShmViewDel(kvspace_t *kv, kvspaceViewToken_t *tok, const char *key) { } __atomic_add_fetch(&g->value_gen, 2u, __ATOMIC_ACQ_REL); /* 静止态保持偶数 */ garb_commit(kv, gidx, AV_OFF(hv), tok->cap); + list_gen_bump(kv); /* bump after key removal */ (void)key; return 0; } @@ -1240,10 +1249,6 @@ int kvspaceShmViewSweep(kvspace_t *kv) { return p2_round(kv); } -int kvspaceShmViewFlushTomb(kvspace_t *kv) { - return kvspaceShmViewSweep(kv); -} - int kvspaceShmViewRefresh(kvspace_t *kv, kvspaceViewToken_t *tok, const char *key) { kvspaceViewToken_t t; if (kvspaceShmViewResolve(kv, key, &t) != 0) @@ -1280,12 +1285,19 @@ static int art_slab_grow(kvspace_t *kv) { sbo_plock, which excludes sbo_alloc/free/allocated_size in every process. */ /* Free list empty and no room to append. Unlocked calls are a hint only. */ +#ifdef BLOCKMALLOC_COUNT_MASK +#define BM_MALLOC_BLOCKS(m) blocks_malloc_count(m) +#define BM_HEAD_SIZE(m) blocks_head_size(m) +#else +#define BM_MALLOC_BLOCKS(m) ((uint64_t)(m)->malloc_blocks) +#define BM_HEAD_SIZE(m) ((uint64_t)(m)->sizeof_block_head) +#endif static bool sbo_pool_full(sbo_meta_t *m) { blocks_meta_t *p = sbo_pool(m); if (p->free_next_id != -1) return false; - uint64_t next_end = (uint64_t)block_offset(p, p->malloc_blocks) + - p->sizeof_block_head + p->block_size; + uint64_t next_end = (uint64_t)block_offset(p, BM_MALLOC_BLOCKS(p)) + + BM_HEAD_SIZE(p) + p->block_size; return next_end > p->total_size; } @@ -1411,9 +1423,6 @@ static void kv_sbo_free(kvspace_t *kv, uint64_t off) { sbo_free(kv->sbo_meta, off); } -/* P2: tombstone —— 有界 ring(L1 内)+ 后台 flush 线程。 - * 前台只做:ring[tail & MASK] = off; tail++(release); 唤醒后台。 - * 队列满时前台等待(backpressure),绝不在调用线程里 sbo_free。 */ /* ---- helpers ---- */ static void *art_blk(kvspace_t *kv, int32_t id) { if (id < 0) @@ -1421,25 +1430,66 @@ static void *art_blk(kvspace_t *kv, int32_t id) { return kv->art_data + blockdata_offset(kv->art_meta, (uint64_t)id); } /* Bump-allocate k consecutive blocks; the node spans them (interior heads unused). */ +/* blockmalloc packs sizeof_block_head:4 and malloc_blocks:60 into a single u64 word + * between block_size and used_blocks. Lock-free readers (art_blk / art_id_of) read this + * word concurrently, so writers holding meta->lock must update the whole word atomically + * (via blocks_set_malloc_count when available, or whole-word atomic store fallback). */ +_Static_assert(offsetof(blocks_meta_t, used_blocks) == + offsetof(blocks_meta_t, block_size) + 2 * sizeof(uint64_t), + "blockmalloc: sizeof_block_head/malloc_blocks must share one u64 word"); +#ifdef BLOCKMALLOC_COUNT_MASK +static inline void bm_set_malloc_blocks(blocks_meta_t *m, uint64_t n) { + blocks_set_malloc_count(m, n); +} +#else +#define BM_WORD_OFF (offsetof(blocks_meta_t, block_size) + sizeof(uint64_t)) +static unsigned bm_count_shift(void) { + blocks_meta_t t; + memset(&t, 0, sizeof t); + t.malloc_blocks = 1; + uint64_t w; + memcpy(&w, (const char *)&t + BM_WORD_OFF, sizeof w); + return w ? (unsigned)__builtin_ctzll(w) : 64u; +} +/* Caller holds m->lock. */ +static void bm_set_malloc_blocks(blocks_meta_t *m, uint64_t n) { + uint64_t *w = (uint64_t *)((char *)m + BM_WORD_OFF); + const unsigned sh = bm_count_shift(); + const uint64_t mask = ((1ULL << 60) - 1) << sh; + uint64_t old = __atomic_load_n(w, __ATOMIC_RELAXED); + __atomic_store_n(w, (old & ~mask) | ((n << sh) & mask), __ATOMIC_RELEASE); +} +#endif +/* Verify whole-word bitfield layout at open time. */ +static bool bm_layout_ok(void) { + blocks_meta_t t; + memset(&t, 0, sizeof t); + t.sizeof_block_head = 8; + t.malloc_blocks = 5; + t.used_blocks = 7; + bm_set_malloc_blocks(&t, 0x0123456789ABCDEull); + return t.sizeof_block_head == 8 && t.malloc_blocks == 0x0123456789ABCDEull && + t.block_size == 0 && t.used_blocks == 7; +} static int64_t art_bump(kvspace_t *kv, uint32_t k) { blocks_meta_t *m = kv->art_meta; spin_lock(&m->lock); - uint64_t id = m->malloc_blocks; + uint64_t id = BM_MALLOC_BLOCKS(m); if ((uint64_t)block_offset(m, id + k) > m->total_size) { spin_unlock(&m->lock); return -1; } - m->malloc_blocks += k; - m->used_blocks += k; setblock_t(m, kv->art_data + block_offset(m, id), 1, -1); + bm_set_malloc_blocks(m, id + k); + m->used_blocks += k; spin_unlock(&m->lock); return (int64_t)id; } static int32_t art_balloc(kvspace_t *kv, int t) { - const uint64_t stride = (uint64_t)kv->art_meta->sizeof_block_head + - kv->art_meta->block_size; + const uint64_t head = BM_HEAD_SIZE(kv->art_meta); + const uint64_t stride = head + kv->art_meta->block_size; const uint32_t k = (uint32_t)(((uint64_t)art_node_sz(t) + - kv->art_meta->sizeof_block_head + stride - 1) / + head + stride - 1) / stride); int64_t id = art_bump(kv, k); while (id < 0) { @@ -1517,16 +1567,11 @@ static int32_t art_walk(kvspace_t *kv, int32_t nid, const uint8_t *key, int klen if (!h || h->type == ART_MOVED) return -1; if (h->prefix_len) { - int s = pfx_shared(h->prefix, h->prefix_len, key + d, klen - d); - if (s != h->prefix_len) { - if (d + s < klen) - return -1; - if (s < h->prefix_len) - return -1; - } - d += h->prefix_len; - if (d > klen) + /* s == prefix_len implies d + prefix_len <= klen */ + if (pfx_shared(h->prefix, h->prefix_len, key + d, klen - d) != + h->prefix_len) return -1; + d += h->prefix_len; } if (d == klen) return AV_HAS(art_val_load(h)) ? nid : -1; @@ -1750,7 +1795,6 @@ static int32_t art_grow(kvspace_t *kv, void *on) { } { /* P2: 搬迁 —— 新节点 struct_gen = 旧 +1;旧节点也动一次 */ int32_t oid = art_id_of(kv, oh); - (void)gens_at; gen_bump_struct(kv, nid); /* 新节点 gen 先抬一代 */ gen_bump_struct(kv, oid); /* 旧节点也失效 */ } @@ -1994,8 +2038,8 @@ static int32_t art_del(kvspace_t *kv, int32_t nid, const uint8_t *key, int klen, if (!h) return -1; if (h->prefix_len) { - int s = pfx_shared(h->prefix, h->prefix_len, key + d, klen - d); - if (s != h->prefix_len || d + h->prefix_len > klen) + if (pfx_shared(h->prefix, h->prefix_len, key + d, klen - d) != + h->prefix_len) return nid; d += h->prefix_len; } @@ -2294,6 +2338,88 @@ static int art_scan_pfx(kvspace_t *kv, const char *pfx, int plen, char *buf, return art_scan_children(kv, nid, buf, bpos, bcap, plen, scan); } +/* Collect all full keys with values under nid (buf[0, bpos) holds the reconstructed prefix). + * Unlike art_scan_children, does not truncate at child boundaries or hide system names (used by DelTree). */ +static int art_collect_keys(kvspace_t *kv, int32_t nid, char *buf, int bpos, + int bcap, keyscan_t *scan) { + if (nid < 0) + return 0; + art_hdr_t *h = art_hdr(kv, nid); + if (!h || h->prefix_len > bcap - bpos - 1) + return -1; + for (int i = 0; i < h->prefix_len; i++) + buf[bpos++] = h->prefix[i]; + if (AV_HAS(art_val_load(h)) && keyscan_add_n(scan, buf, (size_t)bpos) != 0) + return -1; + const uint8_t *keys = NULL; + const int32_t *children = NULL; + int n = 0; + switch (h->type) { + case ART_N4: + keys = ((art_n4_t *)h)->keys; + children = ((art_n4_t *)h)->children; + n = h->count; + break; + case ART_N16: + keys = ((art_n16_t *)h)->keys; + children = ((art_n16_t *)h)->children; + n = h->count; + break; + case ART_N48: { + art_n48_t *x = (art_n48_t *)h; + for (int i = 0; i < 256; i++) { + if (x->index[i] == 255) + continue; + if (bpos + 1 >= bcap) + return -1; + buf[bpos] = (char)i; + if (art_collect_keys(kv, x->children[x->index[i]], buf, bpos + 1, + bcap, scan) != 0) + return -1; + } + return 0; + } + case ART_N256: { + art_n256_t *x = (art_n256_t *)h; + for (int i = 0; i < 256; i++) { + if (x->children[i] < 0) + continue; + if (bpos + 1 >= bcap) + return -1; + buf[bpos] = (char)i; + if (art_collect_keys(kv, x->children[i], buf, bpos + 1, bcap, + scan) != 0) + return -1; + } + return 0; + } + default: + return 0; + } + for (int i = 0; i < n; i++) { + if (bpos + 1 >= bcap) + return -1; + buf[bpos] = (char)keys[i]; + if (art_collect_keys(kv, children[i], buf, bpos + 1, bcap, scan) != 0) + return -1; + } + return 0; +} + +/* Collect all keys with values starting with pfx (including system variables). */ +static int art_scan_keys(kvspace_t *kv, const char *pfx, int plen, + keyscan_t *scan) { + char buf[4096]; + if (plen >= (int)sizeof buf) + return -1; + int bpos = 0; + int32_t nid = art_cover(kv, kv->hdr->art_root, (const uint8_t *)pfx, plen, + buf, (int)sizeof buf, &bpos); + if (nid < 0) + return 0; + return art_collect_keys(kv, nid, buf, bpos, (int)sizeof buf, scan); +} + /* ============ lifecycle ============ */ /* 8 * 64^k; s wraps to 0. */ static bool sbo_data_size_ok(size_t n) { @@ -2306,6 +2432,10 @@ static bool sbo_data_size_ok(size_t n) { kvspace_t *kvspaceShmOpen(const char *path, size_t data_size) { if (!path) return NULL; + if (!bm_layout_ok()) { + fprintf(stderr, "kvspace: unexpected blockmalloc blocks_meta_t layout\n"); + return NULL; + } char head_path[PATH_MAX], data_path[PATH_MAX], gen_path[PATH_MAX], garb_path[PATH_MAX]; if (snprintf(head_path, sizeof head_path, "%s.sbo.head", path) >= @@ -2346,6 +2476,14 @@ kvspace_t *kvspaceShmOpen(const char *path, size_t data_size) { if (pread(kv->r_art.fd, &tmp, sizeof tmp, 0) != (ssize_t)sizeof tmp || memcmp(tmp.magic, KVS_MAGIC, sizeof(KVS_MAGIC) - 1) != 0) goto fail; + /* Check format_version before region_attach: header size changes across versions, + * so an older store would otherwise fail in region_attach without printing a diagnostic. */ + if (tmp.format_version != KVSPACE_FORMAT_VERSION) { + fprintf(stderr, "kvspace: unsupported store format (version=%u, " + "expected %u): %s; rebuild the shm store\n", + tmp.format_version, KVSPACE_FORMAT_VERSION, path); + goto fail; + } art_slab = (size_t)tmp.art_slab_size; sbo_head = (size_t)tmp.sbo_head_size; data_size = (size_t)tmp.sbo_data_size; @@ -2441,6 +2579,7 @@ kvspace_t *kvspaceShmOpen(const char *path, size_t data_size) { goto fail; /* 已有大量垃圾且建不了线程:open 硬失败 */ pthread_mutex_init(&kv->wlock, NULL); + pthread_mutex_init(&kv->lc_mtx, NULL); for (int i = 0; i < WATCH_TABLE_SZ; i++) { pthread_cond_init(&kv->watches[i].cond, NULL); pthread_mutex_init(&kv->watches[i].mtx, NULL); @@ -2483,7 +2622,6 @@ static bool p2_other_alive(kvspace_t *kv) { void kvspaceShmClose(kvspace_t *kv) { if (!kv) return; - p2_setstats_dump(); /* 最后一个存活进程负责清场:Linux 跑一次机会性 round;macOS 直接清环。 */ if (kv->pid == (uint32_t)getpid() && kv->p2_slot >= 0 && !p2_other_alive(kv)) { @@ -2519,6 +2657,8 @@ void kvspaceShmClose(kvspace_t *kv) { free(kv->watches[i].val); } pthread_mutex_destroy(&kv->wlock); + list_cache_drop(kv); + pthread_mutex_destroy(&kv->lc_mtx); region_close(&kv->r_art); region_close(&kv->r_head); region_close(&kv->r_data); @@ -2528,64 +2668,46 @@ void kvspaceShmClose(kvspace_t *kv) { } /* ---- link resolve helpers ---- */ -static int read_value(kvspace_t *kv, uint64_t off, uint8_t **out, int32_t *ol) { - uint8_t *s = kv->sbo_data + off; - size_t sz = sbo_allocated_size(kv->sbo_meta, off); +/* Parse full value length = head + body + attr within [s, s+bound) (empty attr = 2). */ +static int parse_value(const uint8_t *s, uint64_t bound, int32_t *ol) { + if (bound > INT32_MAX) + bound = INT32_MAX; kvspaceXvalueHead wire; - if (head_of(s, (int32_t)sz, &wire) == 0) { - uint64_t l = kvspaceXvalueHeadLen(&wire); - if (l > INT32_MAX) - return -1; - *out = s; - /* 完整值长 = head + body + attr(attr_len:u16 @8+len 自描述;空 attr=2)。 */ - if ((uint64_t)sz < KVSPACE_XVALUE_HEAD_LEN + l + 2) - return -1; - uint64_t alen = kvspaceXvalueHeadAttrLen(s, l); - if (alen < 2 || alen > (uint64_t)sz - KVSPACE_XVALUE_HEAD_LEN - l) - return -1; - *ol = (int32_t)(KVSPACE_XVALUE_HEAD_LEN + l + alen); - return 0; - } - return -1; + if (head_of(s, (int32_t)bound, &wire) != 0) + return -1; + uint64_t l = kvspaceXvalueHeadLen(&wire); + if (bound < KVSPACE_XVALUE_HEAD_LEN + l + 2) + return -1; + uint64_t alen = kvspaceXvalueHeadAttrLen(s, l); + if (alen < 2 || alen > bound - KVSPACE_XVALUE_HEAD_LEN - l) + return -1; + *ol = (int32_t)(KVSPACE_XVALUE_HEAD_LEN + l + alen); + return 0; } -/* link 解析:ptr(ref=1)与 @ext(ref=2)的 XValue 恒为叶子(spec 硬规则——指针/扩展键 - * 之后不可能再有成员,成员只挂在 target 上),故路径中任何 `/` 分隔的容器前缀都绝不会是 - * link;唯一可能是 ptr 的只有整键叶子本身。因此只需查整键一次:非 ptr 即完成,ptr 则顺链 - * 到 target(本身又是一个完整叶子键)再查,最多追 16 跳。叶子不存在→非链,原样返回(调用 - * 方另做 extindex/dir 兜底)。取代旧的「逐前缀 1~3 次 art_search + 追加 rest」全下降探测。 */ -static void resolve_path(kvspace_t *kv, const char *path, char *out, int osz) { - strncpy(out, path, osz - 1); - out[osz - 1] = '\0'; - for (int depth = 0; depth < 16; depth++) { - art_hdr_t *h = - art_search(kv, kv->hdr->art_root, (const uint8_t *)out, (int)strlen(out)); - if (!h) - return; - uint64_t hv = art_val_load(h); - if (!AV_HAS(hv)) - return; - uint8_t *raw; - int32_t rl; - if (read_value(kv, AV_OFF(hv), &raw, &rl) < 0) - return; - kvspaceXvalueHead wire; - if (head_of(raw, rl, &wire) == 0) { - uint64_t l2 = kvspaceXvalueHeadLen(&wire); - if (wire.ptr != KVSPACE_XVALUE_PTR_PTR || l2 >= (uint64_t)osz) - return; - memcpy(out, raw + KVSPACE_XVALUE_HEAD_LEN, (size_t)l2); - out[l2] = 0; - continue; - } - return; - } + +/* Write path: bound by physical box capacity (takes allocator lock) so a corrupted + * header cannot cause caller writes to overflow into adjacent boxes. */ +static int read_value(kvspace_t *kv, uint64_t off, uint8_t **out, int32_t *ol) { + *out = kv->sbo_data + off; + return parse_value(*out, sbo_allocated_size(kv->sbo_meta, off), ol); } -/* resolve_path 的融合版:解析 link 链的同时把终端叶子的值一并取出,令调用方省掉 - * 「解析后再 art_search + read_value」的整套重复下降。每跳只取一次 head(ref 判定与 - * 值长度共用),命中带值节点即返 fetched=1(*raw/*rl 就位)。语义与 - * resolve_path + art_search + read_value 逐字等价:未命中/无值 → 返回该节点、fetched=0, - * 交调用方走 extindex/dir 兜底。 */ +/* Read-only path: bound by mapped data region without taking the allocator lock. + * sbo_allocated_size contends on the cross-process spinlock on every read; since value_ok + * already ensures 8+len+attr_len fits in the box at write time, both bounds agree on valid values. */ +static uint64_t ro_bound(const kvspace_t *kv, uint64_t off) { + uint64_t m = kv->r_data.mapped; + return off < m ? m - off : 0; +} +static int read_value_ro(kvspace_t *kv, uint64_t off, uint8_t **out, + int32_t *ol) { + *out = kv->sbo_data + off; + return parse_value(*out, ro_bound(kv, off), ol); +} +/* Link resolution: ptr (ref=1) and @ext (ref=2) XValues are always leaves (never intermediate + * `/` prefixes), so only the full leaf key can be a link. Follow at most 16 hops, fetching + * the terminal leaf value at the same time (fetched=1) to avoid a second art_search + read_value. + * Missing/valueless leaf returns fetched=0 for extindex/dir fallback. Caller ensures strlen(path) < osz. */ static art_hdr_t *resolve_fetch(kvspace_t *kv, const char *path, char *out, int osz, uint8_t **raw, int32_t *rl, int *fetched) { *fetched = 0; @@ -2600,19 +2722,14 @@ static art_hdr_t *resolve_fetch(kvspace_t *kv, const char *path, char *out, int if (!AV_HAS(hv)) return h; uint8_t *s = kv->sbo_data + AV_OFF(hv); - size_t sz = sbo_allocated_size(kv->sbo_meta, AV_OFF(hv)); + uint64_t sz = ro_bound(kv, AV_OFF(hv)); kvspaceXvalueHead wire; - if (head_of(s, (int32_t)sz, &wire) == 0) { + if (head_of(s, sz > INT32_MAX ? INT32_MAX : (int32_t)sz, &wire) == 0) { uint64_t l3 = kvspaceXvalueHeadLen(&wire); if (wire.ptr != KVSPACE_XVALUE_PTR_PTR || l3 >= (uint64_t)osz) { - /* 完整值长 = head + body + attr(attr_len @8+len 自描述)。 */ - if ((uint64_t)sz < KVSPACE_XVALUE_HEAD_LEN + l3 + 2) - return NULL; - uint64_t alen = kvspaceXvalueHeadAttrLen(s, l3); - if (alen < 2 || alen > (uint64_t)sz - KVSPACE_XVALUE_HEAD_LEN - l3) + if (parse_value(s, sz, rl) != 0) return NULL; *raw = s; - *rl = (int32_t)(KVSPACE_XVALUE_HEAD_LEN + l3 + alen); *fetched = 1; return h; } @@ -2625,6 +2742,14 @@ static art_hdr_t *resolve_fetch(kvspace_t *kv, const char *path, char *out, int return NULL; } +/* Resolve path only (used by List). */ +static void resolve_path(kvspace_t *kv, const char *path, char *out, int osz) { + uint8_t *raw; + int32_t rl; + int fetched; + (void)resolve_fetch(kv, path, out, osz, &raw, &rl, &fetched); +} + /* Read the extension locator stored at a directory key. */ static int dir_ext_path(kvspace_t *kv, const char *dir, char *out, int osz) { out[0] = 0; @@ -2637,7 +2762,7 @@ static int dir_ext_path(kvspace_t *kv, const char *dir, char *out, int osz) { return 0; uint8_t *raw; int32_t rl; - if (read_value(kv, AV_OFF(hv), &raw, &rl) < 0) + if (read_value_ro(kv, AV_OFF(hv), &raw, &rl) < 0) return 0; kvspaceXvalueHead wire; if (head_of(raw, rl, &wire) != 0 || @@ -2662,6 +2787,9 @@ uint8_t *kvspaceShmGet(kvspace_t *kv, const char *key, int resolve, if (kv_sync(kv) != 0) return NULL; char kbuf[1024]; + /* Set rejects >= 1024-byte keys; do not truncate and look up a shorter key. */ + if (strlen(key) >= sizeof kbuf) + return NULL; uint8_t *raw; int32_t rl; int fetched = 0; @@ -2689,7 +2817,7 @@ uint8_t *kvspaceShmGet(kvspace_t *kv, const char *key, int resolve, if (eh && AV_HAS(ehv)) { uint8_t *raw; int32_t rl; - if (read_value(kv, AV_OFF(ehv), &raw, &rl) == 0) { + if (read_value_ro(kv, AV_OFF(ehv), &raw, &rl) == 0) { *ol = rl; free(parent); free(name); @@ -2701,7 +2829,7 @@ uint8_t *kvspaceShmGet(kvspace_t *kv, const char *key, int resolve, free(name); return NULL; } - if (!fetched && read_value(kv, AV_OFF(hv), &raw, &rl) < 0) + if (!fetched && read_value_ro(kv, AV_OFF(hv), &raw, &rl) < 0) return NULL; *ol = rl; return raw; @@ -2772,7 +2900,7 @@ uint8_t *kvspaceShmGetByRef(kvspace_t *kv, kvspaceRef_t *ref, if (h && AV_HAS(hv)) { uint8_t *raw; int32_t rl; - if (read_value(kv, AV_OFF(hv), &raw, &rl) == 0) { + if (read_value_ro(kv, AV_OFF(hv), &raw, &rl) == 0) { *ol = rl; return raw; } @@ -2838,7 +2966,9 @@ int kvspaceShmSetPartByRef(kvspace_t *kv, kvspaceRef_t *ref, return -1; int32_t rl = 0; uint8_t *d = kvspaceShmGet(kv, key_fallback, 0, &rl); - if (!d || offset + buf_len > (uint32_t)rl) + /* Re-bound by physical box capacity before writing; use u64 to prevent u32 wraparound. */ + if (!d || read_value(kv, (uint64_t)(d - kv->sbo_data), &d, &rl) != 0 || + (uint64_t)offset + buf_len > (uint64_t)rl) return -1; kvspaceShmResolveRef(kv, key_fallback, ref); kv_gens_t *g = gens_at(kv, (int32_t)ref->block_id); @@ -2852,7 +2982,7 @@ int kvspaceShmSetPartByRef(kvspace_t *kv, kvspaceRef_t *ref, uint8_t *raw; int32_t rl; if (read_value(kv, AV_OFF(hv), &raw, &rl) < 0 || - offset + buf_len > (uint32_t)rl) + (uint64_t)offset + buf_len > (uint64_t)rl) return -1; kv_gens_t *g2 = gens_at(kv, id); if (g2) @@ -2863,6 +2993,21 @@ int kvspaceShmSetPartByRef(kvspace_t *kv, kvspaceRef_t *ref, return 0; } +/* Partial write by key (follows links and extindex fallback like Get with resolve=1). + * Locates via lock-free read, then re-bounds by physical box capacity before writing. */ +int kvspaceShmSetPart(kvspace_t *kv, const char *key, uint32_t offset, + const uint8_t *buf, uint32_t buf_len) { + if (!kv || !key || (buf_len && !buf)) + return -1; + int32_t rl = 0; + uint8_t *d = kvspaceShmGet(kv, key, 1, &rl); + if (!d || read_value(kv, (uint64_t)(d - kv->sbo_data), &d, &rl) != 0 || + (uint64_t)offset + buf_len > (uint64_t)rl) + return -1; + memcpy(d + offset, buf, buf_len); + return 0; +} + /* /vthread//‥status:合法内容长度上限与创建时物理容量下限。 */ #define KVSPACE_STATUS_MAX_LEN 16u #define KVSPACE_STATUS_MIN_CAP 80u @@ -2909,8 +3054,8 @@ int kvspaceShmXvalueBodyView(kvspace_t *kv, const kvspaceRef_t *ref, if (cap < KVSPACE_XVALUE_HEAD_LEN || l5 > cap - KVSPACE_XVALUE_HEAD_LEN) return -1; - /* status:只暴露规范形状(SLACK、len≤16)且物理容量达标的槽; - 类型([n]char/utf8)在 x‥langtype attr key,不在此判。 */ + /* status: only expose canonical slots (SLACK, len <= 16) with sufficient physical capacity; + langtype ([n]char/utf8) lives in the inline attr region and is not checked here. */ if (is_vthread_status_key(key_fallback)) { if (wire.storetype != KVSPACE_XVALUE_ST_SLACK || cap < KVSPACE_STATUS_MIN_CAP || @@ -2925,45 +3070,29 @@ int kvspaceShmXvalueBodyView(kvspace_t *kv, const kvspaceRef_t *ref, return 0; } -/* 临时探针(P2_SETSTATS=):统计写入形态,用于静默屏障可行性评估。 */ -static struct { uint64_t same, realloc_, fresh, retired_bytes; } g_setstats; -static void p2_setstats_dump(void) { - const char *f = getenv("P2_SETSTATS"); - if (!f || !f[0]) - return; - FILE *fp = fopen(f, "a"); - if (!fp) - return; - fprintf(fp, "pid=%d same=%llu realloc=%llu fresh=%llu retired_bytes=%llu\n", - (int)getpid(), (unsigned long long)g_setstats.same, - (unsigned long long)g_setstats.realloc_, - (unsigned long long)g_setstats.fresh, - (unsigned long long)g_setstats.retired_bytes); - fclose(fp); -} - -/* 原始落盘(不处理容器/member 语义,供内部调用,避免递归)。 */ +/* Raw local write (internal helper without container/member semantics). + * reserve: minimum physical box capacity (for WriteNewPlace body_cap); 0 uses val_len. */ static int shm_set_local(kvspace_t *kv, const char *key, const uint8_t *val, - int32_t val_len, art_hdr_t *old) { - { - static bool reg; - if (!reg) { reg = true; atexit(p2_setstats_dump); } - } + int32_t val_len, art_hdr_t *old, size_t reserve) { + size_t alloc_len = (size_t)val_len; + if (reserve > alloc_len) + alloc_len = reserve; + if (is_vthread_status_key(key) && alloc_len < KVSPACE_STATUS_MIN_CAP) + alloc_len = KVSPACE_STATUS_MIN_CAP; uint64_t ohv = old ? art_val_load(old) : 0; bool retiring = old && AV_HAS(ohv); uint64_t gidx = 0; uint64_t cap = 0; if (retiring) { cap = sbo_allocated_size(kv->sbo_meta, AV_OFF(ohv)); - if ((uint64_t)val_len <= cap) { - /* 同尺寸就地写:指针不变,value_gen 奇偶保护(一次查表) */ + if ((uint64_t)alloc_len <= cap) { + /* Existing box fits (including reserve): write in place under value_gen seqlock */ kv_gens_t *g = gens_at(kv, art_id_of(kv, old)); if (g) __atomic_add_fetch(&g->value_gen, 1u, __ATOMIC_ACQ_REL); memcpy(kv->sbo_data + AV_OFF(ohv), val, (size_t)val_len); if (g) __atomic_add_fetch(&g->value_gen, 1u, __ATOMIC_ACQ_REL); - g_setstats.same++; return 0; } /* 换 box:reserve 在任何变更之前;失败 = 硬错误、旧值原样 */ @@ -2980,14 +3109,7 @@ static int shm_set_local(kvspace_t *kv, const char *key, const uint8_t *val, return -1; } #endif - g_setstats.realloc_++; - g_setstats.retired_bytes += cap; - } else { - g_setstats.fresh++; } - size_t alloc_len = (size_t)val_len; - if (is_vthread_status_key(key) && alloc_len < KVSPACE_STATUS_MIN_CAP) - alloc_len = KVSPACE_STATUS_MIN_CAP; uint64_t off = kv_sbo_alloc(kv, alloc_len); if (off == (uint64_t)-1) { if (retiring) @@ -3014,7 +3136,6 @@ static int shm_set_local(kvspace_t *kv, const char *key, const uint8_t *val, { /* 换 box / 墓碑复活:旧 token 全部 STALE */ int32_t nid = p2_node_at(kv, key); kv_gens_t *g = gens_at(kv, nid); - if (getenv("P2_DEBUG")) { fprintf(stderr, "DBG reset-lookup key=%s nid=%d g=%p root=%d\n", key, nid, (void *)g, kv->hdr->art_root); } if (g) { gen_bump_struct(kv, nid); gen_bump_value(kv, nid); @@ -3022,14 +3143,16 @@ static int shm_set_local(kvspace_t *kv, const char *key, const uint8_t *val, } if (retiring) garb_commit(kv, gidx, AV_OFF(ohv), cap); + else + list_gen_bump(kv); /* new key or tombstone revive: bump after art_ins */ return 0; } static int shm_set_raw(kvspace_t *kv, const char *key, const uint8_t *val, - int32_t val_len) { + int32_t val_len, size_t reserve) { art_hdr_t *old = art_search(kv, kv->hdr->art_root, (const uint8_t *)key, (int)strlen(key)); - return shm_set_local(kv, key, val, val_len, old); + return shm_set_local(kv, key, val, val_len, old, reserve); } static int frame_operand_ptr(const char *key, const kvspaceXvalueHead *wire) { @@ -3082,8 +3205,8 @@ static int shm_set_validated(kvspace_t *kv, const char *key, free(nn); } - /* status 固定档:形状固定(SLACK、len≤16)、物理容量 ≥ 下限。 - * 类型([n]char/utf8)在 x‥langtype attr key,不在此判。 */ + /* status slot: fixed shape (SLACK, len <= 16) and physical capacity >= minimum. + * langtype ([n]char/utf8) lives in the inline attr region and is not checked here. */ if (is_vthread_status_key(kbuf)) { if (wire->storetype != KVSPACE_XVALUE_ST_SLACK || kvspaceXvalueHeadLen(wire) > KVSPACE_STATUS_MAX_LEN) @@ -3101,7 +3224,7 @@ static int shm_set_validated(kvspace_t *kv, const char *key, return -1; } } - return shm_set_local(kv, kbuf, val, val_len, local); + return shm_set_local(kv, kbuf, val, val_len, local, 0); } int kvspaceShmSet(kvspace_t *kv, const char *key, const uint8_t *val, @@ -3109,7 +3232,7 @@ int kvspaceShmSet(kvspace_t *kv, const char *key, const uint8_t *val, if (!kv || !key || !val || val_len <= 0) return -1; kvspaceXvalueHead wire; - if (head_of(val, val_len, &wire) != 0) + if (value_ok(val, val_len, &wire) != 0) return -1; return shm_set_validated(kv, key, val, val_len, &wire); } @@ -3118,7 +3241,7 @@ int kvspaceShmSetValue(kvspace_t *kv, const char *key, const uint8_t *val, int32_t val_len) { kvspaceXvalueHead head; if (!kv || !key || !val || val_len <= 0 || - head_of(val, val_len, &head) != 0) + value_ok(val, val_len, &head) != 0) return -2; return shm_set_validated(kv, key, val, val_len, &head); } @@ -3128,7 +3251,7 @@ int kvspaceShmSetValueByRef(kvspace_t *kv, kvspaceRef_t *ref, int32_t val_len) { kvspaceXvalueHead head; if (!kv || !ref || !key_fallback || !val || val_len <= 0 || - head_of(val, val_len, &head) != 0) + value_ok(val, val_len, &head) != 0) return -2; if (kv_sync(kv) != 0) return -1; @@ -3138,7 +3261,7 @@ int kvspaceShmSetValueByRef(kvspace_t *kv, kvspaceRef_t *ref, int32_t id = ref_leaf(kv, ref, key_fallback); art_hdr_t *h = id >= 0 ? art_hdr(kv, id) : NULL; if (h && AV_HAS(art_val_load(h))) - return shm_set_local(kv, key_fallback, val, val_len, h); + return shm_set_local(kv, key_fallback, val, val_len, h, 0); /* ref 过期或尚无值:退回完整按 key 写(含 extindex 检查),成功后重解析。 */ int rc = shm_set_validated(kv, key_fallback, val, val_len, &head); if (rc == 0) @@ -3153,6 +3276,8 @@ int kvspaceShmWriteInPlace(kvspace_t *kv, const char *key, int resolve, if (kv_sync(kv) != 0) return -1; char kbuf[1024]; + if (strlen(key) >= sizeof kbuf) + return -1; /* reject rather than truncating to another key */ uint8_t *raw; int32_t rl; int fetched = 0; @@ -3160,8 +3285,7 @@ int kvspaceShmWriteInPlace(kvspace_t *kv, const char *key, int resolve, if (resolve) h = resolve_fetch(kv, key, kbuf, sizeof(kbuf), &raw, &rl, &fetched); else { - strncpy(kbuf, key, sizeof(kbuf) - 1); - kbuf[sizeof(kbuf) - 1] = '\0'; + strcpy(kbuf, key); h = art_search(kv, kv->hdr->art_root, (const uint8_t *)kbuf, (int)strlen(kbuf)); } @@ -3170,8 +3294,10 @@ int kvspaceShmWriteInPlace(kvspace_t *kv, const char *key, int resolve, uint64_t hv = art_val_load(h); if (!AV_HAS(hv)) return -1; - if ((!fetched && read_value(kv, AV_OFF(hv), &raw, &rl) < 0) || rl <= 0) - return -1; /* None 或读失败 → 强制走 NewPlace */ + /* Re-bound by physical box capacity before returning writable pointer. */ + (void)fetched; + if (read_value(kv, AV_OFF(hv), &raw, &rl) < 0 || rl <= 0) + return -1; /* None or read failure -> require WriteNewPlace */ kvspaceXvalueHead wire; if (head_of(raw, rl, &wire) == 0) { if (kvspaceXvalueHeadLen(&wire) != (uint64_t)body_len) @@ -3214,10 +3340,16 @@ int kvspaceShmWriteNewPlace(kvspace_t *kv, const char *key, uint8_t ref, free(value); return -1; } - /* 固定头 8 字节:body 偏移恒 = 8;langtype 尾部 @8+body_len 整体单写。 */ - uint32_t headlen = KVSPACE_XVALUE_HEAD_LEN; - (void)ptr; - int rc = shm_set_raw(kv, key, value, (int32_t)total); + /* Reserve produces an inline header; patch ptr byte for ref=1/2. */ + kvspaceXvalueHead hd; + memcpy(&hd, value, sizeof hd); + hd.ptr = ptr; + memcpy(value, &hd, sizeof hd); + /* Fixed 8-byte header (body offset = 8) + attr at 8+body_len in a single write. + * Reserve physical box capacity (8 + body_cap + attr) so body can grow in place. */ + uint64_t attr_len = total - KVSPACE_XVALUE_HEAD_LEN - (uint64_t)body_len; + size_t reserve = (size_t)(KVSPACE_XVALUE_HEAD_LEN + body_cap + attr_len); + int rc = shm_set_raw(kv, key, value, (int32_t)total, reserve); free(value); if (rc != 0) return -1; @@ -3228,23 +3360,21 @@ int kvspaceShmWriteNewPlace(kvspace_t *kv, const char *key, uint8_t ref, uint64_t nv = art_val_load(node); if (!AV_HAS(nv)) return -1; - *body = kv->sbo_data + AV_OFF(nv) + headlen; + *body = kv->sbo_data + AV_OFF(nv) + KVSPACE_XVALUE_HEAD_LEN; return 0; } -int kvspaceShmListLen(kvspace_t *kv, const char *prefix, bool ex, int resolve, - int32_t *out_count) { - char **names; - int32_t count; - if (kvspaceShmList(kv, prefix, ex, resolve, &names, &count) != 0) { - *out_count = 0; +/* Delete a single key directly (without following links) and without bumping + * list_gen. Returns 1 = deleted, 0 = missing, -1 = hard error (tree unchanged). + * Caller has called kv_sync and verified strlen(key) < 1024. */ +static int shm_del_key(kvspace_t *kv, const char *key) { + bool d = false; + int32_t root = art_del(kv, kv->hdr->art_root, (const uint8_t *)key, + (int)strlen(key), 0, &d); + if (root < 0) return -1; - } - for (int32_t i = 0; i < count; i++) - free(names[i]); - free(names); - *out_count = count; - return 0; + kv->hdr->art_root = root; + return d ? 1 : 0; } int kvspaceShmDel(kvspace_t *kv, const char *key) { @@ -3252,81 +3382,12 @@ int kvspaceShmDel(kvspace_t *kv, const char *key) { return -1; if (kv_sync(kv) != 0) return -1; - char kbuf[1024]; - strncpy(kbuf, key, sizeof(kbuf) - 1); - kbuf[sizeof(kbuf) - 1] = '\0'; /* 删键本身,不穿透 link——显式解引用由 runtime 掌控 */ - bool d = false; - int32_t root = art_del(kv, kv->hdr->art_root, (const uint8_t *)kbuf, - (int)strlen(kbuf), 0, &d); - if (root < 0) - return -1; /* 表满等硬错误:树未变 */ - kv->hdr->art_root = root; - return d ? 0 : -1; -} - -int kvspaceShmDeltree(kvspace_t *kv, const char *prefix) { - if (!kv || !prefix || reserved_meta_path(prefix)) - return -1; - if (kv_sync(kv) != 0) + if (strlen(key) >= 1024) + return -1; /* Reject instead of truncating into another key */ + /* Delete the key itself without following links; explicit deref belongs to runtime */ + if (shm_del_key(kv, key) != 1) return -1; - // if prefix itself is a link, only delete the link - art_hdr_t *h = art_search(kv, kv->hdr->art_root, (const uint8_t *)prefix, - (int)strlen(prefix)); - uint64_t hv = h ? art_val_load(h) : 0; - if (h && AV_HAS(hv)) { - uint8_t *raw; - int32_t rl; - read_value(kv, AV_OFF(hv), &raw, &rl); - kvspaceXvalueHead wire; - int is_ptr = rl > 0 && head_of(raw, rl, &wire) == 0 && - wire.ptr == KVSPACE_XVALUE_PTR_PTR; - if (is_ptr) { - return kvspaceShmDel(kv, prefix); - } - } - char *e = edir(prefix); // ensure trailing / for listing children - char **ns; - int32_t nc; - kvspaceShmList(kv, e, false, 1, &ns, &nc); - for (int i = 0; i < nc; i++) { - char *c = pjoin(e, ns[i]); - kvspaceShmDeltree(kv, c); - free(c); - } - for (int i = 0; i < nc; i++) - free(ns[i]); - free(ns); - - /* 成员目录 marker(prefix·,U+00B7):成员 key = - prefix·,直接拼接后递归删除。 slash 版 e=prefix/ 覆盖不到 · - 成员(json 的 stringkeymap 落盘形态)。 */ - size_t pl = strlen(prefix); - char *m = malloc(pl + 3); - memcpy(m, prefix, pl); - m[pl] = (char)0xC2; - m[pl + 1] = (char)0xB7; - m[pl + 2] = 0; - char **ms; - int32_t mc; - kvspaceShmList(kv, m, false, 1, &ms, &mc); - for (int i = 0; i < mc; i++) { - size_t ml = strlen(m), nl = strlen(ms[i]); - char *c = malloc(ml + nl + 1); - memcpy(c, m, ml); - memcpy(c + ml, ms[i], nl + 1); - kvspaceShmDeltree(kv, c); - free(c); - } - for (int i = 0; i < mc; i++) - free(ms[i]); - free(ms); - kvspaceShmDel(kv, m); - free(m); - - kvspaceShmDel(kv, prefix); - if (strcmp(e, prefix) != 0) - kvspaceShmDel(kv, e); - free(e); + list_gen_bump(kv); /* Key disappeared: bump after CAS */ return 0; } @@ -3341,6 +3402,89 @@ static char *memdir(const char *p) { return r; } +/* Whether key belongs to base's subtree: base itself, or base followed by '/' + * (hierarchy), U+00B7 (member), or U+2025 (runtime/attr key). Empty base = root + * (all keys starting with '/'). Matches kvspace-durable store::is_subtree_key. */ +static int subtree_key(const char *base, size_t bl, const char *key) { + if (bl == 0) + return key[0] == '/'; + if (strncmp(key, base, bl) != 0) + return 0; + const unsigned char *r = (const unsigned char *)key + bl; + return r[0] == 0 || r[0] == '/' || (r[0] == 0xC2 && r[1] == 0xB7) || + (r[0] == 0xE2 && r[1] == 0x80 && r[2] == 0xA5); +} + +static int is_ptr_key(kvspace_t *kv, const char *key) { + art_hdr_t *h = art_search(kv, kv->hdr->art_root, (const uint8_t *)key, + (int)strlen(key)); + uint64_t hv = h ? art_val_load(h) : 0; + if (!h || !AV_HAS(hv)) + return 0; + uint8_t *raw = NULL; + int32_t rl = 0; + kvspaceXvalueHead wire; + return read_value(kv, AV_OFF(hv), &raw, &rl) == 0 && rl > 0 && + head_of(raw, rl, &wire) == 0 && wire.ptr == KVSPACE_XVALUE_PTR_PTR; +} + +/* Delete the entire subtree under prefix. A single trailing '/' or U+00B7 on + * prefix normalizes to base ("/d/" and "/d" both delete "/d"; "/" is root). + * Deletes base, base/..., base..., and base... (including hidden + * U+2025 runtime keys). Does not follow links (POSIX rm semantics): if prefix + * or base is a pointer, only the pointer is deleted. */ +int kvspaceShmDeltree(kvspace_t *kv, const char *prefix) { + if (!kv || !prefix || reserved_meta_path(prefix)) + return -1; + if (kv_sync(kv) != 0) + return -1; + size_t pl = strlen(prefix); + if (pl >= 1024) + return -1; /* Reject instead of truncating into another prefix */ + char base[1024]; + memcpy(base, prefix, pl + 1); + size_t bl = pl; + if (strcmp(prefix, "/") == 0) + bl = 0; + else if (bl && base[bl - 1] == '/') + bl--; + else if (bl >= 2 && (unsigned char)base[bl - 2] == 0xC2 && + (unsigned char)base[bl - 1] == 0xB7) + bl -= 2; + base[bl] = 0; + if (bl && reserved_meta_path(base)) + return -1; + /* prefix itself is a link: delete only the link. */ + if (pl && is_ptr_key(kv, prefix)) + return kvspaceShmDel(kv, prefix); + if (bl && bl != pl && is_ptr_key(kv, base)) { + int rc = kvspaceShmDel(kv, base); + kvspaceShmDel(kv, prefix); /* Companion key such as "/p/", may not exist */ + return rc; + } + keyscan_t keys = {0}; + if (art_scan_keys(kv, bl ? base : "/", bl ? (int)bl : 1, &keys) != 0) { + keyscan_free(&keys); + return -1; + } + int rc = 0, any = 0; + for (int32_t i = 0; i < keys.count; i++) { + const char *k = keys.keys[i]; + if (!subtree_key(base, bl, k) || reserved_meta_path(k) || + strlen(k) >= 1024) + continue; + int d = shm_del_key(kv, k); + if (d < 0) + rc = -1; + else if (d) + any = 1; + } + keyscan_free(&keys); + if (any) + list_gen_bump(kv); /* Keys disappeared: bump once after batch delete */ + return rc; +} + /* Copy one value before Set can move its slab. */ int kvspaceShmCp(kvspace_t *kv, const char *src, const char *dst) { if (!kv || !src || !dst) @@ -3538,43 +3682,51 @@ static int same_child(const char *a, const char *b) { return na == nb && memcmp(a, b, na) == 0; } -int kvspaceShmList(kvspace_t *kv, const char *prefix, bool ex, int resolve, - char ***on, int32_t *oc) { - if (!kv || !prefix || !on || !oc) - return -1; - *on = NULL; - *oc = 0; +/* Resolve List input: prefix -> target directory (following links when resolve=1) + * and mounted extindex source ext (when ex=1 and mounted; "" otherwise). + * Returns 0 = scan, 1 = reserved path (empty result), -1 = error. + * target buffer >= 4096 bytes, ext buffer >= 1024 bytes. */ +static int list_target(kvspace_t *kv, const char *prefix, bool ex, int resolve, + char *target, size_t tcap, char *ext, size_t ecap) { + ext[0] = 0; if (reserved_meta_path(prefix)) - return 0; + return 1; if (kv_sync(kv) != 0 || bad_dir_prefix(prefix)) return -1; - const char *pfx = prefix; - char tbuf[1024]; if (resolve) { - resolve_path(kv, prefix, tbuf, sizeof tbuf); - pfx = tbuf; + if (strlen(prefix) >= 1024) + return -1; /* Reject instead of truncating into another dir */ + resolve_path(kv, prefix, target, 1024); + } else { + size_t l = strlen(prefix); + if (l >= tcap) + return -1; + memcpy(target, prefix, l + 1); } - if (reserved_meta_path(pfx)) - return 0; + if (reserved_meta_path(target)) + return 1; + if (ex) { + char *dir = edir(target); + if (!dir) + return -1; + if (!dir_ext_path(kv, dir, ext, (int)ecap)) + ext[0] = 0; + free(dir); + } + return 0; +} +/* Scan direct children of target (and mounted ext): sort, deduplicate (prefer + * directory entry on name collision), and hide runtime entries. */ +static int list_scan(kvspace_t *kv, const char *pfx, const char *ext, + char ***on, int32_t *oc) { keyscan_t children = {0}; char buf[4096]; if (kv->hdr->art_root >= 0 && art_scan_pfx(kv, pfx, (int)strlen(pfx), buf, (int)sizeof buf, &children) != 0) goto fail; - - if (ex) { - char extpath[1024]; - char *dir = edir(pfx); - if (!dir) - goto fail; - int has_ext = dir_ext_path(kv, dir, extpath, sizeof extpath); - free(dir); - if (has_ext) { - if (art_scan_pfx(kv, extpath, (int)strlen(extpath), buf, - (int)sizeof buf, &children) != 0) - goto fail; - } - } + if (ext[0] && art_scan_pfx(kv, ext, (int)strlen(ext), buf, (int)sizeof buf, + &children) != 0) + goto fail; if (children.count > 1) qsort(children.keys, (size_t)children.count, sizeof *children.keys, child_cmp); @@ -3585,20 +3737,10 @@ int kvspaceShmList(kvspace_t *kv, const char *prefix, bool ex, int resolve, free(children.keys[i]); continue; } - /* 成员名含 ‥(\xE2\x80\xA5)是系统变量/attr key(`0‥langtype` 等), - List 时隐藏——对齐 durable 后端(RUNTIME_MEMBER_SEP 隐藏)。 */ - { - const char *p = children.keys[i]; - for (; *p; p++) { - if ((unsigned char)p[0] == 0xE2 && (unsigned char)p[1] == 0x80 && - (unsigned char)p[2] == 0xA5) { - free(children.keys[i]); - goto hidden; - } - } - } - if (0) { -hidden: + /* Child names containing U+2025 (\xE2\x80\xA5) are runtime system keys + (e.g. status, pc); hide from List to match the durable backend. */ + if (strstr(children.keys[i], "\xE2\x80\xA5")) { + free(children.keys[i]); continue; } if (unique && same_child(children.keys[unique - 1], children.keys[i])) { @@ -3616,17 +3758,126 @@ int kvspaceShmList(kvspace_t *kv, const char *prefix, bool ex, int resolve, *on = children.keys; *oc = children.count; return 0; - fail: keyscan_free(&children); return -1; } - +int kvspaceShmList(kvspace_t *kv, const char *prefix, bool ex, int resolve, + char ***on, int32_t *oc) { + if (!kv || !prefix || !on || !oc) + return -1; + *on = NULL; + *oc = 0; + char target[4096], ext[1024]; + int rc = list_target(kv, prefix, ex, resolve, target, sizeof target, ext, + sizeof ext); + if (rc != 0) + return rc > 0 ? 0 : -1; + return list_scan(kv, target, ext, on, oc); +} +/* Caller holds lc_mtx (or handle is unshared, e.g. Close). */ +static void list_cache_drop(kvspace_t *kv) { + for (int32_t i = 0; i < kv->lc_count; i++) + free(kv->lc_names[i]); + free(kv->lc_names); + free(kv->lc_prefix); + free(kv->lc_target); + free(kv->lc_ext); + kv->lc_names = NULL; + kv->lc_prefix = kv->lc_target = kv->lc_ext = NULL; + kv->lc_count = 0; +} +/* Cached name list for ListLen/ListAt; caller holds lc_mtx. On return 0, + * kv->lc_names/lc_count hold the result. A hit requires matching (prefix, ex, + * resolve), unchanged resolved target/ext, and unchanged list_gen (read before + * resolve/scan so any concurrent key-set mutation forces a rescan next time). */ +static int list_cached(kvspace_t *kv, const char *prefix, bool ex, int resolve) { + uint64_t g = __atomic_load_n(&kv->hdr->list_gen, __ATOMIC_ACQUIRE); + char target[4096], ext[1024]; + int rc = list_target(kv, prefix, ex, resolve, target, sizeof target, ext, + sizeof ext); + if (rc < 0) + return -1; + if (rc > 0) { + list_cache_drop(kv); + return 0; + } + if (kv->lc_prefix && kv->lc_gen == g && kv->lc_ex == (uint8_t)ex && + kv->lc_res == (uint8_t)(resolve != 0) && + strcmp(kv->lc_prefix, prefix) == 0 && + strcmp(kv->lc_target, target) == 0 && strcmp(kv->lc_ext, ext) == 0) + return 0; + list_cache_drop(kv); + char **names = NULL; + int32_t count = 0; + if (list_scan(kv, target, ext, &names, &count) != 0) + return -1; + char *p = strdup(prefix), *t = strdup(target), *e = strdup(ext); + if (!p || !t || !e) { + free(p); + free(t); + free(e); + for (int32_t i = 0; i < count; i++) + free(names[i]); + free(names); + return -1; + } + kv->lc_prefix = p; + kv->lc_target = t; + kv->lc_ext = e; + kv->lc_names = names; + kv->lc_count = count; + kv->lc_ex = (uint8_t)ex; + kv->lc_res = (uint8_t)(resolve != 0); + kv->lc_gen = g; + return 0; +} +int kvspaceShmListLen(kvspace_t *kv, const char *prefix, bool ex, int resolve, + int32_t *out_count) { + if (!out_count) + return -1; + *out_count = 0; + if (!kv || !prefix) + return -1; + pthread_mutex_lock(&kv->lc_mtx); + int rc = list_cached(kv, prefix, ex, resolve); + if (rc == 0) + *out_count = kv->lc_count; + pthread_mutex_unlock(&kv->lc_mtx); + return rc; +} +/* Copy the idx-th direct child name into buf (without NUL). *out_len is set to + * the name length; returns non-zero if out of range or buf is too small. Shares + * cache with ListLen so ListLen + indexed ListAt scans the directory once. */ +int kvspaceShmListAt(kvspace_t *kv, const char *prefix, bool ex, int resolve, + int32_t idx, uint8_t *buf, uint32_t buf_cap, + uint32_t *out_len) { + if (!out_len) + return -1; + *out_len = 0; + if (!kv || !prefix) + return -1; + pthread_mutex_lock(&kv->lc_mtx); + int rc = list_cached(kv, prefix, ex, resolve); + if (rc == 0) { + rc = -1; + if (idx >= 0 && idx < kv->lc_count) { + size_t l = strlen(kv->lc_names[idx]); + *out_len = (uint32_t)l; + if (buf && l <= buf_cap) { + memcpy(buf, kv->lc_names[idx], l); + rc = 0; + } + } + } + pthread_mutex_unlock(&kv->lc_mtx); + return rc; +} int kvspaceShmExtindex(kvspace_t *kv, const char *p, const char *ep, char *err, uint32_t err_cap) { if (!kv || !p || !ep) return -1; - /* 对齐 durable ext_index:path 与 ext_path 必须是目录形键(/ 或 · 结尾)。 */ + /* Match durable ext_index: path and ext_path must be directory keys ('/' or U+00B7 suffix). */ if (bad_dir_prefix(p) || bad_dir_prefix(ep)) { if (err && err_cap) snprintf(err, err_cap, @@ -3657,7 +3908,8 @@ int kvspaceShmExtindex(kvspace_t *kv, const char *p, const char *ep, } if (ltl > (uint16_t)(sizeof lt - 1)) ltl = (uint16_t)(sizeof lt - 1); - memcpy(lt, ltd, ltl); + if (ltl) + memcpy(lt, ltd, ltl); lt[ltl] = 0; uint8_t *v = NULL; uint64_t vl = 0; diff --git a/src/kvspace_shm.h b/src/kvspace_shm.h index 7a9be9d..1a48e85 100644 --- a/src/kvspace_shm.h +++ b/src/kvspace_shm.h @@ -5,8 +5,10 @@ * 对齐 kvspace-go KVSpace 接口. */ -#ifndef KVSPACE_H -#define KVSPACE_H +/* Guard must not be KVSPACE_H: that is the canonical ABI header + * guard, and colliding would silently skip whichever header is included second. */ +#ifndef KVSPACE_SHM_H +#define KVSPACE_SHM_H #include "coord.h" #include @@ -15,9 +17,13 @@ typedef struct kvspace kvspace_t; -/* shm store 格式版本:4 = ART val 原子字 + 垃圾环 + 单写者降级 + 8B 节点对齐。 - * 不兼容旧版本:open 硬失败并提示重建(不迁移)。 */ -#define KVSPACE_FORMAT_VERSION 5 +/* SHM store format version: + * 4 = ART atomic val word + garbage ring + single-writer demotion + 8B node align; + * 5 = ART 112-byte block; + * 6 = XValue fixed 8-byte header + inline attr region (#566/#571, incompatible + * with format 5) plus header list_gen for cross-process List cache invalidation. + * Incompatible older versions fail hard on open with a rebuild hint (no migration). */ +#define KVSPACE_FORMAT_VERSION 6 /* 对齐 kvspace/include/kvspace/kvspace.h:叶子 + 目录祖先。 */ typedef struct { @@ -58,6 +64,8 @@ uint8_t *kvspaceShmGetByRef(kvspace_t *kv, kvspaceRef_t *ref, /* shm slot view:返回映射内 head/body 与整 box 物理容量。0=成功,-1=不可视槽。 */ int kvspaceShmXvalueBodyView(kvspace_t *kv, const kvspaceRef_t *ref, const char *key_fallback, kvspaceXvalueBodyView_t *out); +int kvspaceShmSetPart(kvspace_t *kv, const char *key, uint32_t offset, + const uint8_t *buf, uint32_t buf_len); int kvspaceShmSetPartByRef(kvspace_t *kv, kvspaceRef_t *ref, const char *key_fallback, uint32_t offset, const uint8_t *buf, uint32_t buf_len); @@ -77,15 +85,21 @@ int kvspaceShmSetValueByRef(kvspace_t *kv, kvspaceRef_t *ref, // 覆写);否则返回非 0。 int kvspaceShmWriteInPlace(kvspace_t *kv, const char *key, int resolve, int32_t body_len, uint8_t **body); -// WriteNewPlace: 按 (ref, storetype, body_len, langtype) 分配新 box、写固定头 + -// langtype 尾部(整体单写)、维护父索引,返回 body 指针。 +// WriteNewPlace: allocate a box for (ref, storetype, body_len, body_cap, langtype) +// with physical capacity >= 8 + body_cap + attr, write fixed header (ptr from ref) +// and attr region in one atomic store, update parent index, and return body pointer. int kvspaceShmWriteNewPlace(kvspace_t *kv, const char *key, uint8_t ref, uint8_t storetype, int32_t body_len, uint64_t body_cap, const char *langtype, uint8_t **body); -// ListLen: 只返回前缀下子项计数,无缓冲。 +// ListLen: direct child count under prefix. ListAt: copy idx-th child name into buf. +// Both share a per-handle cache so ListLen + indexed ListAt scans the directory once, +// invalidating on key-set, link-target, or extindex changes (including cross-process). int kvspaceShmListLen(kvspace_t *kv, const char *prefix, bool expand_ext, int resolve, int32_t *out_count); +int kvspaceShmListAt(kvspace_t *kv, const char *prefix, bool expand_ext, + int resolve, int32_t idx, uint8_t *buf, uint32_t buf_cap, + uint32_t *out_len); /* ================================================================ * 目录操作 @@ -96,6 +110,9 @@ int kvspaceShmList(kvspace_t *kv, const char *prefix, bool expand_ext, int resolve, char ***out_names, int32_t *out_count); int kvspaceShmDel(kvspace_t *kv, const char *key); +// Deltree: delete base subtree (normalizing one trailing '/' or U+00B7 on prefix +// to base; "/" = root): base plus base/..., base..., and base... +// (including hidden U+2025 keys). Deletes only the pointer if prefix/base is a link. int kvspaceShmDeltree(kvspace_t *kv, const char *prefix); int kvspaceShmCp(kvspace_t *kv, const char *src, const char *dst); // 单 key 拷贝 @@ -158,6 +175,5 @@ int64_t kvspaceShmArtAlignCheck(kvspace_t *kv); void kvspaceShmTestSetFormatVersion(kvspace_t *kv, uint32_t v); #endif int kvspaceShmViewSweep(kvspace_t *kv); -int kvspaceShmViewFlushTomb(kvspace_t *kv); #endif diff --git a/src/xvalue_head.c b/src/xvalue_head.c index 24ed5f3..7deed6d 100644 --- a/src/xvalue_head.c +++ b/src/xvalue_head.c @@ -3,17 +3,26 @@ #include #include -/* V2 最终态(#571):head 固定 8 字节 [len:u48][storetype:u8][ptr:u8], - body 偏移恒 = 8;attr 区 @8+len(长度自描述,含 langtype 记录)。整体单 key - 一次写,天然原子——无 attr key。langtype 空 = 无记录(空 attr)。 */ +/* V2 final layout (#571): fixed 8-byte header [len:u48 LE@0-5][storetype:u8@6][ptr:u8@7], + body always at offset 8, self-delimited attr region at 8+len (carrying langtype). + Single-key atomic write — no separate attr key. Empty langtype = empty attr. */ #define UINT48_MAX UINT64_C(0xFFFFFFFFFFFF) +/* Failure helper: clear *out per header contract. */ +static int fail(uint8_t **out) { + if (out) + *out = NULL; + return -1; +} + +/* Single wire builder. Zero-fills body when body is NULL (Reserve placeholder); + public entry points validate whether non-zero len requires non-NULL body. */ static int build(uint8_t storetype, uint8_t ptr, const uint8_t *body, uint64_t len, const char *langtype, uint8_t **out, uint64_t *out_len) { - if (!out || !out_len || (len && !body)) - return -1; + if (!out || !out_len) + return fail(out); *out = NULL; if (storetype > KVSPACE_XVALUE_ST_EXT || ptr > KVSPACE_XVALUE_PTR_EXT) return -1; @@ -32,7 +41,7 @@ static int build(uint8_t storetype, uint8_t ptr, const uint8_t *body, kvspaceXvalueHead h = {.storetype = storetype, .ptr = ptr}; kvspaceXvalueHeadSetLen(&h, len); memcpy(p, &h, sizeof h); - if (len) + if (len && body) memcpy(p + KVSPACE_XVALUE_HEAD_LEN, body, len); uint8_t *a = p + KVSPACE_XVALUE_HEAD_LEN + len; a[0] = (uint8_t)alen; @@ -57,7 +66,7 @@ int kvspaceXvalueHeadNewScalar(uint8_t storetype, const uint8_t *raw, uint32_t raw_len, const char *langtype, uint8_t **out, uint64_t *out_len) { if (raw_len && !raw) - return -1; + return fail(out); return build(storetype, KVSPACE_XVALUE_PTR_INLINE, raw, raw_len, langtype, out, out_len); } @@ -65,9 +74,8 @@ int kvspaceXvalueHeadNewScalar(uint8_t storetype, const uint8_t *raw, int kvspaceXvalueHeadNewSlack(const uint8_t *data, uint64_t data_len, uint64_t cap, const char *langtype, uint8_t **out, uint64_t *out_len) { - if (cap < data_len || data_len > UINT48_MAX || (data_len && !data)) - return -1; - (void)cap; + if (cap < data_len || (data_len && !data)) + return fail(out); return build(KVSPACE_XVALUE_ST_SLACK, KVSPACE_XVALUE_PTR_INLINE, data, data_len, langtype, out, out_len); } @@ -76,12 +84,9 @@ int kvspaceXvalueHeadNewTensor(const uint64_t *dims, uint32_t ndim, const uint8_t *raw, uint64_t raw_len, const char *langtype, uint8_t **out, uint64_t *out_len) { - if (!dims || ndim == 0 || ndim > 64 || raw_len > UINT48_MAX || - (raw_len && !raw)) - return -1; - (void)dims; - (void)ndim; - /* 形状在 langtype 尾部;body 字节数 = raw_len(明文层校验 numel×width) */ + /* Shape lives in leading [dims] of langtype; body byte length is raw_len. */ + if (!dims || ndim == 0 || ndim > 64 || (raw_len && !raw)) + return fail(out); return build(KVSPACE_XVALUE_ST_LARGE, KVSPACE_XVALUE_PTR_INLINE, raw, raw_len, langtype, out, out_len); } @@ -90,7 +95,7 @@ int kvspaceXvalueHeadNewPtr(const uint8_t *path, uint32_t path_len, const char *langtype, uint8_t **out, uint64_t *out_len) { if (!path_len || !path) - return -1; + return fail(out); return build(KVSPACE_XVALUE_ST_SLACK, KVSPACE_XVALUE_PTR_PTR, path, path_len, langtype, out, out_len); } @@ -99,7 +104,7 @@ int kvspaceXvalueHeadNewExt(const uint8_t *locator, uint32_t locator_len, const char *langtype, uint8_t **out, uint64_t *out_len) { if (!locator_len || !locator) - return -1; + return fail(out); return build(KVSPACE_XVALUE_ST_EXT, KVSPACE_XVALUE_PTR_EXT, locator, locator_len, langtype, out, out_len); } @@ -107,47 +112,17 @@ int kvspaceXvalueHeadNewExt(const uint8_t *locator, uint32_t locator_len, int kvspaceXvalueHeadNewCode(const uint8_t *body, uint64_t body_len, uint64_t cap, const char *langtype, uint8_t **out, uint64_t *out_len) { - if (body_len > cap || body_len > UINT48_MAX || (body_len && !body)) - return -1; - (void)cap; - return build(KVSPACE_XVALUE_ST_SLACK, KVSPACE_XVALUE_PTR_INLINE, body, - body_len, langtype, out, out_len); + return kvspaceXvalueHeadNewSlack(body, body_len, cap, langtype, out, + out_len); } int kvspaceXvalueHeadReserve(uint8_t storetype, uint64_t body_len, uint64_t cap, const char *langtype, uint8_t **out, uint64_t *out_len) { - if (body_len > UINT48_MAX || cap < body_len) - return -1; /* 固定头无容量字段:预留 = 产 body_len 字节零 body 的占位(后由调用方填充), attr 区(含 langtype 记录)一并写入。 */ - (void)cap; - uint64_t ltl = langtype ? strlen(langtype) : 0; - uint64_t alen = 2 + (ltl ? 3 + ltl : 0); - if (alen > UINT16_MAX) - return -1; - if (body_len > SIZE_MAX - KVSPACE_XVALUE_HEAD_LEN - alen) - return -1; - uint64_t n = (uint64_t)KVSPACE_XVALUE_HEAD_LEN + body_len + alen; - if (n > SIZE_MAX) - return -1; - uint8_t *p = (uint8_t *)calloc(1, (size_t)n); - if (!p) - return -1; - kvspaceXvalueHead h = {.storetype = storetype, - .ptr = KVSPACE_XVALUE_PTR_INLINE}; - kvspaceXvalueHeadSetLen(&h, body_len); - memcpy(p, &h, sizeof h); - uint8_t *a = p + KVSPACE_XVALUE_HEAD_LEN + body_len; - a[0] = (uint8_t)alen; - a[1] = (uint8_t)(alen >> 8); - if (ltl) { - a[2] = KVSPACE_XVALUE_ATTR_TAG_LANGTYPE; - a[3] = (uint8_t)ltl; - a[4] = (uint8_t)(ltl >> 8); - memcpy(a + 5, langtype, ltl); - } - *out = p; - *out_len = n; - return 0; + if (cap < body_len) + return fail(out); + return build(storetype, KVSPACE_XVALUE_PTR_INLINE, NULL, body_len, + langtype, out, out_len); } diff --git a/src/xvalue_head.h b/src/xvalue_head.h index 4c56f92..3b12a12 100644 --- a/src/xvalue_head.h +++ b/src/xvalue_head.h @@ -26,8 +26,8 @@ extern "C" { #define KVSPACE_XVALUE_PTR_PTR 1u #define KVSPACE_XVALUE_PTR_EXT 2u -/* 固定 8 字节布局(直接 memcpy struct + body)。 - wire:len:u48@0-5(小端)、storetype@6、ptr@7、body@8、langtype@8+len。 */ +/* Fixed 8-byte layout (direct memcpy struct + body). + wire: len:u48@0-5 (little-endian), storetype@6, ptr@7, body@8. */ typedef struct kvspaceXvalueHead { uint8_t len[6]; /* u48 小端(offset 0-5) */ uint8_t storetype; @@ -51,14 +51,18 @@ static inline void kvspaceXvalueHeadSetLen(kvspaceXvalueHead *h, uint64_t v) { /* attr 区常量:record tag 1 = langtype;2+ 预留扩展(ro/vid 等,未实现)。 */ #define KVSPACE_XVALUE_ATTR_TAG_LANGTYPE 1u -/* 读 attr 总长(含自身 2 字节;空 attr = 2)。 */ +/* Read total attr length (including its own 2-byte length prefix; empty attr = 2). + Unbounded: caller must ensure wire has at least 8+body_len+2 bytes; use + kvspaceXvalueHeadLangtypeBounded for untrusted or backend-borrowed inputs. */ static inline uint16_t kvspaceXvalueHeadAttrLen(const uint8_t *wire, uint64_t body_len) { const uint8_t *p = wire + KVSPACE_XVALUE_HEAD_LEN + body_len; return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); } -/* 扫 attr records 取 tag=1(langtype)记录 data;未找到/坏 attr → 返回 0。 */ +/* Scan attr records for tag=1 (langtype) data; returns 0 if missing or malformed. + Unbounded: caller must ensure wire has at least 8+body_len+attr_len bytes; use + kvspaceXvalueHeadLangtypeBounded when bounds are not yet verified. */ static inline uint16_t kvspaceXvalueHeadLangtype(const uint8_t *wire, uint64_t body_len, const uint8_t **out) { @@ -82,8 +86,31 @@ static inline uint16_t kvspaceXvalueHeadLangtype(const uint8_t *wire, return 0; } -/* 构造(固定头 + body + attr 区,单 key 整体)。langtype 空/NULL = 无 langtype - 记录(空 attr = [attr_len:2])。0 on success. -1 and *out cleared on failure. */ +/* Bounded variant: verifies 8+len+2 <= wire_len and 8+len+attr_len <= wire_len + before scanning records. Returns 0 and sets *out=NULL on missing/malformed/truncated values. */ +static inline uint16_t kvspaceXvalueHeadLangtypeBounded(const uint8_t *wire, + uint64_t wire_len, + const uint8_t **out) { + *out = NULL; + if (!wire || wire_len < KVSPACE_XVALUE_HEAD_LEN + 2u) + return 0; + uint64_t body_len = 0; + for (int i = 5; i >= 0; i--) + body_len = (body_len << 8) | wire[i]; + const uint64_t room = wire_len - KVSPACE_XVALUE_HEAD_LEN; + if (body_len > room - 2u) + return 0; + if (kvspaceXvalueHeadAttrLen(wire, body_len) > room - body_len) + return 0; + return kvspaceXvalueHeadLangtype(wire, body_len, out); +} + +/* Constructors (fixed 8-byte header + body + attr region in a single value). + Empty/NULL langtype omits the langtype record (empty attr = [attr_len:2]). + Returns 0 on success, -1 and clears *out on failure (when out is non-NULL). + Fixed header has no capacity/shape fields: cap only checks cap >= len, and + dims/ndim only check non-empty (shape lives in leading [dims] of langtype). + NewCode is an alias of NewSlack (SLACK/INLINE) kept for ABI compatibility. */ int kvspaceXvalueHeadNewNone(uint8_t **out, uint64_t *out_len); int kvspaceXvalueHeadNewScalar(uint8_t storetype, const uint8_t *raw, uint32_t raw_len, const char *langtype, diff --git a/tests/test_abi_contract.c b/tests/test_abi_contract.c new file mode 100644 index 0000000..b9d5420 --- /dev/null +++ b/tests/test_abi_contract.c @@ -0,0 +1,522 @@ +/* ABI contract regressions for v0.3.23 review findings: + * 1) WriteNewPlace ref sets ptr byte (ref=1 link, ref=2 ext); + * 2) WriteNewPlace body_cap reserves physical box capacity (view.body_cap >= requested); + * 3) SetPart / SetPartByRef bounds checks reject u32 wraparound; + * 4) Keys >= 1024 bytes are rejected without truncating into shorter keys; + * 5) SetValue rejects values with missing or truncated attr regions; + * 6) ViewResolvePrefix stops cleanly at prefix_len==0 nodes (/vthread/1/ beside /vthread/10/); + * 7) ViewWrite accepts only equal-length writes without overwriting attr; + * 8) ListLen/ListAt cache invalidates immediately on key-set changes (including cross-process); + * 9) DSN scheme must be exact "shm"; + * 10) DelTree deletes full subtrees (including U+2025 keys and trailing '/' or U+00B7) + * without following pointer targets. */ +#define _GNU_SOURCE +#include +#include "test_util.h" +#include "xvalue_head.h" +#include +#include +#include +#include +#include + +static char g_dir[] = "/tmp/kvspace-abi-contract-XXXXXX"; + +static void *open_db(const char *name) { + char dsn[256]; + snprintf(dsn, sizeof dsn, "shm://%s/%s", g_dir, name); + return kvspaceConnect(dsn); +} + +static int set_i64(void *h, const char *key, int64_t v) { + uint8_t *val = NULL; + uint32_t n = 0; + if (kvspaceNewInt64(v, "int64", &val, &n) != 0) + return -1; + int rc = kvspaceSetValue(h, key, val, n, NULL, 0); + free(val); + return rc; +} + +static int set_chars(void *h, const char *key, const char *s) { + uint8_t *val = NULL; + uint32_t n = 0; + if (kvspaceNewChar((const uint8_t *)s, (uint32_t)strlen(s), "char/utf8", + &val, &n) != 0) + return -1; + int rc = kvspaceSetValue(h, key, val, n, NULL, 0); + free(val); + return rc; +} + +static uint8_t ptr_byte(void *h, const char *key) { + uint8_t *out = NULL; + uint32_t n = 0; + kvspaceGet(h, key, 0, &out, &n); + return out && n >= KVSPACE_XVALUE_HEAD_LEN ? out[7] : 0xFF; +} + +static void test_writenewplace_ref(void) { + void *h = open_db("ref"); + REQUIRE(h, "connect"); + REQUIRE(set_i64(h, "/t", 7) == 0, "set /t"); + uint8_t *body = NULL; + REQUIRE(kvspaceWriteNewPlace(h, "/p", KVSPACE_REF_PTR, KVSPACE_STORETYPE_SLACK, + 2, 2, "int64", &body, NULL, 0) == 0 && body, + "WriteNewPlace ref=PTR"); + memcpy(body, "/t", 2); + CHECK(ptr_byte(h, "/p") == KVSPACE_XVALUE_PTR_PTR, "ref=PTR -> ptr byte %u", + ptr_byte(h, "/p")); + /* Link is dereferenced when resolve=1. */ + uint8_t *lnk = NULL, *tgt = NULL; + uint32_t ln = 0, tn = 0; + kvspaceGet(h, "/p", 1, &lnk, &ln); + kvspaceGet(h, "/t", 0, &tgt, &tn); + CHECK(lnk && tgt && ln == tn && memcmp(lnk, tgt, tn) == 0, + "resolve=1 follows the WriteNewPlace link"); + + REQUIRE(kvspaceWriteNewPlace(h, "/e", KVSPACE_REF_EXT, KVSPACE_STORETYPE_EXT, 5, + 5, "", &body, NULL, 0) == 0 && body, + "WriteNewPlace ref=EXT"); + memcpy(body, "s3://", 5); + CHECK(ptr_byte(h, "/e") == KVSPACE_XVALUE_PTR_EXT, "ref=EXT -> ptr byte %u", + ptr_byte(h, "/e")); + CHECK(ptr_byte(h, "/t") == KVSPACE_XVALUE_PTR_INLINE, "inline stays inline"); + /* Invalid (ref, storetype) combination is still rejected. */ + CHECK(kvspaceWriteNewPlace(h, "/bad", KVSPACE_REF_PTR, + KVSPACE_STORETYPE_FIXED_SMALL, 8, 8, "", &body, + NULL, 0) != 0, + "ref=PTR + FIXED_SMALL rejected"); + kvspaceClose(h); +} + +static void test_writenewplace_body_cap(void) { + void *h = open_db("cap"); + REQUIRE(h, "connect"); + uint8_t *body = NULL; + REQUIRE(kvspaceWriteNewPlace(h, "/pc", KVSPACE_REF_INLINE, + KVSPACE_STORETYPE_SLACK, 10, 1024, "", &body, + NULL, 0) == 0 && body, + "WriteNewPlace cap=1024"); + memcpy(body, "0123456789", 10); + kvspaceRef_t ref; + REQUIRE(kvspaceResolveRef(h, "/pc", &ref) == 0, "resolve /pc"); + kvspaceXvalueBodyView_t v; + REQUIRE(kvspaceXvalueBodyView(h, &ref, "/pc", &v) == 0, "body view"); + CHECK(v.body_cap >= 1024, "view.body_cap=%llu, want >= 1024", + (unsigned long long)v.body_cap); + /* Logical body length remains 10 bytes on readback. */ + uint8_t *out = NULL; + uint32_t n = 0; + kvspaceGet(h, "/pc", 0, &out, &n); + CHECK(out && n == 8 + 10 + 2 && memcmp(out + 8, "0123456789", 10) == 0, + "value reads back (len=%u)", n); + /* Rewriting within reserved capacity reuses the existing box in place. */ + uint8_t *body2 = NULL; + REQUIRE(kvspaceWriteNewPlace(h, "/pc", KVSPACE_REF_INLINE, + KVSPACE_STORETYPE_SLACK, 20, 1024, "", &body2, + NULL, 0) == 0 && body2, + "WriteNewPlace again"); + CHECK(body2 == body, "box reused when reserve already fits"); + /* body_cap < body_len is still rejected. */ + CHECK(kvspaceWriteNewPlace(h, "/x", KVSPACE_REF_INLINE, KVSPACE_STORETYPE_SLACK, + 10, 5, "", &body, NULL, 0) != 0, + "body_cap < body_len rejected"); + kvspaceClose(h); +} + +static void test_setpart_overflow(void) { + void *h = open_db("part"); + REQUIRE(h, "connect"); + REQUIRE(set_i64(h, "/v", 1) == 0, "set /v"); + uint8_t buf[32]; + memset(buf, 0xAB, sizeof buf); + /* 0xFFFFFFF0 + 32 wraps to 16 in u32; must be rejected without OOB write. */ + CHECK(kvspaceSetPart(h, "/v", 0xFFFFFFF0u, buf, 32, NULL, 0) != 0, + "SetPart wrapped range rejected"); + kvspaceRef_t ref; + REQUIRE(kvspaceResolveRef(h, "/v", &ref) == 0, "resolve /v"); + CHECK(kvspaceSetPartByRef(h, &ref, "/v", 0xFFFFFFF0u, buf, 32, NULL, 0) != 0, + "SetPartByRef wrapped range rejected"); + /* Valid range succeeds. */ + uint8_t nine[8] = {9, 0, 0, 0, 0, 0, 0, 0}; + CHECK(kvspaceSetPart(h, "/v", 8, nine, 8, NULL, 0) == 0, "SetPart in range"); + uint8_t *out = NULL; + uint32_t n = 0; + kvspaceGet(h, "/v", 0, &out, &n); + CHECK(out && out[8] == 9, "in-range SetPart landed"); + kvspaceClose(h); +} + +static void test_long_key(void) { + void *h = open_db("long"); + REQUIRE(h, "connect"); + char k[1024], longk[1100]; + k[0] = '/'; + memset(k + 1, 'a', 1022); + k[1023] = 0; /* 1023 bytes: longest key accepted by Set */ + snprintf(longk, sizeof longk, "%s/other", k); + REQUIRE(set_i64(h, k, 7) == 0, "set 1023-byte key"); + CHECK(set_i64(h, longk, 8) != 0, "Set rejects >= 1024-byte key"); + uint8_t *out = NULL; + uint32_t n = 0; + kvspaceGet(h, longk, 0, &out, &n); + CHECK(!out && n == 0, "Get(long key) is a miss, not its 1023-byte prefix"); + kvspaceGet(h, longk, 1, &out, &n); + CHECK(!out && n == 0, "Get(long key, resolve) is a miss"); + const char *ks[1] = {longk}; + kvspaceDel(h, ks, 1, NULL, 0); + kvspaceGet(h, k, 0, &out, &n); + CHECK(out != NULL, "Del(long key) leaves the 1023-byte key alone"); + uint8_t *body = NULL; + CHECK(kvspaceWriteInPlace(h, longk, 0, 8, &body, NULL, 0) != 0, + "WriteInPlace(long key) rejected"); + char longdir[1200]; + snprintf(longdir, sizeof longdir, "%s/", longk); + int32_t cnt = -1; + CHECK(kvspaceListLen(h, longdir, 0, 1, &cnt) != 0 || cnt == 0, + "ListLen(long prefix, resolve) does not list a truncated dir"); + kvspaceClose(h); +} + +static void test_setvalue_attr(void) { + void *h = open_db("attr"); + REQUIRE(h, "connect"); + /* 8B head + 8B body without attr region must be rejected. */ + uint8_t noattr[16] = {8, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0}; + CHECK(kvspaceSetValue(h, "/a", noattr, sizeof noattr, NULL, 0) != 0, + "value without attr region rejected"); + /* attr_len points past end of value. */ + uint8_t badattr[18] = {8, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, 50, 0}; + CHECK(kvspaceSetValue(h, "/b", badattr, sizeof badattr, NULL, 0) != 0, + "attr_len past end rejected"); + /* attr_len < 2. */ + uint8_t shortattr[18] = {8, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, 1, 0}; + CHECK(kvspaceSetValue(h, "/c", shortattr, sizeof shortattr, NULL, 0) != 0, + "attr_len < 2 rejected"); + /* Valid value with trailing padding bytes: accepted and trimmed to 8+len+attr. */ + uint8_t padded[20] = {8, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, 2, 0, + 0xEE, 0xEE}; + CHECK(kvspaceSetValue(h, "/d", padded, sizeof padded, NULL, 0) == 0, + "valid value with trailing bytes accepted"); + uint8_t *out = NULL; + uint32_t n = 0; + kvspaceGet(h, "/d", 0, &out, &n); + CHECK(out && n == 18, "trailing bytes not part of the value (len=%u)", n); + kvspaceClose(h); +} + +static void test_resolve_prefix(void) { + void *h = open_db("prefix"); + REQUIRE(h, "connect"); + REQUIRE(set_i64(h, "/vthread/1/x", 11) == 0 && set_i64(h, "/vthread/1/y", 12) == 0 && + set_i64(h, "/vthread/10/x", 13) == 0, + "seed keys"); + REQUIRE(kvspaceViewQuiesce(h, 0) == 0, "unquiesce"); + kvspaceRef_t pref; + CHECK(kvspaceViewResolvePrefix(h, "/vthread/1/", &pref) == 0, + "ResolvePrefix(/vthread/1/) with sibling /vthread/10/"); + kvspaceViewToken_t tok; + if (kvspaceViewResolveFrom(h, &pref, "y", &tok) == 0) + CHECK(tok.value_len == 8 && tok.body[0] == 12, "prefix ref resolves /vthread/1/y"); + else + CHECK(0, "ViewResolveFrom(prefix, y)"); + CHECK(kvspaceViewResolvePrefix(h, "/vthread/10/", &pref) == 0, + "ResolvePrefix(/vthread/10/)"); + CHECK(kvspaceViewResolvePrefix(h, "/vthread/2/", &pref) != 0, + "ResolvePrefix of a missing dir fails"); + kvspaceClose(h); +} + +static void test_viewwrite_len(void) { + void *h = open_db("view"); + REQUIRE(h, "connect"); + REQUIRE(set_chars(h, "/s", "ab") == 0, "set /s"); + REQUIRE(kvspaceViewQuiesce(h, 0) == 0, "unquiesce"); + kvspaceViewToken_t tok; + REQUIRE(kvspaceViewResolve(h, "/s", &tok) == 0, "view resolve"); + CHECK(kvspaceViewWrite(h, &tok, (const uint8_t *)"abcdefgh", 8) == 1, + "longer ViewWrite falls back"); + kvspaceHead_t hd; + CHECK(kvspaceGetHead(h, "/s", &hd) == 0 && hd.body_len == 2 && + strcmp(hd.langtype, "char/utf8") == 0, + "value and langtype intact after rejected ViewWrite"); + CHECK(kvspaceViewWrite(h, &tok, (const uint8_t *)"xy", 2) == 0, + "same-length ViewWrite"); + uint8_t *out = NULL; + uint32_t n = 0; + kvspaceGet(h, "/s", 0, &out, &n); + CHECK(out && memcmp(out + 8, "xy", 2) == 0, "same-length ViewWrite landed"); + CHECK(kvspaceGetHead(h, "/s", &hd) == 0 && strcmp(hd.langtype, "char/utf8") == 0, + "langtype intact after same-length ViewWrite"); + kvspaceClose(h); +} + +/* With per-handle ListLen + ListAt caching, any key appearance/disappearance + * must be immediately visible (including across processes). */ +static int list_names(void *h, const char *dir, char out[][32], int cap) { + int32_t n = -1; + if (kvspaceListLen(h, dir, 0, 0, &n) != 0) + return -1; + for (int32_t i = 0; i < n && i < cap; i++) { + uint32_t ol = 0; + if (kvspaceListAt(h, dir, 0, 0, i, (uint8_t *)out[i], 31, &ol) != 0) + return -2; + out[i][ol] = 0; + } + return n; +} + +static void test_list_cache(void) { + void *h = open_db("list"); + REQUIRE(h, "connect"); + REQUIRE(set_i64(h, "/d/a", 1) == 0 && set_i64(h, "/d/b", 2) == 0, "seed"); + char nm[8][32]; + CHECK(list_names(h, "/d/", nm, 8) == 2 && !strcmp(nm[0], "a") && !strcmp(nm[1], "b"), + "initial list a,b"); + REQUIRE(set_i64(h, "/d/c", 3) == 0, "add c"); + CHECK(list_names(h, "/d/", nm, 8) == 3 && !strcmp(nm[2], "c"), "insert visible"); + const char *ka[1] = {"/d/a"}; + kvspaceDel(h, ka, 1, NULL, 0); + CHECK(list_names(h, "/d/", nm, 8) == 2 && !strcmp(nm[0], "b"), "delete visible"); + REQUIRE(set_i64(h, "/d/a", 4) == 0, "revive a"); + CHECK(list_names(h, "/d/", nm, 8) == 3 && !strcmp(nm[0], "a"), "tombstone revive visible"); + /* Same-key overwrite leaves key set and listing unchanged. */ + REQUIRE(set_i64(h, "/d/a", 5) == 0, "overwrite a"); + CHECK(list_names(h, "/d/", nm, 8) == 3, "overwrite keeps list"); + /* View delete (tombstone) must also invalidate the cache. */ + REQUIRE(kvspaceViewQuiesce(h, 0) == 0, "unquiesce"); + kvspaceViewToken_t tok; + REQUIRE(kvspaceViewResolve(h, "/d/c", &tok) == 0, "view resolve c"); + CHECK(kvspaceViewDel(h, &tok, "/d/c") == 0, "view del c"); + CHECK(list_names(h, "/d/", nm, 8) == 2, "view delete visible"); + /* Out-of-range index and short buffer fail as before, reporting required length in *out_len. */ + uint8_t b1[1]; + uint32_t ol = 0; + CHECK(kvspaceListAt(h, "/d/", 0, 0, 9, b1, 1, &ol) != 0, "idx out of range"); + REQUIRE(set_i64(h, "/d/longname", 6) == 0, "add longname"); + int32_t n = 0; + kvspaceListLen(h, "/d/", 0, 0, &n); + int found = 0; + for (int32_t i = 0; i < n; i++) { + if (kvspaceListAt(h, "/d/", 0, 0, i, b1, 1, &ol) != 0 && ol == 8) + found = 1; + } + CHECK(found, "short buffer reports needed length"); + /* Key-set mutation by another process invalidates this handle's cache. */ + CHECK(list_names(h, "/d/", nm, 8) == 3, "list before child (a,b,longname)"); + fflush(NULL); + pid_t pid = fork(); + if (pid == 0) { + void *c = open_db("list"); + if (!c) + _exit(2); + int rc = set_i64(c, "/d/z", 9); + const char *kb[1] = {"/d/b"}; + kvspaceDel(c, kb, 1, NULL, 0); + kvspaceClose(c); + _exit(rc == 0 ? 0 : 3); + } + int st = 0; + waitpid(pid, &st, 0); + CHECK(WIFEXITED(st) && WEXITSTATUS(st) == 0, "child wrote"); + int cnt = list_names(h, "/d/", nm, 8); + int has_z = 0, has_b = 0; + for (int i = 0; i < cnt; i++) { + has_z |= !strcmp(nm[i], "z"); + has_b |= !strcmp(nm[i], "b"); + } + CHECK(cnt == 3 && has_z && !has_b, "cross-process insert/delete visible (n=%d)", cnt); + kvspaceClose(h); +} + +/* DelTree deletes base, base/..., base..., and base... (including + * hidden U+2025 keys), normalizes trailing '/' or U+00B7 on prefix, preserves + * sibling prefixes (/dx, /d2), and invalidates the List cache. */ +static int exists(void *h, const char *key) { + uint8_t *out = NULL; + uint32_t n = 0; + kvspaceGet(h, key, 0, &out, &n); + return out != NULL; +} + +static const char *const dt_in[] = { + "/d", "/d/a", "/d/\xE2\x80\xA5status", "/d/sub/x", "/d/sub/\xE2\x80\xA5pc", + "/d\xC2\xB7k", "/d\xC2\xB7k/y", "/d\xE2\x80\xA5x", +}; +static const char *const dt_out[] = {"/dx", "/d2/a", "/e/\xE2\x80\xA5status"}; + +static int dt_seed(void *h) { + if (kvspaceMkindex(h, "/d/", 0, NULL, 0) != 0) + return -1; + for (size_t i = 0; i < sizeof dt_in / sizeof *dt_in; i++) + if (set_i64(h, dt_in[i], (int64_t)i) != 0) + return -1; + for (size_t i = 0; i < sizeof dt_out / sizeof *dt_out; i++) + if (set_i64(h, dt_out[i], 100 + (int64_t)i) != 0) + return -1; + return 0; +} + +static void dt_check(void *h, const char *how) { + CHECK(!exists(h, "/d/"), "%s: /d/ index deleted", how); + for (size_t i = 0; i < sizeof dt_in / sizeof *dt_in; i++) + CHECK(!exists(h, dt_in[i]), "%s: %s deleted", how, dt_in[i]); + for (size_t i = 0; i < sizeof dt_out / sizeof *dt_out; i++) + CHECK(exists(h, dt_out[i]), "%s: %s kept", how, dt_out[i]); +} + +static void test_deltree(void) { + void *h = open_db("deltree"); + void *h2 = open_db("deltree"); + REQUIRE(h && h2, "connect"); + const char *forms[] = {"/d", "/d/", "/d\xC2\xB7"}; + for (size_t f = 0; f < sizeof forms / sizeof *forms; f++) { + REQUIRE(dt_seed(h) == 0, "seed for DelTree(%s)", forms[f]); + CHECK(exists(h, "/d/\xE2\x80\xA5status"), "seeded runtime key readable"); + /* Warm List caches for /d/ and /d/sub/ across both handles. */ + int32_t n1 = -1, n2 = -1, n3 = -1; + kvspaceListLen(h, "/d/", 0, 0, &n1); + kvspaceListLen(h2, "/d/", 0, 0, &n2); + kvspaceListLen(h, "/d/sub/", 0, 0, &n3); + CHECK(n1 == 2 && n2 == 2 && n3 == 1, "list before (%d,%d,%d)", n1, n2, n3); + CHECK(kvspaceDelTree(h, forms[f], NULL, 0) == 0, "DelTree(%s)", forms[f]); + dt_check(h, forms[f]); + kvspaceListLen(h, "/d/", 0, 0, &n1); + kvspaceListLen(h2, "/d/", 0, 0, &n2); + kvspaceListLen(h, "/d/sub/", 0, 0, &n3); + CHECK(n1 == 0 && n2 == 0 && n3 == 0, "list cache invalidated (%d,%d,%d)", n1, + n2, n3); + uint32_t ol = 0; + uint8_t b[8]; + CHECK(kvspaceListAt(h2, "/d/", 0, 0, 0, b, sizeof b, &ol) != 0, + "ListAt after DelTree misses"); + } + /* Pointer: delete only the pointer itself, leaving target intact (POSIX rm). */ + REQUIRE(set_i64(h, "/t/a", 1) == 0 && set_i64(h, "/t/\xE2\x80\xA5pc", 2) == 0, + "seed /t"); + uint8_t *lv = NULL; + uint32_t ln = 0; + /* REQUIRE evaluates its condition twice; keep allocating call outside to avoid leaking. */ + int prc = kvspaceNewPtr("", "/t", &lv, &ln); + REQUIRE(prc == 0, "NewPtr"); + int sp = kvspaceSetValue(h, "/p", lv, ln, NULL, 0); + int sq = kvspaceSetValue(h, "/q", lv, ln, NULL, 0); + free(lv); + REQUIRE(sp == 0 && sq == 0, "set links /p /q"); + CHECK(kvspaceDelTree(h, "/p", NULL, 0) == 0 && !exists(h, "/p"), "DelTree(link)"); + CHECK(kvspaceDelTree(h, "/q/", NULL, 0) == 0 && !exists(h, "/q"), "DelTree(link/)"); + CHECK(exists(h, "/t/a") && exists(h, "/t/\xE2\x80\xA5pc"), "link target intact"); + /* Root: Clear removes everything (including U+2025 keys). */ + CHECK(kvspaceClear(h, NULL, 0) == 0, "Clear"); + CHECK(!exists(h, "/t/a") && !exists(h, "/t/\xE2\x80\xA5pc") && !exists(h, "/dx") && + !exists(h, "/e/\xE2\x80\xA5status"), + "Clear removed everything"); + kvspaceClose(h2); + kvspaceClose(h); +} + +static void test_dsn_scheme(void) { + char dsn[256]; + snprintf(dsn, sizeof dsn, "s://%s/x", g_dir); + CHECK(kvspaceConnect(dsn) == NULL, "s:// rejected"); + snprintf(dsn, sizeof dsn, "://%s/x", g_dir); + CHECK(kvspaceConnect(dsn) == NULL, ":// rejected"); + CHECK(kvspaceConnect(NULL) == NULL, "NULL dsn rejected"); + CHECK(kvspaceConnect("shm://") == NULL, "empty path rejected"); +} + +static void test_abi_err_reporting(void) { + void *h = open_db("err"); + REQUIRE(h, "connect"); + char err[128]; + + /* kvspaceDel: invalid args return non-zero + non-empty err; missing key stays 0 */ + const char *ks[1] = {"/missing"}; + memset(err, 0, sizeof err); + CHECK(kvspaceDel(NULL, ks, 1, err, sizeof err) != 0 && err[0] != '\0', + "Del(NULL h) fails with err='%s'", err); + memset(err, 0, sizeof err); + CHECK(kvspaceDel(h, NULL, 1, err, sizeof err) != 0 && err[0] != '\0', + "Del(NULL keys, n>0) fails with err='%s'", err); + const char *null_key[1] = {NULL}; + memset(err, 0, sizeof err); + CHECK(kvspaceDel(h, null_key, 1, err, sizeof err) != 0 && err[0] != '\0', + "Del(NULL key[0]) fails with err='%s'", err); + memset(err, 0, sizeof err); + CHECK(kvspaceDel(h, ks, 1, err, sizeof err) == 0 && err[0] == '\0', + "Del(missing key) is idempotent 0"); + + /* kvspaceDelTree / kvspaceCp / kvspaceCpTree / kvspaceCpList / kvspaceMkindex / + * kvspaceRmindexExt / kvspaceClear populate err on failure */ + memset(err, 0, sizeof err); + CHECK(kvspaceDelTree(h, "/.kvspace-meta", err, sizeof err) != 0 && err[0] != '\0', + "DelTree(reserved) fails with err='%s'", err); + memset(err, 0, sizeof err); + CHECK(kvspaceCp(h, "/no-such-src", "/dst", err, sizeof err) != 0 && err[0] != '\0', + "Cp(missing src) fails with err='%s'", err); + memset(err, 0, sizeof err); + CHECK(kvspaceCpTree(h, "/a", "/a/sub", err, sizeof err) != 0 && err[0] != '\0', + "CpTree(into child) fails with err='%s'", err); + memset(err, 0, sizeof err); + CHECK(kvspaceCpList(h, "/.kvspace-meta", "/dst", err, sizeof err) != 0 && err[0] != '\0', + "CpList(reserved) fails with err='%s'", err); + memset(err, 0, sizeof err); + CHECK(kvspaceMkindex(NULL, "/idx/", 0, err, sizeof err) != 0 && err[0] != '\0', + "Mkindex(NULL h) fails with err='%s'", err); + memset(err, 0, sizeof err); + CHECK(kvspaceRmindexExt(h, "/no-such-ext/", err, sizeof err) != 0 && err[0] != '\0', + "RmindexExt(missing) fails with err='%s'", err); + memset(err, 0, sizeof err); + CHECK(kvspaceClear(NULL, err, sizeof err) != 0 && err[0] != '\0', + "Clear(NULL h) fails with err='%s'", err); + + /* Watch(target=None) on a missing/deleted key must wake immediately without waiting out tick_ns. */ + REQUIRE(set_chars(h, "/watch/del", "pc0") == 0, "seed /watch/del"); + const char *del_k[1] = {"/watch/del"}; + REQUIRE(kvspaceDel(h, del_k, 1, err, sizeof err) == 0, "del /watch/del"); + struct timespec wt0, wt1; + clock_gettime(CLOCK_MONOTONIC, &wt0); + uint8_t *wout = (uint8_t *)1; + uint32_t wlen = 99; + CHECK(kvspaceWatch(h, "/watch/del", (const uint8_t *)"", 0, 5000000000ULL, + &wout, &wlen) == 0 && + wout == NULL && wlen == 0, + "Watch(None) wakes immediately on deleted key"); + clock_gettime(CLOCK_MONOTONIC, &wt1); + uint64_t welapsed_ms = + (uint64_t)(wt1.tv_sec - wt0.tv_sec) * 1000ULL + + (uint64_t)(wt1.tv_nsec - wt0.tv_nsec) / 1000000ULL; + CHECK(welapsed_ms < 500ULL, "Watch(None) elapsed %llums < 500ms", + (unsigned long long)welapsed_ms); + + kvspaceClose(h); +} + +int main(void) { + if (!mkdtemp(g_dir)) { + perror("mkdtemp"); + return 1; + } + test_writenewplace_ref(); + test_writenewplace_body_cap(); + test_setpart_overflow(); + test_long_key(); + test_setvalue_attr(); + test_resolve_prefix(); + test_viewwrite_len(); + test_list_cache(); + test_deltree(); + test_dsn_scheme(); + test_abi_err_reporting(); + char cmd[128]; + snprintf(cmd, sizeof cmd, "rm -rf %s", g_dir); + if (system(cmd) != 0) + fprintf(stderr, "cleanup failed: %s\n", g_dir); + if (failures) { + fprintf(stderr, "test_abi_contract: %d failure(s)\n", failures); + return 1; + } + printf("test_abi_contract: ok\n"); + return 0; +} diff --git a/tests/test_art_alignment.c b/tests/test_art_alignment.c index 502f0ee..1ecc2f0 100644 --- a/tests/test_art_alignment.c +++ b/tests/test_art_alignment.c @@ -3,7 +3,8 @@ * 1) 每个 ART 块的数据起点 8 字节对齐,且 art_hdr_t.val(偏移 16)不跨 64B * cache line。否则 8 字节原子字在 x86 上退化为 split lock,在 AArch64 上 * 可能直接陷入(LSE/LLSC 要求自然对齐)——这是正确性前提,不只是性能。 - * 2) format_version 不是当前版本的 store 必须 open 失败(不迁移、不兼容)。 + * 2) Opening a store whose format_version != current version must fail (no + * migration or backward compatibility) and print a rebuild hint to stderr. */ #define _GNU_SOURCE @@ -13,6 +14,7 @@ #include #include #include +#include #include #define CHECK(x) \ @@ -53,10 +55,38 @@ int main(void) { CHECK(kvspaceShmArtAlignCheck(kv) == -1); - /* 旧格式版本(任何非当前版本):close 后 open 必须失败。 */ + /* Older format version (any non-current version): reopen after close must fail + * and print a rebuild hint to stderr (checked before mapping full current header). */ kvspaceShmTestSetFormatVersion(kv, KVSPACE_FORMAT_VERSION - 1); kvspaceShmClose(kv); - CHECK(kvspaceShmOpen(path, 8UL * 64 * 64 * 64 * 64) == NULL); + /* A real format-5 store header had no list_gen (8 bytes shorter), making the + * ART file 8 bytes shorter as well; mapping the current header first would fail + * inside region_attach before reaching a post-mmap version check. */ + struct stat st; + CHECK(stat(path, &st) == 0 && truncate(path, st.st_size - 8) == 0); + char errpath[160]; + snprintf(errpath, sizeof errpath, "%s/stderr", dir); + fflush(stderr); + int saved = dup(STDERR_FILENO); + FILE *ef = fopen(errpath, "w+"); + CHECK(saved >= 0 && ef != NULL); + dup2(fileno(ef), STDERR_FILENO); + kvspace_t *old = kvspaceShmOpen(path, 8UL * 64 * 64 * 64 * 64); + fflush(stderr); + dup2(saved, STDERR_FILENO); + close(saved); + CHECK(old == NULL); + char msg[512] = {0}; + rewind(ef); + size_t ml = fread(msg, 1, sizeof msg - 1, ef); + fclose(ef); + msg[ml] = 0; + char want[64]; + snprintf(want, sizeof want, "version=%u", (unsigned)(KVSPACE_FORMAT_VERSION - 1)); + if (!strstr(msg, "rebuild") || !strstr(msg, want)) + fprintf(stderr, "open stderr: [%s]\n", msg); + CHECK(strstr(msg, "rebuild") != NULL); + CHECK(strstr(msg, want) != NULL); printf("PASS art_alignment\n"); return 0; diff --git a/tests/test_codec_abi.c b/tests/test_codec_abi.c index e17f63d..ed7a721 100644 --- a/tests/test_codec_abi.c +++ b/tests/test_codec_abi.c @@ -6,6 +6,9 @@ #include #include #include +#ifndef _WIN32 +#include +#endif #define CHECK(x) do { if (!(x)) { fprintf(stderr, "failed: %s\n", #x); return 1; } } while (0) diff --git a/tests/test_xvalue_head.c b/tests/test_xvalue_head.c index cd3c279..f2814b2 100644 --- a/tests/test_xvalue_head.c +++ b/tests/test_xvalue_head.c @@ -168,6 +168,44 @@ static void test_reserve_code(void) { "code cap