Skip to content

Commit 77609b0

Browse files
committed
feat: add seeded XXH3 support
Add hashOptions.seed and xxh3SeedFromLabel for reproducible XXH3 namespaces. Includes native Android/iOS support, Zig XXH3-64 support, example controls, tests, docs, and 2.0.6 release notes.
1 parent ca7df03 commit 77609b0

25 files changed

Lines changed: 1268 additions & 241 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ example/ios/FileHashExample.xcworkspace/xcshareddata/WorkspaceSettings.xcsetting
9696

9797
# Local notes
9898
submodule-notes.md
99+
docs/planned-*.md
99100

100101
# Zig prebuilt artifacts
101102
third_party/zig-files-hash-prebuilt/

CHANGELOG.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,82 @@
11
# Releases
22

3+
## v2.0.6 - Seeded XXH3
4+
5+
This release adds optional seeded `XXH3-64` / `XXH3-128` support.
6+
7+
A seed is useful when an app needs reproducible non-cryptographic checksums
8+
across a backend, CLI tooling, and React Native. For example, a backend can
9+
publish both the expected XXH3 digest and the exact `u64` seed used to compute
10+
it; the mobile app can then hash the downloaded local file with the same seed
11+
and compare the result.
12+
13+
Seeded XXH3 is also useful for app-owned cache or deduplication namespaces,
14+
such as `media-cache-v1` or `upload-dedupe-v2`, where the same bytes should
15+
produce stable hashes within that namespace.
16+
17+
Seeds are not secrets and do not make XXH3 cryptographic. Use HMAC or keyed
18+
BLAKE3 when authenticity matters.
19+
20+
### Added
21+
22+
- Added `hashOptions.seed` for `XXH3-64` and `XXH3-128` in both `fileHash` and
23+
`stringHash`.
24+
- `hashOptions.seed` accepts:
25+
- `bigint`
26+
- non-negative safe integer `number`
27+
- decimal `string`
28+
- `0x` hex `string`
29+
- Added `xxh3SeedFromLabel(label)` for deterministic UTF-8 label -> `u64` seed
30+
derivation using FNV-1a 64-bit. It returns a canonical `0x` hex seed that can
31+
be passed directly to `hashOptions.seed`.
32+
- Added seeded XXH3 coverage to the example app, including label, string,
33+
number, and bigint seed inputs.
34+
- Added seeded XXH3 validation and vector tests.
35+
36+
### Usage
37+
38+
Pass large `u64` seeds from a backend as strings so JavaScript does not round
39+
them:
40+
41+
```ts
42+
const manifest = {
43+
xxh3Seed: '12345678901234567890',
44+
xxh3: '4b5e0a417dfa7ed2fb965bc17c16bd34',
45+
};
46+
47+
const actual = await fileHash(localFileUri, {
48+
algorithm: 'XXH3-128',
49+
hashOptions: {
50+
seed: manifest.xxh3Seed,
51+
},
52+
});
53+
54+
if (actual !== manifest.xxh3) {
55+
throw new Error('Downloaded file failed checksum verification');
56+
}
57+
```
58+
59+
For app-owned namespaces, derive a stable seed from a readable label:
60+
61+
```ts
62+
const seed = xxh3SeedFromLabel('media-cache-v1');
63+
64+
const cacheKey = await fileHash(fileUri, {
65+
algorithm: 'XXH3-128',
66+
hashOptions: { seed },
67+
});
68+
```
69+
70+
### Compatibility
71+
72+
- `hashOptions.seed` is optional. Omit it for regular unseeded XXH3.
73+
- `hashOptions.seed` is only valid for `XXH3-64` and `XXH3-128`; other
74+
algorithms reject it.
75+
- Large seeds above `Number.MAX_SAFE_INTEGER` should be passed as `bigint`,
76+
decimal string, or `0x` hex string.
77+
- With the optional `zig` engine, seeded XXH3 currently means `XXH3-64`;
78+
`XXH3-128` remains native-only.
79+
380
## v2.0.5 - Zig C ABI v3, streaming files, and cancellation
481

582
This release updates the Zig core to `zig-files-hash` `v0.0.5`, moves file

README.md

Lines changed: 107 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,81 @@ const keyed = await fileHash(fileUri, {
148148
```
149149

150150
`keyEncoding` can be `'utf8'`, `'hex'`, or `'base64'`. The default is `'utf8'`.
151+
For `BLAKE3`, `hashOptions.key` is optional: omit it for regular BLAKE3, pass
152+
it only when you want keyed BLAKE3.
153+
154+
HMAC algorithms require `hashOptions.key`. If you intentionally need HMAC with
155+
an empty key, pass `key: ''` explicitly; omitting `key` is rejected.
156+
157+
## Seeded XXH3
158+
159+
`XXH3-64` and `XXH3-128` support an optional unsigned 64-bit seed. Omit it for
160+
regular unseeded XXH3. A seed is not a secret and does not make XXH3
161+
cryptographic. It selects a reproducible XXH3 namespace: the same bytes,
162+
algorithm, and seed produce the same digest on your backend, CLI tools, and
163+
mobile app.
164+
165+
With the optional `zig` engine, seeded XXH3 currently means `XXH3-64`; `XXH3-128`
166+
is available only in the default `native` engine.
167+
168+
The most direct use case is server-side verification. For example, your backend
169+
can publish a download manifest with both the expected XXH3 digest and the seed
170+
that was used to compute it:
171+
172+
```ts
173+
import { fileHash } from '@preeternal/react-native-file-hash';
174+
175+
const manifest = {
176+
url: 'https://example.com/assets/video.mp4',
177+
xxh3Seed: '12345678901234567890',
178+
xxh3: '4b5e0a417dfa7ed2fb965bc17c16bd34',
179+
};
180+
181+
const actual = await fileHash(localFileUri, {
182+
algorithm: 'XXH3-128',
183+
hashOptions: {
184+
seed: manifest.xxh3Seed,
185+
},
186+
});
187+
188+
if (actual !== manifest.xxh3) {
189+
throw new Error('Downloaded file failed checksum verification');
190+
}
191+
```
192+
193+
Pass large `u64` seeds from a backend as strings, either decimal or `0x` hex.
194+
JavaScript `number` is safe only up to `Number.MAX_SAFE_INTEGER`; values like
195+
`12345678901234567890` must be passed as a string or `bigint` so they are not
196+
rounded before hashing.
197+
198+
For app-owned namespaces, you can derive a stable seed from a readable label:
199+
200+
```ts
201+
import {
202+
fileHash,
203+
xxh3SeedFromLabel,
204+
} from '@preeternal/react-native-file-hash';
205+
206+
const mediaCacheSeed = xxh3SeedFromLabel('media-cache-v1');
207+
208+
const cacheKey = await fileHash(fileUri, {
209+
algorithm: 'XXH3-128',
210+
hashOptions: {
211+
seed: mediaCacheSeed,
212+
},
213+
});
214+
```
215+
216+
`hashOptions.seed` accepts a `bigint`, a non-negative safe integer, a decimal
217+
string, or a `0x` hex string. Values are normalized before native hashing, so
218+
`12345`, `12345n`, and `'0x3039'` all use the same seed.
219+
220+
`xxh3SeedFromLabel(label)` derives a deterministic seed from a UTF-8 label
221+
using FNV-1a 64-bit. The helper returns a canonical `0x` hex seed, for example
222+
`0x091677a156a7756e`. Use it when the app controls the namespace, such as
223+
`media-cache-v1` or `upload-dedupe-v2`. If a backend or CLI must reproduce the
224+
same hashes, share either the derived seed value or the exact label derivation
225+
rule. Use HMAC or keyed BLAKE3 when authenticity matters.
151226

152227
## API
153228

@@ -211,6 +286,7 @@ type StringHashRequest = HashRequest & {
211286
type HashOptions = {
212287
key?: string;
213288
keyEncoding?: 'utf8' | 'hex' | 'base64';
289+
seed?: bigint | number | string;
214290
};
215291
```
216292

@@ -238,16 +314,16 @@ removed in a future major release.
238314

239315
## Algorithms
240316

241-
| Algorithm | Use case | Notes |
242-
| ------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------- |
243-
| `SHA-256` | Default general-purpose cryptographic hash | Good default for integrity checks |
244-
| `SHA-384`, `SHA-512` | Stronger SHA-2 variants | Larger output, usually slower |
245-
| `SHA-224`, `SHA-512/224`, `SHA-512/256` | SHA-2 compatibility variants | Useful for protocols requiring these exact digests |
246-
| `MD5`, `SHA-1` | Legacy compatibility | Do not use for new security-sensitive designs |
247-
| `HMAC-SHA-256` | Shared-secret authentication | Good default HMAC choice |
248-
| `HMAC-SHA-224/384/512`, `HMAC-MD5`, `HMAC-SHA-1` | Protocol compatibility | Prefer SHA-256+ for new designs |
249-
| `XXH3-64`, `XXH3-128` | Fast non-cryptographic checksums | Great for caching and deduplication, not authentication |
250-
| `BLAKE3` | Modern high-performance hash | Also supports keyed mode with a 32-byte key |
317+
| Algorithm | Use case | Notes |
318+
| ------------------------------------------------ | ------------------------------------------ | -------------------------------------------------- |
319+
| `SHA-256` | Default general-purpose cryptographic hash | Good default for integrity checks |
320+
| `SHA-384`, `SHA-512` | Stronger SHA-2 variants | Larger output, usually slower |
321+
| `SHA-224`, `SHA-512/224`, `SHA-512/256` | SHA-2 compatibility variants | Useful for protocols requiring these exact digests |
322+
| `MD5`, `SHA-1` | Legacy compatibility | Do not use for new security-sensitive designs |
323+
| `HMAC-SHA-256` | Shared-secret authentication | Good default HMAC choice |
324+
| `HMAC-SHA-224/384/512`, `HMAC-MD5`, `HMAC-SHA-1` | Protocol compatibility | Prefer SHA-256+ for new designs |
325+
| `XXH3-64`, `XXH3-128` | Fast non-cryptographic checksums | Supports optional seed; not authentication |
326+
| `BLAKE3` | Modern high-performance hash | Also supports keyed mode with a 32-byte key |
251327

252328
### Output Lengths
253329

@@ -279,10 +355,12 @@ Common error codes:
279355

280356
Key rules:
281357

282-
- HMAC algorithms require `hashOptions.key`.
358+
- HMAC algorithms require `hashOptions.key`; pass `key: ''` explicitly for an
359+
empty HMAC key.
283360
- `BLAKE3` uses keyed mode only when `hashOptions.key` is provided.
284361
- `BLAKE3` keyed mode requires a 32-byte key after decoding.
285362
- Other algorithms reject `hashOptions.key`.
363+
- `hashOptions.seed` is only valid for `XXH3-64` and `XXH3-128`.
286364
- `HashOptions.mode` was removed and is rejected with `E_INVALID_ARGUMENT`.
287365

288366
## Runtime Info
@@ -302,24 +380,6 @@ const diagnostics = await getRuntimeDiagnostics();
302380
`getRuntimeDiagnostics()` includes Zig ABI/core metadata when the Zig engine is
303381
selected. For the default native engine, consumers usually do not need it.
304382

305-
## Performance
306-
307-
Use physical devices and Release builds for performance claims. Debug,
308-
simulator, and emulator runs are useful for smoke checks, but they do not
309-
represent production throughput.
310-
311-
Full current measurements live in [BENCHMARKS.md](./BENCHMARKS.md).
312-
313-
Practical guidance:
314-
315-
- Start with the default `native` engine for most apps.
316-
- Consider `zig` when you care about specific algorithms or want a portable
317-
hashing core; check [BENCHMARKS.md](./BENCHMARKS.md) for current device data.
318-
- Use `SHA-256` for general integrity checks.
319-
- Use `XXH3-64` or `XXH3-128` for fast non-security checksums.
320-
- Use `HMAC-SHA-256` for shared-secret authentication.
321-
- Avoid `MD5` and `SHA-1` for new security-sensitive designs.
322-
323383
## Optional: Engine Selection
324384

325385
This library ships with two build-time engines:
@@ -380,6 +440,24 @@ If `engine` is omitted, `native` is used.
380440

381441
- `XXH3-128` is currently available only in the `native` engine.
382442

443+
## Performance
444+
445+
Use physical devices and Release builds for performance claims. Debug,
446+
simulator, and emulator runs are useful for smoke checks, but they do not
447+
represent production throughput.
448+
449+
Full current measurements live in [BENCHMARKS.md](./BENCHMARKS.md).
450+
451+
Practical guidance:
452+
453+
- Start with the default `native` engine for most apps.
454+
- Consider `zig` when you care about specific algorithms or want a portable
455+
hashing core; check [BENCHMARKS.md](./BENCHMARKS.md) for current device data.
456+
- Use `SHA-256` for general integrity checks.
457+
- Use `XXH3-64` or `XXH3-128` for fast non-security checksums.
458+
- Use `HMAC-SHA-256` for shared-secret authentication.
459+
- Avoid `MD5` and `SHA-1` for new security-sensitive designs.
460+
383461
## Android 16 KB Page Size
384462

385463
The Android build enables 16 KB page alignment when the linker supports

android/src/main/cpp/hash_jni.cpp

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,30 @@
55
#include "blake3.h"
66

77
extern "C" JNIEXPORT jlong JNICALL
8-
Java_com_preeternal_filehash_NativeHasher_xxh3Init64(JNIEnv *, jobject) {
8+
Java_com_preeternal_filehash_NativeHasher_xxh3Init64(JNIEnv *, jobject, jlong seed, jboolean has_seed) {
99
XXH3_state_t *state = XXH3_createState();
1010
if (state == nullptr) {
1111
return 0;
1212
}
13-
XXH3_64bits_reset(state);
13+
if (has_seed == JNI_TRUE) {
14+
XXH3_64bits_reset_withSeed(state, static_cast<XXH64_hash_t>(seed));
15+
} else {
16+
XXH3_64bits_reset(state);
17+
}
1418
return reinterpret_cast<jlong>(state);
1519
}
1620

1721
extern "C" JNIEXPORT jlong JNICALL
18-
Java_com_preeternal_filehash_NativeHasher_xxh3Init128(JNIEnv *, jobject) {
22+
Java_com_preeternal_filehash_NativeHasher_xxh3Init128(JNIEnv *, jobject, jlong seed, jboolean has_seed) {
1923
XXH3_state_t *state = XXH3_createState();
2024
if (state == nullptr) {
2125
return 0;
2226
}
23-
XXH3_128bits_reset(state);
27+
if (has_seed == JNI_TRUE) {
28+
XXH3_128bits_reset_withSeed(state, static_cast<XXH64_hash_t>(seed));
29+
} else {
30+
XXH3_128bits_reset(state);
31+
}
2432
return reinterpret_cast<jlong>(state);
2533
}
2634

0 commit comments

Comments
 (0)