Skip to content

Commit 2a63b4f

Browse files
committed
Adopt a system of exceptions derived from KaitaiStructError
* Resolves #40 * Resolves #41 As explained in #40, this makes it easy to handle to all errors caused by invalid input data by using `kaitaistruct.KaitaiStructError` in a `try..except` statement. Three new exception types were added: `InvalidArgumentError`, `EndOfStreamError` and `NoTerminatorFoundError`. All changes to raised exceptions in this commit should be backward compatible, as we are only moving to subclasses of previously raised exceptions. `NoTerminatorFoundError` is a subclass of `EndOfStreamError` to address the suggestion in #41. Note that the `process_rotate_left` method could only raise `NotImplementedError` if someone called it manually (because KSC-generated parsers hardcode `group_size` to `1`, see https://github.com/kaitai-io/kaitai_struct_compiler/blob/c23ec2ca88d84042edba76f70c1f003d062b7585/shared/src/main/scala/io/kaitai/struct/languages/PythonCompiler.scala#L211), so it makes no sense to raise an exception derived `KaitaiStructError` (it's a programmer error, not a user input error). Most of our runtime libraries in other languages don't even have this `group_size` parameter, and if they do (C#, Java, Ruby), they also throw the equivalent of `NotImplementedError` (except the JavaScript runtime, which throws a plain string, which is _possible_ in JS but considered bad practice, so we should fix this).
1 parent 68cb21d commit 2a63b4f

File tree

1 file changed

+57
-18
lines changed

1 file changed

+57
-18
lines changed

kaitaistruct.py

Lines changed: 57 additions & 18 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
@@ -782,7 +781,7 @@ def process_xor_many(data, key):
782781
@staticmethod
783782
def process_rotate_left(data, amount, group_size):
784783
if group_size != 1:
785-
raise Exception(
784+
raise NotImplementedError(
786785
"unable to rotate group of %d bytes yet" %
787786
(group_size,)
788787
)
@@ -872,15 +871,55 @@ def _write_back(self, parent):
872871

873872

874873
class KaitaiStructError(Exception):
875-
"""Common ancestor for all error originating from Kaitai Struct usage.
876-
Stores KSY source path, pointing to an element supposedly guilty of
877-
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`.
878883
"""
879884
def __init__(self, msg, src_path):
880-
super(KaitaiStructError, self).__init__("%s: %s" % (src_path, msg))
885+
super(KaitaiStructError, self).__init__(("" if src_path is None else src_path + ": ") + msg)
881886
self.src_path = src_path
882887

883888

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+
884923
class UndecidedEndiannessError(KaitaiStructError):
885924
"""Error that occurs when default endianness should be decided with
886925
switch, but nothing matches (although using endianness expression

0 commit comments

Comments
 (0)