diff --git a/.gitignore b/.gitignore index 28c2d7c..0783945 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,10 @@ a.out /test_clientid /test_stamp /test_map +/test_sha1 +/test_sha1_runtime_endian +/test_uuid +/test_elementid /test_element # Tooling diff --git a/Makefile b/Makefile index 9970446..44c562e 100644 --- a/Makefile +++ b/Makefile @@ -37,8 +37,8 @@ test-string: string.c test_string.c test_util.h ./test_string .PHONY: test-counter -test-counter: arena.c string.c hashtable.c clientid.c host_posix.c counter.c test_counter.c test_util.h - $(CC) $(CFLAGS) -o test_counter arena.c string.c hashtable.c clientid.c host_posix.c counter.c test_counter.c +test-counter: arena.c string.c hashtable.c clientid.c elementid.c uuid.c sha1.c host_posix.c counter.c test_counter.c test_util.h + $(CC) $(CFLAGS) -o test_counter arena.c string.c hashtable.c clientid.c elementid.c uuid.c sha1.c host_posix.c counter.c test_counter.c ./test_counter .PHONY: test-scalar @@ -47,20 +47,44 @@ test-scalar: arena.c string.c host_posix.c scalar.c test_scalar.c test_util.h ./test_scalar .PHONY: test-register -test-register: arena.c string.c clientid.c host_posix.c stamp.c scalar.c register.c test_register.c test_util.h - $(CC) $(CFLAGS) -o test_register arena.c string.c clientid.c host_posix.c stamp.c scalar.c register.c test_register.c +test-register: arena.c string.c clientid.c elementid.c uuid.c sha1.c host_posix.c stamp.c scalar.c register.c test_register.c test_util.h + $(CC) $(CFLAGS) -o test_register arena.c string.c clientid.c elementid.c uuid.c sha1.c host_posix.c stamp.c scalar.c register.c test_register.c ./test_register .PHONY: test-map -test-map: arena.c string.c hashtable.c clientid.c host_posix.c stamp.c scalar.c register.c counter.c element.c map.c test_map.c test_util.h - $(CC) $(CFLAGS) -o test_map arena.c string.c hashtable.c clientid.c host_posix.c stamp.c scalar.c register.c counter.c element.c map.c test_map.c +test-map: arena.c string.c hashtable.c clientid.c elementid.c uuid.c sha1.c host_posix.c stamp.c scalar.c register.c counter.c element.c map.c test_map.c test_util.h + $(CC) $(CFLAGS) -o test_map arena.c string.c hashtable.c clientid.c elementid.c uuid.c sha1.c host_posix.c stamp.c scalar.c register.c counter.c element.c map.c test_map.c ./test_map .PHONY: test-element -test-element: arena.c string.c hashtable.c clientid.c host_posix.c stamp.c scalar.c register.c counter.c map.c element.c test_element.c test_util.h - $(CC) $(CFLAGS) -o test_element arena.c string.c hashtable.c clientid.c host_posix.c stamp.c scalar.c register.c counter.c map.c element.c test_element.c +test-element: arena.c string.c hashtable.c clientid.c elementid.c uuid.c sha1.c host_posix.c stamp.c scalar.c register.c counter.c map.c element.c test_element.c test_util.h + $(CC) $(CFLAGS) -o test_element arena.c string.c hashtable.c clientid.c elementid.c uuid.c sha1.c host_posix.c stamp.c scalar.c register.c counter.c map.c element.c test_element.c ./test_element +.PHONY: test-elementid +test-elementid: string.c clientid.c elementid.c uuid.c sha1.c host_posix.c test_elementid.c test_util.h + $(CC) $(CFLAGS) -o test_elementid string.c clientid.c elementid.c uuid.c sha1.c host_posix.c test_elementid.c + ./test_elementid + +.PHONY: test-sha1 +test-sha1: sha1.c test_sha1.c test_util.h + $(CC) $(CFLAGS) -o test_sha1 sha1.c test_sha1.c + ./test_sha1 + +# Exercises sha1.c's runtime endian-detection fallback by forcing the +# SHA1_USE_RUNTIME_ENDIAN path at compile time. Independent of whether the +# host's system headers happen to predefine BYTE_ORDER. Must produce +# byte-identical digests to the default build. +.PHONY: test-sha1-runtime-endian +test-sha1-runtime-endian: sha1.c test_sha1.c test_util.h + $(CC) $(CFLAGS) -DSHA1_USE_RUNTIME_ENDIAN -o test_sha1_runtime_endian sha1.c test_sha1.c + ./test_sha1_runtime_endian + +.PHONY: test-uuid +test-uuid: uuid.c sha1.c test_uuid.c test_util.h + $(CC) $(CFLAGS) -o test_uuid uuid.c sha1.c test_uuid.c + ./test_uuid + .PHONY: test-clientid test-clientid: string.c clientid.c host_posix.c test_clientid.c test_util.h $(CC) $(CFLAGS) -o test_clientid string.c clientid.c host_posix.c test_clientid.c @@ -72,4 +96,4 @@ test-stamp: string.c clientid.c host_posix.c stamp.c test_stamp.c test_util.h ./test_stamp .PHONY: test -test: test-arena test-hashtable test-string test-counter test-scalar test-register test-clientid test-stamp test-map test-element +test: test-arena test-hashtable test-string test-counter test-scalar test-register test-clientid test-stamp test-map test-sha1 test-sha1-runtime-endian test-uuid test-elementid test-element diff --git a/counter.c b/counter.c index 21829bb..2306f05 100644 --- a/counter.c +++ b/counter.c @@ -4,25 +4,26 @@ #include "host.h" struct Counter { + ElementId id; Arena *arena; - HashTable *entries; // client_id (uint32_t) -> CounterEntry + HashTable *entries; // ClientId -> CounterEntry }; static inline uint32_t max_u32(uint32_t a, uint32_t b) { if (a > b) { return a; } - return b; } -Counter *counter_create(Arena *arena) { +Counter *counter_create(Arena *arena, ElementId id) { Counter *counter = arena_alloc(arena, sizeof(Counter)); if (!counter) { host_abortf( "counter_create: arena OOM (requested %zu bytes for Counter)", sizeof(Counter)); } + counter->id = id; counter->arena = arena; counter->entries = hashtable_create(arena); if (!counter->entries) { @@ -32,6 +33,8 @@ Counter *counter_create(Arena *arena) { return counter; } +ElementId counter_id(const Counter *counter) { return counter->id; } + // Get-or-create the per-client CounterEntry for `client_id`. Initializes a // fresh entry to {inc=0, dec=0} on first call for a given client. static CounterEntry *counter_entry_for(Counter *counter, ClientId client_id) { @@ -110,7 +113,7 @@ void counter_dec(Counter *counter, ClientId client_id, uint32_t amount) { } Counter *counter_clone(Arena *arena, const Counter *counter) { - Counter *clone = counter_create(arena); + Counter *clone = counter_create(arena, counter->id); HashTableIter it = hashtable_iter(counter->entries); const void *key; size_t key_len; diff --git a/counter.h b/counter.h index ff4ea6b..8f21fbc 100644 --- a/counter.h +++ b/counter.h @@ -2,9 +2,12 @@ #define _CRDT_COUNTER_H // PN-Counter: integer counter with concurrent increments and decrements. -// Identity is positional: a Counter is "the Counter for this slot" via the -// (key, kind) tuple of the containing Map. The Counter struct itself holds -// no identifier. +// Identity: each Counter is stamped with an ElementId at create, exposed +// via counter_id. Two replicas independently creating "the same Counter +// at the same slot" derive identical ids via +// elementid_derive(parent.id, key, key_len, ELEMENT_COUNTER), which is what +// map_merge's recursive guard uses to know they refer to the same logical +// element. // // Semantics: // - Per-client (inc, dec) tallies, one CounterEntry per ClientId that @@ -26,6 +29,7 @@ #include "arena.h" #include "clientid.h" +#include "elementid.h" #include "hashtable.h" #include @@ -37,7 +41,9 @@ typedef struct CounterEntry { typedef struct Counter Counter; -Counter *counter_create(Arena *arena); +Counter *counter_create(Arena *arena, ElementId id); + +ElementId counter_id(const Counter *counter); int64_t counter_read(const Counter *counter); diff --git a/element.c b/element.c index e181c4f..c1e0939 100644 --- a/element.c +++ b/element.c @@ -5,6 +5,20 @@ #include "register.h" #include "scalar.h" +ElementId element_id(Element e) { + switch (e.kind) { + case ELEMENT_SCALAR: + host_abort("element_id: scalar elements have no id"); + break; + case ELEMENT_REGISTER: + return register_id(e.as.reg); + case ELEMENT_COUNTER: + return counter_id(e.as.counter); + case ELEMENT_MAP: + return map_id(e.as.map); + } +} + Element element_scalar(Scalar s) { Element e = {.kind = ELEMENT_SCALAR, .as.scalar = s}; return e; diff --git a/element.h b/element.h index 551c56b..5ed46fa 100644 --- a/element.h +++ b/element.h @@ -22,6 +22,7 @@ // putting them in the same arena as the containing Map). #include "counter.h" +#include "elementid.h" #include "register.h" #include "scalar.h" @@ -46,6 +47,7 @@ typedef struct Element { } as; } Element; +ElementId element_id(Element e); Element element_scalar(Scalar s); Element element_register(Register *r); Element element_counter(Counter *c); diff --git a/elementid.c b/elementid.c new file mode 100644 index 0000000..f29a3a7 --- /dev/null +++ b/elementid.c @@ -0,0 +1,49 @@ +#include "elementid.h" + +ElementId elementid_from_bytes(const uint8_t bytes[16]) { + ElementId id; + for (int i = 0; i < 16; i++) { + id.uuid.bytes[i] = bytes[i]; + } + return id; +} + +ElementId elementid_root(void) { + ElementId id; + for (int i = 0; i < 16; i++) { + id.uuid.bytes[i] = 0; + } + return id; +} + +bool elementid_eq(ElementId a, ElementId b) { + for (int i = 0; i < 16; i++) { + if (a.uuid.bytes[i] != b.uuid.bytes[i]) { + return false; + } + } + return true; +} + +int elementid_cmp(ElementId a, ElementId b) { + for (int i = 0; i < 16; i++) { + if (a.uuid.bytes[i] < b.uuid.bytes[i]) { + return -1; + } else if (a.uuid.bytes[i] > b.uuid.bytes[i]) { + return 1; + } + } + return 0; +} + +ElementId elementid_derive(ElementId parent, const void *key, size_t key_len, + uint8_t kind) { + UuidV5Ctx ctx = {0}; + uuid_v5_init(&ctx, parent.uuid.bytes); + uuid_v5_update(&ctx, key, key_len); + uuid_v5_update(&ctx, &kind, sizeof(kind)); + + ElementId derived = {0}; + derived.uuid = uuid_v5_final(&ctx); + return derived; +} diff --git a/elementid.h b/elementid.h new file mode 100644 index 0000000..c10452f --- /dev/null +++ b/elementid.h @@ -0,0 +1,40 @@ +#ifndef _CRDT_ELEMENTID_H +#define _CRDT_ELEMENTID_H + +// ElementId: stable identity of a composite element (Register / Counter / +// Map), shared across replicas. Stamped on the composite at create, never +// mutated afterwards. +// +// Wire format: 16-byte UUID per RFC 4122 / RFC 9562. Convergent derivation +// uses UUID v5 (SHA-1 over namespace + name). The version/variant bits are +// set per spec so the result is a valid UUID — useful for cross-language +// interop, debugging, and standard tooling. +// +// Two replicas independently calling elementid_derive with matching +// inputs land on the same UUID by construction. That's how map_merge's +// recursive guard knows two slots refer to the same logical element. +// +// Manual construction (elementid_from_bytes) is supported for imports and +// for cases where the app provides its own convergence guarantee. + +#include "uuid.h" +#include +#include +#include + +typedef struct ElementId { + UuidV5 uuid; +} ElementId; + +ElementId elementid_from_bytes(const uint8_t bytes[16]); + +ElementId elementid_root(void); + +bool elementid_eq(ElementId a, ElementId b); + +int elementid_cmp(ElementId a, ElementId b); + +ElementId elementid_derive(ElementId parent, const void *key, size_t key_len, + uint8_t kind); + +#endif // _CRDT_ELEMENTID_H diff --git a/map.c b/map.c index 8bf1cd0..0ad45fa 100644 --- a/map.c +++ b/map.c @@ -11,16 +11,18 @@ typedef struct MapEntry { } Entry; struct Map { + ElementId id; Arena *arena; HashTable *entries; }; -Map *map_create(Arena *arena) { +Map *map_create(Arena *arena, ElementId id) { Map *map = arena_alloc(arena, sizeof(Map)); if (!map) { host_abortf("map_create: arena OOM (requested %zu bytes for Map)", sizeof(Map)); } + map->id = id; map->arena = arena; map->entries = hashtable_create(arena); if (!map->entries) { @@ -29,6 +31,8 @@ Map *map_create(Arena *arena) { return map; } +ElementId map_id(const Map *map) { return map->id; } + bool map_get(const Map *map, const void *key, size_t key_len, Element *out) { void *entry; bool present = hashtable_get(map->entries, key, key_len, &entry); @@ -124,11 +128,23 @@ void map_merge(Map *dst, const Map *src) { Entry *de; bool dst_has = hashtable_get(dst->entries, k, klen, (void **)&de); - // Recursive: both alive, same composite kind and same id then - // element_merge. This wins over slot LWW. + // Same key, both alive composites of same kind: either recurse in + // place (matching ids → same logical element) or LWW-clone the + // winner (mismatched ids → distinct logical elements that happen + // to share the slot). if (dst_has && !de->is_tombstone && !se->is_tombstone && de->value.kind == se->value.kind && de->value.kind != ELEMENT_SCALAR) { + if (!elementid_eq(element_id(de->value), element_id(se->value))) { + // Distinct logical elements at the same slot. LWW the + // slot; if src wins, replace dst's composite with a + // clone of src's. Loser is orphaned. + if (stamp_gt(se->stamp, de->stamp)) { + de->value = element_clone(dst->arena, se->value); + de->stamp = se->stamp; + } + continue; + } element_merge(de->value, se->value); // Advance slot stamp to max(dst, src) so future slot-level // ops on this key are LWW-deterministic across replicas. @@ -182,7 +198,8 @@ Counter *map_counter(Map *map, const void *key, size_t key_len, Stamp stamp) { return existing->value.as.counter; } - Counter *fresh = counter_create(map->arena); + ElementId id = elementid_derive(map->id, key, key_len, ELEMENT_COUNTER); + Counter *fresh = counter_create(map->arena, id); if (!present || stamp_gt(stamp, existing->stamp)) { map_set(map, key, key_len, element_counter(fresh), stamp); } @@ -201,7 +218,8 @@ Register *map_register(Map *map, const void *key, size_t key_len, Scalar seed, return existing->value.as.reg; } - Register *fresh = register_create(map->arena, seed, stamp); + ElementId id = elementid_derive(map->id, key, key_len, ELEMENT_REGISTER); + Register *fresh = register_create(map->arena, id, seed, stamp); if (!present || stamp_gt(stamp, existing->stamp)) { map_set(map, key, key_len, element_register(fresh), stamp); } @@ -217,7 +235,8 @@ Map *map_map(Map *map, const void *key, size_t key_len, Stamp stamp) { return existing->value.as.map; } - Map *fresh = map_create(map->arena); + ElementId id = elementid_derive(map->id, key, key_len, ELEMENT_MAP); + Map *fresh = map_create(map->arena, id); if (!present || stamp_gt(stamp, existing->stamp)) { map_set(map, key, key_len, element_map(fresh), stamp); } @@ -225,7 +244,7 @@ Map *map_map(Map *map, const void *key, size_t key_len, Stamp stamp) { } Map *map_clone(Arena *arena, const Map *map) { - Map *clone = map_create(arena); + Map *clone = map_create(arena, map->id); HashTableIter it = hashtable_iter(map->entries); const void *k; size_t klen; diff --git a/map.h b/map.h index d45e3cf..0ce7cc1 100644 --- a/map.h +++ b/map.h @@ -2,9 +2,11 @@ #define _CRDT_MAP_H // LWW Map with tombstones, keyed on raw bytes (binary-safe), Element-valued. -// Identity for a composite slot is positional: "the Counter / Register / -// nested Map at this key in this Map." The composites themselves hold no -// identifier; map_merge uses (key, kind) to decide whether to recurse. +// Identity: the Map itself is stamped with an ElementId at create, exposed +// via map_id. Each composite slot value (Counter / Register / nested Map) +// carries its own ElementId; helpers derive child ids convergently via +// elementid_derive(parent.id, key, key_len, kind). map_merge's recursive guard +// uses (kind, id) to know two slots refer to the same logical element. // // Semantics: // - Each slot carries a Stamp. set / delete take effect iff the new stamp @@ -16,12 +18,16 @@ // same delete decision. // // Merge (per src slot): -// - Both alive AND same composite kind (REGISTER / COUNTER / MAP) → -// element_merge(dst, src) recurses in place. Slot stamp advances to -// max(dst, src) so future slot-level ops stay LWW-deterministic. -// - Otherwise → LWW on slot stamp. Scalar winners are scalar_clone'd into -// dst's arena. Composite winners are deep-cloned via element_clone into -// dst's arena, so dst owns its slot fully and survives src arena +// - Both alive AND same composite kind AND matching ids → element_merge +// recurses in place. Slot stamp advances to max(dst, src) so future +// slot-level ops stay LWW-deterministic. +// - Both alive AND same composite kind BUT mismatched ids → distinct +// logical elements that happen to share the slot. LWW on slot stamp; +// if src wins, dst's composite is replaced with a deep clone of src's +// into dst's arena. Loser is orphaned. +// - Otherwise → LWW on slot stamp. Scalar winners are scalar_clone'd +// into dst's arena. Composite winners are deep-cloned via element_clone +// into dst's arena, so dst owns its slot fully and survives src arena // destroy. // // Ownership: @@ -40,6 +46,7 @@ #include "arena.h" #include "element.h" +#include "elementid.h" #include "scalar.h" #include "stamp.h" #include @@ -47,7 +54,9 @@ typedef struct Map Map; -Map *map_create(Arena *arena); +Map *map_create(Arena *arena, ElementId id); + +ElementId map_id(const Map *map); // Returns true if the key has a live (non-tombstone) entry, in which case // *out is set. Returns false otherwise; *out is untouched. diff --git a/register.c b/register.c index 3fd19eb..5899666 100644 --- a/register.c +++ b/register.c @@ -3,27 +3,31 @@ #include "host.h" #include "scalar.h" #include -#include struct Register { + ElementId id; Arena *arena; Scalar value; Stamp stamp; }; -Register *register_create(Arena *arena, Scalar value, Stamp stamp) { +Register *register_create(Arena *arena, ElementId id, Scalar value, + Stamp stamp) { Register *reg = arena_alloc(arena, sizeof(Register)); if (!reg) { host_abortf( "register_create: arena OOM (requested %zu bytes for Register)", sizeof(Register)); } + reg->id = id; reg->arena = arena; reg->value = scalar_clone(arena, value); reg->stamp = stamp; return reg; } +ElementId register_id(const Register *reg) { return reg->id; } + Scalar register_read(const Register *reg) { return reg->value; } void register_set(Register *reg, Scalar value, Stamp stamp) { @@ -47,6 +51,7 @@ Register *register_clone(Arena *arena, const Register *reg) { "register_clone: arena OOM (requested %zu bytes for Register)", sizeof(Register)); } + clone->id = reg->id; clone->arena = arena; clone->value = scalar_clone(arena, reg->value); clone->stamp = reg->stamp; diff --git a/register.h b/register.h index 19c82b9..e7def10 100644 --- a/register.h +++ b/register.h @@ -2,9 +2,12 @@ #define _CRDT_REGISTER_H // LWW (last-writer-wins) Register holding a Scalar value with a -// (lamport, client_id) stamp. Identity is positional: a Register is "the -// Register for this slot" via the (key, kind) tuple of the containing Map. -// The Register struct itself holds no identifier. +// (lamport, client_id) stamp. Identity: each Register is stamped with an +// ElementId at create, exposed via register_id. Two replicas independently +// creating "the same Register at the same slot" derive identical ids via +// elementid_derive(parent.id, key, key_len, ELEMENT_REGISTER), which is what +// map_merge's recursive guard uses to know they refer to the same logical +// element. // // Semantics: // - Register always holds a value (seeded at create); there is no "unset" @@ -31,6 +34,7 @@ // Lifetime: Register must not outlive its arena. #include "arena.h" +#include "elementid.h" #include "scalar.h" #include "stamp.h" #include @@ -38,7 +42,10 @@ typedef struct Register Register; -Register *register_create(Arena *arena, Scalar value, Stamp stamp); +Register *register_create(Arena *arena, ElementId id, Scalar value, + Stamp stamp); + +ElementId register_id(const Register *reg); Scalar register_read(const Register *reg); diff --git a/sha1.c b/sha1.c new file mode 100644 index 0000000..56cb650 --- /dev/null +++ b/sha1.c @@ -0,0 +1,300 @@ +/* +SHA-1 in C +By Steve Reid +100% Public Domain + +Test Vectors (from FIPS PUB 180-1) +"abc" + A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D +"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" + 84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1 +A million repetitions of "a" + 34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F +*/ + +/* #define LITTLE_ENDIAN * This should be #define'd already, if true. */ +/* #define SHA1HANDSOFF * Copies data before messing with it. */ + +#define SHA1HANDSOFF + +#include + +/* for uint32_t */ +#include + +#include "sha1.h" + +#define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits)))) + +/* blk0() and blk() perform the initial expand. */ +/* I got the idea of expanding during the round function from SSLeay */ +#define blk0_le(i) \ + (block->l[i] = (rol(block->l[i], 24) & 0xFF00FF00) | \ + (rol(block->l[i], 8) & 0x00FF00FF)) +#define blk0_be(i) block->l[i] +/* Endianness selection. The original Reid code keyed on `#if BYTE_ORDER == + * LITTLE_ENDIAN`, which is fragile: undefined macros become 0 in + * preprocessor expressions, so a system that defines neither BYTE_ORDER + * nor LITTLE_ENDIAN gets `0 == 0` → true → little-endian forced even on + * big-endian hardware. Guard with explicit `defined()` checks and let the + * runtime fallback handle the unknown case. Define SHA1_USE_RUNTIME_ENDIAN + * at build time to force the runtime path even on systems where the + * compile-time macros are present — used by the runtime-endian test + * target. */ +#if !defined(SHA1_USE_RUNTIME_ENDIAN) && defined(BYTE_ORDER) && \ + defined(LITTLE_ENDIAN) && BYTE_ORDER == LITTLE_ENDIAN +#define blk0(i) blk0_le(i) +#elif !defined(SHA1_USE_RUNTIME_ENDIAN) && defined(BYTE_ORDER) && \ + defined(BIG_ENDIAN) && BYTE_ORDER == BIG_ENDIAN +#define blk0(i) blk0_be(i) +#else +/* Runtime endian check. The union puts a 1 in the low byte on + * little-endian (c == 1) or the high byte on big-endian (c == 0). + * `static` keeps the symbol TU-local so embedding sha1.c into a larger + * project doesn't collide with another `sha1_endian` (or pollute the + * global namespace). */ +static const union { + long l; + char c; +} sha1_endian = {1}; +#define blk0(i) (sha1_endian.c == 0 ? blk0_be(i) : blk0_le(i)) +#endif + +#define blk(i) \ + (block->l[i & 15] = rol(block->l[(i + 13) & 15] ^ block->l[(i + 8) & 15] ^ \ + block->l[(i + 2) & 15] ^ block->l[i & 15], \ + 1)) + +/* (R0+R1), R2, R3, R4 are the different operations used in SHA1 */ +#define R0(v, w, x, y, z, i) \ + z += ((w & (x ^ y)) ^ y) + blk0(i) + 0x5A827999 + rol(v, 5); \ + w = rol(w, 30); +#define R1(v, w, x, y, z, i) \ + z += ((w & (x ^ y)) ^ y) + blk(i) + 0x5A827999 + rol(v, 5); \ + w = rol(w, 30); +#define R2(v, w, x, y, z, i) \ + z += (w ^ x ^ y) + blk(i) + 0x6ED9EBA1 + rol(v, 5); \ + w = rol(w, 30); +#define R3(v, w, x, y, z, i) \ + z += (((w | x) & y) | (w & x)) + blk(i) + 0x8F1BBCDC + rol(v, 5); \ + w = rol(w, 30); +#define R4(v, w, x, y, z, i) \ + z += (w ^ x ^ y) + blk(i) + 0xCA62C1D6 + rol(v, 5); \ + w = rol(w, 30); + +/* Hash a single 512-bit block. This is the core of the algorithm. */ + +void SHA1Transform(uint32_t state[5], const unsigned char buffer[64]) { + uint32_t a, b, c, d, e; + + typedef union { + unsigned char c[64]; + uint32_t l[16]; + } CHAR64LONG16; + +#ifdef SHA1HANDSOFF + CHAR64LONG16 block[1]; /* use array to appear as a pointer */ + + memcpy(block, buffer, 64); +#else + /* The following had better never be used because it causes the + * pointer-to-const buffer to be cast into a pointer to non-const. + * And the result is written through. I threw a "const" in, hoping + * this will cause a diagnostic. + */ + CHAR64LONG16 *block = (const CHAR64LONG16 *)buffer; +#endif + /* Copy context->state[] to working vars */ + a = state[0]; + b = state[1]; + c = state[2]; + d = state[3]; + e = state[4]; + /* 4 rounds of 20 operations each. Loop unrolled. */ + R0(a, b, c, d, e, 0); + R0(e, a, b, c, d, 1); + R0(d, e, a, b, c, 2); + R0(c, d, e, a, b, 3); + R0(b, c, d, e, a, 4); + R0(a, b, c, d, e, 5); + R0(e, a, b, c, d, 6); + R0(d, e, a, b, c, 7); + R0(c, d, e, a, b, 8); + R0(b, c, d, e, a, 9); + R0(a, b, c, d, e, 10); + R0(e, a, b, c, d, 11); + R0(d, e, a, b, c, 12); + R0(c, d, e, a, b, 13); + R0(b, c, d, e, a, 14); + R0(a, b, c, d, e, 15); + R1(e, a, b, c, d, 16); + R1(d, e, a, b, c, 17); + R1(c, d, e, a, b, 18); + R1(b, c, d, e, a, 19); + R2(a, b, c, d, e, 20); + R2(e, a, b, c, d, 21); + R2(d, e, a, b, c, 22); + R2(c, d, e, a, b, 23); + R2(b, c, d, e, a, 24); + R2(a, b, c, d, e, 25); + R2(e, a, b, c, d, 26); + R2(d, e, a, b, c, 27); + R2(c, d, e, a, b, 28); + R2(b, c, d, e, a, 29); + R2(a, b, c, d, e, 30); + R2(e, a, b, c, d, 31); + R2(d, e, a, b, c, 32); + R2(c, d, e, a, b, 33); + R2(b, c, d, e, a, 34); + R2(a, b, c, d, e, 35); + R2(e, a, b, c, d, 36); + R2(d, e, a, b, c, 37); + R2(c, d, e, a, b, 38); + R2(b, c, d, e, a, 39); + R3(a, b, c, d, e, 40); + R3(e, a, b, c, d, 41); + R3(d, e, a, b, c, 42); + R3(c, d, e, a, b, 43); + R3(b, c, d, e, a, 44); + R3(a, b, c, d, e, 45); + R3(e, a, b, c, d, 46); + R3(d, e, a, b, c, 47); + R3(c, d, e, a, b, 48); + R3(b, c, d, e, a, 49); + R3(a, b, c, d, e, 50); + R3(e, a, b, c, d, 51); + R3(d, e, a, b, c, 52); + R3(c, d, e, a, b, 53); + R3(b, c, d, e, a, 54); + R3(a, b, c, d, e, 55); + R3(e, a, b, c, d, 56); + R3(d, e, a, b, c, 57); + R3(c, d, e, a, b, 58); + R3(b, c, d, e, a, 59); + R4(a, b, c, d, e, 60); + R4(e, a, b, c, d, 61); + R4(d, e, a, b, c, 62); + R4(c, d, e, a, b, 63); + R4(b, c, d, e, a, 64); + R4(a, b, c, d, e, 65); + R4(e, a, b, c, d, 66); + R4(d, e, a, b, c, 67); + R4(c, d, e, a, b, 68); + R4(b, c, d, e, a, 69); + R4(a, b, c, d, e, 70); + R4(e, a, b, c, d, 71); + R4(d, e, a, b, c, 72); + R4(c, d, e, a, b, 73); + R4(b, c, d, e, a, 74); + R4(a, b, c, d, e, 75); + R4(e, a, b, c, d, 76); + R4(d, e, a, b, c, 77); + R4(c, d, e, a, b, 78); + R4(b, c, d, e, a, 79); + /* Add the working vars back into context.state[] */ + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + state[4] += e; + /* Wipe variables */ + a = b = c = d = e = 0; +#ifdef SHA1HANDSOFF + memset(block, '\0', sizeof(block)); +#endif +} + +/* SHA1Init - Initialize new context */ + +void SHA1Init(SHA1_CTX *context) { + /* SHA1 initialization constants */ + context->state[0] = 0x67452301; + context->state[1] = 0xEFCDAB89; + context->state[2] = 0x98BADCFE; + context->state[3] = 0x10325476; + context->state[4] = 0xC3D2E1F0; + context->count[0] = context->count[1] = 0; +} + +/* Run your data through this. */ + +void SHA1Update(SHA1_CTX *context, const unsigned char *data, uint32_t len) { + uint32_t i; + + uint32_t j; + + j = context->count[0]; + if ((context->count[0] += len << 3) < j) + context->count[1]++; + context->count[1] += (len >> 29); + j = (j >> 3) & 63; + if ((j + len) > 63) { + memcpy(&context->buffer[j], data, (i = 64 - j)); + SHA1Transform(context->state, context->buffer); + for (; i + 63 < len; i += 64) { + SHA1Transform(context->state, &data[i]); + } + j = 0; + } else + i = 0; + memcpy(&context->buffer[j], &data[i], len - i); +} + +/* Add padding and return the message digest. */ + +void SHA1Final(unsigned char digest[20], SHA1_CTX *context) { + unsigned i; + + unsigned char finalcount[8]; + + unsigned char c; + +#if 0 /* untested "improvement" by DHR */ + /* Convert context->count to a sequence of bytes + * in finalcount. Second element first, but + * big-endian order within element. + * But we do it all backwards. + */ + unsigned char *fcp = &finalcount[8]; + + for (i = 0; i < 2; i++) + { + uint32_t t = context->count[i]; + + int j; + + for (j = 0; j < 4; t >>= 8, j++) + *--fcp = (unsigned char) t} +#else + for (i = 0; i < 8; i++) { + finalcount[i] = (unsigned char)((context->count[(i >= 4 ? 0 : 1)] >> + ((3 - (i & 3)) * 8)) & + 255); /* Endian independent */ + } +#endif + c = 0200; + SHA1Update(context, &c, 1); + while ((context->count[0] & 504) != 448) { + c = 0000; + SHA1Update(context, &c, 1); + } + SHA1Update(context, finalcount, 8); /* Should cause a SHA1Transform() */ + for (i = 0; i < 20; i++) { + digest[i] = + (unsigned char)((context->state[i >> 2] >> ((3 - (i & 3)) * 8)) & + 255); + } + /* Wipe variables */ + memset(context, '\0', sizeof(*context)); + memset(&finalcount, '\0', sizeof(finalcount)); +} + +void SHA1(char *hash_out, const char *str, uint32_t len) { + SHA1_CTX ctx; + unsigned int ii; + + SHA1Init(&ctx); + for (ii = 0; ii < len; ii += 1) + SHA1Update(&ctx, (const unsigned char *)str + ii, 1); + SHA1Final((unsigned char *)hash_out, &ctx); +} diff --git a/sha1.h b/sha1.h new file mode 100644 index 0000000..b11c73f --- /dev/null +++ b/sha1.h @@ -0,0 +1,36 @@ +#ifndef _CRDT_SHA1_H +#define _CRDT_SHA1_H + +/* + SHA-1 in C + By Steve Reid + 100% Public Domain + */ + +#include + +#if defined(__cplusplus) +extern "C" { +#endif + +typedef struct { + uint32_t state[5]; + uint32_t count[2]; + unsigned char buffer[64]; +} SHA1_CTX; + +void SHA1Transform(uint32_t state[5], const unsigned char buffer[64]); + +void SHA1Init(SHA1_CTX *context); + +void SHA1Update(SHA1_CTX *context, const unsigned char *data, uint32_t len); + +void SHA1Final(unsigned char digest[20], SHA1_CTX *context); + +void SHA1(char *hash_out, const char *str, uint32_t len); + +#if defined(__cplusplus) +} +#endif + +#endif /* _CRDT_SHA1_H */ diff --git a/test_counter.c b/test_counter.c index f8a5e88..15bcef9 100644 --- a/test_counter.c +++ b/test_counter.c @@ -1,6 +1,7 @@ #include "arena.h" #include "clientid.h" #include "counter.h" +#include "elementid.h" #include "test_util.h" // Build a ClientId fixture from a single byte (rest zero). Keeps tests @@ -11,9 +12,29 @@ static ClientId cid(uint8_t first_byte) { return clientid_from_bytes(b); } +static ElementId eid(uint64_t hi, uint64_t lo) { + uint8_t b[16]; + for (int i = 0; i < 8; i++) { + b[i] = (uint8_t)((hi >> ((7 - i) * 8)) & 0xff); + b[8 + i] = (uint8_t)((lo >> ((7 - i) * 8)) & 0xff); + } + return elementid_from_bytes(b); +} + +// Default id for tests that don't care about identity. +static ElementId default_id(void) { return eid(0xFF, 0); } + static Counter *fresh(void) { Arena *arena = arena_create(); - return counter_create(arena); + return counter_create(arena, default_id()); +} + +TEST(counter_create_stores_id) { + Arena *a = arena_create(); + ElementId id = eid(7, 42); + Counter *c = counter_create(a, id); + ASSERT(elementid_eq(counter_id(c), id) == true); + arena_destroy(a); } // --- local operations (single replica) --- @@ -243,7 +264,7 @@ TEST(local_inc_after_merge_accumulates) { TEST(clone_empty_counter_reads_zero) { Arena *as = arena_create(); Arena *ad = arena_create(); - Counter *src = counter_create(as); + Counter *src = counter_create(as, default_id()); Counter *clone = counter_clone(ad, src); ASSERT(clone != NULL); ASSERT(clone != src); @@ -252,10 +273,23 @@ TEST(clone_empty_counter_reads_zero) { arena_destroy(ad); } +// Clone preserves the source's id. Cloned element represents the same +// logical element, just materialized in a different arena. +TEST(clone_preserves_id) { + Arena *as = arena_create(); + Arena *ad = arena_create(); + ElementId id = eid(7, 42); + Counter *src = counter_create(as, id); + Counter *clone = counter_clone(ad, src); + ASSERT(elementid_eq(counter_id(clone), id) == true); + arena_destroy(as); + arena_destroy(ad); +} + TEST(clone_preserves_per_client_tallies) { Arena *as = arena_create(); Arena *ad = arena_create(); - Counter *src = counter_create(as); + Counter *src = counter_create(as, default_id()); counter_inc(src, cid(1), 5); counter_inc(src, cid(2), 3); counter_dec(src, cid(1), 2); @@ -271,7 +305,7 @@ TEST(clone_preserves_per_client_tallies) { TEST(clone_survives_src_arena_destroy) { Arena *as = arena_create(); Arena *ad = arena_create(); - Counter *src = counter_create(as); + Counter *src = counter_create(as, default_id()); counter_inc(src, cid(1), 5); counter_inc(src, cid(2), 3); Counter *clone = counter_clone(ad, src); @@ -284,7 +318,7 @@ TEST(clone_survives_src_arena_destroy) { TEST(clone_independent_of_src) { Arena *as = arena_create(); Arena *ad = arena_create(); - Counter *src = counter_create(as); + Counter *src = counter_create(as, default_id()); counter_inc(src, cid(1), 5); Counter *clone = counter_clone(ad, src); counter_inc(src, cid(1), 100); // src now 105 @@ -296,6 +330,7 @@ TEST(clone_independent_of_src) { } int main(void) { + RUN(counter_create_stores_id); RUN(empty_reads_zero); RUN(single_inc); RUN(inc_then_dec_nets); @@ -314,6 +349,7 @@ int main(void) { RUN(local_inc_after_merge_accumulates); RUN(clone_empty_counter_reads_zero); + RUN(clone_preserves_id); RUN(clone_preserves_per_client_tallies); RUN(clone_survives_src_arena_destroy); RUN(clone_independent_of_src); diff --git a/test_element.c b/test_element.c index a9129b5..87bff3e 100644 --- a/test_element.c +++ b/test_element.c @@ -2,6 +2,7 @@ #include "clientid.h" #include "counter.h" #include "element.h" +#include "elementid.h" #include "map.h" #include "register.h" #include "scalar.h" @@ -17,6 +18,17 @@ static ClientId cid(uint8_t first_byte) { return clientid_from_bytes(b); } +static ElementId eid(uint64_t hi, uint64_t lo) { + uint8_t b[16]; + for (int i = 0; i < 8; i++) { + b[i] = (uint8_t)((hi >> ((7 - i) * 8)) & 0xff); + b[8 + i] = (uint8_t)((lo >> ((7 - i) * 8)) & 0xff); + } + return elementid_from_bytes(b); +} + +static ElementId default_id(void) { return eid(0xFF, 0); } + static Stamp stmp(uint64_t lamport, uint8_t client_first_byte) { return (Stamp){.lamport = lamport, .client_id = cid(client_first_byte)}; } @@ -31,7 +43,7 @@ TEST(scalar_constructor_sets_kind_and_value) { TEST(register_constructor_sets_kind_and_pointer) { Arena *a = arena_create(); - Register *r = register_create(a, scalar_int(1), stmp(1, 1)); + Register *r = register_create(a, default_id(), scalar_int(1), stmp(1, 1)); Element e = element_register(r); ASSERT_EQ(element_kind(e), ELEMENT_REGISTER); ASSERT(e.as.reg == r); @@ -40,7 +52,7 @@ TEST(register_constructor_sets_kind_and_pointer) { TEST(counter_constructor_sets_kind_and_pointer) { Arena *a = arena_create(); - Counter *c = counter_create(a); + Counter *c = counter_create(a, default_id()); Element e = element_counter(c); ASSERT_EQ(element_kind(e), ELEMENT_COUNTER); ASSERT(e.as.counter == c); @@ -49,7 +61,7 @@ TEST(counter_constructor_sets_kind_and_pointer) { TEST(map_constructor_sets_kind_and_pointer) { Arena *a = arena_create(); - Map *m = map_create(a); + Map *m = map_create(a, default_id()); Element e = element_map(m); ASSERT_EQ(element_kind(e), ELEMENT_MAP); ASSERT(e.as.map == m); @@ -79,8 +91,10 @@ TEST(kind_name_map) { TEST(merge_register_takes_newer_value) { Arena *ad = arena_create(); Arena *as = arena_create(); - Register *dst = register_create(ad, scalar_int(10), stmp(1, 1)); - Register *src = register_create(as, scalar_int(20), stmp(5, 1)); + Register *dst = + register_create(ad, default_id(), scalar_int(10), stmp(1, 1)); + Register *src = + register_create(as, default_id(), scalar_int(20), stmp(5, 1)); element_merge(element_register(dst), element_register(src)); @@ -92,8 +106,8 @@ TEST(merge_register_takes_newer_value) { TEST(merge_counter_unions_clients) { Arena *ad = arena_create(); Arena *as = arena_create(); - Counter *dst = counter_create(ad); - Counter *src = counter_create(as); + Counter *dst = counter_create(ad, default_id()); + Counter *src = counter_create(as, default_id()); counter_inc(dst, cid(1), 5); counter_inc(src, cid(2), 3); @@ -107,8 +121,8 @@ TEST(merge_counter_unions_clients) { TEST(merge_map_takes_newer_slot) { Arena *ad = arena_create(); Arena *as = arena_create(); - Map *dst = map_create(ad); - Map *src = map_create(as); + Map *dst = map_create(ad, default_id()); + Map *src = map_create(as, default_id()); const uint8_t *k = (const uint8_t *)"k"; size_t klen = 1; @@ -128,8 +142,10 @@ TEST(merge_map_takes_newer_slot) { TEST(merge_register_does_not_mutate_src) { Arena *ad = arena_create(); Arena *as = arena_create(); - Register *dst = register_create(ad, scalar_int(99), stmp(10, 1)); - Register *src = register_create(as, scalar_int(7), stmp(1, 1)); + Register *dst = + register_create(ad, default_id(), scalar_int(99), stmp(10, 1)); + Register *src = + register_create(as, default_id(), scalar_int(7), stmp(1, 1)); element_merge(element_register(dst), element_register(src)); @@ -141,8 +157,8 @@ TEST(merge_register_does_not_mutate_src) { TEST(merge_counter_does_not_mutate_src) { Arena *ad = arena_create(); Arena *as = arena_create(); - Counter *dst = counter_create(ad); - Counter *src = counter_create(as); + Counter *dst = counter_create(ad, default_id()); + Counter *src = counter_create(as, default_id()); counter_inc(src, cid(1), 3); element_merge(element_counter(dst), element_counter(src)); @@ -155,8 +171,8 @@ TEST(merge_counter_does_not_mutate_src) { TEST(merge_map_does_not_mutate_src) { Arena *ad = arena_create(); Arena *as = arena_create(); - Map *dst = map_create(ad); - Map *src = map_create(as); + Map *dst = map_create(ad, default_id()); + Map *src = map_create(as, default_id()); const uint8_t *k = (const uint8_t *)"k"; map_set(src, k, 1, element_scalar(scalar_int(7)), stmp(1, 1)); @@ -172,7 +188,7 @@ TEST(merge_map_does_not_mutate_src) { TEST(round_trip_via_kind_and_payload) { Arena *a = arena_create(); - Counter *c = counter_create(a); + Counter *c = counter_create(a, default_id()); Element e = element_counter(c); ASSERT_EQ(element_kind(e), ELEMENT_COUNTER); ASSERT(e.as.counter == c); @@ -212,7 +228,8 @@ TEST(clone_scalar_string_owns_bytes_in_dst_arena) { TEST(clone_register_deep_copies_value) { Arena *as = arena_create(); Arena *ad = arena_create(); - Register *src = register_create(as, scalar_int(42), stmp(5, 1)); + Register *src = + register_create(as, default_id(), scalar_int(42), stmp(5, 1)); Element clone = element_clone(ad, element_register(src)); arena_destroy(as); ASSERT_EQ(element_kind(clone), ELEMENT_REGISTER); @@ -224,7 +241,7 @@ TEST(clone_register_deep_copies_value) { TEST(clone_counter_deep_copies_per_client_tallies) { Arena *as = arena_create(); Arena *ad = arena_create(); - Counter *src = counter_create(as); + Counter *src = counter_create(as, default_id()); counter_inc(src, cid(1), 5); counter_inc(src, cid(2), 3); Element clone = element_clone(ad, element_counter(src)); @@ -238,7 +255,7 @@ TEST(clone_counter_deep_copies_per_client_tallies) { TEST(clone_map_deep_copies_recursively) { Arena *as = arena_create(); Arena *ad = arena_create(); - Map *src = map_create(as); + Map *src = map_create(as, default_id()); map_set(src, (const void *)"a", 1, element_scalar(scalar_int(1)), stmp(1, 1)); map_set(src, (const void *)"b", 1, element_scalar(scalar_int(2)), @@ -259,7 +276,7 @@ TEST(clone_map_deep_copies_recursively) { TEST(clone_counter_independent_of_src) { Arena *as = arena_create(); Arena *ad = arena_create(); - Counter *src = counter_create(as); + Counter *src = counter_create(as, default_id()); counter_inc(src, cid(1), 5); Element clone = element_clone(ad, element_counter(src)); counter_inc(src, cid(1), 100); @@ -269,6 +286,47 @@ TEST(clone_counter_independent_of_src) { arena_destroy(ad); } +// --- element_clone preserves the source's id --- +// +// Cloned elements represent the same logical element (just materialized +// in a different arena), so ids carry over unchanged. + +TEST(clone_register_preserves_id) { + Arena *as = arena_create(); + Arena *ad = arena_create(); + ElementId id = eid(7, 42); + Register *src = register_create(as, id, scalar_int(1), stmp(1, 1)); + Element clone = element_clone(ad, element_register(src)); + ASSERT_EQ(element_kind(clone), ELEMENT_REGISTER); + ASSERT(elementid_eq(register_id(clone.as.reg), id) == true); + arena_destroy(as); + arena_destroy(ad); +} + +TEST(clone_counter_preserves_id) { + Arena *as = arena_create(); + Arena *ad = arena_create(); + ElementId id = eid(7, 42); + Counter *src = counter_create(as, id); + Element clone = element_clone(ad, element_counter(src)); + ASSERT_EQ(element_kind(clone), ELEMENT_COUNTER); + ASSERT(elementid_eq(counter_id(clone.as.counter), id) == true); + arena_destroy(as); + arena_destroy(ad); +} + +TEST(clone_map_preserves_id) { + Arena *as = arena_create(); + Arena *ad = arena_create(); + ElementId id = eid(7, 42); + Map *src = map_create(as, id); + Element clone = element_clone(ad, element_map(src)); + ASSERT_EQ(element_kind(clone), ELEMENT_MAP); + ASSERT(elementid_eq(map_id(clone.as.map), id) == true); + arena_destroy(as); + arena_destroy(ad); +} + int main(void) { RUN(scalar_constructor_sets_kind_and_value); RUN(register_constructor_sets_kind_and_pointer); @@ -297,5 +355,9 @@ int main(void) { RUN(clone_map_deep_copies_recursively); RUN(clone_counter_independent_of_src); + RUN(clone_register_preserves_id); + RUN(clone_counter_preserves_id); + RUN(clone_map_preserves_id); + TEST_SUMMARY(); } diff --git a/test_elementid.c b/test_elementid.c new file mode 100644 index 0000000..178a3ef --- /dev/null +++ b/test_elementid.c @@ -0,0 +1,214 @@ +#include "elementid.h" +#include "test_util.h" +#include + +// Local kind tags — mirror element.h's enum values. Kept here so this +// test stays independent of element.h. Values must match. +#define K_SCALAR 0 +#define K_REGISTER 1 +#define K_COUNTER 2 +#define K_MAP 3 + +// Test fixture: build an ElementId where bytes[0..7] = hi big-endian and +// bytes[8..15] = lo big-endian. Distinct (hi, lo) pairs produce distinct +// raw byte arrays — convenient for tests that don't care about UUID +// validity, only about distinct-vs-equal. +static ElementId eid(uint64_t hi, uint64_t lo) { + uint8_t b[16]; + for (int i = 0; i < 8; i++) { + b[i] = (uint8_t)((hi >> ((7 - i) * 8)) & 0xff); + b[8 + i] = (uint8_t)((lo >> ((7 - i) * 8)) & 0xff); + } + return elementid_from_bytes(b); +} + +// --- construction --- + +TEST(from_bytes_round_trips) { + uint8_t b[16] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}; + ElementId id = elementid_from_bytes(b); + for (int i = 0; i < 16; i++) { + ASSERT_EQ(id.uuid.bytes[i], b[i]); + } +} + +// --- root sentinel --- + +TEST(root_is_stable) { + ElementId r1 = elementid_root(); + ElementId r2 = elementid_root(); + ASSERT(elementid_eq(r1, r2) == true); +} + +TEST(root_is_all_zero) { + ElementId r = elementid_root(); + for (int i = 0; i < 16; i++) { + ASSERT_EQ(r.uuid.bytes[i], 0); + } +} + +TEST(root_distinct_from_regular_ids) { + ElementId r = elementid_root(); + ASSERT(elementid_eq(r, eid(1, 0)) == false); + ASSERT(elementid_eq(r, eid(0, 1)) == false); +} + +// --- equality --- + +TEST(eq_same) { ASSERT(elementid_eq(eid(5, 100), eid(5, 100)) == true); } + +TEST(eq_different_hi) { + ASSERT(elementid_eq(eid(5, 100), eid(6, 100)) == false); +} + +TEST(eq_different_lo) { + ASSERT(elementid_eq(eid(5, 100), eid(5, 101)) == false); +} + +// --- ordering (lexicographic over the 16 bytes) --- + +TEST(cmp_equal_returns_zero) { + ASSERT_EQ(elementid_cmp(eid(5, 100), eid(5, 100)), 0); +} + +TEST(cmp_lower_hi_less) { ASSERT(elementid_cmp(eid(5, 100), eid(6, 0)) < 0); } + +TEST(cmp_higher_hi_greater) { + ASSERT(elementid_cmp(eid(6, 0), eid(5, 100)) > 0); +} + +TEST(cmp_hi_dominates_lo) { + // a has smaller hi but huge lo; b has bigger hi but lo 0. + ASSERT(elementid_cmp(eid(1, UINT64_MAX), eid(2, 0)) < 0); +} + +TEST(cmp_anti_symmetric) { + ElementId a = eid(1, 100); + ElementId b = eid(2, 100); + int ab = elementid_cmp(a, b); + int ba = elementid_cmp(b, a); + ASSERT((ab < 0 && ba > 0) || (ab > 0 && ba < 0)); +} + +TEST(cmp_transitive) { + ElementId a = eid(5, 1); + ElementId b = eid(5, 2); + ElementId c = eid(5, 3); + ASSERT(elementid_cmp(a, b) < 0); + ASSERT(elementid_cmp(b, c) < 0); + ASSERT(elementid_cmp(a, c) < 0); +} + +// --- derive (UUID v5) --- +// +// elementid_derive is the convergent-creation path: two replicas calling +// it with matching (parent, key, kind) must land on the same ElementId. +// All four inputs are sensitive to differences. + +TEST(derive_is_deterministic) { + ElementId parent = eid(7, 42); + ElementId a = elementid_derive(parent, "votes", 5, K_COUNTER); + ElementId b = elementid_derive(parent, "votes", 5, K_COUNTER); + ASSERT(elementid_eq(a, b) == true); +} + +TEST(derive_different_keys_distinct) { + ElementId parent = eid(7, 42); + ASSERT(elementid_eq(elementid_derive(parent, "a", 1, K_COUNTER), + elementid_derive(parent, "b", 1, K_COUNTER)) == false); +} + +TEST(derive_different_parents_distinct) { + ElementId p1 = eid(7, 42); + ElementId p2 = eid(7, 43); + ASSERT(elementid_eq(elementid_derive(p1, "k", 1, K_COUNTER), + elementid_derive(p2, "k", 1, K_COUNTER)) == false); +} + +// Kind-in-derive: same parent + same key + different kind must produce +// distinct ids. This is what lets map_register("x") and map_counter("x") +// coexist as distinct logical elements. +TEST(derive_different_kinds_distinct) { + ElementId parent = eid(7, 42); + ElementId c = elementid_derive(parent, "x", 1, K_COUNTER); + ElementId r = elementid_derive(parent, "x", 1, K_REGISTER); + ElementId m = elementid_derive(parent, "x", 1, K_MAP); + ASSERT(elementid_eq(c, r) == false); + ASSERT(elementid_eq(c, m) == false); + ASSERT(elementid_eq(r, m) == false); +} + +TEST(derive_distinct_from_root) { + ElementId d = elementid_derive(elementid_root(), "k", 1, K_COUNTER); + ASSERT(elementid_eq(d, elementid_root()) == false); +} + +// Binary-safe: keys differing only past an embedded NUL must produce +// distinct ids. +TEST(derive_binary_safe_keys) { + ElementId parent = eid(7, 42); + uint8_t k1[3] = {0x01, 0x00, 0x02}; + uint8_t k2[3] = {0x01, 0x00, 0x03}; + ASSERT(elementid_eq(elementid_derive(parent, k1, sizeof k1, K_COUNTER), + elementid_derive(parent, k2, sizeof k2, K_COUNTER)) == + false); +} + +TEST(derive_empty_key_deterministic_and_distinct) { + ElementId parent = eid(7, 42); + ElementId e1 = elementid_derive(parent, "", 0, K_COUNTER); + ElementId e2 = elementid_derive(parent, "", 0, K_COUNTER); + ElementId nonempty = elementid_derive(parent, "x", 1, K_COUNTER); + ASSERT(elementid_eq(e1, e2) == true); + ASSERT(elementid_eq(e1, nonempty) == false); +} + +// --- UUID v5 format conformance --- +// +// Per RFC 4122 §4.3 the result of v5 derivation must carry version=5 +// in the high nibble of byte 6 and variant=10xx in the high two bits of +// byte 8. Any client / debugger that parses the output as a UUID relies +// on these bits. + +TEST(derive_sets_version_5) { + ElementId d = elementid_derive(eid(7, 42), "k", 1, K_COUNTER); + ASSERT_EQ((d.uuid.bytes[6] & 0xf0) >> 4, 5); +} + +TEST(derive_sets_variant_rfc4122) { + ElementId d = elementid_derive(eid(7, 42), "k", 1, K_COUNTER); + // Variant is the high two bits of byte 8 == 10 + ASSERT_EQ((d.uuid.bytes[8] & 0xc0), 0x80); +} + +int main(void) { + RUN(from_bytes_round_trips); + + RUN(root_is_stable); + RUN(root_is_all_zero); + RUN(root_distinct_from_regular_ids); + + RUN(eq_same); + RUN(eq_different_hi); + RUN(eq_different_lo); + + RUN(cmp_equal_returns_zero); + RUN(cmp_lower_hi_less); + RUN(cmp_higher_hi_greater); + RUN(cmp_hi_dominates_lo); + RUN(cmp_anti_symmetric); + RUN(cmp_transitive); + + RUN(derive_is_deterministic); + RUN(derive_different_keys_distinct); + RUN(derive_different_parents_distinct); + RUN(derive_different_kinds_distinct); + RUN(derive_distinct_from_root); + RUN(derive_binary_safe_keys); + RUN(derive_empty_key_deterministic_and_distinct); + RUN(derive_sets_version_5); + RUN(derive_sets_variant_rfc4122); + + TEST_SUMMARY(); +} diff --git a/test_map.c b/test_map.c index 24711c2..e9cfdf3 100644 --- a/test_map.c +++ b/test_map.c @@ -2,6 +2,7 @@ #include "clientid.h" #include "counter.h" #include "element.h" +#include "elementid.h" #include "map.h" #include "register.h" #include "scalar.h" @@ -16,6 +17,18 @@ static ClientId cid(uint8_t first_byte) { return clientid_from_bytes(b); } +static ElementId eid(uint64_t hi, uint64_t lo) { + uint8_t b[16]; + for (int i = 0; i < 8; i++) { + b[i] = (uint8_t)((hi >> ((7 - i) * 8)) & 0xff); + b[8 + i] = (uint8_t)((lo >> ((7 - i) * 8)) & 0xff); + } + return elementid_from_bytes(b); +} + +// Default id for tests where the parent Map's identity does not matter. +static ElementId default_id(void) { return eid(0xFF, 0); } + static Stamp stmp(uint64_t lamport, uint8_t client_first_byte) { return (Stamp){.lamport = lamport, .client_id = cid(client_first_byte)}; } @@ -29,7 +42,15 @@ static Stamp stmp(uint64_t lamport, uint8_t client_first_byte) { static Map *fresh(void) { Arena *arena = arena_create(); - return map_create(arena); + return map_create(arena, default_id()); +} + +TEST(map_create_stores_id) { + Arena *a = arena_create(); + ElementId id = eid(7, 42); + Map *m = map_create(a, id); + ASSERT(elementid_eq(map_id(m), id) == true); + arena_destroy(a); } #define ASSERT_SCALAR_EQ(out, expected) \ @@ -231,8 +252,8 @@ TEST(size_recovers_on_resurrect) { TEST(set_counter_then_get_returns_element_counter) { Arena *ar = arena_create(); - Map *m = map_create(ar); - Counter *c = counter_create(ar); + Map *m = map_create(ar, default_id()); + Counter *c = counter_create(ar, default_id()); counter_inc(c, cid(1), 5); map_set(m, SK("votes"), element_counter(c), stmp(1, 1)); @@ -245,8 +266,8 @@ TEST(set_counter_then_get_returns_element_counter) { TEST(set_register_then_get_returns_element_register) { Arena *ar = arena_create(); - Map *m = map_create(ar); - Register *r = register_create(ar, scalar_int(7), stmp(1, 1)); + Map *m = map_create(ar, default_id()); + Register *r = register_create(ar, default_id(), scalar_int(7), stmp(1, 1)); map_set(m, SK("title"), element_register(r), stmp(1, 1)); Element out; @@ -258,8 +279,8 @@ TEST(set_register_then_get_returns_element_register) { TEST(set_nested_map_then_get_returns_element_map) { Arena *ar = arena_create(); - Map *outer = map_create(ar); - Map *inner = map_create(ar); + Map *outer = map_create(ar, default_id()); + Map *inner = map_create(ar, default_id()); map_set(inner, SK("a"), EI(1), stmp(1, 1)); map_set(outer, SK("child"), element_map(inner), stmp(1, 1)); @@ -275,8 +296,8 @@ TEST(set_nested_map_then_get_returns_element_map) { // --- merge (two replicas, scalar slots) --- TEST(merge_disjoint_keys_unions) { - Map *a = map_create(arena_create()); - Map *b = map_create(arena_create()); + Map *a = map_create(arena_create(), default_id()); + Map *b = map_create(arena_create(), default_id()); map_set(a, SK("x"), EI(1), stmp(1, 1)); map_set(b, SK("y"), EI(2), stmp(1, 2)); @@ -290,8 +311,8 @@ TEST(merge_disjoint_keys_unions) { } TEST(merge_same_key_newer_wins) { - Map *a = map_create(arena_create()); - Map *b = map_create(arena_create()); + Map *a = map_create(arena_create(), default_id()); + Map *b = map_create(arena_create(), default_id()); map_set(a, SK("k"), EI(10), stmp(1, 1)); map_set(b, SK("k"), EI(20), stmp(2, 2)); @@ -302,8 +323,8 @@ TEST(merge_same_key_newer_wins) { } TEST(merge_src_older_loses) { - Map *a = map_create(arena_create()); - Map *b = map_create(arena_create()); + Map *a = map_create(arena_create(), default_id()); + Map *b = map_create(arena_create(), default_id()); map_set(a, SK("k"), EI(20), stmp(5, 1)); map_set(b, SK("k"), EI(10), stmp(2, 2)); @@ -314,8 +335,8 @@ TEST(merge_src_older_loses) { } TEST(merge_delete_beats_older_set) { - Map *a = map_create(arena_create()); - Map *b = map_create(arena_create()); + Map *a = map_create(arena_create(), default_id()); + Map *b = map_create(arena_create(), default_id()); map_set(a, SK("k"), EI(10), stmp(1, 1)); map_delete(b, SK("k"), stmp(5, 1)); @@ -325,8 +346,8 @@ TEST(merge_delete_beats_older_set) { } TEST(merge_set_beats_older_delete) { - Map *a = map_create(arena_create()); - Map *b = map_create(arena_create()); + Map *a = map_create(arena_create(), default_id()); + Map *b = map_create(arena_create(), default_id()); map_delete(a, SK("k"), stmp(1, 1)); map_set(b, SK("k"), EI(42), stmp(5, 1)); @@ -337,14 +358,14 @@ TEST(merge_set_beats_older_delete) { } TEST(merge_commutative) { - Map *a1 = map_create(arena_create()); - Map *b1 = map_create(arena_create()); + Map *a1 = map_create(arena_create(), default_id()); + Map *b1 = map_create(arena_create(), default_id()); map_set(a1, SK("k"), EI(10), stmp(5, 1)); map_set(b1, SK("k"), EI(20), stmp(5, 2)); map_merge(a1, b1); - Map *a2 = map_create(arena_create()); - Map *b2 = map_create(arena_create()); + Map *a2 = map_create(arena_create(), default_id()); + Map *b2 = map_create(arena_create(), default_id()); map_set(a2, SK("k"), EI(10), stmp(5, 1)); map_set(b2, SK("k"), EI(20), stmp(5, 2)); map_merge(b2, a2); @@ -359,8 +380,8 @@ TEST(merge_commutative) { } TEST(merge_idempotent) { - Map *a = map_create(arena_create()); - Map *b = map_create(arena_create()); + Map *a = map_create(arena_create(), default_id()); + Map *b = map_create(arena_create(), default_id()); map_set(a, SK("k"), EI(10), stmp(1, 1)); map_set(b, SK("k"), EI(20), stmp(2, 1)); @@ -377,18 +398,18 @@ TEST(merge_idempotent) { } TEST(merge_associative) { - Map *a = map_create(arena_create()); - Map *b = map_create(arena_create()); - Map *c = map_create(arena_create()); + Map *a = map_create(arena_create(), default_id()); + Map *b = map_create(arena_create(), default_id()); + Map *c = map_create(arena_create(), default_id()); map_set(a, SK("k"), EI(10), stmp(1, 1)); map_set(b, SK("k"), EI(20), stmp(2, 1)); map_set(c, SK("k"), EI(30), stmp(3, 1)); map_merge(a, b); map_merge(a, c); - Map *a2 = map_create(arena_create()); - Map *b2 = map_create(arena_create()); - Map *c2 = map_create(arena_create()); + Map *a2 = map_create(arena_create(), default_id()); + Map *b2 = map_create(arena_create(), default_id()); + Map *c2 = map_create(arena_create(), default_id()); map_set(a2, SK("k"), EI(10), stmp(1, 1)); map_set(b2, SK("k"), EI(20), stmp(2, 1)); map_set(c2, SK("k"), EI(30), stmp(3, 1)); @@ -405,8 +426,8 @@ TEST(merge_associative) { } TEST(merge_does_not_mutate_src) { - Map *a = map_create(arena_create()); - Map *b = map_create(arena_create()); + Map *a = map_create(arena_create(), default_id()); + Map *b = map_create(arena_create(), default_id()); map_set(a, SK("k"), EI(99), stmp(10, 1)); map_set(b, SK("k"), EI(7), stmp(1, 1)); @@ -417,8 +438,8 @@ TEST(merge_does_not_mutate_src) { } TEST(merge_copies_string_into_dst_arena) { - Map *a = map_create(arena_create()); - Map *b = map_create(arena_create()); + Map *a = map_create(arena_create(), default_id()); + Map *b = map_create(arena_create(), default_id()); uint8_t src_bytes[8]; memcpy(src_bytes, "hello", 5); @@ -437,8 +458,8 @@ TEST(merge_copies_string_into_dst_arena) { } TEST(merge_preserves_tombstone_against_older_set) { - Map *a = map_create(arena_create()); - Map *b = map_create(arena_create()); + Map *a = map_create(arena_create(), default_id()); + Map *b = map_create(arena_create(), default_id()); map_delete(a, SK("k"), stmp(5, 1)); map_set(b, SK("k"), EI(10), stmp(2, 1)); @@ -457,13 +478,13 @@ TEST(merge_same_kind_counter_recurses) { Arena *ad = arena_create(); Arena *as = arena_create(); - Map *dst = map_create(ad); - Counter *dc = counter_create(ad); + Map *dst = map_create(ad, default_id()); + Counter *dc = counter_create(ad, default_id()); counter_inc(dc, cid(1), 5); map_set(dst, SK("votes"), element_counter(dc), stmp(1, 1)); - Map *src = map_create(as); - Counter *sc = counter_create(as); + Map *src = map_create(as, default_id()); + Counter *sc = counter_create(as, default_id()); counter_inc(sc, cid(2), 3); map_set(src, SK("votes"), element_counter(sc), stmp(10, 1)); @@ -482,12 +503,14 @@ TEST(merge_same_kind_register_recurses) { Arena *ad = arena_create(); Arena *as = arena_create(); - Map *dst = map_create(ad); - Register *dr = register_create(ad, scalar_int(10), stmp(1, 1)); + Map *dst = map_create(ad, default_id()); + Register *dr = + register_create(ad, default_id(), scalar_int(10), stmp(1, 1)); map_set(dst, SK("title"), element_register(dr), stmp(1, 1)); - Map *src = map_create(as); - Register *sr = register_create(as, scalar_int(20), stmp(5, 1)); + Map *src = map_create(as, default_id()); + Register *sr = + register_create(as, default_id(), scalar_int(20), stmp(5, 1)); map_set(src, SK("title"), element_register(sr), stmp(1, 1)); map_merge(dst, src); @@ -505,13 +528,13 @@ TEST(merge_same_kind_nested_map_recurses) { Arena *ad = arena_create(); Arena *as = arena_create(); - Map *dst = map_create(ad); - Map *di = map_create(ad); + Map *dst = map_create(ad, default_id()); + Map *di = map_create(ad, default_id()); map_set(di, SK("a"), EI(1), stmp(1, 1)); map_set(dst, SK("child"), element_map(di), stmp(1, 1)); - Map *src = map_create(as); - Map *si = map_create(as); + Map *src = map_create(as, default_id()); + Map *si = map_create(as, default_id()); map_set(si, SK("b"), EI(2), stmp(1, 2)); map_set(src, SK("child"), element_map(si), stmp(1, 1)); @@ -534,13 +557,13 @@ TEST(merge_same_kind_counter_does_not_mutate_src) { Arena *ad = arena_create(); Arena *as = arena_create(); - Map *dst = map_create(ad); - Counter *dc = counter_create(ad); + Map *dst = map_create(ad, default_id()); + Counter *dc = counter_create(ad, default_id()); counter_inc(dc, cid(1), 5); map_set(dst, SK("votes"), element_counter(dc), stmp(1, 1)); - Map *src = map_create(as); - Counter *sc = counter_create(as); + Map *src = map_create(as, default_id()); + Counter *sc = counter_create(as, default_id()); counter_inc(sc, cid(2), 3); map_set(src, SK("votes"), element_counter(sc), stmp(1, 1)); @@ -559,13 +582,13 @@ TEST(merge_same_kind_counter_advances_slot_stamp) { Arena *ad = arena_create(); Arena *as = arena_create(); - Map *dst = map_create(ad); - Counter *dc = counter_create(ad); + Map *dst = map_create(ad, default_id()); + Counter *dc = counter_create(ad, default_id()); counter_inc(dc, cid(1), 5); map_set(dst, SK("votes"), element_counter(dc), stmp(1, 1)); - Map *src = map_create(as); - Counter *sc = counter_create(as); + Map *src = map_create(as, default_id()); + Counter *sc = counter_create(as, default_id()); counter_inc(sc, cid(2), 3); map_set(src, SK("votes"), element_counter(sc), stmp(10, 1)); @@ -592,10 +615,10 @@ TEST(merge_same_kind_counter_advances_slot_stamp) { TEST(set_composite_displaces_scalar_at_lww) { Arena *ar = arena_create(); - Map *m = map_create(ar); + Map *m = map_create(ar, default_id()); map_set(m, SK("score"), EI(42), stmp(1, 1)); // scalar first - Counter *c = counter_create(ar); + Counter *c = counter_create(ar, default_id()); map_set(m, SK("score"), element_counter(c), stmp(5, 1)); // newer composite Element out; @@ -607,9 +630,9 @@ TEST(set_composite_displaces_scalar_at_lww) { TEST(set_scalar_displaces_composite_at_lww) { Arena *ar = arena_create(); - Map *m = map_create(ar); + Map *m = map_create(ar, default_id()); - Counter *c = counter_create(ar); + Counter *c = counter_create(ar, default_id()); map_set(m, SK("score"), element_counter(c), stmp(1, 1)); map_set(m, SK("score"), EI(42), stmp(5, 1)); @@ -621,11 +644,11 @@ TEST(set_scalar_displaces_composite_at_lww) { TEST(set_different_kind_composite_displaces_at_lww) { Arena *ar = arena_create(); - Map *m = map_create(ar); + Map *m = map_create(ar, default_id()); - Counter *c = counter_create(ar); + Counter *c = counter_create(ar, default_id()); map_set(m, SK("score"), element_counter(c), stmp(1, 1)); - Register *r = register_create(ar, scalar_int(42), stmp(5, 1)); + Register *r = register_create(ar, default_id(), scalar_int(42), stmp(5, 1)); map_set(m, SK("score"), element_register(r), stmp(5, 1)); Element out; @@ -640,9 +663,9 @@ TEST(set_different_kind_composite_displaces_at_lww) { TEST(merge_composite_src_wins_into_empty_slot_clones) { Arena *ad = arena_create(); Arena *as = arena_create(); - Map *dst = map_create(ad); - Map *src = map_create(as); - Counter *sc = counter_create(as); + Map *dst = map_create(ad, default_id()); + Map *src = map_create(as, default_id()); + Counter *sc = counter_create(as, default_id()); counter_inc(sc, cid(1), 5); map_set(src, SK("votes"), element_counter(sc), stmp(5, 1)); @@ -667,14 +690,14 @@ TEST(merge_composite_src_wins_into_empty_slot_clones) { TEST(merge_does_not_clone_when_src_loses_lww) { Arena *ad = arena_create(); Arena *as = arena_create(); - Map *dst = map_create(ad); - Map *src = map_create(as); + Map *dst = map_create(ad, default_id()); + Map *src = map_create(as, default_id()); // dst has newer scalar at "k". map_set(dst, SK("k"), EI(42), stmp(10, 1)); // src has big nested Counter at "k" with OLDER stamp — must lose LWW. - Counter *sc = counter_create(as); + Counter *sc = counter_create(as, default_id()); for (int i = 0; i < 50; i++) { counter_inc(sc, cid((uint8_t)(i + 1)), 1); } @@ -701,13 +724,14 @@ TEST(merge_kind_mismatch_clones_winner_into_dst) { Arena *ad = arena_create(); Arena *as = arena_create(); - Map *dst = map_create(ad); - Counter *dc = counter_create(ad); + Map *dst = map_create(ad, default_id()); + Counter *dc = counter_create(ad, default_id()); counter_inc(dc, cid(1), 5); map_set(dst, SK("x"), element_counter(dc), stmp(1, 1)); - Map *src = map_create(as); - Register *sr = register_create(as, scalar_int(42), stmp(10, 1)); + Map *src = map_create(as, default_id()); + Register *sr = + register_create(as, default_id(), scalar_int(42), stmp(10, 1)); map_set(src, SK("x"), element_register(sr), stmp(10, 1)); map_merge(dst, src); @@ -721,6 +745,48 @@ TEST(merge_kind_mismatch_clones_winner_into_dst) { arena_destroy(as); } +// Two replicas hold a Counter of the same kind at the same slot but with +// DIFFERENT ids. They are two distinct logical elements that happen to +// share a key — typically because the app bypassed the helper and used +// raw counter_create with hand-picked ids. map_merge must NOT recurse +// (which would silently union their tallies); it must take the LWW path +// and orphan one side. +TEST(merge_same_kind_different_id_uses_lww_not_recurse) { + Arena *ad = arena_create(); + Arena *as = arena_create(); + Map *dst = map_create(ad, default_id()); + Map *src = map_create(as, default_id()); + + // dst: distinct id, 5 increments under cid 1, older slot stamp. + Counter *dc = counter_create(ad, eid(7, 1)); + counter_inc(dc, cid(1), 5); + map_set(dst, SK("votes"), element_counter(dc), stmp(1, 1)); + + // src: DIFFERENT id, 3 increments under cid 2, newer slot stamp. + Counter *sc = counter_create(as, eid(7, 2)); + counter_inc(sc, cid(2), 3); + map_set(src, SK("votes"), element_counter(sc), stmp(5, 1)); + + map_merge(dst, src); + + Element out; + ASSERT(map_get(dst, SK("votes"), &out) == true); + ASSERT_EQ(element_kind(out), ELEMENT_COUNTER); + + // LWW (src wins on stamp 5 > 1) → dst holds a CLONE of src's Counter, + // not the unioned recursive-merge result. Recursive would read 8. + ASSERT_EQ(counter_read(out.as.counter), 3); + + // Clone's id is src's id, not dst's old id (dst's Counter is orphaned). + ASSERT(elementid_eq(counter_id(out.as.counter), eid(7, 2)) == true); + + // dst owns the clone in its arena — not src's pointer. + ASSERT(out.as.counter != sc); + + arena_destroy(ad); + arena_destroy(as); +} + // --- get-or-create helpers --- // // map_counter / map_register / map_map: install a composite at the given @@ -731,7 +797,7 @@ TEST(merge_kind_mismatch_clones_winner_into_dst) { TEST(map_counter_creates_and_installs_at_key) { Arena *ar = arena_create(); - Map *m = map_create(ar); + Map *m = map_create(ar, default_id()); Counter *c = map_counter(m, SK("votes"), stmp(1, 1)); ASSERT(c != NULL); @@ -745,7 +811,7 @@ TEST(map_counter_creates_and_installs_at_key) { TEST(map_counter_returns_same_pointer_on_repeat) { Arena *ar = arena_create(); - Map *m = map_create(ar); + Map *m = map_create(ar, default_id()); Counter *first = map_counter(m, SK("votes"), stmp(1, 1)); Counter *second = map_counter(m, SK("votes"), stmp(2, 1)); @@ -755,7 +821,7 @@ TEST(map_counter_returns_same_pointer_on_repeat) { TEST(map_register_creates_and_installs_at_key) { Arena *ar = arena_create(); - Map *m = map_create(ar); + Map *m = map_create(ar, default_id()); Register *r = map_register(m, SK("title"), scalar_int(42), stmp(1, 1)); ASSERT(r != NULL); @@ -770,7 +836,7 @@ TEST(map_register_creates_and_installs_at_key) { TEST(map_register_returns_same_pointer_on_repeat) { Arena *ar = arena_create(); - Map *m = map_create(ar); + Map *m = map_create(ar, default_id()); Register *first = map_register(m, SK("title"), scalar_int(1), stmp(1, 1)); // Second call's seed value is ignored — slot already exists. @@ -783,7 +849,7 @@ TEST(map_register_returns_same_pointer_on_repeat) { TEST(map_map_creates_and_installs_at_key) { Arena *ar = arena_create(); - Map *outer = map_create(ar); + Map *outer = map_create(ar, default_id()); Map *child = map_map(outer, SK("child"), stmp(1, 1)); ASSERT(child != NULL); @@ -797,7 +863,7 @@ TEST(map_map_creates_and_installs_at_key) { TEST(map_map_returns_same_pointer_on_repeat) { Arena *ar = arena_create(); - Map *outer = map_create(ar); + Map *outer = map_create(ar, default_id()); Map *first = map_map(outer, SK("child"), stmp(1, 1)); Map *second = map_map(outer, SK("child"), stmp(2, 1)); @@ -809,7 +875,7 @@ TEST(map_map_returns_same_pointer_on_repeat) { // the kind via LWW and return a fresh composite. TEST(map_register_after_map_counter_flips_kind_via_lww) { Arena *ar = arena_create(); - Map *m = map_create(ar); + Map *m = map_create(ar, default_id()); Counter *c = map_counter(m, SK("score"), stmp(1, 1)); ASSERT(c != NULL); @@ -834,7 +900,7 @@ TEST(map_register_after_map_counter_flips_kind_via_lww) { // just isn't reachable from the slot. TEST(map_helper_losing_stamp_returns_detached_and_keeps_slot) { Arena *ar = arena_create(); - Map *m = map_create(ar); + Map *m = map_create(ar, default_id()); Counter *c = map_counter(m, SK("score"), stmp(10, 1)); ASSERT(c != NULL); @@ -857,8 +923,8 @@ TEST(map_helper_losing_stamp_returns_detached_and_keeps_slot) { TEST(map_counter_cross_replica_merge_recurses) { Arena *ad = arena_create(); Arena *as = arena_create(); - Map *dst = map_create(ad); - Map *src = map_create(as); + Map *dst = map_create(ad, default_id()); + Map *src = map_create(as, default_id()); Counter *dc = map_counter(dst, SK("votes"), stmp(1, 1)); Counter *sc = map_counter(src, SK("votes"), stmp(1, 2)); @@ -875,12 +941,88 @@ TEST(map_counter_cross_replica_merge_recurses) { arena_destroy(as); } +// --- helper id derivation --- +// +// Helpers must derive ids deterministically from (parent_id, key, kind) +// so two replicas independently calling the same helper land on the same +// id. The composite's id_field is the only authoritative source of truth +// for "are these the same logical thing?" + +TEST(map_counter_derives_id_from_parent_key_kind) { + Arena *ar = arena_create(); + ElementId parent_id = eid(7, 42); + Map *m = map_create(ar, parent_id); + + Counter *c = map_counter(m, SK("votes"), stmp(1, 1)); + ElementId expected = + elementid_derive(parent_id, SK("votes"), (uint8_t)ELEMENT_COUNTER); + ASSERT(elementid_eq(counter_id(c), expected) == true); + arena_destroy(ar); +} + +TEST(map_register_derives_id_from_parent_key_kind) { + Arena *ar = arena_create(); + ElementId parent_id = eid(7, 42); + Map *m = map_create(ar, parent_id); + + Register *r = map_register(m, SK("title"), scalar_int(0), stmp(1, 1)); + ElementId expected = + elementid_derive(parent_id, SK("title"), (uint8_t)ELEMENT_REGISTER); + ASSERT(elementid_eq(register_id(r), expected) == true); + arena_destroy(ar); +} + +TEST(map_map_derives_id_from_parent_key_kind) { + Arena *ar = arena_create(); + ElementId parent_id = eid(7, 42); + Map *m = map_create(ar, parent_id); + + Map *child = map_map(m, SK("child"), stmp(1, 1)); + ElementId expected = + elementid_derive(parent_id, SK("child"), (uint8_t)ELEMENT_MAP); + ASSERT(elementid_eq(map_id(child), expected) == true); + arena_destroy(ar); +} + +// Two replicas with the same parent_id calling the same helper at the +// same key land on identical ids — the convergent-creation guarantee. +TEST(helpers_converge_across_replicas) { + Arena *aa = arena_create(); + Arena *ab = arena_create(); + ElementId shared_parent = eid(7, 42); + Map *map_a = map_create(aa, shared_parent); + Map *map_b = map_create(ab, shared_parent); + + Counter *ca = map_counter(map_a, SK("votes"), stmp(1, 1)); + Counter *cb = map_counter(map_b, SK("votes"), stmp(1, 2)); + + ASSERT(elementid_eq(counter_id(ca), counter_id(cb)) == true); + arena_destroy(aa); + arena_destroy(ab); +} + +// Different kinds at the same key derive DIFFERENT ids — that's how +// recursive merge distinguishes Counter@"x" from Register@"x" as +// independent logical elements. +TEST(helpers_at_same_key_different_kind_have_distinct_ids) { + Arena *ar = arena_create(); + Map *m = map_create(ar, eid(7, 42)); + + Counter *c = map_counter(m, SK("x"), stmp(1, 1)); + // Counter is installed in slot. map_register loses the LWW slot here + // (same stamp), so returns a DETACHED Register. Its id should still + // be derived from (parent_id, "x", REGISTER), distinct from c's id. + Register *r = map_register(m, SK("x"), scalar_int(0), stmp(1, 1)); + ASSERT(elementid_eq(counter_id(c), register_id(r)) == false); + arena_destroy(ar); +} + // --- map_clone: deep recursive copy into a target arena --- TEST(clone_empty_map_is_empty) { Arena *as = arena_create(); Arena *ad = arena_create(); - Map *src = map_create(as); + Map *src = map_create(as, default_id()); Map *clone = map_clone(ad, src); ASSERT(clone != NULL); ASSERT(clone != src); @@ -892,7 +1034,7 @@ TEST(clone_empty_map_is_empty) { TEST(clone_preserves_scalar_slots) { Arena *as = arena_create(); Arena *ad = arena_create(); - Map *src = map_create(as); + Map *src = map_create(as, default_id()); map_set(src, SK("a"), EI(1), stmp(1, 1)); map_set(src, SK("b"), ES("hi", 2), stmp(1, 1)); Map *clone = map_clone(ad, src); @@ -911,7 +1053,7 @@ TEST(clone_preserves_scalar_slots) { TEST(clone_survives_src_arena_destroy) { Arena *as = arena_create(); Arena *ad = arena_create(); - Map *src = map_create(as); + Map *src = map_create(as, default_id()); map_set(src, SK("k"), ES("hello", 5), stmp(1, 1)); Map *clone = map_clone(ad, src); arena_destroy(as); @@ -926,8 +1068,8 @@ TEST(clone_survives_src_arena_destroy) { TEST(clone_recurses_into_composite_slots) { Arena *as = arena_create(); Arena *ad = arena_create(); - Map *src = map_create(as); - Counter *sc = counter_create(as); + Map *src = map_create(as, default_id()); + Counter *sc = counter_create(as, default_id()); counter_inc(sc, cid(1), 5); map_set(src, SK("votes"), element_counter(sc), stmp(1, 1)); @@ -950,7 +1092,7 @@ TEST(clone_recurses_into_composite_slots) { TEST(clone_preserves_tombstones) { Arena *as = arena_create(); Arena *ad = arena_create(); - Map *src = map_create(as); + Map *src = map_create(as, default_id()); map_set(src, SK("k"), EI(1), stmp(1, 1)); map_delete(src, SK("k"), stmp(5, 1)); @@ -968,7 +1110,7 @@ TEST(clone_preserves_tombstones) { TEST(clone_independent_of_src) { Arena *as = arena_create(); Arena *ad = arena_create(); - Map *src = map_create(as); + Map *src = map_create(as, default_id()); map_set(src, SK("k"), EI(1), stmp(1, 1)); Map *clone = map_clone(ad, src); map_set(src, SK("k"), EI(99), stmp(5, 1)); @@ -992,10 +1134,10 @@ TEST(clone_independent_of_src) { TEST(clone_tombstone_does_not_recurse_into_stale_value) { Arena *as = arena_create(); Arena *ad = arena_create(); - Map *src = map_create(as); + Map *src = map_create(as, default_id()); // Big inner map under the to-be-tombstoned key. - Map *inner = map_create(as); + Map *inner = map_create(as, default_id()); for (int i = 0; i < 50; i++) { char k[16]; int n = snprintf(k, sizeof k, "k%d", i); @@ -1026,6 +1168,7 @@ TEST(clone_tombstone_does_not_recurse_into_stale_value) { } int main(void) { + RUN(map_create_stores_id); RUN(empty_get_returns_false); RUN(set_then_get); RUN(set_overwrites_with_newer_stamp); @@ -1079,6 +1222,7 @@ int main(void) { RUN(merge_composite_src_wins_into_empty_slot_clones); RUN(merge_does_not_clone_when_src_loses_lww); RUN(merge_kind_mismatch_clones_winner_into_dst); + RUN(merge_same_kind_different_id_uses_lww_not_recurse); RUN(map_counter_creates_and_installs_at_key); RUN(map_counter_returns_same_pointer_on_repeat); @@ -1090,6 +1234,12 @@ int main(void) { RUN(map_helper_losing_stamp_returns_detached_and_keeps_slot); RUN(map_counter_cross_replica_merge_recurses); + RUN(map_counter_derives_id_from_parent_key_kind); + RUN(map_register_derives_id_from_parent_key_kind); + RUN(map_map_derives_id_from_parent_key_kind); + RUN(helpers_converge_across_replicas); + RUN(helpers_at_same_key_different_kind_have_distinct_ids); + RUN(clone_empty_map_is_empty); RUN(clone_preserves_scalar_slots); RUN(clone_survives_src_arena_destroy); diff --git a/test_register.c b/test_register.c index fa71ad5..6205e3d 100644 --- a/test_register.c +++ b/test_register.c @@ -1,5 +1,6 @@ #include "arena.h" #include "clientid.h" +#include "elementid.h" #include "register.h" #include "scalar.h" #include "stamp.h" @@ -13,6 +14,17 @@ static ClientId cid(uint8_t first_byte) { return clientid_from_bytes(b); } +static ElementId eid(uint64_t hi, uint64_t lo) { + uint8_t b[16]; + for (int i = 0; i < 8; i++) { + b[i] = (uint8_t)((hi >> ((7 - i) * 8)) & 0xff); + b[8 + i] = (uint8_t)((lo >> ((7 - i) * 8)) & 0xff); + } + return elementid_from_bytes(b); +} + +static ElementId default_id(void) { return eid(0xFF, 0); } + // Build a Stamp from lamport + a ClientId's first byte. Tests stay readable. static Stamp stmp(uint64_t lamport, uint8_t client_first_byte) { return (Stamp){.lamport = lamport, .client_id = cid(client_first_byte)}; @@ -20,7 +32,26 @@ static Stamp stmp(uint64_t lamport, uint8_t client_first_byte) { static Register *fresh(Scalar value, Stamp stamp) { Arena *arena = arena_create(); - return register_create(arena, value, stamp); + return register_create(arena, default_id(), value, stamp); +} + +TEST(register_create_stores_id) { + Arena *a = arena_create(); + ElementId id = eid(7, 42); + Register *r = register_create(a, id, scalar_int(0), stmp(1, 1)); + ASSERT(elementid_eq(register_id(r), id) == true); + arena_destroy(a); +} + +TEST(register_clone_preserves_id) { + Arena *as = arena_create(); + Arena *ad = arena_create(); + ElementId id = eid(7, 42); + Register *src = register_create(as, id, scalar_int(42), stmp(1, 1)); + Register *clone = register_clone(ad, src); + ASSERT(elementid_eq(register_id(clone), id) == true); + arena_destroy(as); + arena_destroy(ad); } // --- create / read --- @@ -230,7 +261,8 @@ TEST(merge_copies_string_into_dst_arena) { TEST(clone_preserves_value) { Arena *as = arena_create(); Arena *ad = arena_create(); - Register *src = register_create(as, scalar_int(42), stmp(5, 1)); + Register *src = + register_create(as, default_id(), scalar_int(42), stmp(5, 1)); Register *clone = register_clone(ad, src); ASSERT(clone != NULL); ASSERT(clone != src); @@ -244,8 +276,9 @@ TEST(clone_preserves_value) { TEST(clone_string_survives_src_arena_destroy) { Arena *as = arena_create(); Arena *ad = arena_create(); - Register *src = register_create( - as, scalar_string((const uint8_t *)"hello", 5), stmp(1, 1)); + Register *src = + register_create(as, default_id(), + scalar_string((const uint8_t *)"hello", 5), stmp(1, 1)); Register *clone = register_clone(ad, src); arena_destroy(as); ASSERT(scalar_eq(register_read(clone), @@ -257,7 +290,8 @@ TEST(clone_string_survives_src_arena_destroy) { TEST(clone_independent_of_src) { Arena *as = arena_create(); Arena *ad = arena_create(); - Register *src = register_create(as, scalar_int(1), stmp(1, 1)); + Register *src = + register_create(as, default_id(), scalar_int(1), stmp(1, 1)); Register *clone = register_clone(ad, src); register_set(src, scalar_int(99), stmp(10, 1)); register_set(clone, scalar_int(7), stmp(10, 1)); @@ -272,7 +306,8 @@ TEST(clone_independent_of_src) { TEST(clone_preserves_stamp) { Arena *as = arena_create(); Arena *ad = arena_create(); - Register *src = register_create(as, scalar_int(10), stmp(5, 1)); + Register *src = + register_create(as, default_id(), scalar_int(10), stmp(5, 1)); Register *clone = register_clone(ad, src); register_set(clone, scalar_int(99), stmp(3, 1)); // older, must lose ASSERT(scalar_eq(register_read(clone), scalar_int(10))); @@ -281,6 +316,7 @@ TEST(clone_preserves_stamp) { } int main(void) { + RUN(register_create_stores_id); RUN(create_seeds_value); RUN(create_with_string); RUN(create_with_null); @@ -304,6 +340,7 @@ int main(void) { RUN(merge_does_not_mutate_src); RUN(merge_copies_string_into_dst_arena); + RUN(register_clone_preserves_id); RUN(clone_preserves_value); RUN(clone_string_survives_src_arena_destroy); RUN(clone_independent_of_src); diff --git a/test_sha1.c b/test_sha1.c new file mode 100644 index 0000000..7ceb441 --- /dev/null +++ b/test_sha1.c @@ -0,0 +1,118 @@ +#include "sha1.h" +#include "test_util.h" +#include +#include + +// Convert a 20-byte digest to a lowercase hex string of length 40 (+ NUL). +static void hex(const unsigned char digest[20], char out[41]) { + static const char *h = "0123456789abcdef"; + for (int i = 0; i < 20; i++) { + out[i * 2] = h[(digest[i] >> 4) & 0x0f]; + out[i * 2 + 1] = h[digest[i] & 0x0f]; + } + out[40] = '\0'; +} + +// One-shot helper that wraps the Init/Update/Final sequence — keeps the +// test bodies compact and avoids relying on the `SHA1` one-shot whose +// signature uses char* for the digest output. +static void sha1_oneshot(const unsigned char *data, uint32_t len, + unsigned char out[20]) { + SHA1_CTX ctx; + SHA1Init(&ctx); + SHA1Update(&ctx, data, len); + SHA1Final(out, &ctx); +} + +// --- NIST FIPS 180-4 test vectors --- + +TEST(empty_string) { + unsigned char digest[20]; + sha1_oneshot((const unsigned char *)"", 0, digest); + char got[41]; + hex(digest, got); + ASSERT(strcmp(got, "da39a3ee5e6b4b0d3255bfef95601890afd80709") == 0); +} + +TEST(abc) { + unsigned char digest[20]; + sha1_oneshot((const unsigned char *)"abc", 3, digest); + char got[41]; + hex(digest, got); + ASSERT(strcmp(got, "a9993e364706816aba3e25717850c26c9cd0d89d") == 0); +} + +TEST(fips_two_block) { + const char *msg = + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"; + unsigned char digest[20]; + sha1_oneshot((const unsigned char *)msg, (uint32_t)strlen(msg), digest); + char got[41]; + hex(digest, got); + ASSERT(strcmp(got, "84983e441c3bd26ebaae4aa1f95129e5e54670f1") == 0); +} + +TEST(million_a) { + // Exactly 1,000,000 'a' chars, fed via streaming update. + SHA1_CTX ctx; + SHA1Init(&ctx); + unsigned char chunk[1000]; + memset(chunk, 'a', sizeof chunk); + for (int i = 0; i < 1000; i++) { + SHA1Update(&ctx, chunk, sizeof chunk); + } + unsigned char digest[20]; + SHA1Final(digest, &ctx); + char got[41]; + hex(digest, got); + ASSERT(strcmp(got, "34aa973cd4c4daa4f61eeb2bdbad27316534016f") == 0); +} + +// Streaming-update equivalence: feeding the same bytes in chunks must +// produce the same digest as the one-shot call. +TEST(streaming_matches_one_shot) { + const char *msg = "the quick brown fox jumps over the lazy dog"; + uint32_t len = (uint32_t)strlen(msg); + + unsigned char one_shot[20]; + sha1_oneshot((const unsigned char *)msg, len, one_shot); + + SHA1_CTX ctx; + SHA1Init(&ctx); + SHA1Update(&ctx, (const unsigned char *)msg, 5); + SHA1Update(&ctx, (const unsigned char *)msg + 5, 10); + SHA1Update(&ctx, (const unsigned char *)msg + 15, len - 15); + unsigned char streamed[20]; + SHA1Final(streamed, &ctx); + + ASSERT(memcmp(one_shot, streamed, 20) == 0); +} + +// Empty update calls must be a no-op — chunking with zero-length segments +// can happen in normal use and must not affect the digest. +TEST(empty_updates_are_noop) { + const char *msg = "abc"; + unsigned char expected[20]; + sha1_oneshot((const unsigned char *)msg, 3, expected); + + SHA1_CTX ctx; + SHA1Init(&ctx); + SHA1Update(&ctx, (const unsigned char *)"", 0); + SHA1Update(&ctx, (const unsigned char *)msg, 3); + SHA1Update(&ctx, (const unsigned char *)"", 0); + unsigned char got[20]; + SHA1Final(got, &ctx); + + ASSERT(memcmp(expected, got, 20) == 0); +} + +int main(void) { + RUN(empty_string); + RUN(abc); + RUN(fips_two_block); + RUN(million_a); + RUN(streaming_matches_one_shot); + RUN(empty_updates_are_noop); + + TEST_SUMMARY(); +} diff --git a/test_uuid.c b/test_uuid.c new file mode 100644 index 0000000..9b8b2eb --- /dev/null +++ b/test_uuid.c @@ -0,0 +1,203 @@ +#include "test_util.h" +#include "uuid.h" +#include +#include + +// --- format --- + +TEST(format_all_zero) { + uint8_t b[16] = {0}; + char s[37]; + uuid_format(b, s); + ASSERT(strcmp(s, "00000000-0000-0000-0000-000000000000") == 0); +} + +TEST(format_canonical_layout) { + uint8_t b[16] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10}; + char s[37]; + uuid_format(b, s); + ASSERT(strcmp(s, "01234567-89ab-cdef-fedc-ba9876543210") == 0); +} + +TEST(format_lowercase_hex) { + // High-nibble bytes must format with lowercase a-f, not uppercase. + uint8_t b[16] = {0}; + b[0] = 0xAB; + b[15] = 0xCD; + char s[37]; + uuid_format(b, s); + ASSERT(s[0] == 'a' && s[1] == 'b'); + ASSERT(s[34] == 'c' && s[35] == 'd'); +} + +// --- parse --- + +TEST(parse_round_trips_with_format) { + uint8_t in[16]; + for (int i = 0; i < 16; i++) + in[i] = (uint8_t)(i * 17); + char s[37]; + uuid_format(in, s); + uint8_t out[16]; + ASSERT(uuid_parse(s, out) == true); + ASSERT(memcmp(in, out, 16) == 0); +} + +TEST(parse_accepts_uppercase) { + uint8_t out[16]; + ASSERT(uuid_parse("AABBCCDD-EEFF-1122-3344-556677889900", out) == true); + ASSERT_EQ(out[0], 0xaa); + ASSERT_EQ(out[1], 0xbb); + ASSERT_EQ(out[15], 0x00); +} + +TEST(parse_rejects_short) { + uint8_t out[16]; + ASSERT(uuid_parse("01234567-89ab-cdef-fedc-ba98765432", out) == false); +} + +TEST(parse_rejects_missing_hyphen) { + uint8_t out[16]; + ASSERT(uuid_parse("01234567x89ab-cdef-fedc-ba9876543210", out) == false); +} + +TEST(parse_rejects_non_hex) { + uint8_t out[16]; + ASSERT(uuid_parse("0123456g-89ab-cdef-fedc-ba9876543210", out) == false); +} + +// uuid_parse contract: on failure, `out` is untouched. Probe a partial- +// parse failure (valid bytes for a while, then invalid hex deep in the +// string) and verify the entire `out` buffer kept its original sentinel +// bytes — not a partial mix of newly-parsed bytes and untouched bytes. +TEST(parse_failure_leaves_out_untouched) { + uint8_t out[16]; + memset(out, 0xAA, sizeof out); + // Valid first three groups, invalid hex 'z' inside the fourth group. + // A naive impl would write bytes 0..7 before bailing. + ASSERT(uuid_parse("01234567-89ab-cdef-fezc-ba9876543210", out) == false); + for (int i = 0; i < 16; i++) { + ASSERT_EQ(out[i], 0xAA); + } +} + +// --- v5 derivation --- + +TEST(v5_deterministic) { + uint8_t ns[16] = {0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, + 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8}; + const uint8_t name[] = "hello"; + UuidV5 a = uuid_v5(ns, name, sizeof name - 1); + UuidV5 b = uuid_v5(ns, name, sizeof name - 1); + ASSERT(memcmp(a.bytes, b.bytes, 16) == 0); +} + +TEST(v5_sets_version_5) { + uint8_t ns[16] = {0}; + UuidV5 out = uuid_v5(ns, (const uint8_t *)"x", 1); + ASSERT_EQ((out.bytes[6] & 0xf0) >> 4, 5); +} + +TEST(v5_sets_rfc4122_variant) { + uint8_t ns[16] = {0}; + UuidV5 out = uuid_v5(ns, (const uint8_t *)"x", 1); + ASSERT_EQ((out.bytes[8] & 0xc0), 0x80); +} + +TEST(v5_different_names_distinct) { + uint8_t ns[16] = {0}; + UuidV5 a = uuid_v5(ns, (const uint8_t *)"a", 1); + UuidV5 b = uuid_v5(ns, (const uint8_t *)"b", 1); + ASSERT(memcmp(a.bytes, b.bytes, 16) != 0); +} + +TEST(v5_different_namespaces_distinct) { + uint8_t ns1[16] = {0}; + uint8_t ns2[16] = {0}; + ns2[0] = 1; + UuidV5 a = uuid_v5(ns1, (const uint8_t *)"x", 1); + UuidV5 b = uuid_v5(ns2, (const uint8_t *)"x", 1); + ASSERT(memcmp(a.bytes, b.bytes, 16) != 0); +} + +// RFC 4122 Appendix B test vector: namespace = DNS namespace, name = +// "www.widgets.com" → 21f7f8de-8051-5b89-8680-0195ef798b6a. +TEST(v5_rfc_dns_vector) { + uint8_t dns_ns[16] = {0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, + 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8}; + UuidV5 out = uuid_v5(dns_ns, (const uint8_t *)"www.widgets.com", + strlen("www.widgets.com")); + char got[37]; + uuid_format(out.bytes, got); + ASSERT(strcmp(got, "21f7f8de-8051-5b89-8680-0195ef798b6a") == 0); +} + +// Streaming v5 must produce the same result as one-shot v5 over the +// concatenated name bytes. +TEST(v5_streaming_matches_one_shot) { + uint8_t ns[16] = {0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, + 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8}; + const uint8_t name[] = "the-quick-brown-fox"; + size_t len = sizeof name - 1; + + UuidV5 one_shot = uuid_v5(ns, name, len); + + UuidV5Ctx ctx; + uuid_v5_init(&ctx, ns); + uuid_v5_update(&ctx, name, 4); + uuid_v5_update(&ctx, name + 4, 6); + uuid_v5_update(&ctx, name + 10, len - 10); + UuidV5 streamed = uuid_v5_final(&ctx); + + ASSERT(memcmp(one_shot.bytes, streamed.bytes, 16) == 0); +} + +// --- version / variant bit helper --- + +TEST(set_version_and_variant_clears_existing) { + uint8_t b[16]; + memset(b, 0xff, 16); + uuid_set_version_and_variant(b, 5); + ASSERT_EQ((b[6] & 0xf0) >> 4, 5); + ASSERT_EQ((b[8] & 0xc0), 0x80); + // Other bytes untouched. + ASSERT_EQ(b[0], 0xff); + ASSERT_EQ(b[15], 0xff); +} + +TEST(set_version_and_variant_preserves_low_nibble) { + uint8_t b[16] = {0}; + b[6] = 0x0a; + b[8] = 0x33; + uuid_set_version_and_variant(b, 7); + ASSERT_EQ(b[6], 0x7a); // version=7, low nibble preserved + // byte 8: top bits set to 10, low 6 bits preserved (0x33 & 0x3f = 0x33) + ASSERT_EQ(b[8], (0x33 & 0x3f) | 0x80); +} + +int main(void) { + RUN(format_all_zero); + RUN(format_canonical_layout); + RUN(format_lowercase_hex); + + RUN(parse_round_trips_with_format); + RUN(parse_accepts_uppercase); + RUN(parse_rejects_short); + RUN(parse_rejects_missing_hyphen); + RUN(parse_rejects_non_hex); + RUN(parse_failure_leaves_out_untouched); + + RUN(v5_deterministic); + RUN(v5_sets_version_5); + RUN(v5_sets_rfc4122_variant); + RUN(v5_different_names_distinct); + RUN(v5_different_namespaces_distinct); + RUN(v5_rfc_dns_vector); + RUN(v5_streaming_matches_one_shot); + + RUN(set_version_and_variant_clears_existing); + RUN(set_version_and_variant_preserves_low_nibble); + + TEST_SUMMARY(); +} diff --git a/uuid.c b/uuid.c new file mode 100644 index 0000000..ae12aa0 --- /dev/null +++ b/uuid.c @@ -0,0 +1,100 @@ +#include "uuid.h" +#include "sha1.h" +#include + +void uuid_format(const uint8_t bytes[16], char out[37]) { + static const char *hex = "0123456789abcdef"; + int j = 0; + for (int i = 0; i < 16; i++) { + // Hyphens go between bytes 3|4, 5|6, 7|8, 9|10 — the canonical + // 8-4-4-4-12 grouping. Interleave during the write rather than + // stamping them in after, which would clobber hex digits. + if (i == 4 || i == 6 || i == 8 || i == 10) { + out[j++] = '-'; + } + out[j++] = hex[(bytes[i] >> 4) & 0xF]; + out[j++] = hex[bytes[i] & 0xF]; + } + out[36] = '\0'; +} + +static int hex_digit_to_int(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } else if (c >= 'a' && c <= 'f') { + return 10 + (c - 'a'); + } else if (c >= 'A' && c <= 'F') { + return 10 + (c - 'A'); + } else { + return -1; // Invalid hex digit + } +} + +bool uuid_parse(const char *s, uint8_t out[16]) { + // Validate length and hyphens at positions 8, 13, 18, 23. + if (strlen(s) != 36 || s[8] != '-' || s[13] != '-' || s[18] != '-' || + s[23] != '-') { + return false; + } + + // Parse into a local buffer first so a partial-parse failure leaves + // the caller's `out` untouched (header contract). + uint8_t buf[16]; + for (int i = 0; i < 16; i++) { + int hi = hex_digit_to_int( + s[i * 2 + (i >= 4) + (i >= 6) + (i >= 8) + (i >= 10)]); + int lo = hex_digit_to_int( + s[i * 2 + 1 + (i >= 4) + (i >= 6) + (i >= 8) + (i >= 10)]); + if (hi < 0 || lo < 0) { + return false; // Invalid hex digit + } + buf[i] = (hi << 4) | lo; + } + memcpy(out, buf, 16); + return true; +} + +void uuid_v5_init(UuidV5Ctx *ctx, const uint8_t namespace_bytes[16]) { + + SHA1Init(&ctx->sha1_ctx); + SHA1Update(&ctx->sha1_ctx, namespace_bytes, 16); +} + +// SHA1Update takes a uint32_t length, so size_t inputs above UINT32_MAX +// must be chunked to avoid silent truncation on 64-bit platforms. +void uuid_v5_update(UuidV5Ctx *ctx, const uint8_t *data, size_t len) { + while (len > 0) { + size_t chunk = len > UINT32_MAX ? UINT32_MAX : len; + SHA1Update(&ctx->sha1_ctx, data, (uint32_t)chunk); + data += chunk; + len -= chunk; + } +} + +UuidV5 uuid_v5_final(UuidV5Ctx *ctx) { + uint8_t digest[20]; + SHA1Final(digest, &ctx->sha1_ctx); + UuidV5 out; + memcpy(out.bytes, digest, 16); + // Version 5 = SHA-1 + version/variant bits per RFC 4122 §4.3. + // Set them here so streaming callers get a valid v5 UUID without + // having to set the bits themselves. + uuid_set_version_and_variant(out.bytes, 5); + return out; +} + +void uuid_set_version_and_variant(uint8_t bytes[16], uint8_t version) { + // Clear version bits (high nibble of byte 6) and set to `version`. + bytes[6] = (bytes[6] & 0x0F) | ((version & 0x0F) << 4); + // Clear variant bits (high two bits of byte 8) and set to RFC 4122 variant + // (10xx). + bytes[8] = (bytes[8] & 0x3F) | 0x80; +} + +UuidV5 uuid_v5(const uint8_t namespace_bytes[16], const uint8_t *name, + size_t name_len) { + UuidV5Ctx ctx; + uuid_v5_init(&ctx, namespace_bytes); + uuid_v5_update(&ctx, name, name_len); + return uuid_v5_final(&ctx); +} diff --git a/uuid.h b/uuid.h new file mode 100644 index 0000000..860d807 --- /dev/null +++ b/uuid.h @@ -0,0 +1,69 @@ +#ifndef _CRDT_UUID_H +#define _CRDT_UUID_H + +// UUID utilities — format, parse, v5 derivation. Shared by ClientId +// (UUID v7) and ElementId (UUID v5). Operates on raw 16-byte arrays +// rather than a typed Uuid wrapper, so ClientId and ElementId keep +// distinct typedefs for compile-time type-safety while still sharing +// these utilities. +// +// Strings use the canonical RFC 4122 8-4-4-4-12 format: +// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// lowercase hex, 36 chars + NUL. + +#include "sha1.h" +#include +#include +#include + +// --- format / parse --- + +// Write the canonical RFC 4122 string form into `out`. Always lowercase. +// `out` must be at least 37 bytes (36 + NUL). +void uuid_format(const uint8_t bytes[16], char out[37]); + +// Parse a canonical RFC 4122 string into 16 bytes. Accepts lowercase or +// uppercase hex; requires hyphens at positions 8, 13, 18, 23. Returns +// true on success, false on any format violation (length wrong, bad hex +// digit, missing hyphen). `out` is untouched on failure. +bool uuid_parse(const char *s, uint8_t out[16]); + +// --- version 5 (deterministic, SHA-1 over namespace || name) --- + +// Typed v5 output. 16 bytes, RFC 4122 layout, version=5 + RFC variant +// bits set. Pass-by-value is cheap (16 bytes). +typedef struct UuidV5 { + uint8_t bytes[16]; +} UuidV5; + +// One-shot v5 derive. digest = SHA-1(namespace || name), take first 16 +// bytes, then set version=5 in the high nibble of byte 6 and variant=10xx +// in the high two bits of byte 8 per RFC 4122 §4.1. +UuidV5 uuid_v5(const uint8_t namespace_bytes[16], const uint8_t *name, + size_t name_len); + +// Streaming v5 — for callers that build the `name` portion from multiple +// pieces without allocating a contiguous buffer. +typedef struct UuidV5Ctx { + // Opaque to callers; declared here only for stack allocation. + // Layout mirrors SHA1_CTX internally. + SHA1_CTX sha1_ctx; +} UuidV5Ctx; + +void uuid_v5_init(UuidV5Ctx *ctx, const uint8_t namespace_bytes[16]); +void uuid_v5_update(UuidV5Ctx *ctx, const uint8_t *data, size_t len); +UuidV5 uuid_v5_final(UuidV5Ctx *ctx); + +// --- bit-fiddle helpers --- +// +// Used by both v5 and v7 generators. Exposed publicly so app or test +// fixtures can construct UUIDs manually with the correct format bits. + +// Mask byte 6's version nibble to the low 4 bits of `version` (the high +// 4 bits are ignored; no range validation), and force the RFC 4122 +// variant (10xx in high two bits of byte 8). Mutates in place. Callers +// are responsible for passing a UUID version value defined by RFC 4122 +// (extended by RFC 9562); the helper does not enforce that. +void uuid_set_version_and_variant(uint8_t bytes[16], uint8_t version); + +#endif // _CRDT_UUID_H