Compare commits

...

2 Commits

Author SHA1 Message Date
Matt Miller
13cd0420f4 fix: guard getFromFlacFile parse against malformed data
getFromFlacBuffer can throw (DataView out-of-bounds on truncated/malformed
metadata), which now rejects the async promise instead of resolving {}. Wrap
the parse in try/catch to match the avif/ebml/gltf/isobmff refactors and keep
the resolve-only contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:06:14 -07:00
Matt Miller
d1f7a71809 refactor: extract readFileAsArrayBuffer helper for metadata extractors
Collapse the duplicated FileReader read-to-buffer shell (new Promise ->
new FileReader -> onload/onerror/onabort -> readAsArrayBuffer) into a
single readFileAsArrayBuffer(file, maxBytes?) helper that resolves null
on read error/abort/no-result. Apply it to the avif, isobmff, ebml,
gltf, and flac extractors so each keeps only its own parse + fallback
body. Lets flac drop its read-step @ts-expect-error since the helper
null-guards the result.

png.ts is intentionally left as-is (it rejects rather than resolving a
fallback, an opposite semantic callers may depend on).
2026-07-02 12:55:51 -07:00
8 changed files with 141 additions and 110 deletions

View File

@@ -12,6 +12,8 @@ import {
} from '@/types/metadataTypes'
import { parseJsonWithNonFinite } from '@/utils/jsonUtil'
import { readFileAsArrayBuffer } from './readFile'
const readNullTerminatedString = (
dataView: DataView,
start: number,
@@ -388,36 +390,24 @@ function parseExifData(exifData) {
return ifdData
}
export function getFromAvifFile(file: File): Promise<Record<string, string>> {
return new Promise<Record<string, string>>((resolve) => {
const reader = new FileReader()
reader.onload = (event) => {
const buffer = event.target?.result as ArrayBuffer
if (!buffer) {
resolve({})
return
}
export async function getFromAvifFile(
file: File
): Promise<Record<string, string>> {
const buffer = await readFileAsArrayBuffer(file)
if (!buffer) return {}
try {
const comfyMetadata = parseAvifMetadata(buffer)
const result: Record<string, string> = {}
if (comfyMetadata.prompt) {
result.prompt = JSON.stringify(comfyMetadata.prompt)
}
if (comfyMetadata.workflow) {
result.workflow = JSON.stringify(comfyMetadata.workflow)
}
resolve(result)
} catch (e) {
console.error('Parser: Error parsing AVIF metadata:', e)
resolve({})
}
try {
const comfyMetadata = parseAvifMetadata(buffer)
const result: Record<string, string> = {}
if (comfyMetadata.prompt) {
result.prompt = JSON.stringify(comfyMetadata.prompt)
}
reader.onerror = (err) => {
console.error('FileReader: Error reading AVIF file:', err)
resolve({})
if (comfyMetadata.workflow) {
result.workflow = JSON.stringify(comfyMetadata.workflow)
}
reader.onabort = () => resolve({})
reader.readAsArrayBuffer(file)
})
return result
} catch (e) {
console.error('Parser: Error parsing AVIF metadata:', e)
return {}
}
}

View File

@@ -12,6 +12,8 @@ import {
} from '@/types/metadataTypes'
import { parseJsonWithNonFinite } from '@/utils/jsonUtil'
import { readFileAsArrayBuffer } from './readFile'
const WEBM_SIGNATURE = [0x1a, 0x45, 0xdf, 0xa3]
const MAX_READ_BYTES = 2 * 1024 * 1024
const EBML_ID = {
@@ -325,25 +327,14 @@ const parseMetadata = (data: Uint8Array): ComfyMetadata => {
return meta
}
const handleFileLoad = (
event: ProgressEvent<FileReader>,
resolve: (value: ComfyMetadata) => void
) => {
if (!event.target?.result) {
resolve({})
return
}
const parseWebmBuffer = (buffer: ArrayBuffer): ComfyMetadata => {
try {
const data = new Uint8Array(event.target.result as ArrayBuffer)
if (data.length < 4 || !hasWebmSignature(data)) {
resolve({})
return
}
const data = new Uint8Array(buffer)
if (data.length < 4 || !hasWebmSignature(data)) return {}
resolve(parseMetadata(data))
return parseMetadata(data)
} catch {
resolve({})
return {}
}
}
@@ -351,12 +342,9 @@ const handleFileLoad = (
* Extracts ComfyUI Workflow metadata from a WebM file
* @param file - The WebM file to extract metadata from
*/
export function getFromWebmFile(file: File): Promise<ComfyMetadata> {
return new Promise<ComfyMetadata>((resolve) => {
const reader = new FileReader()
reader.onload = (event) => handleFileLoad(event, resolve)
reader.onerror = () => resolve({})
reader.onabort = () => resolve({})
reader.readAsArrayBuffer(file.slice(0, MAX_READ_BYTES))
})
export async function getFromWebmFile(file: File): Promise<ComfyMetadata> {
const buffer = await readFileAsArrayBuffer(file, MAX_READ_BYTES)
if (!buffer) return {}
return parseWebmBuffer(buffer)
}

View File

@@ -53,4 +53,14 @@ describe('FLAC metadata', () => {
expect(result).toEqual({})
})
})
it('resolves empty when parsing throws on malformed data', async () => {
vi.spyOn(console, 'error').mockImplementation(() => {})
const malformed = new Uint8Array([0x66, 0x4c, 0x61, 0x43, 0xff, 0xff])
const file = new File([malformed], 'malformed.flac')
const result = await getFromFlacFile(file)
expect(result).toEqual({})
})
})

View File

@@ -1,3 +1,5 @@
import { readFileAsArrayBuffer } from './readFile'
export function getFromFlacBuffer(buffer: ArrayBuffer): Record<string, string> {
const dataView = new DataView(buffer)
@@ -33,18 +35,18 @@ export function getFromFlacBuffer(buffer: ArrayBuffer): Record<string, string> {
return vorbisComment
}
export function getFromFlacFile(file: File): Promise<Record<string, string>> {
return new Promise((r) => {
const reader = new FileReader()
reader.onload = function (event) {
// @ts-expect-error fixme ts strict error
const arrayBuffer = event.target.result as ArrayBuffer
r(getFromFlacBuffer(arrayBuffer))
}
reader.onerror = () => r({})
reader.onabort = () => r({})
reader.readAsArrayBuffer(file)
})
export async function getFromFlacFile(
file: File
): Promise<Record<string, string>> {
const buffer = await readFileAsArrayBuffer(file)
if (!buffer) return {}
try {
return getFromFlacBuffer(buffer)
} catch (e) {
console.error('Parser: Error parsing FLAC metadata:', e)
return {}
}
}
// Function to parse the Vorbis Comment block

View File

@@ -13,6 +13,8 @@ import {
} from '@/types/metadataTypes'
import { parseJsonWithNonFinite } from '@/utils/jsonUtil'
import { readFileAsArrayBuffer } from './readFile'
const MAX_READ_BYTES = 1 << 20
const isJsonChunk = (chunk: GltfChunkHeader | null): boolean =>
@@ -141,27 +143,15 @@ const processGltfFileBuffer = (buffer: ArrayBuffer): ComfyMetadata => {
/**
* Extract ComfyUI metadata from a GLTF binary file (GLB)
*/
export function getGltfBinaryMetadata(file: File): Promise<ComfyMetadata> {
return new Promise<ComfyMetadata>((resolve) => {
if (!file) return Promise.resolve({})
export async function getGltfBinaryMetadata(
file: File
): Promise<ComfyMetadata> {
const buffer = await readFileAsArrayBuffer(file, MAX_READ_BYTES)
if (!buffer) return {}
const bytesToRead = Math.min(file.size, MAX_READ_BYTES)
const reader = new FileReader()
reader.onload = (event) => {
try {
if (!event.target?.result) {
resolve({})
return
}
resolve(processGltfFileBuffer(event.target.result as ArrayBuffer))
} catch {
resolve({})
}
}
reader.onerror = () => resolve({})
reader.onabort = () => resolve({})
reader.readAsArrayBuffer(file.slice(0, bytesToRead))
})
try {
return processGltfFileBuffer(buffer)
} catch {
return {}
}
}

View File

@@ -10,6 +10,8 @@ import {
} from '@/types/metadataTypes'
import { parseJsonWithNonFinite } from '@/utils/jsonUtil'
import { readFileAsArrayBuffer } from './readFile'
// Set max read high, as atoms are stored near end of file
// while search is made to be efficient.
const MAX_READ_BYTES = 64 * 1024 * 1024
@@ -256,28 +258,14 @@ const parseIsobmffMetadata = (data: Uint8Array): ComfyMetadata => {
* (e.g., MP4, MOV) by parsing the `udta.meta.keys` and `udta.meta.ilst` boxes.
* @param file - The file to extract metadata from.
*/
export function getFromIsobmffFile(file: File): Promise<ComfyMetadata> {
return new Promise<ComfyMetadata>((resolve) => {
const reader = new FileReader()
reader.onload = (event: ProgressEvent<FileReader>) => {
if (!event.target?.result) {
resolve({})
return
}
export async function getFromIsobmffFile(file: File): Promise<ComfyMetadata> {
const buffer = await readFileAsArrayBuffer(file, MAX_READ_BYTES)
if (!buffer) return {}
try {
const data = new Uint8Array(event.target.result as ArrayBuffer)
resolve(parseIsobmffMetadata(data))
} catch (e) {
console.error('Parser: Error parsing ISOBMFF metadata:', e)
resolve({})
}
}
reader.onerror = (err) => {
console.error('FileReader: Error reading ISOBMFF file:', err)
resolve({})
}
reader.onabort = () => resolve({})
reader.readAsArrayBuffer(file.slice(0, MAX_READ_BYTES))
})
try {
return parseIsobmffMetadata(new Uint8Array(buffer))
} catch (e) {
console.error('Parser: Error parsing ISOBMFF metadata:', e)
return {}
}
}

View File

@@ -0,0 +1,41 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
mockFileReaderAbort,
mockFileReaderError
} from './__fixtures__/helpers'
import { readFileAsArrayBuffer } from './readFile'
describe('readFileAsArrayBuffer', () => {
afterEach(() => vi.restoreAllMocks())
it('reads the whole file into an ArrayBuffer when no cap is given', async () => {
const bytes = new Uint8Array([1, 2, 3, 4, 5])
const file = new File([bytes], 'test.bin')
const buffer = await readFileAsArrayBuffer(file)
expect(buffer).toBeInstanceOf(ArrayBuffer)
expect(new Uint8Array(buffer!)).toEqual(bytes)
})
it('reads only the first maxBytes when a cap is given', async () => {
const file = new File([new Uint8Array(100)], 'test.bin')
const buffer = await readFileAsArrayBuffer(file, 10)
expect(buffer?.byteLength).toBe(10)
})
it('resolves null when the read fires an error', async () => {
mockFileReaderError('readAsArrayBuffer')
expect(await readFileAsArrayBuffer(new File([], 'test.bin'))).toBeNull()
})
it('resolves null when the read is aborted', async () => {
mockFileReaderAbort('readAsArrayBuffer')
expect(await readFileAsArrayBuffer(new File([], 'test.bin'))).toBeNull()
})
})

View File

@@ -0,0 +1,22 @@
/**
* Reads a file (optionally capped to its first `maxBytes`) into an ArrayBuffer.
* Resolves `null` when the read errors, aborts, or yields no result, so callers
* can guard with a single truthiness check before parsing.
* @param file - The file to read.
* @param maxBytes - Optional cap; when set, only the first `maxBytes` are read.
*/
export function readFileAsArrayBuffer(
file: File,
maxBytes?: number
): Promise<ArrayBuffer | null> {
return new Promise<ArrayBuffer | null>((resolve) => {
const reader = new FileReader()
reader.onload = () => {
resolve(reader.result instanceof ArrayBuffer ? reader.result : null)
}
reader.onerror = () => resolve(null)
reader.onabort = () => resolve(null)
const blob = maxBytes === undefined ? file : file.slice(0, maxBytes)
reader.readAsArrayBuffer(blob)
})
}