Skip to content

Latest commit

 

History

History
856 lines (593 loc) · 28.1 KB

File metadata and controls

856 lines (593 loc) · 28.1 KB

IntToUuid: Bidirectional Integer-to-UUID Encoding

Specification Version 0.1.0

Abstract

This document specifies an algorithm for bidirectional encoding of a non-negative 64-bit integer identifier and an optional 32-bit integer namespace into a valid Universally Unique Identifier (UUID) as defined by RFC 9562 [RFC9562], using the Version 8 (custom) format. The encoding is deterministic, reversible, and produces non-sequential UUIDs that obscure the underlying integer values while guaranteeing uniqueness within a given namespace.

1. Introduction

Many applications use auto-incrementing integers as primary keys in relational databases. Exposing these identifiers directly in public APIs or URLs reveals information about the system: the total number of records, the rate of creation, and the ability to enumerate all records by iterating through sequential values. A common mitigation is to present a UUID as a proxy identifier in public-facing contexts.

This specification defines an algorithm that converts an integer identifier and an optional namespace integer into a UUID at runtime, without requiring the UUID to be stored, indexed, or otherwise persisted alongside the original integer. The conversion is fully reversible: given the UUID, the original integer values can be recovered. Within a given namespace, each integer produces a unique UUID, and each valid UUID decodes to exactly one integer pair.

The algorithm uses the UUID Version 8 format defined in RFC 9562 [RFC9562], Section 5.8, which is designated for experimental or vendor-specific use. The resulting UUIDs are structurally valid and will be recognized as Version 8 by any compliant UUID parser.

1.1. Design Goals

The algorithm is designed to satisfy the following properties:

(a) Determinism: The same (id, namespace) pair MUST always produce the same UUID.

(b) Reversibility: Any UUID produced by this algorithm MUST be decodable back to the original (id, namespace) pair.

(c) Uniqueness: Within a given namespace, distinct id values MUST produce distinct UUIDs. Across namespaces, distinct namespace values for the same id MUST produce distinct UUIDs.

(d) Non-sequentiality: Consecutive id values (e.g., 1, 2, 3) SHOULD produce UUIDs that appear unrelated to a casual observer.

(e) Integrity checking: The algorithm MUST detect and reject UUIDs that were not produced by this algorithm, even if they are otherwise valid Version 8 UUIDs.

1.2. Non-Goals

This algorithm does NOT provide:

  • Encryption. The integer values are encoded, not encrypted. A motivated party with knowledge of this specification can recover the original values from any UUID produced by the algorithm.

  • Cryptographic integrity. The checksum seed provides error detection, not tamper resistance.

  • Universal uniqueness. UUIDs produced by this algorithm are unique only within the scope of this encoding scheme. They are not globally unique across UUID generation methods.

2. Terminology

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.

The following terms are used throughout this specification:

id: A non-negative integer to be encoded. Referred to as the "identifier" or "id value".

namespace: A non-negative 32-bit integer that partitions the id space. Defaults to 0 when not specified.

seed: A 4-byte value derived from the hash of the packed id and namespace, with UUID Version and Variant bits embedded. Serves as both an encoding parameter and a checksum for decoding validation.

packed: The big-endian byte representation of an integer value.

XOR: The bitwise exclusive-or operation applied byte-by-byte to two byte strings of equal length.

||: Concatenation of byte strings.

3. Relationship to UUID Standards

3.1. RFC 9562 and RFC 4122

The UUID format was originally standardized in RFC 4122 [RFC4122], published in July 2005. RFC 4122 defined UUID versions 1 through 5 and established the variant and version field layout that remains in use today.

In May 2024, RFC 9562 [RFC9562] was published as a Standards Track document that obsoletes RFC 4122. RFC 9562 retains full backward compatibility with RFC 4122 and adds three new UUID versions:

Version 6: Reordered Gregorian time-based (Section 5.6) Version 7: Unix Epoch time-based (Section 5.7) Version 8: Custom, vendor-specific (Section 5.8)

Many existing UUID libraries and tools still reference RFC 4122 in their documentation and APIs. This does not affect interoperability: the binary format, variant field, and version field semantics are identical between the two RFCs. UUID libraries that predate RFC 9562 will correctly parse the variant and version fields of a Version 8 UUID, though they may report the version as "unknown" if they were written before Version 8 was standardized.

Implementors SHOULD reference RFC 9562 as the authoritative specification for UUID format and semantics.

3.2. UUID Version 8

RFC 9562, Section 5.8 defines Version 8 as follows:

UUIDv8 provides an RFC-compatible format for experimental or vendor-specific use cases. The only requirement is that the variant and version bits are set as specified in Sections 4.1 and 4.2 of RFC 9562.

The 128-bit UUID contains 6 bits reserved for the version (4 bits) and variant (2 bits) fields. The remaining 122 bits are available for application-defined content. This specification uses those 122 bits to encode the id, namespace, and seed values.

3.3. Version and Variant Bit Positions

Per RFC 9562, Section 4:

Version field: The 4 most significant bits of octet 6 (bits 48-51 of the UUID). For Version 8, these bits MUST be 0b1000 (decimal 8).

Variant field: The 2 most significant bits of octet 8 (bits 64-65 of the UUID). For the RFC 9562 variant, these bits MUST be 0b10.

In the standard UUID string representation, the version appears as the first hex digit of the third group, and the variant appears as the first hex digit of the fourth group (constrained to 8, 9, a, or b):

xxxxxxxx-xxxx-Vxxx-Nxxx-xxxxxxxxxxxx ^ ^ | variant (8/9/a/b) version (8)

4. Input Parameters

4.1. Identifier (id)

The id parameter is a non-negative integer with the following constraints:

Minimum value: 0 Maximum value: 9,223,372,036,854,775,807 (2^63 - 1)

The maximum corresponds to the largest value representable by a signed 64-bit integer, as used by the reference implementation's host language (PHP). Implementations in languages with native unsigned 64-bit integers MAY choose to support the full range of 0 to 2^64 - 1, but MUST support at least the range specified above for interoperability with the reference implementation.

The id is packed as an unsigned 64-bit big-endian integer for all internal operations, regardless of the host language's native integer representation.

4.2. Namespace

The namespace parameter is a non-negative integer with the following constraints:

Minimum value: 0 Maximum value: 4,294,967,295 (2^32 - 1)

When not specified, the namespace defaults to 0. The namespace is packed as an unsigned 32-bit big-endian integer.

5. Hash Function

5.1. Algorithm

All hashing in this specification uses the XXH3 algorithm, the 64-bit variant of xxHash version 3, as defined by the xxHash reference implementation [XXHASH].

Input: An arbitrary-length byte string Output: A 64-bit digest value Seed: Default (0) Secret: None (default)

5.2. Byte Order of Hash Output

The 64-bit hash digest MUST be serialized as an 8-byte string in big-endian (network) byte order before use in any byte-level operation defined by this specification.

This is a critical interoperability requirement. Different language bindings for xxHash may return the digest as a native integer, a byte array in platform-native order, or a hexadecimal string. Implementations MUST ensure that the 8-byte representation matches the big-endian serialization of the 64-bit integer digest.

5.3. Reference Value

Implementations SHOULD verify their XXH3 configuration by checking:

XXH3_64bits("hello, world") = 0x302cd5fba73d006c

As an 8-byte big-endian string: 30 2c d5 fb a7 3d 00 6c

If the computed value does not match, the implementation has a byte order or algorithm configuration error and will not produce compatible output.

Throughout this specification, the notation hash(x) refers to the XXH3 function applied to byte string x, with the result serialized as 8 bytes in big-endian order.

6. Data Packing Conventions

All integer-to-byte conversions in this specification use unsigned big-endian (network byte order) encoding:

pack_u64(value): Encode a 64-bit unsigned integer as 8 bytes in big-endian order.

pack_u32(value): Encode a 32-bit unsigned integer as 4 bytes in big-endian order.

unpack_u64(bytes): Decode 8 big-endian bytes as a 64-bit unsigned integer.

unpack_u32(bytes): Decode 4 big-endian bytes as a 32-bit unsigned integer.

For reference, the PHP pack format codes are 'J' (unsigned 64-bit big-endian) and 'N' (unsigned 32-bit big-endian).

7. UUID Field Layout

7.1. RFC 9562 Standard Layout

The standard UUID bit layout per RFC 9562, Section 4:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          time_low                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          time_mid             |     time_hi_and_version       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|clk_seq_hi_res |  clk_seq_low  |          node (0-1)           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          node (2-5)                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Note: The field names time_low, time_mid, etc. are inherited from UUID Version 1 and do not carry temporal semantics in Version 8. They are used here solely to identify octet positions within the UUID.

7.2. IntToUuid Encoding Layout

This specification maps the encoded data onto the 16-byte UUID as follows:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     encoded namespace (32)                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|    encoded id [0..1] (16)     |       seed [0..1] (16)        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       seed [2..3] (16)        |    encoded id [2..3] (16)     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    encoded id [4..7] (32)                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Byte positions within the 16-byte UUID:

Octets 0-3: Encoded namespace (4 bytes) Octets 4-5: Encoded id, bytes 0-1 (2 bytes) Octets 6-9: Seed (4 bytes, contains Version and Variant bits) Octets 10-15: Encoded id, bytes 2-7 (6 bytes)

The seed occupies octets 6-9, which span the time_hi_and_version, clock_seq_hi_and_reserved, and clock_seq_low fields of the standard UUID layout. This is the region that contains the Version and Variant bits, which the seed computation embeds via bitmask (see Section 8.2).

8. Encoding Algorithm

8.1. Function Signature

encode(id: uint64, namespace: uint32 = 0) -> UUID

Implementations MUST validate input parameters against the ranges defined in Section 4 before proceeding.

8.2. Seed Computation

The seed is a 4-byte value that serves two purposes: it contributes deterministic pseudo-randomness to the XOR encoding steps, and it acts as a checksum during decoding to verify that a UUID was produced by this algorithm.

compute_seed(packed_id: bytes[8], packed_ns: bytes[4]) -> bytes[4]

Step 1: Compute the hash of the concatenated packed values:

           full_hash = hash(packed_id || packed_ns)

        This produces an 8-byte hash digest.

Step 2: Extract the first 4 bytes as a 32-bit unsigned integer:

           seed_int = unpack_u32(full_hash[0..3])

Step 3: Apply the Version and Variant bitmask:

           seed_int = (seed_int AND 0x0FFF3FFF) OR 0x80008000

        The AND mask 0x0FFF3FFF clears 6 bits at positions
        corresponding to the UUID Version and Variant fields.
        The OR mask 0x80008000 sets those bits to the required
        values:

           Bits 31-28:  Set to 1000 (Version 8)
           Bits 15-14:  Set to 10   (Variant, RFC 9562)

        This leaves 26 bits of hash-derived content in the seed.

Step 4: Pack the masked integer back to 4 bytes:

           seed = pack_u32(seed_int)

Note on operator precedence: The expression uses bitwise AND before bitwise OR. Implementations SHOULD use explicit parentheses for clarity, regardless of the host language's operator precedence rules.

8.3. Encoding Steps

Given input values id (uint64) and namespace (uint32):

Step 1: Pack input integers to byte strings.

           id_bytes = pack_u64(id)            // 8 bytes
           ns_bytes = pack_u32(namespace)      // 4 bytes

Step 2: Compute the seed from the packed input values.

           seed = compute_seed(id_bytes, ns_bytes)   // 4 bytes

Step 3: Encode the id.

        XOR the packed id with the hash of the concatenated
        namespace and seed:

           id_bytes = id_bytes XOR hash(ns_bytes || seed)

        Both operands are 8 bytes; this is a full-length XOR.

Step 4: Encode the namespace.

        XOR the packed namespace with the first 4 bytes of the
        hash of the seed:

           ns_bytes = ns_bytes XOR hash(seed)[0..3]

        The hash output is 8 bytes but the namespace is only 4
        bytes. Implementations MUST truncate (slice) the hash
        output to 4 bytes before applying XOR. Only the first 4
        bytes of the hash are used; the remaining 4 bytes are
        discarded.

        Note: In the PHP reference implementation, this truncation
        occurs implicitly because the XOR operator on strings of
        unequal length produces a result with the length of the
        shorter operand. Other languages MUST perform explicit
        truncation.

Step 5: Assemble the 16-byte UUID.

        Concatenate the encoded components in the order defined
        by Section 7.2:

           uuid_bytes = ns_bytes
                     || id_bytes[0..1]
                     || seed
                     || id_bytes[2..7]

Step 6: Format as a UUID string.

        Encode the 16 bytes as 32 lowercase hexadecimal characters
        and insert hyphens at the standard positions to produce
        the 8-4-4-4-12 grouping:

           xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

9. Decoding Algorithm

9.1. Function Signature

decode(uuid: string) -> (id: uint64, namespace: uint32)

9.2. Preconditions

Before decoding, the UUID string MUST be validated as described in Section 10. If validation fails, implementations MUST raise an error.

9.3. Decoding Steps

Given a validated UUID string:

Step 1: Parse the UUID string to a 16-byte array.

        Strip hyphens and decode the 32 hexadecimal characters to
        16 bytes.

Step 2: Extract the encoded components.

        Using the layout defined in Section 7.2:

           seed   = uuid_bytes[6..9]                         // 4 bytes
           enc_ns = uuid_bytes[0..3]                         // 4 bytes
           enc_id = uuid_bytes[4..5] || uuid_bytes[10..15]   // 8 bytes

Step 3: Decode the namespace.

           ns_bytes = enc_ns XOR hash(seed)[0..3]

        As in encoding, only the first 4 bytes of the 8-byte
        hash output are used.

Step 4: Decode the id.

           id_bytes = enc_id XOR hash(ns_bytes || seed)

Step 5: Verify the checksum.

        Recompute the seed from the decoded byte strings:

           check_seed = compute_seed(id_bytes, ns_bytes)

        If check_seed does not equal seed (byte-for-byte), the
        UUID does not contain data encoded by this algorithm.
        Implementations MUST raise an error.

Step 6: Unpack the integer values.

           id        = unpack_u64(id_bytes)
           namespace = unpack_u32(ns_bytes)

        Return the decoded id and namespace.

10. Validation

10.1. UUID Format Validation

Before decoding, implementations MUST verify that the UUID string conforms to the RFC 9562 Version 8 format. The following regular expression validates the required structure:

/^[0-9a-f]{8}-[0-9a-f]{4}-8[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/

This expression checks:

(a) Correct hyphenated grouping: 8-4-4-4-12 hexadecimal characters. (b) Lowercase hexadecimal only (a-f, not A-F). (c) Version nibble (position 13) is '8'. (d) Variant nibble (position 17) is one of '8', '9', 'a', or 'b'.

Implementations MAY choose to accept uppercase input by normalizing to lowercase before validation.

10.2. Checksum Validation

Even when the format validation succeeds, the UUID may be a valid Version 8 UUID that was not produced by this algorithm. The seed checksum verification in Step 5 of Section 9.3 detects this condition.

11. Error Conditions

Conforming implementations MUST raise errors for the following conditions:

(a) Invalid id range: id < 0 or id > 9,223,372,036,854,775,807.

(b) Invalid namespace range: namespace < 0 or namespace > 4,294,967,295.

(c) Invalid UUID format: The input string does not match the Version 8 validation pattern defined in Section 10.1.

(d) Checksum mismatch: The seed extracted from the UUID does not match the seed recomputed from the decoded id and namespace byte strings. This indicates the UUID was not produced by this algorithm.

The specific error types and messages are left to the implementation.

12. IANA Considerations

This document has no IANA actions.

13. Security Considerations

13.1. Not Encryption

This algorithm provides encoding, not encryption. The XOR-based obfuscation and the deterministic seed provide no cryptographic security. Any party with knowledge of this specification and the xxHash algorithm can decode any UUID produced by this algorithm.

The algorithm is intended to mitigate casual user-enumeration attacks (e.g., incrementing an ID in a URL to discover other records) but does not protect against a determined adversary.

13.2. Hash Function

XXH3 is a non-cryptographic hash function optimized for speed. It does not provide collision resistance, preimage resistance, or any other property required of cryptographic hash functions. Its use here is appropriate because the algorithm requires only determinism and good distribution, not cryptographic strength.

13.3. Information Leakage

The UUID does not directly reveal the encoded integers, but the encoding is deterministic: the same input always produces the same output. An attacker who can observe multiple UUIDs and who knows the namespace in use can attempt to brute-force the id values, which is computationally trivial for small id spaces.

14. References

14.1. Normative References

[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, March 1997, https://www.rfc-editor.org/rfc/rfc2119.

[RFC8174] Leite, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017, https://www.rfc-editor.org/rfc/rfc8174.

[RFC9562] Davis, K., Peabody, B., and P. Leach, "Universally Unique IDentifiers (UUIDs)", RFC 9562, DOI 10.17487/RFC9562, May 2024, https://www.rfc-editor.org/rfc/rfc9562.

[XXHASH] Collet, Y., "xxHash - Extremely fast non-cryptographic hash algorithm", https://github.com/Cyan4973/xxHash.

14.2. Informative References

[RFC4122] Leach, P., Mealling, M., and R. Salz, "A Universally Unique IDentifier (UUID) URN Namespace", RFC 4122, DOI 10.17487/RFC4122, July 2005, https://www.rfc-editor.org/rfc/rfc4122.

          Note: RFC 4122 has been obsoleted by RFC 9562 [RFC9562].
          It is referenced here because many existing UUID
          libraries and tools continue to cite RFC 4122 in their
          documentation and source code. The binary UUID format,
          version field, and variant field are identical between
          the two specifications. UUID Version 8, used by this
          algorithm, was introduced in RFC 9562.

[REFIMPL] WickedByte, "int-to-uuid: Integer ID To RFC 9562 UUID Converter", PHP, https://github.com/wickedbyte/int-to-uuid.

Appendix A. Test Vectors

The following test vectors are generated by the PHP reference implementation [REFIMPL] and MUST be reproduced exactly by all conforming implementations. Each entry consists of an id value, a namespace value, and the expected UUID string.

A.1. Minimum and Boundary Values

id=0, namespace=0: 5fec6701-fc03-8499-8f91-4168936d858f

id=1, namespace=0: 764bb373-b10f-8f8a-b66c-3e7930926aed

id=0, namespace=1: b06454be-e102-87f8-a348-d80b99568ff2

id=1, namespace=1: 22cdb468-b863-86d6-afb9-481e9df6cd53

A.2. Small Identifier Values

id=2, namespace=0: d3162c0a-1529-89ed-a19f-0793f955d3da

id=10, namespace=0: e73a6fb1-0bbf-8d27-8fae-23961165b446

id=42, namespace=0: 99c45a05-a33b-8544-8024-f4be69401069

id=100, namespace=0: 05f7c1b1-f4c6-8779-8dfa-68ea5169967a

id=255, namespace=0: b42736ec-ecbe-808b-b44d-443e30d92215

id=256, namespace=0: 422d289a-e2d9-8464-826f-9632bed9cf80

id=1000, namespace=0: 51763062-68bf-8c4c-8c2e-b94bc694ad81

id=65535, namespace=0: 2d69cd93-4d99-8e94-9c1a-046c899c24d1

A.3. Namespace Variations

id=1, namespace=2: 4c0e091e-3123-8646-9a5c-da6c0a4d0534

id=1, namespace=100: 0f2503af-a932-8e6b-9c55-4c8df212c6c1

id=1, namespace=255: 5e2ca316-ead3-83ea-9152-06659d2d54cb

id=1, namespace=65535: 0a4a2e43-ff0e-8383-af12-e5597447ed20

id=1, namespace=1000000: a9e4d1e3-6df6-8f3a-836d-1e56db53cccd

A.4. Mixed Values

id=12, namespace=0: c81f423b-2ca0-8963-aefa-f067a191123f

id=42, namespace=12: dee5e9d2-c3e4-8273-b0d5-b3b5307bf749

id=100, namespace=200: 94cf7b60-09f0-814e-9305-37f7eb693984

id=12345, namespace=6789: d5deff2c-1aca-8da8-9bb6-0e05d4f2dea6

id=999999, namespace=999999: f2648a6c-3df9-84e4-afe1-0dd5633d5d7e

A.5. 32-Bit Boundary Values

id=2147483647 (2^31 - 1), namespace=0: fce9e64f-576e-8f75-8ca5-d2679725ba9f

id=2147483648 (2^31), namespace=0: 96c28777-fb80-8b44-8f3a-58f41da7dab9

id=4294967295 (2^32 - 1), namespace=0: f42237ec-018c-83f2-af4d-bf0034f41734

id=4294967296 (2^32), namespace=0: ef80231a-3165-8468-b04e-21869ead8e1f

A.6. Large Identifier Values

id=1000000, namespace=0: 0c4b6717-2df3-88b2-b0c2-32cd92760377

id=1000000000, namespace=0: 2e07e8e9-3f04-8c17-bdaf-266326b01b0a

id=9223372036854775807 (2^63 - 1), namespace=0: 977507dc-c58c-8cc5-b9f9-1b7bc1e798c2

A.7. Maximum Namespace Values

id=0, namespace=4294967295 (2^32 - 1): 6d5fa5ad-754f-8082-ada3-61de1b09aa35

id=1, namespace=4294967295: 36354b02-3ed5-829b-9616-aef9c7a135d9

id=4294967295, namespace=4294967295: 08c906ac-bb85-8843-a4fa-8e015c174532

id=9223372036854775807, namespace=4294967295: 019e07d3-805d-88a4-ba7f-9ce33620cdff

A.8. Namespace Boundary Values

id=42, namespace=2147483647 (2^31 - 1): 63133744-f68d-8e67-8870-946b4d157ee3

id=42, namespace=2147483648 (2^31): f01330f6-3ec8-8a67-91fd-1639930088c6

id=42, namespace=4294967295 (2^32 - 1): 20d66b23-150b-8c66-9b1c-f8f7d9212bd7

Appendix B. Test Vector Generation

The test vectors in Appendix A were generated by running the following script against the PHP reference implementation [REFIMPL]:

<?php

require __DIR__ . '/vendor/autoload.php';

use WickedByte\IntToUuid\IntegerId;
use WickedByte\IntToUuid\IntToUuid;

$vectors = [
    // Zero / minimum values
    [0, 0],
    [1, 0],
    [0, 1],
    [1, 1],

    // Small id values, default namespace
    [2, 0],
    [10, 0],
    [42, 0],
    [100, 0],
    [255, 0],
    [256, 0],
    [1000, 0],
    [65535, 0],

    // Small id, various namespaces
    [1, 2],
    [1, 100],
    [1, 255],
    [1, 65535],
    [1, 1000000],

    // Mixed values (README examples)
    [12, 0],
    [42, 12],

    // Medium values
    [100, 200],
    [12345, 6789],
    [999999, 999999],

    // 32-bit boundary values
    [2147483647, 0],            // INT32_MAX
    [2147483648, 0],            // INT32_MAX + 1
    [4294967295, 0],            // UINT32_MAX
    [4294967296, 0],            // UINT32_MAX + 1

    // Large id values
    [1000000, 0],
    [1000000000, 0],
    [9223372036854775807, 0],   // PHP_INT_MAX (2^63 - 1)

    // Maximum namespace values
    [0, 4294967295],            // min id, max namespace
    [1, 4294967295],
    [4294967295, 4294967295],   // UINT32_MAX id, max namespace
    [9223372036854775807, 4294967295], // max id, max namespace

    // Namespace boundary values
    [42, 2147483647],           // INT32_MAX namespace
    [42, 2147483648],           // INT32_MAX + 1 namespace
    [42, 4294967295],           // UINT32_MAX namespace
];

$output = [];
foreach ($vectors as [$id, $ns]) {
    $integer_id = IntegerId::make($id, $ns);
    $uuid = IntToUuid::encode($integer_id);
    $decoded = IntToUuid::decode($uuid);
    assert($decoded->value === $id);
    assert($decoded->namespace === $ns);
    $output[] = ['id' => $id, 'namespace' => $ns, 'uuid' => (string)$uuid];
}

echo json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";

Appendix C. Implementation Notes

C.1. XXH3 Byte Order

The most common source of cross-language incompatibility is incorrect byte order when converting the XXH3 digest integer to a byte string. The 64-bit integer MUST be serialized in big-endian order. Language- specific guidance:

PHP: hash('xxh3', $data, true) returns big-endian bytes.

Python: xxhash.xxh3_64_intdigest(data).to_bytes(8, 'big') Do NOT use xxh3_64_digest() without verifying byte order.

Rust: xxh3::xxh3_64(data).to_be_bytes()

TypeScript/Node.js: Buffer.alloc(8) with writeBigUInt64BE(xxh3.xxh64(data))

C.2. XOR Truncation

In Steps 4 and 3 of the encoding and decoding algorithms respectively, the namespace is 4 bytes but the hash output is 8 bytes. The XOR MUST use only the first 4 bytes of the hash. Implementations MUST explicitly truncate (slice) the hash before applying XOR, unless the host language's XOR semantics guarantee truncation to the shorter operand (as PHP does for string XOR).

C.3. Integer Sizes

JavaScript and some other languages do not have native 64-bit integer types. Implementations in such languages SHOULD use BigInt (or the equivalent arbitrary-precision integer type) for the id parameter to avoid precision loss. The namespace value fits within a standard IEEE 754 double-precision float and does not require special handling.

Authors' Addresses

Andy Snell WickedByte

Email: andy@wickedbyte.com