# 21. `example_error_handling.py` — Exception catalogue
[← Example index](index.md) · [example_error_handling.py on Codeberg](https://codeberg.org/abbra/synta/src/branch/main/examples/example_error_handling.py)
Bindings: `synta.SyntaError`, `ValueError`, `OverflowError`, `EOFError`.
- Inspect `SyntaError` as a module exception class (MRO, `issubclass` check).
- Demonstrate `EOFError` from empty input, tag-only input, and truncated value in `Decoder`.
- Demonstrate `ValueError` from tag mismatch (decoding BOOLEAN as INTEGER).
- Demonstrate `ValueError` from DER constraint violations (non-canonical BOOLEAN, non-minimal INTEGER).
- Demonstrate `OverflowError` from `Integer.to_int()` and `to_i128()` on values exceeding the target type.
- Demonstrate `ValueError` from constructor validation: `BmpString` with a non-BMP character,
`GeneralizedTime` with an invalid month, `ObjectIdentifier` with a malformed string,
`Krb5PrincipalName` with a non-ASCII realm, `GeneralString.from_ascii` with a non-ASCII character.
## Source
```python
#!/usr/bin/env python3
"""
Example 20: Error handling.
Demonstrates: synta.SyntaError (module exception class),
ValueError from invalid DER (wrong tag, malformed encoding),
EOFError from truncated input,
OverflowError from Integer.to_int() / Integer.to_i128() on large values,
ValueError from type validation (BmpString, GeneralizedTime, OID).
"""
import synta
import synta.krb5 as krb5
def section(title):
print(f"\n{'─' * 60}\n{title}\n{'─' * 60}")
def demo_synta_error_class():
section("SyntaError — module exception class")
# SyntaError is exported from the synta module.
# Current parse errors map to standard Python exceptions (ValueError,
# EOFError, OverflowError) rather than SyntaError directly, but
# SyntaError is available for isinstance checks and future use.
print(f" synta.SyntaError: {synta.SyntaError}")
print(f" MRO: {[c.__name__ for c in synta.SyntaError.__mro__]}")
assert issubclass(synta.SyntaError, Exception)
print(" issubclass(synta.SyntaError, Exception): True")
def demo_unexpected_eof():
section("EOFError — truncated / empty input")
# Empty input raises EOFError
try:
synta.Decoder(b"", synta.Encoding.DER).decode_integer()
print(" No error (unexpected)")
except EOFError as e:
print(f" EOFError (empty input): {e}")
# One-byte truncation: tag present but length byte missing
try:
synta.Decoder(b"\x02", synta.Encoding.DER).decode_integer()
print(" No error (unexpected)")
except EOFError as e:
print(f" EOFError (tag only): {e}")
# Length declared but data truncated
try:
synta.Decoder(b"\x02\x04\x01\x02", synta.Encoding.DER).decode_integer()
print(" No error (unexpected)")
except EOFError as e:
print(f" EOFError (truncated value): {e}")
# Certificate.from_der with truncated DER
try:
synta.Certificate.from_der(b"\x30\x82\x04\x00\x30")
print(" No error (unexpected)")
except (EOFError, ValueError) as e:
print(f" {type(e).__name__} (truncated certificate): {e}")
def demo_unexpected_tag():
section("ValueError — unexpected ASN.1 tag")
# Try to decode BOOLEAN (tag 0x01) as INTEGER (tag 0x02)
enc = synta.Encoder(synta.Encoding.DER)
enc.encode_boolean(True)
bool_der = enc.finish()
dec = synta.Decoder(bool_der, synta.Encoding.DER)
try:
dec.decode_integer()
print(" No error (unexpected)")
except ValueError as e:
print(f" ValueError (bool as integer): {e}")
# Try to decode OCTET STRING as UTF8String
enc2 = synta.Encoder(synta.Encoding.DER)
enc2.encode_octet_string(b"hello")
oct_der = enc2.finish()
dec2 = synta.Decoder(oct_der, synta.Encoding.DER)
try:
dec2.decode_utf8_string()
print(" No error (unexpected)")
except ValueError as e:
print(f" ValueError (octet as utf8): {e}")
def demo_invalid_encoding():
section("ValueError — DER constraint violations")
# DER requires minimal encoding for BOOLEAN: 0x00 (FALSE) or 0xff (TRUE)
# A non-canonical BOOLEAN value (e.g., 0x01) violates DER
bad_bool = b"\x01\x01\x01" # BOOLEAN with value 0x01 (not 0xFF)
dec = synta.Decoder(bad_bool, synta.Encoding.DER)
try:
dec.decode_boolean()
print(" No error (unexpected)")
except ValueError as e:
print(f" ValueError (non-canonical BOOLEAN): {e}")
# DER requires INTEGER to use minimal encoding (no leading 0x00 padding)
bad_int = b"\x02\x02\x00\x01" # INTEGER 1 encoded with unnecessary leading 0x00
dec2 = synta.Decoder(bad_int, synta.Encoding.DER)
try:
dec2.decode_integer()
print(" No error (unexpected)")
except ValueError as e:
print(f" ValueError (non-minimal INTEGER): {e}")
def demo_integer_overflow():
section("OverflowError — Integer.to_int() and to_i128() on large values")
# A 20-byte positive integer exceeds i64 range
big_bytes = b"\x7f" + b"\xff" * 19 # 20-byte positive integer
big = synta.Integer.from_bytes(big_bytes)
try:
big.to_int()
print(" No error (unexpected)")
except OverflowError as e:
print(f" OverflowError from to_int(): {e}")
# A 16-byte value 2^127-1 (= i128 max) fits in i128
val_128 = b"\x7f" + b"\xff" * 15
ok = synta.Integer.from_bytes(val_128)
print(f" to_i128() on 2^127-1: {ok.to_i128()}")
# 2^128 overflows i128 (17-byte positive integer)
too_big = b"\x01" + b"\x00" * 16 # 2^128 (17 bytes, positive)
huge = synta.Integer.from_bytes(too_big)
try:
huge.to_i128()
print(" No error (unexpected)")
except OverflowError as e:
print(f" OverflowError from to_i128(): {e}")
def demo_validation_errors():
section("ValueError — type constructor validation")
# BmpString rejects characters outside the BMP (> U+FFFF)
try:
synta.BmpString("𝄞") # U+1D11E: musical symbol G clef, outside BMP
print(" No error (unexpected)")
except ValueError as e:
print(f" ValueError (BmpString non-BMP): {e}")
# UniversalString.from_bytes rejects byte counts not divisible by 4
try:
synta.UniversalString.from_bytes(b"\x00\x00\x00") # 3 bytes — not valid UCS-4
print(" No error (unexpected)")
except ValueError as e:
print(f" ValueError (UniversalString bad length): {e}")
# GeneralizedTime rejects invalid dates
try:
synta.GeneralizedTime(2026, 13, 1, 0, 0, 0, None) # month 13 is invalid
print(" No error (unexpected)")
except ValueError as e:
print(f" ValueError (GeneralizedTime bad month): {e}")
# ObjectIdentifier rejects syntactically invalid OID strings
try:
synta.ObjectIdentifier("not.a.valid.oid.string!")
print(" No error (unexpected)")
except ValueError as e:
print(f" ValueError (invalid OID string): {e}")
# Krb5PrincipalName rejects non-ASCII realm
try:
krb5.Krb5PrincipalName("RÉALM", krb5.NT_PRINCIPAL, ["alice"])
print(" No error (unexpected)")
except ValueError as e:
print(f" ValueError (Krb5PrincipalName non-ASCII realm): {e}")
# GeneralString.from_ascii rejects non-ASCII characters
try:
synta.GeneralString.from_ascii("réalm")
print(" No error (unexpected)")
except ValueError as e:
print(f" ValueError (GeneralString non-ASCII): {e}")
def demo_invalid_oid():
section("ValueError — invalid OID in DER")
# OID with a high-bit set on the last content byte: the arc encoding
# never terminates (the decoder expects more bytes but finds none).
bad_oid = b"\x06\x02\x2b\x80" # length=2, arc 0x2b then 0x80 (continuation flag, no terminator)
dec = synta.Decoder(bad_oid, synta.Encoding.DER)
try:
dec.decode_oid()
print(" No error (unexpected)")
except ValueError as e:
print(f" ValueError (malformed OID arc): {e}")
def demo_encoder_state_errors():
section("Encoder state — normal usage and finish() behavior")
# finish() on an encoder that has an open constructed element
enc = synta.Encoder(synta.Encoding.DER)
# encode_sequence takes pre-built content, so this path isn't directly
# reachable through normal API usage — the encoder manages state internally.
# Demonstrate that normal encode+finish never errors:
enc.encode_integer(1)
data = enc.finish()
assert data == b"\x02\x01\x01"
print(f" Normal encode+finish: {data.hex()} (INTEGER 1)")
# Calling finish() a second time returns an empty byte string (idempotent).
enc2 = synta.Encoder(synta.Encoding.DER)
enc2.encode_integer(2)
first = enc2.finish()
second = enc2.finish()
print(f" First finish(): {first.hex()}")
print(f" Second finish(): {second.hex()!r} (empty — encoder consumed)")
def demo_feature_gate():
section("ImportError / ValueError — feature-gated functionality")
# PKCS#12 and PKCS#7 require the 'pkcs' Rust feature.
# If not compiled in, from_der raises ImportError or ValueError.
try:
import synta
synta.Pkcs12.from_der(b"\x30\x00")
print(" Pkcs12 available")
except AttributeError:
print(" synta.Pkcs12 not available (pkcs feature not compiled)")
except (ImportError, ValueError) as e:
print(f" {type(e).__name__}: {e}")
def main():
print("=" * 60)
print("Example 20: Error handling")
print("=" * 60)
demo_synta_error_class()
demo_unexpected_eof()
demo_unexpected_tag()
demo_invalid_encoding()
demo_integer_overflow()
demo_validation_errors()
demo_invalid_oid()
demo_encoder_state_errors()
demo_feature_gate()
print("\nAll error handling examples completed.")
if __name__ == "__main__":
main()
```