This is an automated email from the ASF dual-hosted git repository.
spetz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new 917612d27 feat(swift): add XXH3 and XXH32 with golden vectors from the
Rust crates (#4306)
917612d27 is described below
commit 917612d27d591ed019ec9bcc3050d564b03637dc
Author: RustToMetal <[email protected]>
AuthorDate: Mon Sep 28 22:59:05 2026 -0500
feat(swift): add XXH3 and XXH32 with golden vectors from the Rust crates
(#4306)
The wire protocol checksums every batch with XXH3-64 and routes keyed
messages to partitions with XXH32, so the SDK needs both hashes before
it can encode a single send request. Both are implemented natively in
Swift rather than bound to a C library, and both are pinned to the Rust
crates the server uses through a golden fixture generated from them.
Second of nine stacked PRs for the Swift SDK; builds on #4190, relates to
#4001
---
.github/actions/swift/pre-merge/action.yml | 35 +-
.github/config/components.yml | 5 +-
.gitignore | 2 +
.typos.toml | 2 +
core/binary_protocol/src/consensus/mod.rs | 2 +
core/binary_protocol/src/consensus/request.rs | 143 ++
core/sdk/src/vsr.rs | 71 +-
foreign/swift/Package.swift | 3 +-
foreign/swift/README.md | 13 +-
.../swift/Sources/Iggy/Errors/IggyErrorCode.swift | 4 +-
.../Iggy/Hash/UnalignedLoads.swift} | 35 +-
foreign/swift/Sources/Iggy/Hash/XXH3.swift | 273 ++++
foreign/swift/Sources/Iggy/Hash/XXH32.swift | 79 ++
foreign/swift/Tests/IggyTests/ErrorCodeTests.swift | 18 +-
foreign/swift/Tests/IggyTests/Fixtures/golden.json | 1004 ++++++++++++++
foreign/swift/Tests/IggyTests/HashTests.swift | 62 +
.../Tests/IggyTests/Support/GoldenFixture.swift | 73 +
foreign/swift/Tools/golden-vectors/Cargo.toml | 36 +
foreign/swift/Tools/golden-vectors/src/main.rs | 1435 ++++++++++++++++++++
19 files changed, 3198 insertions(+), 97 deletions(-)
diff --git a/.github/actions/swift/pre-merge/action.yml
b/.github/actions/swift/pre-merge/action.yml
index 02f383ada..f21a11903 100644
--- a/.github/actions/swift/pre-merge/action.yml
+++ b/.github/actions/swift/pre-merge/action.yml
@@ -20,15 +20,16 @@ description: Swift pre-merge testing github iggy actions
inputs:
task:
- description: "Task to run (lint, test, build, build-macos)"
+ description: "Task to run (lint, test, build, build-macos, golden)"
required: true
runs:
using: "composite"
steps:
- # The macOS lane uses the Swift that ships with Xcode on the runner.
+ # The macOS lane uses the Swift that ships with Xcode on the runner, and
+ # the golden lane only runs the Rust generator.
- name: Setup Swift
- if: inputs.task != 'build-macos'
+ if: inputs.task != 'build-macos' && inputs.task != 'golden'
uses: ./.github/actions/utils/setup-swift
# swift-format ships with the toolchain; the configuration lives in
@@ -73,3 +74,31 @@ runs:
swift --version
swift build -Xswiftc -warnings-as-errors
swift test --parallel
+
+ # The generator is a small standalone workspace that needs no system
+ # packages, and the workspace cache never matches its dependency set, so
+ # the lane skips the disk cleanup, the apt install, and the cache restore.
+ - name: Setup Rust for the golden vectors
+ if: inputs.task == 'golden'
+ uses: ./.github/actions/utils/setup-rust-with-cache
+ with:
+ free-disk-space: "false"
+ read-cache: "false"
+ install-system-dependencies: "false"
+
+ # The generator sits outside the root workspace, so the repo-wide fmt and
+ # clippy runs never see it. The golden fixture is checked in, so this lane
+ # then regenerates it from the Rust crates and fails when the two differ;
+ # otherwise the Swift tests stay green against a stale copy of the
protocol.
+ - name: Check the golden vectors match the Rust crates
+ shell: bash
+ if: inputs.task == 'golden'
+ run: |
+ cargo fmt --manifest-path
foreign/swift/Tools/golden-vectors/Cargo.toml -- --check
+ cargo clippy --manifest-path
foreign/swift/Tools/golden-vectors/Cargo.toml --all-targets -- -D warnings
+ cargo run --manifest-path
foreign/swift/Tools/golden-vectors/Cargo.toml --
foreign/swift/Tests/IggyTests/Fixtures/golden.json
+ if ! git diff --exit-code --
foreign/swift/Tests/IggyTests/Fixtures/golden.json; then
+ echo "❌ golden.json is out of date; regenerate it with the command
above and commit the result"
+ exit 1
+ fi
+ echo "✅ golden.json matches the Rust crates"
diff --git a/.github/config/components.yml b/.github/config/components.yml
index eb77a9764..b8c5dd036 100644
--- a/.github/config/components.yml
+++ b/.github/config/components.yml
@@ -345,10 +345,7 @@ components:
- "foreign/swift/**"
- "bdd/swift/**"
- "examples/swift/**"
- # The SDK compiles against the VSR wire contract, so a change to the
- # protocol crate must rerun it.
- - "core/binary_protocol/**"
- tasks: ["lint", "test", "build", "build-macos"]
+ tasks: ["lint", "test", "build", "build-macos", "golden"]
# Shared BDD infrastructure that affects all BDD tests
bdd-infrastructure:
diff --git a/.gitignore b/.gitignore
index 4c5ecbe94..2b6bf9c31 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,6 +32,8 @@ examples/swift/**/.swiftpm
bdd/swift/.build
bdd/swift/**/.build
bdd/swift/**/.swiftpm
+# A standalone dev tool with path dependencies on the workspace crates.
+foreign/swift/Tools/golden-vectors/Cargo.lock
foreign/node/npm-debug.log*
foreign/node/.npm
**/node_modules
diff --git a/.typos.toml b/.typos.toml
index fd5c96870..88977d7eb 100644
--- a/.typos.toml
+++ b/.typos.toml
@@ -157,5 +157,7 @@ extend-exclude = [
"core/bench/dashboard/frontend/dist",
# WireMock test fixtures with base64-encoded JWT data
"**/wiremock/__files/*.json",
+ # Swift SDK golden vectors dumped from the Rust crates (hex payloads)
+ "foreign/swift/Tests/IggyTests/Fixtures/golden.json",
"**/.python-version",
]
diff --git a/core/binary_protocol/src/consensus/mod.rs
b/core/binary_protocol/src/consensus/mod.rs
index 67525a14e..1b9a44470 100644
--- a/core/binary_protocol/src/consensus/mod.rs
+++ b/core/binary_protocol/src/consensus/mod.rs
@@ -42,6 +42,7 @@ mod error;
mod header;
mod operation;
mod reply_result;
+mod request;
pub use command::Command;
pub use consumer_session::{
@@ -64,3 +65,4 @@ pub use reply_result::{
REJECTION_SECTION_LEN, RESULT_COUNT_LEN, RESULT_ENTRY_LEN, result_code,
result_section_len,
write_rejection_section,
};
+pub use request::{NON_REPLICATED_CODE_RANGE, operation_for_code};
diff --git a/core/binary_protocol/src/consensus/request.rs
b/core/binary_protocol/src/consensus/request.rs
new file mode 100644
index 000000000..0e2d2f8ae
--- /dev/null
+++ b/core/binary_protocol/src/consensus/request.rs
@@ -0,0 +1,143 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::codes::{LOGIN_REGISTER_CODE, LOGIN_REGISTER_WITH_PAT_CODE,
LOGOUT_USER_CODE};
+use crate::consensus::{Command, HEADER_SIZE, Operation, RequestHeader};
+use crate::error::WireError;
+use std::ops::Range;
+use twox_hash::XxHash3_64;
+
+/// Where `RequestHeader::reserved` carries the legacy command code of a
+/// `NonReplicated` request, as little-endian `u32`.
+pub const NON_REPLICATED_CODE_RANGE: Range<usize> = 0..4;
+
+/// Maps a command code onto the consensus operation a client sends it as.
+///
+/// `COMMAND_TABLE` is a protocol registry, not a per-server capability list,
+/// so a client build cannot know which codes a given server implements. The
+/// server is the authority: an unmapped code is forwarded as non-replicated
+/// (the code rides `RequestHeader::reserved`, which that path stamps) and the
+/// server answers with a proper error if it does not know it.
+#[must_use]
+pub fn operation_for_code(code: u32) -> Operation {
+ match code {
+ LOGIN_REGISTER_CODE | LOGIN_REGISTER_WITH_PAT_CODE =>
Operation::Register,
+ LOGOUT_USER_CODE => Operation::Logout,
+ _ =>
Operation::from_command_code(code).unwrap_or(Operation::NonReplicated),
+ }
+}
+
+impl RequestHeader {
+ /// Builds the header a client sends for `code` with `payload`, given the
+ /// identity the session assigned: `client`, `request` and `session`.
+ ///
+ /// Every rule the server checks lives here so that each client, and the
+ /// fixtures other SDKs verify against, agree on the bytes: metadata and
+ /// session operations stamp `request_checksum` with the payload hash,
+ /// partition and non-replicated operations leave it zero (send batches
+ /// carry their own checksum and non-replicated requests bypass dedup),
+ /// and a non-replicated request carries its command code in `reserved`.
+ ///
+ /// # Errors
+ ///
+ /// Returns `WireError::PayloadTooLarge` when the frame would not fit the
+ /// `u32` size field.
+ pub fn for_request(
+ code: u32,
+ client: u128,
+ request: u64,
+ session: u64,
+ payload: &[u8],
+ ) -> Result<Self, WireError> {
+ let operation = operation_for_code(code);
+ let total_size = HEADER_SIZE.saturating_add(payload.len());
+ let size = u32::try_from(total_size).map_err(|_|
WireError::PayloadTooLarge {
+ size: payload.len(),
+ max: u32::MAX as usize - HEADER_SIZE,
+ })?;
+ let request_checksum = if operation.is_partition() || operation ==
Operation::NonReplicated
+ {
+ 0
+ } else {
+ u128::from(XxHash3_64::oneshot(payload))
+ };
+ let mut reserved = [0; 60];
+ if operation == Operation::NonReplicated {
+
reserved[NON_REPLICATED_CODE_RANGE].copy_from_slice(&code.to_le_bytes());
+ }
+ Ok(Self {
+ command: Command::Request,
+ operation,
+ size,
+ client,
+ request,
+ session,
+ request_checksum,
+ // Replicated prepares get a server timestamp; no consumer needs a
+ // client clock read here.
+ timestamp: 0,
+ reserved,
+ ..Default::default()
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::codes::{CREATE_STREAM_CODE, PING_CODE, SEND_MESSAGES_CODE};
+
+ #[test]
+ fn login_codes_register_and_logout_logs_out() {
+ assert_eq!(operation_for_code(LOGIN_REGISTER_CODE),
Operation::Register);
+ assert_eq!(
+ operation_for_code(LOGIN_REGISTER_WITH_PAT_CODE),
+ Operation::Register
+ );
+ assert_eq!(operation_for_code(LOGOUT_USER_CODE), Operation::Logout);
+ assert_eq!(operation_for_code(u32::MAX), Operation::NonReplicated);
+ }
+
+ #[test]
+ fn metadata_requests_are_stamped_with_the_payload_hash() {
+ let payload = [1, 2, 3];
+ let header = RequestHeader::for_request(CREATE_STREAM_CODE, 7, 8, 9,
&payload).unwrap();
+ assert_eq!(header.command, Command::Request);
+ assert_eq!(header.size as usize, HEADER_SIZE + payload.len());
+ assert_eq!((header.client, header.request, header.session), (7, 8, 9));
+ assert_eq!(
+ header.request_checksum,
+ u128::from(XxHash3_64::oneshot(&payload))
+ );
+ assert_eq!(header.reserved, [0; 60]);
+ }
+
+ #[test]
+ fn partition_and_non_replicated_requests_leave_the_stamp_zero() {
+ let send = RequestHeader::for_request(SEND_MESSAGES_CODE, 1, 2, 3,
&[9; 16]).unwrap();
+ assert_eq!(send.request_checksum, 0);
+ assert!(send.operation.is_partition());
+
+ let ping = RequestHeader::for_request(PING_CODE, 1, 2, 3,
&[]).unwrap();
+ assert_eq!(ping.operation, Operation::NonReplicated);
+ assert_eq!(ping.request_checksum, 0);
+ assert_eq!(
+ ping.reserved[NON_REPLICATED_CODE_RANGE],
+ PING_CODE.to_le_bytes()
+ );
+ }
+}
diff --git a/core/sdk/src/vsr.rs b/core/sdk/src/vsr.rs
index 782f9014d..ca5fa7eb9 100644
--- a/core/sdk/src/vsr.rs
+++ b/core/sdk/src/vsr.rs
@@ -17,18 +17,14 @@
use crate::session::ConsensusSession;
use bytes::{BufMut, Bytes, BytesMut};
-use iggy_binary_protocol::codes::{
- LOGIN_REGISTER_CODE, LOGIN_REGISTER_WITH_PAT_CODE, LOGOUT_USER_CODE,
-};
+use iggy_binary_protocol::codes::{LOGIN_REGISTER_CODE,
LOGIN_REGISTER_WITH_PAT_CODE};
use iggy_binary_protocol::consensus::{
Command, EvictionHeader, EvictionReason, GenericHeader, HEADER_SIZE,
Operation, ReplyHeader,
- RequestHeader, read_size_field, result_code, result_section_len,
+ RequestHeader, operation_for_code, read_size_field, result_code,
result_section_len,
};
-use iggy_common::{IggyError, calculate_checksum, eviction_reason_to_error};
+use iggy_common::{IggyError, eviction_reason_to_error};
use std::sync::atomic::{AtomicU64, Ordering};
-const NON_REPLICATED_CODE_RANGE: std::ops::Range<usize> = 0..4;
-
// A reconnect creates a fresh VSR client and session. A replicated request
// retried there gets a new (client_id, request_id) tuple, so server-side
// deduplication cannot match a mutation that may already have committed before
@@ -104,55 +100,14 @@ pub(crate) fn encode_request_header(
}
}
};
- // Metadata dedup compares this stamp with its cached replies. Partition
- // dedup tracks request ids without retaining replies or comparing stamps;
- // send batches carry their own checksum. NonReplicated ops bypass dedup.
- let request_checksum = if operation.is_partition() || operation ==
Operation::NonReplicated {
- 0
- } else {
- u128::from(calculate_checksum(payload))
- };
- let total_size = HEADER_SIZE
- .checked_add(payload.len())
- .ok_or(IggyError::InvalidConfiguration)?;
- let size = u32::try_from(total_size).map_err(|_|
IggyError::InvalidConfiguration)?;
- let mut reserved = [0; 60];
- if operation == Operation::NonReplicated {
-
reserved[NON_REPLICATED_CODE_RANGE].copy_from_slice(&code.to_le_bytes());
- }
- let header = RequestHeader {
- command: Command::Request,
- operation,
- size,
- client: session.client_id(),
- request: request_id,
- session: session_id,
- // Lets the client table tell a genuine retry from a `request` number
reused
- // for different arguments. Zero means unstamped, which is what an SDK
- // predating this sends. A server that rewrites the body (PAT,
password)
- // carries it through untouched, so it keeps describing what the
client sent.
- request_checksum,
- // Replicated prepares get a server timestamp. Direct replies may echo
- // this field, but no RTT consumer needs a client clock read here.
- timestamp: 0,
- reserved,
- ..Default::default()
- };
-
- Ok((header, total_size))
-}
-
-/// `COMMAND_TABLE` is a protocol registry, not a per-server capability list,
so
-/// an SDK build cannot know which codes a given server implements. The server
is
-/// the authority: an unmapped code is forwarded as non-replicated (the code
-/// rides `RequestHeader.reserved`, which that path already stamps) and the
-/// server answers with a proper error if it does not know it.
-pub(crate) fn operation_for_code(code: u32) -> Operation {
- if code == LOGOUT_USER_CODE {
- return Operation::Logout;
- }
-
- Operation::from_command_code(code).unwrap_or(Operation::NonReplicated)
+ // The header rules (checksum stamp, size, reserved code) live in the
+ // protocol crate so every client and the cross-SDK fixtures share them;
+ // only the session identity is decided here.
+ let header =
+ RequestHeader::for_request(code, session.client_id(), request_id,
session_id, payload)
+ .map_err(|_| IggyError::InvalidConfiguration)?;
+ debug_assert_eq!(header.operation, operation);
+ Ok((header, header.size as usize))
}
/// Whether replaying `code` after reconnecting with a new session cannot
@@ -363,12 +318,14 @@ mod tests {
use super::*;
use crate::session::ConsensusSession;
use iggy_binary_protocol::codes::{
- CREATE_STREAM_CODE, GET_STREAM_CODE, PING_CODE, SEND_MESSAGES_CODE,
+ CREATE_STREAM_CODE, GET_STREAM_CODE, LOGOUT_USER_CODE, PING_CODE,
SEND_MESSAGES_CODE,
};
+ use iggy_binary_protocol::consensus::NON_REPLICATED_CODE_RANGE;
use iggy_binary_protocol::requests::streams::CreateStreamRequest;
use iggy_binary_protocol::requests::users::LoginRegisterRequest;
use iggy_binary_protocol::version::IGGY_PROTOCOL_VERSION;
use iggy_binary_protocol::{ClientVersionInfo, WireEncode, WireName,
WireOptions};
+ use iggy_common::calculate_checksum;
use secrecy::SecretString;
fn decode_request_header(bytes: &Bytes) -> RequestHeader {
diff --git a/foreign/swift/Package.swift b/foreign/swift/Package.swift
index c644c9a27..27af6f7ae 100644
--- a/foreign/swift/Package.swift
+++ b/foreign/swift/Package.swift
@@ -34,7 +34,8 @@ let package = Package(
.target(name: "Iggy"),
.testTarget(
name: "IggyTests",
- dependencies: ["Iggy"]
+ dependencies: ["Iggy"],
+ resources: [.copy("Fixtures")]
),
],
swiftLanguageModes: [.v6]
diff --git a/foreign/swift/README.md b/foreign/swift/README.md
index 1b2d243d4..f6657e0e2 100644
--- a/foreign/swift/README.md
+++ b/foreign/swift/README.md
@@ -11,8 +11,9 @@
Official Swift client SDK for [Apache Iggy](https://iggy.apache.org) message
streaming.
The SDK is built up in stages. This stage carries the package scaffold, the
error table,
-and the byte codec the wire protocol is encoded with. The protocol layer, the
TCP and TLS
-client, the producer and consumer, examples, and BDD scenarios follow in later
changes.
+the byte codec the wire protocol is encoded with, and native XXH3-64 and XXH32
hashes
+pinned to the Rust crates by golden vectors. The protocol layer, the TCP and
TLS client,
+the producer and consumer, examples, and BDD scenarios follow in later changes.
## Requirements
@@ -75,6 +76,14 @@ cd foreign/swift
swift test
```
+The hash and error-code tests compare against
`Tests/IggyTests/Fixtures/golden.json`, a
+fixture dumped from the Rust protocol crates. Regenerate it after a protocol
change, from
+`foreign/swift`, and commit the result; CI fails when the checked-in copy is
stale:
+
+```bash
+cargo run --manifest-path Tools/golden-vectors/Cargo.toml --
Tests/IggyTests/Fixtures/golden.json
+```
+
## Contributing
Format the sources with the toolchain's formatter before opening a pull
request; CI runs it
diff --git a/foreign/swift/Sources/Iggy/Errors/IggyErrorCode.swift
b/foreign/swift/Sources/Iggy/Errors/IggyErrorCode.swift
index 1b6940d1f..ffbac5d24 100644
--- a/foreign/swift/Sources/Iggy/Errors/IggyErrorCode.swift
+++ b/foreign/swift/Sources/Iggy/Errors/IggyErrorCode.swift
@@ -16,8 +16,8 @@
// under the License.
// Every discriminant the server can answer with, transcribed from
-// `core/common/src/error/iggy_error.rs`. The golden-vector generator that
-// follows in a later change dumps the same table and the tests compare.
+// `core/common/src/error/iggy_error.rs`. `Tools/golden-vectors` dumps the
+// same table from the crate and `ErrorCodeTests` compares the two.
/// Numeric error code shared with every other Apache Iggy SDK and the server.
///
diff --git a/foreign/swift/Package.swift
b/foreign/swift/Sources/Iggy/Hash/UnalignedLoads.swift
similarity index 61%
copy from foreign/swift/Package.swift
copy to foreign/swift/Sources/Iggy/Hash/UnalignedLoads.swift
index c644c9a27..41157f761 100644
--- a/foreign/swift/Package.swift
+++ b/foreign/swift/Sources/Iggy/Hash/UnalignedLoads.swift
@@ -1,4 +1,3 @@
-// swift-tools-version: 6.0
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
@@ -16,26 +15,16 @@
// specific language governing permissions and limitations
// under the License.
-import PackageDescription
+extension UnsafeRawBufferPointer {
+ /// The little-endian word at `offset`, which both hashes read from
+ /// arbitrary positions of the input and the secret.
+ @inline(__always)
+ func loadLittleEndianUInt32(at offset: Int) -> UInt32 {
+ UInt32(littleEndian: loadUnaligned(fromByteOffset: offset, as:
UInt32.self))
+ }
-let package = Package(
- name: "apache-iggy",
- platforms: [
- .macOS(.v13),
- .iOS(.v16),
- .tvOS(.v16),
- .watchOS(.v9),
- .visionOS(.v1),
- ],
- products: [
- .library(name: "Iggy", targets: ["Iggy"])
- ],
- targets: [
- .target(name: "Iggy"),
- .testTarget(
- name: "IggyTests",
- dependencies: ["Iggy"]
- ),
- ],
- swiftLanguageModes: [.v6]
-)
+ @inline(__always)
+ func loadLittleEndianUInt64(at offset: Int) -> UInt64 {
+ UInt64(littleEndian: loadUnaligned(fromByteOffset: offset, as:
UInt64.self))
+ }
+}
diff --git a/foreign/swift/Sources/Iggy/Hash/XXH3.swift
b/foreign/swift/Sources/Iggy/Hash/XXH3.swift
new file mode 100644
index 000000000..4eebc7417
--- /dev/null
+++ b/foreign/swift/Sources/Iggy/Hash/XXH3.swift
@@ -0,0 +1,273 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/// XXH3 64-bit hash with the default secret and a zero seed.
+///
+/// Every message frame and batch on the wire is checksummed with this hash,
+/// and the server verifies the values, so the implementation has to match the
+/// reference bit for bit. It is checked against vectors produced by the Rust
+/// `twox-hash` crate in the test suite.
+enum XXH3 {
+ private static let prime32_1: UInt64 = 0x9E37_79B1
+ private static let prime32_2: UInt64 = 0x85EB_CA77
+ private static let prime32_3: UInt64 = 0xC2B2_AE3D
+ private static let prime64_1: UInt64 = 0x9E37_79B1_85EB_CA87
+ private static let prime64_2: UInt64 = 0xC2B2_AE3D_27D4_EB4F
+ private static let prime64_3: UInt64 = 0x1656_67B1_9E37_79F9
+ private static let prime64_4: UInt64 = 0x85EB_CA77_C2B2_AE63
+ private static let prime64_5: UInt64 = 0x27D4_EB2F_1656_67C5
+ private static let primeMX1: UInt64 = 0x1656_6791_9E37_79F9
+ private static let primeMX2: UInt64 = 0x9FB2_1C65_1E98_DF25
+
+ private static let stripeLength = 64
+ private static let secretConsumeRate = 8
+ private static let accumulatorCount = 8
+ private static let secretLength = 192
+ private static let stripesPerBlock = (secretLength - stripeLength) /
secretConsumeRate
+ private static let blockLength = stripeLength * stripesPerBlock
+ private static let secretLastAccumulatorStart = 7
+ private static let secretMergeAccumulatorsStart = 11
+ private static let midsizeStartOffset = 3
+ private static let midsizeLastOffset = 17
+ private static let secretSizeMin = 136
+
+ private static let secret: [UInt8] = [
+ 0xb8, 0xfe, 0x6c, 0x39, 0x23, 0xa4, 0x4b, 0xbe, 0x7c, 0x01, 0x81,
0x2c, 0xf7, 0x21, 0xad, 0x1c,
+ 0xde, 0xd4, 0x6d, 0xe9, 0x83, 0x90, 0x97, 0xdb, 0x72, 0x40, 0xa4,
0xa4, 0xb7, 0xb3, 0x67, 0x1f,
+ 0xcb, 0x79, 0xe6, 0x4e, 0xcc, 0xc0, 0xe5, 0x78, 0x82, 0x5a, 0xd0,
0x7d, 0xcc, 0xff, 0x72, 0x21,
+ 0xb8, 0x08, 0x46, 0x74, 0xf7, 0x43, 0x24, 0x8e, 0xe0, 0x35, 0x90,
0xe6, 0x81, 0x3a, 0x26, 0x4c,
+ 0x3c, 0x28, 0x52, 0xbb, 0x91, 0xc3, 0x00, 0xcb, 0x88, 0xd0, 0x65,
0x8b, 0x1b, 0x53, 0x2e, 0xa3,
+ 0x71, 0x64, 0x48, 0x97, 0xa2, 0x0d, 0xf9, 0x4e, 0x38, 0x19, 0xef,
0x46, 0xa9, 0xde, 0xac, 0xd8,
+ 0xa8, 0xfa, 0x76, 0x3f, 0xe3, 0x9c, 0x34, 0x3f, 0xf9, 0xdc, 0xbb,
0xc7, 0xc7, 0x0b, 0x4f, 0x1d,
+ 0x8a, 0x51, 0xe0, 0x4b, 0xcd, 0xb4, 0x59, 0x31, 0xc8, 0x9f, 0x7e,
0xc9, 0xd9, 0x78, 0x73, 0x64,
+ 0xea, 0xc5, 0xac, 0x83, 0x34, 0xd3, 0xeb, 0xc3, 0xc5, 0x81, 0xa0,
0xff, 0xfa, 0x13, 0x63, 0xeb,
+ 0x17, 0x0d, 0xdd, 0x51, 0xb7, 0xf0, 0xda, 0x49, 0xd3, 0x16, 0x55,
0x26, 0x29, 0xd4, 0x68, 0x9e,
+ 0x2b, 0x16, 0xbe, 0x58, 0x7d, 0x47, 0xa1, 0xfc, 0x8f, 0xf8, 0xb8,
0xd1, 0x7a, 0xd0, 0x31, 0xce,
+ 0x45, 0xcb, 0x3a, 0x8f, 0x95, 0x16, 0x04, 0x28, 0xaf, 0xd7, 0xfb,
0xca, 0xbb, 0x4b, 0x40, 0x7e,
+ ]
+
+ static func hash64(_ bytes: [UInt8]) -> UInt64 {
+ bytes.withUnsafeBytes { hash64($0) }
+ }
+
+ static func hash64(_ bytes: ArraySlice<UInt8>) -> UInt64 {
+ bytes.withUnsafeBytes { hash64($0) }
+ }
+
+ static func hash64(_ input: UnsafeRawBufferPointer) -> UInt64 {
+ secret.withUnsafeBytes { secret in
+ let length = input.count
+ switch length {
+ case 0:
+ return avalanche64(secret.loadLittleEndianUInt64(at: 56) ^
secret.loadLittleEndianUInt64(at: 64))
+ case 1...3:
+ return hash1to3(input, secret)
+ case 4...8:
+ return hash4to8(input, secret)
+ case 9...16:
+ return hash9to16(input, secret)
+ case 17...128:
+ return hash17to128(input, secret)
+ case 129...240:
+ return hash129to240(input, secret)
+ default:
+ return hashLong(input, secret)
+ }
+ }
+ }
+
+ @inline(__always)
+ private static func rotl(_ value: UInt64, _ count: UInt64) -> UInt64 {
+ (value << count) | (value >> (64 - count))
+ }
+
+ @inline(__always)
+ private static func mul128Fold64(_ lhs: UInt64, _ rhs: UInt64) -> UInt64 {
+ let product = lhs.multipliedFullWidth(by: rhs)
+ return product.high ^ product.low
+ }
+
+ @inline(__always)
+ private static func avalanche64(_ input: UInt64) -> UInt64 {
+ var hash = input
+ hash ^= hash >> 33
+ hash &*= prime64_2
+ hash ^= hash >> 29
+ hash &*= prime64_3
+ hash ^= hash >> 32
+ return hash
+ }
+
+ @inline(__always)
+ private static func avalanche(_ input: UInt64) -> UInt64 {
+ var hash = input
+ hash ^= hash >> 37
+ hash &*= primeMX1
+ hash ^= hash >> 32
+ return hash
+ }
+
+ @inline(__always)
+ private static func rrmxmx(_ input: UInt64, _ length: Int) -> UInt64 {
+ var hash = input
+ hash ^= rotl(hash, 49) ^ rotl(hash, 24)
+ hash &*= primeMX2
+ hash ^= (hash >> 35) &+ UInt64(length)
+ hash &*= primeMX2
+ return hash ^ (hash >> 28)
+ }
+
+ private static func hash1to3(_ input: UnsafeRawBufferPointer, _ secret:
UnsafeRawBufferPointer) -> UInt64 {
+ let length = input.count
+ let byte1 = UInt32(input[0])
+ let byte2 = UInt32(input[length >> 1])
+ let byte3 = UInt32(input[length - 1])
+ let combined = (byte1 << 16) | (byte2 << 24) | byte3 | (UInt32(length)
<< 8)
+ let bitflip = UInt64(secret.loadLittleEndianUInt32(at: 0) ^
secret.loadLittleEndianUInt32(at: 4))
+ return avalanche64(UInt64(combined) ^ bitflip)
+ }
+
+ private static func hash4to8(_ input: UnsafeRawBufferPointer, _ secret:
UnsafeRawBufferPointer) -> UInt64 {
+ let length = input.count
+ let input1 = input.loadLittleEndianUInt32(at: 0)
+ let input2 = input.loadLittleEndianUInt32(at: length - 4)
+ let bitflip = secret.loadLittleEndianUInt64(at: 8) ^
secret.loadLittleEndianUInt64(at: 16)
+ let input64 = UInt64(input2) &+ (UInt64(input1) << 32)
+ return rrmxmx(input64 ^ bitflip, length)
+ }
+
+ private static func hash9to16(_ input: UnsafeRawBufferPointer, _ secret:
UnsafeRawBufferPointer) -> UInt64 {
+ let length = input.count
+ let bitflip1 = secret.loadLittleEndianUInt64(at: 24) ^
secret.loadLittleEndianUInt64(at: 32)
+ let bitflip2 = secret.loadLittleEndianUInt64(at: 40) ^
secret.loadLittleEndianUInt64(at: 48)
+ let inputLow = input.loadLittleEndianUInt64(at: 0) ^ bitflip1
+ let inputHigh = input.loadLittleEndianUInt64(at: length - 8) ^ bitflip2
+ let accumulator = UInt64(length) &+ inputLow.byteSwapped &+ inputHigh
&+ mul128Fold64(inputLow, inputHigh)
+ return avalanche(accumulator)
+ }
+
+ @inline(__always)
+ private static func mix16(_ input: UnsafeRawBufferPointer, _ inputOffset:
Int, _ secret: UnsafeRawBufferPointer, _ secretOffset: Int) -> UInt64 {
+ let inputLow = input.loadLittleEndianUInt64(at: inputOffset)
+ let inputHigh = input.loadLittleEndianUInt64(at: inputOffset + 8)
+ return mul128Fold64(inputLow ^ secret.loadLittleEndianUInt64(at:
secretOffset), inputHigh ^ secret.loadLittleEndianUInt64(at: secretOffset + 8))
+ }
+
+ private static func hash17to128(_ input: UnsafeRawBufferPointer, _ secret:
UnsafeRawBufferPointer) -> UInt64 {
+ let length = input.count
+ var accumulator = UInt64(length) &* prime64_1
+ if length > 32 {
+ if length > 64 {
+ if length > 96 {
+ accumulator &+= mix16(input, 48, secret, 96)
+ accumulator &+= mix16(input, length - 64, secret, 112)
+ }
+ accumulator &+= mix16(input, 32, secret, 64)
+ accumulator &+= mix16(input, length - 48, secret, 80)
+ }
+ accumulator &+= mix16(input, 16, secret, 32)
+ accumulator &+= mix16(input, length - 32, secret, 48)
+ }
+ accumulator &+= mix16(input, 0, secret, 0)
+ accumulator &+= mix16(input, length - 16, secret, 16)
+ return avalanche(accumulator)
+ }
+
+ private static func hash129to240(_ input: UnsafeRawBufferPointer, _
secret: UnsafeRawBufferPointer) -> UInt64 {
+ let length = input.count
+ var accumulator = UInt64(length) &* prime64_1
+ let rounds = length / 16
+ for round in 0..<8 {
+ accumulator &+= mix16(input, 16 * round, secret, 16 * round)
+ }
+ accumulator = avalanche(accumulator)
+ for round in 8..<rounds {
+ accumulator &+= mix16(input, 16 * round, secret, 16 * (round - 8)
+ midsizeStartOffset)
+ }
+ accumulator &+= mix16(input, length - 16, secret, secretSizeMin -
midsizeLastOffset)
+ return avalanche(accumulator)
+ }
+
+ private static let initialAccumulators: [UInt64] = [prime32_3, prime64_1,
prime64_2, prime64_3, prime64_4, prime32_2, prime64_5, prime32_1]
+
+ /// The accumulators live in a stack allocation: this path runs for every
+ /// input over 240 bytes, so it must not hit the heap.
+ private static func hashLong(_ input: UnsafeRawBufferPointer, _ secret:
UnsafeRawBufferPointer) -> UInt64 {
+ let length = input.count
+ let blocks = (length - 1) / blockLength
+ return withUnsafeTemporaryAllocation(of: UInt64.self, capacity:
accumulatorCount) { accumulators in
+ for index in 0..<accumulatorCount {
+ accumulators.initializeElement(at: index, to:
initialAccumulators[index])
+ }
+ for block in 0..<blocks {
+ accumulate(accumulators, input, block * blockLength, secret,
stripes: stripesPerBlock)
+ scramble(accumulators, secret, secretLength - stripeLength)
+ }
+ let stripes = ((length - 1) - blockLength * blocks) / stripeLength
+ accumulate(accumulators, input, blocks * blockLength, secret,
stripes: stripes)
+ accumulate512(accumulators, input, length - stripeLength, secret,
secretLength - stripeLength - secretLastAccumulatorStart)
+ return mergeAccumulators(accumulators, secret,
secretMergeAccumulatorsStart, start: UInt64(length) &* prime64_1)
+ }
+ }
+
+ @inline(__always)
+ private static func accumulate512(
+ _ accumulators: UnsafeMutableBufferPointer<UInt64>, _ input:
UnsafeRawBufferPointer, _ inputOffset: Int, _ secret: UnsafeRawBufferPointer,
+ _ secretOffset: Int
+ ) {
+ for lane in 0..<accumulatorCount {
+ let dataValue = input.loadLittleEndianUInt64(at: inputOffset + 8 *
lane)
+ let dataKey = dataValue ^ secret.loadLittleEndianUInt64(at:
secretOffset + 8 * lane)
+ accumulators[lane ^ 1] &+= dataValue
+ accumulators[lane] &+= UInt64(UInt32(truncatingIfNeeded: dataKey))
&* (dataKey >> 32)
+ }
+ }
+
+ @inline(__always)
+ private static func accumulate(
+ _ accumulators: UnsafeMutableBufferPointer<UInt64>, _ input:
UnsafeRawBufferPointer, _ inputOffset: Int, _ secret: UnsafeRawBufferPointer,
+ stripes: Int
+ ) {
+ for stripe in 0..<stripes {
+ accumulate512(accumulators, input, inputOffset + stripe *
stripeLength, secret, stripe * secretConsumeRate)
+ }
+ }
+
+ @inline(__always)
+ private static func scramble(_ accumulators:
UnsafeMutableBufferPointer<UInt64>, _ secret: UnsafeRawBufferPointer, _
secretOffset: Int) {
+ for lane in 0..<accumulatorCount {
+ let key = secret.loadLittleEndianUInt64(at: secretOffset + 8 *
lane)
+ var value = accumulators[lane]
+ value ^= value >> 47
+ value ^= key
+ value &*= prime32_1
+ accumulators[lane] = value
+ }
+ }
+
+ private static func mergeAccumulators(
+ _ accumulators: UnsafeMutableBufferPointer<UInt64>, _ secret:
UnsafeRawBufferPointer, _ secretOffset: Int, start: UInt64
+ ) -> UInt64 {
+ var result = start
+ for pair in 0..<4 {
+ let low = accumulators[2 * pair] ^
secret.loadLittleEndianUInt64(at: secretOffset + 16 * pair)
+ let high = accumulators[2 * pair + 1] ^
secret.loadLittleEndianUInt64(at: secretOffset + 16 * pair + 8)
+ result &+= mul128Fold64(low, high)
+ }
+ return avalanche(result)
+ }
+}
diff --git a/foreign/swift/Sources/Iggy/Hash/XXH32.swift
b/foreign/swift/Sources/Iggy/Hash/XXH32.swift
new file mode 100644
index 000000000..1489bb511
--- /dev/null
+++ b/foreign/swift/Sources/Iggy/Hash/XXH32.swift
@@ -0,0 +1,79 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+/// XXH32 with a zero seed: the hash every SDK uses to map a messages key to a
+/// partition, so a key lands on the same partition whichever SDK produced it.
+enum XXH32 {
+ private static let prime1: UInt32 = 0x9E37_79B1
+ private static let prime2: UInt32 = 0x85EB_CA77
+ private static let prime3: UInt32 = 0xC2B2_AE3D
+ private static let prime4: UInt32 = 0x27D4_EB2F
+ private static let prime5: UInt32 = 0x1656_67B1
+
+ static func hash(_ bytes: [UInt8]) -> UInt32 {
+ bytes.withUnsafeBytes { hash($0) }
+ }
+
+ static func hash(_ input: UnsafeRawBufferPointer) -> UInt32 {
+ let length = input.count
+ var offset = 0
+ var hash: UInt32
+ if length >= 16 {
+ var v1 = prime1 &+ prime2
+ var v2 = prime2
+ var v3: UInt32 = 0
+ var v4 = 0 &- prime1
+ while offset + 16 <= length {
+ v1 = round(v1, input.loadLittleEndianUInt32(at: offset))
+ v2 = round(v2, input.loadLittleEndianUInt32(at: offset + 4))
+ v3 = round(v3, input.loadLittleEndianUInt32(at: offset + 8))
+ v4 = round(v4, input.loadLittleEndianUInt32(at: offset + 12))
+ offset += 16
+ }
+ hash = rotl(v1, 1) &+ rotl(v2, 7) &+ rotl(v3, 12) &+ rotl(v4, 18)
+ } else {
+ hash = prime5
+ }
+ hash &+= UInt32(truncatingIfNeeded: length)
+ while offset + 4 <= length {
+ hash &+= input.loadLittleEndianUInt32(at: offset) &* prime3
+ hash = rotl(hash, 17) &* prime4
+ offset += 4
+ }
+ while offset < length {
+ hash &+= UInt32(input[offset]) &* prime5
+ hash = rotl(hash, 11) &* prime1
+ offset += 1
+ }
+ hash ^= hash >> 15
+ hash &*= prime2
+ hash ^= hash >> 13
+ hash &*= prime3
+ hash ^= hash >> 16
+ return hash
+ }
+
+ @inline(__always)
+ private static func rotl(_ value: UInt32, _ count: UInt32) -> UInt32 {
+ (value << count) | (value >> (32 - count))
+ }
+
+ @inline(__always)
+ private static func round(_ accumulator: UInt32, _ lane: UInt32) -> UInt32
{
+ rotl(accumulator &+ lane &* prime2, 13) &* prime1
+ }
+}
diff --git a/foreign/swift/Tests/IggyTests/ErrorCodeTests.swift
b/foreign/swift/Tests/IggyTests/ErrorCodeTests.swift
index 50750bf33..57410106c 100644
--- a/foreign/swift/Tests/IggyTests/ErrorCodeTests.swift
+++ b/foreign/swift/Tests/IggyTests/ErrorCodeTests.swift
@@ -21,13 +21,10 @@ import Testing
@Suite("Error codes")
struct ErrorCodeTests {
- /// Pins the table to the server's `IggyError` discriminants: the count,
- /// both ends, and a code from every band.
+ /// Spot checks a code from every band by name; the golden test below
+ /// covers the whole table.
@Test func tableMatchesTheServerCodes() {
- #expect(IggyErrorCode.allCases.count == 240)
#expect(IggyErrorCode.error.rawValue == 1)
- #expect(IggyErrorCode.allCases.map(\.rawValue).min() == 1)
- #expect(IggyErrorCode.allCases.map(\.rawValue).max() == 14003)
#expect(IggyErrorCode.incompatibleProtocolVersion.rawValue == 14003)
#expect(IggyErrorCode(rawValue: 57) == .transientNotCommitted)
#expect(IggyErrorCode(rawValue: 58) == .transientNotAccepted)
@@ -39,6 +36,17 @@ struct ErrorCodeTests {
#expect(Set(IggyErrorCode.allCases.map(\.name)).count ==
IggyErrorCode.allCases.count)
}
+ /// The full table, against the dump the Rust generator takes from the
+ /// server's error enum.
+ @Test func errorCodesMatchTheServerTable() {
+ let golden = GoldenFixture.shared
+ let known = Dictionary(uniqueKeysWithValues:
IggyErrorCode.allCases.map { ($0.rawValue, $0.name) })
+ #expect(known.count == golden.errors.count)
+ for entry in golden.errors {
+ #expect(known[entry.code] == entry.name, "code \(entry.code)")
+ }
+ }
+
@Test func unknownWireCodesKeepTheRawValue() {
let error = IggyError(wireCode: 65_535)
#expect(error.code == .error)
diff --git a/foreign/swift/Tests/IggyTests/Fixtures/golden.json
b/foreign/swift/Tests/IggyTests/Fixtures/golden.json
new file mode 100644
index 000000000..5ce2ef942
--- /dev/null
+++ b/foreign/swift/Tests/IggyTests/Fixtures/golden.json
@@ -0,0 +1,1004 @@
+{
+ "protocol_version": 11264,
+ "protocol_version_min": 11264,
+ "xxh3_64": {
+ "0": "2d06800538d394c2",
+ "1": "4c5cca45d0f4811f",
+ "2": "a7e250c97710ff27",
+ "3": "15f7093b173d005c",
+ "4": "dca012f95811b6b9",
+ "5": "b290cafc7b254345",
+ "6": "99b2e675fba1e0b5",
+ "7": "7561869c23da3c1b",
+ "8": "dec6a9a43575982e",
+ "9": "cbe393399f17ffbd",
+ "10": "02ab330b44074ad1",
+ "11": "32f214df8eed29c2",
+ "12": "46aaf92c7550afa4",
+ "13": "9e314510b0dbd95c",
+ "14": "11c33b5547089feb",
+ "15": "545e19990471dc37",
+ "16": "7e484c18d74895d0",
+ "17": "208bde5ee2bed407",
+ "18": "0b2fc9efca72f11f",
+ "19": "4a8e9636611ef195",
+ "20": "b752b00444280fd5",
+ "21": "c0c251a7aaa88000",
+ "22": "00bec0f929fb1424",
+ "23": "f1ed4292dd803ac6",
+ "24": "48953b9e46ea32c5",
+ "25": "2cb743cc698a6b5b",
+ "26": "be451d7ce35fa496",
+ "27": "482ddc5d1c67b259",
+ "28": "711a5d15799c5096",
+ "29": "77faf0f24f702d64",
+ "30": "96a2eca234677819",
+ "31": "a937652b0119ca11",
+ "32": "03df0ac5255d1446",
+ "33": "199a362122d71f46",
+ "34": "502358051cdae375",
+ "35": "3979f3a31317573b",
+ "36": "362c6fed990fb45f",
+ "37": "e07bdcb266833293",
+ "38": "3ceb2f4c443330ce",
+ "39": "35590c4b1b716079",
+ "40": "80ef652ab3f1b4b5",
+ "41": "f88aad891ae21edc",
+ "42": "491e0741086bc8b1",
+ "43": "7faba0b013e5699b",
+ "44": "63cc939553a0f673",
+ "45": "fef1383ea53f9386",
+ "46": "ffaa39765685d4d8",
+ "47": "0b9d89b90ef6f825",
+ "48": "37fc8c692bc274e1",
+ "49": "600574020639d802",
+ "50": "98eac754de954c04",
+ "51": "4ef18bcbbbe5c7b4",
+ "52": "b618d180fb23c0e7",
+ "53": "a88e3182010cacd3",
+ "54": "e670fa5605d0bbcf",
+ "55": "1fc37b65eeb8ce92",
+ "56": "3c837cc3c15a01bd",
+ "57": "5298171a4b2c5766",
+ "58": "a4f0435a673b9650",
+ "59": "a12450aededa97c1",
+ "60": "5b53af902b44d30e",
+ "61": "104cc689e1c6359f",
+ "62": "c7415ebecf37be00",
+ "63": "76d4eec1f092847f",
+ "64": "dd30702ab46b3745",
+ "65": "fab36b851b94ce20",
+ "66": "7ed877eaeb7ab09e",
+ "67": "5679e93ebb6b4667",
+ "68": "48a7f2c0bb6660d6",
+ "69": "5b605745067f8f78",
+ "70": "d254549641ca7cda",
+ "71": "6b020d44d649cd18",
+ "72": "8a3f6fb70e7f3c5a",
+ "73": "d63ad17c03358856",
+ "74": "adb5d6b84bb0c7e6",
+ "75": "f38e445be5bb7774",
+ "76": "924ae4523fda2623",
+ "77": "80af1a82d31f26ed",
+ "78": "5bae338f3410811f",
+ "79": "2db5e7edf9911248",
+ "80": "bab4673ab248e1e2",
+ "81": "adbb20e609fbccf3",
+ "82": "914a22ae0735c26c",
+ "83": "d310fea8290adf1c",
+ "84": "ef3d3650aeba996d",
+ "85": "a6b8f2b76480c80f",
+ "86": "422f63ca8db1dc58",
+ "87": "578e0991378b0edb",
+ "88": "56b28c120f398e4e",
+ "89": "b1b9cd0d8836c6e5",
+ "90": "8bf0778b4af4060a",
+ "91": "6c1f0cdee38bbe57",
+ "92": "c8c7b7a3f485984c",
+ "93": "bb1e53010e1fe285",
+ "94": "a381f91d6391149b",
+ "95": "9857e7a5a42c6fe9",
+ "96": "d245cd2541582982",
+ "97": "60e3e1d0d43785b3",
+ "98": "5d08d66481f2ca1d",
+ "99": "5e2543d8ebdd1946",
+ "100": "8c97158042fbf926",
+ "101": "d2fdb1aa73b78a0a",
+ "102": "d9ef45a695356955",
+ "103": "22e5af597a44ac05",
+ "104": "d3dce276c955e780",
+ "105": "77b5b20585cb1848",
+ "106": "fa0d562d176e7144",
+ "107": "763f0966982e0c4e",
+ "108": "baa3a573cadfa833",
+ "109": "56f9c083f935b104",
+ "110": "60bca2d0a693e0ff",
+ "111": "22d06ad6c0216ec3",
+ "112": "dbae773438781980",
+ "113": "18f12e161c06865b",
+ "114": "c48fc7c4e67f0a55",
+ "115": "5394b3c6f33d6c02",
+ "116": "f9dd9e1211e25d2d",
+ "117": "7044bacb399d52ca",
+ "118": "a97b6c41a31ca72d",
+ "119": "d1fa6a6c33ed184c",
+ "120": "dbb9fc4818080eba",
+ "121": "5884cd0cf2cb9749",
+ "122": "866a55eee34e87b1",
+ "123": "08649efe402a8b29",
+ "124": "71ec3fce38b2ea36",
+ "125": "65e3707c889a12d0",
+ "126": "71e3999204f56048",
+ "127": "a915ed6396db8cc0",
+ "128": "f92b70eaa21a6288",
+ "129": "f8f76713f2bb60fa",
+ "130": "303a2c3823c3c34a",
+ "131": "9ddf75554d4ef43f",
+ "132": "ec1ecd2020110609",
+ "133": "8374b8d42931f8f1",
+ "134": "94dfabf82db326cf",
+ "135": "54659005622c9884",
+ "136": "a62bec58f8de6b28",
+ "137": "a1eccf34d87679ab",
+ "138": "e1a6abf613d0c713",
+ "139": "d4d888ea09ad8be2",
+ "140": "73f9bf9367a34a5a",
+ "141": "265cb82209b57673",
+ "142": "c56b3ae0803aa7f2",
+ "143": "c372f52c27156971",
+ "144": "d87f00765ee3d89e",
+ "145": "65d480417b4734f8",
+ "146": "86c2036ce5e060f8",
+ "147": "d07b5c2f0fefd8a8",
+ "148": "91dbbdae16d82b72",
+ "149": "7749c551163f6b04",
+ "150": "b87aa95cb5f54aa2",
+ "151": "01246a53c1e24e38",
+ "152": "665c91b034d599be",
+ "153": "ee6ddbe8d073c666",
+ "154": "2f8d3364d5dfccc8",
+ "155": "11f85d6cd9966443",
+ "156": "15f260ba4b1e0968",
+ "157": "063b1ecd55a72930",
+ "158": "25bb4a9fb16a2d48",
+ "159": "a327b7cae27c8e10",
+ "160": "c90911ffcef461e2",
+ "161": "f11ed84f70bade2e",
+ "162": "de69b26de1a9b3f2",
+ "163": "0ae8762e72422656",
+ "164": "4b00656e32461c31",
+ "165": "7ba0c08511bbe8b3",
+ "166": "27cda8258d9b1179",
+ "167": "87b3e083141f3409",
+ "168": "3a62b95662105e41",
+ "169": "01b490d2d915abad",
+ "170": "643721c9bf32781c",
+ "171": "8287c649ce43e397",
+ "172": "a3dae8f46d8fd330",
+ "173": "c1f47864f99336fc",
+ "174": "5c0072559c0dd67b",
+ "175": "bfdab869a2187b30",
+ "176": "67f10aaf46d79a44",
+ "177": "a8ece1c90e74efbf",
+ "178": "50887a99d15a2cfa",
+ "179": "32da48475d1b8ad5",
+ "180": "bb150b04dbb71027",
+ "181": "ce006955e8ea3d6a",
+ "182": "3d8cc59b34a1d734",
+ "183": "8807c2bf48aadebc",
+ "184": "530061af42546bfa",
+ "185": "955b87dacb465bb7",
+ "186": "82f6fee48cc701c4",
+ "187": "338ed6d7173ffe5d",
+ "188": "e29ffc4b065c0c89",
+ "189": "0fe5c2e35a759ae3",
+ "190": "2f77a9373bb6e149",
+ "191": "f0025e0992b308fb",
+ "192": "084028fa06f01d30",
+ "193": "f0eb08bf31ca42b6",
+ "194": "88510eba2c60efe4",
+ "195": "5ea76d72c792b501",
+ "196": "69a29b581ae42494",
+ "197": "7b7dcee881e9ea8c",
+ "198": "3622e29db96318fc",
+ "199": "8124966e9386ec04",
+ "200": "12fdb864685f344d",
+ "201": "16101b47f8644472",
+ "202": "4a2dec05a37c91a0",
+ "203": "0fac1fabb0a5293a",
+ "204": "0e1f9008494d4fd7",
+ "205": "dc66165d9b20e180",
+ "206": "1718ea095c340389",
+ "207": "5f43962e18eca50e",
+ "208": "80826d7a65be5e69",
+ "209": "6a4f9f953de8f7e4",
+ "210": "afcc79d2b952690b",
+ "211": "b95f2c918d8fa761",
+ "212": "de10988270de16b4",
+ "213": "7f939839dd0460dd",
+ "214": "71c112f5c1a6c2ec",
+ "215": "2b3d2c4e3dca7a16",
+ "216": "7de819be3e17b031",
+ "217": "24667ff21981291c",
+ "218": "403cec6b6dde22fd",
+ "219": "a21db8d0bfda5b12",
+ "220": "72dc002165c95f3c",
+ "221": "5efc45722724a76f",
+ "222": "beff6aef7d2c8900",
+ "223": "6e8f4c03accab5a4",
+ "224": "e69e5c66a23443bc",
+ "225": "d01a4f7215895c93",
+ "226": "33cd79ebc1406b8e",
+ "227": "d16389c3956e82db",
+ "228": "8ba30402067714e9",
+ "229": "efb75e81f42711c2",
+ "230": "b73ba8942ee8ec15",
+ "231": "90b17ddb0b598b06",
+ "232": "1eb54c570c6806ff",
+ "233": "409daebb5cf1cbe0",
+ "234": "7f230cb54edc2575",
+ "235": "89aa4d3171c051ba",
+ "236": "a2c90d7776c670eb",
+ "237": "c80c97d6950b23f5",
+ "238": "7f477f909665b010",
+ "239": "caa9b7a588464745",
+ "240": "ccc7375172c41f03",
+ "241": "0b3b630948ce4a00",
+ "242": "64ac2c6712669e05",
+ "243": "cffaa40f5965922d",
+ "244": "79217054e33ac2a4",
+ "245": "bbedbcb1301bb022",
+ "246": "39b4015e13725a91",
+ "247": "7241d3e647366786",
+ "248": "81d90928ac0eeb66",
+ "249": "2f4f47dd7c3957d9",
+ "250": "359fe5d526851d06",
+ "251": "0ea0f7ed02748029",
+ "252": "a9ccd8780a626691",
+ "253": "aea766e710bb3aeb",
+ "254": "51b4770c44653ef6",
+ "255": "89932170686cdd9a",
+ "256": "ec85b75bafe6ca74",
+ "257": "12ef0ff633841459",
+ "258": "a4e6408d09e952e3",
+ "259": "954bb7c819d7c692",
+ "260": "4150449c54ae2818",
+ "261": "490bafd842c62536",
+ "262": "93c74114185bb9f1",
+ "263": "8c32f72ee30ed67b",
+ "264": "cfec02e4c65bd842",
+ "265": "cba945b79bf66219",
+ "266": "73cc985d4262f1bd",
+ "267": "ea08024b75cee207",
+ "268": "a34a41f5b9290997",
+ "269": "9849db6a8d8a039d",
+ "270": "2d3f19fb47538bfd",
+ "271": "1462787cc436c2ac",
+ "272": "7818444dc3fc7377",
+ "273": "be33f0cc03efec53",
+ "274": "40209d9b28a3090c",
+ "275": "1f2190eae6926c89",
+ "276": "08a98724b9d4be39",
+ "277": "073cc2461c21a8c2",
+ "278": "6573d2625ad5453b",
+ "279": "238e1629b7d8a8dd",
+ "280": "9020cab4d10a7dd4",
+ "281": "3f18233e9a6fefb2",
+ "282": "8e61883aeb841cf6",
+ "283": "ac3e9f2639b847d1",
+ "284": "231ca645b3d6d9d7",
+ "285": "b547f515e1870e8a",
+ "286": "afcdf1eda6591016",
+ "287": "141879f6e2da2d26",
+ "288": "cd3525433788a0c1",
+ "289": "4b4b552404655f01",
+ "290": "c2857d0990b83292",
+ "291": "e5e33ec16832605a",
+ "292": "772c8707770ae6c7",
+ "293": "268b0d6bf2a299b3",
+ "294": "b04d39c78d01f277",
+ "295": "e087beb0838fa21b",
+ "296": "e3759db580b6b195",
+ "297": "43c31cb1bcd727ac",
+ "298": "c2ec247efd191d2e",
+ "299": "c218b3a960ab59ac",
+ "300": "baad8e6a5f186ce6",
+ "511": "0fec8fb6eae1df8b",
+ "512": "efc2e52d1c2ffbaa",
+ "513": "eb39b64b9df98485",
+ "1000": "989765d0ea7a5ecd",
+ "1023": "f0d330ce2b3300fb",
+ "1024": "23bc880ebf0d29c6",
+ "1025": "c09fdfbc398c7d82",
+ "1279": "4e4a4f22808ca125",
+ "1280": "12b5908a8421697e",
+ "1281": "77f822b489e658c9",
+ "2047": "c49f1b36d955611e",
+ "2048": "19f6f9c987331373",
+ "2049": "5045460f5d85c275",
+ "4096": "a3c19f8174cde0bb",
+ "10000": "441f01d9711bebed",
+ "65536": "cf188822048798b0",
+ "100003": "e0950e66cf107fe8"
+ },
+ "xxh32": {
+ "0": "02cc5d05",
+ "1": "002e0d32",
+ "2": "ceb77c10",
+ "3": "aca17380",
+ "4": "073faa82",
+ "5": "16ac9537",
+ "6": "28e6b74b",
+ "7": "a9a7eebf",
+ "8": "b9893c6c",
+ "9": "ef682a43",
+ "10": "df648e86",
+ "11": "b79882ee",
+ "12": "9a1b075b",
+ "13": "84981cca",
+ "14": "f0a519a1",
+ "15": "9f29f87b",
+ "16": "3f6c9665",
+ "17": "e048ecdb",
+ "18": "e735aa1b",
+ "19": "342f1b7a",
+ "20": "cd1d4cf6",
+ "21": "dcb4db13",
+ "22": "4a959fa8",
+ "23": "0e236d9c",
+ "24": "40e1bf6c",
+ "25": "2cd48e6f",
+ "26": "9ab2fa79",
+ "27": "d658b557",
+ "28": "932ee011",
+ "29": "0da6ba78",
+ "30": "70f7a448",
+ "31": "00f1525c",
+ "32": "ee42e668",
+ "33": "770b5126",
+ "34": "d06f9029",
+ "35": "d0b8e260",
+ "36": "2322d463",
+ "37": "ba3f318f",
+ "38": "c0b27692",
+ "39": "06f7aac6",
+ "40": "404d20b6",
+ "41": "bd6bfc29",
+ "42": "18ac587c",
+ "43": "011b4a4a",
+ "44": "4de820bc",
+ "45": "811826a2",
+ "46": "ddb70dd9",
+ "47": "9b8b41ae",
+ "48": "cddaf19f",
+ "49": "9ff7eb17",
+ "50": "7d6b4ad7",
+ "51": "92a09016",
+ "52": "3a5fb3b9",
+ "53": "cc8f1214",
+ "54": "7fdfbfec",
+ "55": "9a4510e6",
+ "56": "10a4653e",
+ "57": "938ea8cb",
+ "58": "6e19f2da",
+ "59": "9b35c6a0",
+ "60": "f108d254",
+ "61": "9b46dfdb",
+ "62": "5b34f093",
+ "63": "471925bb",
+ "64": "f2a797fa",
+ "65": "832b0571",
+ "66": "83479242",
+ "67": "43e06d37",
+ "68": "1a59277c",
+ "69": "0e2caae7",
+ "70": "b1de93c7",
+ "71": "f5d3d88b",
+ "72": "ebd57645",
+ "73": "df41e4c0",
+ "74": "9d04e365",
+ "75": "f1f7f28b",
+ "76": "fe438835",
+ "77": "b9706d77",
+ "78": "79da63fd",
+ "79": "4798b7da",
+ "80": "5941d56e",
+ "81": "317dd0a0",
+ "82": "be2781b0",
+ "83": "82271a1e",
+ "84": "aec7c13c",
+ "85": "8affc730",
+ "86": "a367d4f7",
+ "87": "55b4e6ec",
+ "88": "810f5925",
+ "89": "e0e8454f",
+ "90": "4dce1e40",
+ "91": "df7e00e1",
+ "92": "9a87dcaa",
+ "93": "0a9a1189",
+ "94": "10500aa8",
+ "95": "e6f121e9",
+ "96": "d5a45742",
+ "97": "329fc2fd",
+ "98": "05b76f9c",
+ "99": "b9134f58",
+ "100": "75936eb8",
+ "101": "a876209d",
+ "102": "40aec185",
+ "103": "23de2ca4",
+ "104": "5d029390",
+ "105": "ff3aaa59",
+ "106": "16017ee3",
+ "107": "92c34c0b",
+ "108": "31684422",
+ "109": "158f3945",
+ "110": "414b5aa7",
+ "111": "5181fd0c",
+ "112": "1a8fe9e8",
+ "113": "4978c428",
+ "114": "f8c41f86",
+ "115": "bd0885e5",
+ "116": "0077cb85",
+ "117": "a91c56e4",
+ "118": "1cff50ac",
+ "119": "b3110bfc",
+ "120": "9ac4fe4f",
+ "121": "537ca2c9",
+ "122": "46d63e82",
+ "123": "1c85a07c",
+ "124": "7be6c5bd",
+ "125": "99b14eb0",
+ "126": "3a91dc40",
+ "127": "86025592",
+ "128": "0bf98264",
+ "129": "f2d10021",
+ "130": "1392a7bd",
+ "131": "30cb6f4d",
+ "132": "a9c3af8c",
+ "133": "ec2be087",
+ "134": "b199db99",
+ "135": "616568b5",
+ "136": "3d7d5662",
+ "137": "5d758663",
+ "138": "3a83b054",
+ "139": "95588c70",
+ "140": "2aa29cfd",
+ "141": "6d7eef4e",
+ "142": "19df68de",
+ "143": "6facb331",
+ "144": "db5579cb",
+ "145": "545163cb",
+ "146": "b355146e",
+ "147": "39c7b330",
+ "148": "55690afb",
+ "149": "a4d7b73e",
+ "150": "e72476e7",
+ "151": "d789523c",
+ "152": "bfe4811e",
+ "153": "405f132b",
+ "154": "6a5ef718",
+ "155": "5954ade0",
+ "156": "a69ac10d",
+ "157": "f38ddccb",
+ "158": "0548ae32",
+ "159": "0046cd9f",
+ "160": "c95973c8",
+ "161": "fbe49a53",
+ "162": "7a1832c0",
+ "163": "272ef877",
+ "164": "61e783b4",
+ "165": "d4ea4da7",
+ "166": "5a15bd89",
+ "167": "873824f4",
+ "168": "fc1163ea",
+ "169": "30f344e8",
+ "170": "610f4a57",
+ "171": "6c627d2e",
+ "172": "6779419f",
+ "173": "fb172823",
+ "174": "203816c7",
+ "175": "f9654ce0",
+ "176": "905716fd",
+ "177": "d065830e",
+ "178": "ab7b5244",
+ "179": "d2211f0e",
+ "180": "f83e2a14",
+ "181": "7ebb5d63",
+ "182": "738b1196",
+ "183": "62ce0895",
+ "184": "65382b10",
+ "185": "2299631e",
+ "186": "b5f9edf6",
+ "187": "85b83c4b",
+ "188": "dd03425e",
+ "189": "8b7a00c7",
+ "190": "3c6cd6c2",
+ "191": "bdcacc4d",
+ "192": "a730353b",
+ "193": "2f29e535",
+ "194": "2de42d25",
+ "195": "4fd0d7c1",
+ "196": "b979af56",
+ "197": "a94d4089",
+ "198": "fb2b6758",
+ "199": "5fc42e0a",
+ "200": "bf02ae18",
+ "201": "d446c8c4",
+ "202": "2f323967",
+ "203": "bfa0b579",
+ "204": "164800e9",
+ "205": "38623a71",
+ "206": "95c3dbb7",
+ "207": "708e6dfe",
+ "208": "64bf4c2e",
+ "209": "7fc2ad5d",
+ "210": "88df9963",
+ "211": "8f0f0954",
+ "212": "88733897",
+ "213": "b988b775",
+ "214": "b0eb0103",
+ "215": "d9e76484",
+ "216": "a087b647",
+ "217": "5e12429f",
+ "218": "5f855316",
+ "219": "7126b180",
+ "220": "08d9bbf5",
+ "221": "5029c90f",
+ "222": "87221984",
+ "223": "572b4a70",
+ "224": "216e6caa",
+ "225": "9d7a7a8a",
+ "226": "e5a620d4",
+ "227": "7132cd26",
+ "228": "921f3837",
+ "229": "95d40682",
+ "230": "f62fb72f",
+ "231": "2aca433d",
+ "232": "39c767d1",
+ "233": "25c7401c",
+ "234": "69d3a768",
+ "235": "f78f2430",
+ "236": "3c380b37",
+ "237": "19a5ead9",
+ "238": "0df92aef",
+ "239": "f28a7330",
+ "240": "6be3ca8b",
+ "241": "8c1402a0",
+ "242": "061599eb",
+ "243": "d79e0848",
+ "244": "d52430aa",
+ "245": "3894c6c7",
+ "246": "4a7b19f3",
+ "247": "44e2dfb5",
+ "248": "fbe16171",
+ "249": "e2d44691",
+ "250": "5dafd373",
+ "251": "b4c89dd3",
+ "252": "023ea9ca",
+ "253": "68dcd4a4",
+ "254": "ce927921",
+ "255": "dbe7dd0a",
+ "256": "d40ce0c6",
+ "257": "c1aba8c3",
+ "258": "40f4d5e7",
+ "259": "cb28799e",
+ "260": "a01039df",
+ "261": "fc02b393",
+ "262": "7bb829e3",
+ "263": "72f1ebe2",
+ "264": "efe97ef0",
+ "265": "e842dc00",
+ "266": "10a1bb8a",
+ "267": "2c360d7a",
+ "268": "12b0a3f8",
+ "269": "31214a23",
+ "270": "e00e6457",
+ "271": "ecf57d22",
+ "272": "7478f037",
+ "273": "31d7156c",
+ "274": "7e0dd53e",
+ "275": "68b430b6",
+ "276": "32506bad",
+ "277": "f0e91c11",
+ "278": "8d25c0ef",
+ "279": "cfbd0c21",
+ "280": "dd1fefc9",
+ "281": "ef012cdb",
+ "282": "ba3cd529",
+ "283": "bc283309",
+ "284": "18acf6e9",
+ "285": "6b131ce8",
+ "286": "8d41c940",
+ "287": "a82b85a4",
+ "288": "887dff95",
+ "289": "de705416",
+ "290": "04a48f97",
+ "291": "88bf730d",
+ "292": "56c4edbd",
+ "293": "95e86815",
+ "294": "7e55cbf4",
+ "295": "3f540617",
+ "296": "59a4c515",
+ "297": "1982239e",
+ "298": "1b21f991",
+ "299": "49cced94",
+ "300": "ec3c8ce2",
+ "511": "da196d23",
+ "512": "91fc3706",
+ "513": "13c6bd04",
+ "1000": "a793e7c7",
+ "1023": "2f81efc2",
+ "1024": "ead2ce3c",
+ "1025": "de7d7475",
+ "1279": "768b3053",
+ "1280": "b606805a",
+ "1281": "7b3f1b44",
+ "2047": "7a5fe81e",
+ "2048": "62bf26bf",
+ "2049": "ce1d9edf",
+ "4096": "f11917ba",
+ "10000": "80691c39",
+ "65536": "38f0653c",
+ "100003": "20796e40"
+ },
+ "errors": {
+ "1": "error",
+ "2": "invalid_configuration",
+ "3": "invalid_command",
+ "4": "invalid_format",
+ "5": "feature_unavailable",
+ "6": "invalid_identifier",
+ "7": "invalid_version",
+ "8": "disconnected",
+ "9": "cannot_establish_connection",
+ "10": "cannot_create_base_directory",
+ "11": "cannot_create_runtime_directory",
+ "12": "cannot_remove_runtime_directory",
+ "13": "cannot_create_state_directory",
+ "14": "state_file_not_found",
+ "15": "state_file_corrupted",
+ "16": "invalid_state_entry_checksum",
+ "19": "cannot_open_database",
+ "20": "resource_not_found",
+ "21": "cannot_close_web_socket_connection",
+ "30": "stale_client",
+ "31": "tcp_error",
+ "32": "quic_error",
+ "33": "invalid_server_address",
+ "34": "invalid_client_address",
+ "35": "invalid_ip_address",
+ "36": "http_error",
+ "37": "invalid_api_url",
+ "40": "unauthenticated",
+ "41": "unauthorized",
+ "42": "invalid_credentials",
+ "43": "invalid_username",
+ "44": "invalid_password",
+ "45": "invalid_user_status",
+ "46": "user_already_exists",
+ "47": "user_inactive",
+ "48": "cannot_delete_user",
+ "49": "cannot_change_permissions",
+ "50": "invalid_personal_access_token_name",
+ "51": "personal_access_token_already_exists",
+ "52": "personal_access_tokens_limit_reached",
+ "53": "invalid_personal_access_token",
+ "54": "personal_access_token_expired",
+ "55": "users_limit_reached",
+ "56": "invalid_personal_access_token_expiry",
+ "57": "transient_not_committed",
+ "58": "transient_not_accepted",
+ "59": "request_already_applied",
+ "61": "not_connected",
+ "63": "client_shutdown",
+ "64": "invalid_tls_domain",
+ "65": "invalid_tls_certificate_path",
+ "66": "invalid_tls_certificate",
+ "67": "failed_to_add_certificate",
+ "70": "invalid_encryption_key",
+ "71": "cannot_encrypt_data",
+ "72": "cannot_decrypt_data",
+ "73": "invalid_jwt_algorithm",
+ "74": "invalid_jwt_secret",
+ "75": "jwt_missing",
+ "76": "cannot_generate_jwt",
+ "77": "access_token_missing",
+ "78": "invalid_access_token",
+ "79": "cannot_fetch_jwks",
+ "80": "invalid_size_bytes",
+ "81": "invalid_utf8",
+ "82": "invalid_number_encoding",
+ "83": "invalid_boolean_value",
+ "84": "invalid_number_value",
+ "100": "client_not_found",
+ "101": "invalid_client_id",
+ "206": "connection_closed",
+ "209": "cannot_parse_header_kind",
+ "300": "http_response_error",
+ "301": "invalid_http_request",
+ "302": "invalid_json_response",
+ "303": "invalid_bytes_response",
+ "304": "empty_response",
+ "305": "cannot_create_endpoint",
+ "306": "cannot_parse_url",
+ "400": "web_socket_error",
+ "401": "web_socket_connection_error",
+ "402": "web_socket_close_error",
+ "403": "web_socket_receive_error",
+ "404": "web_socket_send_error",
+ "1000": "cannot_create_streams_directory",
+ "1001": "cannot_create_stream_directory",
+ "1002": "cannot_create_stream_info",
+ "1003": "cannot_update_stream_info",
+ "1004": "cannot_open_stream_info",
+ "1005": "cannot_read_stream_info",
+ "1006": "cannot_create_stream",
+ "1007": "cannot_delete_stream",
+ "1008": "cannot_delete_stream_directory",
+ "1009": "stream_id_not_found",
+ "1010": "stream_name_not_found",
+ "1011": "stream_directory_not_found",
+ "1012": "stream_name_already_exists",
+ "1013": "invalid_stream_name",
+ "1014": "invalid_stream_id",
+ "1015": "cannot_read_streams",
+ "1019": "invalid_topic_size",
+ "1020": "too_many_streams",
+ "2000": "cannot_create_topics_directory",
+ "2001": "cannot_create_topic_directory",
+ "2002": "cannot_create_topic_info",
+ "2003": "cannot_update_topic_info",
+ "2004": "cannot_open_topic_info",
+ "2005": "cannot_read_topic_info",
+ "2006": "cannot_create_topic",
+ "2007": "cannot_delete_topic",
+ "2008": "cannot_delete_topic_directory",
+ "2009": "cannot_poll_topic",
+ "2010": "topic_id_not_found",
+ "2011": "topic_name_not_found",
+ "2013": "topic_name_already_exists",
+ "2014": "invalid_topic_name",
+ "2015": "too_many_partitions",
+ "2016": "invalid_topic_id",
+ "2017": "cannot_read_topics",
+ "2018": "invalid_replication_factor",
+ "2019": "invalid_partitions_count",
+ "2020": "topic_directory_not_found",
+ "2021": "too_many_topics",
+ "3000": "cannot_create_partition",
+ "3001": "cannot_create_partitions_directory",
+ "3002": "cannot_create_partition_directory",
+ "3003": "cannot_open_partition_log_file",
+ "3004": "cannot_read_partitions",
+ "3005": "cannot_delete_partition",
+ "3006": "cannot_delete_partition_directory",
+ "3007": "partition_not_found",
+ "3008": "no_partitions",
+ "3009": "topic_full",
+ "3010": "cannot_delete_consumer_offsets_directory",
+ "3011": "cannot_delete_consumer_offset_file",
+ "3012": "cannot_create_consumer_offsets_directory",
+ "3013": "partition_id_space_exhausted",
+ "3020": "cannot_read_consumer_offsets",
+ "3021": "consumer_offset_not_found",
+ "3022": "not_resolved_consumer",
+ "3023": "cannot_open_consumer_offsets_file",
+ "3024": "too_many_consumer_offsets",
+ "4000": "segment_not_found",
+ "4001": "segment_closed",
+ "4002": "invalid_segment_size",
+ "4003": "cannot_create_segment_log_file",
+ "4004": "cannot_create_segment_index_file",
+ "4005": "cannot_create_segment_time_index_file",
+ "4006": "cannot_save_messages_to_segment",
+ "4007": "cannot_save_index_to_segment",
+ "4008": "cannot_save_time_index_to_segment",
+ "4009": "invalid_messages_count",
+ "4010": "cannot_append_message",
+ "4011": "cannot_read_message",
+ "4012": "cannot_read_message_id",
+ "4013": "cannot_read_message_state",
+ "4014": "cannot_read_message_timestamp",
+ "4015": "cannot_read_headers_length",
+ "4016": "cannot_read_headers_payload",
+ "4017": "too_big_user_headers",
+ "4018": "invalid_header_key",
+ "4019": "invalid_header_value",
+ "4020": "cannot_read_message_length",
+ "4021": "cannot_read_message_payload",
+ "4022": "too_big_message_payload",
+ "4023": "too_many_messages",
+ "4024": "empty_message_payload",
+ "4025": "invalid_message_payload_length",
+ "4026": "cannot_read_message_checksum",
+ "4027": "invalid_message_checksum",
+ "4028": "invalid_key_value_length",
+ "4029": "command_length_error",
+ "4030": "invalid_segments_count",
+ "4031": "non_zero_offset",
+ "4032": "non_zero_timestamp",
+ "4033": "missing_index",
+ "4034": "invalid_indexes_byte_size",
+ "4035": "invalid_indexes_count",
+ "4036": "invalid_messages_size",
+ "4037": "too_small_message",
+ "4038": "invalid_message_timestamp_delta",
+ "4039": "invalid_batch_checksum",
+ "4040": "invalid_header_kind",
+ "4041": "unsupported_option_key",
+ "4042": "invalid_option_value",
+ "4043": "options_block_too_large",
+ "4050": "cannot_send_messages_due_to_client_disconnection",
+ "4051": "background_send_error",
+ "4052": "background_send_timeout",
+ "4053": "background_send_buffer_full",
+ "4054": "background_worker_disconnected",
+ "4055": "background_send_buffer_overflow",
+ "4056": "producer_send_failed",
+ "4057": "producer_closed",
+ "4100": "invalid_offset",
+ "4101": "invalid_reserved_field",
+ "4102": "segment_size_mismatch_at_open",
+ "5000": "consumer_group_id_not_found",
+ "5002": "invalid_consumer_group_id",
+ "5003": "consumer_group_name_not_found",
+ "5004": "consumer_group_name_already_exists",
+ "5005": "invalid_consumer_group_name",
+ "5006": "consumer_group_member_not_found",
+ "5007": "cannot_create_consumer_group_info",
+ "5008": "cannot_delete_consumer_group_info",
+ "5009": "consumer_group_partition_not_owned",
+ "6000": "missing_base_offset_retained_message_batch",
+ "6001": "missing_last_offset_delta_retained_message_batch",
+ "6002": "missing_max_timestamp_retained_message_batch",
+ "6003": "missing_length_retained_message_batch",
+ "6004": "missing_payload_retained_message_batch",
+ "7000": "cannot_read_batch_base_offset",
+ "7001": "cannot_read_batch_length",
+ "7002": "cannot_read_last_offset_delta",
+ "7003": "cannot_read_max_timestamp",
+ "7004": "cannot_read_batch_payload",
+ "8000": "invalid_connection_string",
+ "9000": "snapshot_file_completion_failed",
+ "10000": "cannot_serialize_resource",
+ "10001": "cannot_deserialize_resource",
+ "10002": "cannot_read_file",
+ "10003": "cannot_read_file_metadata",
+ "10004": "cannot_seek_file",
+ "10005": "cannot_append_to_file",
+ "10006": "cannot_write_to_file",
+ "10007": "cannot_overwrite_file",
+ "10008": "cannot_delete_file",
+ "10009": "cannot_sync_file",
+ "10010": "cannot_read_index_offset",
+ "10011": "cannot_read_index_position",
+ "10012": "cannot_read_index_timestamp",
+ "10013": "timestamp_out_of_range",
+ "11000": "shard_not_found",
+ "11001": "shard_communication_error",
+ "12000": "cannot_bind_to_socket",
+ "12001": "task_timeout",
+ "13000": "io_error",
+ "14000": "already_authenticated",
+ "14001": "invalid_session",
+ "14003": "incompatible_protocol_version"
+ },
+ "vectors": {
+ "consumer.numeric.42": "0101042a000000",
+ "consumer_group.named.my-group": "0202086d792d67726f7570",
+ "identifier.named.my-stream": "02096d792d73747265616d",
+ "identifier.numeric.1": "010401000000",
+ "options.golden":
"020a0000006475726162696c6974790209000000706572736973746564020c0000007365676d656e745f73697a650c080000000000004000000000",
+ "options.topic_create.full":
"0215000000636f6d7072657373696f6e5f616c676f726974686d0204000000677a6970021a000000636f6e73756d65725f6f66667365745f6475726162696c697479020a0000007265706c696361746564020a0000006475726162696c6974790209000000706572736973746564020e0000006d61785f746f7069635f73697a650c080000000000004000000000020e0000006d6573736167655f6578706972790c0800000000a0e3d08c00000002190000006d657373616765735f72657175697265645f746f5f736176650b04000000070000000214000000707265616c6c6f63617465
[...]
+ "options.topic_create.raw":
"021a000000636f6e73756d65725f6f66667365745f6475726162696c697479020a0000007265706c696361746564020a0000006475726162696c6974790209000000706572736973746564",
+ "options.topic_update.full":
"0215000000636f6d7072657373696f6e5f616c676f726974686d02040000006e6f6e65020e0000006d61785f746f7069635f73697a650c08000000ffffffffffffffff020e0000006d6573736167655f6578706972790c08000000ffffffffffffffff",
+ "partitioning.balanced": "0100",
+ "partitioning.messages_key.user-123": "0308757365722d313233",
+ "partitioning.partition_id.7": "020407000000",
+ "payload.create_stream": "0673747265616d",
+ "permissions.sample":
"010100010001000101000101000000010100010100010a000000010100010114000000000101010001020000000001010000010000",
+ "polling.first": "030000000000000000",
+ "polling.last": "040000000000000000",
+ "polling.next": "050000000000000000",
+ "polling.offset.100": "016400000000000000",
+ "polling.timestamp.1700000000000": "020068e5cf8b010000",
+ "request.change_password":
"010401000000086f6c642d706173730c6e65772d706173732d313233",
+ "request.create_consumer_group": "01040100000001040200000003677270",
+ "request.create_partitions": "01040100000001040200000005000000",
+ "request.create_personal_access_token":
"086d792d746f6b656e100e000000000000",
+ "request.create_stream": "0b746573742d73747265616d",
+ "request.create_stream.options":
"0b746573742d73747265616d020a0000006475726162696c6974790209000000706572736973746564020c0000007365676d656e745f73697a650c080000000000004000000000",
+ "request.create_topic":
"01040100000003000000066f7264657273020a0000006475726162696c6974790209000000706572736973746564020c0000007365676d656e745f73697a650c080000000000004000000000",
+ "request.create_user":
"0561646d696e087040737377307264010135000000010100010001000101000101000000010100010100010a000000010100010114000000000101010001020000000001010000010000",
+ "request.create_user.no_permissions": "0475736572097365637265743132330200",
+ "request.delete_consumer_group": "010401000000010402000000010403000000",
+ "request.delete_consumer_offset":
"0101040100000001040a000000010414000000010500000001",
+ "request.delete_partitions": "020673747265616d0205746f70696302000000",
+ "request.delete_personal_access_token": "086d792d746f6b656e",
+ "request.delete_segments": "010401000000010402000000030000000a000000",
+ "request.delete_stream": "010405000000",
+ "request.delete_topic": "010401000000010405000000",
+ "request.delete_user": "02086f6c642d75736572",
+ "request.describe_options": "01",
+ "request.get_client": "2a000000",
+ "request.get_clients": "",
+ "request.get_cluster_metadata": "",
+ "request.get_consumer_group":
"020873747265616d2d310207746f7069632d31020767726f75702d31",
+ "request.get_consumer_groups": "010401000000010402000000",
+ "request.get_consumer_offset":
"01020b6d792d636f6e73756d6572020873747265616d2d310207746f7069632d310100000000",
+ "request.get_me": "",
+ "request.get_personal_access_tokens": "",
+ "request.get_snapshot": "02020105",
+ "request.get_stats": "",
+ "request.get_stream": "02096d792d73747265616d",
+ "request.get_streams": "",
+ "request.get_topic": "02096d792d73747265616d02086d792d746f706963",
+ "request.get_topics": "01042a000000",
+ "request.get_user": "01042a000000",
+ "request.get_users": "",
+ "request.join_consumer_group": "010401000000010402000000010403000000",
+ "request.leave_consumer_group": "010401000000010402000000020167",
+ "request.login_register":
"002c00000973776966742d73646b05302e312e300469676779046967677900000000",
+ "request.login_register_with_pat":
"002c00000973776966742d73646b05302e312e30107061742d61626331323364656634353600000000",
+ "request.logout_user": "",
+ "request.ping": "",
+ "request.poll_messages":
"0101040100000001040a00000001041400000001050000000164000000000000003200000001",
+ "request.poll_messages.group":
"0201040300000001040100000001040100000000000000000300000000000000000a00000000",
+ "request.purge_stream": "010401000000",
+ "request.purge_topic": "010401000000010403000000",
+ "request.send_messages":
"1600000001040100000001040200000002040000000003000000000000000000000000000000000000000000000000000000e803000000000000c501000000000000dcffef596f23a1cb03000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
[...]
+ "request.store_consumer_offset":
"0101040100000001040a0000000104140000000105000000393000000000000001",
+ "request.store_consumer_offset.no_partition":
"020104030000000104010000000104010000000000000000ffffffffffffffff00",
+ "request.sync_consumer_group":
"020873747265616d2d310207746f7069632d31020767726f75702d31",
+ "request.update_permissions":
"0104010000000135000000010100010001000101000101000000010100010100010a000000010100010114000000000101010001020000000001010000010000",
+ "request.update_permissions.none": "020561646d696e00",
+ "request.update_stream": "02086f6c642d6e616d65086e65772d6e616d65",
+ "request.update_topic":
"0104010000000104020000000d757064617465642d746f706963020e0000006d6573736167655f6578706972790c0800000000a0e3d08c000000",
+ "request.update_user": "01040100000001086e65772d6e616d650102",
+ "request.update_user.none": "0104050000000000",
+ "response.client":
"010000000a000000010e0000003132372e302e302e313a3830383002000000",
+ "response.client_details":
"010000000a000000010e0000003132372e302e302e313a3830383002000000010000000200000003000000040000000500000006000000",
+ "response.client_details.no_user":
"02000000ffffffff010d00000031302e302e302e313a3630303000000000",
+ "response.cluster_metadata":
"0c00000070726f642d636c757374657202000000060000006e6f64652d310800000031302e302e302e319a1f901fb80b9c1f0000060000006e6f64652d3207000000666430303a3a329b1f0000b90b00000103",
+ "response.consumer_group": "010000000400000002000000086d792d67726f7570",
+ "response.consumer_group_details":
"010000000600000002000000086d792d67726f757001000000030000000000000001000000020000000200000003000000030000000400000005000000",
+ "response.consumer_offset": "01000000e803000000000000f401000000000000",
+ "response.describe_options":
"020000000c7365676d656e745f73697a650c0800000000000040000000000c0000005365676d656e742073697a650a6475726162696c697479020a0000007265706c69636174656400000000",
+ "response.get_clients":
"010000000a000000010e0000003132372e302e302e313a38303830020000000200000014000000020d00000031302e302e302e313a3630303003000000",
+ "response.get_consumer_groups":
"0100000004000000020000000767726f75702d610200000004000000000000000767726f75702d62",
+ "response.get_personal_access_tokens":
"07746f6b656e2d6100e090663c13060007746f6b656e2d620000000000000000",
+ "response.get_stream":
"0100000000e090663c130600020000000008000000000000c800000000000000096d792d73747265616d000000000100000000e090663c1306000300000000a0e3d08c0000000100000040000000000010000000000000640000000000000007746f7069632d613b000000020a0000006475726162696c6974790209000000706572736973746564020c0000007365676d656e745f73697a650c08000000000000400000000020000000020e0000006d61785f746f7069635f73697a650c08000000ffffffffffffffff0200000000e090663c1306000300000000a0e3d08c000000010000004000
[...]
+ "response.get_streams":
"0100000000e090663c130600020000000008000000000000c800000000000000096d792d73747265616d000000000200000000e090663c130600000000000008000000000000c800000000000000096d792d73747265616d00000000",
+ "response.get_topic":
"0100000000e090663c1306000300000000a0e3d08c00000001000000400000000000100000000000006400000000000000086d792d746f7069633b000000020a0000006475726162696c6974790209000000706572736973746564020c0000007365676d656e745f73697a650c08000000000000400000000020000000020e0000006d61785f746f7069635f73697a650c08000000ffffffffffffffff0000000000e090663c130600020000006300000000000000000400000000000064000000000000000100000001e090663c13060001000000000000000000000000000000000000000000000
[...]
+ "response.get_topics":
"020000000100000000e090663c1306000300000000a0e3d08c00000001000000400000000000100000000000006400000000000000066576656e74733b000000020a0000006475726162696c6974790209000000706572736973746564020c0000007365676d656e745f73697a650c08000000000000400000000020000000020e0000006d61785f746f7069635f73697a650c08000000ffffffffffffffff0200000000e090663c1306000300000000a0e3d08c00000001000000400000000000100000000000006400000000000000046c6f67733b000000020a0000006475726162696c697479
[...]
+ "response.get_topics.empty": "00000000",
+ "response.get_users":
"0100000000e090663c130600010561646d696e0000000002000000c8000000000000000205616c69636500000000",
+ "response.identity": "2a000000",
+ "response.login_register":
"2a0000006400000000000000002c000006302e31312e30",
+ "response.poll_messages":
"07000000660000000000000003000000070000000000000064000000000000008813000000000000e8030000000000006e01000000000000f576a2492e1328b3020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
[...]
+ "response.poll_messages.empty": "00000000000000000000000000000000",
+ "response.raw_personal_access_token":
"167261772d7365637265742d746f6b656e2d76616c7565",
+ "response.send_messages":
"0200000001000000020000000300000004000000000000000100000002000000000000002a00000000000000",
+ "response.send_messages.empty": "00000000",
+ "response.stats":
"d20400000000cc4100004842000000400000000000000000020000000000000001000000100e00000000000000e090663c13060040420f000000000020a107000000000080841e0000000000030000000a0000001e0000005a00000050c30000000000000500000002000000060000006e6f64652d31050000004c696e757803000000362e3105000000362e312e3006000000302e31312e30002c000001000000010000000100000000000000e80300000000000032000000000000003dcf733f1000000000000000190000000060253c77000000",
+ "response.stream":
"0100000000e090663c130600020000000008000000000000c800000000000000096d792d73747265616d00000000",
+ "response.sync_consumer_group":
"070000000000000003000000000000000200000004000000",
+ "response.topic_header":
"0500000000e090663c1306000300000000a0e3d08c00000001000000400000000000100000000000006400000000000000066576656e74733b000000020a0000006475726162696c6974790209000000706572736973746564020c0000007365676d656e745f73697a650c08000000000000400000000020000000020e0000006d61785f746f7069635f73697a650c08000000ffffffffffffffff",
+ "response.user": "0100000000e090663c130600010561646d696e00000000",
+ "response.user_details":
"0100000000e090663c130600010561646d696e000000000135000000010100010001000101000101000000010100010100010a000000010100010114000000000101010001020000000001010000010000",
+ "response.user_details.no_permissions":
"0100000000e090663c130600010561646d696e0000000000000000",
+ "user_headers.sample":
"0207000000617474656d70740b0400000003000000020800000074726163652d69640203000000616263",
+ "version_info": "002c00000973776966742d73646b05302e312e30",
+ "vsr.eviction.incompatible_protocol":
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001032547698badcfeefcdab8967452301002c0000002c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
[...]
+ "vsr.eviction.stale_client":
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001032547698badcfeefcdab896745230100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
[...]
+ "vsr.reply.denied":
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000020000000000000029000000000000000000000000000000000
[...]
+ "vsr.reply.ok":
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0100000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001032547698badcfeefcdab896745230100000000000000000000000000000000000000000000000007000000000000008000000000000000000000000000000000000000000000000000000
[...]
+ "vsr.request.create_stream":
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007010000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001032547698badcfeefcdab89674523013a9e6b43adf27bf20000000000000000000000000000000001000000000000008000000000000000630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
[...]
+ "vsr.request.logout":
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001032547698badcfeefcdab8967452301c294d3380580062d00000000000000000000000000000000040000000000000003000000000000006300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
[...]
+ "vsr.request.ping":
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001032547698badcfeefcdab896745230100000000000000000000000000000000000000000000000001000000000000000200000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000
[...]
+ "vsr.request.ping.bound":
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001032547698badcfeefcdab896745230100000000000000000000000000000000000000000000000003000000000000000200000000000000630000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000
[...]
+ "vsr.request.register":
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003010000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001032547698badcfeefcdab89674523013b73ae32769bceeb000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
[...]
+ "vsr.request.send_messages":
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004010000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001032547698badcfeefcdab89674523010000000000000000000000000000000000000000000000000200000000000000a000000000000000630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
[...]
+ "vsr.request.unknown_code":
"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000005000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001032547698badcfeefcdab89674523010000000000000000000000000000000000000000000000000500000000000000020000000000000063000000000000000000000060ea000000000000000000000000000000000000000000000000000000000000000
[...]
+ }
+}
diff --git a/foreign/swift/Tests/IggyTests/HashTests.swift
b/foreign/swift/Tests/IggyTests/HashTests.swift
new file mode 100644
index 000000000..6fc254026
--- /dev/null
+++ b/foreign/swift/Tests/IggyTests/HashTests.swift
@@ -0,0 +1,62 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import Testing
+
+@testable import Iggy
+
+@Suite("XXH3-64")
+struct XXH3Tests {
+ @Test("matches the reference implementation for every golden length")
+ func matchesGoldenVectors() {
+ let fixture = GoldenFixture.shared
+ #expect(fixture.xxh3_64.count > 300)
+ for vector in fixture.xxh3_64 {
+ let input = GoldenFixture.pattern(vector.len)
+ #expect(UInt64(vector.hash, radix: 16) == XXH3.hash64(input),
"length \(vector.len)")
+ }
+ }
+
+ @Test("hashes slices by their own bounds")
+ func hashesSlices() {
+ let input = GoldenFixture.pattern(600)
+ let slice = input[100..<400]
+ #expect(XXH3.hash64(slice) == XXH3.hash64(Array(slice)))
+ }
+
+ @Test("empty input has the well-known value")
+ func emptyInput() {
+ #expect(XXH3.hash64([]) == 0x2D06_8005_38D3_94C2)
+ }
+}
+
+@Suite("XXH32")
+struct XXH32Tests {
+ @Test("matches the reference implementation for every golden length")
+ func matchesGoldenVectors() {
+ let fixture = GoldenFixture.shared
+ #expect(fixture.xxh32.count > 300)
+ for vector in fixture.xxh32 {
+ #expect(UInt32(vector.hash, radix: 16) ==
XXH32.hash(GoldenFixture.pattern(vector.len)), "length \(vector.len)")
+ }
+ }
+
+ @Test("empty input has the well-known value")
+ func emptyInput() {
+ #expect(XXH32.hash([]) == 0x02CC_5D05)
+ }
+}
diff --git a/foreign/swift/Tests/IggyTests/Support/GoldenFixture.swift
b/foreign/swift/Tests/IggyTests/Support/GoldenFixture.swift
new file mode 100644
index 000000000..a68de70eb
--- /dev/null
+++ b/foreign/swift/Tests/IggyTests/Support/GoldenFixture.swift
@@ -0,0 +1,73 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import Foundation
+import Testing
+
+/// Byte-exact fixtures produced by `Tools/golden-vectors` from the Rust
+/// protocol crates. Regenerate with:
+///
+/// ```text
+/// cargo run --manifest-path Tools/golden-vectors/Cargo.toml --
Tests/IggyTests/Fixtures/golden.json
+/// ```
+struct GoldenFixture: Decodable {
+ struct HashVector {
+ let len: Int
+ let hash: String
+ }
+
+ struct ErrorEntry {
+ let code: UInt32
+ let name: String
+ }
+
+ private let xxh3_64Table: [String: String]
+ private let xxh32Table: [String: String]
+ private let errorTable: [String: String]
+
+ /// The XXH3-64 vectors, by input length.
+ var xxh3_64: [HashVector] { Self.hashVectors(xxh3_64Table) }
+
+ /// The XXH32 vectors, by input length.
+ var xxh32: [HashVector] { Self.hashVectors(xxh32Table) }
+
+ /// Every error code the server defines, with its snake_case name.
+ var errors: [ErrorEntry] {
+ errorTable.map { ErrorEntry(code: UInt32($0.key)!, name: $0.value)
}.sorted { $0.code < $1.code }
+ }
+
+ private static func hashVectors(_ table: [String: String]) -> [HashVector]
{
+ table.map { HashVector(len: Int($0.key)!, hash: $0.value) }.sorted {
$0.len < $1.len }
+ }
+
+ enum CodingKeys: String, CodingKey {
+ case xxh3_64Table = "xxh3_64"
+ case xxh32Table = "xxh32"
+ case errorTable = "errors"
+ }
+
+ static let shared: GoldenFixture = {
+ let url = Bundle.module.url(forResource: "golden", withExtension:
"json", subdirectory: "Fixtures")!
+ let data = try! Data(contentsOf: url)
+ return try! JSONDecoder().decode(GoldenFixture.self, from: data)
+ }()
+
+ /// The deterministic input the hash vectors were computed over.
+ static func pattern(_ length: Int) -> [UInt8] {
+ (0..<length).map { UInt8(truncatingIfNeeded: $0) &* 31 &+ 7 }
+ }
+}
diff --git a/foreign/swift/Tools/golden-vectors/Cargo.toml
b/foreign/swift/Tools/golden-vectors/Cargo.toml
new file mode 100644
index 000000000..a1a685e11
--- /dev/null
+++ b/foreign/swift/Tools/golden-vectors/Cargo.toml
@@ -0,0 +1,36 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+[package]
+name = "iggy-swift-golden-vectors"
+version = "0.1.0"
+edition = "2024"
+publish = false
+description = "Dumps byte-exact wire fixtures from the Rust protocol crates
for the Swift SDK tests"
+
+# Standalone: the root workspace must not pick this tool up.
+[workspace]
+
+[dependencies]
+bytemuck = "1"
+bytes = "1"
+iggy_binary_protocol = { path = "../../../../core/binary_protocol" }
+iggy_common = { path = "../../../../core/common" }
+secrecy = "0.10"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+twox-hash = { version = "2", features = ["xxhash32", "xxhash3_64"] }
diff --git a/foreign/swift/Tools/golden-vectors/src/main.rs
b/foreign/swift/Tools/golden-vectors/src/main.rs
new file mode 100644
index 000000000..b599ae9d4
--- /dev/null
+++ b/foreign/swift/Tools/golden-vectors/src/main.rs
@@ -0,0 +1,1435 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Dumps byte-exact wire fixtures from the Rust protocol crates.
+//!
+//! The Swift SDK re-implements the binary protocol from scratch, so every
+//! encoder and decoder it ships is checked against bytes produced by the
+//! reference implementation rather than against itself. Run from
+//! `foreign/swift`:
+//!
+//! ```text
+//! cargo run --manifest-path Tools/golden-vectors/Cargo.toml --
Tests/IggyTests/Fixtures/golden.json
+//! ```
+
+use bytes::{BufMut, Bytes, BytesMut};
+use iggy_binary_protocol::batch::{
+ BATCH_HEADER_SIZE, BATCH_MESSAGE_HEADER_SIZE, BatchHeader,
calculate_batch_checksum,
+};
+use iggy_binary_protocol::codec::WireEncode;
+use iggy_binary_protocol::codes::*;
+use iggy_binary_protocol::consensus::{
+ Command, EvictionHeader, EvictionReason, HEADER_SIZE, Operation,
ReplyHeader, RequestHeader,
+};
+use iggy_binary_protocol::primitives::options::WireOptions;
+use iggy_binary_protocol::primitives::permissions::{
+ WireGlobalPermissions, WirePermissions, WireStreamPermissions,
WireTopicPermissions,
+};
+use iggy_binary_protocol::primitives::user_headers::encode_user_headers;
+use iggy_binary_protocol::requests::consumer_groups::*;
+use iggy_binary_protocol::requests::consumer_offsets::*;
+use iggy_binary_protocol::requests::messages::{
+ PollMessagesRequest, RawMessage, SendMessagesEncoder,
+};
+use iggy_binary_protocol::requests::partitions::{
+ CreatePartitionsRequest, DeletePartitionsRequest,
+};
+use iggy_binary_protocol::requests::personal_access_tokens::*;
+use iggy_binary_protocol::requests::segments::DeleteSegmentsRequest;
+use iggy_binary_protocol::requests::streams::*;
+use iggy_binary_protocol::requests::system::*;
+use iggy_binary_protocol::requests::topics::{
+ CreateTopicRequest, DeleteTopicRequest, GetTopicRequest, GetTopicsRequest,
PurgeTopicRequest,
+ UpdateTopicRequest,
+};
+use iggy_binary_protocol::requests::users::*;
+use iggy_binary_protocol::responses::clients::{
+ ClientDetailsResponse, ClientResponse, ConsumerGroupInfoResponse,
GetClientsResponse,
+};
+use iggy_binary_protocol::responses::consumer_groups::{
+ ConsumerGroupDetailsResponse, ConsumerGroupMemberResponse,
ConsumerGroupResponse,
+ GetConsumerGroupsResponse, SyncConsumerGroupResponse,
+};
+use iggy_binary_protocol::responses::consumer_offsets::ConsumerOffsetResponse;
+use iggy_binary_protocol::responses::messages::{
+ PollMessagesResponseHeader, SendMessagesConfirmationResponse,
SendMessagesResponse,
+};
+use iggy_binary_protocol::responses::personal_access_tokens::{
+ GetPersonalAccessTokensResponse, PersonalAccessTokenResponse,
RawPersonalAccessTokenResponse,
+};
+use iggy_binary_protocol::responses::streams::{
+ GetStreamResponse, GetStreamsResponse, StreamResponse, TopicHeader,
+};
+use iggy_binary_protocol::responses::system::get_stats::{CacheMetricEntry,
StatsResponse};
+use iggy_binary_protocol::responses::system::{
+ ClusterMetadataResponse, ClusterNodeResponse, DescribeOptionsResponse,
OptionDescriptor,
+};
+use iggy_binary_protocol::responses::topics::{
+ GetTopicResponse, GetTopicsResponse, PartitionResponse,
+};
+use iggy_binary_protocol::responses::users::{
+ GetUsersResponse, IdentityResponse, LoginRegisterResponse,
UserDetailsResponse, UserResponse,
+};
+use iggy_binary_protocol::{
+ AckLevel, ClientVersionInfo, IGGY_PROTOCOL_VERSION,
IGGY_PROTOCOL_VERSION_MIN, WireConsumer,
+ WireIdentifier, WireName, WirePartitioning, WirePollingStrategy,
+};
+use iggy_common::{
+ CompressionAlgorithm, Durability, IggyByteSize, IggyDuration, IggyError,
IggyExpiry,
+ MaxTopicSize, TopicCreateOptions, TopicUpdateOptions,
+};
+use secrecy::SecretString;
+use serde::Serialize;
+use std::collections::BTreeMap;
+use twox_hash::{XxHash3_64, XxHash32};
+
+/// Hash and error tables are maps keyed by length and code, so each entry is
+/// one line of the pretty-printed fixture and the file stays reviewable.
+#[derive(Serialize)]
+struct Golden {
+ protocol_version: u32,
+ protocol_version_min: u32,
+ xxh3_64: BTreeMap<usize, String>,
+ xxh32: BTreeMap<usize, String>,
+ errors: BTreeMap<u32, String>,
+ vectors: BTreeMap<String, String>,
+}
+
+fn pattern(len: usize) -> Vec<u8> {
+ (0..len)
+ .map(|i| (i as u8).wrapping_mul(31).wrapping_add(7))
+ .collect()
+}
+
+fn hex(bytes: &[u8]) -> String {
+ bytes.iter().map(|b| format!("{b:02x}")).collect()
+}
+
+fn numeric(id: u32) -> WireIdentifier {
+ WireIdentifier::numeric(id)
+}
+
+fn named(name: &str) -> WireIdentifier {
+ WireIdentifier::named(name).expect("valid name")
+}
+
+fn name(value: &str) -> WireName {
+ WireName::new(value).expect("valid name")
+}
+
+fn options(entries: &[(u8, &[u8], u8, &[u8])]) -> WireOptions {
+ let mut buf = BytesMut::new();
+ encode_user_headers(entries, &mut buf);
+ WireOptions::from_bytes(buf.freeze()).expect("valid options")
+}
+
+fn sample_options() -> WireOptions {
+ options(&[
+ (2, b"durability", 2, b"persisted"),
+ (2, b"segment_size", 12, &1_073_741_824u64.to_le_bytes()),
+ ])
+}
+
+fn version_info() -> ClientVersionInfo {
+ ClientVersionInfo {
+ protocol_version: IGGY_PROTOCOL_VERSION,
+ sdk_name: name("swift-sdk"),
+ sdk_version: name("0.1.0"),
+ }
+}
+
+fn sample_permissions() -> WirePermissions {
+ WirePermissions {
+ global: WireGlobalPermissions {
+ manage_servers: true,
+ read_servers: true,
+ manage_users: false,
+ read_users: true,
+ manage_streams: false,
+ read_streams: true,
+ manage_topics: false,
+ read_topics: true,
+ poll_messages: true,
+ send_messages: false,
+ },
+ streams: vec![
+ WireStreamPermissions {
+ stream_id: 1,
+ manage_stream: true,
+ read_stream: true,
+ manage_topics: false,
+ read_topics: true,
+ poll_messages: true,
+ send_messages: false,
+ topics: vec![
+ WireTopicPermissions {
+ topic_id: 10,
+ manage_topic: true,
+ read_topic: true,
+ poll_messages: false,
+ send_messages: true,
+ },
+ WireTopicPermissions {
+ topic_id: 20,
+ manage_topic: false,
+ read_topic: true,
+ poll_messages: true,
+ send_messages: true,
+ },
+ ],
+ },
+ WireStreamPermissions {
+ stream_id: 2,
+ manage_stream: false,
+ read_stream: true,
+ manage_topics: true,
+ read_topics: false,
+ poll_messages: false,
+ send_messages: true,
+ topics: vec![],
+ },
+ ],
+ }
+}
+
+fn frame(
+ id: u128,
+ offset_delta: u32,
+ timestamp_delta: u32,
+ payload: &[u8],
+ user_headers: &[u8],
+) -> Vec<u8> {
+ let mut bytes = vec![0u8; BATCH_MESSAGE_HEADER_SIZE];
+ bytes[8..24].copy_from_slice(&id.to_le_bytes());
+ bytes[24..28].copy_from_slice(&offset_delta.to_le_bytes());
+ bytes[28..32].copy_from_slice(×tamp_delta.to_le_bytes());
+ bytes[32..36].copy_from_slice(&(user_headers.len() as u32).to_le_bytes());
+ bytes[36..40].copy_from_slice(&(payload.len() as u32).to_le_bytes());
+ bytes.extend_from_slice(payload);
+ bytes.extend_from_slice(user_headers);
+ let checksum = XxHash3_64::oneshot(&bytes[8..]);
+ bytes[0..8].copy_from_slice(&checksum.to_le_bytes());
+ bytes
+}
+
+fn batch_record(
+ partition_id: u64,
+ base_offset: u64,
+ base_timestamp: u64,
+ origin_timestamp: u64,
+ frames: &[Vec<u8>],
+) -> Vec<u8> {
+ let blob: Vec<u8> = frames.concat();
+ let mut header = BatchHeader::new(
+ partition_id,
+ origin_timestamp,
+ (BATCH_HEADER_SIZE + blob.len()) as u64,
+ frames.len() as u32,
+ );
+ header.base_offset = base_offset;
+ header.base_timestamp = base_timestamp;
+ header.batch_checksum = calculate_batch_checksum(&header, &blob);
+ let mut bytes = vec![0u8; BATCH_HEADER_SIZE];
+ header.encode_into(&mut bytes);
+ bytes.extend_from_slice(&blob);
+ bytes
+}
+
+fn topic_header(id: u32, name_value: &str) -> TopicHeader {
+ TopicHeader {
+ id,
+ created_at: 1_710_000_000_000_000,
+ partitions_count: 3,
+ message_expiry: 604_800_000_000,
+ compression_algorithm: 1,
+ max_topic_size: 1_073_741_824,
+ size_bytes: 4096,
+ messages_count: 100,
+ name: name(name_value),
+ options: sample_options(),
+ derived_options: options(&[(2, b"max_topic_size", 12,
&u64::MAX.to_le_bytes())]),
+ }
+}
+
+fn stream_response(id: u32, topics_count: u32) -> StreamResponse {
+ StreamResponse {
+ id,
+ created_at: 1_710_000_000_000_000,
+ topics_count,
+ size_bytes: 2048,
+ messages_count: 200,
+ name: name("my-stream"),
+ options: WireOptions::empty(),
+ }
+}
+
+fn add<T: WireEncode>(vectors: &mut BTreeMap<String, String>, key: &str,
value: &T) {
+ vectors.insert(key.to_owned(), hex(&value.to_bytes()));
+}
+
+/// The header a client sends, built by the same rules the Rust SDK uses.
+fn request_header(
+ code: u32,
+ client: u128,
+ request: u64,
+ session: u64,
+ payload: &[u8],
+) -> RequestHeader {
+ RequestHeader::for_request(code, client, request, session, payload)
+ .expect("fixture payloads fit a request frame")
+}
+
+fn main() {
+ let output = std::env::args().nth(1).expect("output path argument");
+ let mut vectors = BTreeMap::new();
+
+ let mut lengths: Vec<usize> = (0..=300).collect();
+ lengths.extend([
+ 511, 512, 513, 1000, 1023, 1024, 1025, 1279, 1280, 1281, 2047, 2048,
2049, 4096, 10_000,
+ 65_536, 100_003,
+ ]);
+ let xxh3_64 = lengths
+ .iter()
+ .map(|&len| (len, format!("{:016x}",
XxHash3_64::oneshot(&pattern(len)))))
+ .collect();
+ let xxh32 = lengths
+ .iter()
+ .map(|&len| (len, format!("{:08x}", XxHash32::oneshot(0,
&pattern(len)))))
+ .collect();
+
+ let errors = (1u32..=20_000)
+ .filter_map(|code| {
+ let error = IggyError::from_code(code);
+ (error.as_code() == code).then(|| (code,
error.as_string().to_owned()))
+ })
+ .collect();
+
+ // Primitives.
+ add(&mut vectors, "identifier.numeric.1", &numeric(1));
+ add(
+ &mut vectors,
+ "identifier.named.my-stream",
+ &named("my-stream"),
+ );
+ add(
+ &mut vectors,
+ "consumer.numeric.42",
+ &WireConsumer::consumer(numeric(42)),
+ );
+ add(
+ &mut vectors,
+ "consumer_group.named.my-group",
+ &WireConsumer::consumer_group(named("my-group")),
+ );
+ add(
+ &mut vectors,
+ "partitioning.balanced",
+ &WirePartitioning::Balanced,
+ );
+ add(
+ &mut vectors,
+ "partitioning.partition_id.7",
+ &WirePartitioning::PartitionId(7),
+ );
+ add(
+ &mut vectors,
+ "partitioning.messages_key.user-123",
+ &WirePartitioning::MessagesKey(b"user-123".to_vec()),
+ );
+ add(
+ &mut vectors,
+ "polling.offset.100",
+ &WirePollingStrategy::offset(100),
+ );
+ add(
+ &mut vectors,
+ "polling.timestamp.1700000000000",
+ &WirePollingStrategy::timestamp(1_700_000_000_000),
+ );
+ add(&mut vectors, "polling.first", &WirePollingStrategy::first());
+ add(&mut vectors, "polling.last", &WirePollingStrategy::last());
+ add(&mut vectors, "polling.next", &WirePollingStrategy::next());
+ add(&mut vectors, "options.golden", &sample_options());
+ add(&mut vectors, "permissions.sample", &sample_permissions());
+ add(&mut vectors, "version_info", &version_info());
+
+ // Requests.
+ add(
+ &mut vectors,
+ "request.create_stream",
+ &CreateStreamRequest {
+ name: name("test-stream"),
+ options: WireOptions::empty(),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.create_stream.options",
+ &CreateStreamRequest {
+ name: name("test-stream"),
+ options: sample_options(),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.delete_stream",
+ &DeleteStreamRequest {
+ stream_id: numeric(5),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.get_stream",
+ &GetStreamRequest {
+ stream_id: named("my-stream"),
+ },
+ );
+ add(&mut vectors, "request.get_streams", &GetStreamsRequest);
+ add(
+ &mut vectors,
+ "request.purge_stream",
+ &PurgeStreamRequest {
+ stream_id: numeric(1),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.update_stream",
+ &UpdateStreamRequest {
+ stream_id: named("old-name"),
+ name: name("new-name"),
+ options: WireOptions::empty(),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.create_topic",
+ &CreateTopicRequest {
+ stream_id: numeric(1),
+ partitions_count: 3,
+ name: name("orders"),
+ options: sample_options(),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.delete_topic",
+ &DeleteTopicRequest {
+ stream_id: numeric(1),
+ topic_id: numeric(5),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.get_topic",
+ &GetTopicRequest {
+ stream_id: named("my-stream"),
+ topic_id: named("my-topic"),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.get_topics",
+ &GetTopicsRequest {
+ stream_id: numeric(42),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.purge_topic",
+ &PurgeTopicRequest {
+ stream_id: numeric(1),
+ topic_id: numeric(3),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.update_topic",
+ &UpdateTopicRequest {
+ stream_id: numeric(1),
+ topic_id: numeric(2),
+ name: name("updated-topic"),
+ options: options(&[(2, b"message_expiry", 12,
&604_800_000_000u64.to_le_bytes())]),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.create_partitions",
+ &CreatePartitionsRequest {
+ stream_id: numeric(1),
+ topic_id: numeric(2),
+ partitions_count: 5,
+ },
+ );
+ add(
+ &mut vectors,
+ "request.delete_partitions",
+ &DeletePartitionsRequest {
+ stream_id: named("stream"),
+ topic_id: named("topic"),
+ partitions_count: 2,
+ },
+ );
+ add(
+ &mut vectors,
+ "request.delete_segments",
+ &DeleteSegmentsRequest {
+ stream_id: numeric(1),
+ topic_id: numeric(2),
+ partition_id: 3,
+ segments_count: 10,
+ },
+ );
+ add(
+ &mut vectors,
+ "request.create_consumer_group",
+ &CreateConsumerGroupRequest {
+ stream_id: numeric(1),
+ topic_id: numeric(2),
+ name: name("grp"),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.delete_consumer_group",
+ &DeleteConsumerGroupRequest {
+ stream_id: numeric(1),
+ topic_id: numeric(2),
+ group_id: numeric(3),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.get_consumer_group",
+ &GetConsumerGroupRequest {
+ stream_id: named("stream-1"),
+ topic_id: named("topic-1"),
+ group_id: named("group-1"),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.get_consumer_groups",
+ &GetConsumerGroupsRequest {
+ stream_id: numeric(1),
+ topic_id: numeric(2),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.join_consumer_group",
+ &JoinConsumerGroupRequest {
+ stream_id: numeric(1),
+ topic_id: numeric(2),
+ group_id: numeric(3),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.leave_consumer_group",
+ &LeaveConsumerGroupRequest {
+ stream_id: numeric(1),
+ topic_id: numeric(2),
+ group_id: named("g"),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.sync_consumer_group",
+ &SyncConsumerGroupRequest {
+ stream_id: named("stream-1"),
+ topic_id: named("topic-1"),
+ group_id: named("group-1"),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.store_consumer_offset",
+ &StoreConsumerOffsetRequest {
+ consumer: WireConsumer::consumer(numeric(1)),
+ stream_id: numeric(10),
+ topic_id: numeric(20),
+ partition_id: Some(5),
+ offset: 12_345,
+ ack: AckLevel::Quorum,
+ },
+ );
+ add(
+ &mut vectors,
+ "request.store_consumer_offset.no_partition",
+ &StoreConsumerOffsetRequest {
+ consumer: WireConsumer::consumer_group(numeric(3)),
+ stream_id: numeric(1),
+ topic_id: numeric(1),
+ partition_id: None,
+ offset: u64::MAX,
+ ack: AckLevel::NoAck,
+ },
+ );
+ add(
+ &mut vectors,
+ "request.get_consumer_offset",
+ &GetConsumerOffsetRequest {
+ consumer: WireConsumer::consumer(named("my-consumer")),
+ stream_id: named("stream-1"),
+ topic_id: named("topic-1"),
+ partition_id: Some(0),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.delete_consumer_offset",
+ &DeleteConsumerOffsetRequest {
+ consumer: WireConsumer::consumer(numeric(1)),
+ stream_id: numeric(10),
+ topic_id: numeric(20),
+ partition_id: Some(5),
+ ack: AckLevel::Quorum,
+ },
+ );
+ add(
+ &mut vectors,
+ "request.poll_messages",
+ &PollMessagesRequest {
+ consumer: WireConsumer::consumer(numeric(1)),
+ stream_id: numeric(10),
+ topic_id: numeric(20),
+ partition_id: Some(5),
+ strategy: WirePollingStrategy::offset(100),
+ count: 50,
+ auto_commit: true,
+ },
+ );
+ add(
+ &mut vectors,
+ "request.poll_messages.group",
+ &PollMessagesRequest {
+ consumer: WireConsumer::consumer_group(numeric(3)),
+ stream_id: numeric(1),
+ topic_id: numeric(1),
+ partition_id: None,
+ strategy: WirePollingStrategy::first(),
+ count: 10,
+ auto_commit: false,
+ },
+ );
+ add(
+ &mut vectors,
+ "request.create_user",
+ &CreateUserRequest {
+ username: name("admin"),
+ password: "p@ssw0rd".to_owned(),
+ status: 1,
+ permissions: Some(sample_permissions()),
+ options: WireOptions::empty(),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.create_user.no_permissions",
+ &CreateUserRequest {
+ username: name("user"),
+ password: "secret123".to_owned(),
+ status: 2,
+ permissions: None,
+ options: WireOptions::empty(),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.delete_user",
+ &DeleteUserRequest {
+ user_id: named("old-user"),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.get_user",
+ &GetUserRequest {
+ user_id: numeric(42),
+ },
+ );
+ add(&mut vectors, "request.get_users", &GetUsersRequest);
+ add(
+ &mut vectors,
+ "request.update_user",
+ &UpdateUserRequest {
+ user_id: numeric(1),
+ username: Some(name("new-name")),
+ status: Some(2),
+ options: WireOptions::empty(),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.update_user.none",
+ &UpdateUserRequest {
+ user_id: numeric(5),
+ username: None,
+ status: None,
+ options: WireOptions::empty(),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.update_permissions",
+ &UpdatePermissionsRequest {
+ user_id: numeric(1),
+ permissions: Some(sample_permissions()),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.update_permissions.none",
+ &UpdatePermissionsRequest {
+ user_id: named("admin"),
+ permissions: None,
+ },
+ );
+ add(
+ &mut vectors,
+ "request.change_password",
+ &ChangePasswordRequest {
+ user_id: numeric(1),
+ current_password: "old-pass".to_owned(),
+ new_password: "new-pass-123".to_owned(),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.login_register",
+ &LoginRegisterRequest {
+ version_info: version_info(),
+ username: name("iggy"),
+ password: SecretString::from("iggy"),
+ client_context: None,
+ },
+ );
+ add(
+ &mut vectors,
+ "request.login_register_with_pat",
+ &LoginRegisterWithPatRequest {
+ version_info: version_info(),
+ token: SecretString::from("pat-abc123def456"),
+ client_context: None,
+ },
+ );
+ add(&mut vectors, "request.logout_user", &LogoutUserRequest);
+ add(
+ &mut vectors,
+ "request.create_personal_access_token",
+ &CreatePersonalAccessTokenRequest {
+ name: name("my-token"),
+ expiry: 3600,
+ },
+ );
+ add(
+ &mut vectors,
+ "request.delete_personal_access_token",
+ &DeletePersonalAccessTokenRequest {
+ name: name("my-token"),
+ },
+ );
+ add(
+ &mut vectors,
+ "request.get_personal_access_tokens",
+ &GetPersonalAccessTokensRequest,
+ );
+ add(&mut vectors, "request.ping", &PingRequest);
+ add(&mut vectors, "request.get_stats", &GetStatsRequest);
+ add(&mut vectors, "request.get_me", &GetMeRequest);
+ add(
+ &mut vectors,
+ "request.get_client",
+ &GetClientRequest { client_id: 42 },
+ );
+ add(&mut vectors, "request.get_clients", &GetClientsRequest);
+ add(
+ &mut vectors,
+ "request.get_cluster_metadata",
+ &GetClusterMetadataRequest,
+ );
+ add(
+ &mut vectors,
+ "request.get_snapshot",
+ &GetSnapshotRequest {
+ compression: 2,
+ snapshot_types: vec![1, 5],
+ },
+ );
+ add(
+ &mut vectors,
+ "request.describe_options",
+ &DescribeOptionsRequest { scope: 1 },
+ );
+
+ // Send messages: the full body a producer writes.
+ {
+ let stream_id = numeric(1);
+ let topic_id = numeric(2);
+ let partitioning = WirePartitioning::PartitionId(0);
+ let mut headers = BytesMut::new();
+ // Key order matters for the bytes: the reference SDK keeps headers in
a
+ // `BTreeMap`, so entries are written sorted by key.
+ encode_user_headers(
+ &[
+ (2, b"attempt", 11, &3u32.to_le_bytes()),
+ (2, b"trace-id", 2, b"abc"),
+ ],
+ &mut headers,
+ );
+ let headers = headers.freeze();
+ let messages = [
+ RawMessage {
+ id: 1,
+ origin_timestamp: 1_000,
+ headers: None,
+ payload: b"first",
+ },
+ RawMessage {
+ id: 2,
+ origin_timestamp: 1_500,
+ headers: Some(&headers),
+ payload: b"second",
+ },
+ RawMessage {
+ id: 0xDEAD_BEEF_0000_0000_0000_0000_0000_0003,
+ origin_timestamp: 1_000,
+ headers: None,
+ payload: b"",
+ },
+ ];
+ let mut buf = BytesMut::new();
+ SendMessagesEncoder::encode(&mut buf, &stream_id, &topic_id,
&partitioning, &messages)
+ .expect("encodes");
+ vectors.insert("request.send_messages".to_owned(), hex(&buf));
+ vectors.insert("user_headers.sample".to_owned(), hex(&headers));
+ }
+
+ // Consensus headers.
+ let create_stream_payload = CreateStreamRequest {
+ name: name("stream"),
+ options: WireOptions::empty(),
+ }
+ .to_bytes();
+ vectors.insert(
+ "payload.create_stream".to_owned(),
+ hex(&create_stream_payload),
+ );
+ let client_id: u128 = 0x0123_4567_89AB_CDEF_FEDC_BA98_7654_3210;
+ vectors.insert(
+ "vsr.request.register".to_owned(),
+ hex(bytemuck::bytes_of(&request_header(
+ LOGIN_REGISTER_CODE,
+ client_id,
+ 0,
+ 0,
+ &[1, 2, 3],
+ ))),
+ );
+ vectors.insert(
+ "vsr.request.ping".to_owned(),
+ hex(bytemuck::bytes_of(&request_header(
+ PING_CODE,
+ client_id,
+ 1,
+ 0,
+ &[],
+ ))),
+ );
+ vectors.insert(
+ "vsr.request.ping.bound".to_owned(),
+ hex(bytemuck::bytes_of(&request_header(
+ PING_CODE,
+ client_id,
+ 3,
+ 99,
+ &[],
+ ))),
+ );
+ vectors.insert(
+ "vsr.request.create_stream".to_owned(),
+ hex(bytemuck::bytes_of(&request_header(
+ CREATE_STREAM_CODE,
+ client_id,
+ 1,
+ 99,
+ &create_stream_payload,
+ ))),
+ );
+ vectors.insert(
+ "vsr.request.send_messages".to_owned(),
+ hex(bytemuck::bytes_of(&request_header(
+ SEND_MESSAGES_CODE,
+ client_id,
+ 2,
+ 99,
+ &[9, 9, 9, 9],
+ ))),
+ );
+ vectors.insert(
+ "vsr.request.logout".to_owned(),
+ hex(bytemuck::bytes_of(&request_header(
+ LOGOUT_USER_CODE,
+ client_id,
+ 4,
+ 99,
+ &[],
+ ))),
+ );
+ vectors.insert(
+ "vsr.request.unknown_code".to_owned(),
+ hex(bytemuck::bytes_of(&request_header(
+ 60_000,
+ client_id,
+ 5,
+ 99,
+ &[],
+ ))),
+ );
+
+ let reply = ReplyHeader {
+ command: Command::Reply,
+ operation: Operation::CreateStream,
+ size: (HEADER_SIZE + 12) as u32,
+ request: 7,
+ status: 0,
+ client: client_id,
+ ..Default::default()
+ };
+ vectors.insert("vsr.reply.ok".to_owned(), hex(bytemuck::bytes_of(&reply)));
+ let denied = ReplyHeader {
+ command: Command::Reply,
+ operation: Operation::NonReplicated,
+ size: HEADER_SIZE as u32,
+ request: 8,
+ status: IggyError::Unauthorized.as_code(),
+ ..Default::default()
+ };
+ vectors.insert(
+ "vsr.reply.denied".to_owned(),
+ hex(bytemuck::bytes_of(&denied)),
+ );
+ let eviction = EvictionHeader::new(0, 0, 0, client_id,
EvictionReason::StaleClient);
+ vectors.insert(
+ "vsr.eviction.stale_client".to_owned(),
+ hex(bytemuck::bytes_of(&eviction)),
+ );
+ let incompatible = EvictionHeader::incompatible_protocol(
+ 0,
+ 0,
+ 0,
+ client_id,
+ IGGY_PROTOCOL_VERSION,
+ IGGY_PROTOCOL_VERSION_MIN,
+ );
+ vectors.insert(
+ "vsr.eviction.incompatible_protocol".to_owned(),
+ hex(bytemuck::bytes_of(&incompatible)),
+ );
+
+ // Responses.
+ add(&mut vectors, "response.stream", &stream_response(1, 2));
+ add(
+ &mut vectors,
+ "response.get_stream",
+ &GetStreamResponse {
+ stream: stream_response(1, 2),
+ topics: vec![topic_header(1, "topic-a"), topic_header(2,
"topic-b")],
+ },
+ );
+ add(
+ &mut vectors,
+ "response.get_streams",
+ &GetStreamsResponse {
+ streams: vec![stream_response(1, 2), stream_response(2, 0)],
+ },
+ );
+ add(
+ &mut vectors,
+ "response.topic_header",
+ &topic_header(5, "events"),
+ );
+ let partitions = vec![
+ PartitionResponse {
+ id: 0,
+ created_at: 1_710_000_000_000_000,
+ segments_count: 2,
+ current_offset: 99,
+ size_bytes: 1024,
+ messages_count: 100,
+ },
+ PartitionResponse {
+ id: 1,
+ created_at: 1_710_000_000_000_001,
+ segments_count: 1,
+ current_offset: 0,
+ size_bytes: 0,
+ messages_count: 0,
+ },
+ PartitionResponse {
+ id: 2,
+ created_at: 1_710_000_000_000_002,
+ segments_count: 3,
+ current_offset: 7,
+ size_bytes: 512,
+ messages_count: 8,
+ },
+ ];
+ add(
+ &mut vectors,
+ "response.get_topic",
+ &GetTopicResponse {
+ topic: topic_header(1, "my-topic"),
+ partitions,
+ },
+ );
+ add(
+ &mut vectors,
+ "response.get_topics",
+ &GetTopicsResponse {
+ topics: vec![topic_header(1, "events"), topic_header(2, "logs")],
+ },
+ );
+ add(
+ &mut vectors,
+ "response.get_topics.empty",
+ &GetTopicsResponse { topics: vec![] },
+ );
+ add(
+ &mut vectors,
+ "response.consumer_group",
+ &ConsumerGroupResponse {
+ id: 1,
+ partitions_count: 4,
+ members_count: 2,
+ name: name("my-group"),
+ },
+ );
+ add(
+ &mut vectors,
+ "response.consumer_group_details",
+ &ConsumerGroupDetailsResponse {
+ group: ConsumerGroupResponse {
+ id: 1,
+ partitions_count: 6,
+ members_count: 2,
+ name: name("my-group"),
+ },
+ members: vec![
+ ConsumerGroupMemberResponse {
+ id: 1,
+ partitions_count: 3,
+ partitions: vec![0, 1, 2],
+ },
+ ConsumerGroupMemberResponse {
+ id: 2,
+ partitions_count: 3,
+ partitions: vec![3, 4, 5],
+ },
+ ],
+ },
+ );
+ add(
+ &mut vectors,
+ "response.get_consumer_groups",
+ &GetConsumerGroupsResponse {
+ groups: vec![
+ ConsumerGroupResponse {
+ id: 1,
+ partitions_count: 4,
+ members_count: 2,
+ name: name("group-a"),
+ },
+ ConsumerGroupResponse {
+ id: 2,
+ partitions_count: 4,
+ members_count: 0,
+ name: name("group-b"),
+ },
+ ],
+ },
+ );
+ add(
+ &mut vectors,
+ "response.sync_consumer_group",
+ &SyncConsumerGroupResponse {
+ generation: 7,
+ partitions: vec![0, 2, 4],
+ },
+ );
+ add(
+ &mut vectors,
+ "response.consumer_offset",
+ &ConsumerOffsetResponse {
+ partition_id: 1,
+ current_offset: 1000,
+ stored_offset: 500,
+ },
+ );
+ let client = ClientResponse {
+ client_id: 1,
+ user_id: 10,
+ transport: 1,
+ address: "127.0.0.1:8080".to_owned(),
+ consumer_groups_count: 2,
+ };
+ add(&mut vectors, "response.client", &client);
+ add(
+ &mut vectors,
+ "response.client_details",
+ &ClientDetailsResponse {
+ client: client.clone(),
+ consumer_groups: vec![
+ ConsumerGroupInfoResponse {
+ stream_id: 1,
+ topic_id: 2,
+ group_id: 3,
+ },
+ ConsumerGroupInfoResponse {
+ stream_id: 4,
+ topic_id: 5,
+ group_id: 6,
+ },
+ ],
+ },
+ );
+ add(
+ &mut vectors,
+ "response.client_details.no_user",
+ &ClientDetailsResponse {
+ client: ClientResponse {
+ client_id: 2,
+ user_id: u32::MAX,
+ transport: 1,
+ address: "10.0.0.1:6000".to_owned(),
+ consumer_groups_count: 0,
+ },
+ consumer_groups: vec![],
+ },
+ );
+ add(
+ &mut vectors,
+ "response.get_clients",
+ &GetClientsResponse {
+ clients: vec![
+ client,
+ ClientResponse {
+ client_id: 2,
+ user_id: 20,
+ transport: 2,
+ address: "10.0.0.1:6000".to_owned(),
+ consumer_groups_count: 3,
+ },
+ ],
+ },
+ );
+ let user = UserResponse {
+ id: 1,
+ created_at: 1_710_000_000_000_000,
+ status: 1,
+ username: name("admin"),
+ options: WireOptions::empty(),
+ };
+ add(&mut vectors, "response.user", &user);
+ add(
+ &mut vectors,
+ "response.user_details",
+ &UserDetailsResponse {
+ user: user.clone(),
+ permissions: Some(sample_permissions()),
+ },
+ );
+ add(
+ &mut vectors,
+ "response.user_details.no_permissions",
+ &UserDetailsResponse {
+ user: user.clone(),
+ permissions: None,
+ },
+ );
+ add(
+ &mut vectors,
+ "response.get_users",
+ &GetUsersResponse {
+ users: vec![
+ user,
+ UserResponse {
+ id: 2,
+ created_at: 200,
+ status: 2,
+ username: name("alice"),
+ options: WireOptions::empty(),
+ },
+ ],
+ },
+ );
+ add(
+ &mut vectors,
+ "response.login_register",
+ &LoginRegisterResponse {
+ user_id: 42,
+ session: 100,
+ server_protocol_version: IGGY_PROTOCOL_VERSION,
+ server_version: name("0.11.0"),
+ },
+ );
+ add(
+ &mut vectors,
+ "response.identity",
+ &IdentityResponse { user_id: 42 },
+ );
+ add(
+ &mut vectors,
+ "response.raw_personal_access_token",
+ &RawPersonalAccessTokenResponse {
+ token: name("raw-secret-token-value"),
+ },
+ );
+ add(
+ &mut vectors,
+ "response.get_personal_access_tokens",
+ &GetPersonalAccessTokensResponse {
+ tokens: vec![
+ PersonalAccessTokenResponse {
+ name: name("token-a"),
+ expiry_at: 1_710_000_000_000_000,
+ },
+ PersonalAccessTokenResponse {
+ name: name("token-b"),
+ expiry_at: 0,
+ },
+ ],
+ },
+ );
+ add(
+ &mut vectors,
+ "response.stats",
+ &StatsResponse {
+ process_id: 1234,
+ cpu_usage: 25.5,
+ total_cpu_usage: 50.0,
+ memory_usage: 1_073_741_824,
+ total_memory: 8_589_934_592,
+ available_memory: 4_294_967_296,
+ run_time: 3600,
+ start_time: 1_710_000_000_000_000,
+ read_bytes: 1_000_000,
+ written_bytes: 500_000,
+ messages_size_bytes: 2_000_000,
+ streams_count: 3,
+ topics_count: 10,
+ partitions_count: 30,
+ segments_count: 90,
+ messages_count: 50_000,
+ clients_count: 5,
+ consumer_groups_count: 2,
+ hostname: "node-1".to_owned(),
+ os_name: "Linux".to_owned(),
+ os_version: "6.1".to_owned(),
+ kernel_version: "6.1.0".to_owned(),
+ iggy_server_version: "0.11.0".to_owned(),
+ iggy_server_semver: Some(11_264),
+ cache_metrics: vec![CacheMetricEntry {
+ stream_id: 1,
+ topic_id: 1,
+ partition_id: 0,
+ hits: 1000,
+ misses: 50,
+ hit_ratio: 0.952_380_95,
+ }],
+ threads_count: 16,
+ free_disk_space: 107_374_182_400,
+ total_disk_space: 512_110_190_592,
+ },
+ );
+ add(
+ &mut vectors,
+ "response.cluster_metadata",
+ &ClusterMetadataResponse {
+ name: "prod-cluster".to_owned(),
+ nodes: vec![
+ ClusterNodeResponse {
+ name: "node-1".to_owned(),
+ ip: "10.0.0.1".to_owned(),
+ tcp_port: 8090,
+ quic_port: 8080,
+ http_port: 3000,
+ websocket_port: 8092,
+ role: 0,
+ status: 0,
+ },
+ ClusterNodeResponse {
+ name: "node-2".to_owned(),
+ ip: "fd00::2".to_owned(),
+ tcp_port: 8091,
+ quic_port: 0,
+ http_port: 3001,
+ websocket_port: 0,
+ role: 1,
+ status: 3,
+ },
+ ],
+ },
+ );
+ add(
+ &mut vectors,
+ "response.describe_options",
+ &DescribeOptionsResponse {
+ entries: vec![
+ OptionDescriptor {
+ key: name("segment_size"),
+ kind: 12,
+ default_value:
Bytes::copy_from_slice(&1_073_741_824u64.to_le_bytes()),
+ description: "Segment size".to_owned(),
+ },
+ OptionDescriptor {
+ key: name("durability"),
+ kind: 2,
+ default_value: Bytes::from_static(b"replicated"),
+ description: String::new(),
+ },
+ ],
+ },
+ );
+ add(
+ &mut vectors,
+ "response.send_messages",
+ &SendMessagesResponse {
+ confirmations: vec![
+ SendMessagesConfirmationResponse {
+ stream_id: 1,
+ topic_id: 2,
+ partition_id: 3,
+ base_offset: 4,
+ },
+ SendMessagesConfirmationResponse {
+ stream_id: 1,
+ topic_id: 2,
+ partition_id: 0,
+ base_offset: 42,
+ },
+ ],
+ },
+ );
+ add(
+ &mut vectors,
+ "response.send_messages.empty",
+ &SendMessagesResponse {
+ confirmations: vec![],
+ },
+ );
+
+ // Poll response: header + two batch records.
+ {
+ let first = batch_record(
+ 7,
+ 100,
+ 5_000,
+ 1_000,
+ &[
+ frame(11, 0, 0, b"a", &[]),
+ frame(12, 1, 10, b"b", &vectors_user_headers()),
+ ],
+ );
+ let second = batch_record(7, 102, 6_000, 2_000, &[frame(13, 0, 0,
b"c", &[])]);
+ let mut body = BytesMut::new();
+ PollMessagesResponseHeader {
+ partition_id: 7,
+ current_offset: 102,
+ messages_count: 3,
+ }
+ .encode(&mut body);
+ body.put_slice(&first);
+ body.put_slice(&second);
+ vectors.insert("response.poll_messages".to_owned(), hex(&body));
+ let mut empty = BytesMut::new();
+ PollMessagesResponseHeader {
+ partition_id: 0,
+ current_offset: 0,
+ messages_count: 0,
+ }
+ .encode(&mut empty);
+ vectors.insert("response.poll_messages.empty".to_owned(), hex(&empty));
+ }
+
+ // Domain option blocks.
+ let create_options = TopicCreateOptions {
+ partitions_count: Some(3),
+ compression_algorithm: Some(CompressionAlgorithm::Gzip),
+ message_expiry:
Some(IggyExpiry::ExpireDuration(IggyDuration::new_from_secs(
+ 604_800,
+ ))),
+ max_topic_size:
Some(MaxTopicSize::Custom(IggyByteSize::from(1_073_741_824))),
+ segment_size: Some(IggyByteSize::from(1_048_576)),
+ durability: Durability::Persisted,
+ consumer_offset_durability: Durability::Replicated,
+ messages_required_to_save: Some(7),
+ size_of_messages_required_to_save: Some(IggyByteSize::from(4096)),
+ preallocate_segments: Some(false),
+ raw: BTreeMap::new(),
+ };
+ add(
+ &mut vectors,
+ "options.topic_create.full",
+ &create_options.to_wire().expect("encodes"),
+ );
+ let update_options = TopicUpdateOptions {
+ compression_algorithm: Some(CompressionAlgorithm::None),
+ message_expiry: Some(IggyExpiry::NeverExpire),
+ max_topic_size: Some(MaxTopicSize::Unlimited),
+ raw: BTreeMap::new(),
+ };
+ add(
+ &mut vectors,
+ "options.topic_update.full",
+ &update_options.to_wire().expect("encodes"),
+ );
+ let mut raw = BTreeMap::new();
+ raw.insert("durability".to_owned(), "persisted".to_owned());
+ let raw_options = TopicCreateOptions {
+ raw,
+ ..Default::default()
+ };
+ add(
+ &mut vectors,
+ "options.topic_create.raw",
+ &raw_options.to_wire().expect("encodes"),
+ );
+
+ let golden = Golden {
+ protocol_version: IGGY_PROTOCOL_VERSION,
+ protocol_version_min: IGGY_PROTOCOL_VERSION_MIN,
+ xxh3_64,
+ xxh32,
+ errors,
+ vectors,
+ };
+ let mut json = serde_json::to_string_pretty(&golden).expect("serializes");
+ json.push('\n');
+ if let Some(parent) = std::path::Path::new(&output).parent() {
+ std::fs::create_dir_all(parent).expect("creates the output directory");
+ }
+ std::fs::write(&output, json).expect("writes output");
+ println!("wrote {output}");
+}
+
+fn vectors_user_headers() -> Vec<u8> {
+ let mut headers = BytesMut::new();
+ encode_user_headers(&[(2, b"k", 2, b"v")], &mut headers);
+ headers.to_vec()
+}