Skip to content

Adds deterministic ids from a from input - #5

Merged
johnnyt merged 1 commit into
mainfrom
uxjv-iq8-deterministic-ids
Oct 21, 2026
Merged

johnnyt merged 1 commit into
mainfrom
uxjv-iq8-deterministic-ids

Conversation

@johnnyt

@johnnyt johnnyt commented Oct 21, 2026

Copy link
Copy Markdown
Member

What

Deterministic ids: the same prefix, input and size always give the same id.

  • UxidOptions.Builder.from(String) asks for a deterministic id. Uxid.generate(UxidOptions) then hashes, with MessageDigest SHA-256 (a new instance per call), the byte string the wire format (uxid-ex guides/wire-format.md at v2.10.0, "Deterministic bodies") lays out: the prefix's UTF-8 bytes, a zero byte, the input's UTF-8 bytes, a zero byte and one byte holding the size's hash character count. The body is the marker z (Z in upper case) followed by the digest's first count x 5 bits, 5 bits per character, at the size's standard body length.
  • Fixed random bytes with from are refused with ErrorCode.RAND_WITH_FROM (rand_with_from), whatever their count.
  • Size holds each size's hash character count beside its random widths (package-private), and Base32 gains a package-private writer for a whole number of leading 5-bit groups.
  • Javadoc on Uxid, UxidOptions, the builder's from and byteSource, and Size describes the deterministic form; CLAUDE.md's description of the tree now names it; changelog.d/uxjv-iq8.md records the additions.

Behaviour worth reading

  • With from set, the clock, the time form, the random width and the byte source are not read, as in the reference's deterministic path (uxid-ex UXID.Encoder.process at v2.10.0): a compact time at the marker or a random width above 10 is not refused on a deterministic request, because neither is used.
  • The rand_with_from refusal comes before anything else on a deterministic request, as in the reference's UXID.new, so fixed bytes of any count are refused with that name and never with rand_width_mismatch.
  • An input holding an unpaired surrogate has no UTF-8 encoding. from refuses it with IllegalArgumentException when set, rather than hashing it with a replacement character, which would give two different strings one id; a prefix holding one is refused the same way when a deterministic id is generated, since the prefix is part of the hash input. Time-based generation does not encode the prefix and is unchanged.
  • from_with_monotonic belongs to the monotonic generator, which a later change adds.

Provenance

  • Choices made in this change, 2026-10-21:
    • Not reading the time, time form, random width and byte source with from follows the reference's deterministic path rather than refusing those combinations; the wire format names no refusal for them.
    • The unpaired-surrogate refusal is an IllegalArgumentException, not a UxidException, so ErrorCode stays exactly the wire format's closed set; it follows the rule the builder already states, that a setter refuses a value wrong on its own.

Tests

  • DeterministicCorpusTest copies every case of conformance/deterministic.json at v2.10.0 into a parameterized test, each named by its corpus id (the three golden ids, each size under usr, the empty and absent prefix, upper case and a non-ASCII input), and the invalid/generate-rand-with-from refusal from conformance/invalid.json. The full corpus runner comes with a later change.
  • UxidDeterministicTest recomputes the body a second way (the hash input built byte by byte, the digest read as one unsigned number and cut into characters) for each size; checks that a request loaded with a compact time at the marker, a random width of 11 and a refusing byte source still gives the golden id; that fixed bytes of several counts are refused with rand_with_from; that two prefixes give unrelated bodies for one input; and the unpaired-surrogate refusals for the input and the prefix.
  • Every new test carries a sabotage note on its annotation line naming a mutation that was applied, run and restored; each turned that test red on an assertion.

Gate

mise run gate is green on this head (Temurin 25.0.4, Maven 3.9.16): the enforcer rules, the compile with every javac lint and Error Prone, the tests, Spotless, the JaCoCo line check, and the sources and javadoc jars with doclint. The commit is the exact tree that run passed on, and main had not moved.

Refs: uxjv-iq8

UxidOptions.Builder.from asks for an id derived from the input and
the prefix, as the wire format at uxid-ex v2.10.0 lays it out:
SHA-256 over the prefix, a zero byte, the input as UTF-8, a zero byte
and one byte holding the size's hash character count, then the z
marker and the digest's first count x 5 bits. The clock, time form,
random width and byte source are not read, and fixed random bytes
with it are refused with rand_with_from, as the reference does.

An input or prefix holding an unpaired surrogate is refused rather
than hashed with a replacement character, which would give two
different strings one id. The corpus's deterministic cases and the
rand_with_from refusal are copied into tests until the corpus
runner lands.

Refs: uxjv-iq8
@johnnyt
johnnyt merged commit c4300e3 into main Oct 21, 2026
4 checks passed
@johnnyt
johnnyt deleted the uxjv-iq8-deterministic-ids branch October 21, 2026 22:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant