Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions source/ulid.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import crypto from "node:crypto";
import { incrementBase32 } from "./crockford.js";
import { ENCODING, ENCODING_LEN, RANDOM_LEN, TIME_LEN, TIME_MAX } from "./constants.js";
import { ENCODING, ENCODING_LEN, RANDOM_LEN, TIME_LEN, TIME_MAX, ULID_REGEX } from "./constants.js";
import { ULIDError, ULIDErrorCode } from "./error.js";
import { PRNG, ULID, ULIDFactory } from "./types.js";
import { randomChar } from "./utils.js";
Expand Down Expand Up @@ -133,14 +133,9 @@ function inWebWorker(): boolean {
* isValid(""); // false
*/
export function isValid(id: string): boolean {
return (
typeof id === "string" &&
id.length === TIME_LEN + RANDOM_LEN &&
id
.toUpperCase()
.split("")
.every(char => ENCODING.indexOf(char) !== -1)
);
// ULID_REGEX also constrains the leading character to 0-7, so a string whose
// timestamp exceeds the 48-bit maximum (which decodeTime rejects) is invalid.
return typeof id === "string" && ULID_REGEX.test(id);
}

/**
Expand Down
36 changes: 35 additions & 1 deletion test/node/ulid.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { decodeTime, encodeTime, monotonicFactory, ulid, ULIDFactory } from "../../";
import { decodeTime, encodeTime, isValid, monotonicFactory, ulid, ULIDFactory } from "../../";

const ULID_REXP = /^[0-7][0-9a-hjkmnp-tv-zA-HJKMNP-TV-Z]{25}$/;

Expand Down Expand Up @@ -85,3 +85,37 @@ describe("ulid", () => {
expect(id).toMatch(ULID_REXP);
});
});

describe("isValid", () => {
it("accepts a generated ULID, in either case", () => {
const id = ulid();
expect(isValid(id)).toBe(true);
expect(isValid(id.toLowerCase())).toBe(true);
});

it("rejects non-strings, the wrong length and out-of-alphabet characters", () => {
expect(isValid("")).toBe(false);
expect(isValid("0".repeat(25))).toBe(false);
expect(isValid("0I" + "0".repeat(24))).toBe(false); // I is not in Crockford base32
});

it("rejects a timestamp larger than the 48-bit maximum", () => {
// The largest valid ULID is 7ZZ…; a leading character above 7 overflows
// the 48-bit timestamp, which decodeTime rejects — so isValid must too.
expect(isValid("8" + "0".repeat(25))).toBe(false);
expect(isValid("Z".repeat(26))).toBe(false);
});

it("agrees with decodeTime", () => {
const ids = ["7" + "Z".repeat(25), "8" + "0".repeat(25), "Z".repeat(26)];
for (const id of ids) {
let decodes = true;
try {
decodeTime(id);
} catch {
decodes = false;
}
expect(isValid(id)).toBe(decodes);
}
});
});