1# -*- coding: utf-8 -*-
2# Copyright (C) 2005  Michael Urman
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation; either version 2 of the License, or
7# (at your option) any later version.
8
9import struct
10import codecs
11from struct import unpack, pack
12
13from .._compat import text_type, chr_, PY3, swap_to_string, string_types, \
14    xrange
15from .._util import total_ordering, decode_terminated, enum, izip, flags, \
16    cdata, encode_endian, intround
17from ._util import BitPaddedInt, is_valid_frame_id
18
19
20@enum
21class PictureType(object):
22    """Enumeration of image types defined by the ID3 standard for the APIC
23    frame, but also reused in WMA/FLAC/VorbisComment.
24    """
25
26    OTHER = 0
27    """Other"""
28
29    FILE_ICON = 1
30    """32x32 pixels 'file icon' (PNG only)"""
31
32    OTHER_FILE_ICON = 2
33    """Other file icon"""
34
35    COVER_FRONT = 3
36    """Cover (front)"""
37
38    COVER_BACK = 4
39    """Cover (back)"""
40
41    LEAFLET_PAGE = 5
42    """Leaflet page"""
43
44    MEDIA = 6
45    """Media (e.g. label side of CD)"""
46
47    LEAD_ARTIST = 7
48    """Lead artist/lead performer/soloist"""
49
50    ARTIST = 8
51    """Artist/performer"""
52
53    CONDUCTOR = 9
54    """Conductor"""
55
56    BAND = 10
57    """Band/Orchestra"""
58
59    COMPOSER = 11
60    """Composer"""
61
62    LYRICIST = 12
63    """Lyricist/text writer"""
64
65    RECORDING_LOCATION = 13
66    """Recording Location"""
67
68    DURING_RECORDING = 14
69    """During recording"""
70
71    DURING_PERFORMANCE = 15
72    """During performance"""
73
74    SCREEN_CAPTURE = 16
75    """Movie/video screen capture"""
76
77    FISH = 17
78    """A bright coloured fish"""
79
80    ILLUSTRATION = 18
81    """Illustration"""
82
83    BAND_LOGOTYPE = 19
84    """Band/artist logotype"""
85
86    PUBLISHER_LOGOTYPE = 20
87    """Publisher/Studio logotype"""
88
89    def _pprint(self):
90        return text_type(self).split(".", 1)[-1].lower().replace("_", " ")
91
92
93@flags
94class CTOCFlags(object):
95
96    TOP_LEVEL = 0x2
97    """Identifies the CTOC root frame"""
98
99    ORDERED = 0x1
100    """Child elements are ordered"""
101
102
103class SpecError(Exception):
104    pass
105
106
107class Spec(object):
108
109    handle_nodata = False
110    """If reading empty data is possible and writing it back will again
111    result in no data.
112    """
113
114    def __init__(self, name, default):
115        self.name = name
116        self.default = default
117
118    def __hash__(self):
119        raise TypeError("Spec objects are unhashable")
120
121    def _validate23(self, frame, value, **kwargs):
122        """Return a possibly modified value which, if written,
123        results in valid id3v2.3 data.
124        """
125
126        return value
127
128    def read(self, header, frame, data):
129        """
130        Returns:
131            (value: object, left_data: bytes)
132        Raises:
133            SpecError
134        """
135
136        raise NotImplementedError
137
138    def write(self, config, frame, value):
139        """
140        Returns:
141            bytes: The serialized data
142        Raises:
143            SpecError
144        """
145        raise NotImplementedError
146
147    def validate(self, frame, value):
148        """
149        Returns:
150            the validated value
151        Raises:
152            ValueError
153            TypeError
154        """
155
156        raise NotImplementedError
157
158
159class ByteSpec(Spec):
160
161    def __init__(self, name, default=0):
162        super(ByteSpec, self).__init__(name, default)
163
164    def read(self, header, frame, data):
165        return bytearray(data)[0], data[1:]
166
167    def write(self, config, frame, value):
168        return chr_(value)
169
170    def validate(self, frame, value):
171        if value is not None:
172            chr_(value)
173        return value
174
175
176class PictureTypeSpec(ByteSpec):
177
178    def __init__(self, name, default=PictureType.COVER_FRONT):
179        super(PictureTypeSpec, self).__init__(name, default)
180
181    def read(self, header, frame, data):
182        value, data = ByteSpec.read(self, header, frame, data)
183        return PictureType(value), data
184
185    def validate(self, frame, value):
186        value = ByteSpec.validate(self, frame, value)
187        if value is not None:
188            return PictureType(value)
189        return value
190
191
192class CTOCFlagsSpec(ByteSpec):
193
194    def read(self, header, frame, data):
195        value, data = ByteSpec.read(self, header, frame, data)
196        return CTOCFlags(value), data
197
198    def validate(self, frame, value):
199        value = ByteSpec.validate(self, frame, value)
200        if value is not None:
201            return CTOCFlags(value)
202        return value
203
204
205class IntegerSpec(Spec):
206    def read(self, header, frame, data):
207        return int(BitPaddedInt(data, bits=8)), b''
208
209    def write(self, config, frame, value):
210        return BitPaddedInt.to_str(value, bits=8, width=-1)
211
212    def validate(self, frame, value):
213        return value
214
215
216class SizedIntegerSpec(Spec):
217
218    def __init__(self, name, size, default):
219        self.name, self.__sz = name, size
220        self.default = default
221
222    def read(self, header, frame, data):
223        return int(BitPaddedInt(data[:self.__sz], bits=8)), data[self.__sz:]
224
225    def write(self, config, frame, value):
226        return BitPaddedInt.to_str(value, bits=8, width=self.__sz)
227
228    def validate(self, frame, value):
229        return value
230
231
232@enum
233class Encoding(object):
234    """Text Encoding"""
235
236    LATIN1 = 0
237    """ISO-8859-1"""
238
239    UTF16 = 1
240    """UTF-16 with BOM"""
241
242    UTF16BE = 2
243    """UTF-16BE without BOM"""
244
245    UTF8 = 3
246    """UTF-8"""
247
248
249class EncodingSpec(ByteSpec):
250
251    def __init__(self, name, default=Encoding.UTF16):
252        super(EncodingSpec, self).__init__(name, default)
253
254    def read(self, header, frame, data):
255        enc, data = super(EncodingSpec, self).read(header, frame, data)
256        if enc not in (Encoding.LATIN1, Encoding.UTF16, Encoding.UTF16BE,
257                       Encoding.UTF8):
258            raise SpecError('Invalid Encoding: %r' % enc)
259        return Encoding(enc), data
260
261    def validate(self, frame, value):
262        if value is None:
263            raise TypeError
264        if value not in (Encoding.LATIN1, Encoding.UTF16, Encoding.UTF16BE,
265                         Encoding.UTF8):
266            raise ValueError('Invalid Encoding: %r' % value)
267        return Encoding(value)
268
269    def _validate23(self, frame, value, **kwargs):
270        # only 0, 1 are valid in v2.3, default to utf-16
271        if value not in (Encoding.LATIN1, Encoding.UTF16):
272            value = Encoding.UTF16
273        return value
274
275
276class StringSpec(Spec):
277    """A fixed size ASCII only payload."""
278
279    def __init__(self, name, length, default=None):
280        if default is None:
281            default = u" " * length
282        super(StringSpec, self).__init__(name, default)
283        self.len = length
284
285    def read(s, header, frame, data):
286        chunk = data[:s.len]
287        try:
288            ascii = chunk.decode("ascii")
289        except UnicodeDecodeError:
290            raise SpecError("not ascii")
291        else:
292            if PY3:
293                chunk = ascii
294
295        return chunk, data[s.len:]
296
297    def write(self, config, frame, value):
298        if PY3:
299            value = value.encode("ascii")
300        return (bytes(value) + b'\x00' * self.len)[:self.len]
301
302    def validate(self, frame, value):
303        if value is None:
304            raise TypeError
305        if PY3:
306            if not isinstance(value, str):
307                raise TypeError("%s has to be str" % self.name)
308            value.encode("ascii")
309        else:
310            if not isinstance(value, bytes):
311                value = value.encode("ascii")
312
313        if len(value) == self.len:
314            return value
315
316        raise ValueError('Invalid StringSpec[%d] data: %r' % (self.len, value))
317
318
319class RVASpec(Spec):
320
321    def __init__(self, name, stereo_only, default=[0, 0]):
322        # two_chan: RVA has only 2 channels, while RVAD has 6 channels
323        super(RVASpec, self).__init__(name, default)
324        self._max_values = 4 if stereo_only else 12
325
326    def read(self, header, frame, data):
327        # inc/dec flags
328        spec = ByteSpec("flags", 0)
329        flags, data = spec.read(header, frame, data)
330        if not data:
331            raise SpecError("truncated")
332
333        # how many bytes per value
334        bits, data = spec.read(header, frame, data)
335        if bits == 0:
336            # not allowed according to spec
337            raise SpecError("bits used has to be > 0")
338        bytes_per_value = (bits + 7) // 8
339
340        values = []
341        while len(data) >= bytes_per_value and len(values) < self._max_values:
342            v = BitPaddedInt(data[:bytes_per_value], bits=8)
343            data = data[bytes_per_value:]
344            values.append(v)
345
346        if len(values) < 2:
347            raise SpecError("First two values not optional")
348
349        # if the respective flag bit is zero, take as decrement
350        for bit, index in enumerate([0, 1, 4, 5, 8, 10]):
351            if not cdata.test_bit(flags, bit):
352                try:
353                    values[index] = -values[index]
354                except IndexError:
355                    break
356
357        return values, data
358
359    def write(self, config, frame, values):
360        if len(values) < 2 or len(values) > self._max_values:
361            raise SpecError(
362                "at least two volume change values required, max %d" %
363                self._max_values)
364
365        spec = ByteSpec("flags", 0)
366
367        flags = 0
368        values = list(values)
369        for bit, index in enumerate([0, 1, 4, 5, 8, 10]):
370            try:
371                if values[index] < 0:
372                    values[index] = -values[index]
373                else:
374                    flags |= (1 << bit)
375            except IndexError:
376                break
377
378        buffer_ = bytearray()
379        buffer_.extend(spec.write(config, frame, flags))
380
381        # serialized and make them all the same size (min 2 bytes)
382        byte_values = [
383            BitPaddedInt.to_str(v, bits=8, width=-1, minwidth=2)
384            for v in values]
385        max_bytes = max([len(v) for v in byte_values])
386        byte_values = [v.ljust(max_bytes, b"\x00") for v in byte_values]
387
388        bits = max_bytes * 8
389        buffer_.extend(spec.write(config, frame, bits))
390
391        for v in byte_values:
392            buffer_.extend(v)
393
394        return bytes(buffer_)
395
396    def validate(self, frame, values):
397        if len(values) < 2 or len(values) > self._max_values:
398            raise ValueError("needs list of length 2..%d" % self._max_values)
399        return values
400
401
402class FrameIDSpec(StringSpec):
403
404    def __init__(self, name, length):
405        super(FrameIDSpec, self).__init__(name, length, u"X" * length)
406
407    def validate(self, frame, value):
408        value = super(FrameIDSpec, self).validate(frame, value)
409        if not is_valid_frame_id(value):
410            raise ValueError("Invalid frame ID")
411        return value
412
413
414class BinaryDataSpec(Spec):
415
416    handle_nodata = True
417
418    def __init__(self, name, default=b""):
419        super(BinaryDataSpec, self).__init__(name, default)
420
421    def read(self, header, frame, data):
422        return data, b''
423
424    def write(self, config, frame, value):
425        if isinstance(value, bytes):
426            return value
427        value = text_type(value).encode("ascii")
428        return value
429
430    def validate(self, frame, value):
431        if value is None:
432            raise TypeError
433        if isinstance(value, bytes):
434            return value
435        elif PY3:
436            raise TypeError("%s has to be bytes" % self.name)
437
438        value = text_type(value).encode("ascii")
439        return value
440
441
442def iter_text_fixups(data, encoding):
443    """Yields a series of repaired text values for decoding"""
444
445    yield data
446    if encoding == Encoding.UTF16BE:
447        # wrong termination
448        yield data + b"\x00"
449    elif encoding == Encoding.UTF16:
450        # wrong termination
451        yield data + b"\x00"
452        # utf-16 is missing BOM, content is usually utf-16-le
453        yield codecs.BOM_UTF16_LE + data
454        # both cases combined
455        yield codecs.BOM_UTF16_LE + data + b"\x00"
456
457
458class EncodedTextSpec(Spec):
459
460    _encodings = {
461        Encoding.LATIN1: ('latin1', b'\x00'),
462        Encoding.UTF16: ('utf16', b'\x00\x00'),
463        Encoding.UTF16BE: ('utf_16_be', b'\x00\x00'),
464        Encoding.UTF8: ('utf8', b'\x00'),
465    }
466
467    def __init__(self, name, default=u""):
468        super(EncodedTextSpec, self).__init__(name, default)
469
470    def read(self, header, frame, data):
471        enc, term = self._encodings[frame.encoding]
472        err = None
473        for data in iter_text_fixups(data, frame.encoding):
474            try:
475                value, data = decode_terminated(data, enc, strict=False)
476            except ValueError as e:
477                err = e
478            else:
479                # Older id3 did not support multiple values, but we still
480                # read them. To not missinterpret zero padded values with
481                # a list of empty strings, stop if everything left is zero.
482                # https://github.com/quodlibet/mutagen/issues/276
483                if header.version < header._V24 and not data.strip(b"\x00"):
484                    data = b""
485                return value, data
486        raise SpecError(err)
487
488    def write(self, config, frame, value):
489        enc, term = self._encodings[frame.encoding]
490        try:
491            return encode_endian(value, enc, le=True) + term
492        except UnicodeEncodeError as e:
493            raise SpecError(e)
494
495    def validate(self, frame, value):
496        return text_type(value)
497
498
499class MultiSpec(Spec):
500    def __init__(self, name, *specs, **kw):
501        super(MultiSpec, self).__init__(name, default=kw.get('default'))
502        self.specs = specs
503        self.sep = kw.get('sep')
504
505    def read(self, header, frame, data):
506        values = []
507        while data:
508            record = []
509            for spec in self.specs:
510                value, data = spec.read(header, frame, data)
511                record.append(value)
512            if len(self.specs) != 1:
513                values.append(record)
514            else:
515                values.append(record[0])
516        return values, data
517
518    def write(self, config, frame, value):
519        data = []
520        if len(self.specs) == 1:
521            for v in value:
522                data.append(self.specs[0].write(config, frame, v))
523        else:
524            for record in value:
525                for v, s in izip(record, self.specs):
526                    data.append(s.write(config, frame, v))
527        return b''.join(data)
528
529    def validate(self, frame, value):
530        if self.sep and isinstance(value, string_types):
531            value = value.split(self.sep)
532        if isinstance(value, list):
533            if len(self.specs) == 1:
534                return [self.specs[0].validate(frame, v) for v in value]
535            else:
536                return [
537                    [s.validate(frame, v) for (v, s) in izip(val, self.specs)]
538                    for val in value]
539        raise ValueError('Invalid MultiSpec data: %r' % value)
540
541    def _validate23(self, frame, value, **kwargs):
542        if len(self.specs) != 1:
543            return [[s._validate23(frame, v, **kwargs)
544                     for (v, s) in izip(val, self.specs)]
545                    for val in value]
546
547        spec = self.specs[0]
548
549        # Merge single text spec multispecs only.
550        # (TimeStampSpec beeing the exception, but it's not a valid v2.3 frame)
551        if not isinstance(spec, EncodedTextSpec) or \
552                isinstance(spec, TimeStampSpec):
553            return value
554
555        value = [spec._validate23(frame, v, **kwargs) for v in value]
556        if kwargs.get("sep") is not None:
557            return [spec.validate(frame, kwargs["sep"].join(value))]
558        return value
559
560
561class EncodedNumericTextSpec(EncodedTextSpec):
562    pass
563
564
565class EncodedNumericPartTextSpec(EncodedTextSpec):
566    pass
567
568
569class Latin1TextSpec(Spec):
570
571    def __init__(self, name, default=u""):
572        super(Latin1TextSpec, self).__init__(name, default)
573
574    def read(self, header, frame, data):
575        if b'\x00' in data:
576            data, ret = data.split(b'\x00', 1)
577        else:
578            ret = b''
579        return data.decode('latin1'), ret
580
581    def write(self, config, data, value):
582        return value.encode('latin1') + b'\x00'
583
584    def validate(self, frame, value):
585        return text_type(value)
586
587
588class ID3FramesSpec(Spec):
589
590    handle_nodata = True
591
592    def __init__(self, name, default=[]):
593        super(ID3FramesSpec, self).__init__(name, default)
594
595    def read(self, header, frame, data):
596        from ._tags import ID3Tags
597
598        tags = ID3Tags()
599        return tags, tags._read(header, data)
600
601    def _validate23(self, frame, value, **kwargs):
602        from ._tags import ID3Tags
603
604        v = ID3Tags()
605        for frame in value.values():
606            v.add(frame._get_v23_frame(**kwargs))
607        return v
608
609    def write(self, config, frame, value):
610        return bytes(value._write(config))
611
612    def validate(self, frame, value):
613        from ._tags import ID3Tags
614
615        if isinstance(value, ID3Tags):
616            return value
617
618        tags = ID3Tags()
619        for v in value:
620            tags.add(v)
621
622        return tags
623
624
625class Latin1TextListSpec(Spec):
626
627    def __init__(self, name, default=[]):
628        super(Latin1TextListSpec, self).__init__(name, default)
629        self._bspec = ByteSpec("entry_count", default=0)
630        self._lspec = Latin1TextSpec("child_element_id")
631
632    def read(self, header, frame, data):
633        count, data = self._bspec.read(header, frame, data)
634        entries = []
635        for i in xrange(count):
636            entry, data = self._lspec.read(header, frame, data)
637            entries.append(entry)
638        return entries, data
639
640    def write(self, config, frame, value):
641        b = self._bspec.write(config, frame, len(value))
642        for v in value:
643            b += self._lspec.write(config, frame, v)
644        return b
645
646    def validate(self, frame, value):
647        return [self._lspec.validate(frame, v) for v in value]
648
649
650@swap_to_string
651@total_ordering
652class ID3TimeStamp(object):
653    """A time stamp in ID3v2 format.
654
655    This is a restricted form of the ISO 8601 standard; time stamps
656    take the form of:
657        YYYY-MM-DD HH:MM:SS
658    Or some partial form (YYYY-MM-DD HH, YYYY, etc.).
659
660    The 'text' attribute contains the raw text data of the time stamp.
661    """
662
663    import re
664
665    def __init__(self, text):
666        if isinstance(text, ID3TimeStamp):
667            text = text.text
668        elif not isinstance(text, text_type):
669            if PY3:
670                raise TypeError("not a str")
671            text = text.decode("utf-8")
672
673        self.text = text
674
675    __formats = ['%04d'] + ['%02d'] * 5
676    __seps = ['-', '-', ' ', ':', ':', 'x']
677
678    def get_text(self):
679        parts = [self.year, self.month, self.day,
680                 self.hour, self.minute, self.second]
681        pieces = []
682        for i, part in enumerate(parts):
683            if part is None:
684                break
685            pieces.append(self.__formats[i] % part + self.__seps[i])
686        return u''.join(pieces)[:-1]
687
688    def set_text(self, text, splitre=re.compile('[-T:/.]|\\s+')):
689        year, month, day, hour, minute, second = \
690            splitre.split(text + ':::::')[:6]
691        for a in 'year month day hour minute second'.split():
692            try:
693                v = int(locals()[a])
694            except ValueError:
695                v = None
696            setattr(self, a, v)
697
698    text = property(get_text, set_text, doc="ID3v2.4 date and time.")
699
700    def __str__(self):
701        return self.text
702
703    def __bytes__(self):
704        return self.text.encode("utf-8")
705
706    def __repr__(self):
707        return repr(self.text)
708
709    def __eq__(self, other):
710        return isinstance(other, ID3TimeStamp) and self.text == other.text
711
712    def __lt__(self, other):
713        return self.text < other.text
714
715    __hash__ = object.__hash__
716
717    def encode(self, *args):
718        return self.text.encode(*args)
719
720
721class TimeStampSpec(EncodedTextSpec):
722    def read(self, header, frame, data):
723        value, data = super(TimeStampSpec, self).read(header, frame, data)
724        return self.validate(frame, value), data
725
726    def write(self, config, frame, data):
727        return super(TimeStampSpec, self).write(config, frame,
728                                                data.text.replace(' ', 'T'))
729
730    def validate(self, frame, value):
731        try:
732            return ID3TimeStamp(value)
733        except TypeError:
734            raise ValueError("Invalid ID3TimeStamp: %r" % value)
735
736
737class ChannelSpec(ByteSpec):
738    (OTHER, MASTER, FRONTRIGHT, FRONTLEFT, BACKRIGHT, BACKLEFT, FRONTCENTRE,
739     BACKCENTRE, SUBWOOFER) = xrange(9)
740
741
742class VolumeAdjustmentSpec(Spec):
743    def read(self, header, frame, data):
744        value, = unpack('>h', data[0:2])
745        return value / 512.0, data[2:]
746
747    def write(self, config, frame, value):
748        number = intround(value * 512)
749        # pack only fails in 2.7, do it manually in 2.6
750        if not -32768 <= number <= 32767:
751            raise SpecError("not in range")
752        return pack('>h', number)
753
754    def validate(self, frame, value):
755        if value is not None:
756            try:
757                self.write(None, frame, value)
758            except SpecError:
759                raise ValueError("out of range")
760        return value
761
762
763class VolumePeakSpec(Spec):
764    def read(self, header, frame, data):
765        # http://bugs.xmms.org/attachment.cgi?id=113&action=view
766        peak = 0
767        data_array = bytearray(data)
768        bits = data_array[0]
769        vol_bytes = min(4, (bits + 7) >> 3)
770        # not enough frame data
771        if vol_bytes + 1 > len(data):
772            raise SpecError("not enough frame data")
773        shift = ((8 - (bits & 7)) & 7) + (4 - vol_bytes) * 8
774        for i in xrange(1, vol_bytes + 1):
775            peak *= 256
776            peak += data_array[i]
777        peak *= 2 ** shift
778        return (float(peak) / (2 ** 31 - 1)), data[1 + vol_bytes:]
779
780    def write(self, config, frame, value):
781        number = intround(value * 32768)
782        # pack only fails in 2.7, do it manually in 2.6
783        if not 0 <= number <= 65535:
784            raise SpecError("not in range")
785        # always write as 16 bits for sanity.
786        return b"\x10" + pack('>H', number)
787
788    def validate(self, frame, value):
789        if value is not None:
790            try:
791                self.write(None, frame, value)
792            except SpecError:
793                raise ValueError("out of range")
794        return value
795
796
797class SynchronizedTextSpec(EncodedTextSpec):
798    def read(self, header, frame, data):
799        texts = []
800        encoding, term = self._encodings[frame.encoding]
801        while data:
802            try:
803                value, data = decode_terminated(data, encoding)
804            except ValueError:
805                raise SpecError("decoding error")
806
807            if len(data) < 4:
808                raise SpecError("not enough data")
809            time, = struct.unpack(">I", data[:4])
810
811            texts.append((value, time))
812            data = data[4:]
813        return texts, b""
814
815    def write(self, config, frame, value):
816        data = []
817        encoding, term = self._encodings[frame.encoding]
818        for text, time in value:
819            try:
820                text = encode_endian(text, encoding, le=True) + term
821            except UnicodeEncodeError as e:
822                raise SpecError(e)
823            data.append(text + struct.pack(">I", time))
824        return b"".join(data)
825
826    def validate(self, frame, value):
827        return value
828
829
830class KeyEventSpec(Spec):
831    def read(self, header, frame, data):
832        events = []
833        while len(data) >= 5:
834            events.append(struct.unpack(">bI", data[:5]))
835            data = data[5:]
836        return events, data
837
838    def write(self, config, frame, value):
839        return b"".join(struct.pack(">bI", *event) for event in value)
840
841    def validate(self, frame, value):
842        return list(value)
843
844
845class VolumeAdjustmentsSpec(Spec):
846    # Not to be confused with VolumeAdjustmentSpec.
847    def read(self, header, frame, data):
848        adjustments = {}
849        while len(data) >= 4:
850            freq, adj = struct.unpack(">Hh", data[:4])
851            data = data[4:]
852            freq /= 2.0
853            adj /= 512.0
854            adjustments[freq] = adj
855        adjustments = sorted(adjustments.items())
856        return adjustments, data
857
858    def write(self, config, frame, value):
859        value.sort()
860        return b"".join(struct.pack(">Hh", int(freq * 2), int(adj * 512))
861                        for (freq, adj) in value)
862
863    def validate(self, frame, value):
864        return list(value)
865
866
867class ASPIIndexSpec(Spec):
868
869    def read(self, header, frame, data):
870        if frame.b == 16:
871            format = "H"
872            size = 2
873        elif frame.b == 8:
874            format = "B"
875            size = 1
876        else:
877            raise SpecError("invalid bit count in ASPI (%d)" % frame.b)
878
879        indexes = data[:frame.N * size]
880        data = data[frame.N * size:]
881        try:
882            return list(struct.unpack(">" + format * frame.N, indexes)), data
883        except struct.error as e:
884            raise SpecError(e)
885
886    def write(self, config, frame, values):
887        if frame.b == 16:
888            format = "H"
889        elif frame.b == 8:
890            format = "B"
891        else:
892            raise SpecError("frame.b must be 8 or 16")
893        try:
894            return struct.pack(">" + format * frame.N, *values)
895        except struct.error as e:
896            raise SpecError(e)
897
898    def validate(self, frame, values):
899        return list(values)
900