Skip to content

Commit

Permalink
Merge pull request #127 from effigies/fix/nifti_headers
Browse files Browse the repository at this point in the history
fix: Truncate NIfTI header data to avoid extensions
  • Loading branch information
nellh authored Dec 6, 2024
2 parents 9e8da14 + 9963e7d commit a24486c
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 13 deletions.
49 changes: 49 additions & 0 deletions changelog.d/20241204_103403_effigies_nifti_headers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
-->

<!--
### Added
- A bullet item for the Added category.
-->
<!--
### Changed
- A bullet item for the Changed category.
-->
### 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

<!--
### Deprecated
- A bullet item for the Deprecated category.
-->
<!--
### Removed
- A bullet item for the Removed category.
-->
<!--
### Security
- A bullet item for the Security category.
-->
<!--
### Infrastructure
- A bullet item for the Infrastructure category.
-->
7 changes: 5 additions & 2 deletions src/files/deno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uint8Array> {
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)
}

/**
Expand Down
19 changes: 19 additions & 0 deletions src/files/nifti.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
})
})
34 changes: 23 additions & 11 deletions src/files/nifti.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -11,27 +11,39 @@ async function extract(buffer: Uint8Array, nbytes: number): Promise<Uint8Array>
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<NiftiHeader> {
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' }
}
Expand Down
Binary file added tests/data/big_header.nii.gz
Binary file not shown.

0 comments on commit a24486c

Please sign in to comment.