Skip to content

Commit 1e2d40b

Browse files
authored
Merge pull request #80 from kaitai-io/better-exceptions
Adopt a system of exceptions derived from KaitaiStructError
2 parents 67ffcff + 2a63b4f commit 1e2d40b

File tree

1 file changed

+73
-23
lines changed

1 file changed

+73
-23
lines changed

kaitaistruct.py

Lines changed: 73 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ def is_eof(self):
128128
return self._io.tell() >= self.size()
129129

130130
def seek(self, n):
131+
if n < 0:
132+
raise InvalidArgumentError("cannot seek to invalid position %d" % (n,))
133+
131134
if self.bits_write_mode:
132135
self.write_align_to_byte()
133136
else:
@@ -376,7 +379,7 @@ def read_bytes(self, n):
376379

377380
def _read_bytes_not_aligned(self, n):
378381
if n < 0:
379-
raise ValueError(
382+
raise InvalidArgumentError(
380383
"requested invalid %d amount of bytes" %
381384
(n,)
382385
)
@@ -404,9 +407,10 @@ def _read_bytes_not_aligned(self, n):
404407

405408
if not is_satisfiable:
406409
# noinspection PyUnboundLocalVariable
407-
raise EOFError(
410+
raise EndOfStreamError(
408411
"requested %d bytes, but only %d bytes available" %
409-
(n, num_bytes_available)
412+
(n, num_bytes_available),
413+
n, num_bytes_available
410414
)
411415

412416
# noinspection PyUnboundLocalVariable
@@ -424,10 +428,7 @@ def read_bytes_term(self, term, include_term, consume_term, eos_error):
424428
c = self._io.read(1)
425429
if not c:
426430
if eos_error:
427-
raise Exception(
428-
"end of stream reached, but no terminator %d found" %
429-
(term,)
430-
)
431+
raise NoTerminatorFoundError(term_byte, 0)
431432

432433
return bytes(r)
433434

@@ -448,10 +449,7 @@ def read_bytes_term_multi(self, term, include_term, consume_term, eos_error):
448449
c = self._io.read(unit_size)
449450
if len(c) < unit_size:
450451
if eos_error:
451-
raise Exception(
452-
"end of stream reached, but no terminator %s found" %
453-
(repr(term),)
454-
)
452+
raise NoTerminatorFoundError(term, len(c))
455453

456454
r += c
457455
return bytes(r)
@@ -523,9 +521,10 @@ def _ensure_bytes_left_to_write(self, n, pos):
523521

524522
num_bytes_left = full_size - pos
525523
if n > num_bytes_left:
526-
raise EOFError(
524+
raise EndOfStreamError(
527525
"requested to write %d bytes, but only %d bytes left in the stream" %
528-
(n, num_bytes_left)
526+
(n, num_bytes_left),
527+
n, num_bytes_left
529528
)
530529

531530
# region Integer numbers
@@ -739,14 +738,25 @@ def _write_bytes_not_aligned(self, buf):
739738

740739
def write_bytes_limit(self, buf, size, term, pad_byte):
741740
n = len(buf)
741+
# Strictly speaking, this assertion is redundant because it is already
742+
# done in the corresponding _check() method in the generated code, but
743+
# it seems to make sense to include it here anyway so that this method
744+
# itself does something reasonable for every set of arguments.
745+
#
746+
# However, it should never be `false` when operated correctly (and in
747+
# this case, assigning inconsistent values to fields of a KS-generated
748+
# object is considered correct operation if the user application calls
749+
# the corresponding _check(), which we know would raise an error and
750+
# thus the code should not reach _write() and this method at all). So
751+
# it's by design that this throws AssertionError, not any specific
752+
# error, because it's not intended to be caught in user applications,
753+
# but avoided by calling all _check() methods correctly.
754+
assert n <= size, "writing %d bytes, but %d bytes were given" % (size, n)
755+
742756
self.write_bytes(buf)
743757
if n < size:
744758
self.write_u1(term)
745-
pad_len = size - n - 1
746-
for _ in range(pad_len):
747-
self.write_u1(pad_byte)
748-
elif n > size:
749-
raise ValueError("writing %d bytes, but %d bytes were given" % (size, n))
759+
self.write_bytes(KaitaiStream.byte_from_int(pad_byte) * (size - n - 1))
750760

751761
# endregion
752762

@@ -771,7 +781,7 @@ def process_xor_many(data, key):
771781
@staticmethod
772782
def process_rotate_left(data, amount, group_size):
773783
if group_size != 1:
774-
raise Exception(
784+
raise NotImplementedError(
775785
"unable to rotate group of %d bytes yet" %
776786
(group_size,)
777787
)
@@ -861,15 +871,55 @@ def _write_back(self, parent):
861871

862872

863873
class KaitaiStructError(Exception):
864-
"""Common ancestor for all error originating from Kaitai Struct usage.
865-
Stores KSY source path, pointing to an element supposedly guilty of
866-
an error.
874+
"""Common ancestor for all errors originating from correct Kaitai Struct
875+
usage (i.e. errors that indicate a problem with user input, not errors
876+
indicating incorrect usage that are not meant to be caught but fixed in the
877+
application code). Use this exception type in the `except` clause if you
878+
want to handle all parse errors and serialization errors.
879+
880+
If available, the `src_path` attribute will contain the KSY source path
881+
pointing to the element where the error occurred. If it is not available,
882+
`src_path` will be `None`.
867883
"""
868884
def __init__(self, msg, src_path):
869-
super(KaitaiStructError, self).__init__("%s: %s" % (src_path, msg))
885+
super(KaitaiStructError, self).__init__(("" if src_path is None else src_path + ": ") + msg)
870886
self.src_path = src_path
871887

872888

889+
class InvalidArgumentError(KaitaiStructError, ValueError):
890+
"""Indicates that an invalid argument value was received (like `ValueError`),
891+
but used in places where this might indicate invalid user input and
892+
therefore represents a parse error or serialization error.
893+
"""
894+
def __init__(self, msg):
895+
super(InvalidArgumentError, self).__init__(msg, None)
896+
897+
898+
class EndOfStreamError(KaitaiStructError, EOFError):
899+
"""Read or write beyond end of stream. Provides the `bytes_needed` (number
900+
of bytes requested to read or write) and `bytes_available` (number of bytes
901+
remaining in the stream) attributes.
902+
"""
903+
def __init__(self, msg, bytes_needed, bytes_available):
904+
super(EndOfStreamError, self).__init__(msg, None)
905+
self.bytes_needed = bytes_needed
906+
self.bytes_available = bytes_available
907+
908+
909+
class NoTerminatorFoundError(EndOfStreamError):
910+
"""Special type of `EndOfStreamError` that occurs when end of stream is
911+
reached before the required terminator is found. If you want to tolerate a
912+
missing terminator, you can specify `eos-error: false` in the KSY
913+
specification, in which case the end of stream will be considered a valid
914+
end of field and this error will no longer be raised.
915+
916+
The `term` attribute contains a `bytes` object with the searched terminator.
917+
"""
918+
def __init__(self, term, bytes_available):
919+
super(NoTerminatorFoundError, self).__init__("end of stream reached, but no terminator %r found" % (term,), len(term), bytes_available)
920+
self.term = term
921+
922+
873923
class UndecidedEndiannessError(KaitaiStructError):
874924
"""Error that occurs when default endianness should be decided with
875925
switch, but nothing matches (although using endianness expression

0 commit comments

Comments
 (0)