synta 0.2.2

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
#!/usr/bin/env python3
"""
Example 16: Advanced Decoder operations.

Demonstrates: Decoder.decode_set, Decoder.peek_tag, Decoder.decode_raw_tlv,
Decoder.remaining_bytes, Decoder.decode_implicit_tag, Decoder.position,
Decoder.remaining, Decoder.is_empty, Decoder.decode_any_str.
"""

import synta


def section(title):
    print(f"\n{'' * 60}\n{title}\n{'' * 60}")


def demo_decode_set():
    section("decode_set — decode a SET and iterate elements")
    # Build a SET containing two PrintableStrings
    inner = synta.Encoder(synta.Encoding.DER)
    inner.encode_printable_string("Alice")
    inner.encode_printable_string("Bob")
    inner_bytes = inner.finish()

    outer = synta.Encoder(synta.Encoding.DER)
    outer.encode_set(inner_bytes)
    data = outer.finish()
    print(f"  SET DER: {data.hex()}")
    assert data[0] == 0x31  # SET tag

    dec = synta.Decoder(data, synta.Encoding.DER)
    child = dec.decode_set()
    assert dec.is_empty()

    s1 = child.decode_printable_string()
    s2 = child.decode_printable_string()
    assert child.is_empty()
    print(f"  Decoded: {s1.as_str()!r}, {s2.as_str()!r}")


def demo_peek_tag():
    section("peek_tag — CHOICE dispatch without consuming")
    # Encode INTEGER then BOOLEAN back to back
    enc = synta.Encoder(synta.Encoding.DER)
    enc.encode_integer(99)
    enc.encode_boolean(True)
    data = enc.finish()

    dec = synta.Decoder(data, synta.Encoding.DER)

    # First element: INTEGER (tag 2)
    tag_no, tag_class, is_constructed = dec.peek_tag()
    print(f"  peek → ({tag_no}, {tag_class!r}, {is_constructed})")
    assert tag_no == 2 and tag_class == "Universal"
    # peek_tag does NOT advance the position
    pos_before = dec.position()
    dec.peek_tag()
    assert dec.position() == pos_before
    print("  peek_tag is non-consuming: OK")

    val = dec.decode_integer()
    assert val.to_int() == 99

    # Second element: BOOLEAN (tag 1)
    tag_no, _, _ = dec.peek_tag()
    assert tag_no == 1
    val2 = dec.decode_boolean()
    assert val2.value() is True
    assert dec.is_empty()
    print("  CHOICE dispatch via peek_tag: OK")


def demo_choice_dispatch_loop():
    section("peek_tag in a loop — parse heterogeneous SEQUENCE")
    # Build SEQUENCE { INTEGER, BOOLEAN, UTF8String }
    inner = synta.Encoder(synta.Encoding.DER)
    inner.encode_integer(7)
    inner.encode_boolean(False)
    inner.encode_utf8_string("hello")
    outer = synta.Encoder(synta.Encoding.DER)
    outer.encode_sequence(inner.finish())
    data = outer.finish()

    dec = synta.Decoder(data, synta.Encoding.DER)
    seq = dec.decode_sequence()
    results = []
    while not seq.is_empty():
        tag_no, _, _ = seq.peek_tag()
        if tag_no == 2:    # INTEGER
            results.append(seq.decode_integer().to_int())
        elif tag_no == 1:  # BOOLEAN
            results.append(seq.decode_boolean().value())
        elif tag_no == 12: # UTF8String
            results.append(seq.decode_utf8_string().as_str())
        else:
            seq.decode_raw_tlv()  # skip unknown
    print(f"  Parsed: {results}")
    assert results == [7, False, "hello"]


def demo_decode_raw_tlv():
    section("decode_raw_tlv — capture an element as raw TLV bytes")
    enc = synta.Encoder(synta.Encoding.DER)
    enc.encode_integer(42)
    enc.encode_boolean(True)
    data = enc.finish()

    dec = synta.Decoder(data, synta.Encoding.DER)
    # Capture first element (INTEGER 42) without parsing its type
    raw = dec.decode_raw_tlv()
    print(f"  Captured TLV: {raw.hex()}  (tag 0x{raw[0]:02x} = INTEGER)")
    assert raw == b'\x02\x01\x2a'

    # Re-parse the captured bytes
    dec2 = synta.Decoder(raw, synta.Encoding.DER)
    val = dec2.decode_integer()
    assert val.to_int() == 42
    print("  Re-parsed TLV: OK")

    # Remaining byte: BOOLEAN
    assert not dec.is_empty()
    b = dec.decode_boolean()
    assert b.value() is True
    assert dec.is_empty()


def demo_decode_implicit_tag():
    section("decode_implicit_tag + remaining_bytes — primitive implicit type")
    # GeneralName dNSName [2] IMPLICIT IA5String "synta.example.com"
    name = b"synta.example.com"
    tlv = bytes([0x82, len(name)]) + name  # 0x82 = Context[2] primitive

    dec = synta.Decoder(tlv, synta.Encoding.DER)
    child = dec.decode_implicit_tag(2, "Context")
    assert dec.is_empty()

    # remaining_bytes() returns value bytes without advancing position
    raw = child.remaining_bytes()
    assert raw == name
    assert child.remaining() == len(name)  # position unchanged

    # Can also call remaining_bytes again (non-consuming)
    raw2 = child.remaining_bytes()
    assert raw2 == raw
    print(f"  dNSName: {raw.decode('ascii')!r}")
    print(f"  remaining_bytes() non-consuming: OK")


def demo_position_and_remaining():
    section("position() and remaining() — track decoder progress")
    enc = synta.Encoder(synta.Encoding.DER)
    enc.encode_integer(1)
    enc.encode_integer(2)
    enc.encode_integer(3)
    data = enc.finish()
    print(f"  Data: {data.hex()} ({len(data)} bytes)")

    dec = synta.Decoder(data, synta.Encoding.DER)
    assert dec.position() == 0
    assert dec.remaining() == len(data)
    print(f"  Start: position={dec.position()} remaining={dec.remaining()}")

    dec.decode_integer()  # consume INTEGER 1 (3 bytes)
    print(f"  After 1st: position={dec.position()} remaining={dec.remaining()}")
    assert dec.position() == 3

    dec.decode_integer()  # consume INTEGER 2
    print(f"  After 2nd: position={dec.position()} remaining={dec.remaining()}")

    dec.decode_integer()  # consume INTEGER 3
    assert dec.is_empty()
    assert dec.remaining() == 0
    print(f"  After 3rd: position={dec.position()} remaining={dec.remaining()} is_empty={dec.is_empty()}")


def demo_remaining_bytes_toplevel():
    section("remaining_bytes() at top level — bytes left after partial decode")
    enc = synta.Encoder(synta.Encoding.DER)
    enc.encode_integer(10)
    enc.encode_integer(20)
    data = enc.finish()

    dec = synta.Decoder(data, synta.Encoding.DER)
    dec.decode_integer()  # consume first INTEGER

    # remaining_bytes returns the rest of the input intact (with TLV headers)
    rest = dec.remaining_bytes()
    print(f"  Remaining after consuming first INTEGER: {rest.hex()}")
    # The rest is the DER for INTEGER 20
    dec2 = synta.Decoder(rest, synta.Encoding.DER)
    assert dec2.decode_integer().to_int() == 20
    print("  remaining_bytes() returned valid TLV for second INTEGER: OK")


def demo_decode_any_str():
    section("decode_any_str — decode any ASN.1 string type as Python str")
    # Build a SEQUENCE holding three different string types: UTF8String,
    # PrintableString, and TeletexString (Latin-1 encoded).
    inner = synta.Encoder(synta.Encoding.DER)
    inner.encode_utf8_string("café")               # UTF8String (tag 12)
    inner.encode_printable_string("Hello")          # PrintableString (tag 19)
    inner.encode_teletex_string(b"caf\xe9")         # TeletexString (tag 20) — Latin-1 bytes
    outer = synta.Encoder(synta.Encoding.DER)
    outer.encode_sequence(inner.finish())
    data = outer.finish()

    dec = synta.Decoder(data, synta.Encoding.DER)
    seq = dec.decode_sequence()

    # decode_any_str() handles all nine string types without caller needing
    # to inspect the tag or choose among decode_utf8_string / decode_teletex etc.
    s1 = seq.decode_any_str()
    s2 = seq.decode_any_str()
    s3 = seq.decode_any_str()
    assert seq.is_empty()

    print(f"  UTF8String:     {s1!r}")
    print(f"  PrintableString:{s2!r}")
    print(f"  TeletexString:  {s3!r}")
    assert s1 == "café"
    assert s2 == "Hello"
    assert s3 == "café"    # Latin-1 b"caf\xe9" → U+00E9 = é

    # EOFError when decoder is exhausted
    try:
        seq.decode_any_str()
        assert False, "expected EOFError"
    except EOFError:
        print("  EOFError on empty decoder: OK")

    # ValueError for non-string tag (INTEGER)
    enc2 = synta.Encoder(synta.Encoding.DER)
    enc2.encode_integer(1)
    dec2 = synta.Decoder(enc2.finish(), synta.Encoding.DER)
    try:
        dec2.decode_any_str()
        assert False, "expected ValueError"
    except ValueError as e:
        print(f"  ValueError for INTEGER: {e}")


def main():
    print("=" * 60)
    print("Example 16: Advanced Decoder operations")
    print("=" * 60)
    demo_decode_set()
    demo_peek_tag()
    demo_choice_dispatch_loop()
    demo_decode_raw_tlv()
    demo_decode_implicit_tag()
    demo_position_and_remaining()
    demo_remaining_bytes_toplevel()
    demo_decode_any_str()
    print("\nAll advanced Decoder examples completed.")


if __name__ == "__main__":
    main()