Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
0.5.16
- fix: read Igor Pro 8 binary wave version 7 long wave names (#32)
0.5.15
- fix: read Igor Pro 8 long data-folder names (#28, #30)
- tests: add trimmed Igor Pro 8 experiment from #28
Expand Down
44 changes: 44 additions & 0 deletions igor2/binarywave.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,35 @@ def post_unpack(self, parents, data):
wave_data[self.name] = dim_labels


class DynamicLongWaveNameField (DynamicStringField):
"""Igor Pro 8 name for a wave that does not fit in 31 bytes.

Version 7 keeps the version 5 layout and stores the name after the
last version 5 section. ``optionsSize1``, which TN003 says to
ignore, is the byte count. The bytes are not NUL-terminated. Igor
writes ``:wave name too long:`` in ``bname`` when this name is
present.
"""

_size_field = 'optionsSize1'

def pre_unpack(self, parents, data):
size = self._get_size_data(parents, data)
if size < 0:
raise ValueError(
'long wave name length {} is negative'.format(size))
super(DynamicLongWaveNameField, self).pre_unpack(parents, data)

def post_unpack(self, parents, data):
super(DynamicLongWaveNameField, self).post_unpack(parents, data)
wave_structure = parents[-1]
wave_data = self._get_structure_data(parents, data, wave_structure)
name = wave_data[self.name]
placeholder = b':wave name too long:'
if name and wave_data['wave_header']['bname'] == placeholder:
wave_data['wave_header']['bname'] = name


class DynamicStringIndicesDataField (_DynamicField):
"""String indices used for text waves only
"""
Expand Down Expand Up @@ -633,6 +662,7 @@ def post_unpack(self, parents, data):
2: Wave2,
3: Wave3,
5: Wave5,
7: Wave7,
})
old_format = wave_structure.fields[-1].format
if version in version_map:
Expand Down Expand Up @@ -797,11 +827,24 @@ def post_unpack(self, parents, data):
])


# Version 7 is version 5 plus the long name. Built from a clone so the
# version 5 field objects stay independent.
Wave7 = _clone_structure(Wave5)
Wave7.name = 'Wave7'
Wave7.fields.append(DynamicLongWaveNameField(
'c', 'long_name',
help='Long wave name stored after the version 5 sections.',
count=0,
array=True))
Wave7.setup()


def setup_wave(byte_order='='):
wave1 = _clone_structure(Wave1)
wave2 = _clone_structure(Wave2)
wave3 = _clone_structure(Wave3)
wave5 = _clone_structure(Wave5)
wave7 = _clone_structure(Wave7)
wave = _DynamicStructure(
name='Wave',
fields=[
Expand All @@ -820,6 +863,7 @@ def setup_wave(byte_order='='):
2: wave2,
3: wave3,
5: wave5,
7: wave7,
}
wave.setup()
return wave
Expand Down
33 changes: 33 additions & 0 deletions tests/helper.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,43 @@
import pathlib
import struct

import numpy as _numpy
from pprint import PrettyPrinter
from igor2.binarywave import load as loadibw

data_dir = pathlib.Path(__file__).parent / "data"

# Igor writes this in the 32-byte bname field when the real name follows
# the version 5 sections. See issue 32.
LONG_WAVE_NAME_PLACEHOLDER = b':wave name too long:'


def version7_from_version5(source, name, order, replace_bname=True):
"""Return version 5 bytes relabeled as a version 7 long-name wave.

``name`` is appended after the version 5 sections and recorded in
``optionsSize1``. The numeric payload is left unchanged.
"""
data = bytearray(source)
if order == '<':
if data[:2] != b'\x05\x00':
raise ValueError(data[:2])
data[:2] = b'\x07\x00'
data[56:60] = struct.pack('<i', len(name))
elif order == '>':
if data[:2] != b'\x00\x05':
raise ValueError(data[:2])
data[:2] = b'\x00\x07'
data[56:60] = struct.pack('>i', len(name))
else:
raise ValueError(order)
if replace_bname:
field = LONG_WAVE_NAME_PLACEHOLDER
data[92:124] = field + b'\x00' * (32 - len(field))
if name:
data.extend(name)
return bytes(data)


class NumpyPrettyPrinter(PrettyPrinter):
def format(self, object, context, maxlevels, level):
Expand Down
76 changes: 75 additions & 1 deletion tests/test_ibw.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from io import BytesIO

import numpy as np

from helper import assert_equal_dump_no_whitespace_no_byte, dumpibw
from helper import data_dir, loadibw
from helper import data_dir, loadibw, version7_from_version5


def test_ibw01():
Expand Down Expand Up @@ -641,3 +645,73 @@ def test_multidimensional_text_wave():
[b'dog', b'wolf'],
[b'fish', b'shark'],
]


def test_version7_long_wave_name():
# 32-byte name from issue 32, plus a longer name that cannot fit
# in the version 5 bname field even without a trailing NUL.
issue_name = b'r_ReactedQ_1p73Inlet_0006_270_30'
longer_name = issue_name + b'_extra'
assert len(issue_name) == 32
cases = (
('<', 'win-version5.ibw'),
('>', 'mac-version5.ibw'),
)
for order, filename in cases:
original = (data_dir / filename).read_bytes()
original_wave = loadibw(BytesIO(original))['wave']
for name in (issue_name, longer_name):
loaded = loadibw(BytesIO(
version7_from_version5(original, name, order)))
assert loaded['version'] == 7
wave = loaded['wave']
assert wave['wave_header']['bname'] == name
assert wave['long_name'] == name
assert wave['bin_header']['optionsSize1'] == len(name)
assert wave['note'] == original_wave['note']
assert np.array_equal(wave['wData'], original_wave['wData'])


def test_version7_without_long_name_keeps_short_bname():
original = (data_dir / 'win-version5.ibw').read_bytes()
renamed = bytearray(original)
renamed[:2] = b'\x07\x00'
loaded = loadibw(BytesIO(bytes(renamed)))
original_wave = loadibw(BytesIO(original))['wave']
assert loaded['version'] == 7
assert loaded['wave']['wave_header']['bname'] == b'version5'
assert loaded['wave']['long_name'] == b''
assert np.array_equal(
loaded['wave']['wData'], original_wave['wData'])


def test_version7_keeps_bname_when_it_is_not_the_placeholder():
original = (data_dir / 'win-version5.ibw').read_bytes()
extra = b'not-the-wave-name'
loaded = loadibw(BytesIO(version7_from_version5(
original, extra, '<', replace_bname=False)))
assert loaded['wave']['wave_header']['bname'] == b'version5'
assert loaded['wave']['long_name'] == extra


def test_version7_rejects_negative_name_length():
original = bytearray((data_dir / 'win-version5.ibw').read_bytes())
original[:2] = b'\x07\x00'
original[56:60] = b'\xff\xff\xff\xff'
try:
loadibw(BytesIO(bytes(original)))
except ValueError as exc:
assert 'negative' in str(exc)
else:
raise AssertionError('negative long wave name length was accepted')


def test_version7_rejects_truncated_name():
original = (data_dir / 'win-version5.ibw').read_bytes()
blob = version7_from_version5(original, b'abcdef', '<')[:-3]
try:
loadibw(BytesIO(blob))
except ValueError as exc:
assert 'not enough data' in str(exc)
else:
raise AssertionError('truncated long wave name was accepted')
18 changes: 17 additions & 1 deletion tests/test_pxp.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from igor2.binarywave import load as loadibw
from igor2.packed import load as loadpxp

from helper import data_dir
from helper import data_dir, version7_from_version5


def tostr(data):
Expand Down Expand Up @@ -320,6 +320,22 @@ def test_igor8_long_folder_name():
assert children[short_name] == {}


def test_version7_long_wave_name_in_packed_experiment():
name = b'r_ReactedQ_1p73Inlet_0006_270_30_more'
cases = (
('<', 'win-version5.ibw'),
('>', 'mac-version5.ibw'),
)
for order, filename in cases:
payload = version7_from_version5(
(data_dir / filename).read_bytes(), name, order)
records, filesystem = loadpxp(BytesIO(
_pxp_record(3, 1, payload, order)))
assert set(filesystem['root']) == {name}
assert records[0].wave['wave']['wave_header']['bname'] == name
assert records[0].wave['wave']['wData'].shape == (5,)


def test_long_folder_name_rejects_bad_length():
payload = struct.pack('<I', 3) + b'abcd'
blob = _pxp_record(9, 1, _short_folder_name(b'short'), '<')
Expand Down
Loading