Skip to content

Commit 69307d3

Browse files
authored
Merge pull request #1140 from anibalsolon/fix/invalid_tck_handling
FIX: Handles invalid TCK files, prevents infinite loop
2 parents feb2063 + a4c420d commit 69307d3

File tree

5 files changed

+79
-19
lines changed

5 files changed

+79
-19
lines changed

nibabel/streamlines/tck.py

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,10 @@ def is_correct_format(cls, fileobj):
9191
otherwise returns False.
9292
"""
9393
with Opener(fileobj) as f:
94-
magic_number = asstr(f.fobj.readline())
95-
f.seek(-len(magic_number), os.SEEK_CUR)
94+
magic_number = f.read(len(cls.MAGIC_NUMBER))
95+
f.seek(-len(cls.MAGIC_NUMBER), os.SEEK_CUR)
9696

97-
return magic_number.strip() == cls.MAGIC_NUMBER
97+
return asstr(magic_number) == cls.MAGIC_NUMBER
9898

9999
@classmethod
100100
def create_empty_header(cls):
@@ -287,8 +287,8 @@ def _write_header(fileobj, header):
287287
fileobj.write(asbytes(str(new_offset) + "\n"))
288288
fileobj.write(asbytes("END\n"))
289289

290-
@staticmethod
291-
def _read_header(fileobj):
290+
@classmethod
291+
def _read_header(cls, fileobj):
292292
""" Reads a TCK header from a file.
293293
294294
Parameters
@@ -304,23 +304,56 @@ def _read_header(fileobj):
304304
header : dict
305305
Metadata associated with this tractogram file.
306306
"""
307-
# Record start position if this is a file-like object
308-
start_position = fileobj.tell() if hasattr(fileobj, 'tell') else None
307+
308+
# Build header dictionary from the buffer
309+
hdr = {}
310+
offset_data = 0
309311

310312
with Opener(fileobj) as f:
313+
314+
# Record start position
315+
start_position = f.tell()
316+
317+
# Make sure we are at the beginning of the file
318+
f.seek(0, os.SEEK_SET)
319+
311320
# Read magic number
312-
magic_number = f.fobj.readline().strip()
321+
magic_number = f.read(len(cls.MAGIC_NUMBER))
322+
323+
if asstr(magic_number) != cls.MAGIC_NUMBER:
324+
raise HeaderError(f"Invalid magic number: {magic_number}")
325+
326+
hdr[Field.MAGIC_NUMBER] = magic_number
313327

314-
# Read all key-value pairs contained in the header.
315-
buf = asstr(f.fobj.readline())
316-
while not buf.rstrip().endswith("END"):
317-
buf += asstr(f.fobj.readline())
328+
f.seek(1, os.SEEK_CUR) # Skip \n
329+
330+
found_end = False
331+
332+
# Read all key-value pairs contained in the header, stop at EOF
333+
for n_line, line in enumerate(f, 1):
334+
line = asstr(line).strip()
335+
336+
if not line: # Skip empty lines
337+
continue
338+
339+
if line == "END": # End of the header
340+
found_end = True
341+
break
342+
343+
if ':' not in line: # Invalid header line
344+
raise HeaderError(f"Invalid header (line {n_line}): {line}")
345+
346+
key, value = line.split(":", 1)
347+
hdr[key.strip()] = value.strip()
348+
349+
if not found_end:
350+
raise HeaderError("Missing END in the header.")
318351

319352
offset_data = f.tell()
320353

321-
# Build header dictionary from the buffer.
322-
hdr = dict(item.split(': ') for item in buf.rstrip().split('\n')[:-1])
323-
hdr[Field.MAGIC_NUMBER] = magic_number
354+
# Set the file position where it was, in case it was previously open
355+
if start_position is not None:
356+
f.seek(start_position, os.SEEK_SET)
324357

325358
# Check integrity of TCK header.
326359
if 'datatype' not in hdr:
@@ -352,10 +385,6 @@ def _read_header(fileobj):
352385
# Keep the file position where the data begin.
353386
hdr['_offset_data'] = int(hdr['file'].split()[1])
354387

355-
# Set the file position where it was, if it was previously open.
356-
if start_position is not None:
357-
fileobj.seek(start_position, os.SEEK_SET)
358-
359388
return hdr
360389

361390
@classmethod

nibabel/streamlines/tests/test_tck.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ def setup_module():
2525
global DATA
2626

2727
DATA['empty_tck_fname'] = pjoin(data_path, "empty.tck")
28+
DATA['no_magic_number_tck_fname'] = pjoin(data_path, "no_magic_number.tck")
29+
DATA['no_header_end_tck_fname'] = pjoin(data_path, "no_header_end.tck")
30+
DATA['no_header_end_eof_tck_fname'] = pjoin(data_path, "no_header_end_eof.tck")
2831
# simple.tck contains only streamlines
2932
DATA['simple_tck_fname'] = pjoin(data_path, "simple.tck")
3033
DATA['simple_tck_big_endian_fname'] = pjoin(data_path,
@@ -50,6 +53,30 @@ def test_load_empty_file(self):
5053
with pytest.warns(Warning) if lazy_load else error_warnings():
5154
assert_tractogram_equal(tck.tractogram, DATA['empty_tractogram'])
5255

56+
def test_load_no_magic_number_file(self):
57+
for lazy_load in [False, True]:
58+
with pytest.raises(HeaderError):
59+
TckFile.load(
60+
DATA['no_magic_number_tck_fname'],
61+
lazy_load=lazy_load
62+
)
63+
64+
def test_load_no_header_end_file(self):
65+
for lazy_load in [False, True]:
66+
with pytest.raises(HeaderError):
67+
TckFile.load(
68+
DATA['no_header_end_tck_fname'],
69+
lazy_load=lazy_load
70+
)
71+
72+
def test_load_no_header_end_eof_file(self):
73+
for lazy_load in [False, True]:
74+
with pytest.raises(HeaderError):
75+
TckFile.load(
76+
DATA['no_header_end_eof_tck_fname'],
77+
lazy_load=lazy_load
78+
)
79+
5380
def test_load_simple_file(self):
5481
for lazy_load in [False, True]:
5582
tck = TckFile.load(DATA['simple_tck_fname'], lazy_load=lazy_load)

nibabel/tests/data/no_header_end.tck

81 Bytes
Binary file not shown.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
mrtrix tracks
2+
count: 0000000000
3+
datatype: Float32LE
4+
file: . 67
71 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)