synta 0.1.5

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
# 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()
```