Use variable names to store variables in the filesystem (vs. VariablesRecord).
[igor.git] / igor / packed.py
1 # Copyright
2
3 "Read IGOR Packed Experiment files files into records."
4
5 from .struct import Structure as _Structure
6 from .struct import Field as _Field
7 from .util import byte_order as _byte_order
8 from .util import need_to_reorder_bytes as _need_to_reorder_bytes
9 from .record import RECORD_TYPE as _RECORD_TYPE
10 from .record.base import UnknownRecord as _UnknownRecord
11 from .record.base import UnusedRecord as _UnusedRecord
12 from .record.folder import FolderStartRecord as _FolderStartRecord
13 from .record.folder import FolderEndRecord as _FolderEndRecord
14 from .record.variables import VariablesRecord as _VariablesRecord
15 from .record.wave import WaveRecord as _WaveRecord
16
17
18 # From PTN003:
19 # Igor writes other kinds of records in a packed experiment file, for
20 # storing things like pictures, page setup records, and miscellaneous
21 # settings.  The format for these records is quite complex and is not
22 # described in PTN003.  If you are writing a program to read packed
23 # files, you must skip any record with a record type that is not
24 # listed above.
25
26 PackedFileRecordHeader = _Structure(
27     name='PackedFileRecordHeader',
28     fields=[
29         _Field('H', 'recordType', help='Record type plus superceded flag.'),
30         _Field('h', 'version', help='Version information depends on the type of record.'),
31         _Field('l', 'numDataBytes', help='Number of data bytes in the record following this record header.'),
32         ])
33
34 #CR_STR = '\x15'  (\r)
35
36 PACKEDRECTYPE_MASK = 0x7FFF  # Record type = (recordType & PACKEDREC_TYPE_MASK)
37 SUPERCEDED_MASK = 0x8000  # Bit is set if the record is superceded by
38                           # a later record in the packed file.
39
40
41 def load(filename, strict=True, ignore_unknown=True):
42     records = []
43     if hasattr(filename, 'read'):
44         f = filename  # filename is actually a stream object
45     else:
46         f = open(filename, 'rb')
47     byte_order = None
48     initial_byte_order = '='
49     try:
50         while True:
51             b = buffer(f.read(PackedFileRecordHeader.size))
52             if not b:
53                 break
54             PackedFileRecordHeader.set_byte_order(initial_byte_order)
55             header = PackedFileRecordHeader.unpack_from(b)
56             if header['version'] and not byte_order:
57                 need_to_reorder = _need_to_reorder_bytes(header['version'])
58                 byte_order = initial_byte_order = _byte_order(need_to_reorder)
59                 if need_to_reorder:
60                     PackedFileRecordHeader.set_byte_order(byte_order)
61                     header = PackedFileRecordHeader.unpack_from(b)
62             data = buffer(f.read(header['numDataBytes']))
63             record_type = _RECORD_TYPE.get(
64                 header['recordType'] & PACKEDRECTYPE_MASK, _UnknownRecord)
65             if record_type in [_UnknownRecord, _UnusedRecord
66                                ] and not ignore_unknown:
67                 raise KeyError('unkown record type {}'.format(
68                         header['recordType']))
69             records.append(record_type(header, data, byte_order=byte_order))
70     finally:
71         if not hasattr(filename, 'read'):
72             f.close()
73
74     filesystem = _build_filesystem(records)
75
76     return (records, filesystem)
77
78 def _build_filesystem(records):
79     # From PTN003:
80     """The name must be a valid Igor data folder name. See Object
81     Names in the Igor Reference help file for name rules.
82
83     When Igor Pro reads the data folder start record, it creates a new
84     data folder with the specified name. Any subsequent variable, wave
85     or data folder start records cause Igor to create data objects in
86     this new data folder, until Igor Pro reads a corresponding data
87     folder end record."""
88     # From the Igor Manual, chapter 2, section 8, page II-123
89     # http://www.wavemetrics.net/doc/igorman/II-08%20Data%20Folders.pdf
90     """Like the Macintosh file system, Igor Pro's data folders use the
91     colon character (:) to separate components of a path to an
92     object. This is analogous to Unix which uses / and Windows which
93     uses \. (Reminder: Igor's data folders exist wholly in memory
94     while an experiment is open. It is not a disk file system!)
95
96     A data folder named "root" always exists and contains all other
97     data folders.
98     """
99     # From the Igor Manual, chapter 4, page IV-2
100     # http://www.wavemetrics.net/doc/igorman/IV-01%20Commands.pdf
101     """For waves and data folders only, you can also use "liberal"
102     names. Liberal names can include almost any character, including
103     spaces and dots (see Liberal Object Names on page III-415 for
104     details).
105     """
106     # From the Igor Manual, chapter 3, section 16, page III-416
107     # http://www.wavemetrics.net/doc/igorman/III-16%20Miscellany.pdf
108     """Liberal names have the same rules as standard names except you
109     may use any character except control characters and the following:
110
111       " ' : ;
112     """
113     filesystem = {'root': {}}
114     dir_stack = [('root', filesystem['root'])]
115     for record in records:
116         cwd = dir_stack[-1][-1]
117         if isinstance(record, _FolderStartRecord):
118             name = record.null_terminated_text
119             cwd[name] = {}
120             dir_stack.append((name, cwd[name]))
121         elif isinstance(record, _FolderEndRecord):
122             dir_stack.pop()
123         elif isinstance(record, (_VariablesRecord, _WaveRecord)):
124             if isinstance(record, _VariablesRecord):
125                 _add_variables(dir_stack, cwd, record)
126                 # start with an invalid character to avoid collisions
127                 # with folder names
128                 #filename = ':variables'
129                 #_check_filename(dir_stack, filename)
130                 #cwd[filename] = record
131             else:  # WaveRecord
132                 filename = ''.join(c for c in record.wave_info['bname']
133                                    ).split('\x00', 1)[0]
134                 _check_filename(dir_stack, filename)
135                 cwd[filename] = record
136     return filesystem
137
138 def _check_filename(dir_stack, filename):
139     cwd = dir_stack[-1][-1]
140     if filename in cwd:
141         raise ValueError('collision on name {} in {}'.format(
142                 filename, ':'.join(d for d,cwd in dir_stack)))
143
144 def _add_variables(dir_stack, cwd, record):
145     if len(dir_stack) == 1:
146         # From PTN003:
147         """When reading a packed file, any system variables
148         encountered while the current data folder is not the root
149         should be ignored.
150         """
151         for i,value in enumerate(record.variables['sysVars']):
152             name = 'K{}'.format(i)
153             _check_filename(dir_stack, name)
154             cwd[name] = value
155     for name,value in (
156         record.variables['userVars'].items() +
157         record.variables['userStrs'].items()):
158         _check_filename(dir_stack, name)
159         cwd[name] = value
160     if record.variables['header']['version'] == 2:
161         raise NotImplementedError('add dependent variables to filesystem')