diff --git a/changelog.d/20241204_103403_effigies_nifti_headers.md b/changelog.d/20241204_103403_effigies_nifti_headers.md new file mode 100644 index 00000000..e5f74285 --- /dev/null +++ b/changelog.d/20241204_103403_effigies_nifti_headers.md @@ -0,0 +1,49 @@ + + + + +### Fixed + +- Resolve issue with parsing headers of NIfTI files with large extensions. + Fixes [issue 126]. + +[issue 126]: https://github.com/bids-standard/bids-validator/issues/126 + + + + + diff --git a/src/files/deno.ts b/src/files/deno.ts index 3895a0cd..ebae2dfb 100644 --- a/src/files/deno.ts +++ b/src/files/deno.ts @@ -93,14 +93,17 @@ export class BIDSFileDeno implements BIDSFile { /** * Read bytes in a range efficiently from a given file + * + * Reads up to size bytes, starting at offset. + * If EOF is encountered, the resulting array may be smaller. */ async readBytes(size: number, offset = 0): Promise { const handle = this.#openHandle() const buf = new Uint8Array(size) await handle.seek(offset, Deno.SeekMode.Start) - await handle.read(buf) + const nbytes = await handle.read(buf) ?? 0 handle.close() - return buf + return buf.subarray(0, nbytes) } /** diff --git a/src/files/nifti.test.ts b/src/files/nifti.test.ts index 909c49b5..df80f759 100644 --- a/src/files/nifti.test.ts +++ b/src/files/nifti.test.ts @@ -53,4 +53,23 @@ Deno.test('Test loading nifti header', async (t) => { }) assertObjectMatch(error, { key: 'NIFTI_HEADER_UNREADABLE' }) }) + + await t.step('Tolerate big headers', async () => { + const path = 'big_header.nii.gz' + const root = './tests/data/' + const file = new BIDSFileDeno(root, path, ignore) + let error: any = undefined + const header = await loadHeader(file) + assert(header !== undefined) + assertObjectMatch(header, { + dim: [3, 1, 1, 1, 1, 1, 1], + pixdim: [1, 1, 1, 1, 1, 1, 1], + shape: [1, 1, 1], + voxel_sizes: [1, 1, 1], + dim_info: { freq: 0, phase: 0, slice: 0 }, + xyzt_units: { xyz: 'unknown', t: 'unknown' }, + qform_code: 0, + sform_code: 2, + }) + }) }) diff --git a/src/files/nifti.ts b/src/files/nifti.ts index 58a05895..e3d58b57 100644 --- a/src/files/nifti.ts +++ b/src/files/nifti.ts @@ -1,4 +1,4 @@ -import { isCompressed, readHeader } from '@mango/nifti' +import { isCompressed, isNIFTI1, isNIFTI2, NIFTI1, NIFTI2 } from '@mango/nifti' import type { BIDSFile } from '../types/filetree.ts' import { logger } from '../utils/logger.ts' import type { NiftiHeader } from '@bids/schema/context' @@ -11,27 +11,39 @@ async function extract(buffer: Uint8Array, nbytes: number): Promise const stream = new ReadableStream({ start(controller) { controller.enqueue(buffer) + controller.close() }, }) const reader = stream.pipeThrough(new DecompressionStream('gzip')).getReader() let offset = 0 - while (offset < nbytes) { - const { value, done } = await reader.read() - if (done) { - break + try { + while (offset < nbytes) { + const { value, done } = await reader.read() + if (done || !value) { + break + } + result.set(value.subarray(0, Math.min(value.length, nbytes - offset)), offset) + offset += value.length } - result.set(value.subarray(0, Math.min(value.length, nbytes - offset)), offset) - offset += value.length + } finally { + await reader.cancel() } - await reader.cancel() - return result + return result.subarray(0, offset) } export async function loadHeader(file: BIDSFile): Promise { try { const buf = await file.readBytes(1024) - const data = isCompressed(buf.buffer) ? await extract(buf, 540) : buf - const header = readHeader(data.buffer) + const data = isCompressed(buf.buffer) ? await extract(buf, 540) : buf.slice(0, 540) + let header + if (isNIFTI1(data.buffer)) { + header = new NIFTI1() + // Truncate to 348 bytes to avoid attempting to parse extensions + header.readHeader(data.buffer.slice(0, 348)) + } else if (isNIFTI2(data.buffer)) { + header = new NIFTI2() + header.readHeader(data.buffer) + } if (!header) { throw { key: 'NIFTI_HEADER_UNREADABLE' } } diff --git a/tests/data/big_header.nii.gz b/tests/data/big_header.nii.gz new file mode 100644 index 00000000..9821d7f4 Binary files /dev/null and b/tests/data/big_header.nii.gz differ