From 5dc508346cd05ea755581ffd9ca8baa6ff8d4d91 Mon Sep 17 00:00:00 2001 From: mq1 <74494945+chan-mai@users.noreply.github.com> Date: Wed, 15 Apr 2026 09:02:43 +0900 Subject: [PATCH] =?UTF-8?q?fix(backend):=20ULID=E3=82=92=E6=AD=A3=E3=81=97?= =?UTF-8?q?=E3=81=8F=E5=87=A6=E7=90=86=E3=81=A7=E3=81=8D=E3=81=AA=E3=81=84?= =?UTF-8?q?=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(#17310)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(backend): fix parseUlidFull to correctly handle Crockford Base32 chars W/X/Y/Z --- packages/backend/src/misc/id/ulid.ts | 11 +++++-- packages/backend/test/unit/misc/ulid.ts | 41 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 packages/backend/test/unit/misc/ulid.ts diff --git a/packages/backend/src/misc/id/ulid.ts b/packages/backend/src/misc/id/ulid.ts index 8b81702d19..291a33385f 100644 --- a/packages/backend/src/misc/id/ulid.ts +++ b/packages/backend/src/misc/id/ulid.ts @@ -5,12 +5,19 @@ // Crockford's Base32 // https://github.com/ulid/spec#encoding -import { parseBigInt32 } from '@/misc/bigint.js'; const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; export const ulidRegExp = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/; +function parseBigIntCrockford(str: string): bigint { + let result = 0n; + for (let i = 0; i < str.length; i++) { + result = result * 32n + BigInt(CHARS.indexOf(str[i])); + } + return result; +} + function parseBase32(timestamp: string) { let time = 0; for (let i = 0; i < timestamp.length; i++) { @@ -26,6 +33,6 @@ export function parseUlid(id: string): { date: Date; } { export function parseUlidFull(id: string): { date: number; additional: bigint; } { return { date: parseBase32(id.slice(0, 10)), - additional: parseBigInt32(id.slice(10, 26)), + additional: parseBigIntCrockford(id.slice(10, 26)), }; } diff --git a/packages/backend/test/unit/misc/ulid.ts b/packages/backend/test/unit/misc/ulid.ts new file mode 100644 index 0000000000..b79e3bc2b4 --- /dev/null +++ b/packages/backend/test/unit/misc/ulid.ts @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { describe, expect, test } from '@jest/globals'; +import { parseUlidFull } from '@/misc/id/ulid.js'; + +// Timestamp part "01KPS7S300" encodes 1776816000000ms (2026-04-22T00:00:00.000Z) +// Verified: 1*32^8 + 19*32^7 + 22*32^6 + 25*32^5 + 7*32^4 + 25*32^3 + 3*32^2 = 1776816000000 + +describe('misc:ulid', () => { + test('parseUlidFull - timestamp is parsed correctly', () => { + // id[10..25] = all zeros (valid Crockford Base32) + // 2026-04-22T00:00:00.000Z + const { date } = parseUlidFull('01KPS7S3000000000000000000'); + expect(date).toBe(1776816000000); + }); + + test('parseUlidFull - W/X/Y/Z at id[10] (chunk 1 head) do not throw', () => { + // id[10] = W + expect(() => parseUlidFull('01KPS7S300W000000000000000')).not.toThrow(); + // id[10] = X + expect(() => parseUlidFull('01KPS7S300X000000000000000')).not.toThrow(); + // id[10] = Y + expect(() => parseUlidFull('01KPS7S300Y000000000000000')).not.toThrow(); + // id[10] = Z + expect(() => parseUlidFull('01KPS7S300Z000000000000000')).not.toThrow(); + }); + + test('parseUlidFull - W/X/Y/Z at id[16] (chunk 2 head) do not throw', () => { + // id[16] = W + expect(() => parseUlidFull('01KPS7S300ABCDEFW000000000')).not.toThrow(); + // id[16] = X + expect(() => parseUlidFull('01KPS7S300ABCDEFX000000000')).not.toThrow(); + // id[16] = Y + expect(() => parseUlidFull('01KPS7S300ABCDEFY000000000')).not.toThrow(); + // id[16] = Z + expect(() => parseUlidFull('01KPS7S300ABCDEFZ000000000')).not.toThrow(); + }); +});