From 585258516b650506361ba19a6e6d992c52ad3d96 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 16 Jul 2026 20:22:02 -0300 Subject: [PATCH] blossom: optimize hash extraction from url. --- nipb0.test.ts | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++ nipb0.ts | 25 ++++++++++++++----- 2 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 nipb0.test.ts diff --git a/nipb0.test.ts b/nipb0.test.ts new file mode 100644 index 0000000..b6f79be --- /dev/null +++ b/nipb0.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from 'bun:test' +import { getHashFromURL } from './nipb0.ts' + +const VALID_HASH = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' + +describe('getHashFromURL', () => { + it('extracts hash from plain URL string', () => { + expect(getHashFromURL(`https://example.com/${VALID_HASH}`)).toBe(VALID_HASH) + }) + + it('extracts hash from URL with extension', () => { + expect(getHashFromURL(`https://example.com/${VALID_HASH}.jpg`)).toBe(VALID_HASH) + }) + + it('extracts hash from URL with query string', () => { + expect(getHashFromURL(`https://example.com/${VALID_HASH}?foo=bar`)).toBe(VALID_HASH) + }) + + it('extracts hash from URL with fragment', () => { + expect(getHashFromURL(`https://example.com/${VALID_HASH}#section`)).toBe(VALID_HASH) + }) + + it('extracts hash from URL with query and fragment', () => { + expect(getHashFromURL(`https://example.com/${VALID_HASH}?foo=bar#section`)).toBe(VALID_HASH) + }) + + it('extracts hash from URL object', () => { + expect(getHashFromURL(new URL(`https://example.com/${VALID_HASH}`))).toBe(VALID_HASH) + }) + + it('extracts hash from URL object with pathname', () => { + const url = new URL(`https://example.com/${VALID_HASH}.png`) + expect(getHashFromURL(url)).toBe(VALID_HASH) + }) + + it('returns null when segment is too short', () => { + expect(getHashFromURL('https://example.com/abc')).toBeNull() + }) + + it('returns null when segment has non-hex characters', () => { + expect(getHashFromURL(`https://example.com/${VALID_HASH.slice(0, 63)}x`)).toBeNull() + }) + + it('returns null when extension is not at position 64', () => { + expect(getHashFromURL('https://example.com/short.jpg')).toBeNull() + }) + + it('returns null when uppercase hex chars are used', () => { + expect(getHashFromURL(`https://example.com/${VALID_HASH.toUpperCase()}`)).toBeNull() + }) + + it('returns null for URL without hash in path', () => { + expect(getHashFromURL('https://example.com/')).toBeNull() + }) + + it('handles localhost URL with hash', () => { + expect(getHashFromURL(`http://localhost:3000/${VALID_HASH}`)).toBe(VALID_HASH) + }) + + it('returns hash from nested path', () => { + expect(getHashFromURL(`https://example.com/files/${VALID_HASH}.jpg`)).toBe(VALID_HASH) + }) + + it('returns null for plain string without slash', () => { + expect(getHashFromURL('not-a-url')).toBeNull() + }) +}) diff --git a/nipb0.ts b/nipb0.ts index ad5c72c..19be7a7 100644 --- a/nipb0.ts +++ b/nipb0.ts @@ -373,13 +373,26 @@ export function getServersFromServerListEvent(event: { tags: string[][] }): URL[ } export function getHashFromURL(url: string | URL): string | null { - try { - if (typeof url === 'string') url = new URL(url) - const hashes = Array.from(url.pathname.matchAll(/[0-9a-f]{64}/gi)) - return hashes.length > 0 ? hashes[hashes.length - 1][0] : null - } catch (_err) { - return null + // strip fragment, then query string + const path = typeof url === 'string' ? url.split('#', 1)[0].split('?', 1)[0] : url.pathname + + // get the last path segment + const lastSlash = path.lastIndexOf('/') + let segment = lastSlash === -1 ? path : path.slice(lastSlash + 1) + + // if there is an extension it should start at exactly the 64th character + const dotIndex = segment.indexOf('.') + if (dotIndex !== -1 && dotIndex !== 64) return null + + // the first 64 characters must be all lowercase hex + for (let i = 0; i < 64; i++) { + const c = segment.charCodeAt(i) + const isDigit = c >= 48 && c <= 57 // '0'-'9' + const isLowerHex = c >= 97 && c <= 102 // 'a'-'f' + if (!isDigit && !isLowerHex) return null } + + return segment.slice(0, 64) } export type UploadOptions = {