Add igor.packed.walk for traversing a packed experiment filesystem.
[igor.git] / igor / packed.py
1 # Copyright (C) 2012 W. Trevor King <wking@tremily.us>
2 #
3 # This file is part of igor.
4 #
5 # igor is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU Lesser General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option) any
8 # later version.
9 #
10 # igor is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
13 # details.
14 #
15 # You should have received a copy of the GNU Lesser General Public License
16 # along with igor.  If not, see <http://www.gnu.org/licenses/>.
17
18 "Read IGOR Packed Experiment files files into records."
19
20 from . import LOG as _LOG
21 from .struct import Structure as _Structure
22 from .struct import Field as _Field
23 from .util import byte_order as _byte_order
24 from .util import need_to_reorder_bytes as _need_to_reorder_bytes
25 from .util import _bytes
26 from .record import RECORD_TYPE as _RECORD_TYPE
27 from .record.base import UnknownRecord as _UnknownRecord
28 from .record.base import UnusedRecord as _UnusedRecord
29 from .record.folder import FolderStartRecord as _FolderStartRecord
30 from .record.folder import FolderEndRecord as _FolderEndRecord
31 from .record.variables import VariablesRecord as _VariablesRecord
32 from .record.wave import WaveRecord as _WaveRecord
33
34
35 # From PTN003:
36 # Igor writes other kinds of records in a packed experiment file, for
37 # storing things like pictures, page setup records, and miscellaneous
38 # settings.  The format for these records is quite complex and is not
39 # described in PTN003.  If you are writing a program to read packed
40 # files, you must skip any record with a record type that is not
41 # listed above.
42
43 PackedFileRecordHeader = _Structure(
44     name='PackedFileRecordHeader',
45     fields=[
46         _Field('H', 'recordType', help='Record type plus superceded flag.'),
47         _Field('h', 'version', help='Version information depends on the type of record.'),
48         _Field('l', 'numDataBytes', help='Number of data bytes in the record following this record header.'),
49         ])
50
51 #CR_STR = '\x15'  (\r)
52
53 PACKEDRECTYPE_MASK = 0x7FFF  # Record type = (recordType & PACKEDREC_TYPE_MASK)
54 SUPERCEDED_MASK = 0x8000  # Bit is set if the record is superceded by
55                           # a later record in the packed file.
56
57
58 def load(filename, strict=True, ignore_unknown=True):
59     _LOG.debug('loading a packed experiment file from {}'.format(filename))
60     records = []
61     if hasattr(filename, 'read'):
62         f = filename  # filename is actually a stream object
63     else:
64         f = open(filename, 'rb')
65     byte_order = None
66     initial_byte_order = '='
67     try:
68         while True:
69             PackedFileRecordHeader.byte_order = initial_byte_order
70             PackedFileRecordHeader.setup()
71             b = bytes(f.read(PackedFileRecordHeader.size))
72             if not b:
73                 break
74             if len(b) < PackedFileRecordHeader.size:
75                 raise ValueError(
76                     ('not enough data for the next record header ({} < {})'
77                      ).format(len(b), PackedFileRecordHeader.size))
78             _LOG.debug('reading a new packed experiment file record')
79             header = PackedFileRecordHeader.unpack_from(b)
80             if header['version'] and not byte_order:
81                 need_to_reorder = _need_to_reorder_bytes(header['version'])
82                 byte_order = initial_byte_order = _byte_order(need_to_reorder)
83                 _LOG.debug(
84                     'get byte order from version: {} (reorder? {})'.format(
85                         byte_order, need_to_reorder))
86                 if need_to_reorder:
87                     PackedFileRecordHeader.byte_order = byte_order
88                     PackedFileRecordHeader.setup()
89                     header = PackedFileRecordHeader.unpack_from(b)
90                     _LOG.debug(
91                         'reordered version: {}'.format(header['version']))
92             data = bytes(f.read(header['numDataBytes']))
93             if len(data) < header['numDataBytes']:
94                 raise ValueError(
95                     ('not enough data for the next record ({} < {})'
96                      ).format(len(b), header['numDataBytes']))
97             record_type = _RECORD_TYPE.get(
98                 header['recordType'] & PACKEDRECTYPE_MASK, _UnknownRecord)
99             _LOG.debug('the new record has type {} ({}).'.format(
100                     record_type, header['recordType']))
101             if record_type in [_UnknownRecord, _UnusedRecord
102                                ] and not ignore_unknown:
103                 raise KeyError('unkown record type {}'.format(
104                         header['recordType']))
105             records.append(record_type(header, data, byte_order=byte_order))
106     finally:
107         _LOG.debug('finished loading {} records from {}'.format(
108                 len(records), filename))
109         if not hasattr(filename, 'read'):
110             f.close()
111
112     filesystem = _build_filesystem(records)
113
114     return (records, filesystem)
115
116 def _build_filesystem(records):
117     # From PTN003:
118     """The name must be a valid Igor data folder name. See Object
119     Names in the Igor Reference help file for name rules.
120
121     When Igor Pro reads the data folder start record, it creates a new
122     data folder with the specified name. Any subsequent variable, wave
123     or data folder start records cause Igor to create data objects in
124     this new data folder, until Igor Pro reads a corresponding data
125     folder end record."""
126     # From the Igor Manual, chapter 2, section 8, page II-123
127     # http://www.wavemetrics.net/doc/igorman/II-08%20Data%20Folders.pdf
128     """Like the Macintosh file system, Igor Pro's data folders use the
129     colon character (:) to separate components of a path to an
130     object. This is analogous to Unix which uses / and Windows which
131     uses \. (Reminder: Igor's data folders exist wholly in memory
132     while an experiment is open. It is not a disk file system!)
133
134     A data folder named "root" always exists and contains all other
135     data folders.
136     """
137     # From the Igor Manual, chapter 4, page IV-2
138     # http://www.wavemetrics.net/doc/igorman/IV-01%20Commands.pdf
139     """For waves and data folders only, you can also use "liberal"
140     names. Liberal names can include almost any character, including
141     spaces and dots (see Liberal Object Names on page III-415 for
142     details).
143     """
144     # From the Igor Manual, chapter 3, section 16, page III-416
145     # http://www.wavemetrics.net/doc/igorman/III-16%20Miscellany.pdf
146     """Liberal names have the same rules as standard names except you
147     may use any character except control characters and the following:
148
149       " ' : ;
150     """
151     filesystem = {'root': {}}
152     dir_stack = [('root', filesystem['root'])]
153     for record in records:
154         cwd = dir_stack[-1][-1]
155         if isinstance(record, _FolderStartRecord):
156             name = record.null_terminated_text
157             cwd[name] = {}
158             dir_stack.append((name, cwd[name]))
159         elif isinstance(record, _FolderEndRecord):
160             dir_stack.pop()
161         elif isinstance(record, (_VariablesRecord, _WaveRecord)):
162             if isinstance(record, _VariablesRecord):
163                 sys_vars = record.variables['variables']['sysVars'].keys()
164                 for filename,value in record.namespace.items():
165                     if len(dir_stack) > 1 and filename in sys_vars:
166                         # From PTN003:
167                         """When reading a packed file, any system
168                         variables encountered while the current data
169                         folder is not the root should be ignored.
170                         """
171                         continue
172                     _check_filename(dir_stack, filename)
173                     cwd[filename] = value
174             else:  # WaveRecord
175                 filename = record.wave['wave']['wave_header']['bname']
176                 _check_filename(dir_stack, filename)
177                 cwd[filename] = record
178     return filesystem
179
180 def _check_filename(dir_stack, filename):
181     cwd = dir_stack[-1][-1]
182     if filename in cwd:
183         raise ValueError('collision on name {} in {}'.format(
184                 filename, ':'.join(d for d,cwd in dir_stack)))
185
186 def walk(filesystem, callback, dirpath=None):
187     """Walk a packed experiment filesystem, operating on each key,value pair.
188     """
189     if dirpath is None:
190         dirpath = []
191     for key,value in sorted((_bytes(k),v) for k,v in filesystem.items()):
192         callback(dirpath, key, value)
193         if isinstance(value, dict):
194             walk(filesystem=value, callback=callback, dirpath=dirpath+[key])