Skip to content

[P1][Correctness] Validate custom NanoID alphabets and numeric bounds #334

Description

@baixiangcpp

Summary

The ID Generator accepts an arbitrary custom NanoID alphabet but passes it directly to a byte-based sampling implementation with no validation. Several user-reachable inputs either throw during generation or produce identifiers from a different alphabet/entropy space than the UI implies.

Confirmed defects

1. Empty alphabet throws a runtime exception

For an empty alphabet, the mask becomes -1 and the calculated step becomes -Infinity. Generation then executes:

new Uint8Array(step)

which throws RangeError: Invalid typed array length: -Infinity.

Because generation runs from a React effect, clearing the alphabet can break the tool flow rather than produce a field-level validation error.

2. Alphabets longer than 256 characters are not sampled fully

The implementation draws Uint8Array values, so each random value is limited to 0..255. When the alphabet contains more than 256 entries, indices after 255 are unreachable even though the mask calculation can be larger.

The displayed alphabet is therefore not the actual sampling alphabet.

3. Duplicate characters bias the output space

Duplicate alphabet entries occupy multiple indices and are selected more often than unique entries. This reduces the effective identifier entropy and makes collision expectations different from a unique-alphabet calculation.

4. Non-BMP Unicode alphabets are split into UTF-16 code units

The code uses alphabet.length and alphabet[index]. An alphabet containing emoji or other non-BMP symbols can select individual surrogate halves and emit malformed text instead of complete symbols.

5. Size is not validated before allocation/generation

The number input uses Number(event.target.value) directly. Blank/zero values can produce empty identifiers; negative or non-finite values can result in invalid allocation/math paths despite the HTML min/max attributes.

Why this matters

NanoID is selected specifically for compact, collision-resistant identifiers. Silent alphabet truncation, biased sampling, malformed symbols, or empty output invalidates that purpose while the generated strings may still look usable.

HTML input constraints are not a substitute for validating state in the generation function.

Recommended implementation

Extract the ID algorithms from the page into a tested logic.ts module and validate before generation.

Choose and document one custom-alphabet policy:

  • restrict to 2-256 unique ASCII/single-byte symbols; or
  • implement code-point-aware sampling with a random source/algorithm that supports the documented upper bound.

For the current byte-based algorithm, the simpler safe contract is 2-256 unique code points with duplicates rejected or explicitly deduplicated after warning the user.

Also validate quantity and NanoID size as finite integers within their UI limits before allocating or looping.

Acceptance criteria

  • Empty or one-symbol alphabets are rejected with a localized field error.
  • Alphabets above the supported limit are rejected; no configured symbol is silently unreachable.
  • Duplicate symbols are rejected or deterministically deduplicated with visible feedback.
  • The Unicode policy is explicit; supported symbols are sampled as complete code points and unsupported alphabets are rejected.
  • NanoID size and batch quantity are finite integers within documented bounds.
  • Invalid settings never throw from a React effect and never produce empty/partial IDs.
  • Generation uses unbiased rejection sampling for every supported alphabet length.
  • Unit tests cover lengths 0, 1, 2, 255, 256, and 257; duplicates; emoji/non-BMP input; blank/negative sizes; and deterministic boundary samples.
  • UUID v4, UUID v7, and ULID generation/extraction receive regression tests after logic extraction.

Relevant files

  • src/features/tools/id-generator/page.tsx
  • new/extracted src/features/tools/id-generator/logic.ts
  • new unit/component tests for ID Generator

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions