replace ad-hoc regexes with isHex32().

This commit is contained in:
fiatjaf
2026-07-16 20:22:13 -03:00
parent 585258516b
commit fa21fd6671
8 changed files with 45 additions and 42 deletions

View File

@@ -1,3 +1,5 @@
import { isHex32 } from './utils.ts'
export interface Nostr {
generateSecretKey(): Uint8Array
getPublicKey(secretKey: Uint8Array): string
@@ -36,7 +38,7 @@ export function validateEvent<T>(event: T): event is T & UnsignedEvent {
if (typeof event.content !== 'string') return false
if (typeof event.created_at !== 'number') return false
if (typeof event.pubkey !== 'string') return false
if (!event.pubkey.match(/^[a-f0-9]{64}$/)) return false
if (!isHex32(event.pubkey)) return false
if (!Array.isArray(event.tags)) return false
for (let i = 0; i < event.tags.length; i++) {

View File

@@ -1,7 +1,6 @@
import type { Event } from './core.ts'
import type { EventPointer, ProfilePointer } from './nip19.ts'
const HEX64 = /^[0-9a-fA-F]{64}$/
import { isHex32 } from './utils.ts'
export function parse(event: Pick<Event, 'tags'>): {
/**
@@ -43,7 +42,7 @@ export function parse(event: Pick<Event, 'tags'>): {
for (let i = event.tags.length - 1; i >= 0; i--) {
const tag = event.tags[i]
if (tag[0] === 'e' && tag[1] && HEX64.test(tag[1])) {
if (tag[0] === 'e' && tag[1] && isHex32(tag[1])) {
const [_, eTagEventId, eTagRelayUrl, eTagMarker, eTagAuthor] = tag as [
string,
string,
@@ -55,7 +54,7 @@ export function parse(event: Pick<Event, 'tags'>): {
const eventPointer: EventPointer = {
id: eTagEventId,
relays: eTagRelayUrl ? [eTagRelayUrl] : [],
author: eTagAuthor && HEX64.test(eTagAuthor) ? eTagAuthor : undefined,
author: eTagAuthor && isHex32(eTagAuthor) ? eTagAuthor : undefined,
}
if (eTagMarker === 'root') {
@@ -83,7 +82,7 @@ export function parse(event: Pick<Event, 'tags'>): {
continue
}
if (tag[0] === 'q' && tag[1] && HEX64.test(tag[1])) {
if (tag[0] === 'q' && tag[1] && isHex32(tag[1])) {
const [_, eTagEventId, eTagRelayUrl] = tag as [string, string, undefined | string]
result.quotes.push({
id: eTagEventId,
@@ -91,7 +90,7 @@ export function parse(event: Pick<Event, 'tags'>): {
})
}
if (tag[0] === 'p' && tag[1] && HEX64.test(tag[1])) {
if (tag[0] === 'p' && tag[1] && isHex32(tag[1])) {
result.profiles.push({
pubkey: tag[1],
relays: tag[2] ? [tag[2]] : [],

View File

@@ -1,7 +1,6 @@
import type { Event } from './core.ts'
import type { AddressPointer, EventPointer, ProfilePointer } from './nip19.ts'
const HEX64 = /^[0-9a-fA-F]{64}$/
import { isHex32 } from './utils.ts'
export type ExternalPointer = {
value: string
@@ -22,7 +21,7 @@ function parseAddressPointer(value: string, relayUrl?: string): AddressPointer |
if (Number.isNaN(kind)) return undefined
const pubkey = value.slice(idx + 1, idx2)
if (!HEX64.test(pubkey)) return undefined
if (!isHex32(pubkey)) return undefined
return {
kind,
@@ -36,11 +35,11 @@ function parsePointer(tag: string[]): EventPointer | AddressPointer | ExternalPo
switch (tag[0]) {
case 'E':
case 'e':
if (!tag[1] || !HEX64.test(tag[1])) return undefined
if (!tag[1] || !isHex32(tag[1])) return undefined
return {
id: tag[1],
relays: tag[2] ? [tag[2]] : [],
author: tag[3] && HEX64.test(tag[3]) ? tag[3] : undefined,
author: tag[3] && isHex32(tag[3]) ? tag[3] : undefined,
}
case 'A':
case 'a':
@@ -63,12 +62,12 @@ function parseQuote(tag: string[]): EventPointer | AddressPointer | ExternalPoin
return parseAddressPointer(tag[1], tag[2])
}
if (!HEX64.test(tag[1])) return undefined
if (!isHex32(tag[1])) return undefined
return {
id: tag[1],
relays: tag[2] ? [tag[2]] : [],
author: tag[3] && HEX64.test(tag[3]) ? tag[3] : undefined,
author: tag[3] && isHex32(tag[3]) ? tag[3] : undefined,
}
}
@@ -179,7 +178,7 @@ export function parse(event: Pick<Event, 'tags'>): {
continue
}
if ((tag[0] === 'P' || tag[0] === 'p') && tag[1] && HEX64.test(tag[1])) {
if ((tag[0] === 'P' || tag[0] === 'p') && tag[1] && isHex32(tag[1])) {
result.profiles.push({
pubkey: tag[1],
relays: tag[2] ? [tag[2]] : [],

View File

@@ -3,6 +3,7 @@ import { bytesToHex } from '@noble/hashes/utils.js'
import type { Event } from './core.ts'
import type { Filter } from './filter.ts'
import { isHex32 } from './utils.ts'
const M = 256
const HLL_HEX_LENGTH = M * 2
@@ -32,7 +33,7 @@ export function newHll(): Uint8Array {
}
export function hllDecode(hex: string): Uint8Array | undefined {
if (hex.length !== HLL_HEX_LENGTH || !/^[0-9a-fA-F]+$/.test(hex)) return undefined
if (hex.length !== HLL_HEX_LENGTH || !/^[0-9a-f]+$/.test(hex)) return undefined
const registers = new Uint8Array(M)
for (let i = 0; i < M; i++) {
@@ -54,9 +55,9 @@ export function hllEncode(registers: Uint8Array): string {
export function computeOffset(filterFirstTagValue: string): number {
let hex = filterFirstTagValue
if (!isHex64(hex)) {
if (!isHex32(hex)) {
const parts = hex.split(':')
if (parts.length === 3 && isHex64(parts[1])) {
if (parts.length === 3 && isHex32(parts[1])) {
hex = parts[1]
} else {
hex = bytesToHex(sha256(utf8Encoder.encode(filterFirstTagValue)))
@@ -79,7 +80,7 @@ export function getFilterFirstTagValue(filter: Filter): string | undefined {
export function feedPubkey(hll: Uint8Array, pubkey: string, offset: number): Uint8Array {
if (offset < 0 || offset > 24) throw new Error(`invalid offset ${offset}`)
if (!isHex64(pubkey)) throw new Error('pubkey must be 32-byte hex')
if (!isHex32(pubkey)) throw new Error('pubkey must be 32-byte hex')
if (hll.length === 0) hll = newHll()
if (hll.length !== M) throw new Error(`invalid number of registers ${hll.length}`)
@@ -123,10 +124,6 @@ export function estimateCount(hll: Uint8Array): number {
return Math.floor(estimate)
}
function isHex64(value: string): boolean {
return /^[0-9a-fA-F]{64}$/.test(value)
}
function countLeadingZeroBitsAfterOffset(pubkey: string, offset: number): number {
let zeroBits = 0
for (let i = offset + 1; i < offset + 8; i++) {

View File

@@ -1,7 +1,7 @@
import { bech32 } from '@scure/base'
import { NostrEvent, validateEvent, verifyEvent, type Event, type EventTemplate } from './pure.ts'
import { utf8Decoder } from './utils.ts'
import { isHex32, utf8Decoder } from './utils.ts'
import { isReplaceableKind, isAddressableKind } from './kinds.ts'
var _fetch: any
@@ -100,10 +100,10 @@ export function validateZapRequest(zapRequestString: string): string | null {
let p = zapRequest.tags.find(([t, v]) => t === 'p' && v)
if (!p) return "Zap request doesn't have a 'p' tag."
if (!p[1].match(/^[a-f0-9]{64}$/)) return "Zap request 'p' tag is not valid hex."
if (!isHex32(p[1])) return "Zap request 'p' tag is not valid hex."
let e = zapRequest.tags.find(([t, v]) => t === 'e' && v)
if (e && !e[1].match(/^[a-f0-9]{64}$/)) return "Zap request 'e' tag is not valid hex."
if (e && !isHex32(e[1])) return "Zap request 'e' tag is not valid hex."
let relays = zapRequest.tags.find(([t, v]) => t === 'relays' && v)
if (!relays) return "Zap request doesn't have a 'relays' tag."

View File

@@ -3,6 +3,7 @@ import { bytesToHex } from '@noble/hashes/utils.js'
import type { EventTemplate } from './core.ts'
import type { Signer } from './signer.ts'
import { kinds } from './index.ts'
import { isHex32 } from './utils.ts'
export type BlobDescriptor = {
url: string
@@ -27,10 +28,6 @@ export type AuthEventOptions = {
expiration?: number
}
export function isSha256(str: string): boolean {
return /^[0-9a-f]{64}$/i.test(str)
}
export function getBlobSize(blob: UploadType): number {
if ((typeof File !== 'undefined' && blob instanceof File) || blob instanceof Blob) {
return blob.size
@@ -213,7 +210,7 @@ export function parseBlossomURI(uri: string): BlossomURI {
if (dotIndex === -1) throw new Error('Invalid blossom URI: missing file extension')
const sha256 = path.slice(0, dotIndex)
const ext = path.slice(dotIndex + 1)
if (!isSha256(sha256)) throw new Error('Invalid blossom URI: invalid sha256 hash')
if (!isHex32(sha256)) throw new Error('Invalid blossom URI: invalid sha256 hash')
if (!ext) throw new Error('Invalid blossom URI: empty file extension')
const params = new URLSearchParams(query)
const servers = params.getAll('xs')
@@ -250,7 +247,7 @@ export function blossomURIFromURL(url: URL): BlossomURI {
if (dotIndex === -1) throw new Error('Invalid blossom URL: missing file extension')
const sha256 = path.slice(0, dotIndex)
const ext = path.slice(dotIndex + 1)
if (!isSha256(sha256)) throw new Error('Invalid blossom URL: invalid sha256 hash')
if (!isHex32(sha256)) throw new Error('Invalid blossom URL: invalid sha256 hash')
if (!ext) throw new Error('Invalid blossom URL: empty file extension')
const servers = url.searchParams.getAll('xs')
const authors = url.searchParams.getAll('as')
@@ -743,7 +740,7 @@ export class BlossomClient {
}
async check(hash: string): Promise<void> {
if (!isSha256(hash)) throw new Error(`${hash} is not valid 32-byte hex`)
if (!isHex32(hash)) throw new Error(`${hash} is not valid 32-byte hex`)
await this.httpCall('HEAD', hash)
}

View File

@@ -1,7 +1,7 @@
import { sha256 } from '@noble/hashes/sha2.js'
import { EventTemplate } from './core.ts'
import { Signer } from './signer.ts'
import { bytesToHex } from './utils.ts'
import { bytesToHex, isHex32 } from './utils.ts'
export type BlobDescriptor = {
url: string
@@ -84,12 +84,8 @@ export class BlossomClient {
}
}
private isValid32ByteHex(hash: string): boolean {
return /^[a-f0-9]{64}$/i.test(hash)
}
async check(hash: string): Promise<void> {
if (!this.isValid32ByteHex(hash)) {
if (!isHex32(hash)) {
throw new Error(`${hash} is not a valid 32-byte hex string`)
}
@@ -125,7 +121,7 @@ export class BlossomClient {
}
async download(hash: string): Promise<ArrayBuffer> {
if (!this.isValid32ByteHex(hash)) {
if (!isHex32(hash)) {
throw new Error(`${hash} is not a valid 32-byte hex string`)
}
@@ -156,7 +152,7 @@ export class BlossomClient {
async list(): Promise<BlobDescriptor[]> {
const pubkey = await this.signer.getPublicKey()
if (!this.isValid32ByteHex(pubkey)) {
if (!isHex32(pubkey)) {
throw new Error(`pubkey ${pubkey} is not valid`)
}
@@ -179,7 +175,7 @@ export class BlossomClient {
}
async delete(hash: string): Promise<void> {
if (!this.isValid32ByteHex(hash)) {
if (!isHex32(hash)) {
throw new Error(`${hash} is not a valid 32-byte hex string`)
}

View File

@@ -123,3 +123,16 @@ export function mergeReverseSortedLists(list1: NostrEvent[], list2: NostrEvent[]
return result
}
/**
* Checks if a string is a 64-char lowercase hex string as most Nostr ids and pubkeys.
*/
export function isHex32(input: string): boolean {
for (let i = 0; i < 64; i++) {
let cc = input.charCodeAt(i)
if (isNaN(cc) || cc < 48 || cc > 102 || (cc > 57 && cc < 97)) {
return false
}
}
return true
}