Legacy Event File Format (MWK)

Description

MWorks’ legacy event file format uses the file extension .mwk.

An MWK file is a simple stream of MWorks events. Each event is encoded as a list in the LDO format defined by the Scarab library. The list contains the code, time, and data associated with the event. (The only exception is the RESERVED_TERMINATION_CODE event that ends the event file, which has no associated data and is encoded as a two-element list.)

The first time an MWK file is opened using MWorks’ standard data analysis tools, it is converted into a directory containing both the original, unmodified MWK file and an index file. For example:

$ # Before opening
$ ls -l data.mwk
-rw-r--r--  1 cstawarz  staff  7953812 Aug  9  2018 data.mwk

$ # After opening
$ ls -l data.mwk/
total 16400
-rw-r--r--  1 cstawarz  staff  7953812 Aug  9  2018 data.mwk
-rw-r--r--  1 cstawarz  staff      686 Aug 10  2018 data.mwk.idx

If the .mwk.idx file is deleted, the data tools will recreate it the next time the event file is opened.

Example Code

The following Python code demonstrates the MWK format in detail by a implementing a reader for .mwk files:

import struct


class LDOError(Exception):
    pass


class LDOReader:

    MAGIC = b'\x89CBF\x01\x00\x00'

    INTEGER_N =  0x02
    INTEGER_P =  0x03
    OPAQUE =     0x0A
    NULL =       0x0B
    LIST =       0x0C
    DICTIONARY = 0x0D
    FLOAT =      0x11

    def __init__(self, file, string_encoding='utf-8'):
        magic = file.read(len(self.MAGIC))
        if not isinstance(magic, bytes):
            raise TypeError('file.read() must return binary data')
        if magic != self.MAGIC:
            raise LDOError('invalid magic')

        def _read(size):
            assert size > 0
            data = file.read(size)
            if not data:
                raise EOFError
            return data
        self._read = _read

        self._readers = {
            self.INTEGER_N: self._read_integer_n,
            self.INTEGER_P: self._read_integer_p,
            self.OPAQUE: self._read_opaque,
            self.NULL: self._read_null,
            self.LIST: self._read_list,
            self.DICTIONARY: self._read_dict,
            self.FLOAT: self._read_float,
            }

        self.string_encoding = string_encoding

    def _read_ord(self):
        return ord(self._read(1))

    def _read_ber(self):
        # Adapted from https://stackoverflow.com/questions/6776553/
        value = 0
        while True:
            tmp = self._read_ord()
            value = (value << 7) | (tmp & 0x7f)
            if tmp & 0x80 == 0:
                return value

    def _read_integer_n(self):
        return -(self._read_ber())

    def _read_integer_p(self):
        return self._read_ber()

    def _read_opaque(self):
        size = self._read_ber()
        value = self._read(size)

        # If value contains exactly one NUL, and that NUL is the last
        # byte in the array, then value is an encoded string.  Strip
        # the NUL and decode with the specified encoding.
        if value.find(b'\0') == len(value) - 1:
            value = value[:-1].decode(self.string_encoding)

        return value

    def _read_null(self):
        return None

    def _read_list(self):
        size = self._read_ber()
        return list(self.read() for _ in range(size))

    def _read_dict(self):
        size = self._read_ber()
        return dict((self.read(), self.read()) for _ in range(size))

    def _read_float(self):
        size = self._read_ber()  # Should always be 8
        return struct.unpack(b'<d', self._read(size))[0]

    def read(self):
        typecode = self._read_ord()
        try:
            return self._readers[typecode]()
        except KeyError:
            raise LDOError('invalid type code (0x%0.2X)' % typecode)


class MWKReader:

    def __init__(self, filename):
        self._fp = open(filename, 'rb')

    def close(self):
        self._fp.close()

    def __enter__(self):
        return self

    def __exit__(self, type, value, tb):
        self.close()

    def __iter__(self):
        self._fp.seek(0)
        reader = LDOReader(self._fp)
        while True:
            try:
                event = reader.read()
                assert isinstance(event, list)
                assert len(event) in (2, 3)
                if len(event) == 2:
                    event.append(None)
                yield event
            except EOFError:
                break

The MWKReader class defined above can be used as follows:

with MWKReader('my_data.mwk') as event_file:
    for code, time, data in event_file:
        # Process the current event
        ...