From dc3c99e463f482c68bb14afb9a63231d643610ae Mon Sep 17 00:00:00 2001 From: Xinyu Ma Date: Wed, 7 Oct 2026 23:25:03 -0700 Subject: [PATCH 1/8] encoding: add dataclass TLV codec to canonical model Co-authored-by: Cursor --- src/ndn/encoding/tlv_model.py | 1011 +++++++++++++++++++++++ src/ndn/encoding/tlv_model_v2.py | 0 tests/encoding/tlv_model_test.py | 1308 ++++++++++++++++++++++++++++++ 3 files changed, 2319 insertions(+) delete mode 100644 src/ndn/encoding/tlv_model_v2.py diff --git a/src/ndn/encoding/tlv_model.py b/src/ndn/encoding/tlv_model.py index 1cd0514..b7266c5 100644 --- a/src/ndn/encoding/tlv_model.py +++ b/src/ndn/encoding/tlv_model.py @@ -1067,3 +1067,1014 @@ def asdict(self, instance): else: ret[key] = val return ret + + +# ----------------------------------------------------------------------------- +# Copyright (C) 2019-2020 The python-ndn authors +# +# This file is part of python-ndn. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ----------------------------------------------------------------------------- +""" +Dataclass-based TLV encoding/decoding (v2 API). + +Usage:: + + from dataclasses import dataclass, field + from typing import List, Optional + from ndn.encoding import tlv_encode, tlv_parse, NDNName + + @dataclass + class Inner: + value: int = field(default=None, metadata={'tlv_type': 0x01}) + + @dataclass + class Outer: + name: NDNName = field(default=None, metadata={'tlv_type': 0x07}) + count: int = field(default=None, metadata={'tlv_type': 0x0a}) + payload: bytes = field(default=None, metadata={'tlv_type': 0x15}) + sub: Inner = field(default=None, metadata={'tlv_type': 0x16}) + tags: List[bytes] = field(default_factory=list, + metadata={'tlv_type': 0x17}) + + wire = tlv_encode(obj) + obj = tlv_parse(Outer, wire) + +Field-kind inference from Python annotation +------------------------------------------- ++--------------------------------------------+----------+------------------+ +| Annotation | Kind | Old equivalent | ++============================================+==========+==================+ +| int / Enum / Flag subclass | uint | UintField | ++--------------------------------------------+----------+------------------+ +| bool | bool | BoolField | ++--------------------------------------------+----------+------------------+ +| bytes / bytearray / memoryview | bytes | BytesField | ++--------------------------------------------+----------+------------------+ +| str | str | BytesField | +| | | (is_string=True) | ++--------------------------------------------+----------+------------------+ +| NDNName (sentinel) | name | NameField | ++--------------------------------------------+----------+------------------+ +| Any @dataclass type | model | ModelField | ++--------------------------------------------+----------+------------------+ +| List[T] | repeated | RepeatedField | ++--------------------------------------------+----------+------------------+ +| Dict[K, V] | map | MapField | ++--------------------------------------------+----------+------------------+ +| None + field_type='offset_marker' | (zero) | OffsetMarker | ++--------------------------------------------+----------+------------------+ +| bytes + field_type='sig_value' | (special)| SignatureValue | ++--------------------------------------------+----------+------------------+ +| NDNName + field_type='interest_name' | (special)| InterestNameField| ++--------------------------------------------+----------+------------------+ + +Supported metadata keys +----------------------- +``'tlv_type'`` int TLV type number (required except for offset_marker) +``'fixed_len'`` int Force uint value width: 1, 2, 4, or 8 bytes +``'ignore_critical' bool Suppress DecodeError for nested model parsing +``'field_type'`` str Explicit kind override when inference is insufficient + +For **map** fields (``Dict[K, V]``): +``'val_tlv_type'`` int TLV type for map values (required) + +For **sig_value** fields: +``'cover_start'`` str Name of the offset_marker field where sig coverage begins +``'digest_cover_start' str Same or different offset_marker; where digest coverage begins +``'digest_cover_end'`` str Offset_marker after sig_value; where digest coverage ends + +Signature machinery markers (set by caller before tlv_encode / tlv_parse): +``markers['##signer']`` Signer instance; absent means unsigned +``markers['##need_digest']`` True ⟹ insert/compute ParametersSha256DigestComponent + +Signature machinery markers (set by tlv_encode / tlv_parse internally): +``markers['##sig_covered_part']`` list[memoryview | bytes]: regions covered by sig +``markers['##sig_value_buf']`` writable memoryview into the placeholder bytes +``markers['##shrink_len']`` int: bytes trimmed from end after sig finalization +``markers['##digest_buf']`` writable memoryview into the digest component value +``markers[fname]`` int: recorded byte offset for each offset_marker field +""" +import dataclasses +import struct +import typing +import weakref +from enum import Enum, Flag +from hashlib import sha256 +from types import UnionType + +from .tlv_type import VarBinaryStr, is_binary_str +from .tlv_var import write_tl_num, parse_tl_num, get_tl_num_size +from .name import Name, Component + + +__all__ += [ + 'tlv_encode', 'tlv_parse', 'NDNName', 'DecodeError', + 'tlv_get_arg', 'tlv_set_arg', +] + +# Kinds that occupy zero wire bytes and may not have a 'tlv_type' metadata key. +_ZERO_WIRE_KINDS = frozenset({'offset_marker'}) + + +# --------------------------------------------------------------------------- +# NDNName sentinel — used as a type annotation for NDN Name fields +# --------------------------------------------------------------------------- + +class NDNName: + """ + Sentinel annotation type that marks a field as an NDN Name. + + Use it wherever you would have used :class:`~ndn.encoding.NameField` in + the old metaclass API:: + + name: NDNName = field(default=None, metadata={'tlv_type': 0x07}) + # repeated Names: + names: List[NDNName] = field(default_factory=list, + metadata={'tlv_type': 0x07}) + + The actual runtime value is :any:`FormalName` (a list of encoded + component bytes), exactly as returned by the old NameField. + """ + + +# --------------------------------------------------------------------------- +# Annotation helpers +# --------------------------------------------------------------------------- + +def _unwrap_optional(annotation): + """Return T for Optional[T] = Union[T, None]; otherwise return unchanged.""" + if typing.get_origin(annotation) in (typing.Union, UnionType): + args = [a for a in typing.get_args(annotation) if a is not type(None)] + if len(args) == 1: + return args[0] + return annotation + + +def _infer_kind(annotation, metadata: dict) -> str: + """ + Determine the TLV field kind from a Python type annotation plus metadata. + + Returns one of: ``'uint'``, ``'bool'``, ``'bytes'``, ``'str'``, + ``'name'``, ``'model'``, ``'repeated'``. + + The ``'field_type'`` metadata key overrides automatic inference. + """ + if 'field_type' in metadata: + return metadata['field_type'] + + annotation = _unwrap_optional(annotation) + origin = typing.get_origin(annotation) + + if origin is list: + return 'repeated' + if origin is dict: + return 'map' + if annotation is NDNName: + return 'name' + # bool must be checked before int since bool is a subclass of int + if annotation is bool: + return 'bool' + if annotation is int or ( + isinstance(annotation, type) + and issubclass(annotation, (int, Enum, Flag)) + and annotation is not bool): + return 'uint' + if annotation in (bytes, bytearray, memoryview): + return 'bytes' + if annotation is str: + return 'str' + if dataclasses.is_dataclass(annotation): + return 'model' + + raise TypeError( + f'Cannot infer TLV field kind from annotation {annotation!r}. ' + f"Use metadata key 'field_type' to override." + ) + + +def _element_annotation(annotation): + """Extract T from List[T]; falls back to bytes.""" + annotation = _unwrap_optional(annotation) + args = typing.get_args(annotation) + return args[0] if args else bytes + + +def _map_annotations(annotation): + """Extract (K, V) from Dict[K, V]; falls back to (str, bytes).""" + annotation = _unwrap_optional(annotation) + args = typing.get_args(annotation) + if len(args) == 2: + return args[0], args[1] + return str, bytes + + +def _map_key_meta(metadata: dict) -> dict: + """Build a synthetic metadata dict for a map key sub-field.""" + return {'tlv_type': metadata['tlv_type']} + + +def _map_val_meta(metadata: dict) -> dict: + """Build a synthetic metadata dict for a map value sub-field.""" + m = {'tlv_type': metadata['val_tlv_type']} + if 'ignore_critical' in metadata: + m['ignore_critical'] = metadata['ignore_critical'] + return m + + +# --------------------------------------------------------------------------- +# Per-class schema cache +# --------------------------------------------------------------------------- + +@dataclasses.dataclass(frozen=True, slots=True) +class _FieldSpec: + """Everything the encoder/parser needs about one field, resolved once.""" + name: str + kind: str + metadata: typing.Mapping + annotation: typing.Any + tlv_type: typing.Optional[int] + enum_cls: typing.Optional[type] = None + elem: typing.Optional['_FieldSpec'] = None + key: typing.Optional['_FieldSpec'] = None + val: typing.Optional['_FieldSpec'] = None + + +def _make_spec(name: str, annotation, metadata) -> _FieldSpec: + kind = _infer_kind(annotation, metadata) + annotation = _unwrap_optional(annotation) + enum_cls = elem = key = val = None + if kind == 'uint' and isinstance(annotation, type) and issubclass(annotation, (Enum, Flag)): + enum_cls = annotation + elif kind == 'repeated': + elem = _make_spec(name, _element_annotation(annotation), metadata) + elif kind == 'map': + key_ann, val_ann = _map_annotations(annotation) + key = _make_spec(name, key_ann, _map_key_meta(metadata)) + val = _make_spec(name, val_ann, _map_val_meta(metadata)) + return _FieldSpec(name, kind, metadata, annotation, metadata.get('tlv_type'), + enum_cls, elem, key, val) + + +_SCHEMA_CACHE: 'weakref.WeakKeyDictionary[type, tuple[_FieldSpec, ...]]' = weakref.WeakKeyDictionary() + + +def _get_schema(cls) -> tuple[_FieldSpec, ...]: + """ + Return the TLV field specs of dataclass *cls* in declaration order. + + Built on first use rather than at class definition so that forward + references to classes defined later in the same module can be resolved. + Fields with neither ``tlv_type`` nor ``field_type`` metadata are skipped. + """ + try: + return _SCHEMA_CACHE[cls] + except KeyError: + pass + hints = typing.get_type_hints(cls) + specs = [] + for f in dataclasses.fields(cls): + if 'tlv_type' not in f.metadata and 'field_type' not in f.metadata: + continue + spec = _make_spec(f.name, hints[f.name], f.metadata) + if spec.kind not in _ZERO_WIRE_KINDS and spec.tlv_type is None: + continue + specs.append(spec) + schema = tuple(specs) + _SCHEMA_CACHE[cls] = schema + return schema + + +# --------------------------------------------------------------------------- +# Interest-name helpers (used by both pass-1 and pass-2) +# --------------------------------------------------------------------------- + +def _encoded_length_interest_name(fname: str, val, metadata: dict, + markers: dict) -> int: + """ + Size pass for an Interest Name field. + + Mirrors ``InterestNameField.encoded_length``. If ``markers['##need_digest']`` + is truthy and the name does not already contain a + ``ParametersSha256DigestComponent``, 34 extra bytes are reserved for one. + """ + if val is None: + return 0 + type_num = metadata['tlv_type'] + need_digest = markers.get('##need_digest', False) + + # Normalize to a list of component bytes. + if isinstance(val, str): + name = Name.from_str(val) + elif is_binary_str(val): + name = Name.decode(val)[0] + else: + name = list(val) + for i, comp in enumerate(name): + if isinstance(comp, str): + name[i] = Component.from_str(Component.escape_str(comp)) + elif not is_binary_str(comp): + raise TypeError(f'{fname}: invalid name component {comp!r}') + + # Locate an existing ParametersSha256DigestComponent (at most one allowed). + digest_pos = None + for i, comp in enumerate(name): + if Component.get_type(comp) == Component.TYPE_PARAMETERS_SHA256: + if len(Component.get_value(comp)) != 32: + raise ValueError( + f'{fname}: ParametersSha256DigestComponent must be 32 bytes') + if need_digest: + if digest_pos is None: + digest_pos = i + else: + raise ValueError( + f'{fname}: multiple ParametersSha256DigestComponent in name') + + markers[f'{fname}##digest_pos'] = digest_pos + markers[f'{fname}##preprocessed_name'] = name + + comp_total = sum(len(c) for c in name) + if need_digest and digest_pos is None: + # Reserve space for a new digest component: T(1B) + L(1B) + V(32B). + comp_total += (get_tl_num_size(Component.TYPE_PARAMETERS_SHA256) + + get_tl_num_size(32) + 32) + + markers[f'{fname}##name_value_len'] = comp_total + return get_tl_num_size(type_num) + get_tl_num_size(comp_total) + comp_total + + +def _encode_into_interest_name(fname: str, val, metadata: dict, markers: dict, + wire: VarBinaryStr, offset: int) -> int: + """ + Write pass for an Interest Name field. + + Mirrors ``InterestNameField.encode_into``. Appends non-digest name + components to ``markers['##sig_covered_part']`` (wire slices) and stores + the writable digest-value buffer in ``markers['##digest_buf']``. + """ + if val is None: + return 0 + type_num = metadata['tlv_type'] + name = markers[f'{fname}##preprocessed_name'] + comp_total = markers[f'{fname}##name_value_len'] + digest_pos = markers[f'{fname}##digest_pos'] + need_digest = markers.get('##need_digest', False) + sig_covered_part = markers.setdefault('##sig_covered_part', []) + + origin = offset + t_sz = write_tl_num(type_num, wire, offset); offset += t_sz + l_sz = write_tl_num(comp_total, wire, offset); offset += l_sz + cover_start = offset + + for i, comp in enumerate(name): + comp_len = len(comp) + wire[offset:offset + comp_len] = comp + if i == digest_pos: + if offset > cover_start: + sig_covered_part.append(wire[cover_start:offset]) + # Value of the digest component sits after T + L (each 1 byte for + # TYPE_PARAMETERS_SHA256=2 < 253 and length=32 < 253). + c_t_sz = get_tl_num_size(Component.TYPE_PARAMETERS_SHA256) + c_l_sz = get_tl_num_size(32) + markers['##digest_buf'] = wire[offset + c_t_sz + c_l_sz:offset + comp_len] + cover_start = offset + comp_len + offset += comp_len + + if offset > cover_start: + sig_covered_part.append(wire[cover_start:offset]) + + if need_digest and digest_pos is None: + # Append a new ParametersSha256DigestComponent at the end of the name. + c_t_sz = write_tl_num(Component.TYPE_PARAMETERS_SHA256, wire, offset) + offset += c_t_sz + c_l_sz = write_tl_num(32, wire, offset) + offset += c_l_sz + markers['##digest_buf'] = wire[offset:offset + 32] + # Keep the preprocessed name up-to-date for get_final_name use. + name.append(bytes(wire[offset - c_t_sz - c_l_sz:offset + 32])) + offset += 32 + + return offset - origin + + +# --------------------------------------------------------------------------- +# Post-encoding finalization (signature + SHA-256 digest) +# --------------------------------------------------------------------------- + +def _finalize_encode(markers: dict, mv: memoryview, model_end: int) -> int: + """ + Called by :func:`tlv_encode` after all bytes have been written. + + 1. Asks the signer to fill in the signature-value placeholder, updates the + inline length byte if the actual signature is shorter (ECDSA), and + records ``markers['##shrink_len']``. + 2. If ``markers['##need_digest']`` is set, computes ``SHA-256`` over the + digest-covered range and writes it into the name's digest-component + placeholder (``markers['##digest_buf']``). + + Returns *shrink_size* (0 for fixed-length signature schemes like HMAC/EdDSA). + All offsets in *markers* are absolute positions within *mv*. + """ + signer = markers.get('##signer') + shrink_size = 0 + + if signer is not None and '##sig_value_buf' in markers: + sig_value_buf = markers['##sig_value_buf'] + alloc_size = len(sig_value_buf) + real_size = signer.write_signature_value( + sig_value_buf, markers.get('##sig_covered_part', [])) + shrink_size = alloc_size - real_size + markers['##shrink_len'] = shrink_size + if shrink_size > 0: + if alloc_size >= 253: + raise ValueError( + f'Signature with variable length ≥ 253 bytes is not supported ' + f'(allocated {alloc_size})') + markers['##sig_wire_l_field'][0] = real_size + + if markers.get('##need_digest') and '##digest_buf' in markers: + d_start_field = markers.get('##_digest_cover_start_field') + d_end_field = markers.get('##_digest_cover_end_field') + d_start = markers[d_start_field] if (d_start_field and d_start_field in markers) else 0 + d_end = markers[d_end_field] if (d_end_field and d_end_field in markers) else model_end + d_end -= shrink_size + markers['##digest_buf'][:] = sha256(bytes(mv[d_start:d_end])).digest() + + return shrink_size + + +# --------------------------------------------------------------------------- +# Encoding — pass 1: size computation +# --------------------------------------------------------------------------- + +def _uint_value_len(val: int, fname: str, fixed_len) -> int: + if fixed_len is not None: + if fixed_len not in (1, 2, 4, 8): + raise ValueError("uint fixed_len must be 1, 2, 4, or 8") + n = fixed_len + elif val <= 0xFF: + n = 1 + elif val <= 0xFFFF: + n = 2 + elif val <= 0xFFFFFFFF: + n = 4 + else: + n = 8 + if val >= 0x100 ** n: + raise ValueError(f'{fname}={val!r} cannot be encoded into {n} bytes') + return n + + +def _encoded_length_field(fname: str, val, spec: _FieldSpec, markers: dict) -> int: + """ + Compute the encoded byte count of one TLV field (T + L + V). + + Intermediate values are cached in *markers* under ``fname##...`` keys, + exactly mirroring the convention used by the v1 :class:`~ndn.encoding.Field` + subclasses. Returns 0 when the field is absent (*val* is ``None``/falsy + for bool). + """ + kind = spec.kind + # Zero-wire kinds: handled before looking up tlv_type. + if kind == 'offset_marker': + return 0 + + if kind == 'sig_value': + signer = markers.get('##signer') + if signer is None: + return 0 + type_num = spec.tlv_type + sig_size = signer.get_signature_value_size() + markers[f'{fname}##sig_size'] = sig_size + markers.setdefault('##sig_covered_part', []) + return get_tl_num_size(type_num) + get_tl_num_size(sig_size) + sig_size + + if kind == 'interest_name': + return _encoded_length_interest_name(fname, val, spec.metadata, markers) + + type_num = spec.tlv_type + + # BoolField: present if truthy, absent otherwise + if kind == 'bool': + return (get_tl_num_size(type_num) + 1) if val else 0 + + if val is None: + return 0 + + if kind == 'uint': + if isinstance(val, (Enum, Flag)): + val = val.value + if not isinstance(val, int) or val < 0: + raise TypeError(f'{fname}={val!r} is not a non-negative integer') + fixed_len = spec.metadata.get('fixed_len') + vlen = _uint_value_len(val, fname, fixed_len) + markers[f'{fname}##encoded_length'] = vlen + # L for uint is always 1 byte because vlen ∈ {1,2,4,8} < 253 + return get_tl_num_size(type_num) + 1 + vlen + + if kind in ('bytes', 'str'): + if isinstance(val, str): + raw = val.encode('utf-8') + markers[f'{fname}##encoded_str'] = raw + else: + raw = val + n = len(raw) + return get_tl_num_size(type_num) + get_tl_num_size(n) + n + + if kind == 'name': + # Normalise to list-of-components or a pre-encoded binary blob + name_val = val + if isinstance(name_val, str): + name_val = Name.from_str(name_val) + elif not is_binary_str(name_val): + if hasattr(name_val, '__iter__'): + name_val = list(name_val) + for i, comp in enumerate(name_val): + if isinstance(comp, str): + name_val[i] = Component.from_str(Component.escape_str(comp)) + elif not is_binary_str(comp): + raise TypeError(f'{fname}: invalid name component type') + else: + raise TypeError(f'{fname}: invalid name type') + if isinstance(name_val, list): + total_with_tl = Name.encoded_length(name_val) + else: + total_with_tl = len(name_val) + markers[f'{fname}##preprocessed_name'] = name_val + markers[f'{fname}##encoded_length_with_tl'] = total_with_tl + return total_with_tl + + if kind == 'model': + if not isinstance(val, spec.annotation): + raise TypeError(f'{fname}={val!r} is not of type {spec.annotation!r}') + inner_markers: dict = {} + length = _encoded_length_model(val, inner_markers) + markers[f'{fname}##inner_markers'] = inner_markers + markers[f'{fname}##encoded_length'] = length + return get_tl_num_size(type_num) + get_tl_num_size(length) + length + + if kind == 'repeated': + if not val: + return 0 + elem = spec.elem + total = 0 + for i, ele in enumerate(val): + total += _encoded_length_field(f'{fname}[{i}]', ele, elem, markers) + return total + + if kind == 'map': + if not val: + return 0 + key_spec, val_spec = spec.key, spec.val + total = 0 + for i, (k, v) in enumerate(val.items()): + total += _encoded_length_field(f'{fname}[{i}#k]', k, key_spec, markers) + total += _encoded_length_field(f'{fname}[{i}#v]', v, val_spec, markers) + return total + + raise TypeError(f'Unknown field kind {kind!r} for {fname!r}') + + +def _encoded_length_model(obj, markers: dict) -> int: + """Compute the total encoded length for all TLV fields of a dataclass object.""" + total = 0 + for spec in _get_schema(type(obj)): + total += _encoded_length_field(spec.name, getattr(obj, spec.name), spec, markers) + markers['##encoded_length'] = total + return total + + +# --------------------------------------------------------------------------- +# Encoding — pass 2: write bytes +# --------------------------------------------------------------------------- + +def _encode_into_field(fname: str, val, spec: _FieldSpec, + markers: dict, wire: VarBinaryStr, offset: int) -> int: + """ + Write one TLV field into *wire* at *offset*. + + *wire* must be a writable :class:`memoryview` (or :class:`bytearray`). + Returns the number of bytes written. Must be called after the matching + :func:`_encoded_length_field` call so that ``markers`` is populated. + """ + kind = spec.kind + metadata = spec.metadata + # Zero-wire kinds: handled before looking up tlv_type. + if kind == 'offset_marker': + markers[fname] = offset + return 0 + + if kind == 'sig_value': + signer = markers.get('##signer') + if signer is None: + return 0 + type_num = spec.tlv_type + sig_size = markers[f'{fname}##sig_size'] + # Collect the covered region: from cover_start up to current offset. + cover_start_field = metadata.get('cover_start') + cover_start = markers.get(cover_start_field, 0) if cover_start_field else 0 + markers.setdefault('##sig_covered_part', []).append(wire[cover_start:offset]) + # Store digest-coverage field names for _finalize_encode. + for mkey in ('digest_cover_start', 'digest_cover_end'): + if mkey in metadata: + markers[f'##_{mkey}_field'] = metadata[mkey] + # Write T + L (stored for in-place shrink) + placeholder V. + t_sz = write_tl_num(type_num, wire, offset) + l_off = offset + t_sz + l_sz = write_tl_num(sig_size, wire, l_off) + markers['##sig_wire_l_field'] = wire[l_off:l_off + l_sz] + v_start = l_off + l_sz + markers['##sig_value_buf'] = wire[v_start:v_start + sig_size] + return t_sz + l_sz + sig_size + + if kind == 'interest_name': + return _encode_into_interest_name(fname, val, metadata, markers, wire, offset) + + type_num = spec.tlv_type + + if kind == 'bool': + if val: + t_size = write_tl_num(type_num, wire, offset) + wire[offset + t_size] = 0 # L = 0 + return t_size + 1 + return 0 + + if val is None: + return 0 + + if kind == 'uint': + if isinstance(val, (Enum, Flag)): + val = val.value + vlen = markers[f'{fname}##encoded_length'] + t_size = write_tl_num(type_num, wire, offset) + if vlen == 1: + struct.pack_into('!BB', wire, offset + t_size, 1, val) + elif vlen == 2: + struct.pack_into('!BH', wire, offset + t_size, 2, val) + elif vlen == 4: + struct.pack_into('!BI', wire, offset + t_size, 4, val) + else: + struct.pack_into('!BQ', wire, offset + t_size, 8, val) + return t_size + 1 + vlen # T + L(1 byte) + V + + if kind in ('bytes', 'str'): + raw = markers.get(f'{fname}##encoded_str') + if raw is None: + raw = val.encode('utf-8') if isinstance(val, str) else val + n = len(raw) + t_size = write_tl_num(type_num, wire, offset) + l_size = write_tl_num(n, wire, offset + t_size) + v_start = offset + t_size + l_size + wire[v_start:v_start + n] = raw # zero-copy slice assignment + return t_size + l_size + n + + if kind == 'name': + name_val = markers[f'{fname}##preprocessed_name'] + name_len = markers[f'{fname}##encoded_length_with_tl'] + if isinstance(name_val, list): + Name.encode(name_val, wire, offset) + else: + wire[offset:offset + name_len] = name_val + return name_len + + if kind == 'model': + inner_markers = markers[f'{fname}##inner_markers'] + length = markers[f'{fname}##encoded_length'] + t_size = write_tl_num(type_num, wire, offset) + l_size = write_tl_num(length, wire, offset + t_size) + _encode_into_model(val, inner_markers, wire, offset + t_size + l_size) + return t_size + l_size + length + + if kind == 'repeated': + if not val: + return 0 + elem = spec.elem + total = 0 + for i, ele in enumerate(val): + total += _encode_into_field( + f'{fname}[{i}]', ele, elem, markers, wire, offset + total) + return total + + if kind == 'map': + if not val: + return 0 + key_spec, val_spec = spec.key, spec.val + total = 0 + for i, (k, v) in enumerate(val.items()): + total += _encode_into_field( + f'{fname}[{i}#k]', k, key_spec, markers, wire, offset + total) + total += _encode_into_field( + f'{fname}[{i}#v]', v, val_spec, markers, wire, offset + total) + return total + + raise TypeError(f'Unknown field kind {kind!r} for {fname!r}') + + +def _encode_into_model(obj, markers: dict, wire: VarBinaryStr, offset: int) -> None: + """Write all TLV fields of a dataclass object into *wire* starting at *offset*.""" + for spec in _get_schema(type(obj)): + offset += _encode_into_field( + spec.name, getattr(obj, spec.name), spec, markers, wire, offset) + + +# --------------------------------------------------------------------------- +# Public encode entry point +# --------------------------------------------------------------------------- + +def tlv_encode(obj, wire=None, offset: int = 0, markers: dict = None): + """ + Encode a dataclass TLV object. + + **Allocating form** — ``tlv_encode(obj)`` + Allocates a new :class:`bytearray`, fills it, and returns it. + + **In-place form** — ``tlv_encode(obj, wire, offset=0)`` + Encodes into an existing *wire* (:class:`bytearray` or writable + :class:`memoryview`) starting at *offset*. Returns a zero-copy + :class:`memoryview` slice of the written region. + + :param obj: dataclass instance to encode. + :param wire: optional writable buffer. + :param offset: starting byte offset within *wire*. + :param markers: optional shared markers dict (for multi-model coordination). + :return: :class:`bytearray` (allocating) or :class:`memoryview` (in-place). + """ + if markers is None: + markers = {} + total = _encoded_length_model(obj, markers) + if wire is None: + buf = bytearray(total) + mv = memoryview(buf) + _encode_into_model(obj, markers, mv, 0) + shrink = _finalize_encode(markers, mv, total) + if shrink: + # Can't resize bytearray while memoryview exports are live (the sig/digest + # slices in markers still reference mv). Return a trimmed copy instead. + return bytearray(mv[:total - shrink]) + return buf + mv = memoryview(wire) + _encode_into_model(obj, markers, mv, offset) + shrink = _finalize_encode(markers, mv, offset + total) + return mv[offset:offset + total - shrink] + + +# --------------------------------------------------------------------------- +# Parsing +# --------------------------------------------------------------------------- + +def _make_default_instance(cls): + """ + Create a dataclass instance with all fields set to their defaults. + + Uses ``object.__new__`` to bypass ``__init__``, then sets each field: + - ``field(default=X)`` → X + - ``field(default_factory=F)`` → F() + - no default → None (same behaviour as old TlvModel.parse) + """ + obj = object.__new__(cls) + for f in dataclasses.fields(cls): + if f.default is not dataclasses.MISSING: + object.__setattr__(obj, f.name, f.default) + elif f.default_factory is not dataclasses.MISSING: + object.__setattr__(obj, f.name, f.default_factory()) + else: + object.__setattr__(obj, f.name, None) + return obj + + +def _parse_value(fname: str, spec: _FieldSpec, + wire, offset: int, length: int, offset_btl: int, + ignore_critical: bool): + """ + Parse a single TLV *value* (V only, not T or L) from *wire*. + + :param fname: field name (for error messages). + :param spec: resolved field spec. + :param wire: memoryview of the full wire buffer. + :param offset: byte offset of V within *wire*. + :param length: byte length of V. + :param offset_btl: byte offset of the TLV's T field within *wire* + (used by NameField to pass to ``Name.decode``). + :param ignore_critical: forwarded to nested ``tlv_parse`` calls. + :return: the parsed Python value. + """ + kind = spec.kind + if kind == 'bool': + return True + + if kind == 'uint': + if length == 1: + raw = struct.unpack_from('!B', wire, offset)[0] + elif length == 2: + raw = struct.unpack_from('!H', wire, offset)[0] + elif length == 4: + raw = struct.unpack_from('!I', wire, offset)[0] + elif length == 8: + raw = struct.unpack_from('!Q', wire, offset)[0] + else: + raise ValueError( + f'{fname}: uint value length must be 1, 2, 4, or 8; got {length}') + # Auto-convert to the annotated Enum/Flag type if applicable + if spec.enum_cls is not None: + try: + return spec.enum_cls(raw) + except ValueError: + pass + return raw + + if kind == 'bytes': + return wire[offset:offset + length] # zero-copy memoryview slice + + if kind == 'str': + return bytes(wire[offset:offset + length]).decode('utf-8') + + if kind == 'name': + return Name.decode(wire, offset_btl)[0] + + if kind == 'model': + ignore = spec.metadata.get('ignore_critical', ignore_critical) + return tlv_parse(spec.annotation, wire[offset:offset + length], ignore) + + raise TypeError(f'Unknown kind {kind!r} for {fname!r}') + + +def tlv_parse(cls, wire, ignore_critical: bool = False, markers: dict = None): + """ + Parse a TLV-encoded buffer into a fresh dataclass instance. + + Matching follows NDN ordering rules — fields are matched in their + declaration order within *cls* (parent class fields come first, as per + standard Python dataclass inheritance). + + Unknown critical TLV types (odd type numbers) raise + :exc:`~ndn.encoding.DecodeError` unless *ignore_critical* is ``True``. + + Bytes-typed fields (``bytes``, ``bytearray``, ``memoryview`` annotations) + are returned as zero-copy :class:`memoryview` slices into *wire*. + + :param cls: dataclass class to parse into. + :param wire: TLV-encoded buffer + (:class:`bytes`, :class:`bytearray`, or :class:`memoryview`). + :param ignore_critical: suppress :exc:`DecodeError` for unknown critical + TLV types. + :param markers: optional dict for out-of-band state (offset_marker positions, + sig/digest buffers). A fresh ``{}`` is used when ``None``. + :return: populated dataclass instance. + :raises DecodeError: unknown critical TLV type encountered. + """ + if markers is None: + markers = {} + + # Wrap in memoryview for zero-copy slicing throughout the parse + if isinstance(wire, memoryview): + mv = wire + else: + mv = memoryview(wire if isinstance(wire, (bytes, bytearray)) else bytes(wire)) + + ordered = _get_schema(cls) + + obj = _make_default_instance(cls) + offset = 0 + field_pos = 0 # lowest index still eligible for matching + + while offset < len(mv): + offset_btl = offset + typ, sz_t = parse_tl_num(mv, offset) + offset += sz_t + length, sz_l = parse_tl_num(mv, offset) + offset += sz_l + if length > len(mv) - offset: + raise IndexError('TLV length exceeds the input buffer') + + found = False + for i in range(field_pos, len(ordered)): + spec = ordered[i] + kind = spec.kind + if kind == 'offset_marker': + continue # never matches a wire TLV type + + if spec.tlv_type != typ: + continue + + fname = spec.name + # Advance any offset_markers between field_pos and i. + for j in range(field_pos, i): + if ordered[j].kind == 'offset_marker': + markers[ordered[j].name] = offset_btl + + if kind == 'repeated': + val = _parse_value(fname, spec.elem, + mv, offset, length, offset_btl, ignore_critical) + lst = getattr(obj, fname) + if lst is None: + lst = [] + object.__setattr__(obj, fname, lst) + lst.append(val) + field_pos = i # stay at i to accept more elements + + elif kind == 'map': + # Two-phase parse: consume key, then immediately read value TLV. + dct = getattr(obj, fname) + if dct is None: + dct = {} + object.__setattr__(obj, fname, dct) + idx = len(dct) + + key = _parse_value(f'{fname}[{idx}#k]', spec.key, + mv, offset, length, offset_btl, ignore_critical) + + # advance past key value → now at the value TLV + offset += length + offset_btl = offset + _val_typ, _sz_t2 = parse_tl_num(mv, offset) + offset += _sz_t2 + length, _sz_l2 = parse_tl_num(mv, offset) + offset += _sz_l2 + if _val_typ != spec.val.tlv_type: + raise DecodeError( + f'{fname}: expected map value type {spec.val.tlv_type:#x}, got {_val_typ:#x}') + if length > len(mv) - offset: + raise IndexError('map value length exceeds the input buffer') + + val = _parse_value(f'{fname}[{idx}#v]', spec.val, + mv, offset, length, offset_btl, ignore_critical) + dct[key] = val + field_pos = i # stay at i to accept more pairs + + elif kind == 'sig_value': + # Extract sig buffer; append covered region to ##sig_covered_part. + sig_buf = mv[offset:offset + length] + markers['##sig_value_buf'] = sig_buf + cover_start_field = spec.metadata.get('cover_start') + if cover_start_field is not None: + cover_start = markers.get(cover_start_field) + if cover_start is not None: + markers.setdefault('##sig_covered_part', []).append( + mv[cover_start:offset_btl]) + object.__setattr__(obj, fname, sig_buf) + field_pos = i + 1 + + elif kind == 'interest_name': + # Decode name; split into sig-covered components and digest buf. + name = Name.decode(mv, offset_btl)[0] + sig_cp = markers.setdefault('##sig_covered_part', []) + for comp in name: + if Component.get_type(comp) == Component.TYPE_PARAMETERS_SHA256: + markers['##digest_buf'] = Component.get_value(comp) + else: + sig_cp.append(comp) + object.__setattr__(obj, fname, name) + field_pos = i + 1 + + else: + val = _parse_value(fname, spec, + mv, offset, length, offset_btl, ignore_critical) + object.__setattr__(obj, fname, val) + field_pos = i + 1 + + found = True + break + + if not found and (typ & 1) and not ignore_critical: + raise DecodeError( + f'unknown critical TLV type {typ:#x} is unrecognized, ' + f'redundant, or out-of-order') + + offset += length + + return obj + + +# --------------------------------------------------------------------------- +# Marker helpers (convenience wrappers for the markers dict) +# --------------------------------------------------------------------------- + +def tlv_get_arg(markers: dict, key: str, default=None): + """ + Read a value from the *markers* dict used by :func:`tlv_encode` / + :func:`tlv_parse`. + + Equivalent to ``markers.get(key, default)``. + """ + return markers.get(key, default) + + +def tlv_set_arg(markers: dict, key: str, val) -> None: + """ + Write a value into the *markers* dict used by :func:`tlv_encode` / + :func:`tlv_parse`. + + Equivalent to ``markers[key] = val``. + """ + markers[key] = val diff --git a/src/ndn/encoding/tlv_model_v2.py b/src/ndn/encoding/tlv_model_v2.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/encoding/tlv_model_test.py b/tests/encoding/tlv_model_test.py index ebb26ef..bf8625c 100644 --- a/tests/encoding/tlv_model_test.py +++ b/tests/encoding/tlv_model_test.py @@ -203,3 +203,1311 @@ class Model(TlvModel): 'enum_arr': [EnumVal.E1, EnumVal.E2], 'str_val': 'वरुण', 'str_arr': ['あいう', 'utf-8']} + + +# ----------------------------------------------------------------------------- +# Copyright (C) 2019-2020 The python-ndn authors +# +# This file is part of python-ndn. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ----------------------------------------------------------------------------- +"""Tests for the dataclass-based TLV v2 API (tlv_encode / tlv_parse).""" +from dataclasses import dataclass, field +from enum import IntEnum, IntFlag +from hashlib import sha256 + +import pytest + +from ndn.encoding import ( + tlv_encode, tlv_parse, NDNName, DecodeError, tlv_get_arg, tlv_set_arg, + # v1 equivalents used for binary-compatibility checks + TlvModel, UintField, BoolField, BytesField, NameField, ModelField, + RepeatedField, Name, Signer, +) + + +# --------------------------------------------------------------------------- +# Shared dataclass fixtures (defined at module scope for get_type_hints) +# --------------------------------------------------------------------------- + +@dataclass +class _Inner: + val: int = field(default=None, metadata={'tlv_type': 0x01}) + + +@dataclass +class _Outer: + inner: _Inner = field(default=None, metadata={'tlv_type': 0x02}) + + +@dataclass +class _RepeatedUint: + words: list[int] = field(default_factory=list, + metadata={'tlv_type': 0x01, 'fixed_len': 2}) + + +@dataclass +class _RepeatedModel: + items: list[_Inner] = field(default_factory=list, metadata={'tlv_type': 0x10}) + + +# --------------------------------------------------------------------------- +# TestUintField +# --------------------------------------------------------------------------- + +class TestUintField: + """UintField: variable-width and fixed-width non-negative integers.""" + + def test_min_width_1_byte(self): + @dataclass + class M: + x: int = field(default=None, metadata={'tlv_type': 0x03}) + + obj = M(x=0) + wire = tlv_encode(obj) + assert wire == b'\x03\x01\x00' + assert tlv_parse(M, wire).x == 0 + + def test_min_width_2_bytes(self): + @dataclass + class M: + x: int = field(default=None, metadata={'tlv_type': 0x03}) + + obj = M(x=0x0100) + wire = tlv_encode(obj) + assert wire == b'\x03\x02\x01\x00' + assert tlv_parse(M, wire).x == 0x0100 + + def test_min_width_4_bytes(self): + @dataclass + class M: + x: int = field(default=None, metadata={'tlv_type': 0x03}) + + obj = M(x=0x00010000) + wire = tlv_encode(obj) + assert wire == b'\x03\x04\x00\x01\x00\x00' + assert tlv_parse(M, wire).x == 0x00010000 + + def test_min_width_8_bytes(self): + @dataclass + class M: + x: int = field(default=None, metadata={'tlv_type': 0x03}) + + obj = M(x=0x0000000100000000) + wire = tlv_encode(obj) + assert wire == b'\x03\x08\x00\x00\x00\x01\x00\x00\x00\x00' + assert tlv_parse(M, wire).x == 0x0000000100000000 + + def test_fixed_len_1(self): + @dataclass + class M: + x: int = field(default=None, metadata={'tlv_type': 0x1b, 'fixed_len': 1}) + + obj = M(x=3) + wire = tlv_encode(obj) + # T=0x1b L=0x01 V=0x03 + assert wire == b'\x1b\x01\x03' + assert tlv_parse(M, wire).x == 3 + + def test_fixed_len_2(self): + @dataclass + class M: + x: int = field(default=None, metadata={'tlv_type': 0x03, 'fixed_len': 2}) + + obj = M(x=5) + wire = tlv_encode(obj) + assert wire == b'\x03\x02\x00\x05' + assert tlv_parse(M, wire).x == 5 + + def test_none_omitted(self): + @dataclass + class M: + x: int = field(default=None, metadata={'tlv_type': 0x03}) + y: int = field(default=None, metadata={'tlv_type': 0x05}) + + wire = tlv_encode(M(x=None, y=7)) + assert wire == b'\x05\x01\x07' + p = tlv_parse(M, wire) + assert p.x is None + assert p.y == 7 + + def test_optional_annotation(self): + @dataclass + class M: + x: int | None = field(default=None, metadata={'tlv_type': 0x03}) + + obj = M(x=42) + wire = tlv_encode(obj) + assert tlv_parse(M, wire).x == 42 + + +class TestUintFieldEnum: + """IntEnum and IntFlag auto-conversion on parse.""" + + def test_intenum_roundtrip(self): + class FaceType(IntEnum): + PERSISTENT = 1 + ON_DEMAND = 2 + + @dataclass + class M: + face_type: FaceType = field(default=None, metadata={'tlv_type': 0x84}) + + obj = M(face_type=FaceType.PERSISTENT) + wire = tlv_encode(obj) + p = tlv_parse(M, wire) + assert p.face_type == FaceType.PERSISTENT + assert isinstance(p.face_type, FaceType) + + def test_intflag_roundtrip(self): + class Flags(IntFlag): + A = 1 + B = 2 + + @dataclass + class M: + f: Flags = field(default=None, metadata={'tlv_type': 0x06}) + + obj = M(f=Flags.A | Flags.B) + wire = tlv_encode(obj) + p = tlv_parse(M, wire) + assert p.f == Flags.A | Flags.B + assert isinstance(p.f, Flags) + + def test_unknown_enum_value_returns_int(self): + class Color(IntEnum): + RED = 1 + + @dataclass + class M: + c: Color = field(default=None, metadata={'tlv_type': 0x01}) + + # Wire with value 99, which is not a valid Color + wire = b'\x01\x01\x63' + p = tlv_parse(M, wire) + assert p.c == 99 + assert type(p.c) is int + + +# --------------------------------------------------------------------------- +# TestBoolField +# --------------------------------------------------------------------------- + +class TestBoolField: + """BoolField: 0-length TLV present when truthy, absent otherwise.""" + + def test_present(self): + @dataclass + class M: + flag: bool = field(default=None, metadata={'tlv_type': 0x12}) + + wire = tlv_encode(M(flag=True)) + # T=0x12 L=0x00 + assert wire == b'\x12\x00' + assert tlv_parse(M, wire).flag is True + + def test_absent_when_false(self): + @dataclass + class M: + flag: bool = field(default=None, metadata={'tlv_type': 0x12}) + + assert tlv_encode(M(flag=False)) == b'' + assert tlv_encode(M(flag=None)) == b'' + + def test_absent_field_returns_none(self): + @dataclass + class M: + flag: bool = field(default=None, metadata={'tlv_type': 0x12}) + x: int = field(default=None, metadata={'tlv_type': 0x14}) + + wire = tlv_encode(M(flag=None, x=1)) + p = tlv_parse(M, wire) + assert p.flag is None + assert p.x == 1 + + def test_optional_annotation(self): + @dataclass + class M: + flag: bool | None = field(default=None, metadata={'tlv_type': 0x12}) + + wire = tlv_encode(M(flag=True)) + assert tlv_parse(M, wire).flag is True + + +# --------------------------------------------------------------------------- +# TestBytesField +# --------------------------------------------------------------------------- + +class TestBytesField: + """BytesField: raw bytes and UTF-8 strings.""" + + def test_bytes_roundtrip(self): + @dataclass + class M: + data: bytes = field(default=None, metadata={'tlv_type': 0x15}) + + obj = M(data=b'\x01\x02\x03') + wire = tlv_encode(obj) + assert wire == b'\x15\x03\x01\x02\x03' + p = tlv_parse(M, wire) + assert bytes(p.data) == b'\x01\x02\x03' + + def test_bytes_parse_is_memoryview(self): + @dataclass + class M: + data: bytes = field(default=None, metadata={'tlv_type': 0x15}) + + wire = b'\x15\x02\xde\xad' + p = tlv_parse(M, wire) + assert isinstance(p.data, memoryview) + + def test_str_field_roundtrip(self): + @dataclass + class M: + label: str = field(default=None, metadata={'tlv_type': 0x16}) + + obj = M(label='hello') + wire = tlv_encode(obj) + assert wire == b'\x16\x05hello' + assert tlv_parse(M, wire).label == 'hello' + + def test_str_field_unicode(self): + @dataclass + class M: + s: str = field(default=None, metadata={'tlv_type': 0x16}) + + obj = M(s='日本語') + wire = tlv_encode(obj) + assert tlv_parse(M, wire).s == '日本語' + + def test_bytearray_annotation(self): + @dataclass + class M: + data: bytearray = field(default=None, metadata={'tlv_type': 0x15}) + + wire = tlv_encode(M(data=bytearray(b'abc'))) + p = tlv_parse(M, wire) + assert bytes(p.data) == b'abc' + + def test_memoryview_annotation(self): + @dataclass + class M: + data: memoryview = field(default=None, metadata={'tlv_type': 0x15}) + + src = bytearray(b'\xca\xfe') + wire = tlv_encode(M(data=memoryview(src))) + p = tlv_parse(M, wire) + assert isinstance(p.data, memoryview) + assert bytes(p.data) == b'\xca\xfe' + + def test_large_value_multibyte_length(self): + """Length field uses multi-byte varint when value ≥ 253 bytes.""" + @dataclass + class M: + data: bytes = field(default=None, metadata={'tlv_type': 0x15}) + + payload = bytes(range(256)) + wire = tlv_encode(M(data=payload)) + # Length 256 → encoded as 0xFD 0x01 0x00 (3 bytes) + assert wire[1:4] == b'\xfd\x01\x00' + p = tlv_parse(M, wire) + assert bytes(p.data) == payload + + +# --------------------------------------------------------------------------- +# TestNameField +# --------------------------------------------------------------------------- + +class TestNameField: + """NDNName: NDN Name TLV via string, FormalName list, or binary.""" + + def test_from_string(self): + @dataclass + class M: + name: NDNName = field(default=None, metadata={'tlv_type': 0x07}) + + obj = M(name='/foo/bar') + wire = tlv_encode(obj) + # 0x07 0x0a [0x08 0x03 foo] [0x08 0x03 bar] + assert wire == b'\x07\x0a\x08\x03foo\x08\x03bar' + p = tlv_parse(M, wire) + assert Name.to_str(p.name) == '/foo/bar' + + def test_from_formal_name(self): + @dataclass + class M: + name: NDNName = field(default=None, metadata={'tlv_type': 0x07}) + + formal = Name.from_str('/a/b') + wire = tlv_encode(M(name=formal)) + p = tlv_parse(M, wire) + assert Name.to_str(p.name) == '/a/b' + + def test_empty_name(self): + @dataclass + class M: + name: NDNName = field(default=None, metadata={'tlv_type': 0x07}) + + wire = tlv_encode(M(name='/')) + p = tlv_parse(M, wire) + assert p.name == [] + + def test_none_omitted(self): + @dataclass + class M: + name: NDNName = field(default=None, metadata={'tlv_type': 0x07}) + + assert tlv_encode(M(name=None)) == b'' + + def test_repeated_names(self): + """List[NDNName] — multiple Name TLVs with the same type number.""" + @dataclass + class M: + names: list[NDNName] = field(default_factory=list, + metadata={'tlv_type': 0x07}) + + obj = M(names=['/foo', '/bar']) + wire = tlv_encode(obj) + p = tlv_parse(M, wire) + assert len(p.names) == 2 + assert Name.to_str(p.names[0]) == '/foo' + assert Name.to_str(p.names[1]) == '/bar' + + +# --------------------------------------------------------------------------- +# TestModelField +# --------------------------------------------------------------------------- + +class TestModelField: + """ModelField: nested dataclass, recursively encoded.""" + + def test_basic_nested(self): + wire = tlv_encode(_Outer(inner=_Inner(val=255))) + # 0x02 (outer T) 0x03 (outer L) 0x01 0x01 0xFF + assert wire == b'\x02\x03\x01\x01\xff' + p = tlv_parse(_Outer, wire) + assert p.inner.val == 255 + + def test_absent_nested(self): + wire = tlv_encode(_Outer(inner=None)) + assert wire == b'' + p = tlv_parse(_Outer, wire) + assert p.inner is None + + def test_deeply_nested(self): + @dataclass + class Level3: + x: int = field(default=None, metadata={'tlv_type': 0x01}) + + @dataclass + class Level2: + sub: Level3 = field(default=None, metadata={'tlv_type': 0x10}) + + @dataclass + class Level1: + sub: Level2 = field(default=None, metadata={'tlv_type': 0x20}) + + obj = Level1(sub=Level2(sub=Level3(x=7))) + wire = tlv_encode(obj) + p = tlv_parse(Level1, wire) + assert p.sub.sub.x == 7 + + def test_ignore_critical_propagated(self): + """ignore_critical in metadata is forwarded to nested parse.""" + @dataclass + class Inner: + x: int = field(default=None, metadata={'tlv_type': 0x01}) + + @dataclass + class Outer: + sub: Inner = field(default=None, + metadata={'tlv_type': 0x10, + 'ignore_critical': True}) + + # Inject an unknown critical TLV (type 0x03, odd) inside sub + inner_wire = b'\x03\x01\x00' + sub_wire = bytes([0x10, len(inner_wire)]) + inner_wire + # With ignore_critical via metadata, this must not raise + p = tlv_parse(Outer, sub_wire) + assert p.sub.x is None + + +# --------------------------------------------------------------------------- +# TestRepeatedField +# --------------------------------------------------------------------------- + +class TestRepeatedField: + """RepeatedField: multiple TLVs of the same type, no outer wrapper.""" + + def test_uint_elements(self): + wire = tlv_encode(_RepeatedUint(words=[0, 1, 2])) + # Each word: T=0x01 L=0x02 V=2-byte big-endian + assert wire == b'\x01\x02\x00\x00\x01\x02\x00\x01\x01\x02\x00\x02' + p = tlv_parse(_RepeatedUint, wire) + assert p.words == [0, 1, 2] + + def test_bytes_elements(self): + @dataclass + class M: + tags: list[bytes] = field(default_factory=list, + metadata={'tlv_type': 0x17}) + + obj = M(tags=[b'a', b'bb', b'ccc']) + wire = tlv_encode(obj) + assert wire == b'\x17\x01a\x17\x02bb\x17\x03ccc' + p = tlv_parse(M, wire) + assert [bytes(t) for t in p.tags] == [b'a', b'bb', b'ccc'] + + def test_str_elements(self): + @dataclass + class M: + labels: list[str] = field(default_factory=list, + metadata={'tlv_type': 0x16}) + + obj = M(labels=['hello', 'world']) + wire = tlv_encode(obj) + p = tlv_parse(M, wire) + assert p.labels == ['hello', 'world'] + + def test_model_elements(self): + wire = tlv_encode(_RepeatedModel(items=[_Inner(val=10), _Inner(val=20)])) + p = tlv_parse(_RepeatedModel, wire) + assert [i.val for i in p.items] == [10, 20] + + def test_empty_list_produces_no_bytes(self): + wire = tlv_encode(_RepeatedUint(words=[])) + assert wire == b'' + + def test_default_factory_list_initialised_on_parse(self): + """A repeated field with no default_factory should still get a list on parse.""" + @dataclass + class M: + items: list[int] = field(metadata={'tlv_type': 0x05}) + + wire = b'\x05\x01\x01\x05\x01\x02' + p = tlv_parse(M, wire) + assert p.items == [1, 2] + + +# --------------------------------------------------------------------------- +# TestOrdering +# --------------------------------------------------------------------------- + +class TestOrdering: + """NDN TLV ordering rules: forward-only matching, critical type handling.""" + + def test_unknown_even_type_skipped(self): + """Unknown even TLV types are silently ignored (non-critical).""" + @dataclass + class M: + x: int = field(default=None, metadata={'tlv_type': 0x04}) + + # Wire: unknown even type 0x02, then known type 0x04 + wire = b'\x02\x01\xff\x04\x01\x07' + p = tlv_parse(M, wire) + assert p.x == 7 + + def test_unknown_odd_type_raises(self): + """Unknown odd TLV types are critical — must raise DecodeError.""" + @dataclass + class M: + x: int = field(default=None, metadata={'tlv_type': 0x04}) + + wire = b'\x03\x01\x00\x04\x01\x07' + with pytest.raises(DecodeError): + tlv_parse(M, wire) + + def test_ignore_critical_suppresses_error(self): + @dataclass + class M: + x: int = field(default=None, metadata={'tlv_type': 0x04}) + + wire = b'\x03\x01\x00\x04\x01\x07' + p = tlv_parse(M, wire, ignore_critical=True) + assert p.x == 7 + + def test_out_of_order_critical_field_raises(self): + """ + A critical (odd type) field appearing out-of-order is unrecognised and + must raise DecodeError. Even types are non-critical and silently dropped. + """ + @dataclass + class M: + a: int = field(default=None, metadata={'tlv_type': 0x03}) # odd = critical + b: int = field(default=None, metadata={'tlv_type': 0x05}) # odd = critical + + # b (0x05) comes first; after matching it, field_pos advances past a (0x03). + # The parser then sees 0x03 as unknown critical → DecodeError. + wire = b'\x05\x01\x02\x03\x01\x01' + with pytest.raises(DecodeError): + tlv_parse(M, wire) + + def test_out_of_order_even_field_silently_dropped(self): + """Even (non-critical) fields seen out-of-order are silently skipped.""" + @dataclass + class M: + a: int = field(default=None, metadata={'tlv_type': 0x02}) # even + b: int = field(default=None, metadata={'tlv_type': 0x04}) # even + + # b before a — a is dropped silently (non-critical) + wire = b'\x04\x01\x02\x02\x01\x01' + p = tlv_parse(M, wire) + assert p.b == 2 + assert p.a is None + + def test_out_of_order_non_critical_silently_skipped(self): + @dataclass + class M: + a: int = field(default=None, metadata={'tlv_type': 0x04}) # even, non-critical when out of order + b: int = field(default=None, metadata={'tlv_type': 0x06}) + + # b before a — a is even so silently skipped + wire = b'\x06\x01\x02\x04\x01\x01' + p = tlv_parse(M, wire) + assert p.b == 2 + assert p.a is None + + +# --------------------------------------------------------------------------- +# TestInheritance +# --------------------------------------------------------------------------- + +class TestInheritance: + """Dataclass inheritance: parent fields come first (no IncludeBase needed).""" + + def test_parent_fields_encoded_first(self): + @dataclass + class Base: + x: int = field(default=None, metadata={'tlv_type': 0x01}) + + @dataclass + class Child(Base): + y: int = field(default=None, metadata={'tlv_type': 0x03}) + + wire = tlv_encode(Child(x=1, y=2)) + # x (0x01) must appear before y (0x03) + assert wire == b'\x01\x01\x01\x03\x01\x02' + p = tlv_parse(Child, wire) + assert p.x == 1 and p.y == 2 + + def test_child_only_fields(self): + @dataclass + class Base: + x: int = field(default=None, metadata={'tlv_type': 0x01}) + + @dataclass + class Child(Base): + y: int = field(default=None, metadata={'tlv_type': 0x03}) + + wire = tlv_encode(Child(x=None, y=5)) + assert wire == b'\x03\x01\x05' + p = tlv_parse(Child, wire) + assert p.x is None + assert p.y == 5 + + def test_non_tlv_fields_ignored(self): + """Fields without 'tlv_type' in metadata are silently skipped.""" + @dataclass + class M: + internal: str = field(default='ignored') # no metadata + x: int = field(default=None, metadata={'tlv_type': 0x01}) + + wire = tlv_encode(M(internal='should_not_appear', x=42)) + assert wire == b'\x01\x01\x2a' + p = tlv_parse(M, wire) + assert p.x == 42 + + +# --------------------------------------------------------------------------- +# TestZeroCopyAndInPlace +# --------------------------------------------------------------------------- + +class TestZeroCopyAndInPlace: + """Zero-copy memoryview slices and in-place buffer encoding.""" + + def test_bytes_parse_shares_buffer(self): + """Parsed bytes field is a memoryview slice — no copy.""" + @dataclass + class M: + data: bytes = field(default=None, metadata={'tlv_type': 0x15}) + + wire = bytearray(b'\x15\x04\xde\xad\xbe\xef') + p = tlv_parse(M, wire) + assert isinstance(p.data, memoryview) + assert bytes(p.data) == b'\xde\xad\xbe\xef' + + def test_inplace_encode_returns_memoryview(self): + @dataclass + class M: + x: int = field(default=None, metadata={'tlv_type': 0x03}) + + buf = bytearray(20) + mv = tlv_encode(M(x=7), buf, offset=5) + assert isinstance(mv, memoryview) + assert bytes(mv) == b'\x03\x01\x07' + # Bytes written at correct position + assert buf[5:8] == b'\x03\x01\x07' + # Surrounding bytes untouched + assert buf[:5] == b'\x00' * 5 + assert buf[8:] == b'\x00' * 12 + + def test_inplace_matches_standalone(self): + @dataclass + class M: + x: int = field(default=None, metadata={'tlv_type': 0x03}) + y: bytes = field(default=None, metadata={'tlv_type': 0x15}) + + obj = M(x=300, y=b'hello') + standalone = tlv_encode(obj) + buf = bytearray(len(standalone) + 10) + mv = tlv_encode(obj, buf, offset=3) + assert bytes(mv) == bytes(standalone) + + def test_inplace_memoryview_buffer(self): + """In-place encoding also works with a memoryview target.""" + @dataclass + class M: + x: int = field(default=None, metadata={'tlv_type': 0x05}) + + buf = bytearray(10) + mv_buf = memoryview(buf) + result = tlv_encode(M(x=1), mv_buf, offset=2) + assert bytes(result) == b'\x05\x01\x01' + + +# --------------------------------------------------------------------------- +# TestDefaultHandling +# --------------------------------------------------------------------------- + +class TestDefaultHandling: + """Default values and field initialisation during parse.""" + + def test_field_with_explicit_default_preserved_if_absent(self): + @dataclass + class M: + x: int = field(default=42, metadata={'tlv_type': 0x01}) + + # Wire that does not contain field x + wire = b'' + p = tlv_parse(M, wire) + assert p.x == 42 + + def test_field_without_default_is_none_if_absent(self): + @dataclass + class M: + x: int = field(metadata={'tlv_type': 0x01}) + + wire = b'' + p = tlv_parse(M, wire) + assert p.x is None + + def test_default_factory_list_preserved_if_absent(self): + wire = tlv_encode(_RepeatedUint(words=[])) + p = tlv_parse(_RepeatedUint, wire) + assert p.words == [] + + +# --------------------------------------------------------------------------- +# TestBinaryCompatibility +# --------------------------------------------------------------------------- + +class TestBinaryCompatibility: + """Byte-for-byte compatibility with the v1 TlvModel metaclass API.""" + + def test_uint_compat(self): + class V1(TlvModel): + sig_type = UintField(0x1b, fixed_len=1) + nonce = UintField(0x26) + + @dataclass + class V2: + sig_type: int = field(default=None, + metadata={'tlv_type': 0x1b, 'fixed_len': 1}) + nonce: int = field(default=None, metadata={'tlv_type': 0x26}) + + v1 = V1(); v1.sig_type = 3; v1.nonce = 42 + assert bytes(v1.encode()) == bytes(tlv_encode(V2(sig_type=3, nonce=42))) + + def test_bool_compat(self): + class V1(TlvModel): + flag = BoolField(0x12) + count = UintField(0x0a) + + @dataclass + class V2: + flag: bool = field(default=None, metadata={'tlv_type': 0x12}) + count: int = field(default=None, metadata={'tlv_type': 0x0a}) + + for flag_val in (True, False, None): + v1 = V1(); v1.flag = flag_val; v1.count = 5 + v2 = V2(flag=flag_val, count=5) + assert bytes(v1.encode()) == bytes(tlv_encode(v2)) + + def test_bytes_compat(self): + class V1(TlvModel): + raw = BytesField(0x15) + label = BytesField(0x16, is_string=True) + + @dataclass + class V2: + raw: bytes = field(default=None, metadata={'tlv_type': 0x15}) + label: str = field(default=None, metadata={'tlv_type': 0x16}) + + v1 = V1(); v1.raw = b'\x01\x02\x03'; v1.label = 'hi' + v2 = V2(raw=b'\x01\x02\x03', label='hi') + assert bytes(v1.encode()) == bytes(tlv_encode(v2)) + + def test_name_compat(self): + class V1(TlvModel): + name = NameField() + + @dataclass + class V2: + name: NDNName = field(default=None, metadata={'tlv_type': 0x07}) + + v1 = V1(); v1.name = '/foo/bar' + v2 = V2(name='/foo/bar') + assert bytes(v1.encode()) == bytes(tlv_encode(v2)) + + def test_model_compat(self): + class V1Inner(TlvModel): + val = UintField(0x01) + + class V1Outer(TlvModel): + inner = ModelField(0x10, V1Inner) + + @dataclass + class V2Inner: + val: int = field(default=None, metadata={'tlv_type': 0x01}) + + @dataclass + class V2Outer: + inner: V2Inner = field(default=None, metadata={'tlv_type': 0x10}) + + v1 = V1Outer(); v1.inner = V1Inner(); v1.inner.val = 99 + v2 = V2Outer(inner=V2Inner(val=99)) + assert bytes(v1.encode()) == bytes(tlv_encode(v2)) + + def test_repeated_uint_compat(self): + class V1(TlvModel): + words = RepeatedField(UintField(0x01, fixed_len=2)) + + v1 = V1(); v1.words = [0, 1, 2] + v2 = _RepeatedUint(words=[0, 1, 2]) + assert bytes(v1.encode()) == bytes(tlv_encode(v2)) + + def test_repeated_model_compat(self): + class V1Inner(TlvModel): + val = UintField(0x01) + + class V1Rep(TlvModel): + items = RepeatedField(ModelField(0x10, V1Inner)) + + v1 = V1Rep() + r1 = V1Inner(); r1.val = 10 + r2 = V1Inner(); r2.val = 20 + v1.items = [r1, r2] + + v2 = _RepeatedModel(items=[_Inner(val=10), _Inner(val=20)]) + assert bytes(v1.encode()) == bytes(tlv_encode(v2)) + + def test_parse_interop(self): + """Wire produced by v1 can be parsed by v2 and vice-versa.""" + class V1(TlvModel): + name = NameField() + count = UintField(0x0a) + + @dataclass + class V2: + name: NDNName = field(default=None, metadata={'tlv_type': 0x07}) + count: int = field(default=None, metadata={'tlv_type': 0x0a}) + + v1 = V1(); v1.name = '/test'; v1.count = 7 + wire_from_v1 = bytes(v1.encode()) + + p = tlv_parse(V2, wire_from_v1) + assert Name.to_str(p.name) == '/test' + assert p.count == 7 + + v2 = V2(name='/test', count=7) + wire_from_v2 = bytes(tlv_encode(v2)) + + p2 = V1.parse(wire_from_v2) + assert Name.to_str(p2.name) == '/test' + assert p2.count == 7 + + +# --------------------------------------------------------------------------- +# MapField tests +# --------------------------------------------------------------------------- + +@dataclass +class _StrBytesMap: + entries: dict[str, bytes] = field(default_factory=dict, metadata={ + 'tlv_type': 0x21, + 'val_tlv_type': 0x23, + }) + + +@dataclass +class _Inner2: + value: int = field(default=None, metadata={'tlv_type': 0x01}) + + +@dataclass +class _StrModelMap: + entries: dict[str, _Inner2] = field(default_factory=dict, metadata={ + 'tlv_type': 0x21, + 'val_tlv_type': 0x22, + }) + + +class TestMapField: + def test_str_bytes_roundtrip(self): + obj = _StrBytesMap(entries={'alpha': b'\x01\x02', 'beta': b'\x03'}) + wire = tlv_encode(obj) + p = tlv_parse(_StrBytesMap, wire) + assert list(p.entries.keys()) == ['alpha', 'beta'] + assert bytes(p.entries['alpha']) == b'\x01\x02' + assert bytes(p.entries['beta']) == b'\x03' + + def test_insertion_order_preserved(self): + """Dict round-trip must preserve the original key insertion order.""" + obj = _StrBytesMap(entries={'z': b'\x00', 'a': b'\x01', 'm': b'\x02'}) + p = tlv_parse(_StrBytesMap, tlv_encode(obj)) + assert list(p.entries.keys()) == ['z', 'a', 'm'] + + def test_empty_map_produces_no_bytes(self): + obj = _StrBytesMap(entries={}) + assert tlv_encode(obj) == b'' + + def test_none_map_produces_no_bytes(self): + obj = _StrBytesMap(entries=None) + assert tlv_encode(obj) == b'' + + def test_none_map_defaults_to_empty_on_parse(self): + """Parsing wire with no map TLVs leaves entries as the default_factory value.""" + p = tlv_parse(_StrBytesMap, b'') + assert p.entries == {} + + def test_str_model_map_roundtrip(self): + obj = _StrModelMap(entries={'x': _Inner2(value=7), 'y': _Inner2(value=99)}) + wire = tlv_encode(obj) + p = tlv_parse(_StrModelMap, wire) + assert list(p.entries.keys()) == ['x', 'y'] + assert p.entries['x'].value == 7 + assert p.entries['y'].value == 99 + + def test_v1_compat_wire(self): + """v2 map encoding must be byte-for-byte identical to v1 MapField.""" + from ndn.encoding import MapField, BytesField + + class V1Map(TlvModel): + entries = MapField(BytesField(0x21, is_string=True), BytesField(0x23)) + + v1 = V1Map() + v1.entries['alpha'] = b'\x01\x02' + v1.entries['beta'] = b'\x03' + v1_wire = bytes(v1.encode()) + + v2 = _StrBytesMap(entries={'alpha': b'\x01\x02', 'beta': b'\x03'}) + v2_wire = bytes(tlv_encode(v2)) + + assert v1_wire == v2_wire + + def test_v1_produced_wire_parsed_by_v2(self): + from ndn.encoding import MapField, BytesField + + class V1Map(TlvModel): + entries = MapField(BytesField(0x21, is_string=True), BytesField(0x23)) + + v1 = V1Map() + v1.entries['hello'] = b'\xde\xad' + wire = bytes(v1.encode()) + + p = tlv_parse(_StrBytesMap, wire) + assert bytes(p.entries['hello']) == b'\xde\xad' + + def test_bytes_values_are_memoryview_zero_copy(self): + obj = _StrBytesMap(entries={'k': b'\xca\xfe'}) + wire = tlv_encode(obj) + p = tlv_parse(_StrBytesMap, wire) + assert isinstance(p.entries['k'], memoryview) + + +# --------------------------------------------------------------------------- +# Signature machinery tests +# --------------------------------------------------------------------------- + +# ── Simple fixed-length mock signer (HMAC-like) ────────────────────────────── + +class _HmacSigner(Signer): + """Deterministic 32-byte 'HMAC' signer using SHA-256(key || content).""" + SIG_SIZE = 32 + + def __init__(self, key: bytes = b'secret'): + self._key = key + + def write_signature_info(self, sig_info): + sig_info.signature_type = 4 # HMAC_WITH_SHA256 + + def get_signature_value_size(self) -> int: + return self.SIG_SIZE + + def write_signature_value(self, wire, contents) -> int: + h = sha256(self._key) + for blk in contents: + h.update(bytes(blk)) + sig = h.digest() + wire[:] = sig + return len(sig) + + def verify(self, sig: bytes, contents) -> bool: + buf = bytearray(self.SIG_SIZE) + mv = memoryview(buf) + self.write_signature_value(mv, contents) + return bytes(buf) == bytes(sig) + + +# ── Variable-length mock signer (ECDSA-like, sometimes shorter) ─────────────── + +class _EcdsaSigner(Signer): + """Always signs with 71 bytes, but reports max 72 (tests shrink path).""" + MAX_SIZE = 72 + REAL_SIZE = 71 + + def write_signature_info(self, sig_info): + sig_info.signature_type = 3 # SHA256_WITH_ECDSA + + def get_signature_value_size(self): + return self.MAX_SIZE + + def write_signature_value(self, wire, contents): + for i in range(self.REAL_SIZE): + wire[i] = i & 0xFF + return self.REAL_SIZE + + +# ── Data-like model (no digest, no interest-name) ──────────────────────────── + +@dataclass +class _SigInfo: + signature_type: int = field(default=None, metadata={'tlv_type': 0x1b, 'fixed_len': 1}) + + +@dataclass +class _DataValue: + _sig_cover_start: None = field(default=None, metadata={'field_type': 'offset_marker'}) + name: NDNName = field(default=None, metadata={'tlv_type': 0x07}) + content: bytes | None = field(default=None, metadata={'tlv_type': 0x15}) + signature_info: _SigInfo | None = field(default=None, metadata={'tlv_type': 0x16}) + signature_value: bytes | None = field(default=None, metadata={ + 'tlv_type': 0x17, + 'field_type': 'sig_value', + 'cover_start': '_sig_cover_start', + }) + + +# ── Interest-like model (interest_name + digest) ────────────────────────────── + +@dataclass +class _InterestValue: + name: NDNName = field(default=None, metadata={ + 'tlv_type': 0x07, 'field_type': 'interest_name'}) + nonce: int | None = field(default=None, metadata={ + 'tlv_type': 0x0a, 'fixed_len': 4}) + _sig_cover_start: None = field(default=None, metadata={'field_type': 'offset_marker'}) + application_parameters: bytes | None = field(default=None, metadata={'tlv_type': 0x24}) + signature_info: _SigInfo | None = field(default=None, metadata={'tlv_type': 0x2c}) + signature_value: bytes | None = field(default=None, metadata={ + 'tlv_type': 0x2e, + 'field_type': 'sig_value', + 'cover_start': '_sig_cover_start', + 'digest_cover_start': '_sig_cover_start', + 'digest_cover_end': '_digest_cover_end', + }) + _digest_cover_end: None = field(default=None, metadata={'field_type': 'offset_marker'}) + + +class TestSignatureMachinery: + # ── offset_marker ────────────────────────────────────────────────────────── + + def test_offset_marker_produces_no_bytes(self): + @dataclass + class M: + _mark: None = field(default=None, metadata={'field_type': 'offset_marker'}) + v: int = field(default=None, metadata={'tlv_type': 0x01}) + + wire = tlv_encode(M(v=7)) + assert wire == bytes(tlv_encode(_Inner(val=7))) # only the uint TLV, no extra bytes + + def test_offset_marker_records_position_during_encode(self): + @dataclass + class M: + a: int = field(default=None, metadata={'tlv_type': 0x01}) + _mark: None = field(default=None, metadata={'field_type': 'offset_marker'}) + b: int = field(default=None, metadata={'tlv_type': 0x03}) + + markers = {} + tlv_encode(M(a=1, b=2), markers=markers) + # 'a' occupies 3 bytes (T=1, L=1, V=1), so _mark records offset 3. + assert markers['_mark'] == 3 + + def test_offset_marker_records_position_during_parse(self): + @dataclass + class M: + a: int = field(default=None, metadata={'tlv_type': 0x01}) + _mark: None = field(default=None, metadata={'field_type': 'offset_marker'}) + b: int = field(default=None, metadata={'tlv_type': 0x03}) + + wire = tlv_encode(M(a=1, b=2)) + markers = {} + tlv_parse(M, wire, markers=markers) + # offset_btl of 'b' = 3 (after 'a'), so _mark records 3. + assert markers.get('_mark') == 3 + + # ── sig_value / Data-like encoding ──────────────────────────────────────── + + def test_data_encode_produces_signature(self): + signer = _HmacSigner() + obj = _DataValue(name='/test', content=b'hello') + obj.signature_info = _SigInfo() + signer.write_signature_info(obj.signature_info) + + markers = {'##signer': signer} + wire = tlv_encode(obj, markers=markers) + + # Signature value TLV must be present. + assert 0x17 in bytes(wire) + # Wire must end with 32 sig bytes (preceded by TL 17 20). + assert wire[-34:-32] == b'\x17\x20' + + def test_data_encode_decode_roundtrip(self): + signer = _HmacSigner() + obj = _DataValue(name='/test/data', content=b'payload') + obj.signature_info = _SigInfo() + signer.write_signature_info(obj.signature_info) + + markers = {'##signer': signer} + wire = tlv_encode(obj, markers=markers) + + parse_markers = {} + p = tlv_parse(_DataValue, wire, markers=parse_markers) + assert Name.to_str(p.name) == '/test/data' + assert bytes(p.content) == b'payload' + assert p.signature_info.signature_type == 4 # HMAC_WITH_SHA256 + + def test_data_signature_verifies(self): + signer = _HmacSigner() + obj = _DataValue(name='/verify/me', content=b'data') + obj.signature_info = _SigInfo() + signer.write_signature_info(obj.signature_info) + + enc_markers = {'##signer': signer} + wire = tlv_encode(obj, markers=enc_markers) + enc_covered = enc_markers['##sig_covered_part'] + + parse_markers = {} + p = tlv_parse(_DataValue, wire, markers=parse_markers) + parse_covered = parse_markers.get('##sig_covered_part', []) + sig_buf = parse_markers['##sig_value_buf'] + + assert signer.verify(bytes(sig_buf), parse_covered) + + def test_data_signature_is_deterministic(self): + """Same object encoded twice with the same signer → identical wires.""" + signer = _HmacSigner() + obj1 = _DataValue(name='/det/test', content=b'hello') + obj1.signature_info = _SigInfo() + signer.write_signature_info(obj1.signature_info) + + obj2 = _DataValue(name='/det/test', content=b'hello') + obj2.signature_info = _SigInfo() + signer.write_signature_info(obj2.signature_info) + + w1 = tlv_encode(obj1, markers={'##signer': signer}) + w2 = tlv_encode(obj2, markers={'##signer': signer}) + assert bytes(w1) == bytes(w2) + + def test_ecdsa_signer_shrinks_wire(self): + signer = _EcdsaSigner() + obj = _DataValue(name='/shrink', content=b'x') + obj.signature_info = _SigInfo() + signer.write_signature_info(obj.signature_info) + + markers = {'##signer': signer} + wire = tlv_encode(obj, markers=markers) + + # Allocated 72 bytes, actual 71 → last byte trimmed. + assert markers['##shrink_len'] == 1 + # The sig_value TLV's L byte should now read 71 (0x47). + sig_tlv_idx = bytes(wire).index(0x17) # find sig_value type byte + assert wire[sig_tlv_idx + 1] == 71 + + def test_unsigned_data_produces_no_sig_tlv(self): + obj = _DataValue(name='/unsigned', content=b'ok') + wire = tlv_encode(obj) + assert b'\x17' not in bytes(wire) + + # ── interest_name + digest ──────────────────────────────────────────────── + + def test_interest_name_without_digest(self): + obj = _InterestValue(name='/plain/interest', nonce=42) + wire = tlv_encode(obj) + p = tlv_parse(_InterestValue, wire) + assert Name.to_str(p.name) == '/plain/interest' + assert p.nonce == 42 + + def test_interest_with_digest_appended(self): + """When ##need_digest is True and no digest component exists, one is appended.""" + app_param = b'\x01\x02\x03' + obj = _InterestValue(name='/digest/test', application_parameters=app_param) + obj.signature_info = _SigInfo() + signer = _HmacSigner() + signer.write_signature_info(obj.signature_info) + + markers = {'##signer': signer, '##need_digest': True} + wire = tlv_encode(obj, markers=markers) + + # Parse back and check digest component exists in name. + p = tlv_parse(_InterestValue, wire) + name_str = Name.to_str(p.name) + assert 'params-sha256=' in name_str + + def test_interest_digest_value_is_sha256(self): + """The ParametersSha256DigestComponent must equal SHA-256 of the digest-covered part.""" + from ndn.encoding.name import Component as C + app_param = b'\xde\xad\xbe\xef' + obj = _InterestValue(name='/verify/digest', application_parameters=app_param) + obj.signature_info = _SigInfo() + signer = _HmacSigner() + signer.write_signature_info(obj.signature_info) + + markers = {'##signer': signer, '##need_digest': True} + wire = tlv_encode(obj, markers=markers) + + # Locate the ParametersSha256DigestComponent in the encoded name. + p = tlv_parse(_InterestValue, wire) + digest_comp = None + for comp in p.name: + if C.get_type(comp) == C.TYPE_PARAMETERS_SHA256: + digest_comp = comp + break + assert digest_comp is not None + + digest_val = bytes(C.get_value(digest_comp)) + # Determine what the digest should cover: find where _sig_cover_start landed. + raw = bytes(wire) + sig_cover_start = markers.get('_sig_cover_start', 0) + d_end_field = '_digest_cover_end' + sig_cover_end = markers.get(d_end_field, len(raw)) + expected = sha256(raw[sig_cover_start:sig_cover_end]).digest() + assert digest_val == expected + + def test_interest_sig_covered_part_set_on_parse(self): + """After parsing an Interest, ##sig_covered_part is populated.""" + obj = _InterestValue(name='/parse/sig', application_parameters=b'\x00') + obj.signature_info = _SigInfo() + signer = _HmacSigner() + signer.write_signature_info(obj.signature_info) + + markers = {'##signer': signer, '##need_digest': True} + wire = tlv_encode(obj, markers=markers) + + parse_markers = {} + tlv_parse(_InterestValue, wire, markers=parse_markers) + assert '##sig_covered_part' in parse_markers + assert len(parse_markers['##sig_covered_part']) > 0 + + # ── tlv_get_arg / tlv_set_arg ───────────────────────────────────────────── + + def test_tlv_get_arg_missing_returns_default(self): + m = {} + assert tlv_get_arg(m, 'x', 42) == 42 + + def test_tlv_set_arg_stores_value(self): + m = {} + tlv_set_arg(m, 'key', 'value') + assert tlv_get_arg(m, 'key') == 'value' + + +# --------------------------------------------------------------------------- +# Schema cache +# --------------------------------------------------------------------------- + +@dataclass +class _ForwardOuter: + inner: '_ForwardInner' = field(default=None, metadata={'tlv_type': 0x10}) + + +@dataclass +class _ForwardInner: + val: int = field(default=None, metadata={'tlv_type': 0x01}) + + +class TestSchemaCache: + def test_type_hints_resolved_once_per_class(self, monkeypatch): + import typing + from ndn.encoding import tlv_model + + @dataclass + class Inner: + v: int = field(default=None, metadata={'tlv_type': 0x01}) + + @dataclass + class Outer: + items: list[Inner] = field(default_factory=list, metadata={'tlv_type': 0x10}) + m: dict[str, bytes] = field(default_factory=dict, metadata={'tlv_type': 0x21, 'val_tlv_type': 0x23}) + + calls = [] + real = typing.get_type_hints + monkeypatch.setattr(tlv_model.typing, 'get_type_hints', + lambda cls, *a, **k: calls.append(cls) or real(cls, *a, **k)) + obj = Outer(items=[Inner(v=1), Inner(v=2)], m={'k': b'v'}) + for _ in range(3): + assert tlv_parse(Outer, tlv_encode(obj)).items[1].v == 2 + assert sorted(c.__name__ for c in calls) == ['Inner', 'Outer'] + + def test_forward_reference_resolved_lazily(self): + wire = tlv_encode(_ForwardOuter(inner=_ForwardInner(val=5))) + assert wire == b'\x10\x03\x01\x01\x05' + assert tlv_parse(_ForwardOuter, wire).inner.val == 5 + + def test_local_class_not_kept_alive(self): + import gc + import weakref + + def make(): + @dataclass + class Local: + x: int = field(default=None, metadata={'tlv_type': 0x01}) + tlv_parse(Local, tlv_encode(Local(x=1))) + return weakref.ref(Local) + + ref = make() + gc.collect() + assert ref() is None + + def test_non_tlv_field_with_unsupported_annotation_ignored(self): + class Opaque: + pass + + @dataclass + class M: + cache: Opaque = None + x: int = field(default=None, metadata={'tlv_type': 0x01}) + + wire = tlv_encode(M(cache=Opaque(), x=3)) + assert wire == b'\x01\x01\x03' + assert tlv_parse(M, wire).x == 3 From a9cfd47054f0420cb4a26a9da28e6cb6072c427c Mon Sep 17 00:00:00 2001 From: Xinyu Ma Date: Wed, 7 Oct 2026 23:27:07 -0700 Subject: [PATCH 2/8] encoding: migrate packet and LP models to dataclasses Co-authored-by: Cursor --- src/ndn/app_support/security_v2.py | 153 +++-- src/ndn/encoding/ndn_format_0_3.py | 613 ++++++++------------ src/ndn/encoding/ndn_format_0_3_2017.py | 633 ++++++++------------- src/ndn/encoding/ndnlp_v2.py | 160 +++--- tests/encoding/ndn_format_0_3_2017_test.py | 24 + tests/encoding/ndn_format_0_3_test.py | 71 ++- tests/encoding/ndnlp_v2_test.py | 60 +- tests/misc/security_v2_test.py | 83 +++ 8 files changed, 874 insertions(+), 923 deletions(-) create mode 100644 tests/encoding/ndn_format_0_3_2017_test.py create mode 100644 tests/misc/security_v2_test.py diff --git a/src/ndn/app_support/security_v2.py b/src/ndn/app_support/security_v2.py index b382b33..ccd5e6d 100644 --- a/src/ndn/app_support/security_v2.py +++ b/src/ndn/app_support/security_v2.py @@ -15,12 +15,27 @@ # See the License for the specific language governing permissions and # limitations under the License. # ----------------------------------------------------------------------------- +import dataclasses as dc from datetime import datetime, timedelta, UTC +from typing import Optional + from ..utils import timestamp -from ..encoding import Component, Name, ModelField, TlvModel, ContentType, BytesField, \ - SignatureInfo, TypeNumber, RepeatedField, IncludeBase, MetaInfo, VarBinaryStr, \ - get_tl_num_size, write_tl_num, parse_and_check_tl, FormalName -from ..encoding.ndn_format_0_3 import DataPacketValue +from ..encoding import ( + Component, + FormalName, + Name, + VarBinaryStr, + parse_and_check_tl, +) +from ..encoding.tlv_model import tlv_encode, tlv_parse +from ..encoding.ndn_format_0_3 import ( + ContentType, + DataPacketValue, + MetaInfo, + SignatureInfo, + TypeNumber, + write_signature_info, +) KEY_COMPONENT = Component.from_str('KEY') @@ -41,73 +56,106 @@ class SecurityV2TypeNumber: ENCRYPTED_KEY_BAG = 0x81 -class DescriptionEntry(TlvModel): - description_key = BytesField(SecurityV2TypeNumber.DESCRIPTION_KEY) - description_value = BytesField(SecurityV2TypeNumber.DESCRIPTION_VALUE) +@dc.dataclass +class DescriptionEntry: + description_key: Optional[bytes] = dc.field( + default=None, metadata={ + 'tlv_type': SecurityV2TypeNumber.DESCRIPTION_KEY}) + description_value: Optional[bytes] = dc.field( + default=None, metadata={ + 'tlv_type': SecurityV2TypeNumber.DESCRIPTION_VALUE}) -class AdditionalDescription(TlvModel): - description_entry = RepeatedField(ModelField(SecurityV2TypeNumber.DESCRIPTION_ENTRY, DescriptionEntry)) +@dc.dataclass +class AdditionalDescription: + description_entry: list[DescriptionEntry] = dc.field( + default_factory=list, metadata={ + 'tlv_type': SecurityV2TypeNumber.DESCRIPTION_ENTRY}) -class CertificateV2Extension(TlvModel): - additional_description = ModelField(SecurityV2TypeNumber.ADDITIONAL_DESCRIPTION, AdditionalDescription) +@dc.dataclass +class CertificateV2Extension: + additional_description: Optional[AdditionalDescription] = dc.field( + default=None, metadata={ + 'tlv_type': SecurityV2TypeNumber.ADDITIONAL_DESCRIPTION}) -class ValidityPeriod(TlvModel): - not_before = BytesField(SecurityV2TypeNumber.NOT_BEFORE) - not_after = BytesField(SecurityV2TypeNumber.NOT_AFTER) +@dc.dataclass +class ValidityPeriod: + not_before: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': SecurityV2TypeNumber.NOT_BEFORE}) + not_after: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': SecurityV2TypeNumber.NOT_AFTER}) -class CertificateV2SignatureInfo(SignatureInfo, CertificateV2Extension): - signature_info = IncludeBase(SignatureInfo) - validity_period = ModelField(SecurityV2TypeNumber.VALIDITY_PERIOD, ValidityPeriod) - certificate_v2_extension = IncludeBase(CertificateV2Extension) +@dc.dataclass +class CertificateV2SignatureInfo(SignatureInfo): + validity_period: Optional[ValidityPeriod] = dc.field( + default=None, metadata={ + 'tlv_type': SecurityV2TypeNumber.VALIDITY_PERIOD}) + additional_description: Optional[AdditionalDescription] = dc.field( + default=None, metadata={ + 'tlv_type': SecurityV2TypeNumber.ADDITIONAL_DESCRIPTION}) +@dc.dataclass class CertificateV2Value(DataPacketValue): - _base = IncludeBase(DataPacketValue) - signature_info = ModelField(TypeNumber.SIGNATURE_INFO, CertificateV2SignatureInfo, ignore_critical=True) + signature_info: Optional[CertificateV2SignatureInfo] = dc.field( + default=None, metadata={ + 'tlv_type': TypeNumber.SIGNATURE_INFO, + 'ignore_critical': True, + }) -class SafeBag(TlvModel): - certificate_v2 = BytesField(TypeNumber.DATA) +@dc.dataclass +class SafeBag: + certificate_v2: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.DATA}) # We do not use ModelField due to 2 reasons: # 1. The encoded length of CertificateV2 is unknown. - # 2. Generally we already have an encoded certificate when exporting a SafeBag. - encrypted_key_bag = BytesField(SecurityV2TypeNumber.ENCRYPTED_KEY_BAG) + # 2. Generally we already have an encoded certificate when exporting a + # SafeBag. + encrypted_key_bag: Optional[bytes] = dc.field( + default=None, metadata={ + 'tlv_type': SecurityV2TypeNumber.ENCRYPTED_KEY_BAG}) + + +@dc.dataclass +class _CertificateEnvelope: + value: bytes = dc.field(metadata={'tlv_type': TypeNumber.DATA}) def parse_certificate(wire) -> CertificateV2Value: wire = parse_and_check_tl(wire, TypeNumber.DATA) - return CertificateV2Value.parse(wire) - - -def new_cert(key_name, issuer_id_component, pub_key, signer, start_time, end_time) -> tuple[FormalName, VarBinaryStr]: - cert_val = CertificateV2Value() - cert_name = Name.normalize(key_name) + [issuer_id_component, Component.from_version(timestamp())] - cert_val.name = cert_name - cert_val.content = pub_key - cert_val.meta_info = MetaInfo(content_type=ContentType.KEY, freshness_period=3600000) - cert_val.signature_info = CertificateV2SignatureInfo() - cert_val.signature_info.validity_period = ValidityPeriod() - cur_time = start_time - not_before = cur_time.strftime('%Y%m%dT%H%M%S').encode() - cert_val.signature_info.validity_period.not_before = not_before - not_after = end_time.strftime('%Y%m%dT%H%M%S').encode() - cert_val.signature_info.validity_period.not_after = not_after + return tlv_parse(CertificateV2Value, wire) + - markers = {} - cert_val._signer.set_arg(markers, signer) - value = cert_val.encode(markers=markers) - shrink_size = cert_val._shrink_len.get_arg(markers) - type_len = get_tl_num_size(TypeNumber.DATA) - size_len = get_tl_num_size(len(value) - shrink_size) - buf = bytearray(type_len + size_len + len(value) - shrink_size) - write_tl_num(TypeNumber.DATA, buf) - write_tl_num(len(value) - shrink_size, buf, type_len) - buf[type_len + size_len:] = memoryview(value)[0:len(value) - shrink_size] - return cert_name, buf +def new_cert(key_name, issuer_id_component, pub_key, signer, + start_time, end_time) -> tuple[FormalName, VarBinaryStr]: + cert_name = Name.normalize(key_name) + [ + issuer_id_component, + Component.from_version(timestamp()), + ] + not_before = start_time.strftime('%Y%m%dT%H%M%S').encode() + not_after = end_time.strftime('%Y%m%dT%H%M%S').encode() + signature_info = CertificateV2SignatureInfo( + validity_period=ValidityPeriod( + not_before=not_before, + not_after=not_after, + ), + ) + write_signature_info(signer, signature_info) + cert_val = CertificateV2Value( + name=cert_name, + content=pub_key, + meta_info=MetaInfo( + content_type=ContentType.KEY, + freshness_period=3600000, + ), + signature_info=signature_info, + ) + value = tlv_encode(cert_val, markers={'##signer': signer}) + return cert_name, tlv_encode(_CertificateEnvelope(value=value)) def self_sign(key_name, pub_key, signer) -> tuple[FormalName, VarBinaryStr]: @@ -124,7 +172,8 @@ def sign_req(key_name, pub_key, signer) -> tuple[FormalName, VarBinaryStr]: datetime.now(UTC), end_time) -def derive_cert(key_name, issuer_id, pub_key, signer, start_time, expire_sec) -> tuple[FormalName, VarBinaryStr]: +def derive_cert(key_name, issuer_id, pub_key, signer, + start_time, expire_sec) -> tuple[FormalName, VarBinaryStr]: end_time = start_time + timedelta(seconds=expire_sec) if isinstance(issuer_id, str): issuer_id = Component.from_str(issuer_id) diff --git a/src/ndn/encoding/ndn_format_0_3.py b/src/ndn/encoding/ndn_format_0_3.py index a0d773d..0b5f513 100644 --- a/src/ndn/encoding/ndn_format_0_3.py +++ b/src/ndn/encoding/ndn_format_0_3.py @@ -1,51 +1,32 @@ # ----------------------------------------------------------------------------- # Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# # Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. # ----------------------------------------------------------------------------- +"""NDN Packet Format v0.3 models using the dataclass TLV API.""" import dataclasses as dc -from hashlib import sha256 +from typing import Optional + from .name import Name, Component from .signer import Signer -from .tlv_type import VarBinaryStr, BinaryStr, NonStrictName, FormalName -from .tlv_var import parse_and_check_tl, shrink_length -from .tlv_model import TlvModel, InterestNameField, BoolField, UintField, \ - SignatureValueField, OffsetMarker, BytesField, ModelField, NameField, \ - ProcedureArgument, RepeatedField - +from .tlv_model import NDNName, tlv_encode, tlv_parse +from .tlv_type import BinaryStr, VarBinaryStr, NonStrictName, FormalName +from .tlv_var import get_tl_num_size, parse_and_check_tl, write_tl_num -__all__ = ['TypeNumber', 'ContentType', 'SignatureType', 'KeyLocator', 'SignatureInfo', - 'Links', 'MetaInfo', 'InterestParam', 'SignaturePtrs', 'make_interest', 'make_data', - 'parse_interest', 'parse_data', 'Interest', 'Data'] +__all__ = [ + 'TypeNumber', 'ContentType', 'SignatureType', 'KeyLocator', + 'SignatureInfo', 'write_signature_info', + 'Links', 'MetaInfo', 'InterestParam', 'SignaturePtrs', 'make_interest', + 'make_data', 'parse_interest', 'parse_data', 'Interest', 'Data', +] class TypeNumber: - r""" - TLV Type numbers used in `NDN Packet Format 0.3 - `_. - - Constant names are changed to PEP 8 style, i.e., all upper cases with underscores separating words. - """ INTEREST = 0x05 DATA = 0x06 - NAME = Name.TYPE_NAME GENERIC_NAME_COMPONENT = Component.TYPE_GENERIC IMPLICIT_SHA256_DIGEST_COMPONENT = Component.TYPE_IMPLICIT_SHA256 PARAMETERS_SHA256_DIGEST_COMPONENT = Component.TYPE_PARAMETERS_SHA256 - CAN_BE_PREFIX = 0x21 MUST_BE_FRESH = 0x12 FORWARDING_HINT = 0x1e @@ -55,7 +36,6 @@ class TypeNumber: APPLICATION_PARAMETERS = 0x24 INTEREST_SIGNATURE_INFO = 0x2c INTEREST_SIGNATURE_VALUE = 0x2e - META_INFO = 0x14 CONTENT = 0x15 SIGNATURE_INFO = 0x16 @@ -63,31 +43,17 @@ class TypeNumber: CONTENT_TYPE = 0x18 FRESHNESS_PERIOD = 0x19 FINAL_BLOCK_ID = 0x1a - SIGNATURE_TYPE = 0x1b KEY_LOCATOR = 0x1c KEY_DIGEST = 0x1d SIGNATURE_NONCE = 0x26 SIGNATURE_TIME = 0x28 SIGNATURE_SEQ_NUM = 0x2a - DELEGATION = 0x1f PREFERENCE = 0x1e class ContentType: - r""" - Numbers used in ContentType. - - ==== =================================== - Type Description - ==== =================================== - BLOB Payload identified by the data name - LINK A list of delegation names - KEY Public Key - NACK Application-level NACK - ==== =================================== - """ BLOB = 0 LINK = 1 KEY = 2 @@ -95,20 +61,6 @@ class ContentType: class SignatureType: - r""" - Numbers used in SignatureType. - - ================= ================================================== - Type Description - ================= ================================================== - NOT_SIGNED Not signed - DIGEST_SHA256 SHA-256 digest (only for integrity protection) - SHA256_WITH_RSA RSA signature over a SHA-256 digest - SHA256_WITH_ECDSA An ECDSA signature over a SHA-256 digest - HMAC_WITH_SHA256 SHA256 hash-based message authentication codes - NULL An empty signature for testing and experimentation - ================= ================================================== - """ NOT_SIGNED = None DIGEST_SHA256 = 0 SHA256_WITH_RSA = 1 @@ -118,393 +70,274 @@ class SignatureType: NULL = 200 -class KeyLocator(TlvModel): - name = NameField() - key_digest = BytesField(TypeNumber.KEY_DIGEST) - - -class SignatureInfo(TlvModel): - signature_type = UintField(TypeNumber.SIGNATURE_TYPE, fixed_len=1) - key_locator = ModelField(TypeNumber.KEY_LOCATOR, KeyLocator) - signature_nonce = UintField(TypeNumber.SIGNATURE_NONCE) - signature_time = UintField(TypeNumber.SIGNATURE_TIME) - signature_seq_num = UintField(TypeNumber.SIGNATURE_SEQ_NUM) - - -class Links(TlvModel): - names = RepeatedField(NameField()) - - -class InterestPacketValue(TlvModel): - _signer = ProcedureArgument() - _sig_cover_part = ProcedureArgument() - _sig_value_buf = ProcedureArgument() - _need_digest = ProcedureArgument() - _digest_cover_part = ProcedureArgument() - _digest_buf = ProcedureArgument() - _shrink_len = ProcedureArgument(0) - - name = InterestNameField(need_digest=_need_digest, - signature_covered_part=_sig_cover_part, - digest_buffer=_digest_buf, - default="/") - can_be_prefix = BoolField(TypeNumber.CAN_BE_PREFIX, default=False) - must_be_fresh = BoolField(TypeNumber.MUST_BE_FRESH, default=False) - forwarding_hint = ModelField(TypeNumber.FORWARDING_HINT, Links) - nonce = UintField(TypeNumber.NONCE, fixed_len=4) - lifetime = UintField(TypeNumber.INTEREST_LIFETIME) # We can not write 4000 as a parse default - hop_limit = UintField(TypeNumber.HOP_LIMIT, fixed_len=1) - _sig_cover_start = OffsetMarker() - _digest_cover_start = OffsetMarker() - application_parameters = BytesField(TypeNumber.APPLICATION_PARAMETERS) - signature_info = ModelField(TypeNumber.INTEREST_SIGNATURE_INFO, SignatureInfo) - signature_value = SignatureValueField(TypeNumber.INTEREST_SIGNATURE_VALUE, - signer=_signer, - covered_part=_sig_cover_part, - starting_point=_sig_cover_start, - value_buffer=_sig_value_buf, - shrink_len=_shrink_len) - _digest_cover_end = OffsetMarker() - - def encoded_length(self, markers: dict | None = None) -> int: - if markers is None: - markers = {} - self._sig_cover_part.set_arg(markers, []) - - signer = self._signer.get_arg(markers) - if signer is not None: - signer.write_signature_info(self.signature_info) - app_param = self.application_parameters - if (signer is not None) and (app_param is None): - app_param = b'' - self.application_parameters = app_param - - self._need_digest.set_arg(markers, app_param is not None) - - return super().encoded_length(markers) - - def encode(self, - wire: VarBinaryStr = None, - offset: int = 0, - markers: dict | None = None) -> VarBinaryStr: - if markers is None: - markers = {} - ret = super().encode(wire, offset, markers) - wire_view = memoryview(ret) - - InterestPacketValue.signature_value.calculate_signature(markers) - if self._need_digest.get_arg(markers): - digest_cover_start = self._digest_cover_start.get_arg(markers) - shrink_size = self._shrink_len.get_arg(markers) - digest_cover_end = self._digest_cover_end.get_arg(markers) - shrink_size - digest_covered_part = [wire_view[digest_cover_start:digest_cover_end]] - self._digest_cover_part.set_arg(markers, digest_covered_part) - sha256_algo = sha256() - digest_buf = self._digest_buf.get_arg(markers) - for blk in digest_covered_part: - sha256_algo.update(blk) - digest_buf[:] = sha256_algo.digest() - - return ret - - @classmethod - def parse(cls, wire: BinaryStr, markers: dict | None = None, ignore_critical: bool = False): - if markers is None: - markers = {} - cls._sig_cover_part.set_arg(markers, []) - ret = super().parse(wire, markers, ignore_critical) - digest_cover_start = cls._digest_cover_start.get_arg(markers) - digest_cover_end = cls._digest_cover_end.get_arg(markers) - digest_cover_part = [memoryview(wire)[digest_cover_start:digest_cover_end]] - cls._digest_cover_part.set_arg(markers, digest_cover_part) - return ret - - -class InterestPacket(TlvModel): - _signer = ProcedureArgument() - interest = ModelField(TypeNumber.INTEREST, InterestPacketValue, [_signer]) - - -class MetaInfo(TlvModel): - content_type = UintField(TypeNumber.CONTENT_TYPE) - freshness_period = UintField(TypeNumber.FRESHNESS_PERIOD) - final_block_id = BytesField(TypeNumber.FINAL_BLOCK_ID) +@dc.dataclass +class KeyLocator: + name: NDNName = dc.field( + default=None, metadata={'tlv_type': TypeNumber.NAME}) + key_digest: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.KEY_DIGEST}) + + +@dc.dataclass +class SignatureInfo: + signature_type: Optional[int] = dc.field( + default=None, metadata={ + 'tlv_type': TypeNumber.SIGNATURE_TYPE, 'fixed_len': 1}) + key_locator: Optional[KeyLocator] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.KEY_LOCATOR}) + signature_nonce: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.SIGNATURE_NONCE}) + signature_time: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.SIGNATURE_TIME}) + signature_seq_num: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.SIGNATURE_SEQ_NUM}) + + +def write_signature_info(signer: Signer, signature_info: SignatureInfo) -> None: + """ + Let *signer* fill *signature_info*. + + Signers still assign the v1 ``KeyLocator`` model, which the dataclass + encoder cannot serialize, so it is converted to :class:`KeyLocator`. + """ + signer.write_signature_info(signature_info) + key_locator = signature_info.key_locator + if key_locator is not None and not isinstance(key_locator, KeyLocator): + signature_info.key_locator = KeyLocator( + name=key_locator.name, key_digest=key_locator.key_digest) + + +@dc.dataclass +class Links: + names: list[NDNName] = dc.field( + default_factory=list, metadata={'tlv_type': TypeNumber.NAME}) + + +@dc.dataclass +class InterestPacketValue: + name: NDNName = dc.field(default='/', metadata={ + 'tlv_type': TypeNumber.NAME, 'field_type': 'interest_name'}) + can_be_prefix: bool = dc.field( + default=False, metadata={'tlv_type': TypeNumber.CAN_BE_PREFIX}) + must_be_fresh: bool = dc.field( + default=False, metadata={'tlv_type': TypeNumber.MUST_BE_FRESH}) + forwarding_hint: Optional[Links] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.FORWARDING_HINT}) + nonce: Optional[int] = dc.field(default=None, metadata={ + 'tlv_type': TypeNumber.NONCE, 'fixed_len': 4}) + lifetime: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.INTEREST_LIFETIME}) + hop_limit: Optional[int] = dc.field(default=None, metadata={ + 'tlv_type': TypeNumber.HOP_LIMIT, 'fixed_len': 1}) + _sig_cover_start: None = dc.field( + default=None, metadata={'field_type': 'offset_marker'}) + _digest_cover_start: None = dc.field( + default=None, metadata={'field_type': 'offset_marker'}) + application_parameters: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.APPLICATION_PARAMETERS}) + signature_info: Optional[SignatureInfo] = dc.field( + default=None, metadata={ + 'tlv_type': TypeNumber.INTEREST_SIGNATURE_INFO}) + signature_value: Optional[bytes] = dc.field(default=None, metadata={ + 'tlv_type': TypeNumber.INTEREST_SIGNATURE_VALUE, + 'field_type': 'sig_value', + 'cover_start': '_sig_cover_start', + 'digest_cover_start': '_digest_cover_start', + 'digest_cover_end': '_digest_cover_end', + }) + _digest_cover_end: None = dc.field( + default=None, metadata={'field_type': 'offset_marker'}) + + +@dc.dataclass +class InterestPacket: + interest: Optional[InterestPacketValue] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.INTEREST}) + + +@dc.dataclass(init=False) +class MetaInfo: + content_type: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.CONTENT_TYPE}) + freshness_period: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.FRESHNESS_PERIOD}) + final_block_id: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.FINAL_BLOCK_ID}) def __init__(self, - content_type: int = ContentType.BLOB, - freshness_period: int | None = None, - final_block_id: BinaryStr = None): + content_type: Optional[int] = ContentType.BLOB, + freshness_period: Optional[int] = None, + final_block_id: Optional[BinaryStr] = None): self.content_type = content_type self.freshness_period = freshness_period self.final_block_id = final_block_id @staticmethod def from_dict(kwargs): - return MetaInfo(**{f.name: kwargs[f.name] - for f in MetaInfo._encoded_fields - if f.name in kwargs}) - - -class DataPacketValue(TlvModel): - _signer = ProcedureArgument() - _sig_cover_part = ProcedureArgument() - _sig_value_buf = ProcedureArgument() - _shrink_len = ProcedureArgument(0) - - _sig_cover_start = OffsetMarker() - name = NameField("/") - meta_info = ModelField(TypeNumber.META_INFO, MetaInfo) - content = BytesField(TypeNumber.CONTENT) - # v0.2 Data packets has critical SignatureType-specific TLVs - signature_info = ModelField(TypeNumber.SIGNATURE_INFO, SignatureInfo, ignore_critical=True) - signature_value = SignatureValueField(TypeNumber.SIGNATURE_VALUE, - signer=_signer, - covered_part=_sig_cover_part, - starting_point=_sig_cover_start, - value_buffer=_sig_value_buf, - shrink_len=_shrink_len) - - def encoded_length(self, markers: dict | None = None) -> int: - if markers is None: - markers = {} - self._sig_cover_part.set_arg(markers, []) - - signer = self._signer.get_arg(markers) - if signer is not None: - signer.write_signature_info(self.signature_info) - - return super().encoded_length(markers) - - def encode(self, - wire: VarBinaryStr = None, - offset: int = 0, - markers: dict | None = None) -> VarBinaryStr: - if markers is None: - markers = {} - ret = super().encode(wire, offset, markers) - DataPacketValue.signature_value.calculate_signature(markers) - return ret - - @classmethod - def parse(cls, wire: BinaryStr, markers: dict | None = None, ignore_critical: bool = False): - if markers is None: - markers = {} - cls._sig_cover_part.set_arg(markers, []) - return super().parse(wire, markers, ignore_critical) - - -class DataPacket(TlvModel): - _signer = ProcedureArgument() - data = ModelField(TypeNumber.DATA, DataPacketValue, [_signer]) + return MetaInfo(**{ + f.name: kwargs[f.name] + for f in dc.fields(MetaInfo) + if f.name in kwargs + }) @dc.dataclass -class InterestParam: - r""" - A dataclass collecting the parameters of an Interest, except ApplicationParameters. +class DataPacketValue: + _sig_cover_start: None = dc.field( + default=None, metadata={'field_type': 'offset_marker'}) + name: NDNName = dc.field( + default='/', metadata={'tlv_type': TypeNumber.NAME}) + meta_info: Optional[MetaInfo] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.META_INFO}) + content: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.CONTENT}) + signature_info: Optional[SignatureInfo] = dc.field(default=None, metadata={ + 'tlv_type': TypeNumber.SIGNATURE_INFO, 'ignore_critical': True}) + signature_value: Optional[bytes] = dc.field(default=None, metadata={ + 'tlv_type': TypeNumber.SIGNATURE_VALUE, + 'field_type': 'sig_value', + 'cover_start': '_sig_cover_start', + }) - :ivar can_be_prefix: CanBePrefix. ``False`` by default. - :vartype can_be_prefix: bool - :ivar must_be_fresh: MustBeFresh. ``False`` by default. - :vartype must_be_fresh: bool - - :ivar nonce: Nonce. ``None`` by default. - :vartype nonce: int - - :ivar lifetime: InterestLifetime in milliseconds. ``4000`` by default. - :vartype lifetime: int +@dc.dataclass +class DataPacket: + data: Optional[DataPacketValue] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.DATA}) - :ivar hop_limit: HopLimit. ``None`` by default. - :vartype hop_limit: int - :ivar forwarding_hint: ForwardingHint. The type should be list of Names. - e.g.: ``["/ndn/name1", ["ndn", "name2"]]`` - :vartype forwarding_hint: :class:`List` [ :class:`Tuple` [ :class:`int` , :any:`NonStrictName` ]] - """ +@dc.dataclass +class InterestParam: can_be_prefix: bool = False must_be_fresh: bool = False - nonce: int | None = None - lifetime: int | None = 4000 - hop_limit: int | None = None + nonce: Optional[int] = None + lifetime: Optional[int] = 4000 + hop_limit: Optional[int] = None forwarding_hint: list[NonStrictName] = dc.field(default_factory=list) @staticmethod def from_dict(kwargs): - return InterestParam(**{f.name: kwargs[f.name] - for f in dc.fields(InterestParam) - if f.name in kwargs}) + return InterestParam(**{ + f.name: kwargs[f.name] + for f in dc.fields(InterestParam) + if f.name in kwargs + }) @dc.dataclass class SignaturePtrs: - r""" - A set of pointers used to verify a packet. - - :ivar signature_info: the SignatureInfo. - :vartype signature_info: :any:`SignatureInfo` - - :ivar signature_covered_part: a list of pointers, each of which points to a memory covered by signature. - :vartype signature_covered_part: :class:`List` [ :class:`memoryview` ] - - :ivar signature_value_buf: a pointer to SignatureValue (TL excluded). - :vartype signature_value_buf: :class:`memoryview` + signature_info: Optional[SignatureInfo] = None + signature_covered_part: list[BinaryStr] = dc.field(default_factory=list) + signature_value_buf: Optional[BinaryStr] = None + digest_covered_part: list[BinaryStr] = dc.field(default_factory=list) + digest_value_buf: Optional[BinaryStr] = None - :ivar digest_covered_part: a list of pointers, each of which points to a memory covered by - ParametersSha256DigestComponent. - :vartype digest_covered_part: :class:`List` [ :class:`memoryview` ] - :ivar digest_value_buf: a pointer to ParametersSha256DigestComponent (TL excluded). - :vartype digest_value_buf: :class:`memoryview` - """ - signature_info: SignatureInfo | None = None - signature_covered_part: list[BinaryStr] | None = dc.field(default_factory=list) - signature_value_buf: BinaryStr | None = None - digest_covered_part: list[BinaryStr] | None = dc.field(default_factory=list) - digest_value_buf: BinaryStr | None = None +Interest = tuple[FormalName, InterestParam, Optional[BinaryStr], SignaturePtrs] +Data = tuple[FormalName, MetaInfo, Optional[BinaryStr], SignaturePtrs] -Interest = tuple[FormalName, InterestParam, BinaryStr | None, SignaturePtrs] -Data = tuple[FormalName, MetaInfo, BinaryStr | None, SignaturePtrs] +def _wrap_tlv(type_num: int, value: BinaryStr) -> VarBinaryStr: + total = ( + get_tl_num_size(type_num) + + get_tl_num_size(len(value)) + + len(value) + ) + wire = bytearray(total) + offset = write_tl_num(type_num, wire, 0) + offset += write_tl_num(len(value), wire, offset) + wire[offset:] = value + return wire def make_interest(name: NonStrictName, interest_param: InterestParam, - app_param: BinaryStr | None = None, - signer: Signer | None = None, + app_param: Optional[BinaryStr] = None, + signer: Optional[Signer] = None, need_final_name: bool = False): - r""" - Make an Interest packet. - - :param name: the Name field. - :type name: :any:`NonStrictName` - :param interest_param: basic parameters of the Interest. - :param app_param: the ApplicationParameters field. - :type app_param: :class:`Optional` [ :any:`BinaryStr` ] - :param signer: a Signer to sign this Interest. ``None`` if it is unsigned. - :param need_final_name: if ``True``, also return the final Name with ParametersSha256DigestComponent. - :return: TLV encoded Interest packet. If ``need_final_name``, return a tuple of the packet - and the final Name. - """ - interest = InterestPacket() - interest.interest = InterestPacketValue() - interest.interest.name = name - interest.interest.can_be_prefix = interest_param.can_be_prefix - interest.interest.must_be_fresh = interest_param.must_be_fresh - interest.interest.nonce = interest_param.nonce - interest.interest.lifetime = interest_param.lifetime - interest.interest.hop_limit = interest_param.hop_limit - + value = InterestPacketValue( + name=name, + can_be_prefix=interest_param.can_be_prefix, + must_be_fresh=interest_param.must_be_fresh, + nonce=interest_param.nonce, + lifetime=interest_param.lifetime, + hop_limit=interest_param.hop_limit, + application_parameters=app_param, + ) if interest_param.forwarding_hint: - interest.interest.forwarding_hint = Links() - for cur in interest_param.forwarding_hint: - interest.interest.forwarding_hint.names.append(cur) - - interest.interest.application_parameters = app_param + value.forwarding_hint = Links( + names=list(interest_param.forwarding_hint)) if signer is not None: - interest.interest.signature_info = SignatureInfo() - markers = {} - interest._signer.set_arg(markers, signer) - ret = interest.encode(markers=markers) - shrink_size = interest.interest._shrink_len.get_arg(markers['interest##inner_markers']) - if shrink_size > 0: - ret = shrink_length(ret, shrink_size) + value.signature_info = SignatureInfo() + write_signature_info(signer, value.signature_info) + if value.application_parameters is None: + value.application_parameters = b'' + + markers = { + '##signer': signer, + '##need_digest': value.application_parameters is not None, + '##_digest_cover_start_field': '_digest_cover_start', + '##_digest_cover_end_field': '_digest_cover_end', + } + encoded_value = tlv_encode(value, markers=markers) + wire = _wrap_tlv(TypeNumber.INTEREST, encoded_value) if need_final_name: - return ret, InterestPacketValue.name.get_final_name(markers['interest##inner_markers']) - else: - return ret + final_value = tlv_parse(InterestPacketValue, encoded_value) + return wire, final_value.name + return wire def make_data(name: NonStrictName, meta_info: MetaInfo, - content: BinaryStr | None = None, - signer: Signer | None = None) -> VarBinaryStr: - r""" - Make a Data packet. - - :param name: the Name field. - :type name: :any:`NonStrictName` - :param meta_info: the MetaIndo field. - :param content: the Content. - :type content: :class:`Optional` [ :any:`BinaryStr` ] - :param signer: a Signer to sign this Interest. ``None`` if it is unsigned. - :return: TLV encoded Data packet. - """ - data = DataPacket() - data.data = DataPacketValue() - data.data.meta_info = meta_info - data.data.name = name - data.data.content = content + content: Optional[BinaryStr] = None, + signer: Optional[Signer] = None) -> VarBinaryStr: + value = DataPacketValue(name=name, meta_info=meta_info, content=content) if signer is not None: - data.data.signature_info = SignatureInfo() - markers = {} - data._signer.set_arg(markers, signer) - ret = data.encode(markers=markers) - shrink_size = data.data._shrink_len.get_arg(markers['data##inner_markers']) - if shrink_size > 0: - ret = shrink_length(ret, shrink_size) - return ret + value.signature_info = SignatureInfo() + write_signature_info(signer, value.signature_info) + encoded_value = tlv_encode(value, markers={'##signer': signer}) + return _wrap_tlv(TypeNumber.DATA, encoded_value) def parse_interest(wire: BinaryStr, with_tl: bool = True) -> Interest: - r""" - Parse a TLV encoded Interest. - - :param wire: the buffer. - :type wire: :any:`BinaryStr` - :param with_tl: ``True`` if the packet has Type and Length. - ``False`` if ``wire`` only has the Value part. - :return: a Tuple of Name, InterestParameters, ApplicationParameters and :any:`SignaturePtrs`. - :rtype: :class:`Tuple` [ :any:`FormalName` , :any:`InterestParam` , - :class:`Optional` [ :any:`BinaryStr` ], :any:`SignaturePtrs` ] - """ - if with_tl: - wire = parse_and_check_tl(wire, TypeNumber.INTEREST) + value_wire = ( + parse_and_check_tl(wire, TypeNumber.INTEREST) + if with_tl else wire + ) markers = {} - ret = InterestPacketValue.parse(wire, markers) - params = InterestParam() - params.can_be_prefix = ret.can_be_prefix - params.must_be_fresh = ret.must_be_fresh - params.nonce = ret.nonce - params.lifetime = ret.lifetime - params.hop_limit = ret.hop_limit - - if ret.forwarding_hint and ret.forwarding_hint.names: - for cur in ret.forwarding_hint.names: - params.forwarding_hint.append(cur) + ret = tlv_parse(InterestPacketValue, value_wire, markers=markers) + params = InterestParam( + can_be_prefix=ret.can_be_prefix, + must_be_fresh=ret.must_be_fresh, + nonce=ret.nonce, + lifetime=ret.lifetime, + hop_limit=ret.hop_limit, + ) + if ret.forwarding_hint: + params.forwarding_hint.extend(ret.forwarding_hint.names) + digest_parts = [] + digest_start = markers.get('_digest_cover_start') + if digest_start is not None: + digest_parts.append(memoryview(value_wire)[digest_start:]) sig_ptrs = SignaturePtrs( signature_info=ret.signature_info, - signature_covered_part=ret._sig_cover_part.get_arg(markers), + signature_covered_part=markers.get('##sig_covered_part', []), signature_value_buf=ret.signature_value, - digest_covered_part=ret._digest_cover_part.get_arg(markers), - digest_value_buf=ret._digest_buf.get_arg(markers) + digest_covered_part=digest_parts, + digest_value_buf=markers.get('##digest_buf'), ) return ret.name, params, ret.application_parameters, sig_ptrs def parse_data(wire: BinaryStr, with_tl: bool = True) -> Data: - r""" - Parse a TLV encoded Data. - - :param wire: the buffer. - :type wire: :any:`BinaryStr` - :param with_tl: ``True`` if the packet has Type and Length. - ``False`` if ``wire`` only has the Value part. - :return: a Tuple of Name, MetaInfo, Content and :any:`SignaturePtrs`. - :rtype: :class:`Tuple` [ :any:`FormalName` , :any:`MetaInfo` , - :class:`Optional` [ :any:`BinaryStr` ], :any:`SignaturePtrs` ] - """ - if with_tl: - wire = parse_and_check_tl(wire, TypeNumber.DATA) + value_wire = parse_and_check_tl(wire, TypeNumber.DATA) if with_tl else wire markers = {} - ret = DataPacketValue.parse(wire, markers) - params = ret.meta_info - if params is None: - params = MetaInfo() + ret = tlv_parse(DataPacketValue, value_wire, markers=markers) + meta_info = ret.meta_info if ret.meta_info is not None else MetaInfo() sig_ptrs = SignaturePtrs( signature_info=ret.signature_info, - signature_covered_part=ret._sig_cover_part.get_arg(markers), + signature_covered_part=markers.get('##sig_covered_part', []), signature_value_buf=ret.signature_value, ) - return ret.name, params, ret.content, sig_ptrs + return ret.name, meta_info, ret.content, sig_ptrs diff --git a/src/ndn/encoding/ndn_format_0_3_2017.py b/src/ndn/encoding/ndn_format_0_3_2017.py index 44db967..45a3933 100644 --- a/src/ndn/encoding/ndn_format_0_3_2017.py +++ b/src/ndn/encoding/ndn_format_0_3_2017.py @@ -1,51 +1,32 @@ # ----------------------------------------------------------------------------- # Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# # Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. # ----------------------------------------------------------------------------- +"""NDN Packet Format v0.3 (2017 forwarding hint) dataclass models.""" import dataclasses as dc -from hashlib import sha256 +from typing import Optional + from .name import Name, Component from .signer import Signer -from .tlv_type import VarBinaryStr, BinaryStr, NonStrictName, FormalName -from .tlv_var import parse_and_check_tl, shrink_length -from .tlv_model import TlvModel, InterestNameField, BoolField, UintField, \ - SignatureValueField, OffsetMarker, BytesField, ModelField, NameField, \ - ProcedureArgument, RepeatedField - +from .tlv_model import NDNName, tlv_encode, tlv_parse +from .tlv_type import BinaryStr, VarBinaryStr, NonStrictName, FormalName +from .tlv_var import get_tl_num_size, parse_and_check_tl, write_tl_num -__all__ = ['TypeNumber', 'ContentType', 'SignatureType', 'KeyLocator', 'SignatureInfo', 'Delegation', - 'Links', 'MetaInfo', 'InterestParam', 'SignaturePtrs', 'make_interest', 'make_data', - 'parse_interest', 'parse_data', 'Interest', 'Data'] +__all__ = [ + 'TypeNumber', 'ContentType', 'SignatureType', 'KeyLocator', + 'SignatureInfo', 'write_signature_info', + 'Delegation', 'Links', 'MetaInfo', 'InterestParam', 'SignaturePtrs', 'make_interest', + 'make_data', 'parse_interest', 'parse_data', 'Interest', 'Data', +] class TypeNumber: - r""" - TLV Type numbers used in `NDN Packet Format 0.3 - `_. - - Constant names are changed to PEP 8 style, i.e., all upper cases with underscores separating words. - """ INTEREST = 0x05 DATA = 0x06 - NAME = Name.TYPE_NAME GENERIC_NAME_COMPONENT = Component.TYPE_GENERIC IMPLICIT_SHA256_DIGEST_COMPONENT = Component.TYPE_IMPLICIT_SHA256 PARAMETERS_SHA256_DIGEST_COMPONENT = Component.TYPE_PARAMETERS_SHA256 - CAN_BE_PREFIX = 0x21 MUST_BE_FRESH = 0x12 FORWARDING_HINT = 0x1e @@ -55,7 +36,6 @@ class TypeNumber: APPLICATION_PARAMETERS = 0x24 INTEREST_SIGNATURE_INFO = 0x2c INTEREST_SIGNATURE_VALUE = 0x2e - META_INFO = 0x14 CONTENT = 0x15 SIGNATURE_INFO = 0x16 @@ -63,31 +43,17 @@ class TypeNumber: CONTENT_TYPE = 0x18 FRESHNESS_PERIOD = 0x19 FINAL_BLOCK_ID = 0x1a - SIGNATURE_TYPE = 0x1b KEY_LOCATOR = 0x1c KEY_DIGEST = 0x1d SIGNATURE_NONCE = 0x26 SIGNATURE_TIME = 0x28 SIGNATURE_SEQ_NUM = 0x2a - DELEGATION = 0x1f PREFERENCE = 0x1e class ContentType: - r""" - Numbers used in ContentType. - - ==== =================================== - Type Description - ==== =================================== - BLOB Payload identified by the data name - LINK A list of delegations - KEY Public Key - NACK Application-level NACK - ==== =================================== - """ BLOB = 0 LINK = 1 KEY = 2 @@ -95,20 +61,6 @@ class ContentType: class SignatureType: - r""" - Numbers used in SignatureType. - - ================= ================================================== - Type Description - ================= ================================================== - NOT_SIGNED Not signed - DIGEST_SHA256 SHA-256 digest (only for integrity protection) - SHA256_WITH_RSA RSA signature over a SHA-256 digest - SHA256_WITH_ECDSA An ECDSA signature over a SHA-256 digest - HMAC_WITH_SHA256 SHA256 hash-based message authentication codes - NULL An empty signature for testing and experimentation - ================= ================================================== - """ NOT_SIGNED = None DIGEST_SHA256 = 0 SHA256_WITH_RSA = 1 @@ -117,399 +69,286 @@ class SignatureType: NULL = 200 -class KeyLocator(TlvModel): - name = NameField() - key_digest = BytesField(TypeNumber.KEY_DIGEST) - - -class SignatureInfo(TlvModel): - signature_type = UintField(TypeNumber.SIGNATURE_TYPE, fixed_len=1) - key_locator = ModelField(TypeNumber.KEY_LOCATOR, KeyLocator) - signature_nonce = UintField(TypeNumber.SIGNATURE_NONCE) - signature_time = UintField(TypeNumber.SIGNATURE_TIME) - signature_seq_num = UintField(TypeNumber.SIGNATURE_SEQ_NUM) - - -class Delegation(TlvModel): - preference = UintField(TypeNumber.PREFERENCE) - delegation = NameField() - - -class Links(TlvModel): - delegations = RepeatedField(ModelField(TypeNumber.DELEGATION, Delegation)) - - -class InterestPacketValue(TlvModel): - _signer = ProcedureArgument() - _sig_cover_part = ProcedureArgument() - _sig_value_buf = ProcedureArgument() - _need_digest = ProcedureArgument() - _digest_cover_part = ProcedureArgument() - _digest_buf = ProcedureArgument() - _shrink_len = ProcedureArgument(0) - - name = InterestNameField(need_digest=_need_digest, - signature_covered_part=_sig_cover_part, - digest_buffer=_digest_buf, - default="/") - can_be_prefix = BoolField(TypeNumber.CAN_BE_PREFIX, default=False) - must_be_fresh = BoolField(TypeNumber.MUST_BE_FRESH, default=False) - forwarding_hint = ModelField(TypeNumber.FORWARDING_HINT, Links) - nonce = UintField(TypeNumber.NONCE, fixed_len=4) - lifetime = UintField(TypeNumber.INTEREST_LIFETIME) # We can not write 4000 as a parse default - hop_limit = UintField(TypeNumber.HOP_LIMIT, fixed_len=1) - _sig_cover_start = OffsetMarker() - _digest_cover_start = OffsetMarker() - application_parameters = BytesField(TypeNumber.APPLICATION_PARAMETERS) - signature_info = ModelField(TypeNumber.INTEREST_SIGNATURE_INFO, SignatureInfo) - signature_value = SignatureValueField(TypeNumber.INTEREST_SIGNATURE_VALUE, - signer=_signer, - covered_part=_sig_cover_part, - starting_point=_sig_cover_start, - value_buffer=_sig_value_buf, - shrink_len=_shrink_len) - _digest_cover_end = OffsetMarker() - - def encoded_length(self, markers: dict | None = None) -> int: - if markers is None: - markers = {} - self._sig_cover_part.set_arg(markers, []) - - signer = self._signer.get_arg(markers) - if signer is not None: - signer.write_signature_info(self.signature_info) - app_param = self.application_parameters - if (signer is not None) and (app_param is None): - app_param = b'' - self.application_parameters = app_param - - self._need_digest.set_arg(markers, app_param is not None) - - return super().encoded_length(markers) - - def encode(self, - wire: VarBinaryStr = None, - offset: int = 0, - markers: dict | None = None) -> VarBinaryStr: - if markers is None: - markers = {} - ret = super().encode(wire, offset, markers) - wire_view = memoryview(ret) - - InterestPacketValue.signature_value.calculate_signature(markers) - if self._need_digest.get_arg(markers): - digest_cover_start = self._digest_cover_start.get_arg(markers) - shrink_size = self._shrink_len.get_arg(markers) - digest_cover_end = self._digest_cover_end.get_arg(markers) - shrink_size - digest_covered_part = [wire_view[digest_cover_start:digest_cover_end]] - self._digest_cover_part.set_arg(markers, digest_covered_part) - sha256_algo = sha256() - digest_buf = self._digest_buf.get_arg(markers) - for blk in digest_covered_part: - sha256_algo.update(blk) - digest_buf[:] = sha256_algo.digest() - - return ret - - @classmethod - def parse(cls, wire: BinaryStr, markers: dict | None = None, ignore_critical: bool = False): - if markers is None: - markers = {} - cls._sig_cover_part.set_arg(markers, []) - ret = super().parse(wire, markers, ignore_critical) - digest_cover_start = cls._digest_cover_start.get_arg(markers) - digest_cover_end = cls._digest_cover_end.get_arg(markers) - digest_cover_part = [memoryview(wire)[digest_cover_start:digest_cover_end]] - cls._digest_cover_part.set_arg(markers, digest_cover_part) - return ret - - -class InterestPacket(TlvModel): - _signer = ProcedureArgument() - interest = ModelField(TypeNumber.INTEREST, InterestPacketValue, [_signer]) - - -class MetaInfo(TlvModel): - content_type = UintField(TypeNumber.CONTENT_TYPE) - freshness_period = UintField(TypeNumber.FRESHNESS_PERIOD) - final_block_id = BytesField(TypeNumber.FINAL_BLOCK_ID) +@dc.dataclass +class KeyLocator: + name: NDNName = dc.field( + default=None, metadata={'tlv_type': TypeNumber.NAME}) + key_digest: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.KEY_DIGEST}) + + +@dc.dataclass +class SignatureInfo: + signature_type: Optional[int] = dc.field( + default=None, metadata={ + 'tlv_type': TypeNumber.SIGNATURE_TYPE, 'fixed_len': 1}) + key_locator: Optional[KeyLocator] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.KEY_LOCATOR}) + signature_nonce: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.SIGNATURE_NONCE}) + signature_time: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.SIGNATURE_TIME}) + signature_seq_num: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.SIGNATURE_SEQ_NUM}) + + +def write_signature_info(signer: Signer, signature_info: SignatureInfo) -> None: + """ + Let *signer* fill *signature_info*. + + Signers still assign the v1 ``KeyLocator`` model, which the dataclass + encoder cannot serialize, so it is converted to :class:`KeyLocator`. + """ + signer.write_signature_info(signature_info) + key_locator = signature_info.key_locator + if key_locator is not None and not isinstance(key_locator, KeyLocator): + signature_info.key_locator = KeyLocator( + name=key_locator.name, key_digest=key_locator.key_digest) + + +@dc.dataclass +class Delegation: + preference: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.PREFERENCE}) + delegation: NDNName = dc.field( + default=None, metadata={'tlv_type': TypeNumber.NAME}) + + +@dc.dataclass +class Links: + delegations: list[Delegation] = dc.field( + default_factory=list, metadata={'tlv_type': TypeNumber.DELEGATION}) + + +@dc.dataclass +class InterestPacketValue: + name: NDNName = dc.field(default='/', metadata={ + 'tlv_type': TypeNumber.NAME, 'field_type': 'interest_name'}) + can_be_prefix: bool = dc.field( + default=False, metadata={'tlv_type': TypeNumber.CAN_BE_PREFIX}) + must_be_fresh: bool = dc.field( + default=False, metadata={'tlv_type': TypeNumber.MUST_BE_FRESH}) + forwarding_hint: Optional[Links] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.FORWARDING_HINT}) + nonce: Optional[int] = dc.field(default=None, metadata={ + 'tlv_type': TypeNumber.NONCE, 'fixed_len': 4}) + lifetime: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.INTEREST_LIFETIME}) + hop_limit: Optional[int] = dc.field(default=None, metadata={ + 'tlv_type': TypeNumber.HOP_LIMIT, 'fixed_len': 1}) + _sig_cover_start: None = dc.field( + default=None, metadata={'field_type': 'offset_marker'}) + _digest_cover_start: None = dc.field( + default=None, metadata={'field_type': 'offset_marker'}) + application_parameters: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.APPLICATION_PARAMETERS}) + signature_info: Optional[SignatureInfo] = dc.field( + default=None, metadata={ + 'tlv_type': TypeNumber.INTEREST_SIGNATURE_INFO}) + signature_value: Optional[bytes] = dc.field(default=None, metadata={ + 'tlv_type': TypeNumber.INTEREST_SIGNATURE_VALUE, + 'field_type': 'sig_value', + 'cover_start': '_sig_cover_start', + 'digest_cover_start': '_digest_cover_start', + 'digest_cover_end': '_digest_cover_end', + }) + _digest_cover_end: None = dc.field( + default=None, metadata={'field_type': 'offset_marker'}) + + +@dc.dataclass +class InterestPacket: + interest: Optional[InterestPacketValue] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.INTEREST}) + + +@dc.dataclass(init=False) +class MetaInfo: + content_type: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.CONTENT_TYPE}) + freshness_period: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.FRESHNESS_PERIOD}) + final_block_id: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.FINAL_BLOCK_ID}) def __init__(self, - content_type: int = ContentType.BLOB, - freshness_period: int | None = None, - final_block_id: BinaryStr = None): + content_type: Optional[int] = ContentType.BLOB, + freshness_period: Optional[int] = None, + final_block_id: Optional[BinaryStr] = None): self.content_type = content_type self.freshness_period = freshness_period self.final_block_id = final_block_id @staticmethod def from_dict(kwargs): - return MetaInfo(**{f.name: kwargs[f.name] - for f in MetaInfo._encoded_fields - if f.name in kwargs}) - - -class DataPacketValue(TlvModel): - _signer = ProcedureArgument() - _sig_cover_part = ProcedureArgument() - _sig_value_buf = ProcedureArgument() - _shrink_len = ProcedureArgument(0) - - _sig_cover_start = OffsetMarker() - name = NameField("/") - meta_info = ModelField(TypeNumber.META_INFO, MetaInfo) - content = BytesField(TypeNumber.CONTENT) - # v0.2 Data packets has critical SignatureType-specific TLVs - signature_info = ModelField(TypeNumber.SIGNATURE_INFO, SignatureInfo, ignore_critical=True) - signature_value = SignatureValueField(TypeNumber.SIGNATURE_VALUE, - signer=_signer, - covered_part=_sig_cover_part, - starting_point=_sig_cover_start, - value_buffer=_sig_value_buf, - shrink_len=_shrink_len) - - def encoded_length(self, markers: dict | None = None) -> int: - if markers is None: - markers = {} - self._sig_cover_part.set_arg(markers, []) - - signer = self._signer.get_arg(markers) - if signer is not None: - signer.write_signature_info(self.signature_info) - - return super().encoded_length(markers) - - def encode(self, - wire: VarBinaryStr = None, - offset: int = 0, - markers: dict | None = None) -> VarBinaryStr: - if markers is None: - markers = {} - ret = super().encode(wire, offset, markers) - DataPacketValue.signature_value.calculate_signature(markers) - return ret - - @classmethod - def parse(cls, wire: BinaryStr, markers: dict | None = None, ignore_critical: bool = False): - if markers is None: - markers = {} - cls._sig_cover_part.set_arg(markers, []) - return super().parse(wire, markers, ignore_critical) - - -class DataPacket(TlvModel): - _signer = ProcedureArgument() - data = ModelField(TypeNumber.DATA, DataPacketValue, [_signer]) + return MetaInfo(**{ + f.name: kwargs[f.name] + for f in dc.fields(MetaInfo) + if f.name in kwargs + }) @dc.dataclass -class InterestParam: - r""" - A dataclass collecting the parameters of an Interest, except ApplicationParameters. - - :ivar can_be_prefix: CanBePrefix. ``False`` by default. - :vartype can_be_prefix: bool - - :ivar must_be_fresh: MustBeFresh. ``False`` by default. - :vartype must_be_fresh: bool +class DataPacketValue: + _sig_cover_start: None = dc.field( + default=None, metadata={'field_type': 'offset_marker'}) + name: NDNName = dc.field( + default='/', metadata={'tlv_type': TypeNumber.NAME}) + meta_info: Optional[MetaInfo] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.META_INFO}) + content: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.CONTENT}) + signature_info: Optional[SignatureInfo] = dc.field(default=None, metadata={ + 'tlv_type': TypeNumber.SIGNATURE_INFO, 'ignore_critical': True}) + signature_value: Optional[bytes] = dc.field(default=None, metadata={ + 'tlv_type': TypeNumber.SIGNATURE_VALUE, + 'field_type': 'sig_value', + 'cover_start': '_sig_cover_start', + }) - :ivar nonce: Nonce. ``None`` by default. - :vartype nonce: int - :ivar lifetime: InterestLifetime in milliseconds. ``4000`` by default. - :vartype lifetime: int +@dc.dataclass +class DataPacket: + data: Optional[DataPacketValue] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.DATA}) - :ivar hop_limit: HopLimit. ``None`` by default. - :vartype hop_limit: int - :ivar forwarding_hint: ForwardingHint. The type should be list of pairs of Preference and Name. - e.g.: ``[(1, "/ndn/name1"), (2, ["ndn", "name2"])]`` - :vartype forwarding_hint: :class:`List` [ :class:`Tuple` [ :class:`int` , :any:`NonStrictName` ]] - """ +@dc.dataclass +class InterestParam: can_be_prefix: bool = False must_be_fresh: bool = False - nonce: int | None = None - lifetime: int | None = 4000 - hop_limit: int | None = None + nonce: Optional[int] = None + lifetime: Optional[int] = 4000 + hop_limit: Optional[int] = None forwarding_hint: list[tuple[int, NonStrictName]] = dc.field(default_factory=list) @staticmethod def from_dict(kwargs): - return InterestParam(**{f.name: kwargs[f.name] - for f in dc.fields(InterestParam) - if f.name in kwargs}) + return InterestParam(**{ + f.name: kwargs[f.name] + for f in dc.fields(InterestParam) + if f.name in kwargs + }) @dc.dataclass class SignaturePtrs: - r""" - A set of pointers used to verify a packet. + signature_info: Optional[SignatureInfo] = None + signature_covered_part: list[BinaryStr] = dc.field(default_factory=list) + signature_value_buf: Optional[BinaryStr] = None + digest_covered_part: list[BinaryStr] = dc.field(default_factory=list) + digest_value_buf: Optional[BinaryStr] = None - :ivar signature_info: the SignatureInfo. - :vartype signature_info: :any:`SignatureInfo` - :ivar signature_covered_part: a list of pointers, each of which points to a memory covered by signature. - :vartype signature_covered_part: :class:`List` [ :class:`memoryview` ] +Interest = tuple[FormalName, InterestParam, Optional[BinaryStr], SignaturePtrs] +Data = tuple[FormalName, MetaInfo, Optional[BinaryStr], SignaturePtrs] - :ivar signature_value_buf: a pointer to SignatureValue (TL excluded). - :vartype signature_value_buf: :class:`memoryview` - - :ivar digest_covered_part: a list of pointers, each of which points to a memory covered by - ParametersSha256DigestComponent. - :vartype digest_covered_part: :class:`List` [ :class:`memoryview` ] - - :ivar digest_value_buf: a pointer to ParametersSha256DigestComponent (TL excluded). - :vartype digest_value_buf: :class:`memoryview` - """ - signature_info: SignatureInfo | None = None - signature_covered_part: list[BinaryStr] | None = dc.field(default_factory=list) - signature_value_buf: BinaryStr | None = None - digest_covered_part: list[BinaryStr] | None = dc.field(default_factory=list) - digest_value_buf: BinaryStr | None = None - -Interest = tuple[FormalName, InterestParam, BinaryStr | None, SignaturePtrs] -Data = tuple[FormalName, MetaInfo, BinaryStr | None, SignaturePtrs] +def _wrap_tlv(type_num: int, value: BinaryStr) -> VarBinaryStr: + total = ( + get_tl_num_size(type_num) + + get_tl_num_size(len(value)) + + len(value) + ) + wire = bytearray(total) + offset = write_tl_num(type_num, wire, 0) + offset += write_tl_num(len(value), wire, offset) + wire[offset:] = value + return wire def make_interest(name: NonStrictName, interest_param: InterestParam, - app_param: BinaryStr | None = None, - signer: Signer | None = None, + app_param: Optional[BinaryStr] = None, + signer: Optional[Signer] = None, need_final_name: bool = False): - r""" - Make an Interest packet. - - :param name: the Name field. - :type name: :any:`NonStrictName` - :param interest_param: basic parameters of the Interest. - :param app_param: the ApplicationParameters field. - :type app_param: :class:`Optional` [ :any:`BinaryStr` ] - :param signer: a Signer to sign this Interest. ``None`` if it is unsigned. - :param need_final_name: if ``True``, also return the final Name with ParametersSha256DigestComponent. - :return: TLV encoded Interest packet. If ``need_final_name``, return a tuple of the packet - and the final Name. - """ - interest = InterestPacket() - interest.interest = InterestPacketValue() - interest.interest.name = name - interest.interest.can_be_prefix = interest_param.can_be_prefix - interest.interest.must_be_fresh = interest_param.must_be_fresh - interest.interest.nonce = interest_param.nonce - interest.interest.lifetime = interest_param.lifetime - interest.interest.hop_limit = interest_param.hop_limit - + value = InterestPacketValue( + name=name, + can_be_prefix=interest_param.can_be_prefix, + must_be_fresh=interest_param.must_be_fresh, + nonce=interest_param.nonce, + lifetime=interest_param.lifetime, + hop_limit=interest_param.hop_limit, + application_parameters=app_param, + ) if interest_param.forwarding_hint: - interest.interest.forwarding_hint = Links() - for preference, delegation in interest_param.forwarding_hint: - cur = Delegation() - cur.preference = preference - cur.delegation = delegation - interest.interest.forwarding_hint.delegations.append(cur) - - interest.interest.application_parameters = app_param + value.forwarding_hint = Links(delegations=[ + Delegation(preference=preference, delegation=delegation) + for preference, delegation in interest_param.forwarding_hint + ]) if signer is not None: - interest.interest.signature_info = SignatureInfo() - markers = {} - interest._signer.set_arg(markers, signer) - ret = interest.encode(markers=markers) - shrink_size = interest.interest._shrink_len.get_arg(markers['interest##inner_markers']) - if shrink_size > 0: - ret = shrink_length(ret, shrink_size) + value.signature_info = SignatureInfo() + write_signature_info(signer, value.signature_info) + if value.application_parameters is None: + value.application_parameters = b'' + + markers = { + '##signer': signer, + '##need_digest': value.application_parameters is not None, + '##_digest_cover_start_field': '_digest_cover_start', + '##_digest_cover_end_field': '_digest_cover_end', + } + encoded_value = tlv_encode(value, markers=markers) + wire = _wrap_tlv(TypeNumber.INTEREST, encoded_value) if need_final_name: - return ret, InterestPacketValue.name.get_final_name(markers['interest##inner_markers']) - else: - return ret + final_value = tlv_parse(InterestPacketValue, encoded_value) + return wire, final_value.name + return wire def make_data(name: NonStrictName, meta_info: MetaInfo, - content: BinaryStr | None = None, - signer: Signer | None = None) -> VarBinaryStr: - r""" - Make a Data packet. - - :param name: the Name field. - :type name: :any:`NonStrictName` - :param meta_info: the MetaIndo field. - :param content: the Content. - :type content: :class:`Optional` [ :any:`BinaryStr` ] - :param signer: a Signer to sign this Interest. ``None`` if it is unsigned. - :return: TLV encoded Data packet. - """ - data = DataPacket() - data.data = DataPacketValue() - data.data.meta_info = meta_info - data.data.name = name - data.data.content = content + content: Optional[BinaryStr] = None, + signer: Optional[Signer] = None) -> VarBinaryStr: + value = DataPacketValue(name=name, meta_info=meta_info, content=content) if signer is not None: - data.data.signature_info = SignatureInfo() - markers = {} - data._signer.set_arg(markers, signer) - ret = data.encode(markers=markers) - shrink_size = data.data._shrink_len.get_arg(markers['data##inner_markers']) - if shrink_size > 0: - ret = shrink_length(ret, shrink_size) - return ret + value.signature_info = SignatureInfo() + write_signature_info(signer, value.signature_info) + encoded_value = tlv_encode(value, markers={'##signer': signer}) + return _wrap_tlv(TypeNumber.DATA, encoded_value) def parse_interest(wire: BinaryStr, with_tl: bool = True) -> Interest: - r""" - Parse a TLV encoded Interest. - - :param wire: the buffer. - :type wire: :any:`BinaryStr` - :param with_tl: ``True`` if the packet has Type and Length. - ``False`` if ``wire`` only has the Value part. - :return: a Tuple of Name, InterestParameters, ApplicationParameters and :any:`SignaturePtrs`. - :rtype: :class:`Tuple` [ :any:`FormalName` , :any:`InterestParam` , - :class:`Optional` [ :any:`BinaryStr` ], :any:`SignaturePtrs` ] - """ - if with_tl: - wire = parse_and_check_tl(wire, TypeNumber.INTEREST) + value_wire = ( + parse_and_check_tl(wire, TypeNumber.INTEREST) + if with_tl else wire + ) markers = {} - ret = InterestPacketValue.parse(wire, markers) - params = InterestParam() - params.can_be_prefix = ret.can_be_prefix - params.must_be_fresh = ret.must_be_fresh - params.nonce = ret.nonce - params.lifetime = ret.lifetime - params.hop_limit = ret.hop_limit - - if ret.forwarding_hint and ret.forwarding_hint.delegations: - for cur in ret.forwarding_hint.delegations: - params.forwarding_hint.append((cur.preference, cur.delegation)) - + ret = tlv_parse(InterestPacketValue, value_wire, markers=markers) + params = InterestParam( + can_be_prefix=ret.can_be_prefix, + must_be_fresh=ret.must_be_fresh, + nonce=ret.nonce, + lifetime=ret.lifetime, + hop_limit=ret.hop_limit, + ) + if ret.forwarding_hint: + params.forwarding_hint.extend( + (item.preference, item.delegation) + for item in ret.forwarding_hint.delegations) + + digest_parts = [] + digest_start = markers.get('_digest_cover_start') + if digest_start is not None: + digest_parts.append(memoryview(value_wire)[digest_start:]) sig_ptrs = SignaturePtrs( signature_info=ret.signature_info, - signature_covered_part=ret._sig_cover_part.get_arg(markers), + signature_covered_part=markers.get('##sig_covered_part', []), signature_value_buf=ret.signature_value, - digest_covered_part=ret._digest_cover_part.get_arg(markers), - digest_value_buf=ret._digest_buf.get_arg(markers) + digest_covered_part=digest_parts, + digest_value_buf=markers.get('##digest_buf'), ) return ret.name, params, ret.application_parameters, sig_ptrs def parse_data(wire: BinaryStr, with_tl: bool = True) -> Data: - r""" - Parse a TLV encoded Data. - - :param wire: the buffer. - :type wire: :any:`BinaryStr` - :param with_tl: ``True`` if the packet has Type and Length. - ``False`` if ``wire`` only has the Value part. - :return: a Tuple of Name, MetaInfo, Content and :any:`SignaturePtrs`. - :rtype: :class:`Tuple` [ :any:`FormalName` , :any:`MetaInfo` , - :class:`Optional` [ :any:`BinaryStr` ], :any:`SignaturePtrs` ] - """ - if with_tl: - wire = parse_and_check_tl(wire, TypeNumber.DATA) + value_wire = parse_and_check_tl(wire, TypeNumber.DATA) if with_tl else wire markers = {} - ret = DataPacketValue.parse(wire, markers) - params = ret.meta_info + ret = tlv_parse(DataPacketValue, value_wire, markers=markers) + meta_info = ret.meta_info sig_ptrs = SignaturePtrs( signature_info=ret.signature_info, - signature_covered_part=ret._sig_cover_part.get_arg(markers), + signature_covered_part=markers.get('##sig_covered_part', []), signature_value_buf=ret.signature_value, ) - return ret.name, params, ret.content, sig_ptrs + return ret.name, meta_info, ret.content, sig_ptrs diff --git a/src/ndn/encoding/ndnlp_v2.py b/src/ndn/encoding/ndnlp_v2.py index 290c46a..35f7a1f 100644 --- a/src/ndn/encoding/ndnlp_v2.py +++ b/src/ndn/encoding/ndnlp_v2.py @@ -1,26 +1,21 @@ # ----------------------------------------------------------------------------- # Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# # Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. # ----------------------------------------------------------------------------- +"""NDNLPv2 models using the dataclass TLV API.""" +import dataclasses as dc +from typing import Optional + +from .tlv_model import DecodeError +from .tlv_model import tlv_encode, tlv_parse from .tlv_type import BinaryStr, VarBinaryStr from .tlv_var import parse_and_check_tl -from .tlv_model import TlvModel, UintField, BytesField, ModelField, BoolField, DecodeError -__all__ = ['LpTypeNumber', 'NackReason', 'parse_network_nack', 'make_network_nack', 'parse_lp_packet', - 'parse_lp_packet_v2'] +__all__ = [ + 'LpTypeNumber', 'NackReason', 'NetworkNack', 'CachePolicy', + 'LpPacketValue', 'LpPacket', 'parse_network_nack', 'make_network_nack', + 'parse_lp_packet', 'parse_lp_packet_v2', +] class LpTypeNumber: @@ -51,85 +46,88 @@ class NackReason: NO_ROUTE = 150 -class NetworkNack(TlvModel): - nack_reason = UintField(LpTypeNumber.NACK_REASON) - - -class CachePolicy(TlvModel): - cache_policy_type = UintField(LpTypeNumber.CACHE_POLICY_TYPE) - - -class LpPacketValue(TlvModel): - frag_index = UintField(LpTypeNumber.FRAG_INDEX) - frag_count = UintField(LpTypeNumber.FRAG_COUNT) - pit_token = BytesField(LpTypeNumber.PIT_TOKEN) - nack = ModelField(LpTypeNumber.NACK, NetworkNack) - incoming_face_id = UintField(LpTypeNumber.INCOMING_FACE_ID) - next_hop_face_id = UintField(LpTypeNumber.NEXT_HOP_FACE_ID) - cache_policy = ModelField(LpTypeNumber.CACHE_POLICY, CachePolicy) - congestion_mark = UintField(LpTypeNumber.CONGESTION_MARK) - tx_sequence = BytesField(LpTypeNumber.TX_SEQUENCE) - ack = BytesField(LpTypeNumber.ACK) - non_discovery = BoolField(LpTypeNumber.NON_DISCOVERY) - prefix_announcement = BytesField(LpTypeNumber.PREFIX_ANNOUNCEMENT) - - fragment = BytesField(LpTypeNumber.FRAGMENT) - - -class LpPacket(TlvModel): - lp_packet = ModelField(LpTypeNumber.LP_PACKET, LpPacketValue) - - -def parse_lp_packet(wire: BinaryStr, with_tl: bool = True) -> (int | None, BinaryStr | None): - """ - Parse an LpPacket, return NackReason (if exists) and the fragment. - - :param wire: an LpPacket. - :param with_tl: if `wire` has the TL fields. - :return: a tuple of NackReason and Fragment. - """ +@dc.dataclass +class NetworkNack: + nack_reason: Optional[int] = dc.field( + default=None, metadata={'tlv_type': LpTypeNumber.NACK_REASON}) + + +@dc.dataclass +class CachePolicy: + cache_policy_type: Optional[int] = dc.field( + default=None, metadata={'tlv_type': LpTypeNumber.CACHE_POLICY_TYPE}) + + +@dc.dataclass +class LpPacketValue: + frag_index: Optional[int] = dc.field( + default=None, metadata={'tlv_type': LpTypeNumber.FRAG_INDEX}) + frag_count: Optional[int] = dc.field( + default=None, metadata={'tlv_type': LpTypeNumber.FRAG_COUNT}) + pit_token: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': LpTypeNumber.PIT_TOKEN}) + nack: Optional[NetworkNack] = dc.field( + default=None, metadata={ + 'tlv_type': LpTypeNumber.NACK, 'ignore_critical': False}) + incoming_face_id: Optional[int] = dc.field( + default=None, metadata={'tlv_type': LpTypeNumber.INCOMING_FACE_ID}) + next_hop_face_id: Optional[int] = dc.field( + default=None, metadata={'tlv_type': LpTypeNumber.NEXT_HOP_FACE_ID}) + cache_policy: Optional[CachePolicy] = dc.field( + default=None, metadata={ + 'tlv_type': LpTypeNumber.CACHE_POLICY, 'ignore_critical': False}) + congestion_mark: Optional[int] = dc.field( + default=None, metadata={'tlv_type': LpTypeNumber.CONGESTION_MARK}) + tx_sequence: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': LpTypeNumber.TX_SEQUENCE}) + ack: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': LpTypeNumber.ACK}) + non_discovery: bool = dc.field( + default=False, metadata={'tlv_type': LpTypeNumber.NON_DISCOVERY}) + prefix_announcement: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': LpTypeNumber.PREFIX_ANNOUNCEMENT}) + fragment: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': LpTypeNumber.FRAGMENT}) + + +@dc.dataclass +class LpPacket: + lp_packet: Optional[LpPacketValue] = dc.field( + default=None, metadata={'tlv_type': LpTypeNumber.LP_PACKET}) + + +def parse_lp_packet(wire: BinaryStr, + with_tl: bool = True + ) -> tuple[Optional[int], Optional[BinaryStr]]: ret = parse_lp_packet_v2(wire, with_tl) - if ret.nack is not None: - return ret.nack.nack_reason, ret.fragment - else: - return None, ret.fragment + reason = ret.nack.nack_reason if ret.nack is not None else None + return reason, ret.fragment def parse_lp_packet_v2(wire: BinaryStr, with_tl: bool = True) -> LpPacketValue: - """ - Parse an LpPacket, return NackReason (if exists) and the fragment. - - :param wire: an LpPacket. - :param with_tl: if `wire` has the TL fields. - :return: LpPacketValue. - """ if with_tl: wire = parse_and_check_tl(wire, LpTypeNumber.LP_PACKET) - markers = {} - ret = LpPacketValue.parse(wire, markers, ignore_critical=True) - + ret = tlv_parse(LpPacketValue, wire, ignore_critical=True) if ret.frag_index is not None or ret.frag_count is not None: raise DecodeError('NDNLP fragmentation is not implemented yet.') - return ret -def parse_network_nack(wire: BinaryStr, with_tl: bool = True) -> (int | None, BinaryStr | None): +def parse_network_nack( + wire: BinaryStr, + with_tl: bool = True) -> tuple[Optional[int], Optional[BinaryStr]]: if with_tl: wire = parse_and_check_tl(wire, LpTypeNumber.LP_PACKET) - markers = {} - ret = LpPacketValue.parse(wire, markers, ignore_critical=True) - + ret = tlv_parse(LpPacketValue, wire, ignore_critical=True) if ret.nack is not None: return ret.nack.nack_reason, ret.fragment - else: - return None, None + return None, None -def make_network_nack(encoded_interest: BinaryStr, nack_reason: int) -> VarBinaryStr: - lp_packet = LpPacket() - lp_packet.lp_packet = LpPacketValue() - lp_packet.lp_packet.nack = NetworkNack() - lp_packet.lp_packet.nack.nack_reason = nack_reason - lp_packet.lp_packet.fragment = encoded_interest - return lp_packet.encode() +def make_network_nack(encoded_interest: BinaryStr, + nack_reason: int) -> VarBinaryStr: + value = LpPacketValue( + nack=NetworkNack(nack_reason=nack_reason), + fragment=encoded_interest, + ) + return tlv_encode(LpPacket(lp_packet=value)) diff --git a/tests/encoding/ndn_format_0_3_2017_test.py b/tests/encoding/ndn_format_0_3_2017_test.py new file mode 100644 index 0000000..e24c33e --- /dev/null +++ b/tests/encoding/ndn_format_0_3_2017_test.py @@ -0,0 +1,24 @@ +from ndn.encoding import Name +from ndn.encoding import ndn_format_0_3_2017 as fmt + + +def test_forwarding_hint_round_trip(): + wire = fmt.make_interest( + '/test', + fmt.InterestParam(forwarding_hint=[(1, '/hint')]), + ) + assert wire == bytes.fromhex( + '051b07060804746573741e0d1f0b1e01010706080468696e740c020fa0' + ) + + name, params, app_params, _ = fmt.parse_interest(wire) + assert name == Name.from_str('/test') + assert params.forwarding_hint == [(1, Name.from_str('/hint'))] + assert app_params is None + + +def test_missing_meta_info_is_preserved(): + name, meta_info, content, _ = fmt.parse_data(bytes.fromhex('06020700')) + assert name == [] + assert meta_info is None + assert content is None diff --git a/tests/encoding/ndn_format_0_3_test.py b/tests/encoding/ndn_format_0_3_test.py index 58d6e43..5470626 100644 --- a/tests/encoding/ndn_format_0_3_test.py +++ b/tests/encoding/ndn_format_0_3_test.py @@ -17,7 +17,7 @@ # ----------------------------------------------------------------------------- import hashlib import pytest -from ndn.security import DigestSha256Signer +from ndn.security import DigestSha256Signer, HmacSha256Signer from ndn.encoding import Name, Component, InterestParam, MetaInfo, ContentType, SignatureType, \ make_interest, make_data, parse_interest, parse_data, DecodeError, Signer, VarBinaryStr @@ -441,3 +441,72 @@ def test_none_meta_info(): _, meta_info, _, _ = parse_data(wire) assert meta_info is not None assert meta_info.content_type == ContentType.BLOB + +def test_default_interest_wire_format(): + wire = make_interest('/local/ndn/prefix', InterestParam()) + assert wire == ( + b'\x05\x1a\x07\x14\x08\x05local\x08\x03ndn\x08\x06prefix' + b'\x0c\x02\x0f\xa0' + ) + + name, params, app_params, sig = parse_interest(wire) + assert name == Name.from_str('/local/ndn/prefix') + assert params.lifetime == 4000 + assert app_params is None + assert sig.signature_info is None + + +def test_signed_interest_wire_format_and_coverage(): + wire = make_interest( + '/local/ndn/prefix', + InterestParam(nonce=0x6c211166), + b'\x01\x02\x03\x04', + DigestSha256Signer(), + ) + assert wire == ( + b'\x05\x6f\x07\x36\x08\x05local\x08\x03ndn\x08\x06prefix' + b'\x02 \x8e\x6e\x36\xd7\xea\xbc\xde\x43\x75\x61\x40\xc9' + b'\x0b\xda\x09\xd5' + b'\x00\xd2\xa5\x77\xf2\xf5\x33\xb5\x69\xf0\x44\x1d\xf0\xa7\xf9\xe2' + b'\x0a\x04\x6c\x21\x11\x66\x0c\x02\x0f\xa0' + b'\x24\x04\x01\x02\x03\x04\x2c\x03\x1b\x01\x00' + b'\x2e \xea\xa8\xf0\x99\x08\x63\x78\x95\x1d\xe0\x5f\xf1' + b'\xde\xbb\xc1\x18' + b'\xb5\x21\x8b\x2f\xca\xa0\xb5\x1d\x18\xfa\xbc\x29\xf5\x4d\x58\xff' + ) + + _, _, _, sig = parse_interest(wire) + assert sig.signature_info.signature_type == SignatureType.DIGEST_SHA256 + signature = hashlib.sha256(b''.join(sig.signature_covered_part)).digest() + digest = hashlib.sha256(b''.join(sig.digest_covered_part)).digest() + assert signature == sig.signature_value_buf + assert digest == sig.digest_value_buf + + +def test_data_wire_format_and_coverage(): + wire = make_data( + '/local/ndn/prefix', MetaInfo(), signer=DigestSha256Signer()) + assert wire == ( + b"\x06\x42\x07\x14\x08\x05local\x08\x03ndn\x08\x06prefix" + b"\x14\x03\x18\x01\x00\x16\x03\x1b\x01\x00" + b"\x17 \x7f1\xe4\t\xc5z/\x1d\r\xdaVh8\xfd\xd9\x94" + b"\xd8\'S\x13[\xd7\x15\xa5\x9d%^\x80\xf2\xab\xf0\xb5" + ) + + name, meta_info, content, sig = parse_data(wire) + assert name == Name.from_str('/local/ndn/prefix') + assert meta_info.content_type == ContentType.BLOB + assert content is None + signature = hashlib.sha256(b''.join(sig.signature_covered_part)).digest() + assert signature == sig.signature_value_buf + + +def test_key_locator_signer(): + signer = HmacSha256Signer('/local/KEY/1', b'secret') + data = make_data('/local/data', MetaInfo(), b'content', signer=signer) + _, _, _, sig = parse_data(data) + assert sig.signature_info.key_locator.name == Name.from_str('/local/KEY/1') + + interest = make_interest('/local/int', InterestParam(nonce=1), b'\x01', signer) + _, _, _, sig = parse_interest(interest) + assert sig.signature_info.key_locator.name == Name.from_str('/local/KEY/1') diff --git a/tests/encoding/ndnlp_v2_test.py b/tests/encoding/ndnlp_v2_test.py index 774907e..f451ac1 100644 --- a/tests/encoding/ndnlp_v2_test.py +++ b/tests/encoding/ndnlp_v2_test.py @@ -15,8 +15,23 @@ # See the License for the specific language governing permissions and # limitations under the License. # ----------------------------------------------------------------------------- -from ndn.encoding import parse_network_nack, parse_interest, make_network_nack, make_interest, \ - NackReason, Name, InterestParam +import pytest +from ndn.encoding import ( + DecodeError, + InterestParam, + LpPacketValue, + LpTypeNumber, + NackReason, + Name, + NetworkNack, + make_interest, + make_network_nack, + parse_interest, + parse_lp_packet_v2, + parse_network_nack, + tlv_encode, + write_tl_num, +) class TestNetworkNack: @@ -40,3 +55,44 @@ def test2(): assert lp_packet == (b"\x64\x36\xfd\x03\x20\x05\xfd\x03\x21\x01\x96" b"\x50\x2b\x05\x29\x07\x1f\x08\tlocalhost\x08\x03nfd\x08\x05faces\x08\x06events" b"\x21\x00\x12\x00\x0c\x02\x0f\xa0") + +def test_network_nack_wire_format(): + interest = make_interest( + '/localhost/nfd/faces/events', + InterestParam(must_be_fresh=True, can_be_prefix=True), + ) + lp_packet = make_network_nack(interest, NackReason.NO_ROUTE) + + assert lp_packet == ( + b"\x64\x36\xfd\x03\x20\x05\xfd\x03\x21\x01\x96" + b"\x50\x2b\x05\x29\x07\x1f\x08\tlocalhost\x08\x03nfd" + b"\x08\x05faces\x08\x06events\x21\x00\x12\x00\x0c\x02\x0f\xa0" + ) + + reason, encoded_interest = parse_network_nack(lp_packet) + name, params, _, _ = parse_interest(encoded_interest) + assert reason == NackReason.NO_ROUTE + assert name == Name.from_str('/localhost/nfd/faces/events') + assert params.can_be_prefix + assert params.must_be_fresh + + +def test_network_nack_parser_accepts_fragment_metadata(): + value = tlv_encode(LpPacketValue( + frag_index=0, + frag_count=1, + nack=NetworkNack(nack_reason=NackReason.NO_ROUTE), + fragment=b'\x05\x00', + )) + wire = bytearray(2 + len(value)) + offset = write_tl_num(LpTypeNumber.LP_PACKET, wire, 0) + offset += write_tl_num(len(value), wire, offset) + wire[offset:] = value + + assert parse_network_nack(wire) == (NackReason.NO_ROUTE, b'\x05\x00') + + +def test_nested_unknown_critical_field_is_rejected(): + wire = b'\x64\x06\xfd\x03\x20\x02\x01\x00' + with pytest.raises(DecodeError): + parse_lp_packet_v2(wire) diff --git a/tests/misc/security_v2_test.py b/tests/misc/security_v2_test.py new file mode 100644 index 0000000..b0333e3 --- /dev/null +++ b/tests/misc/security_v2_test.py @@ -0,0 +1,83 @@ +import dataclasses as dc +import hashlib +from datetime import UTC, datetime + +from ndn.app_support.security_v2 import ( + CertificateV2SignatureInfo, + CertificateV2Value, + ContentType, + SafeBag, + SecurityV2TypeNumber, + new_cert, + parse_certificate, +) +from ndn.encoding import ( + Component, + Name, + SignatureType, + tlv_encode, + tlv_parse, +) +from ndn.encoding.ndn_format_0_3 import parse_data +from ndn.security import DigestSha256Signer, HmacSha256Signer + + +def test_certificate_models_use_dataclass_tlv_format(): + assert dc.is_dataclass(CertificateV2SignatureInfo) + assert dc.is_dataclass(CertificateV2Value) + assert dc.is_dataclass(SafeBag) + + +def test_new_cert_round_trip(): + start = datetime(2025, 1, 2, 3, 4, 5, tzinfo=UTC) + end = datetime(2026, 2, 3, 4, 5, 6, tzinfo=UTC) + cert_name, wire = new_cert( + '/test/KEY/key-id', + Component.from_str('issuer'), + b'public-key', + DigestSha256Signer(), + start, + end, + ) + + cert = parse_certificate(wire) + assert cert.name == cert_name + assert cert.content == b'public-key' + assert cert.meta_info.content_type == ContentType.KEY + assert cert.meta_info.freshness_period == 3600000 + assert cert.signature_info.signature_type == SignatureType.DIGEST_SHA256 + assert cert.signature_info.validity_period.not_before == b'20250102T030405' + assert cert.signature_info.validity_period.not_after == b'20260203T040506' + assert Name.is_prefix(Name.from_str('/test/KEY/key-id'), cert_name) + + _, _, _, sig = parse_data(wire) + covered = b''.join(sig.signature_covered_part) + assert hashlib.sha256(covered).digest() == sig.signature_value_buf + + +def test_new_cert_converts_legacy_signer_key_locator(): + _, wire = new_cert( + '/test/KEY/key-id', + Component.from_str('issuer'), + b'public-key', + HmacSha256Signer('/signer/key', b'secret'), + datetime(2025, 1, 1, tzinfo=UTC), + datetime(2026, 1, 1, tzinfo=UTC), + ) + + cert = parse_certificate(wire) + assert cert.signature_info.key_locator.name == Name.from_str('/signer/key') + + +def test_safe_bag_round_trip(): + safe_bag = SafeBag(certificate_v2=b'\x06\x00', encrypted_key_bag=b'key') + wire = tlv_encode(safe_bag) + + assert wire == ( + bytes([0x06, 0x02, 0x06, 0x00]) + + bytes([SecurityV2TypeNumber.ENCRYPTED_KEY_BAG, 0x03]) + + b'key' + ) + parsed = tlv_parse(SafeBag, wire) + assert parsed.certificate_v2 == b'\x06\x00' + assert parsed.encrypted_key_bag == b'key' From 32f76fdb3148e52b7222d8af14630a7851ce8cd4 Mon Sep 17 00:00:00 2001 From: Xinyu Ma Date: Wed, 7 Oct 2026 23:28:44 -0700 Subject: [PATCH 3/8] app-support: migrate protocol models to dataclasses Co-authored-by: Cursor --- src/ndn/app_support/light_versec/binary.py | 146 +++++---- src/ndn/app_support/nfd_mgmt.py | 346 ++++++++++++--------- src/ndn/app_support/svs/sync.py | 4 +- src/ndn/app_support/svs/tlv.py | 38 ++- src/ndn/bin/nfdc/cmd_get_face.py | 11 +- src/ndn/bin/nfdc/cmd_get_route.py | 5 +- src/ndn/bin/nfdc/cmd_get_status.py | 3 +- src/ndn/bin/nfdc/cmd_get_strategy.py | 3 +- src/ndn/bin/nfdc/cmd_new_face.py | 2 +- src/ndn/bin/nfdc/cmd_remove_face.py | 7 +- tests/misc/light_versec_test.py | 9 + tests/misc/nfd_mgmt_test.py | 92 ++++++ tests/misc/svs_tlv_test.py | 44 +++ 13 files changed, 477 insertions(+), 233 deletions(-) create mode 100644 tests/misc/nfd_mgmt_test.py create mode 100644 tests/misc/svs_tlv_test.py diff --git a/src/ndn/app_support/light_versec/binary.py b/src/ndn/app_support/light_versec/binary.py index 8f88df8..58e760d 100644 --- a/src/ndn/app_support/light_versec/binary.py +++ b/src/ndn/app_support/light_versec/binary.py @@ -20,7 +20,11 @@ # See the License for the specific language governing permissions and # limitations under the License. # ----------------------------------------------------------------------------- -import ndn.encoding as enc +import dataclasses as dc +from typing import Optional + +from ...encoding import BinaryStr +from ...encoding.tlv_model import tlv_encode, tlv_parse __all__ = [ @@ -62,63 +66,101 @@ class TypeNumber: NAMED_PATTERN_NUM = 0x69 -class UserFnArg(enc.TlvModel): +@dc.dataclass +class UserFnArg: # A given component - value = enc.BytesField(TypeNumber.COMPONENT_VALUE) + value: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.COMPONENT_VALUE}) # Referring to a previous matched pattern - tag = enc.UintField(TypeNumber.PATTERN_TAG) + tag: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.PATTERN_TAG}) -class UserFnCall(enc.TlvModel): - fn_id = enc.BytesField(TypeNumber.USER_FN_ID, is_string=True) - args = enc.RepeatedField(enc.ModelField(TypeNumber.FN_ARGS, UserFnArg)) +@dc.dataclass +class UserFnCall: + fn_id: Optional[str] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.USER_FN_ID}) + args: list[UserFnArg] = dc.field( + default_factory=list, metadata={'tlv_type': TypeNumber.FN_ARGS}) -class ConstraintOption(enc.TlvModel): +@dc.dataclass +class ConstraintOption: # Equal to a given NameComponent value - value = enc.BytesField(TypeNumber.COMPONENT_VALUE) + value: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.COMPONENT_VALUE}) # Equal to another pattern - tag = enc.UintField(TypeNumber.PATTERN_TAG) + tag: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.PATTERN_TAG}) # Decide by a user function call - fn = enc.ModelField(TypeNumber.USER_FN_CALL, UserFnCall) - - -class PatternConstraint(enc.TlvModel): - options = enc.RepeatedField( - enc.ModelField(TypeNumber.CONS_OPTION, ConstraintOption) - ) - - -class PatternEdge(enc.TlvModel): - dest = enc.UintField(TypeNumber.NODE_ID) - tag = enc.UintField(TypeNumber.PATTERN_TAG) - cons_sets = enc.RepeatedField( - enc.ModelField(TypeNumber.CONSTRAINT, PatternConstraint) - ) - - -class ValueEdge(enc.TlvModel): - dest = enc.UintField(TypeNumber.NODE_ID) - value = enc.BytesField(TypeNumber.COMPONENT_VALUE) - - -class Node(enc.TlvModel): - id = enc.UintField(TypeNumber.NODE_ID) - parent = enc.UintField(TypeNumber.PARENT_ID) - rule_name = enc.RepeatedField(enc.BytesField(TypeNumber.IDENTIFIER, is_string=True)) - v_edges = enc.RepeatedField(enc.ModelField(TypeNumber.VALUE_EDGE, ValueEdge)) - p_edges = enc.RepeatedField(enc.ModelField(TypeNumber.PATTERN_EDGE, PatternEdge)) - sign_cons = enc.RepeatedField(enc.UintField(TypeNumber.KEY_NODE_ID)) - - -class TagSymbol(enc.TlvModel): - tag = enc.UintField(TypeNumber.PATTERN_TAG) - ident = enc.BytesField(TypeNumber.IDENTIFIER, is_string=True) - - -class LvsModel(enc.TlvModel): - version = enc.UintField(TypeNumber.VERSION) - start_id = enc.UintField(TypeNumber.NODE_ID) - named_pattern_cnt = enc.UintField(TypeNumber.NAMED_PATTERN_NUM) - nodes = enc.RepeatedField(enc.ModelField(TypeNumber.NODE, Node)) - symbols = enc.RepeatedField(enc.ModelField(TypeNumber.TAG_SYMBOL, TagSymbol)) + fn: Optional[UserFnCall] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.USER_FN_CALL}) + + +@dc.dataclass +class PatternConstraint: + options: list[ConstraintOption] = dc.field( + default_factory=list, metadata={'tlv_type': TypeNumber.CONS_OPTION}) + + +@dc.dataclass +class PatternEdge: + dest: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.NODE_ID}) + tag: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.PATTERN_TAG}) + cons_sets: list[PatternConstraint] = dc.field( + default_factory=list, metadata={'tlv_type': TypeNumber.CONSTRAINT}) + + +@dc.dataclass +class ValueEdge: + dest: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.NODE_ID}) + value: Optional[bytes] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.COMPONENT_VALUE}) + + +@dc.dataclass +class Node: + id: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.NODE_ID}) + parent: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.PARENT_ID}) + rule_name: list[str] = dc.field( + default_factory=list, metadata={'tlv_type': TypeNumber.IDENTIFIER}) + v_edges: list[ValueEdge] = dc.field( + default_factory=list, metadata={'tlv_type': TypeNumber.VALUE_EDGE}) + p_edges: list[PatternEdge] = dc.field( + default_factory=list, metadata={'tlv_type': TypeNumber.PATTERN_EDGE}) + sign_cons: list[int] = dc.field( + default_factory=list, metadata={'tlv_type': TypeNumber.KEY_NODE_ID}) + + +@dc.dataclass +class TagSymbol: + tag: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.PATTERN_TAG}) + ident: Optional[str] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.IDENTIFIER}) + + +@dc.dataclass +class LvsModel: + version: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.VERSION}) + start_id: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.NODE_ID}) + named_pattern_cnt: Optional[int] = dc.field( + default=None, metadata={'tlv_type': TypeNumber.NAMED_PATTERN_NUM}) + nodes: list[Node] = dc.field( + default_factory=list, metadata={'tlv_type': TypeNumber.NODE}) + symbols: list[TagSymbol] = dc.field( + default_factory=list, metadata={'tlv_type': TypeNumber.TAG_SYMBOL}) + + def encode(self) -> bytearray: + return tlv_encode(self) + + @classmethod + def parse(cls, wire: BinaryStr) -> 'LvsModel': + return tlv_parse(cls, wire) diff --git a/src/ndn/app_support/nfd_mgmt.py b/src/ndn/app_support/nfd_mgmt.py index a7b6989..4119df4 100644 --- a/src/ndn/app_support/nfd_mgmt.py +++ b/src/ndn/app_support/nfd_mgmt.py @@ -15,13 +15,17 @@ # See the License for the specific language governing permissions and # limitations under the License. # ----------------------------------------------------------------------------- +"""NFD management protocol models using the dataclass TLV API.""" +import dataclasses as dc import struct +from typing import Optional from enum import Enum, Flag + from ..transport.face import Face from ..utils import timestamp, gen_nonce_64 -from ..encoding import Component, Name, ModelField, TlvModel, NameField, UintField, BytesField, \ - SignatureInfo, get_tl_num_size, TypeNumber, write_tl_num, parse_and_check_tl, \ - RepeatedField +from ..encoding import Component, Name, get_tl_num_size, write_tl_num, parse_and_check_tl +from ..encoding.tlv_model import NDNName, tlv_encode, tlv_parse +from ..encoding.ndn_format_0_3 import SignatureInfo, TypeNumber, write_signature_info from ..security import DigestSha256Signer @@ -62,172 +66,214 @@ class FaceEventKind(Enum): DOWN = 4 -class Strategy(TlvModel): - name = NameField() - - -class ControlParametersValue(TlvModel): - name = NameField() - face_id = UintField(0x69) - uri = BytesField(0x72, is_string=True) - local_uri = BytesField(0x81, is_string=True) - origin = UintField(0x6f) - cost = UintField(0x6a) - capacity = UintField(0x83) - count = UintField(0x84) - base_congestion_mark_interval = UintField(0x87) - default_congestion_threshold = UintField(0x88) - mtu = UintField(0x89) - flags = UintField(0x6c) - mask = UintField(0x70) - strategy = ModelField(0x6b, Strategy) - expiration_period = UintField(0x6d) - face_persistency = UintField(0x85, val_base_type=FacePersistency) - - -class ControlParameters(TlvModel): - cp = ModelField(0x68, ControlParametersValue) - - -class ControlResponse(TlvModel): - status_code = UintField(0x66) - status_text = BytesField(0x67, is_string=True) - body = ModelField(0x68, ControlParametersValue) - - -class FaceEventNotificationValue(TlvModel): - face_event_kind = UintField(0xc1, val_base_type=FaceEventKind) - face_id = UintField(0x69) - uri = BytesField(0x72, is_string=True) - local_uri = BytesField(0x81, is_string=True) - face_scope = UintField(0x84, val_base_type=FaceScope) - face_persistency = UintField(0x85, val_base_type=FacePersistency) - link_type = UintField(0x86, val_base_type=FaceLinkType) - flags = UintField(0x6c, val_base_type=FaceFlags) - - -class FaceEventNotification(TlvModel): - event = ModelField(0xc0, FaceEventNotificationValue) - - -class GeneralStatus(TlvModel): - nfd_version = BytesField(0x80, is_string=True) - start_timestamp = UintField(0x81) - current_timestamp = UintField(0x82) - n_name_tree_entries = UintField(0x83) - n_fib_entries = UintField(0x84) - n_pit_entries = UintField(0x85) - n_measurement_entries = UintField(0x86) - n_cs_entries = UintField(0x87) - n_in_interests = UintField(0x90) - n_in_data = UintField(0x91) - n_in_nacks = UintField(0x97) - n_out_interests = UintField(0x92) - n_out_data = UintField(0x93) - n_out_nacks = UintField(0x98) - n_satisfied_interests = UintField(0x99) - n_unsatisfied_interests = UintField(0x9a) +__all__ = [ + 'FaceScope', 'FacePersistency', 'FaceLinkType', 'FaceFlags', 'RouteFlags', 'FaceEventKind', + 'Strategy', 'ControlParametersValue', 'ControlParameters', 'ControlResponse', + 'FaceEventNotificationValue', 'FaceEventNotification', 'GeneralStatus', 'FaceStatus', + 'FaceStatusMsg', 'FaceQueryFilterValue', 'FaceQueryFilter', 'Route', 'RibEntry', 'RibStatus', + 'NextHopRecord', 'FibEntry', 'FibStatus', 'StrategyChoice', 'StrategyChoiceMsg', 'CsInfo', + 'make_command', 'make_command_v2', 'parse_response', +] + + +def _tlv(type_num: int): + return dc.field(default=None, metadata={'tlv_type': type_num}) + + +def _name(): + return _tlv(TypeNumber.NAME) + + +def _repeated(type_num: int): + return dc.field(default_factory=list, metadata={'tlv_type': type_num}) + + +@dc.dataclass +class Strategy: + name: NDNName = _name() + + +@dc.dataclass +class ControlParametersValue: + name: NDNName = _name() + face_id: Optional[int] = _tlv(0x69) + uri: Optional[str] = _tlv(0x72) + local_uri: Optional[str] = _tlv(0x81) + origin: Optional[int] = _tlv(0x6f) + cost: Optional[int] = _tlv(0x6a) + capacity: Optional[int] = _tlv(0x83) + count: Optional[int] = _tlv(0x84) + base_congestion_mark_interval: Optional[int] = _tlv(0x87) + default_congestion_threshold: Optional[int] = _tlv(0x88) + mtu: Optional[int] = _tlv(0x89) + flags: Optional[int] = _tlv(0x6c) + mask: Optional[int] = _tlv(0x70) + strategy: Optional[Strategy] = _tlv(0x6b) + expiration_period: Optional[int] = _tlv(0x6d) + face_persistency: Optional[FacePersistency] = _tlv(0x85) + + +@dc.dataclass +class ControlParameters: + cp: Optional[ControlParametersValue] = _tlv(0x68) + + +@dc.dataclass +class ControlResponse: + status_code: Optional[int] = _tlv(0x66) + status_text: Optional[str] = _tlv(0x67) + body: Optional[ControlParametersValue] = _tlv(0x68) + + +@dc.dataclass +class FaceEventNotificationValue: + face_event_kind: Optional[FaceEventKind] = _tlv(0xc1) + face_id: Optional[int] = _tlv(0x69) + uri: Optional[str] = _tlv(0x72) + local_uri: Optional[str] = _tlv(0x81) + face_scope: Optional[FaceScope] = _tlv(0x84) + face_persistency: Optional[FacePersistency] = _tlv(0x85) + link_type: Optional[FaceLinkType] = _tlv(0x86) + flags: Optional[FaceFlags] = _tlv(0x6c) + + +@dc.dataclass +class FaceEventNotification: + event: Optional[FaceEventNotificationValue] = _tlv(0xc0) + + +@dc.dataclass +class GeneralStatus: + nfd_version: Optional[str] = _tlv(0x80) + start_timestamp: Optional[int] = _tlv(0x81) + current_timestamp: Optional[int] = _tlv(0x82) + n_name_tree_entries: Optional[int] = _tlv(0x83) + n_fib_entries: Optional[int] = _tlv(0x84) + n_pit_entries: Optional[int] = _tlv(0x85) + n_measurement_entries: Optional[int] = _tlv(0x86) + n_cs_entries: Optional[int] = _tlv(0x87) + n_in_interests: Optional[int] = _tlv(0x90) + n_in_data: Optional[int] = _tlv(0x91) + n_in_nacks: Optional[int] = _tlv(0x97) + n_out_interests: Optional[int] = _tlv(0x92) + n_out_data: Optional[int] = _tlv(0x93) + n_out_nacks: Optional[int] = _tlv(0x98) + n_satisfied_interests: Optional[int] = _tlv(0x99) + n_unsatisfied_interests: Optional[int] = _tlv(0x9a) # The following comes from DNMP's extension to NFD mgmt protocol: # https://github.com/pollere/DNMP-v2/blob/c4359ae1af03824ec1ee8cd27a7d52c9151fa813/formats/forwarder-status.proto # It does not show up in the standard protocol: # https://redmine.named-data.net/projects/nfd/wiki/ForwarderStatus - n_fragmentation_errors = UintField(0xc8) - n_out_over_mtu = UintField(0xc9) - n_in_lp_invalid = UintField(0xca) - n_reassembly_timeouts = UintField(0xcb) - n_in_net_invalid = UintField(0xcc) - n_acknowledged = UintField(0xcd) - n_retransmitted = UintField(0xce) - n_retx_exhausted = UintField(0xcf) - n_congestion_marked = UintField(0xd0) - + n_fragmentation_errors: Optional[int] = _tlv(0xc8) + n_out_over_mtu: Optional[int] = _tlv(0xc9) + n_in_lp_invalid: Optional[int] = _tlv(0xca) + n_reassembly_timeouts: Optional[int] = _tlv(0xcb) + n_in_net_invalid: Optional[int] = _tlv(0xcc) + n_acknowledged: Optional[int] = _tlv(0xcd) + n_retransmitted: Optional[int] = _tlv(0xce) + n_retx_exhausted: Optional[int] = _tlv(0xcf) + n_congestion_marked: Optional[int] = _tlv(0xd0) + -class FaceStatus(TlvModel): - face_id = UintField(0x69) - uri = BytesField(0x72, is_string=True) - local_uri = BytesField(0x81, is_string=True) - expiration_period = UintField(0x6d) - face_scope = UintField(0x84, val_base_type=FaceScope) - face_persistency = UintField(0x85, val_base_type=FacePersistency) - link_type = UintField(0x86, val_base_type=FaceLinkType) - base_congestion_mark_interval = UintField(0x87) - default_congestion_threshold = UintField(0x88) - mtu = UintField(0x89) - n_in_interests = UintField(0x90) - n_in_data = UintField(0x91) - n_in_nacks = UintField(0x97) - n_out_interests = UintField(0x92) - n_out_data = UintField(0x93) - n_out_nacks = UintField(0x98) - n_in_bytes = UintField(0x94) - n_out_bytes = UintField(0x95) - flags = UintField(0x6c, val_base_type=FaceFlags) +@dc.dataclass +class FaceStatus: + face_id: Optional[int] = _tlv(0x69) + uri: Optional[str] = _tlv(0x72) + local_uri: Optional[str] = _tlv(0x81) + expiration_period: Optional[int] = _tlv(0x6d) + face_scope: Optional[FaceScope] = _tlv(0x84) + face_persistency: Optional[FacePersistency] = _tlv(0x85) + link_type: Optional[FaceLinkType] = _tlv(0x86) + base_congestion_mark_interval: Optional[int] = _tlv(0x87) + default_congestion_threshold: Optional[int] = _tlv(0x88) + mtu: Optional[int] = _tlv(0x89) + n_in_interests: Optional[int] = _tlv(0x90) + n_in_data: Optional[int] = _tlv(0x91) + n_in_nacks: Optional[int] = _tlv(0x97) + n_out_interests: Optional[int] = _tlv(0x92) + n_out_data: Optional[int] = _tlv(0x93) + n_out_nacks: Optional[int] = _tlv(0x98) + n_in_bytes: Optional[int] = _tlv(0x94) + n_out_bytes: Optional[int] = _tlv(0x95) + flags: Optional[FaceFlags] = _tlv(0x6c) -class FaceStatusMsg(TlvModel): - face_status = RepeatedField(ModelField(0x80, FaceStatus)) +@dc.dataclass +class FaceStatusMsg: + face_status: list[FaceStatus] = _repeated(0x80) -class FaceQueryFilterValue(TlvModel): - face_id = UintField(0x69) - uri_scheme = BytesField(0x83, is_string=True) - uri = BytesField(0x72, is_string=True) - local_uri = BytesField(0x81, is_string=True) - face_scope = UintField(0x84, val_base_type=FaceScope) - face_persistency = UintField(0x85, val_base_type=FacePersistency) - link_type = UintField(0x86, val_base_type=FaceLinkType) +@dc.dataclass +class FaceQueryFilterValue: + face_id: Optional[int] = _tlv(0x69) + uri_scheme: Optional[str] = _tlv(0x83) + uri: Optional[str] = _tlv(0x72) + local_uri: Optional[str] = _tlv(0x81) + face_scope: Optional[FaceScope] = _tlv(0x84) + face_persistency: Optional[FacePersistency] = _tlv(0x85) + link_type: Optional[FaceLinkType] = _tlv(0x86) -class FaceQueryFilter(TlvModel): - face_query_filter = ModelField(0x96, FaceQueryFilterValue) +@dc.dataclass +class FaceQueryFilter: + face_query_filter: Optional[FaceQueryFilterValue] = _tlv(0x96) -class Route(TlvModel): - face_id = UintField(0x69) - origin = UintField(0x6f) - cost = UintField(0x6a) - flags = UintField(0x6c, val_base_type=RouteFlags) - expiration_period = UintField(0x6d) +@dc.dataclass +class Route: + face_id: Optional[int] = _tlv(0x69) + origin: Optional[int] = _tlv(0x6f) + cost: Optional[int] = _tlv(0x6a) + flags: Optional[RouteFlags] = _tlv(0x6c) + expiration_period: Optional[int] = _tlv(0x6d) -class RibEntry(TlvModel): - name = NameField() - routes = RepeatedField(ModelField(0x81, Route)) +@dc.dataclass +class RibEntry: + name: NDNName = _name() + routes: list[Route] = _repeated(0x81) -class RibStatus(TlvModel): - entries = RepeatedField(ModelField(0x80, RibEntry)) +@dc.dataclass +class RibStatus: + entries: list[RibEntry] = _repeated(0x80) -class NextHopRecord(TlvModel): - face_id = UintField(0x69) - cost = UintField(0x6a) +@dc.dataclass +class NextHopRecord: + face_id: Optional[int] = _tlv(0x69) + cost: Optional[int] = _tlv(0x6a) -class FibEntry(TlvModel): - name = NameField() - next_hop_records = RepeatedField(ModelField(0x81, NextHopRecord)) +@dc.dataclass +class FibEntry: + name: NDNName = _name() + next_hop_records: list[NextHopRecord] = _repeated(0x81) -class FibStatus(TlvModel): - entries = RepeatedField(ModelField(0x80, FibEntry)) +@dc.dataclass +class FibStatus: + entries: list[FibEntry] = _repeated(0x80) -class StrategyChoice(TlvModel): - name = NameField() - strategy = ModelField(0x6b, Strategy) +@dc.dataclass +class StrategyChoice: + name: NDNName = _name() + strategy: Optional[Strategy] = _tlv(0x6b) -class StrategyChoiceMsg(TlvModel): - strategy_choices = RepeatedField(ModelField(0x80, StrategyChoice)) +@dc.dataclass +class StrategyChoiceMsg: + strategy_choices: list[StrategyChoice] = _repeated(0x80) -class CsInfo(TlvModel): - capacity = UintField(0x83) - flags = UintField(0x6c) - n_cs_entries = UintField(0x87) - n_hits = UintField(0x81) - n_misses = UintField(0x82) +@dc.dataclass +class CsInfo: + capacity: Optional[int] = _tlv(0x83) + flags: Optional[int] = _tlv(0x6c) + n_cs_entries: Optional[int] = _tlv(0x87) + n_hits: Optional[int] = _tlv(0x81) + n_misses: Optional[int] = _tlv(0x82) def make_command(module, command, face: Face | None = None, **kwargs): @@ -240,8 +286,8 @@ def make_command(module, command, face: Face | None = None, **kwargs): # SignatureInfo signer = DigestSha256Signer() sig_info = SignatureInfo() - signer.write_signature_info(sig_info) - buf = sig_info.encode() + write_signature_info(signer, sig_info) + buf = tlv_encode(sig_info) ret.append(Component.from_bytes(bytes([TypeNumber.SIGNATURE_INFO, len(buf)]) + buf)) # SignatureValue @@ -268,28 +314,26 @@ def make_command_v2(module, command, face: Face | None = None, **kwargs): else: ret = Name.from_str(f"/localhop/nfd/{module}/{command}") # Command parameters - cp = ControlParameters() - cp.cp = ControlParametersValue() + cp = ControlParameters(cp=ControlParametersValue()) for k, v in kwargs.items(): if k == 'strategy': - cp.cp.strategy = Strategy() - cp.cp.strategy.name = v + cp.cp.strategy = Strategy(name=v) else: setattr(cp.cp, k, v) - ret.append(Component.from_bytes(cp.encode())) + ret.append(Component.from_bytes(tlv_encode(cp))) return ret def parse_response(buf): buf = parse_and_check_tl(memoryview(buf), 0x65) - cr = ControlResponse.parse(buf) + cr = tlv_parse(ControlResponse, buf) ret = {} ret['status_code'] = cr.status_code ret['status_text'] = cr.status_text params = cr.body - for k in ControlParametersValue._encoded_fields: - val = getattr(params, k.name) + for f in dc.fields(ControlParametersValue): + val = getattr(params, f.name) if params is not None else None if isinstance(val, memoryview): val = bytes(val) - ret[k.name] = val + ret[f.name] = val return ret diff --git a/src/ndn/app_support/svs/sync.py b/src/ndn/app_support/svs/sync.py index 9012326..a143fd9 100644 --- a/src/ndn/app_support/svs/sync.py +++ b/src/ndn/app_support/svs/sync.py @@ -108,7 +108,7 @@ def sync_handler(self, name: enc.FormalName, _app_param: enc.BinaryStr | None, self.logger.error('Received invalid Sync Interest: %s', enc.Name.to_str(name)) return try: - remote_sv_pkt = StateVecWrapper.parse(name[-2]).val + remote_sv_pkt = enc.tlv_parse(StateVecWrapper, name[-2]).val except (enc.DecodeError, IndexError) as e: self.logger.error('Unable to decode state vector [%s]: %s', enc.Name.to_str(name), e) return @@ -203,7 +203,7 @@ def express_sync_interest(self): cur.node_id = enc.Name.from_bytes(lsv_id) cur.seq_no = lsv_seq sv_pkt.val.entries.append(cur) - sync_name = self.base_prefix + [sv_pkt.encode()] + sync_name = self.base_prefix + [enc.tlv_encode(sv_pkt)] self.ndn_app.express(sync_name, app.pass_all, signer=self.int_signer, no_response=True) def new_data(self): diff --git a/src/ndn/app_support/svs/tlv.py b/src/ndn/app_support/svs/tlv.py index bb7146a..5d08918 100644 --- a/src/ndn/app_support/svs/tlv.py +++ b/src/ndn/app_support/svs/tlv.py @@ -15,34 +15,42 @@ # See the License for the specific language governing permissions and # limitations under the License. # ----------------------------------------------------------------------------- +import dataclasses as dc +from typing import Optional from ... import encoding as enc __all__ = ['StateVecEntry', 'StateVec', 'StateVecWrapper', 'MappingEntry', 'MappingData', 'MappingDataWrapper'] -class StateVecEntry(enc.TlvModel): - node_id = enc.NameField() - seq_no = enc.UintField(0xcc) +@dc.dataclass +class StateVecEntry: + node_id: enc.NDNName = dc.field(default=None, metadata={'tlv_type': enc.Name.TYPE_NAME}) + seq_no: Optional[int] = dc.field(default=None, metadata={'tlv_type': 0xcc}) -class StateVec(enc.TlvModel): - entries = enc.RepeatedField(enc.ModelField(0xca, StateVecEntry)) +@dc.dataclass +class StateVec: + entries: list[StateVecEntry] = dc.field(default_factory=list, metadata={'tlv_type': 0xca}) -class StateVecWrapper(enc.TlvModel): - val = enc.ModelField(0xc9, StateVec) +@dc.dataclass +class StateVecWrapper: + val: Optional[StateVec] = dc.field(default=None, metadata={'tlv_type': 0xc9}) -class MappingEntry(enc.TlvModel): - seq_no = enc.UintField(0xcc) - app_name = enc.NameField() +@dc.dataclass +class MappingEntry: + seq_no: Optional[int] = dc.field(default=None, metadata={'tlv_type': 0xcc}) + app_name: enc.NDNName = dc.field(default=None, metadata={'tlv_type': enc.Name.TYPE_NAME}) -class MappingData(enc.TlvModel): - node_id = enc.NameField() - entries = enc.ModelField(0xce, MappingEntry) +@dc.dataclass +class MappingData: + node_id: enc.NDNName = dc.field(default=None, metadata={'tlv_type': enc.Name.TYPE_NAME}) + entries: Optional[MappingEntry] = dc.field(default=None, metadata={'tlv_type': 0xce}) -class MappingDataWrapper(enc.TlvModel): - val = enc.ModelField(0xcd, MappingEntry) +@dc.dataclass +class MappingDataWrapper: + val: Optional[MappingEntry] = dc.field(default=None, metadata={'tlv_type': 0xcd}) diff --git a/src/ndn/bin/nfdc/cmd_get_face.py b/src/ndn/bin/nfdc/cmd_get_face.py index 2da16c3..79d090e 100644 --- a/src/ndn/bin/nfdc/cmd_get_face.py +++ b/src/ndn/bin/nfdc/cmd_get_face.py @@ -18,6 +18,7 @@ import argparse from ...appv2 import NDNApp from ...encoding import Name, Component +from ...encoding.tlv_model import tlv_encode, tlv_parse from ...app_support.nfd_mgmt import FaceStatusMsg, FaceQueryFilter, FaceQueryFilterValue, parse_response from .utils import express_interest @@ -35,7 +36,7 @@ def execute(args: argparse.Namespace): async def list_face(): try: data = await express_interest(app, "/localhost/nfd/faces/list") - msg = FaceStatusMsg.parse(data) + msg = tlv_parse(FaceStatusMsg, data) # TODO: Should calculate the length instead of using a fixed number print(f'{"FaceID":7}{"RemoteURI":<30}\t{"LocalURI":<30}') print(f'{"------":7}{"---------":<30}\t{"--------":<30}') @@ -53,7 +54,7 @@ async def exec_query(): msg = parse_response(data) print('Query failed with response', msg['status_code'], msg['status_text']) else: - msg = FaceStatusMsg.parse(data) + msg = tlv_parse(FaceStatusMsg, data) for f in msg.face_status: print() print(f'{"Face ID":>12}\t{f.face_id}') @@ -78,16 +79,16 @@ async def exec_query(): filt.face_query_filter = FaceQueryFilterValue() if face_id is not None: filt.face_query_filter.face_id = face_id - data_name = Name.from_str(name) + [Component.from_bytes(filt.encode())] + data_name = Name.from_str(name) + [Component.from_bytes(tlv_encode(filt))] if not await exec_query(): print('No face is found') else: filt.face_query_filter.uri = face_uri - data_name = Name.from_str(name) + [Component.from_bytes(filt.encode())] + data_name = Name.from_str(name) + [Component.from_bytes(tlv_encode(filt))] if not await exec_query(): filt.face_query_filter.uri = None filt.face_query_filter.local_uri = face_uri - data_name = Name.from_str(name) + [Component.from_bytes(filt.encode())] + data_name = Name.from_str(name) + [Component.from_bytes(tlv_encode(filt))] if not await exec_query(): print('No face is found') app.shutdown() diff --git a/src/ndn/bin/nfdc/cmd_get_route.py b/src/ndn/bin/nfdc/cmd_get_route.py index 91acebe..b14af2f 100644 --- a/src/ndn/bin/nfdc/cmd_get_route.py +++ b/src/ndn/bin/nfdc/cmd_get_route.py @@ -18,6 +18,7 @@ import argparse from ...appv2 import NDNApp from ...encoding import Name +from ...encoding.tlv_model import tlv_parse from ...app_support.nfd_mgmt import FibStatus, RibStatus from .utils import express_interest @@ -36,9 +37,9 @@ def execute(args: argparse.Namespace): async def list_route(): try: fib_data = await express_interest(app, "/localhost/nfd/fib/list") - fib_msg = FibStatus.parse(fib_data) + fib_msg = tlv_parse(FibStatus, fib_data) rib_data = await express_interest(app, "/localhost/nfd/rib/list") - rib_msg = RibStatus.parse(rib_data) + rib_msg = tlv_parse(RibStatus, rib_data) # TODO: Should calculate the length instead of using a fixed number print('Forwarding Table (FIB)') for ent in fib_msg.entries: diff --git a/src/ndn/bin/nfdc/cmd_get_status.py b/src/ndn/bin/nfdc/cmd_get_status.py index e1273d1..babc3f7 100644 --- a/src/ndn/bin/nfdc/cmd_get_status.py +++ b/src/ndn/bin/nfdc/cmd_get_status.py @@ -18,6 +18,7 @@ import argparse import datetime from ...appv2 import NDNApp +from ...encoding.tlv_model import tlv_parse from ...app_support.nfd_mgmt import GeneralStatus from .utils import express_interest @@ -34,7 +35,7 @@ async def after_start(): try: data = await express_interest(app, "/localhost/nfd/status/general") - msg = GeneralStatus.parse(data) + msg = tlv_parse(GeneralStatus, data) print('General status:') print(f'{"version":>25}\t{msg.nfd_version}') diff --git a/src/ndn/bin/nfdc/cmd_get_strategy.py b/src/ndn/bin/nfdc/cmd_get_strategy.py index 03c0f57..c523139 100644 --- a/src/ndn/bin/nfdc/cmd_get_strategy.py +++ b/src/ndn/bin/nfdc/cmd_get_strategy.py @@ -18,6 +18,7 @@ import argparse from ...appv2 import NDNApp from ...encoding import Name +from ...encoding.tlv_model import tlv_parse from ...app_support.nfd_mgmt import StrategyChoiceMsg from .utils import express_interest @@ -36,7 +37,7 @@ def execute(args: argparse.Namespace): async def list_strategy(): try: data = await express_interest(app, "/localhost/nfd/strategy-choice/list") - msg = StrategyChoiceMsg.parse(data) + msg = tlv_parse(StrategyChoiceMsg, data) for s in msg.strategy_choices: s_prefix = Name.to_str(s.name) if prefix and s_prefix != prefix: diff --git a/src/ndn/bin/nfdc/cmd_new_face.py b/src/ndn/bin/nfdc/cmd_new_face.py index f622365..0ef64f8 100644 --- a/src/ndn/bin/nfdc/cmd_new_face.py +++ b/src/ndn/bin/nfdc/cmd_new_face.py @@ -38,7 +38,7 @@ def execute(args: argparse.Namespace): uri = uri + ":6363" async def create_face(): - cmd = make_command_v2('faces', 'create', uri=uri.encode()) + cmd = make_command_v2('faces', 'create', uri=uri) res = await express_interest(app, cmd) msg = parse_response(res) print(f'{msg["status_code"]} {msg["status_text"]}') diff --git a/src/ndn/bin/nfdc/cmd_remove_face.py b/src/ndn/bin/nfdc/cmd_remove_face.py index b615071..9ddf141 100644 --- a/src/ndn/bin/nfdc/cmd_remove_face.py +++ b/src/ndn/bin/nfdc/cmd_remove_face.py @@ -18,6 +18,7 @@ import argparse from ...appv2 import NDNApp from ...encoding import Name, Component +from ...encoding.tlv_model import tlv_encode, tlv_parse from ...app_support.nfd_mgmt import FaceStatusMsg, FaceQueryFilter, FaceQueryFilterValue, parse_response, \ make_command_v2 from .utils import express_interest @@ -56,7 +57,7 @@ async def try_remove(): msg = parse_response(data) print('Query failed with response', msg['status_code'], msg['status_text']) else: - msg = FaceStatusMsg.parse(data) + msg = tlv_parse(FaceStatusMsg, data) for f in msg.face_status: await remove_face(f.face_id) return True @@ -66,11 +67,11 @@ async def try_remove(): filt = FaceQueryFilter() filt.face_query_filter = FaceQueryFilterValue() filt.face_query_filter.uri = uri - data_name = Name.from_str(name) + [Component.from_bytes(filt.encode())] + data_name = Name.from_str(name) + [Component.from_bytes(tlv_encode(filt))] if not await try_remove(): filt.face_query_filter.uri = None filt.face_query_filter.local_uri = uri - data_name = Name.from_str(name) + [Component.from_bytes(filt.encode())] + data_name = Name.from_str(name) + [Component.from_bytes(tlv_encode(filt))] if not await try_remove(): print('No face is found') finally: diff --git a/tests/misc/light_versec_test.py b/tests/misc/light_versec_test.py index df95d5c..bea990e 100644 --- a/tests/misc/light_versec_test.py +++ b/tests/misc/light_versec_test.py @@ -34,6 +34,15 @@ def test_compile(): checker = Checker(compile_lvs(lvs), {}) assert checker.validate_user_fns() + @staticmethod + def test_binary_round_trip(): + model = compile_lvs('#rule: "a"/b/"c"') + wire = bytes(model.encode()) + restored = type(model).parse(wire) + + assert bytes(restored.encode()) == wire + assert Checker(restored, {}).validate_user_fns() + @staticmethod def test_temp_identifiers(): lvs = r''' diff --git a/tests/misc/nfd_mgmt_test.py b/tests/misc/nfd_mgmt_test.py new file mode 100644 index 0000000..67399a8 --- /dev/null +++ b/tests/misc/nfd_mgmt_test.py @@ -0,0 +1,92 @@ +from ndn.app_support import nfd_mgmt +from ndn.encoding import Name, tlv_encode, tlv_parse + + +def test_make_command_wire_format(monkeypatch): + monkeypatch.setattr(nfd_mgmt, 'timestamp', lambda: 1234567) + monkeypatch.setattr(nfd_mgmt, 'gen_nonce_64', lambda: 0xdeadbeef) + + command = nfd_mgmt.make_command( + 'faces', + 'create', + uri='udp4://127.0.0.1:6363', + ) + assert Name.to_bytes(command) == bytes.fromhex( + '077908096c6f63616c686f737408036e6664080566616365730806637265617465' + '081968177215756470343a2f2f3132372e302e302e313a36333633080800000000' + '0012d687080800000000deadbeef080516031b010008221720fba73d0533f977a6' + '343e39fb147118e397d9a17dbfeb7f1843ecfe903908082a' + ) + + +def test_make_command_v2_wire_format(): + command = nfd_mgmt.make_command_v2( + 'rib', + 'register', + name='/example/prefix', + face_id=300, + origin=65, + cost=10, + flags=1, + expiration_period=3600000, + face_persistency=nfd_mgmt.FacePersistency.PERMANENT, + ) + assert Name.to_bytes(command) == bytes.fromhex( + '074c08096c6f63616c686f737408036e6664080372696208087265676973746572' + '082b6829071108076578616d706c6508067072656669786902012c6f01416a010a' + '6c01016d040036ee80850102' + ) + + +def test_parse_response_round_trip(): + response = nfd_mgmt.ControlResponse( + status_code=200, + status_text='OK', + body=nfd_mgmt.ControlParametersValue( + name='/example', + face_id=5, + uri='udp4://1.2.3.4:6363', + face_persistency=nfd_mgmt.FacePersistency.ON_DEMAND, + ), + ) + body = bytes(tlv_encode(response)) + parsed = nfd_mgmt.parse_response(bytes([0x65, len(body)]) + body) + + assert parsed['status_code'] == 200 + assert parsed['status_text'] == 'OK' + assert Name.to_str(parsed['name']) == '/example' + assert parsed['face_id'] == 5 + assert parsed['face_persistency'] is nfd_mgmt.FacePersistency.ON_DEMAND + + +def test_face_status_wire_format_and_round_trip(): + status = nfd_mgmt.FaceStatus( + face_id=1, + uri='internal://', + face_scope=nfd_mgmt.FaceScope.LOCAL, + link_type=nfd_mgmt.FaceLinkType.POINT_TO_POINT, + flags=( + nfd_mgmt.FaceFlags.LOCAL_FIELDS_ENABLED + | nfd_mgmt.FaceFlags.LP_RELIABILITY_ENABLED + ), + n_in_bytes=2 ** 40, + ) + wire = bytes(tlv_encode(nfd_mgmt.FaceStatusMsg(face_status=[status]))) + assert wire == bytes.fromhex( + '8023690101720b696e7465726e616c3a2f2f840101860100' + '940800000100000000006c0103' + ) + + parsed = tlv_parse(nfd_mgmt.FaceStatusMsg, wire) + assert parsed.face_status[0].face_scope is nfd_mgmt.FaceScope.LOCAL + assert parsed.face_status[0].n_in_bytes == 2 ** 40 + + +def test_parse_response_without_body(): + body = tlv_encode( + nfd_mgmt.ControlResponse(status_code=404, status_text='Not found') + ) + parsed = nfd_mgmt.parse_response(bytes([0x65, len(body)]) + body) + assert parsed['status_code'] == 404 + assert parsed['status_text'] == 'Not found' + assert parsed['face_id'] is None diff --git a/tests/misc/svs_tlv_test.py b/tests/misc/svs_tlv_test.py new file mode 100644 index 0000000..3c56666 --- /dev/null +++ b/tests/misc/svs_tlv_test.py @@ -0,0 +1,44 @@ +import dataclasses as dc + +from ndn.app_support.svs.tlv import ( + MappingData, + MappingDataWrapper, + MappingEntry, + StateVecEntry, + StateVec, + StateVecWrapper, +) +from ndn.encoding import Name, tlv_encode, tlv_parse + + +def test_state_vector_dataclass_wire_format(): + state = StateVecWrapper( + StateVec(entries=[StateVecEntry(node_id='/node', seq_no=7)]) + ) + assert dc.is_dataclass(state) + + wire = bytes(tlv_encode(state)) + assert wire == bytes.fromhex('c90dca0b070608046e6f6465cc0107') + + parsed = tlv_parse(StateVecWrapper, wire) + assert Name.to_str(parsed.val.entries[0].node_id) == '/node' + assert parsed.val.entries[0].seq_no == 7 + + +def test_mapping_models_wire_format(): + mapping = MappingData( + node_id='/node', + entries=MappingEntry(seq_no=7, app_name='/app'), + ) + wire = bytes(tlv_encode(mapping)) + assert wire == bytes.fromhex('070608046e6f6465ce0acc010707050803617070') + + parsed = tlv_parse(MappingData, wire) + assert Name.to_str(parsed.node_id) == '/node' + assert Name.to_str(parsed.entries.app_name) == '/app' + assert parsed.entries.seq_no == 7 + + wrapper_wire = bytes(tlv_encode( + MappingDataWrapper(MappingEntry(seq_no=7, app_name='/app')) + )) + assert wrapper_wire == bytes.fromhex('cd0acc010707050803617070') From 5ea1c18555e3ced40b276db9b556e5db5bedd425 Mon Sep 17 00:00:00 2001 From: Xinyu Ma Date: Wed, 7 Oct 2026 23:30:52 -0700 Subject: [PATCH 4/8] app: promote v2 implementation to ndn.app Co-authored-by: Cursor --- src/ndn/app.py | 974 +++++++++++------- src/ndn/app_support/keychain_register.py | 2 +- src/ndn/app_support/svs/sync.py | 2 +- src/ndn/bin/nfdc/cmd_get_face.py | 2 +- src/ndn/bin/nfdc/cmd_get_route.py | 2 +- src/ndn/bin/nfdc/cmd_get_status.py | 2 +- src/ndn/bin/nfdc/cmd_get_strategy.py | 2 +- src/ndn/bin/nfdc/cmd_new_face.py | 2 +- src/ndn/bin/nfdc/cmd_new_route.py | 2 +- src/ndn/bin/nfdc/cmd_remove_face.py | 2 +- src/ndn/bin/nfdc/cmd_remove_route.py | 2 +- src/ndn/bin/nfdc/cmd_remove_strategy.py | 2 +- src/ndn/bin/nfdc/cmd_set_strategy.py | 2 +- src/ndn/bin/nfdc/utils.py | 2 +- src/ndn/bin/tools/cmd_fetch_data.py | 2 +- src/ndn/bin/tools/cmd_fetch_rdrcontent.py | 2 +- src/ndn/bin/tools/cmd_serve_data.py | 2 +- src/ndn/bin/tools/cmd_serve_rdrcontent.py | 2 +- .../security/validator/digest_validator.py | 19 +- .../security/validator/known_key_validator.py | 12 +- tests/integration/app_test.py | 213 ++-- tests/security/signer_test.py | 9 +- 22 files changed, 754 insertions(+), 507 deletions(-) diff --git a/src/ndn/app.py b/src/ndn/app.py index 6342c3d..7c88d48 100644 --- a/src/ndn/app.py +++ b/src/ndn/app.py @@ -1,5 +1,5 @@ # ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors +# Copyright (C) 2019-2022 The python-ndn authors # # This file is part of python-ndn. # @@ -15,62 +15,231 @@ # See the License for the specific language governing permissions and # limitations under the License. # ----------------------------------------------------------------------------- +import asyncio as aio +import typing import struct import logging -import asyncio as aio -from typing import Any -from collections.abc import Awaitable, Coroutine -from .utils import gen_nonce -from .encoding import BinaryStr, TypeNumber, LpTypeNumber, parse_interest, \ - parse_tl_num, parse_data, DecodeError, Name, NonStrictName, MetaInfo, \ - make_data, InterestParam, make_interest, FormalName, SignaturePtrs, parse_lp_packet, Component -from .security import Keychain, sha256_digest_checker, params_sha256_checker, NullSigner +from hashlib import sha256 +from dataclasses import dataclass from .transport.face import Face -from .app_support.nfd_mgmt import make_command, parse_response -from .name_tree import NameTrie, InterestTreeNode, PrefixTreeNode -from .types import NetworkError, InterestTimeout, Validator, Route, InterestCanceled, \ - InterestNack, ValidationFailure +from .transport.prefix_registerer import PrefixRegisterer +from . import security as sec +from . import encoding as enc +from . import name_tree +from . import types +from . import utils +from .encoding import ndnlp_v2 as ndnlp +from .encoding import ndn_format_0_3 as fmt +from .encoding.tlv_model import tlv_encode from .client_conf import read_client_conf, default_face, default_keychain +from .transport.nfd_registerer import NfdRegister + + +DEFAULT_LIFETIME = 4000 + +ValidResult = types.ValidResult + +PktContext = dict[str, typing.Any] +r"""The context for NDN Interest or Data handling.""" + +ReplyFunc = typing.Callable[[enc.BinaryStr], bool] +r""" +Continuation function for :any:`IntHandler` to respond to an Interest. + +.. function:: (data: BinaryStr) -> bool + + :param data: an encoded Data packet. + :type data: :any:`BinaryStr` + :return: True for success, False upon error. +""" + +IntHandler = typing.Callable[[enc.FormalName, enc.BinaryStr | None, ReplyFunc, PktContext], None] +r""" +Interest handler function associated with a name prefix. + +The function should use the provided ``reply`` callback to reply with Data, which can handle PIT +token properly. + +.. function:: (name: FormalName, app_param: Optional[BinaryStr], reply: ReplyFunc, context: PktContext) -> None + + :param name: Interest name. + :type name: :any:`FormalName` + :param app_param: Interest ApplicationParameters value, or None if absent. + :type app_param: Optional[:any:`BinaryStr`] + :param reply: continuation function to respond with Data. + :type reply: :any:`ReplyFunc` + :param context: packet handler context. + :type context: :any:`PktContext` + +.. note:: + Interest handler function must be a normal function instead of an ``async`` one. + This is on purpose, because an Interest is supposed to be replied ASAP, + even it cannot finish the request in time. + To provide some feedback, a better practice is replying with an Application NACK + (or some equivalent Data packet saying the operation cannot be finished in time). + If you want to use ``await`` in the handler, please use ``asyncio.create_task`` to create a new coroutine. +""" + +Validator = typing.Callable[[enc.FormalName, fmt.SignaturePtrs, PktContext], + typing.Coroutine[typing.Any, None, ValidResult]] +r""" +Validator function that validates Interest or Data signature against trust policy. + +.. function:: (name: FormalName, sig: SignaturePtrs, context: PktContext) -> Coroutine[ValidResult] + + :param name: Interest or Data name. + :type name: :any:`FormalName` + :param sig: packet signature pointers. + :type sig: :any:`SignaturePtrs` + :param context: packet handler context. + :type context: :any:`PktContext` +""" + + +async def pass_all(_name, _sig, _context): + return types.ValidResult.PASS + + +@dataclass +class PrefixTreeNode: + callback: IntHandler = None + validator: Validator | None = None + + +@dataclass +class PendingIntEntry: + future: aio.Future + deadline: int + can_be_prefix: bool + must_be_fresh: bool + validator: Validator + implicit_sha256: enc.BinaryStr = b'' + task: aio.Task | None = None + + async def satisfy(self, data: types.DataTuple): + name, meta_info, content, sig, raw_packet = data + pkt_context = { + 'meta_info': meta_info, + 'sig_ptrs': sig, + 'raw_packet': raw_packet, + 'deadline': self.deadline, + } + if self.validator is not None: + try: + valid = await self.validator(name, sig, pkt_context) + except (TimeoutError, aio.CancelledError): + valid = ValidResult.TIMEOUT + else: + valid = ValidResult.FAIL + if self.future.cancelled() or self.future.done(): + # Don't know why but there was a race condition with timeout() + # The sequence was: Interest sent -> Data arrived -> timeout() -> satisfy() + # Cannot reproduce the scenario. Especially, delay in validator() does not trigger the race condition + # But anyway, let me add a guard check here. + return + if valid == ValidResult.PASS or valid == ValidResult.ALLOW_BYPASS: + self.future.set_result((name, content, pkt_context)) + else: + self.future.set_exception(types.ValidationFailure(name, meta_info, content, sig, valid)) + + +class InterestTreeNode: + pending_list: list[PendingIntEntry] + + def __init__(self): + self.pending_list = [] + + def append_interest(self, future: aio.Future, deadline: int, param: fmt.InterestParam, + validator: Validator, implicit_sha256: enc.BinaryStr): + self.pending_list.append( + PendingIntEntry(future, deadline, param.can_be_prefix, param.must_be_fresh, validator, implicit_sha256)) + + def nack_interest(self, nack_reason: int) -> bool: + for entry in self.pending_list: + entry.future.set_exception(types.InterestNack(nack_reason)) + return True + + def satisfy(self, data: types.DataTuple, is_prefix: bool) -> bool: + unsatisfied_entries = [] + raw_packet = data[4] + for entry in self.pending_list: + if entry.can_be_prefix or not is_prefix: + if len(entry.implicit_sha256) > 0: + data_sha256 = sha256(raw_packet).digest() + passed = data_sha256 == entry.implicit_sha256 + else: + passed = True + else: + passed = False + if passed: + # Try to validate the packet + aio.create_task(entry.satisfy(data)) + else: + unsatisfied_entries.append(entry) + if unsatisfied_entries: + self.pending_list = unsatisfied_entries + return False + else: + return True + + def timeout(self, future: aio.Future): + # Exception is raised by outside code. + for ele in self.pending_list: + if ele.future is future and ele.task is not None: + ele.task.cancel() + self.pending_list = [ele for ele in self.pending_list if ele.future is not future] + return not self.pending_list + + def cancel(self): + for entry in self.pending_list: + entry.future.cancel() + if entry.task is not None: + entry.task.cancel() class NDNApp: """ An NDN application. - - :ivar face: the Face used to connection to a NFD node. - :ivar keychain: the Keychain to store Identities and Keys, providing Signers. - :ivar int_validator: the default validator for Interest packets. - :ivar data_validator: the default validator for Data packets. """ + # PIT and FIB here are not real PIT/FIB, but a data structure that handles expressed Interests (for PIT) + # and registered handlers & routes (for FIB). Since they share the functionality with real PIT and FIB, + # I borrow the word to have a shorter variable name. + _pit: name_tree.NameTrie = None + _fib: name_tree.NameTrie = None face: Face = None - keychain: Keychain = None - _int_tree: NameTrie = None - _prefix_tree: NameTrie = None - int_validator: Validator = None - data_validator: Validator = None - _autoreg_routes: list[tuple[FormalName, Route, Validator | None, bool, bool]] - _prefix_register_semaphore: aio.Semaphore = None + registerer: PrefixRegisterer = None + _autoreg_routes: list[enc.FormalName] logger: logging.Logger - def __init__(self, face=None, keychain=None): + def __init__(self, face=None, client_conf=None, registerer=None): self.logger = logging.getLogger(__name__) - config = read_client_conf() if not face or not keychain else {} + config = client_conf if client_conf else {} + if not face: + if 'transport' not in config: + config = read_client_conf() | config if face is not None: self.face = face else: self.face = default_face(config['transport']) - self.face.callback = self._receive - if keychain is not None: - self.keychain = keychain + if registerer is not None: + self.registerer = registerer else: - self.keychain = default_keychain(config['pib'], config['tpm']) - self._int_tree = NameTrie() - self._prefix_tree = NameTrie() - self.data_validator = sha256_digest_checker - self.int_validator = sha256_digest_checker + self.registerer = NfdRegister() + self.registerer.set_app(app=self) + self.face.callback = self._receive + self._pit = name_tree.NameTrie() + self._fib = name_tree.NameTrie() self._autoreg_routes = [] - async def _receive(self, typ: int, data: BinaryStr): + @staticmethod + def default_keychain(client_conf=None) -> sec.Keychain: + if not client_conf: + config = read_client_conf() + else: + config = read_client_conf() | client_conf + return default_keychain(config['pib'], config['tpm']) + + async def _receive(self, typ: int, data: enc.BinaryStr): """ Pipeline when a packet is received. @@ -79,49 +248,150 @@ async def _receive(self, typ: int, data: BinaryStr): """ # if self.logger.isEnabledFor(logging.DEBUG): # self.logger.debug('Packet received %s, %s' % (typ, bytes(data))) - if typ == LpTypeNumber.LP_PACKET: + if typ == ndnlp.LpTypeNumber.LP_PACKET: try: - nack_reason, fragment = parse_lp_packet(data, with_tl=True) - except (DecodeError, TypeError, ValueError, struct.error): + lp_pkt = ndnlp.parse_lp_packet_v2(data, with_tl=True) + except (enc.DecodeError, TypeError, ValueError, struct.error): self.logger.warning('Unable to decode received packet') return - data = fragment - typ, _ = parse_tl_num(data) + if lp_pkt.nack is not None: + nack_reason = lp_pkt.nack.nack_reason + else: + nack_reason = None + pit_token = lp_pkt.pit_token + data = lp_pkt.fragment + if data is None: + # Only Nack and Data can reach this function. + self.logger.fatal('LP packet without a fragment reaching _receive branch. Unexpected behavior.') + return + typ, _ = enc.parse_tl_num(data) else: nack_reason = None + pit_token = None if nack_reason is not None: try: - name, _, _, _ = parse_interest(data, with_tl=True) - except (DecodeError, TypeError, ValueError, struct.error): + name, _, _, _ = fmt.parse_interest(data, with_tl=True) + except (enc.DecodeError, TypeError, ValueError, struct.error): self.logger.warning('Unable to decode the fragment of LpPacket') return if self.logger.isEnabledFor(logging.DEBUG): - self.logger.debug('NetworkNack received %s, reason=%s', Name.to_str(name), nack_reason) + self.logger.debug('NetworkNack received %s, reason=%s', enc.Name.to_str(name), nack_reason) self._on_nack(name, nack_reason) else: - if typ == TypeNumber.INTEREST: + if typ == fmt.TypeNumber.INTEREST: try: - name, param, app_param, sig = parse_interest(data, with_tl=True) - except (DecodeError, TypeError, ValueError, struct.error): + name, param, app_param, sig = fmt.parse_interest(data, with_tl=True) + except (enc.DecodeError, TypeError, ValueError, struct.error): self.logger.warning('Unable to decode received packet') return if self.logger.isEnabledFor(logging.DEBUG): - self.logger.debug('Interest received %s', Name.to_str(name)) - await self._on_interest(name, param, app_param, sig, raw_packet=data) - elif typ == TypeNumber.DATA: + if pit_token: + self.logger.debug('Interest received %s w/ token=%s', + enc.Name.to_str(name), bytes(pit_token).hex()) + else: + self.logger.debug('Interest received %s', enc.Name.to_str(name)) + await self._on_interest(name, pit_token, param, app_param, sig, raw_packet=data) + elif typ == fmt.TypeNumber.DATA: try: - name, meta_info, content, sig = parse_data(data, with_tl=True) - except (DecodeError, TypeError, ValueError, struct.error): + name, meta_info, content, sig = fmt.parse_data(data, with_tl=True) + except (enc.DecodeError, TypeError, ValueError, struct.error): self.logger.warning('Unable to decode received packet') return if self.logger.isEnabledFor(logging.DEBUG): - self.logger.debug('Data received %s', Name.to_str(name)) + self.logger.debug('Data received %s', enc.Name.to_str(name)) await self._on_data(name, meta_info, content, sig, raw_packet=data) else: self.logger.warning('Unable to decode received packet') - def put_raw_packet(self, data: BinaryStr): + @staticmethod + def make_data(name: enc.NonStrictName, content: enc.BinaryStr | None, + signer: enc.Signer | None, **kwargs): + r""" + Encode a data packet without requiring an NDNApp instance. + This is simply a wrapper of encoding.make_data. + I write this because most people seem not aware of the ``make_data`` function in the encoding package. + The corresponding ``make_interest`` is less useful (one should not reuse nonce) and thus not wrapped. + Sync protocol should use encoding.make_interest if necessary. + Also, since having a default signer encourages bad habit, + callers must provide a signer explicitly. + + :param name: the Name. + :type name: :any:`NonStrictName` + :param content: the Content. + :type content: Optional[:any:`BinaryStr`] + :param signer: the Signer used to sign the packet. + :type signer: Optional[:any:`Signer`] + :param kwargs: arguments for :any:`MetaInfo`. + :return: TLV encoded Data packet. + """ + if 'meta_info' in kwargs: + meta_info = kwargs['meta_info'] + else: + meta_info = fmt.MetaInfo.from_dict(kwargs) + return fmt.make_data(name, meta_info, content, signer=signer) + + async def _on_interest(self, name: enc.FormalName, pit_token: enc.BinaryStr | None, + param: fmt.InterestParam, app_param: enc.BinaryStr | None, sig: fmt.SignaturePtrs, + raw_packet: enc.BinaryStr): + trie_step = self._fib.longest_prefix(name) + if not trie_step: + self.logger.warning('No route: %s', name) + return + node: PrefixTreeNode = trie_step.value + if node.callback is None: + self.logger.warning('No callback: %s', name) + return + sig_required = app_param is not None or sig.signature_info is not None + if sig_required: + if not await sec.params_sha256_checker(name, sig): + self.logger.warning('Drop malformed Interest: %s', name) + return + + # Use context to handle misc parameters + if param.lifetime is not None: + deadline = utils.timestamp() + param.lifetime + else: + deadline = utils.timestamp() + DEFAULT_LIFETIME + context = { + 'int_param': param, + 'pit_token': pit_token, + 'sig_ptrs': sig, + 'raw_packet': raw_packet, + 'deadline': deadline, + } + + def reply(data: enc.BinaryStr) -> bool: + now = utils.timestamp() + if now > deadline: + self.logger.warning('Deadline passed, unable to reply to %s', enc.Name.to_str(name)) + return False + if pit_token is None: + self._put_raw_packet(data) + else: + self._put_raw_packet_with_pit_token(data, pit_token) + return True + + # In case the validator blocks the pipeline, create a task + async def submit_interest(): + if sig_required: + # In v2, to enforce security, validator is required. Also, all interests with app_param are checked. + # The validator needs to manually pass it if the application wants to handle unsigned Interests with + # app_param. + if node.validator is not None: + valid = await node.validator(name, sig, context) + else: + valid = ValidResult.FAIL + else: + valid = ValidResult.PASS + if valid == ValidResult.PASS or valid == ValidResult.ALLOW_BYPASS: + node.callback(name, app_param, reply, context) + else: + self.logger.warning('Drop unvalidated Interest: %s', name) + return + aio.create_task(submit_interest()) + + def _put_raw_packet(self, data: enc.BinaryStr): r""" Send a raw Data packet. @@ -130,76 +400,236 @@ def put_raw_packet(self, data: BinaryStr): :raises NetworkError: the face to NFD is down. """ if not self.face.running: - raise NetworkError('cannot send packet before connected') + raise types.NetworkError('cannot send packet before connected') self.face.send(data) - def prepare_data(self, name: NonStrictName, content: BinaryStr | None = None, **kwargs): + def _put_raw_packet_with_pit_token(self, data: enc.BinaryStr, pit_token: enc.BinaryStr): r""" - Prepare a Data packet by generating, encoding and signing it. + Wrap a raw Data packet with PIT Token and send. + Used to reply an Interest with PIT Token provided. - :param name: the Name. - :type name: :any:`NonStrictName` - :param content: the Content. - :type content: Optional[:any:`BinaryStr`] - :param kwargs: :ref:`label-keyword-arguments`. - :return: TLV encoded Data packet. + :param data: TLV encoded Data packet. + :type data: :any:`BinaryStr` + :param pit_token: The PIT Token provided. + :type pit_token: :any:`BinaryStr` + :raises NetworkError: the face to NFD is down. """ - if kwargs.get('no_signature', False): - signer = NullSigner() - elif 'signer' in kwargs: - signer = kwargs['signer'] - else: - signer = self.keychain.get_signer(kwargs) - if 'meta_info' in kwargs: - meta_info = kwargs['meta_info'] - else: - meta_info = MetaInfo.from_dict(kwargs) - return make_data(name, meta_info, content, signer=signer) + if not self.face.running: + raise types.NetworkError('cannot send packet before connected') + pkt = ndnlp.LpPacket(lp_packet=ndnlp.LpPacketValue(pit_token=pit_token, fragment=data)) + wire = tlv_encode(pkt) + self.face.send(wire) - def put_data(self, name: NonStrictName, content: BinaryStr | None = None, **kwargs): + def _put_raw_packet_with_pit_token_nocopy(self, data: enc.BinaryStr, pit_token: enc.BinaryStr): r""" - Publish a Data packet. + Wrap a raw Data packet with PIT Token and send. + Used to reply an Interest with PIT Token provided. - :param name: the Name. + This function is reserved as a backup because it assumes the face to be stream face. + + :param data: TLV encoded Data packet. + :type data: :any:`BinaryStr` + :param pit_token: The PIT Token provided. + :type pit_token: :any:`BinaryStr` + :raises NetworkError: the face to NFD is down. + """ + # To avoid extra copy, we manually encode the header and send it separately from Data body + # The format is: LP-T LP-L (PIT-TOKEN-TLV) FRAG-T FRAG-L + if not self.face.running: + raise types.NetworkError('cannot send packet before connected') + pt_wire = tlv_encode(ndnlp.LpPacketValue(pit_token=pit_token)) + frag_l = len(data) + frag_header_l = (enc.get_tl_num_size(ndnlp.LpTypeNumber.FRAGMENT) + enc.get_tl_num_size(frag_l)) + lp_l = len(pt_wire) + frag_header_l + frag_l + wire_l = (enc.get_tl_num_size(ndnlp.LpTypeNumber.LP_PACKET) + + enc.get_tl_num_size(lp_l) + len(pt_wire) + frag_header_l) + wire = bytearray(wire_l) + pos = 0 + pos += enc.write_tl_num(ndnlp.LpTypeNumber.LP_PACKET, wire, pos) + pos += enc.write_tl_num(lp_l, wire, pos) + wire[pos:pos+len(pt_wire)] = pt_wire + pos += len(pt_wire) + pos += enc.write_tl_num(ndnlp.LpTypeNumber.FRAGMENT, wire, pos) + pos += enc.write_tl_num(frag_l, wire, pos) + self.face.send(wire) + self.face.send(data) + + def attach_handler(self, name: enc.NonStrictName, handler: IntHandler, + validator: Validator | None = None): + """ + Attach an Interest handler at a name prefix. + Incoming Interests under the specified name prefix will be dispatched to the handler. + + This only sets the handler within NDNApp, but does not send prefix registration commands + to the forwarder. + To register the prefix in the forwarder, use :any:`register`. + The handler association is retained even if the forwarder is disconnected. + + :param name: name prefix. :type name: :any:`NonStrictName` - :param content: the Content. - :type content: Optional[:any:`BinaryStr`] - :param kwargs: :ref:`label-keyword-arguments`. - :return: TLV encoded Data packet. + :param handler: Interest handler function. + :type handler: :any:`IntHandler` + :param validator: validator for signed Interests. + Non signed Interests, i.e. those without ApplicationParameters and SignatureInfo, are + passed to the handler directly without calling the validator. + Interests with malformed ParametersSha256DigestComponent are dropped silently. + If a validator is not provided (set to ``None``), signed Interests will be dropped. + Otherwise, signed Interests are passed to the validator. + Those failing the validation are dropped silently. + Those passing the validation are passed to the handler function. + :type validator: Optional[:any:`Validator`] + """ + name = enc.Name.normalize(name) + node = self._fib.setdefault(name, PrefixTreeNode()) + if node.callback: + raise ValueError(f'Duplicated handler attachment: {enc.Name.to_str(name)}') + node.callback = handler + node.validator = validator + + def detach_handler(self, name: enc.NonStrictName): + """ + Detach an Interest handler at a name prefix. + + This only deletes the handler within NDNApp, but does not unregister the prefix in the + forwarder. + To unregister the prefix in the forwarder, use :any:`unregister`. + + :param name: name prefix. This must exactly match the name passed to :any:`attach_handler`. + If there are Interest handlers attached to longer prefixes, each handler must + be removed explicitly. + :type name: :any:`NonStrictName` + """ + del self._fib[enc.Name.normalize(name)] + + async def register(self, name: enc.NonStrictName) -> bool: + """ + Register a prefix in the forwarder. + + This only sends the prefix registration command to the forwarder. + In order to receive incoming Interests, you also need to use :any:`attach_handler` to + attach an Interest handler function. + + :param name: name prefix. + :type name: :any:`NonStrictName` + + :raises ValueError: the prefix is already registered. + :raises NetworkError: the face to NFD is down now. """ - self.put_raw_packet(self.prepare_data(name, content, **kwargs)) - - def express_interest(self, - name: NonStrictName, - app_param: BinaryStr | None = None, - validator: Validator | None = None, - need_raw_packet: bool = False, - **kwargs) -> Coroutine[Any, None, tuple[FormalName, MetaInfo, BinaryStr | None]]: + name = enc.Name.normalize(name) + return await self.registerer.register(name) + + async def unregister(self, name: enc.NonStrictName) -> bool: + """ + Unregister a prefix in the forwarder. + + :param name: name prefix. + :type name: :any:`NonStrictName` + """ + name = enc.Name.normalize(name) + return await self.registerer.unregister(name) + + def express_raw_interest(self, + final_name: enc.NonStrictName, + interest_param: fmt.InterestParam, + raw_interest: enc.BinaryStr, + validator: Validator, + no_response: bool = False + ) -> typing.Coroutine[typing.Any, None, + tuple[enc.FormalName, enc.BinaryStr | None, PktContext]]: + if no_response: + self.face.send(raw_interest) + return None + if validator is None: + raise ValueError('Data Validator must not be None when expressing an Interest.') + final_name = enc.Name.normalize(final_name) + future = aio.get_running_loop().create_future() + # Handle implicit SHA256 + if enc.Component.get_type(final_name[-1]) == enc.Component.TYPE_IMPLICIT_SHA256: + node_name = final_name[:-1] + implicit_sha256 = enc.Component.get_value(final_name[-1]) + else: + node_name = final_name + implicit_sha256 = b'' + node: InterestTreeNode = self._pit.setdefault(node_name, InterestTreeNode()) + deadline = utils.timestamp() + if interest_param.lifetime is not None: + deadline += interest_param.lifetime + else: + deadline += DEFAULT_LIFETIME + node.append_interest(future, deadline, interest_param, validator, implicit_sha256) + self.face.send(raw_interest) + return self._wait_for_data(future, deadline, node_name, node) + + async def _wait_for_data(self, future: aio.Future, deadline: int, node_name: enc.FormalName, + node: InterestTreeNode): + lifetime = deadline - utils.timestamp() + if lifetime <= 0: + # This happens if the application sends an Interest, does some calculation, and then fetches the result. + # The Interest should be satisfied now. Thus, it should not be considered as an error. + lifetime = 100 + try: + data_name, content, pkt_context = await aio.wait_for(future, timeout=lifetime/1000.0) + except TimeoutError: + if node.timeout(future): + del self._pit[node_name] + raise types.InterestTimeout() + except aio.CancelledError: + raise types.InterestCanceled() + # ValidationError, InterestNack are passed to the parent caller + return data_name, content, pkt_context + + async def _on_data(self, name: enc.FormalName, meta_info: fmt.MetaInfo, + content: enc.BinaryStr | None, sig: fmt.SignaturePtrs, + raw_packet: enc.BinaryStr): + clean_list = [] + for prefix, node in self._pit.prefixes(name): + if node.satisfy((name, meta_info, content, sig, raw_packet), prefix != name): + clean_list.append(prefix) + for prefix in clean_list: + del self._pit[prefix] + + def _on_nack(self, name: enc.FormalName, nack_reason: int): + try: + node = self._pit[name] + except KeyError: + node = None + if node: + if node.nack_interest(nack_reason): + del self._pit[name] + + def express(self, name: enc.NonStrictName, validator: Validator, + app_param: enc.BinaryStr | None = None, + signer: enc.Signer | None = None, + **kwargs) -> typing.Coroutine[typing.Any, None, + tuple[enc.FormalName, enc.BinaryStr | None, PktContext]]: r""" - Express an Interest packet. + Express an Interest. The Interest packet is sent immediately and a coroutine used to get the result is returned. - Awaiting on what is returned will block until the Data is received and return that Data. - An exception is raised if unable to receive the Data. + Awaiting on the returned coroutine will block until the Data is received. + It then returns the Data name, Data Content value, and :any:`PktContext`. + An exception is raised if NDNApp is unable to retrieve the Data. - :param name: the Name. + :param name: Interest name. :type name: :any:`NonStrictName` - :param app_param: the ApplicationParameters. + :param validator: validator for the retrieved Data packet. + :type validator: :any:`Validator` + :param app_param: Interest ApplicationParameters value. If this is not None, a signed + Interest is sent. NDNApp does not support sending parameterized + Interests that are not signed. :type app_param: Optional[:any:`BinaryStr`] - :param validator: the Validator used to verify the Data received. - :type validator: Optional[:any:`Validator`] - :param need_raw_packet: if True, return the raw Data packet with TL. - :type need_raw_packet: bool - :param kwargs: :ref:`label-keyword-arguments`. - :return: A tuple of (Name, MetaInfo, Content) after ``await``. - If need_raw_packet is True, return a tuple (Name, MetaInfo, Content, RawPacket). - :rtype: Coroutine[Any, None, Tuple[:any:`FormalName`, :any:`MetaInfo`, Optional[:any:`BinaryStr`]]] + :param signer: Signer for Interest signing. This is required if `app_param` is specified. + :type signer: Optional[:any:`Signer`] + :param kwargs: arguments for :any:`InterestParam`. + :return: A tuple of (Name, Content, PacketContext) after ``await``. + :rtype: Coroutine[Any, None, Tuple[:any:`FormalName`, Optional[:any:`BinaryStr`], :any:`PktContext`]] - The following exception is raised by ``express_interest``: + The following exceptions may be raised by ``express``: :raises NetworkError: the face to NFD is down before sending this Interest. + :raises ValueError: when the signer is missing but app_param presents. - The following exceptions are raised by the coroutine returned: + The following exceptions may be raised by the returned coroutine: :raises InterestNack: an NetworkNack is received. :raises InterestTimeout: time out. @@ -207,64 +637,69 @@ def express_interest(self, :raises InterestCanceled: the face to NFD is shut down after sending this Interest. """ if not self.face.running: - raise NetworkError('cannot send packet before connected') - if 'signer' in kwargs: - signer = kwargs['signer'] - elif app_param is not None: - signer = self.keychain.get_signer(kwargs) - else: - signer = None + raise types.NetworkError('cannot send packet before connected') + if app_param is not None and signer is None: + raise ValueError('An Interest with AppParam is required to be signed.') if 'interest_param' in kwargs: interest_param = kwargs['interest_param'] else: if 'nonce' not in kwargs: - kwargs['nonce'] = gen_nonce() - interest_param = InterestParam.from_dict(kwargs) - interest, final_name = make_interest(name, interest_param, app_param, signer=signer, need_final_name=True) - return self.express_raw_interest(final_name, interest_param, interest, validator, need_raw_packet) + kwargs['nonce'] = utils.gen_nonce() + interest_param = fmt.InterestParam.from_dict(kwargs) + interest, final_name = fmt.make_interest(name, interest_param, app_param, signer=signer, need_final_name=True) + no_response = kwargs.get('no_response', False) + return self.express_raw_interest(final_name, interest_param, interest, validator, no_response) - def express_raw_interest(self, - final_name: NonStrictName, - interest_param: InterestParam, - raw_interest: BinaryStr, - validator: Validator | None = None, - need_raw_packet: bool = False - ) -> Coroutine[Any, None, tuple[FormalName, MetaInfo, BinaryStr | None]]: - final_name = Name.normalize(final_name) - future = aio.get_running_loop().create_future() - if Component.get_type(final_name[-1]) == Component.TYPE_IMPLICIT_SHA256: - node_name = final_name[:-1] - implicit_sha256 = Component.get_value(final_name[-1]) - else: - node_name = final_name - implicit_sha256 = b'' - node = self._int_tree.setdefault(node_name, InterestTreeNode()) - node.append_interest(future, interest_param, implicit_sha256) - self.face.send(raw_interest) - return self._wait_for_data(future, interest_param.lifetime, node_name, node, validator, need_raw_packet) + def route(self, name: enc.NonStrictName, validator: Validator | None = None): + r""" + A decorator used to register a permanent route for a specific prefix. + The decorated function should be an :any:`IntHandler`. - async def _wait_for_data(self, future: aio.Future, lifetime: int, node_name: FormalName, - node: InterestTreeNode, validator: Validator, need_raw_packet: bool): - lifetime = 100 if lifetime is None else lifetime - try: - data_name, meta_info, content, sig, raw_packet = await aio.wait_for(future, timeout=lifetime/1000.0) - except TimeoutError: - if node.timeout(future): - del self._int_tree[node_name] - raise InterestTimeout() - except aio.CancelledError: - raise InterestCanceled() - if validator is None: - validator = self.data_validator - if await validator(data_name, sig): - if need_raw_packet: - return data_name, meta_info, content, raw_packet - else: - return data_name, meta_info, content - else: - raise ValidationFailure(data_name, meta_info, content, sig) + This function is non-blocking and can be called at any time. + It can be called before connecting to the forwarder. + Every time a forwarder connection is established, NDNApp will automatically send + prefix registration commands. + Errors in prefix registration are ignored. + + :param name: name prefix. + :type name: :any:`NonStrictName` + :param validator: validator for signed Interests. See :any:`attach_handler` for details. + :type validator: Optional[:any:`Validator`] + + :examples: + .. code-block:: python3 + + app = NDNApp() + + @app.route('/example/rpc') + def on_interest(name, app_param, reply, context): + pass + + """ + name = enc.Name.normalize(name) + + def decorator(func: IntHandler): + self._autoreg_routes.append(name) + self.attach_handler(name, func, validator) + if self.face.running: + aio.create_task(self.register(name)) + return func + return decorator - async def main_loop(self, after_start: Awaitable = None) -> bool: + def _clean_up(self): + for node in self._pit.itervalues(): + node.cancel() + # FIB is not cleared now + self._pit.clear() + + def shutdown(self): + """ + Manually shutdown the face to NFD. + """ + self.logger.info('Manually shutdown') + self.face.shutdown() + + async def main_loop(self, after_start: typing.Awaitable = None) -> bool: """ The main loop of NDNApp. @@ -272,11 +707,9 @@ async def main_loop(self, after_start: Awaitable = None) -> bool: :return: ``True`` if the connection is shutdown not by ``Ctrl+C``. For example, manually or by the other side. """ - self._prefix_register_semaphore = aio.Semaphore(1) - async def starting_task(): - for name, route, validator, need_raw_packet, need_sig_ptrs in self._autoreg_routes: - await self.register(name, route, validator, need_raw_packet, need_sig_ptrs) + for name in self._autoreg_routes: + await self.register(name) if after_start: try: await after_start @@ -288,7 +721,7 @@ async def starting_task(): await self.face.open() except (FileNotFoundError, ConnectionError, OSError, PermissionError): if after_start: - if isinstance(after_start, Coroutine): + if isinstance(after_start, typing.Coroutine): after_start.close() elif isinstance(after_start, (aio.Task, aio.Future)): after_start.cancel() @@ -307,20 +740,7 @@ async def starting_task(): await task return ret - def _clean_up(self): - for node in self._int_tree.itervalues(): - node.cancel() - self._prefix_tree.clear() - self._int_tree.clear() - - def shutdown(self): - """ - Manually shutdown the face to NFD. - """ - self.logger.info('Manually shutdown') - self.face.shutdown() - - def run_forever(self, after_start: Awaitable = None): + def run_forever(self, after_start: typing.Awaitable = None): """ A non-async wrapper of :meth:`main_loop`. @@ -338,209 +758,3 @@ def run_forever(self, after_start: Awaitable = None): aio.run(self.main_loop(after_start)) except KeyboardInterrupt: self.logger.info('Receiving Ctrl+C, exit') - - def route(self, name: NonStrictName, validator: Validator | None = None, - need_raw_packet: bool = False, need_sig_ptrs: bool = False): - """ - A decorator used to register a permanent route for a specific prefix. - - This function is non-blocking and can be called at any time. - If it is called before connecting to NFD, NDNApp will remember this route and - automatically register it every time when a connection is established. - Failure in registering this route to NFD will be ignored. - - The decorated function should accept 3 arguments: Name, Interest parameters and ApplicationParameters. - - :param name: the Name prefix for this route. - :type name: :any:`NonStrictName` - :param validator: the Validator used to validate coming Interests. - An Interest without ApplicationParameters and SignatureInfo will be considered valid without - calling validator. - Interests with malformed ParametersSha256DigestComponent will be dropped before going into the validator. - Otherwise NDNApp will try to validate the Interest with the validator. - Interests which fail to be validated will be dropped without raising any exception. - :type validator: Optional[:any:`Validator`] - :param need_raw_packet: if True, pass the raw Interest packet to the callback as a keyword argument - ``raw_packet``. - :type need_raw_packet: bool - :param need_sig_ptrs: if True, pass the Signature pointers to the callback as a keyword argument - ``sig_ptrs``. - :type need_sig_ptrs: bool - - :examples: - .. code-block:: python3 - - app = NDNApp() - - @app.route('/example/rpc') - def on_interest(name: FormalName, param: InterestParam, app_param): - pass - - .. note:: - The route function must be a normal function instead of an ``async`` one. - This is on purpose, because an Interest is supposed to be replied ASAP, - even it cannot finish the request in time. - To provide some feedback, a better practice is replying with an Application NACK - (or some equivalent Data packet saying the operation cannot be finished in time). - If you want to use ``await`` in the handler, please use ``asyncio.create_task`` to create a new coroutine. - - .. note:: - Currently, python-ndn does not handle PIT Tokens. - """ - name = Name.normalize(name) - - def decorator(func: Route): - self._autoreg_routes.append((name, func, validator, need_raw_packet, need_sig_ptrs)) - if self.face.running: - aio.create_task(self.register(name, func, validator, need_raw_packet, need_sig_ptrs)) - return func - return decorator - - async def register(self, name: NonStrictName, func: Route | None, validator: Validator | None = None, - need_raw_packet: bool = False, need_sig_ptrs: bool = False) -> bool: - """ - Register a route for a specific prefix dynamically. - - :param name: the Name prefix for this route. - :type name: :any:`NonStrictName` - :param func: the onInterest function for the specified route. - If ``None``, the NDNApp will only send the register command to forwarder, - without setting any callback function. - :type func: Optional[Callable[[:any:`FormalName`, :any:`InterestParam`, Optional[:any:`BinaryStr`]], ``None``]] - :param validator: the Validator used to validate coming Interests. - :type validator: Optional[:any:`Validator`] - :return: ``True`` if the registration succeeded. - :param need_raw_packet: if True, pass the raw Interest packet to the callback as a keyword argument - ``raw_packet``. - :type need_raw_packet: bool - :param need_sig_ptrs: if True, pass the Signature pointers to the callback as a keyword argument - ``sig_ptrs``. - :type need_sig_ptrs: bool - - :raises ValueError: the prefix is already registered. - :raises NetworkError: the face to NFD is down now. - """ - name = Name.normalize(name) - if func is not None: - self.set_interest_filter(name, func, validator, need_raw_packet, need_sig_ptrs) - - # Fix the issue that NFD only allows one packet signed by a specific key for a timestamp number - async with self._prefix_register_semaphore: - try: - _, _, reply = await self.express_interest( - name=make_command('rib', 'register', self.face, name=name), - lifetime=1000) - ret = parse_response(reply) - if ret['status_code'] != 200: - self.logger.error('Registration for %s failed: %s %s', - Name.to_str(name), ret["status_code"], ret["status_text"]) - return False - else: - self.logger.debug('Registration for %s succeeded: %s %s', - Name.to_str(name), ret["status_code"], ret["status_text"]) - return True - except (InterestNack, InterestTimeout, InterestCanceled, ValidationFailure) as e: - self.logger.error('Registration for %s failed: %s', Name.to_str(name), e.__class__.__name__) - return False - - async def unregister(self, name: NonStrictName) -> bool: - """ - Unregister a route for a specific prefix. - - :param name: the Name prefix. - :type name: :any:`NonStrictName` - """ - name = Name.normalize(name) - del self._prefix_tree[name] - try: - await self.express_interest(make_command('rib', 'unregister', self.face, name=name), lifetime=1000) - return True - except (InterestNack, InterestTimeout, InterestCanceled, ValidationFailure): - return False - - def set_interest_filter(self, name: NonStrictName, func: Route, - validator: Validator | None = None, need_raw_packet: bool = False, - need_sig_ptrs: bool = False): - """ - Set the callback function for an Interest prefix without sending a register command to the forwarder. - - .. note:: - All callbacks registered by ``set_interest_filter`` are removed when disconnected from - the the forwarder, and will not be added back after reconnection. - This behaviour is the same as ``register``. - Therefore, it is strongly recommended to use ``route`` for static routes. - """ - name = Name.normalize(name) - node = self._prefix_tree.setdefault(name, PrefixTreeNode()) - if node.callback: - raise ValueError(f'Duplicated registration: {Name.to_str(name)}') - node.callback = func - node.extra_param = {'raw_packet': need_raw_packet, 'sig_ptrs': need_sig_ptrs} - if validator: - node.validator = validator - - def unset_interest_filter(self, name: NonStrictName): - """ - Remove the callback function for an Interest prefix without sending an unregister command. - - .. note:: - ``unregister`` will only remove the callback if the callback's name matches exactly - the route's name. - This is because there may be one route whose name is the prefix of another. - To avoid cancelling unexpected routes, neither ``unregister`` nor ``unset_interest_filter`` - behaves in a cascading manner. - Please remove callbacks manually. - """ - name = Name.normalize(name) - del self._prefix_tree[name] - - def _on_nack(self, name: FormalName, nack_reason: int): - node = self._int_tree[name] - if node: - if node.nack_interest(nack_reason): - del self._int_tree[name] - - async def _on_data(self, name: FormalName, meta_info: MetaInfo, - content: BinaryStr | None, sig: SignaturePtrs, raw_packet): - clean_list = [] - for prefix, node in self._int_tree.prefixes(name): - if node.satisfy((name, meta_info, content, sig, raw_packet), prefix != name): - clean_list.append(prefix) - for prefix in clean_list: - del self._int_tree[prefix] - - async def _on_interest(self, name: FormalName, param: InterestParam, - app_param: BinaryStr | None, sig: SignaturePtrs, raw_packet: BinaryStr): - trie_step = self._prefix_tree.longest_prefix(name) - if not trie_step: - self.logger.warning('No route: %s', name) - return - node = trie_step.value - if node.callback is None: - self.logger.warning('No callback: %s', name) - return - if app_param is not None or sig.signature_info is not None: - if not await params_sha256_checker(name, sig): - self.logger.warning('Drop malformed Interest: %s', name) - return - - # In case the validator blocks the pipeline, create a task - async def submit_interest(): - if sig.signature_info is not None: - validator = node.validator if node.validator else self.int_validator - valid = await validator(name, sig) - else: - valid = True - if not valid: - self.logger.warning('Drop unvalidated Interest: %s', name) - return - if node.extra_param: - kwargs = {} - if node.extra_param.get('raw_packet', False): - kwargs['raw_packet'] = raw_packet - if node.extra_param.get('sig_ptrs', False): - kwargs['sig_ptrs'] = sig - node.callback(name, param, app_param, **kwargs) - else: - node.callback(name, param, app_param) - aio.create_task(submit_interest()) diff --git a/src/ndn/app_support/keychain_register.py b/src/ndn/app_support/keychain_register.py index 8b47de9..2fb5ec9 100644 --- a/src/ndn/app_support/keychain_register.py +++ b/src/ndn/app_support/keychain_register.py @@ -16,7 +16,7 @@ # limitations under the License. # ----------------------------------------------------------------------------- import logging -from ..appv2 import NDNApp, ReplyFunc +from ..app import NDNApp, ReplyFunc from .. import security as sec from .. import encoding as enc from . import security_v2 as secv2 diff --git a/src/ndn/app_support/svs/sync.py b/src/ndn/app_support/svs/sync.py index a143fd9..4613e02 100644 --- a/src/ndn/app_support/svs/sync.py +++ b/src/ndn/app_support/svs/sync.py @@ -22,7 +22,7 @@ import asyncio as aio from enum import Enum from ... import encoding as enc -from ... import appv2 as app +from ... import app from .tlv import StateVec, StateVecWrapper, StateVecEntry diff --git a/src/ndn/bin/nfdc/cmd_get_face.py b/src/ndn/bin/nfdc/cmd_get_face.py index 79d090e..8854e0c 100644 --- a/src/ndn/bin/nfdc/cmd_get_face.py +++ b/src/ndn/bin/nfdc/cmd_get_face.py @@ -16,7 +16,7 @@ # limitations under the License. # ----------------------------------------------------------------------------- import argparse -from ...appv2 import NDNApp +from ...app import NDNApp from ...encoding import Name, Component from ...encoding.tlv_model import tlv_encode, tlv_parse from ...app_support.nfd_mgmt import FaceStatusMsg, FaceQueryFilter, FaceQueryFilterValue, parse_response diff --git a/src/ndn/bin/nfdc/cmd_get_route.py b/src/ndn/bin/nfdc/cmd_get_route.py index b14af2f..d178889 100644 --- a/src/ndn/bin/nfdc/cmd_get_route.py +++ b/src/ndn/bin/nfdc/cmd_get_route.py @@ -16,7 +16,7 @@ # limitations under the License. # ----------------------------------------------------------------------------- import argparse -from ...appv2 import NDNApp +from ...app import NDNApp from ...encoding import Name from ...encoding.tlv_model import tlv_parse from ...app_support.nfd_mgmt import FibStatus, RibStatus diff --git a/src/ndn/bin/nfdc/cmd_get_status.py b/src/ndn/bin/nfdc/cmd_get_status.py index babc3f7..b0e1f42 100644 --- a/src/ndn/bin/nfdc/cmd_get_status.py +++ b/src/ndn/bin/nfdc/cmd_get_status.py @@ -17,7 +17,7 @@ # ----------------------------------------------------------------------------- import argparse import datetime -from ...appv2 import NDNApp +from ...app import NDNApp from ...encoding.tlv_model import tlv_parse from ...app_support.nfd_mgmt import GeneralStatus from .utils import express_interest diff --git a/src/ndn/bin/nfdc/cmd_get_strategy.py b/src/ndn/bin/nfdc/cmd_get_strategy.py index c523139..3d242e6 100644 --- a/src/ndn/bin/nfdc/cmd_get_strategy.py +++ b/src/ndn/bin/nfdc/cmd_get_strategy.py @@ -16,7 +16,7 @@ # limitations under the License. # ----------------------------------------------------------------------------- import argparse -from ...appv2 import NDNApp +from ...app import NDNApp from ...encoding import Name from ...encoding.tlv_model import tlv_parse from ...app_support.nfd_mgmt import StrategyChoiceMsg diff --git a/src/ndn/bin/nfdc/cmd_new_face.py b/src/ndn/bin/nfdc/cmd_new_face.py index 0ef64f8..fdab7f8 100644 --- a/src/ndn/bin/nfdc/cmd_new_face.py +++ b/src/ndn/bin/nfdc/cmd_new_face.py @@ -16,7 +16,7 @@ # limitations under the License. # ----------------------------------------------------------------------------- import argparse -from ...appv2 import NDNApp +from ...app import NDNApp from ...app_support.nfd_mgmt import parse_response, make_command_v2 from .utils import express_interest diff --git a/src/ndn/bin/nfdc/cmd_new_route.py b/src/ndn/bin/nfdc/cmd_new_route.py index b929fe2..332b418 100644 --- a/src/ndn/bin/nfdc/cmd_new_route.py +++ b/src/ndn/bin/nfdc/cmd_new_route.py @@ -16,7 +16,7 @@ # limitations under the License. # ----------------------------------------------------------------------------- import argparse -from ...appv2 import NDNApp +from ...app import NDNApp from ...app_support.nfd_mgmt import make_command_v2, parse_response from .utils import express_interest diff --git a/src/ndn/bin/nfdc/cmd_remove_face.py b/src/ndn/bin/nfdc/cmd_remove_face.py index 9ddf141..368773d 100644 --- a/src/ndn/bin/nfdc/cmd_remove_face.py +++ b/src/ndn/bin/nfdc/cmd_remove_face.py @@ -16,7 +16,7 @@ # limitations under the License. # ----------------------------------------------------------------------------- import argparse -from ...appv2 import NDNApp +from ...app import NDNApp from ...encoding import Name, Component from ...encoding.tlv_model import tlv_encode, tlv_parse from ...app_support.nfd_mgmt import FaceStatusMsg, FaceQueryFilter, FaceQueryFilterValue, parse_response, \ diff --git a/src/ndn/bin/nfdc/cmd_remove_route.py b/src/ndn/bin/nfdc/cmd_remove_route.py index 6a44375..8c274c5 100644 --- a/src/ndn/bin/nfdc/cmd_remove_route.py +++ b/src/ndn/bin/nfdc/cmd_remove_route.py @@ -16,7 +16,7 @@ # limitations under the License. # ----------------------------------------------------------------------------- import argparse -from ...appv2 import NDNApp +from ...app import NDNApp from ...app_support.nfd_mgmt import parse_response, make_command_v2 from .utils import express_interest diff --git a/src/ndn/bin/nfdc/cmd_remove_strategy.py b/src/ndn/bin/nfdc/cmd_remove_strategy.py index 3fafeca..686be39 100644 --- a/src/ndn/bin/nfdc/cmd_remove_strategy.py +++ b/src/ndn/bin/nfdc/cmd_remove_strategy.py @@ -16,7 +16,7 @@ # limitations under the License. # ----------------------------------------------------------------------------- import argparse -from ...appv2 import NDNApp +from ...app import NDNApp from ...app_support.nfd_mgmt import parse_response, make_command_v2 from .utils import express_interest diff --git a/src/ndn/bin/nfdc/cmd_set_strategy.py b/src/ndn/bin/nfdc/cmd_set_strategy.py index d617690..14ab239 100644 --- a/src/ndn/bin/nfdc/cmd_set_strategy.py +++ b/src/ndn/bin/nfdc/cmd_set_strategy.py @@ -16,7 +16,7 @@ # limitations under the License. # ----------------------------------------------------------------------------- import argparse -from ...appv2 import NDNApp +from ...app import NDNApp from ...app_support.nfd_mgmt import parse_response, make_command_v2 from .utils import express_interest diff --git a/src/ndn/bin/nfdc/utils.py b/src/ndn/bin/nfdc/utils.py index ce086de..7cd30e7 100644 --- a/src/ndn/bin/nfdc/utils.py +++ b/src/ndn/bin/nfdc/utils.py @@ -15,7 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # ----------------------------------------------------------------------------- -from ...appv2 import NDNApp, pass_all +from ...app import NDNApp, pass_all from ...security import DigestSha256Signer from ...types import InterestNack, InterestTimeout, InterestCanceled, ValidationFailure diff --git a/src/ndn/bin/tools/cmd_fetch_data.py b/src/ndn/bin/tools/cmd_fetch_data.py index a819637..6a4f87b 100644 --- a/src/ndn/bin/tools/cmd_fetch_data.py +++ b/src/ndn/bin/tools/cmd_fetch_data.py @@ -19,7 +19,7 @@ import sys import argparse from ...encoding import Name, InterestParam -from ...appv2 import NDNApp, pass_all +from ...app import NDNApp, pass_all # from ...security import KeychainDigest from ...types import InterestNack, InterestTimeout, InterestCanceled, ValidationFailure diff --git a/src/ndn/bin/tools/cmd_fetch_rdrcontent.py b/src/ndn/bin/tools/cmd_fetch_rdrcontent.py index 62db5e9..f92c413 100644 --- a/src/ndn/bin/tools/cmd_fetch_rdrcontent.py +++ b/src/ndn/bin/tools/cmd_fetch_rdrcontent.py @@ -19,7 +19,7 @@ import sys import argparse from ...encoding import Name, Component -from ...appv2 import NDNApp, pass_all +from ...app import NDNApp, pass_all # from ...security import KeychainDigest from ...types import InterestTimeout, InterestNack, InterestCanceled, ValidationFailure diff --git a/src/ndn/bin/tools/cmd_serve_data.py b/src/ndn/bin/tools/cmd_serve_data.py index 44636df..4ee631b 100644 --- a/src/ndn/bin/tools/cmd_serve_data.py +++ b/src/ndn/bin/tools/cmd_serve_data.py @@ -19,7 +19,7 @@ import sys import argparse from ...encoding import Name, MetaInfo -from ...appv2 import NDNApp +from ...app import NDNApp from ...security import KeychainDigest diff --git a/src/ndn/bin/tools/cmd_serve_rdrcontent.py b/src/ndn/bin/tools/cmd_serve_rdrcontent.py index 3af5132..d6356be 100644 --- a/src/ndn/bin/tools/cmd_serve_rdrcontent.py +++ b/src/ndn/bin/tools/cmd_serve_rdrcontent.py @@ -19,7 +19,7 @@ import sys import argparse from ...encoding import Name, Component -from ...appv2 import NDNApp +from ...app import NDNApp from ...security import KeychainDigest from ...utils import timestamp diff --git a/src/ndn/security/validator/digest_validator.py b/src/ndn/security/validator/digest_validator.py index 7af61e0..f88581b 100644 --- a/src/ndn/security/validator/digest_validator.py +++ b/src/ndn/security/validator/digest_validator.py @@ -18,10 +18,10 @@ import logging from hashlib import sha256 from ...encoding import FormalName, SignatureType, Name, SignaturePtrs -from ...types import Validator +from ...types import Validator, ValidResult -async def sha256_digest_checker(name: FormalName, sig: SignaturePtrs) -> bool: +async def sha256_digest_checker(name: FormalName, sig: SignaturePtrs, _context=None) -> ValidResult: sig_info = sig.signature_info covered_part = sig.signature_covered_part sig_value = sig.signature_value_buf @@ -34,9 +34,9 @@ async def sha256_digest_checker(name: FormalName, sig: SignaturePtrs) -> bool: sha256_algo.update(blk) ret = sha256_algo.digest() == sig_value logging.getLogger(__name__).debug('Digest check %s -> %s', Name.to_str(name), ret) - return ret + return ValidResult.PASS if ret else ValidResult.FAIL else: - return True + return ValidResult.PASS # This is automatically called @@ -55,9 +55,12 @@ async def params_sha256_checker(name: FormalName, sig: SignaturePtrs) -> bool: def union_checker(*args) -> Validator: - async def wrapper(name: FormalName, sig: SignaturePtrs) -> bool: + async def wrapper(name: FormalName, sig: SignaturePtrs, context) -> ValidResult: for checker in args: - if not await checker(name, sig): - return False - return True + result = await checker(name, sig, context) + if result in (ValidResult.FAIL, ValidResult.TIMEOUT): + return result + if result is ValidResult.ALLOW_BYPASS: + return result + return ValidResult.PASS return wrapper diff --git a/src/ndn/security/validator/known_key_validator.py b/src/ndn/security/validator/known_key_validator.py index aa63819..2a21d8c 100644 --- a/src/ndn/security/validator/known_key_validator.py +++ b/src/ndn/security/validator/known_key_validator.py @@ -20,7 +20,7 @@ from Cryptodome.PublicKey import ECC, RSA from Cryptodome.Signature import DSS, pkcs1_15, eddsa from ...encoding import FormalName, BinaryStr, NonStrictName, SignaturePtrs, Name, SignatureType -from ...types import Validator +from ...types import Validator, ValidResult from ...app_support.security_v2 import parse_certificate @@ -69,14 +69,14 @@ def _verify(cls, pub_key_bits, sig_ptrs) -> bool: def from_key(cls, key_name: NonStrictName, pub_key_bits: BinaryStr) -> Validator: key_name = Name.normalize(key_name) - async def validator(_name: FormalName, sig_ptrs: SignaturePtrs) -> bool: + async def validator(_name: FormalName, sig_ptrs: SignaturePtrs, _context=None) -> ValidResult: if not sig_ptrs.signature_info or not sig_ptrs.signature_info.key_locator: - return False + return ValidResult.FAIL if not sig_ptrs.signature_info.key_locator.name: - return False + return ValidResult.FAIL if not Name.is_prefix(key_name, sig_ptrs.signature_info.key_locator.name): - return False - return cls._verify(pub_key_bits, sig_ptrs) + return ValidResult.FAIL + return ValidResult.PASS if cls._verify(pub_key_bits, sig_ptrs) else ValidResult.FAIL return validator diff --git a/tests/integration/app_test.py b/tests/integration/app_test.py index 1155f12..698a3ec 100644 --- a/tests/integration/app_test.py +++ b/tests/integration/app_test.py @@ -1,5 +1,5 @@ # ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors +# Copyright (C) 2019-2022 The python-ndn authors # # This file is part of python-ndn. # @@ -18,26 +18,30 @@ import abc import asyncio as aio import pytest -from ndn.app import NDNApp -from ndn.types import InterestCanceled, InterestNack, InterestTimeout, ValidationFailure -from ndn.encoding import Component, Name, FormalName, SignaturePtrs +from ndn import app +from ndn import security as sec +from ndn import encoding as enc +from ndn import types +from ndn.app_support import nfd_mgmt +from ndn.encoding import ndn_format_0_3 as fmt +from ndn.encoding.tlv_model import tlv_encode, tlv_parse from ndn.transport.dummy_face import DummyFace -from ndn.security import KeychainDigest +from ndn.transport.nfd_registerer import NfdRegister class NDNAppTestSuite: app = None + signer = None def test_main(self): aio.run(self.comain()) async def comain(self): face = DummyFace(self.face_proc) - keychain = KeychainDigest() - self.app = NDNApp(face, keychain) + self.signer = sec.DigestSha256Signer() + self.app = app.NDNApp(face) face.app = self.app await self.app.main_loop(self.app_main()) - # self.app.run_forever(after_start=self.app_main()) @abc.abstractmethod async def face_proc(self, face: DummyFace): @@ -57,11 +61,11 @@ async def face_proc(self, face: DummyFace): b'\x15\rHello, world!') async def app_main(self): - name = f'/example/testApp/randomData/{Component.TYPE_TIMESTAMP}=%00%00%01%6d%a4%f3%ff%6d' - data_name, meta_info, content = await self.app.express_interest( - name, must_be_fresh=True, can_be_prefix=False, lifetime=6000, nonce=None) - assert data_name == Name.from_str(name) - assert meta_info.freshness_period == 1000 + name = '/example/testApp/randomData/t=1570430517101' + data_name, content, pkt_context = await self.app.express( + name, app.pass_all, must_be_fresh=True, can_be_prefix=False, lifetime=6000, nonce=None) + assert data_name == enc.Name.from_str(name) + assert pkt_context['meta_info'].freshness_period == 1000 assert content == b'Hello, world!' @@ -70,22 +74,22 @@ async def face_proc(self, face: DummyFace): await face.consume_output(b'\x05\x15\x07\x0f\x08\rnot important\x0c\x02\x0f\xa0') async def app_main(self): - with pytest.raises(InterestCanceled): - await self.app.express_interest('not important', nonce=None) + with pytest.raises(types.InterestCanceled): + await self.app.express('not important', app.pass_all, nonce=None) class TestInterestNack(NDNAppTestSuite): async def face_proc(self, face: DummyFace): await face.consume_output(b'\x05)\x07\x1f\x08\tlocalhost\x08\x03nfd\x08\x05faces\x08\x06events' b'\x21\x00\x12\x00\x0c\x02\x03\xe8') - await face.input_packet(b'\x64\x36\xfd\x03 \x05\xfd\x03!\x01\x96' - b'P\x43\x05)\x07\x1f\x08\tlocalhost\x08\x03nfd\x08\x05faces\x08\x06events' + await face.input_packet(b'\x64\x36\xfd\x03\x20\x05\xfd\x03\x21\x01\x96' + b'\x50\x2b\x05)\x07\x1f\x08\tlocalhost\x08\x03nfd\x08\x05faces\x08\x06events' b'\x21\x00\x12\x00\x0c\x02\x03\xe8') async def app_main(self): - with pytest.raises(InterestNack) as nack: - await self.app.express_interest('/localhost/nfd/faces/events', nonce=None, lifetime=1000, - must_be_fresh=True, can_be_prefix=True) + with pytest.raises(types.InterestNack) as nack: + await self.app.express('/localhost/nfd/faces/events', app.pass_all, nonce=None, lifetime=1000, + must_be_fresh=True, can_be_prefix=True) assert nack.value.reason == 150 @@ -95,115 +99,99 @@ async def face_proc(self, face: DummyFace): await aio.sleep(0.05) async def app_main(self): - with pytest.raises(InterestTimeout): - await self.app.express_interest('not important', nonce=None, lifetime=10) + with pytest.raises(types.InterestTimeout): + await self.app.express('not important', app.pass_all, nonce=None, lifetime=10) class TestDataValidationFalure(NDNAppTestSuite): @staticmethod - async def validator(_name: FormalName, _sig: SignaturePtrs) -> bool: + async def validator(_name, _sig, _context) -> types.ValidResult: await aio.sleep(0.003) - return False + return types.ValidResult.FAIL async def face_proc(self, face: DummyFace): - await face.consume_output(b'\x05\x1b\x07\x10\x08\x03not\x08\timportant\n\x04\x00\x00\x00\x00\x0c\x01\x05') + await face.consume_output(b'\x05\x1b\x07\x10\x08\x03not\x08\timportant\n\x04\x00\x00\x00\x00\x0c\x01\xfa') await face.input_packet(b'\x06\x1d\x07\x10\x08\x03not\x08\timportant\x14\x03\x18\x01\x00\x15\x04test') async def app_main(self): - with pytest.raises(ValidationFailure) as e: - await self.app.express_interest('/not/important', nonce=0, lifetime=5, validator=self.validator) - assert e.value.name == Name.from_str('/not/important') + with pytest.raises(types.ValidationFailure) as e: + await self.app.express('/not/important', validator=self.validator, nonce=0, lifetime=250) + assert e.value.name == enc.Name.from_str('/not/important') assert e.value.content == b'test' + assert e.value.result == types.ValidResult.FAIL class TestInterestCanBePrefix(NDNAppTestSuite): async def face_proc(self, face: DummyFace): - await face.consume_output(b'\x05\x0a\x07\x05\x08\x03not\x0c\x01\x05' - b'\x05\x0c\x07\x05\x08\x03not\x21\x00\x0c\x01\x05' - b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\x05') + await face.consume_output(b'\x05\x0a\x07\x05\x08\x03not\x0c\x01\xfa' + b'\x05\x0c\x07\x05\x08\x03not\x21\x00\x0c\x01\xfa' + b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\xfa') await face.input_packet(b'\x06\x1d\x07\x10\x08\x03not\x08\timportant\x14\x03\x18\x01\x00\x15\x04test') - await aio.sleep(0.1) + await aio.sleep(0.4) async def app_main(self): - future1 = self.app.express_interest('/not', nonce=None, lifetime=5, can_be_prefix=False) - future2 = self.app.express_interest('/not', nonce=None, lifetime=5, can_be_prefix=True) - future3 = self.app.express_interest('/not/important', nonce=None, lifetime=5, can_be_prefix=False) - name2, _, content2 = await future3 - name1, _, content1 = await future2 - with pytest.raises(InterestTimeout): + future1 = self.app.express('/not', app.pass_all, nonce=None, lifetime=250, can_be_prefix=False) + future2 = self.app.express('/not', app.pass_all, nonce=None, lifetime=250, can_be_prefix=True) + future3 = self.app.express('/not/important', app.pass_all, nonce=None, lifetime=250, can_be_prefix=False) + name2, content2, _ = await future3 + name1, content1, _ = await future2 + with pytest.raises(types.InterestTimeout): await future1 - assert name1 == Name.from_str('/not/important') + assert name1 == enc.Name.from_str('/not/important') assert content1 == b'test' - assert name2 == Name.from_str('/not/important') + assert name2 == enc.Name.from_str('/not/important') assert content2 == b'test' class TestRoute(NDNAppTestSuite): async def face_proc(self, face: DummyFace): await face.ignore_output(0) - await face.input_packet(b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\x05') + await face.input_packet(b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\xfa') await face.consume_output(b'\x06\x24\x07\x10\x08\x03not\x08\timportant\x14\x03\x18\x01\x00\x15\x04test' b'\x16\x03\x1b\x01\xc8\x17\x00') async def app_main(self): @self.app.route('/not') - def on_interest(name, _param, _app_param): - self.app.put_data(name, b'test', no_signature=True) - - -class TestNoValidationNeededInterest(NDNAppTestSuite): - counter = 0 - - @staticmethod - async def validator(_name: FormalName, _sig: SignaturePtrs) -> bool: - return False - - async def face_proc(self, face: DummyFace): - await face.ignore_output(0) - await face.input_packet(b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\x05') - await aio.sleep(0.005) - assert self.counter == 1 - - async def app_main(self): - @self.app.route('/not', validator=self.validator) - def on_interest(_name, _param, _app_param): - self.counter += 1 + def on_interest(name, _app_param, reply: app.ReplyFunc, _context): + data = self.app.make_data(name, b'test', signer=sec.NullSigner()) + assert reply(data) class TestInvalidInterest(NDNAppTestSuite): @staticmethod - async def validator(_name: FormalName, _sig: SignaturePtrs) -> bool: - return False + async def validator(_name, _sig, _context) -> types.ValidResult: + await aio.sleep(0.003) + return types.ValidResult.FAIL async def face_proc(self, face: DummyFace): await face.ignore_output(0) await face.input_packet(b'\x05`\x072\x08\x03not\x08\timportant' b'\x02 E\x8a\xeaxI}[\xb1\xcd\xf0\x01\xbe' b'\xdb\xe9\x03\x085\xb1g+K\xa8jK,\xd0\xad' - b')\x07\x83\x96\xbb\x0c\x01\x05$\x00,\x03' + b')\x07\x83\x96\xbb\x0c\x01\xfa$\x00,\x03' b'\x1b\x01\x00. !\x93!zG[%\xcfs\xe89\\\x8f' b'^\xd3\xa4\xb9\x13\xaa\x7f\xa6?\xd7\x13aVyS\xdc\x1dW\xea') await aio.sleep(0.005) async def app_main(self): @self.app.route('/not', validator=self.validator) - def on_interest(_name, _param, _app_param): + def on_interest(_name, _app_param, _reply: app.ReplyFunc, _context): raise ValueError('This test fails') class TestRoute2(NDNAppTestSuite): async def face_proc(self, face: DummyFace): await face.ignore_output(0) - await face.input_packet(b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\x05') + await face.input_packet(b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\xfa') await face.consume_output(b'\x06\x24\x07\x10\x08\x03not\x08\timportant\x14\x03\x18\x01\x00\x15\x04test' b'\x16\x03\x1b\x01\xc8\x17\x00') async def app_main(self): - @self.app.route('/not', need_raw_packet=True, need_sig_ptrs=True) - def on_interest(name, _param, _app_param, raw_packet, sig_ptrs): - assert raw_packet == b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\x05' - assert not sig_ptrs.signature_info - self.app.put_data(name, b'test', no_signature=True) + @self.app.route('/not') + def on_interest(name, _app_param, reply: app.ReplyFunc, context): + assert context['raw_packet'] == b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\xfa' + assert not context['sig_ptrs'].signature_info + reply(self.app.make_data(name, b'test', signer=sec.NullSigner())) class TestConsumerRawPacket(NDNAppTestSuite): @@ -215,9 +203,11 @@ async def face_proc(self, face: DummyFace): b'\x15\rHello, world!') async def app_main(self): - name = f'/example/testApp/randomData/{Component.TYPE_TIMESTAMP}=%00%00%01%6d%a4%f3%ff%6d' - _, _, _, raw = await self.app.express_interest( - name, must_be_fresh=True, can_be_prefix=False, lifetime=6000, nonce=None, need_raw_packet=True) + name = '/example/testApp/randomData/t=1570430517101' + _, _, pkt_context = await self.app.express( + name, validator=app.pass_all, + must_be_fresh=True, can_be_prefix=False, lifetime=6000, nonce=None, need_raw_packet=True) + raw = pkt_context['raw_packet'] assert (raw == b'\x06\x42\x07(\x08\x07example\x08\x07testApp\x08\nrandomData' b'\x38\x08\x00\x00\x01m\xa4\xf3\xffm\x14\x07\x18\x01\x00\x19\x02\x03\xe8' b'\x15\rHello, world!') @@ -227,16 +217,16 @@ class TestCongestionMark(NDNAppTestSuite): async def face_proc(self, face: DummyFace): await face.ignore_output(0) await face.input_packet(b'\x64\x1e\xfd\x03\x40\x01\x01\x50\x17' - b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\x05') + b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\xfa') await face.consume_output(b'\x06\x24\x07\x10\x08\x03not\x08\timportant\x14\x03\x18\x01\x00\x15\x04test' - b'\x16\x03\x1b\x01\xc8\x17\x00') + b'\x16\x03\x1b\x01\xc8\x17\x00', timeout=0.5) async def app_main(self): - @self.app.route('/not', need_raw_packet=True, need_sig_ptrs=True) - def on_interest(name, _param, _app_param, raw_packet, sig_ptrs): - assert raw_packet == b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\x05' - assert not sig_ptrs.signature_info - self.app.put_data(name, b'test', no_signature=True) + @self.app.route('/not') + def on_interest(name, _app_param, reply: app.ReplyFunc, context): + assert context['raw_packet'] == b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\xfa' + assert not context['sig_ptrs'].signature_info + reply(self.app.make_data(name, b'test', signer=sec.NullSigner())) class TestImplicitSha256(NDNAppTestSuite): @@ -244,23 +234,62 @@ async def face_proc(self, face: DummyFace): await face.consume_output(b'\x05\x2d\x07\x28\x08\x04test\x01\x20' b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' - b'\x0c\x01\x05' + b'\x0c\x01\xfa' b'\x05\x2d\x07\x28\x08\x04test\x01\x20' b'\x54\x88\xf2\xc1\x1b\x56\x6d\x49\xe9\x90\x4f\xb5\x2a\xa6\xf6\xf9' b'\xe6\x6a\x95\x41\x68\x10\x9c\xe1\x56\xee\xa2\xc9\x2c\x57\xe4\xc2' - b'\x0c\x01\x05') + b'\x0c\x01\xfa') await face.input_packet(b'\x06\x13\x07\x06\x08\x04test\x14\x03\x18\x01\x00\x15\x04test') - await aio.sleep(0.1) + await aio.sleep(0.4) async def app_main(self): - fut1 = self.app.express_interest( + fut1 = self.app.express( '/test/sha256digest=FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', - nonce=None, lifetime=5) - fut2 = self.app.express_interest( + validator=app.pass_all, nonce=None, lifetime=250) + fut2 = self.app.express( '/test/sha256digest=5488f2c11b566d49e9904fb52aa6f6f9e66a954168109ce156eea2c92c57e4c2', - nonce=None, lifetime=5) - name2, _, content2 = await fut2 - with pytest.raises(InterestTimeout): + validator=app.pass_all, nonce=None, lifetime=250) + name2, content2, _ = await fut2 + with pytest.raises(types.InterestTimeout): await fut1 - assert name2 == Name.from_str('/test') + assert name2 == enc.Name.from_str('/test') assert content2 == b'test' + + +class TestPitToken(NDNAppTestSuite): + async def face_proc(self, face: DummyFace): + await face.ignore_output(0) + await face.input_packet(b'\x64\x1f\x62\x04\x01\x02\x03\x04\x50\x17' + b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\xfa') + await face.consume_output(b'\x64\x2e\x62\x04\x01\x02\x03\x04\x50\x26' + b'\x06\x24\x07\x10\x08\x03not\x08\timportant\x14\x03\x18\x01\x00\x15\x04test' + b'\x16\x03\x1b\x01\xc8\x17\x00') + + async def app_main(self): + @self.app.route('/not') + def on_interest(name, _app_param, reply: app.ReplyFunc, _context): + data = self.app.make_data(name, b'test', signer=sec.NullSigner()) + assert reply(data) + + +class TestRegister(NDNAppTestSuite): + async def face_proc(self, face: DummyFace): + async with aio.timeout(1): + while not face.output_buf: + await aio.sleep(0.001) + interest, face.output_buf = face.output_buf, b'' + name, _, app_param, sig = fmt.parse_interest(interest) + assert name[:4] == enc.Name.from_str('/localhost/nfd/rib/register') + cp = tlv_parse(nfd_mgmt.ControlParameters, enc.Component.get_value(name[4])) + assert cp.cp.name == enc.Name.from_str('/test/prefix') + assert app_param == b'' + assert sig.signature_info.signature_type == fmt.SignatureType.DIGEST_SHA256 + + response = tlv_encode(nfd_mgmt.ControlResponse( + status_code=200, status_text='OK', body=nfd_mgmt.ControlParametersValue(name='/test/prefix'))) + content = bytes([0x65, len(response)]) + response + await face.input_packet(fmt.make_data(name, fmt.MetaInfo(), content, signer=sec.NullSigner())) + + async def app_main(self): + assert isinstance(self.app.registerer, NfdRegister) + assert await self.app.register('/test/prefix') diff --git a/tests/security/signer_test.py b/tests/security/signer_test.py index b283dfe..0de7acc 100644 --- a/tests/security/signer_test.py +++ b/tests/security/signer_test.py @@ -22,6 +22,7 @@ from ndn.security import Sha256WithEcdsaSigner, Sha256WithRsaSigner, HmacSha256Signer, \ EccChecker, RsaChecker, HmacChecker from ndn.security import Ed25519Signer, Ed25519Checker +from ndn.types import ValidResult class TestSha256WithEcdsaSigner: @@ -36,7 +37,7 @@ def test_verify(self): # Test its format is ASN.1 der format DerSequence().decode(bytes(sig_ptrs.signature_value_buf)) validator = EccChecker.from_key("/K/KEY/x", bytes(pub_key.export_key(format='DER'))) - assert aio.run(validator(Name.from_str("/test"), sig_ptrs)) + assert aio.run(validator(Name.from_str("/test"), sig_ptrs, {})) is ValidResult.PASS class TestSha256WithHmacSigner: @@ -75,7 +76,7 @@ def test_data_1(self): '172019868e7183998df373332f3dd1c9c950fc29d734c07977791d8396fa3b91fd36') _, _, _, sig_ptrs = parse_data(data) validator = HmacChecker.from_key('key1', key) - assert aio.run(validator(Name.from_str('/ndn/abc'), sig_ptrs)) + assert aio.run(validator(Name.from_str('/ndn/abc'), sig_ptrs, {})) is ValidResult.PASS class TestSha256WithRsaSigner: @@ -177,7 +178,7 @@ def test_data(self): 'abb372a3aaf43307331a2796e913e3d36150f6a387b4c97c19a493bb4513af3f') validator = RsaChecker.from_key('/testname/KEY/123', key) _, _, _, sig_ptrs = parse_data(data) - assert aio.run(validator(Name.from_str('/ndn/abc'), sig_ptrs)) + assert aio.run(validator(Name.from_str('/ndn/abc'), sig_ptrs, {})) is ValidResult.PASS class TestEd25519: @@ -190,4 +191,4 @@ def test_verify(self): _, _, _, sig_ptrs = parse_data(pkt) pub_bits = pub_key.public_key().export_key(format='DER') validator = Ed25519Checker.from_key("/K/KEY/x", bytes(pub_bits)) - assert aio.run(validator(Name.from_str("/test"), sig_ptrs)) + assert aio.run(validator(Name.from_str("/test"), sig_ptrs, {})) is ValidResult.PASS From ac293399170228dbda1d9aa9fe8212700b8ded76 Mon Sep 17 00:00:00 2001 From: Xinyu Ma Date: Wed, 7 Oct 2026 23:32:27 -0700 Subject: [PATCH 5/8] app: remove legacy application surfaces Co-authored-by: Cursor --- .../{appv2 => app}/basic_packets/consumer.py | 6 +- .../{appv2 => app}/basic_packets/producer.py | 6 +- .../forwarding_hint/consumer.py | 8 +- .../forwarding_hint/producer.py | 8 +- .../keychain_cert/fetch_certificate.py | 6 +- .../keychain_cert/keychain_register.py | 4 +- examples/{appv2 => app}/svs/sync_example.py | 8 +- examples/catchunks.py | 44 - examples/consumer.py | 58 -- examples/dpdk_experimental/udp_consumer.py | 6 +- examples/dpdk_experimental/udp_producer.py | 6 +- examples/lvs/consumer.py | 68 -- examples/lvs/producer.py | 120 --- examples/nfd_status.py | 85 -- examples/producer.py | 44 - examples/putchunks.py | 63 -- examples/rdrnode.py | 86 -- examples/rpc_consumer.py | 62 -- examples/rpc_producer.py | 49 -- src/ndn/app_support/dispatcher.py | 66 -- src/ndn/app_support/light_versec/__init__.py | 1 - src/ndn/app_support/light_versec/validator.py | 56 -- src/ndn/app_support/segment_fetcher.py | 72 -- src/ndn/appv2.py | 756 ------------------ src/ndn/bin/nfdc/cmd_get_face.py | 6 +- src/ndn/bin/nfdc/cmd_get_route.py | 6 +- src/ndn/bin/nfdc/cmd_get_status.py | 4 +- src/ndn/bin/nfdc/cmd_get_strategy.py | 4 +- src/ndn/bin/nfdc/cmd_new_face.py | 4 +- src/ndn/bin/nfdc/cmd_new_route.py | 4 +- src/ndn/bin/nfdc/cmd_remove_face.py | 6 +- src/ndn/bin/nfdc/cmd_remove_route.py | 4 +- src/ndn/bin/nfdc/cmd_remove_strategy.py | 4 +- src/ndn/bin/nfdc/cmd_set_strategy.py | 4 +- src/ndn/bin/nfdc/utils.py | 2 +- src/ndn/name_tree.py | 68 +- src/ndn/schema/__init__.py | 0 src/ndn/schema/policy.py | 133 --- src/ndn/schema/schema_tree.py | 597 -------------- src/ndn/schema/simple_cache.py | 69 -- src/ndn/schema/simple_node.py | 179 ----- src/ndn/schema/simple_trust.py | 132 --- src/ndn/schema/util.py | 63 -- src/ndn/security/validator/__init__.py | 4 +- .../security/validator/cascade_validator.py | 122 --- .../security/validator/digest_validator.py | 14 +- .../security/validator/known_key_validator.py | 6 +- src/ndn/types.py | 13 +- tests/integration/app_v2_test.py | 268 ------- 49 files changed, 62 insertions(+), 3342 deletions(-) rename examples/{appv2 => app}/basic_packets/consumer.py (95%) rename examples/{appv2 => app}/basic_packets/producer.py (93%) rename examples/{appv2 => app}/forwarding_hint/consumer.py (94%) rename examples/{appv2 => app}/forwarding_hint/producer.py (88%) rename examples/{appv2 => app}/keychain_cert/fetch_certificate.py (95%) rename examples/{appv2 => app}/keychain_cert/keychain_register.py (96%) rename examples/{appv2 => app}/svs/sync_example.py (97%) delete mode 100644 examples/catchunks.py delete mode 100644 examples/consumer.py delete mode 100644 examples/lvs/consumer.py delete mode 100644 examples/lvs/producer.py delete mode 100644 examples/nfd_status.py delete mode 100644 examples/producer.py delete mode 100644 examples/putchunks.py delete mode 100644 examples/rdrnode.py delete mode 100644 examples/rpc_consumer.py delete mode 100644 examples/rpc_producer.py delete mode 100644 src/ndn/app_support/dispatcher.py delete mode 100644 src/ndn/app_support/light_versec/validator.py delete mode 100644 src/ndn/app_support/segment_fetcher.py delete mode 100644 src/ndn/appv2.py delete mode 100644 src/ndn/schema/__init__.py delete mode 100644 src/ndn/schema/policy.py delete mode 100644 src/ndn/schema/schema_tree.py delete mode 100644 src/ndn/schema/simple_cache.py delete mode 100644 src/ndn/schema/simple_node.py delete mode 100644 src/ndn/schema/simple_trust.py delete mode 100644 src/ndn/schema/util.py delete mode 100644 src/ndn/security/validator/cascade_validator.py delete mode 100644 tests/integration/app_v2_test.py diff --git a/examples/appv2/basic_packets/consumer.py b/examples/app/basic_packets/consumer.py similarity index 95% rename from examples/appv2/basic_packets/consumer.py rename to examples/app/basic_packets/consumer.py index cd3a0cd..563a799 100644 --- a/examples/appv2/basic_packets/consumer.py +++ b/examples/app/basic_packets/consumer.py @@ -16,7 +16,7 @@ # limitations under the License. # ----------------------------------------------------------------------------- import logging -from ndn import utils, appv2, types +from ndn import utils, app, types from ndn import encoding as enc @@ -26,7 +26,7 @@ style='{') -app = appv2.NDNApp() +app = app.NDNApp() async def main(): @@ -36,7 +36,7 @@ async def main(): print(f'Sending Interest {enc.Name.to_str(name)}, {enc.InterestParam(must_be_fresh=True, lifetime=6000)}') # TODO: Write a better validator data_name, content, pkt_context = await app.express( - name, validator=appv2.pass_all, + name, validator=app.pass_all, must_be_fresh=True, can_be_prefix=False, lifetime=6000) print(f'Received Data Name: {enc.Name.to_str(data_name)}') diff --git a/examples/appv2/basic_packets/producer.py b/examples/app/basic_packets/producer.py similarity index 93% rename from examples/appv2/basic_packets/producer.py rename to examples/app/basic_packets/producer.py index ae4851a..a3d4f82 100644 --- a/examples/appv2/basic_packets/producer.py +++ b/examples/app/basic_packets/producer.py @@ -16,7 +16,7 @@ # limitations under the License. # ----------------------------------------------------------------------------- import logging -from ndn import appv2 +from ndn import app from ndn import encoding as enc @@ -26,13 +26,13 @@ style='{') -app = appv2.NDNApp() +app = app.NDNApp() keychain = app.default_keychain() @app.route('/example/testApp') def on_interest(name: enc.FormalName, _app_param: enc.BinaryStr | None, - reply: appv2.ReplyFunc, context: appv2.PktContext): + reply: app.ReplyFunc, context: app.PktContext): print(f'>> I: {enc.Name.to_str(name)}, {context["int_param"]}') content = b"Hello, world!" reply(app.make_data(name, content=content, signer=keychain.get_signer({}), diff --git a/examples/appv2/forwarding_hint/consumer.py b/examples/app/forwarding_hint/consumer.py similarity index 94% rename from examples/appv2/forwarding_hint/consumer.py rename to examples/app/forwarding_hint/consumer.py index 9fdbb78..aa9545e 100644 --- a/examples/appv2/forwarding_hint/consumer.py +++ b/examples/app/forwarding_hint/consumer.py @@ -16,7 +16,7 @@ # limitations under the License. # ----------------------------------------------------------------------------- import logging -from ndn import utils, appv2, types +from ndn import utils, app, types from ndn import encoding as enc @@ -26,7 +26,7 @@ style='{') -app = appv2.NDNApp() +app = app.NDNApp() async def express_int(name, fw_hint): @@ -34,13 +34,13 @@ async def express_int(name, fw_hint): if fw_hint is None: print(f'Sending Interest {enc.Name.to_str(name)}, {enc.InterestParam(must_be_fresh=True, lifetime=6000)}') data_name, content, pkt_context = await app.express( - name, validator=appv2.pass_all, + name, validator=app.pass_all, must_be_fresh=True, can_be_prefix=False, lifetime=6000) else: print(f'Sending Interest {enc.Name.to_str(name)}, ' f'{enc.InterestParam(must_be_fresh=True, lifetime=6000, forwarding_hint=[fw_hint])}') data_name, content, pkt_context = await app.express( - name, validator=appv2.pass_all, + name, validator=app.pass_all, must_be_fresh=True, can_be_prefix=False, lifetime=6000, forwarding_hint=[fw_hint]) print(f'Received Data Name: {enc.Name.to_str(data_name)}') diff --git a/examples/appv2/forwarding_hint/producer.py b/examples/app/forwarding_hint/producer.py similarity index 88% rename from examples/appv2/forwarding_hint/producer.py rename to examples/app/forwarding_hint/producer.py index dcd0b66..b712ec5 100644 --- a/examples/appv2/forwarding_hint/producer.py +++ b/examples/app/forwarding_hint/producer.py @@ -1,5 +1,5 @@ import logging -from ndn import appv2 +from ndn import app from ndn import encoding as enc @@ -9,13 +9,13 @@ style='{') -app = appv2.NDNApp() +app = app.NDNApp() keychain = app.default_keychain() @app.route('/repo/command') def on_cmd(name: enc.FormalName, _app_param: enc.BinaryStr | None, - reply: appv2.ReplyFunc, context: appv2.PktContext): + reply: app.ReplyFunc, context: app.PktContext): print(f'>> I: {enc.Name.to_str(name)}, {context["int_param"]}') content = b"Hello, world!" reply(app.make_data(name, content=content, signer=keychain.get_signer({}), @@ -30,7 +30,7 @@ def on_cmd(name: enc.FormalName, _app_param: enc.BinaryStr | None, # So we can dispatch by forwarding hints. @app.route('/') def on_fwd_hint(name: enc.FormalName, app_param: enc.BinaryStr | None, - reply: appv2.ReplyFunc, context: appv2.PktContext): + reply: app.ReplyFunc, context: app.PktContext): fwd_hints = context["int_param"].forwarding_hint if fwd_hints: fh_name = fwd_hints[0] diff --git a/examples/appv2/keychain_cert/fetch_certificate.py b/examples/app/keychain_cert/fetch_certificate.py similarity index 95% rename from examples/appv2/keychain_cert/fetch_certificate.py rename to examples/app/keychain_cert/fetch_certificate.py index 3d17f15..46b533f 100644 --- a/examples/appv2/keychain_cert/fetch_certificate.py +++ b/examples/app/keychain_cert/fetch_certificate.py @@ -17,7 +17,7 @@ # ----------------------------------------------------------------------------- import sys import logging -from ndn import appv2, types +from ndn import app, types from ndn import encoding as enc from ndn.app_support import security_v2 as secv2 @@ -32,7 +32,7 @@ logging.fatal('Please input a KEY or CERT name') exit(0) -app = appv2.NDNApp() +app = app.NDNApp() async def main(): @@ -43,7 +43,7 @@ async def main(): f'{enc.InterestParam(must_be_fresh=True, can_be_prefix=can_be_prefix, lifetime=6000)}') # TODO: Write a better validator data_name, content, pkt_context = await app.express( - name, validator=appv2.pass_all, + name, validator=app.pass_all, must_be_fresh=True, can_be_prefix=can_be_prefix, lifetime=6000) print(f'Received Data Name: {enc.Name.to_str(data_name)}') diff --git a/examples/appv2/keychain_cert/keychain_register.py b/examples/app/keychain_cert/keychain_register.py similarity index 96% rename from examples/appv2/keychain_cert/keychain_register.py rename to examples/app/keychain_cert/keychain_register.py index ce0b376..d291aa8 100644 --- a/examples/appv2/keychain_cert/keychain_register.py +++ b/examples/app/keychain_cert/keychain_register.py @@ -16,7 +16,7 @@ # limitations under the License. # ----------------------------------------------------------------------------- import logging -from ndn import appv2 +from ndn import app from ndn.app_support.keychain_register import attach_keychain_register @@ -26,7 +26,7 @@ style='{') -app = appv2.NDNApp() +app = app.NDNApp() keychain = app.default_keychain() attach_keychain_register(keychain, app) diff --git a/examples/appv2/svs/sync_example.py b/examples/app/svs/sync_example.py similarity index 97% rename from examples/appv2/svs/sync_example.py rename to examples/app/svs/sync_example.py index 256795f..f10127a 100644 --- a/examples/appv2/svs/sync_example.py +++ b/examples/app/svs/sync_example.py @@ -1,6 +1,6 @@ import logging import asyncio as aio -from ndn import appv2 +from ndn import app from ndn import encoding as enc from ndn import types from ndn import security as sec @@ -25,7 +25,7 @@ def on_missing_data(_svs_inst: svs.SvsInst): fetch_signal.set() -app = appv2.NDNApp() +app = app.NDNApp() keychain = app.default_keychain() text_node_id = f'node-{random.randbytes(4).hex()}' name_node_id = enc.Name.from_str(text_node_id) @@ -34,7 +34,7 @@ def on_missing_data(_svs_inst: svs.SvsInst): group_prefix, name_node_id, on_missing_data, sec.DigestSha256Signer(), - appv2.pass_all, + app.pass_all, sync_interval=10, ) packet_cache = {} @@ -64,7 +64,7 @@ async def fetch_missing_data(): for i in range(fetched_seq+1, seq+1): pkt_name = node_name + [enc.Component.from_sequence_num(i)] try: - _, data, _ = await app.express(pkt_name, appv2.pass_all) + _, data, _ = await app.express(pkt_name, app.pass_all) logging.info(f'Fetched {enc.Name.to_str(pkt_name)}: {bytes(data).decode()}') except types.InterestNack as e: logging.info(f'[{enc.Name.to_str(pkt_name)}] Nacked with reason={e.reason}') diff --git a/examples/catchunks.py b/examples/catchunks.py deleted file mode 100644 index 0ed3b85..0000000 --- a/examples/catchunks.py +++ /dev/null @@ -1,44 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -import logging -import sys -from ndn.app import NDNApp -from ndn.app_support.segment_fetcher import segment_fetcher - - -logging.basicConfig(format='[{asctime}]{levelname}:{message}', - datefmt='%Y-%m-%d %H:%M:%S', - level=logging.INFO, - style='{') -app = NDNApp() - - -async def main(): - cnt = 0 - async for seg in segment_fetcher(app, sys.argv[1]): - print(bytes(seg).decode(), end='') - cnt += 1 - print(f'\n{cnt} segments fetched.') - app.shutdown() - - -if __name__ == '__main__': - if len(sys.argv) <= 1: - print(f'Usage: {sys.argv[0]} ') - exit(0) - app.run_forever(after_start=main()) diff --git a/examples/consumer.py b/examples/consumer.py deleted file mode 100644 index a701a08..0000000 --- a/examples/consumer.py +++ /dev/null @@ -1,58 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -import logging -import ndn.utils -from ndn.app import NDNApp -from ndn.types import InterestNack, InterestTimeout, InterestCanceled, ValidationFailure -from ndn.encoding import Name, Component, InterestParam - - -logging.basicConfig(format='[{asctime}]{levelname}:{message}', - datefmt='%Y-%m-%d %H:%M:%S', - level=logging.INFO, - style='{') - - -app = NDNApp() - - -async def main(): - try: - timestamp = ndn.utils.timestamp() - name = Name.from_str('/example/testApp/randomData') + [Component.from_timestamp(timestamp)] - print(f'Sending Interest {Name.to_str(name)}, {InterestParam(must_be_fresh=True, lifetime=6000)}') - data_name, meta_info, content = await app.express_interest( - name, must_be_fresh=True, can_be_prefix=False, lifetime=6000) - - print(f'Received Data Name: {Name.to_str(data_name)}') - print(meta_info) - print(bytes(content) if content else None) - except InterestNack as e: - print(f'Nacked with reason={e.reason}') - except InterestTimeout: - print(f'Timeout') - except InterestCanceled: - print(f'Canceled') - except ValidationFailure: - print(f'Data failed to validate') - finally: - app.shutdown() - - -if __name__ == '__main__': - app.run_forever(after_start=main()) diff --git a/examples/dpdk_experimental/udp_consumer.py b/examples/dpdk_experimental/udp_consumer.py index cba3ae3..bdfbdb5 100644 --- a/examples/dpdk_experimental/udp_consumer.py +++ b/examples/dpdk_experimental/udp_consumer.py @@ -17,7 +17,7 @@ # ----------------------------------------------------------------------------- import logging import sys -from ndn import utils, appv2, types +from ndn import utils, app, types from ndn import encoding as enc from ndn.transport.ndn_dpdk import NdnDpdkUdpFace, DpdkRegisterer @@ -42,7 +42,7 @@ face = NdnDpdkUdpFace(gql_url, self_addr, self_port, dpdk_addr, dpdk_port) registerer = DpdkRegisterer(face) -app = appv2.NDNApp(face=face, registerer=registerer) +app = app.NDNApp(face=face, registerer=registerer) keychain = app.default_keychain() @@ -53,7 +53,7 @@ async def main(): print(f'Sending Interest {enc.Name.to_str(name)}, {enc.InterestParam(must_be_fresh=True, lifetime=6000)}') # TODO: Write a better validator data_name, content, pkt_context = await app.express( - name, validator=appv2.pass_all, + name, validator=app.pass_all, must_be_fresh=True, can_be_prefix=False, lifetime=6000) print(f'Received Data Name: {enc.Name.to_str(data_name)}') diff --git a/examples/dpdk_experimental/udp_producer.py b/examples/dpdk_experimental/udp_producer.py index 4a0ef8d..6b77e09 100644 --- a/examples/dpdk_experimental/udp_producer.py +++ b/examples/dpdk_experimental/udp_producer.py @@ -17,7 +17,7 @@ # ----------------------------------------------------------------------------- import logging import sys -from ndn import appv2 +from ndn import app from ndn import encoding as enc from ndn.transport.ndn_dpdk import NdnDpdkUdpFace, DpdkRegisterer @@ -42,13 +42,13 @@ face = NdnDpdkUdpFace(gql_url, self_addr, self_port, dpdk_addr, dpdk_port) registerer = DpdkRegisterer(face) -app = appv2.NDNApp(face=face, registerer=registerer) +app = app.NDNApp(face=face, registerer=registerer) keychain = app.default_keychain() @app.route('/example/testApp') def on_interest(name: enc.FormalName, _app_param: enc.BinaryStr | None, - reply: appv2.ReplyFunc, context: appv2.PktContext): + reply: app.ReplyFunc, context: app.PktContext): print(f'>> I: {enc.Name.to_str(name)}, {context["int_param"]}') content = b"Hello, world!" reply(app.make_data(name, content=content, signer=keychain.get_signer({}), diff --git a/examples/lvs/consumer.py b/examples/lvs/consumer.py deleted file mode 100644 index 317d2ee..0000000 --- a/examples/lvs/consumer.py +++ /dev/null @@ -1,68 +0,0 @@ -import os -import sys -import logging -from ndn.encoding import Name -from ndn.security import TpmFile, KeychainSqlite3 -from ndn.app import NDNApp, InterestNack, InterestTimeout, InterestCanceled, ValidationFailure -from ndn.app_support.light_versec import compile_lvs, Checker, DEFAULT_USER_FNS, lvs_validator - - -logging.basicConfig(format='[{asctime}]{levelname}:{message}', - datefmt='%Y-%m-%d %H:%M:%S', - level=logging.INFO, - style='{') - -lvs_text = r''' -#KEY: "KEY"/_/_/_ -#site: "lvs-test" -#article: #site/"article"/author/post/_version & {_version: $eq_type("v=0")} <= #author -#author: #site/"author"/author/"KEY"/_/admin/_ <= #admin -#admin: #site/"admin"/admin/#KEY <= #root -#root: #site/#KEY -''' - - -def main(): - basedir = os.path.dirname(os.path.abspath(sys.argv[0])) - tpm_path = os.path.join(basedir, 'privKeys') - pib_path = os.path.join(basedir, 'pib.db') - keychain = KeychainSqlite3(pib_path, TpmFile(tpm_path)) - - trust_anchor = keychain['/lvs-test'].default_key().default_cert() - print(f'Trust anchor name: {Name.to_str(trust_anchor.name)}') - - lvs_model = compile_lvs(lvs_text) - checker = Checker(lvs_model, DEFAULT_USER_FNS) - app = NDNApp(keychain=keychain) - validator = lvs_validator(checker, app, trust_anchor.data) - - async def fetch_interest(article: str): - try: - name = Name.from_str(f'/lvs-test/article/xinyu/{article}') - print(f'Sending Interest {Name.to_str(name)}') - data_name, meta_info, content = await app.express_interest( - name, must_be_fresh=True, can_be_prefix=True, lifetime=6000, - validator=validator) - print(f'Received Data Name: {Name.to_str(data_name)}') - print(meta_info) - print(bytes(content).decode() if content else None) - except InterestNack as e: - print(f'Nacked with reason={e.reason}') - except InterestTimeout: - print(f'Timeout') - except InterestCanceled: - print(f'Canceled') - except ValidationFailure: - print(f'Data failed to validate') - - async def ndn_main(): - await fetch_interest('hello') - await fetch_interest('world') - - app.shutdown() - - app.run_forever(ndn_main()) - - -if __name__ == '__main__': - main() diff --git a/examples/lvs/producer.py b/examples/lvs/producer.py deleted file mode 100644 index ad9b265..0000000 --- a/examples/lvs/producer.py +++ /dev/null @@ -1,120 +0,0 @@ -import os -import sys -import logging -from ndn.utils import timestamp -from ndn.encoding import Name, Component -from ndn.security import TpmFile, KeychainSqlite3 -from ndn.app import NDNApp -from ndn.app_support.light_versec import compile_lvs, Checker, DEFAULT_USER_FNS - - -logging.basicConfig(format='[{asctime}]{levelname}:{message}', - datefmt='%Y-%m-%d %H:%M:%S', - level=logging.INFO, - style='{') - -lvs_text = r''' -#KEY: "KEY"/_/_/_ -#site: "lvs-test" -#article: #site/"article"/author/post/_version & {_version: $eq_type("v=0")} <= #author -#author: #site/"author"/author/"KEY"/_/admin/_ <= #admin -#admin: #site/"admin"/admin/#KEY <= #root -#root: #site/#KEY -''' - - -def main(): - basedir = os.path.dirname(os.path.abspath(sys.argv[0])) - tpm_path = os.path.join(basedir, 'privKeys') - pib_path = os.path.join(basedir, 'pib.db') - keychain = KeychainSqlite3(pib_path, TpmFile(tpm_path)) - - trust_anchor = keychain['/lvs-test'].default_key().default_cert() - admin_cert = keychain['/lvs-test/admin/ndn'].default_key().default_cert() - author_cert = keychain['/lvs-test/author/xinyu'].default_key().default_cert() - print(f'Trust anchor name: {Name.to_str(trust_anchor.name)}') - print(f'Admin name: {Name.to_str(admin_cert.name)}') - print(f'Author name: {Name.to_str(author_cert.name)}') - - lvs_model = compile_lvs(lvs_text) - checker = Checker(lvs_model, DEFAULT_USER_FNS) - # The following manual checks are listed for demonstration only. - # In real implementation they are automatically done - root_of_trust = checker.root_of_trust() - print(f'LVS model root of trust: {root_of_trust}') - print(f'LVS model user functions provided: {checker.validate_user_fns()}') - ta_matches = sum((m[0] for m in checker.match(trust_anchor.name)), start=[]) - assert len(ta_matches) > 0 - assert root_of_trust.issubset(ta_matches) - print(f'Trust anchor matches the root of trust: OK') - - app = NDNApp(keychain=keychain) - - # Note: This producer example does not use LVS validator at all - # Also, the content of keychain is as follows: - # /lvs-test - # +->* /lvs-test/KEY/%5Cs%F8%B5%D9k%D2%D2 - # +->* /lvs-test/KEY/%5Cs%F8%B5%D9k%D2%D2/self/v=1647829075409 - # -- - # /lvs-test/admin/ndn - # +->* /lvs-test/admin/ndn/KEY/z%C7%D2%B0%22%FB%D0%F3 - # +-> /lvs-test/admin/ndn/KEY/z%C7%D2%B0%22%FB%D0%F3/self/v=1647828984149 - # +->* /lvs-test/admin/ndn/KEY/z%C7%D2%B0%22%FB%D0%F3/lvs-test/v=1647829580626 - # -- - # * /lvs-test/author/xinyu - # +->* /lvs-test/author/xinyu/KEY/%18%F9%A7CP%F6%BD%1B - # +-> /lvs-test/author/xinyu/KEY/%18%F9%A7CP%F6%BD%1B/self/v=1647828975217 - # +->* /lvs-test/author/xinyu/KEY/%18%F9%A7CP%F6%BD%1B/ndn/v=1647829957196 - - - @app.route('/lvs-test/article/xinyu/hello') - def on_interest(name, param, _app_param): - print(f'>> I: {Name.to_str(name)}, {param}') - content = b"Hello," - data_name = name + [Component.from_version(timestamp())] - sign_cert_name = checker.suggest(data_name, app.keychain) - print(f' Suggested signing cert: {Name.to_str(sign_cert_name)}') - app.put_data(data_name, content=content, freshness_period=10000, cert=sign_cert_name) - print(f'<< D: {Name.to_str(data_name)}') - print(f'Content: {content.decode()}') - print('') - - @app.route('/lvs-test/article/xinyu/world') - def on_interest(name, param, _app_param): - print(f'>> I: {Name.to_str(name)}, {param}') - content = b"world!" - data_name = name + [Component.from_version(timestamp())] - sign_cert_name = checker.suggest(data_name, app.keychain) - print(f' Suggested signing cert: {Name.to_str(sign_cert_name)}') - app.put_data(data_name, content=content, freshness_period=10000, cert=sign_cert_name) - print(f'<< D: {Name.to_str(data_name)}') - print(f'Content: {content.decode()}') - print('') - - @app.route(trust_anchor.name) - def on_interest(name, param, _app_param): - print(f'>> I: {Name.to_str(name)}, {param}') - app.put_raw_packet(trust_anchor.data) - print(f'<< D: {Name.to_str(trust_anchor.name)}') - print('') - - @app.route(admin_cert.name) - def on_interest(name, param, _app_param): - print(f'>> I: {Name.to_str(name)}, {param}') - app.put_raw_packet(admin_cert.data) - print(f'<< D: {Name.to_str(admin_cert.name)}') - print('') - - @app.route(author_cert.name) - def on_interest(name, param, _app_param): - print(f'>> I: {Name.to_str(name)}, {param}') - app.put_raw_packet(author_cert.data) - print(f'<< D: {Name.to_str(author_cert.name)}') - print('') - - print('Start serving ...') - app.run_forever() - - -if __name__ == '__main__': - main() diff --git a/examples/nfd_status.py b/examples/nfd_status.py deleted file mode 100644 index 46c1ee9..0000000 --- a/examples/nfd_status.py +++ /dev/null @@ -1,85 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -import asyncio as aio -from ndn.app import NDNApp -from ndn.types import InterestNack, InterestTimeout, InterestCanceled, ValidationFailure, NetworkError -from ndn.encoding import Name, is_binary_str -from ndn.app_support.nfd_mgmt import GeneralStatus -import logging -import time - - -logging.basicConfig(format='[{asctime}]{levelname}:{message}', - datefmt='%Y-%m-%d %H:%M:%S', - level=logging.DEBUG, - style='{') - - -app = NDNApp() - - -def decode_dict(msg): - ret = msg.asdict() - for k, v in ret.items(): - if is_binary_str(v): - ret[k] = bytes(v).decode() - else: - ret[k] = str(v) - return ret - - -async def retry(): - while True: - try: - name = Name.from_str('/localhost/nfd/status/general') - print(f'Sending Interest') - data_name, meta_info, content = await app.express_interest( - name, must_be_fresh=True, can_be_prefix=True, lifetime=60000) - print(bytes(content) if content else None) - msg = GeneralStatus.parse(content) - status = decode_dict(msg) - print(status) - - except InterestNack as e: - print(f'Nacked with reason={e.reason}') - except InterestTimeout: - print(f'Timeout') - except (InterestCanceled, NetworkError): - print(f'Canceled') - break - except ValidationFailure: - print(f'Data failed to validate') - await aio.sleep(1.0) - print('Finished run') - - -def main(): - running = True - while running: - print('Connecting') - app_main = retry() - try: - running = app.run_forever(after_start=app_main) - except (FileNotFoundError, ConnectionRefusedError): - app_main.close() - if running: - time.sleep(1.0) - - -if __name__ == '__main__': - main() diff --git a/examples/producer.py b/examples/producer.py deleted file mode 100644 index a000e01..0000000 --- a/examples/producer.py +++ /dev/null @@ -1,44 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -from ndn.app import NDNApp -from ndn.encoding import Name, InterestParam, BinaryStr, FormalName, MetaInfo -import logging - - -logging.basicConfig(format='[{asctime}]{levelname}:{message}', - datefmt='%Y-%m-%d %H:%M:%S', - level=logging.INFO, - style='{') - - -app = NDNApp() - - -@app.route('/example/testApp') -def on_interest(name: FormalName, param: InterestParam, _app_param: BinaryStr | None): - print(f'>> I: {Name.to_str(name)}, {param}') - content = b"Hello, world!" - app.put_data(name, content=content, freshness_period=10000) - print(f'<< D: {Name.to_str(name)}') - print(MetaInfo(freshness_period=10000)) - print(f'Content: (size: {len(content)})') - print('') - - -if __name__ == '__main__': - app.run_forever() diff --git a/examples/putchunks.py b/examples/putchunks.py deleted file mode 100644 index a9fe2eb..0000000 --- a/examples/putchunks.py +++ /dev/null @@ -1,63 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -import logging -import sys -from ndn.utils import timestamp -from ndn.app import NDNApp -from ndn.encoding import Name, Component - -SEGMENT_SIZE = 4400 - - -def main(): - if len(sys.argv) <= 2: - print(f'Usage: {sys.argv[0]} ') - exit(0) - logging.basicConfig(format='[{asctime}]{levelname}:{message}', - datefmt='%Y-%m-%d %H:%M:%S', - level=logging.INFO, - style='{') - - app = NDNApp() - name = Name.normalize(sys.argv[1]) - name.append(Component.from_version(timestamp())) - - with open(sys.argv[2], 'rb') as f: - data = f.read() - seg_cnt = (len(data) + SEGMENT_SIZE - 1) // SEGMENT_SIZE - packets = [app.prepare_data(name + [Component.from_segment(i)], - data[i*SEGMENT_SIZE:(i+1)*SEGMENT_SIZE], - freshness_period=10000, - final_block_id=Component.from_segment(seg_cnt - 1)) - for i in range(seg_cnt)] - print(f'Created {seg_cnt} chunks under name {Name.to_str(name)}') - - @app.route(name) - def on_interest(int_name, _int_param, _app_param): - if Component.get_type(int_name[-1]) == Component.TYPE_SEGMENT: - seg_no = Component.to_number(int_name[-1]) - else: - seg_no = 0 - if seg_no < seg_cnt: - app.put_raw_packet(packets[seg_no]) - - app.run_forever() - - -if __name__ == '__main__': - main() diff --git a/examples/rdrnode.py b/examples/rdrnode.py deleted file mode 100644 index 4370e10..0000000 --- a/examples/rdrnode.py +++ /dev/null @@ -1,86 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -import sys -import asyncio as aio -import logging -from ndn.app import NDNApp -from ndn.encoding import Name -from ndn.schema import policy -from ndn.schema.schema_tree import Node -from ndn.schema.simple_node import RDRNode -from ndn.schema.simple_cache import MemoryCache, MemoryCachePolicy -from ndn.schema.simple_trust import SignedBy - - -logging.basicConfig(format='[{asctime}]{levelname}:{message}', - datefmt='%Y-%m-%d %H:%M:%S', - level=logging.INFO, - style='{') -app = NDNApp() - - -async def main(): - if len(sys.argv) <= 1: - print(f'Usage: {sys.argv[0]} []') - exit(0) - - # Make schema tree - root = Node() - root['//KEY//self/'] = Node() - root['/file/'] = RDRNode() - - # Set policies - id_name = Name.Component.get_value(app.keychain.default_identity().name[0]) - cache = MemoryCache() - root.set_policy(policy.Cache, MemoryCachePolicy(cache)) - root['/file/'].set_policy( - policy.DataValidator, - SignedBy(root['//KEY/'], - subject_to=lambda _, vars: vars['IDName'] == id_name)) - - # Store the certificate - cert = app.keychain.default_identity().default_key().default_cert() - await cache.save(Name.normalize(cert.name), cert.data) - - # Attach the tree to the face - await root.attach(app, '/') - - filename = sys.argv[1] - if len(sys.argv) > 2: - # If it's the producer - filepath = sys.argv[2] - print(f'Read {filename} from file {filepath}...') - # Provider with file - with open(filepath, 'rb') as f: - data = f.read() - await root.match('/file/' + filename).provide(data, freshness_period=60000) - # Wait for it to be cached - await aio.sleep(0.1) - else: - # If it's the producer - print(f'Try to fetch {filename}...') - - # The file is ready! - data, metadata = await root.match('/file/' + filename).need() - print(f'Content size: {len(data)}') - print(f'Content: {data[:70]} ...') - print(f'Number of segments: {metadata["block_count"]}') - print(f'Serving {filename}') - -if __name__ == '__main__': - app.run_forever(after_start=main()) diff --git a/examples/rpc_consumer.py b/examples/rpc_consumer.py deleted file mode 100644 index 62300a2..0000000 --- a/examples/rpc_consumer.py +++ /dev/null @@ -1,62 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -import sys -import logging -import ndn.utils -from ndn.app import NDNApp -from ndn.types import InterestNack, InterestTimeout, InterestCanceled, ValidationFailure -from ndn.encoding import Name, Component, InterestParam - - -logging.basicConfig(format='[{asctime}]{levelname}:{message}', - datefmt='%Y-%m-%d %H:%M:%S', - level=logging.INFO, - style='{') - - -app = NDNApp() - - -async def main(): - try: - app_param = ' '.join(x for x in sys.argv[1:]) - timestamp = ndn.utils.timestamp() - name = Name.from_str('/example/rpc') + [Component.from_timestamp(timestamp)] - print(f'Sending Interest {Name.to_str(name)}, ' - f'{InterestParam(must_be_fresh=True, lifetime=6000)}, ' - f'{app_param}') - data_name, meta_info, content = await app.express_interest( - name, app_param.encode(), must_be_fresh=True, can_be_prefix=False, lifetime=6000) - - print(f'Received Data Name: {Name.to_str(data_name)}') - print(meta_info) - print(bytes(content) if content else None) - except InterestNack as e: - print(f'Nacked with reason={e.reason}') - except InterestTimeout: - print(f'Timeout') - except InterestCanceled: - print(f'Canceled') - except ValidationFailure: - print(f'Data failed to validate') - finally: - app.shutdown() - - -if __name__ == '__main__': - app.run_forever(after_start=main()) diff --git a/examples/rpc_producer.py b/examples/rpc_producer.py deleted file mode 100644 index 68389a4..0000000 --- a/examples/rpc_producer.py +++ /dev/null @@ -1,49 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -from ndn.app import NDNApp -from ndn.encoding import Name, InterestParam, BinaryStr, FormalName, MetaInfo -import logging - - -logging.basicConfig(format='[{asctime}]{levelname}:{message}', - datefmt='%Y-%m-%d %H:%M:%S', - level=logging.INFO, - style='{') - - -app = NDNApp() - - -@app.route('/example/rpc') -def on_interest(name: FormalName, param: InterestParam, app_param: BinaryStr | None): - app_param = bytes(app_param) - print(f'>> I: {Name.to_str(name)}, {param}, {app_param}') - if not app_param: - print("<< No application parameter, dropped") - return - s = sum(int(x) for x in app_param.split()) - content = str(s).encode() - app.put_data(name, content=content, freshness_period=500) - print(f'<< D: {Name.to_str(name)}') - print(MetaInfo(freshness_period=500)) - print(f'Content: {content}') - print('') - - -if __name__ == '__main__': - app.run_forever() diff --git a/src/ndn/app_support/dispatcher.py b/src/ndn/app_support/dispatcher.py deleted file mode 100644 index 41368c4..0000000 --- a/src/ndn/app_support/dispatcher.py +++ /dev/null @@ -1,66 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2021 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -from ..encoding import NonStrictName, Name, BinaryStr, InterestParam, FormalName -from ..types import Route -from ..name_tree import NameTrie, PrefixTreeNode - - -class Dispatcher: - """ - An Interest dispatcher that helps a producer application further dispatches Interests under some route. - """ - - _tree: NameTrie = None - - def __init__(self): - self._tree = NameTrie() - - def register(self, name: NonStrictName, func: Route): - """ - Register a callback function. This will not register an NDN route. - - :param name: the name prefix. - :param func: the callback function. - :raises ValueError: the name prefix is already registered. - """ - name = Name.normalize(name) - node = self._tree.setdefault(name, PrefixTreeNode()) - if node.callback: - raise ValueError(f'Duplicated registration: {Name.to_str(name)}') - node.callback = func - - def unregister(self, name: NonStrictName): - """ - Unregister a callback function. - - :param name: the name prefix. - """ - name = Name.normalize(name) - del self._tree[name] - - def dispatch(self, name: FormalName, param: InterestParam, app_param: BinaryStr | None) -> bool: - """ - Dispatch the Interest to registered callbacks using longest match. - - :return: ``True`` if the Interest is dispatched to some callbacks. - """ - trie_step = self._tree.longest_prefix(name) - if not trie_step: - return False - trie_step.value.callback(name, param, app_param) - return True diff --git a/src/ndn/app_support/light_versec/__init__.py b/src/ndn/app_support/light_versec/__init__.py index 8032c68..b764ed6 100644 --- a/src/ndn/app_support/light_versec/__init__.py +++ b/src/ndn/app_support/light_versec/__init__.py @@ -1,7 +1,6 @@ from .checker import * from .binary import * from .compiler import * -from .validator import * __all__ = [] __all__.extend(checker.__all__) diff --git a/src/ndn/app_support/light_versec/validator.py b/src/ndn/app_support/light_versec/validator.py deleted file mode 100644 index 385eadd..0000000 --- a/src/ndn/app_support/light_versec/validator.py +++ /dev/null @@ -1,56 +0,0 @@ -# ----------------------------------------------------------------------------- -# This piece of work is inspired by Pollere' VerSec: -# https://github.com/pollere/DCT -# But this code is implemented independently without using any line of the -# original one, and released under Apache License. -# -# Copyright (C) 2019-2022 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -import logging -from ...encoding import BinaryStr, SignaturePtrs, FormalName, parse_data, Name -from ...app import NDNApp, Validator -from ...security import union_checker -from ...security.validator.cascade_validator import CascadeChecker, PublicKeyStorage, MemoryKeyStorage -from .checker import Checker - -__all__ = ['lvs_validator'] - - -def lvs_validator(checker: Checker, app: NDNApp, trust_anchor: BinaryStr, - storage: PublicKeyStorage = MemoryKeyStorage()) -> Validator: - async def validate_name(name: FormalName, sig_ptrs: SignaturePtrs) -> bool: - if (not sig_ptrs.signature_info or not sig_ptrs.signature_info.key_locator - or not sig_ptrs.signature_info.key_locator.name): - return False - cert_name = sig_ptrs.signature_info.key_locator.name - logging.getLogger(__name__).debug('LVS Checking %s <- %s ...', Name.to_str(name), Name.to_str(cert_name)) - return checker.check(name, cert_name) - - def sanity_check(): - root_of_trust = checker.root_of_trust() - if not checker.validate_user_fns(): - raise ValueError('Missing user functions for LVS validator') - cert_name, _, _, _ = parse_data(trust_anchor) - ta_matches = sum((m[0] for m in checker.match(cert_name)), start=[]) - if not ta_matches or not root_of_trust.issubset(ta_matches): - raise ValueError('Trust anchor does not match all roots of trust of LVS model') - - sanity_check() - cas_checker = CascadeChecker(app, trust_anchor, storage) - ret = union_checker(validate_name, cas_checker) - cas_checker.next_level = ret - return ret diff --git a/src/ndn/app_support/segment_fetcher.py b/src/ndn/app_support/segment_fetcher.py deleted file mode 100644 index 9eaaf06..0000000 --- a/src/ndn/app_support/segment_fetcher.py +++ /dev/null @@ -1,72 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -from ..encoding import NonStrictName, Name, Component -from ..app import NDNApp -from ..types import InterestTimeout - - -async def segment_fetcher(app: NDNApp, name: NonStrictName, timeout=4000, retry_times=3, - validator=None, must_be_fresh=True): - """ - An async-generator to fetch a segmented object. Interests are issued one by one. - - :param app: NDN Application - :param name: Name prefix of Data - :param timeout: Timeout value, in milliseconds - :param retry_times: Times for retry - :param validator: Validator - :param must_be_fresh: MustBeFresh field of Interest - :return: Data segments in order. - """ - async def retry(first): - nonlocal name - trial_times = 0 - while True: - future = app.express_interest(name, validator=validator, can_be_prefix=first, - must_be_fresh=must_be_fresh, lifetime=timeout) - try: - return await future - except InterestTimeout: - trial_times += 1 - if trial_times >= retry_times: - raise - - name = Name.normalize(name) - # First Interest - name, meta, content = await retry(True) - # If it's not segmented - if Component.get_type(name[-1]) != Component.TYPE_SEGMENT: - yield content - return - # If it's segmented - if Component.to_number(name[-1]) == 0: - yield content - if meta.final_block_id == name[-1]: - return - seg_no = 1 - else: - # If it's not segment 0, starting from 0 - seg_no = 0 - # Following Interests - while True: - name[-1] = Component.from_segment(seg_no) - name, meta, content = await retry(False) - yield content - if meta.final_block_id == name[-1]: - return - seg_no += 1 diff --git a/src/ndn/appv2.py b/src/ndn/appv2.py deleted file mode 100644 index e320459..0000000 --- a/src/ndn/appv2.py +++ /dev/null @@ -1,756 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2022 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -import asyncio as aio -import typing -import struct -import logging -from hashlib import sha256 -from dataclasses import dataclass -from .transport.face import Face -from .transport.prefix_registerer import PrefixRegisterer -from . import security as sec -from . import encoding as enc -from . import name_tree -from . import types -from . import utils -from .encoding import ndnlp_v2 as ndnlp -from .client_conf import read_client_conf, default_face, default_keychain, default_registerer - - -DEFAULT_LIFETIME = 4000 - -ValidResult = types.ValidResult - -PktContext = dict[str, any] -r"""The context for NDN Interest or Data handling.""" - -ReplyFunc = typing.Callable[[enc.BinaryStr], bool] -r""" -Continuation function for :any:`IntHandler` to respond to an Interest. - -.. function:: (data: BinaryStr) -> bool - - :param data: an encoded Data packet. - :type data: :any:`BinaryStr` - :return: True for success, False upon error. -""" - -IntHandler = typing.Callable[[enc.FormalName, enc.BinaryStr | None, ReplyFunc, PktContext], None] -r""" -Interest handler function associated with a name prefix. - -The function should use the provided ``reply`` callback to reply with Data, which can handle PIT -token properly. - -.. function:: (name: FormalName, app_param: Optional[BinaryStr], reply: ReplyFunc, context: PktContext) -> None - - :param name: Interest name. - :type name: :any:`FormalName` - :param app_param: Interest ApplicationParameters value, or None if absent. - :type app_param: Optional[:any:`BinaryStr`] - :param reply: continuation function to respond with Data. - :type reply: :any:`ReplyFunc` - :param context: packet handler context. - :type context: :any:`PktContext` - -.. note:: - Interest handler function must be a normal function instead of an ``async`` one. - This is on purpose, because an Interest is supposed to be replied ASAP, - even it cannot finish the request in time. - To provide some feedback, a better practice is replying with an Application NACK - (or some equivalent Data packet saying the operation cannot be finished in time). - If you want to use ``await`` in the handler, please use ``asyncio.create_task`` to create a new coroutine. -""" - -Validator = typing.Callable[[enc.FormalName, enc.SignaturePtrs, PktContext], - typing.Coroutine[any, None, ValidResult]] -r""" -Validator function that validates Interest or Data signature against trust policy. - -.. function:: (name: FormalName, sig: SignaturePtrs, context: PktContext) -> Coroutine[ValidResult] - - :param name: Interest or Data name. - :type name: :any:`FormalName` - :param sig: packet signature pointers. - :type sig: :any:`SignaturePtrs` - :param context: packet handler context. - :type context: :any:`PktContext` -""" - - -async def pass_all(_name, _sig, _context): - return types.ValidResult.PASS - - -@dataclass -class PrefixTreeNode: - callback: IntHandler = None - validator: Validator | None = None - - -@dataclass -class PendingIntEntry: - future: aio.Future - deadline: int - can_be_prefix: bool - must_be_fresh: bool - validator: Validator - implicit_sha256: enc.BinaryStr = b'' - task: aio.Task | None = None - - async def satisfy(self, data: types.DataTuple): - name, meta_info, content, sig, raw_packet = data - pkt_context = { - 'meta_info': meta_info, - 'sig_ptrs': sig, - 'raw_packet': raw_packet, - 'deadline': self.deadline, - } - if self.validator is not None: - try: - valid = await self.validator(name, sig, pkt_context) - except (TimeoutError, aio.CancelledError): - valid = ValidResult.TIMEOUT - else: - valid = ValidResult.FAIL - if self.future.cancelled() or self.future.done(): - # Don't know why but there was a race condition with timeout() - # The sequence was: Interest sent -> Data arrived -> timeout() -> satisfy() - # Cannot reproduce the scenario. Especially, delay in validator() does not trigger the race condition - # But anyway, let me add a guard check here. - return - if valid == ValidResult.PASS or valid == ValidResult.ALLOW_BYPASS: - self.future.set_result((name, content, pkt_context)) - else: - self.future.set_exception(types.ValidationFailure(name, meta_info, content, sig, valid)) - - -class InterestTreeNode: - pending_list: list[PendingIntEntry] - - def __init__(self): - self.pending_list = [] - - def append_interest(self, future: aio.Future, deadline: int, param: enc.InterestParam, - validator: Validator, implicit_sha256: enc.BinaryStr): - self.pending_list.append( - PendingIntEntry(future, deadline, param.can_be_prefix, param.must_be_fresh, validator, implicit_sha256)) - - def nack_interest(self, nack_reason: int) -> bool: - for entry in self.pending_list: - entry.future.set_exception(types.InterestNack(nack_reason)) - return True - - def satisfy(self, data: types.DataTuple, is_prefix: bool) -> bool: - unsatisfied_entries = [] - raw_packet = data[4] - for entry in self.pending_list: - if entry.can_be_prefix or not is_prefix: - if len(entry.implicit_sha256) > 0: - data_sha256 = sha256(raw_packet).digest() - passed = data_sha256 == entry.implicit_sha256 - else: - passed = True - else: - passed = False - if passed: - # Try to validate the packet - aio.create_task(entry.satisfy(data)) - else: - unsatisfied_entries.append(entry) - if unsatisfied_entries: - self.pending_list = unsatisfied_entries - return False - else: - return True - - def timeout(self, future: aio.Future): - # Exception is raised by outside code. - for ele in self.pending_list: - if ele.future is future and ele.task is not None: - ele.task.cancel() - self.pending_list = [ele for ele in self.pending_list if ele.future is not future] - return not self.pending_list - - def cancel(self): - for entry in self.pending_list: - entry.future.cancel() - if entry.task is not None: - entry.task.cancel() - - -class NDNApp: - """ - An NDN application. - """ - # PIT and FIB here are not real PIT/FIB, but a data structure that handles expressed Interests (for PIT) - # and registered handlers & routes (for FIB). Since they share the functionality with real PIT and FIB, - # I borrow the word to have a shorter variable name. - _pit: name_tree.NameTrie = None - _fib: name_tree.NameTrie = None - face: Face = None - registerer: PrefixRegisterer = None - _autoreg_routes: list[enc.FormalName] - logger: logging.Logger - - def __init__(self, face=None, client_conf=None, registerer=None): - self.logger = logging.getLogger(__name__) - config = client_conf if client_conf else {} - if not face: - if 'transport' not in config: - config = read_client_conf() | config - if face is not None: - self.face = face - else: - self.face = default_face(config['transport']) - if registerer is not None: - self.registerer = registerer - else: - self.registerer = default_registerer() - self.registerer.set_app(app=self) - self.face.callback = self._receive - self._pit = name_tree.NameTrie() - self._fib = name_tree.NameTrie() - self._autoreg_routes = [] - - @staticmethod - def default_keychain(client_conf=None) -> sec.Keychain: - if not client_conf: - config = read_client_conf() - else: - config = read_client_conf() | client_conf - return default_keychain(config['pib'], config['tpm']) - - async def _receive(self, typ: int, data: enc.BinaryStr): - """ - Pipeline when a packet is received. - - :param typ: the Type. - :param data: the Value of the packet with TL. - """ - # if self.logger.isEnabledFor(logging.DEBUG): - # self.logger.debug('Packet received %s, %s' % (typ, bytes(data))) - if typ == enc.LpTypeNumber.LP_PACKET: - try: - lp_pkt = enc.parse_lp_packet_v2(data, with_tl=True) - except (enc.DecodeError, TypeError, ValueError, struct.error): - self.logger.warning('Unable to decode received packet') - return - if lp_pkt.nack is not None: - nack_reason = lp_pkt.nack.nack_reason - else: - nack_reason = None - pit_token = lp_pkt.pit_token - data = lp_pkt.fragment - typ, _ = enc.parse_tl_num(data) - else: - nack_reason = None - pit_token = None - - if nack_reason is not None: - try: - name, _, _, _ = enc.parse_interest(data, with_tl=True) - except (enc.DecodeError, TypeError, ValueError, struct.error): - self.logger.warning('Unable to decode the fragment of LpPacket') - return - if self.logger.isEnabledFor(logging.DEBUG): - self.logger.debug('NetworkNack received %s, reason=%s', enc.Name.to_str(name), nack_reason) - self._on_nack(name, nack_reason) - else: - if typ == enc.TypeNumber.INTEREST: - try: - name, param, app_param, sig = enc.parse_interest(data, with_tl=True) - except (enc.DecodeError, TypeError, ValueError, struct.error): - self.logger.warning('Unable to decode received packet') - return - if self.logger.isEnabledFor(logging.DEBUG): - if pit_token: - self.logger.debug('Interest received %s w/ token=%s', - enc.Name.to_str(name), bytes(pit_token).hex()) - else: - self.logger.debug('Interest received %s', enc.Name.to_str(name)) - await self._on_interest(name, pit_token, param, app_param, sig, raw_packet=data) - elif typ == enc.TypeNumber.DATA: - try: - name, meta_info, content, sig = enc.parse_data(data, with_tl=True) - except (enc.DecodeError, TypeError, ValueError, struct.error): - self.logger.warning('Unable to decode received packet') - return - if self.logger.isEnabledFor(logging.DEBUG): - self.logger.debug('Data received %s', enc.Name.to_str(name)) - await self._on_data(name, meta_info, content, sig, raw_packet=data) - else: - self.logger.warning('Unable to decode received packet') - - @staticmethod - def make_data(name: enc.NonStrictName, content: enc.BinaryStr | None, - signer: enc.Signer | None, **kwargs): - r""" - Encode a data packet without requiring an NDNApp instance. - This is simply a wrapper of encoding.make_data. - I write this because most people seem not aware of the ``make_data`` function in the encoding package. - The corresponding ``make_interest`` is less useful (one should not reuse nonce) and thus not wrapped. - Sync protocol should use encoding.make_interest if necessary. - Also, since having a default signer encourages bad habit, - prepare_data is removed except for command Interests sent to NFD. - Please call ``keychain.get_signer({})`` to use the default certificate. - - :param name: the Name. - :type name: :any:`NonStrictName` - :param content: the Content. - :type content: Optional[:any:`BinaryStr`] - :param signer: the Signer used to sign the packet. - :type signer: Optional[:any:`Signer`] - :param kwargs: arguments for :any:`MetaInfo`. - :return: TLV encoded Data packet. - """ - if 'meta_info' in kwargs: - meta_info = kwargs['meta_info'] - else: - meta_info = enc.MetaInfo.from_dict(kwargs) - return enc.make_data(name, meta_info, content, signer=signer) - - async def _on_interest(self, name: enc.FormalName, pit_token: enc.BinaryStr | None, - param: enc.InterestParam, app_param: enc.BinaryStr | None, sig: enc.SignaturePtrs, - raw_packet: enc.BinaryStr): - trie_step = self._fib.longest_prefix(name) - if not trie_step: - self.logger.warning('No route: %s', name) - return - node: PrefixTreeNode = trie_step.value - if node.callback is None: - self.logger.warning('No callback: %s', name) - return - sig_required = app_param is not None or sig.signature_info is not None - if sig_required: - if not await sec.params_sha256_checker(name, sig): - self.logger.warning('Drop malformed Interest: %s', name) - return - - # Use context to handle misc parameters - if param.lifetime is not None: - deadline = utils.timestamp() + param.lifetime - else: - deadline = utils.timestamp() + DEFAULT_LIFETIME - context = { - 'int_param': param, - 'pit_token': pit_token, - 'sig_ptrs': sig, - 'raw_packet': raw_packet, - 'deadline': deadline, - } - - def reply(data: enc.BinaryStr) -> bool: - now = utils.timestamp() - if now > deadline: - self.logger.warning('Deadline passed, unable to reply to %s', enc.Name.to_str(name)) - return False - if pit_token is None: - self._put_raw_packet(data) - else: - self._put_raw_packet_with_pit_token(data, pit_token) - - # In case the validator blocks the pipeline, create a task - async def submit_interest(): - if sig_required: - # In v2, to enforce security, validator is required. Also, all interests with app_param are checked. - # The validator needs to manually pass it if the application wants to handle unsigned Interests with - # app_param. - if node.validator is not None: - valid = await node.validator(name, sig, context) - else: - valid = ValidResult.FAIL - else: - valid = ValidResult.PASS - if valid == ValidResult.PASS or valid == ValidResult.ALLOW_BYPASS: - node.callback(name, app_param, reply, context) - else: - self.logger.warning('Drop unvalidated Interest: %s', name) - return - aio.create_task(submit_interest()) - - def _put_raw_packet(self, data: enc.BinaryStr): - r""" - Send a raw Data packet. - - :param data: TLV encoded Data packet. - :type data: :any:`BinaryStr` - :raises NetworkError: the face to NFD is down. - """ - if not self.face.running: - raise types.NetworkError('cannot send packet before connected') - self.face.send(data) - - def _put_raw_packet_with_pit_token(self, data: enc.BinaryStr, pit_token: enc.BinaryStr): - r""" - Wrap a raw Data packet with PIT Token and send. - Used to reply an Interest with PIT Token provided. - - :param data: TLV encoded Data packet. - :type data: :any:`BinaryStr` - :param pit_token: The PIT Token provided. - :type pit_token: :any:`BinaryStr` - :raises NetworkError: the face to NFD is down. - """ - if not self.face.running: - raise types.NetworkError('cannot send packet before connected') - pkt = ndnlp.LpPacket() - pkt.lp_packet = ndnlp.LpPacketValue() - pkt.lp_packet.pit_token = pit_token - pkt.lp_packet.fragment = data - wire = pkt.encode() - self.face.send(wire) - - def _put_raw_packet_with_pit_token_nocopy(self, data: enc.BinaryStr, pit_token: enc.BinaryStr): - r""" - Wrap a raw Data packet with PIT Token and send. - Used to reply an Interest with PIT Token provided. - - This function is reserved as a backup because it assumes the face to be stream face. - - :param data: TLV encoded Data packet. - :type data: :any:`BinaryStr` - :param pit_token: The PIT Token provided. - :type pit_token: :any:`BinaryStr` - :raises NetworkError: the face to NFD is down. - """ - # To avoid extra copy, we manually encode the header and send it separately from Data body - # The format is: LP-T LP-L (PIT-TOKEN-TLV) FRAG-T FRAG-L - if not self.face.running: - raise types.NetworkError('cannot send packet before connected') - pt = ndnlp.LpPacketValue() - pt.pit_token = pit_token - pt_wire = pt.encode() - frag_l = len(data) - lp_l = len(pt_wire) + enc.get_tl_num_size(ndnlp.LpTypeNumber.FRAGMENT) + enc.get_tl_num_size(frag_l) - wire_l = enc.get_tl_num_size(ndnlp.LpTypeNumber.LP_PACKET) + enc.get_tl_num_size(lp_l) + lp_l - wire = bytearray(wire_l) - pos = 0 - pos += enc.write_tl_num(ndnlp.LpTypeNumber.LP_PACKET, wire, pos) - pos += enc.write_tl_num(lp_l, wire, pos) - wire[pos:pos+len(pt_wire)] = pt_wire - pos += len(pt_wire) - pos += enc.write_tl_num(ndnlp.LpTypeNumber.FRAGMENT, wire, pos) - pos += enc.write_tl_num(frag_l, wire, pos) - self.face.send(wire) - self.face.send(data) - - def attach_handler(self, name: enc.NonStrictName, handler: IntHandler, - validator: Validator | None = None): - """ - Attach an Interest handler at a name prefix. - Incoming Interests under the specified name prefix will be dispatched to the handler. - - This only sets the handler within NDNApp, but does not send prefix registration commands - to the forwarder. - To register the prefix in the forwarder, use :any:`register`. - The handler association is retained even if the forwarder is disconnected. - - :param name: name prefix. - :type name: :any:`NonStrictName` - :param handler: Interest handler function. - :type handler: :any:`IntHandler` - :param validator: validator for signed Interests. - Non signed Interests, i.e. those without ApplicationParameters and SignatureInfo, are - passed to the handler directly without calling the validator. - Interests with malformed ParametersSha256DigestComponent are dropped silently. - If a validator is not provided (set to ``None``), signed Interests will be dropped. - Otherwise, signed Interests are passed to the validator. - Those failing the validation are dropped silently. - Those passing the validation are passed to the handler function. - :type validator: Optional[:any:`Validator`] - """ - name = enc.Name.normalize(name) - node = self._fib.setdefault(name, PrefixTreeNode()) - if node.callback: - raise ValueError(f'Duplicated handler attachment: {enc.Name.to_str(name)}') - node.callback = handler - node.validator = validator - - def detach_handler(self, name: enc.NonStrictName): - """ - Detach an Interest handler at a name prefix. - - This only deletes the handler within NDNApp, but does not unregister the prefix in the - forwarder. - To unregister the prefix in the forwarder, use :any:`unregister`. - - :param name: name prefix. This must exactly match the name passed to :any:`attach_handler`. - If there are Interest handlers attached to longer prefixes, each handler must - be removed explicitly. - :type name: :any:`NonStrictName` - """ - del self._fib[enc.Name.normalize(name)] - - async def register(self, name: enc.NonStrictName) -> bool: - """ - Register a prefix in the forwarder. - - This only sends the prefix registration command to the forwarder. - In order to receive incoming Interests, you also need to use :any:`attach_handler` to - attach an Interest handler function. - - :param name: name prefix. - :type name: :any:`NonStrictName` - - :raises ValueError: the prefix is already registered. - :raises NetworkError: the face to NFD is down now. - """ - name = enc.Name.normalize(name) - return await self.registerer.register(name) - - async def unregister(self, name: enc.NonStrictName) -> bool: - """ - Unregister a prefix in the forwarder. - - :param name: name prefix. - :type name: :any:`NonStrictName` - """ - name = enc.Name.normalize(name) - return await self.registerer.unregister(name) - - def express_raw_interest(self, - final_name: enc.NonStrictName, - interest_param: enc.InterestParam, - raw_interest: enc.BinaryStr, - validator: Validator, - no_response: bool = False - ) -> typing.Coroutine[any, None, - tuple[enc.FormalName, enc.BinaryStr | None, PktContext]]: - if no_response: - self.face.send(raw_interest) - return None - if validator is None: - raise ValueError('Data Validator must not be None when expressing an Interest.') - final_name = enc.Name.normalize(final_name) - future = aio.get_running_loop().create_future() - # Handle implicit SHA256 - if enc.Component.get_type(final_name[-1]) == enc.Component.TYPE_IMPLICIT_SHA256: - node_name = final_name[:-1] - implicit_sha256 = enc.Component.get_value(final_name[-1]) - else: - node_name = final_name - implicit_sha256 = b'' - node: InterestTreeNode = self._pit.setdefault(node_name, InterestTreeNode()) - deadline = utils.timestamp() - if interest_param.lifetime is not None: - deadline += interest_param.lifetime - else: - deadline += DEFAULT_LIFETIME - node.append_interest(future, deadline, interest_param, validator, implicit_sha256) - self.face.send(raw_interest) - return self._wait_for_data(future, deadline, node_name, node) - - async def _wait_for_data(self, future: aio.Future, deadline: int, node_name: enc.FormalName, - node: InterestTreeNode): - lifetime = deadline - utils.timestamp() - if lifetime <= 0: - # This happens if the application sends an Interest, does some calculation, and then fetches the result. - # The Interest should be satisfied now. Thus, it should not be considered as an error. - lifetime = 100 - try: - data_name, content, pkt_context = await aio.wait_for(future, timeout=lifetime/1000.0) - except TimeoutError: - if node.timeout(future): - del self._pit[node_name] - raise types.InterestTimeout() - except aio.CancelledError: - raise types.InterestCanceled() - # ValidationError, InterestNack are passed to the parent caller - return data_name, content, pkt_context - - async def _on_data(self, name: enc.FormalName, meta_info: enc.MetaInfo, - content: enc.BinaryStr | None, sig: enc.SignaturePtrs, - raw_packet: enc.BinaryStr): - clean_list = [] - for prefix, node in self._pit.prefixes(name): - if node.satisfy((name, meta_info, content, sig, raw_packet), prefix != name): - clean_list.append(prefix) - for prefix in clean_list: - del self._pit[prefix] - - def _on_nack(self, name: enc.FormalName, nack_reason: int): - try: - node = self._pit[name] - except KeyError: - node = None - if node: - if node.nack_interest(nack_reason): - del self._pit[name] - - def express(self, name: enc.NonStrictName, validator: Validator, - app_param: enc.BinaryStr | None = None, - signer: enc.Signer | None = None, - **kwargs) -> typing.Coroutine[any, None, - tuple[enc.FormalName, enc.BinaryStr | None, PktContext]]: - r""" - Express an Interest. - - The Interest packet is sent immediately and a coroutine used to get the result is returned. - Awaiting on the returned coroutine will block until the Data is received. - It then returns the Data name, Data Content value, and :any:`PktContext`. - An exception is raised if NDNApp is unable to retrieve the Data. - - :param name: Interest name. - :type name: :any:`NonStrictName` - :param validator: validator for the retrieved Data packet. - :type validator: :any:`Validator` - :param app_param: Interest ApplicationParameters value. If this is not None, a signed - Interest is sent. NDNApp does not support sending parameterized - Interests that are not signed. - :type app_param: Optional[:any:`BinaryStr`] - :param signer: Signer for Interest signing. This is required if `app_param` is specified. - :type signer: Optional[:any:`Signer`] - :param kwargs: arguments for :any:`InterestParam`. - :return: A tuple of (Name, Content, PacketContext) after ``await``. - :rtype: Coroutine[Any, None, Tuple[:any:`FormalName`, Optional[:any:`BinaryStr`], :any:`PktContext`]] - - The following exceptions may be raised by ``express``: - - :raises NetworkError: the face to NFD is down before sending this Interest. - :raises ValueError: when the signer is missing but app_param presents. - - The following exceptions may be raised by the returned coroutine: - - :raises InterestNack: an NetworkNack is received. - :raises InterestTimeout: time out. - :raises ValidationFailure: unable to validate the Data packet. - :raises InterestCanceled: the face to NFD is shut down after sending this Interest. - """ - if not self.face.running: - raise types.NetworkError('cannot send packet before connected') - if app_param is not None and signer is None: - raise ValueError('An Interest with AppParam is required to be signed.') - if 'interest_param' in kwargs: - interest_param = kwargs['interest_param'] - else: - if 'nonce' not in kwargs: - kwargs['nonce'] = utils.gen_nonce() - interest_param = enc.InterestParam.from_dict(kwargs) - interest, final_name = enc.make_interest(name, interest_param, app_param, signer=signer, need_final_name=True) - no_response = kwargs.get('no_response', False) - return self.express_raw_interest(final_name, interest_param, interest, validator, no_response) - - def route(self, name: enc.NonStrictName, validator: Validator | None = None): - r""" - A decorator used to register a permanent route for a specific prefix. - The decorated function should be an :any:`IntHandler`. - - This function is non-blocking and can be called at any time. - It can be called before connecting to the forwarder. - Every time a forwarder connection is established, NDNApp will automatically send - prefix registration commands. - Errors in prefix registration are ignored. - - :param name: name prefix. - :type name: :any:`NonStrictName` - :param validator: validator for signed Interests. See :any:`attach_handler` for details. - :type validator: Optional[:any:`Validator`] - - :examples: - .. code-block:: python3 - - app = NDNApp() - - @app.route('/example/rpc') - def on_interest(name, app_param, reply, context): - pass - - """ - name = enc.Name.normalize(name) - - def decorator(func: IntHandler): - self._autoreg_routes.append(name) - self.attach_handler(name, func, validator) - if self.face.running: - aio.create_task(self.register(name)) - return func - return decorator - - def _clean_up(self): - for node in self._pit.itervalues(): - node.cancel() - # FIB is not cleared now - self._pit.clear() - - def shutdown(self): - """ - Manually shutdown the face to NFD. - """ - self.logger.info('Manually shutdown') - self.face.shutdown() - - async def main_loop(self, after_start: typing.Awaitable = None) -> bool: - """ - The main loop of NDNApp. - - :param after_start: the coroutine to start after connection to NFD is established. - :return: ``True`` if the connection is shutdown not by ``Ctrl+C``. - For example, manually or by the other side. - """ - async def starting_task(): - for name in self._autoreg_routes: - await self.register(name) - if after_start: - try: - await after_start - except Exception: - self.face.shutdown() - raise - - try: - await self.face.open() - except (FileNotFoundError, ConnectionError, OSError, PermissionError): - if after_start: - if isinstance(after_start, typing.Coroutine): - after_start.close() - elif isinstance(after_start, (aio.Task, aio.Future)): - after_start.cancel() - raise - task = aio.create_task(starting_task()) - self.logger.debug('Connected to NFD node, start running...') - try: - await self.face.run() - ret = True - except aio.CancelledError: - self.logger.info('Shutting down') - ret = False - finally: - self.face.shutdown() - self._clean_up() - await task - return ret - - def run_forever(self, after_start: typing.Awaitable = None): - """ - A non-async wrapper of :meth:`main_loop`. - - :param after_start: the coroutine to start after connection to NFD is established. - - :examples: - .. code-block:: python3 - - app = NDNApp() - - if __name__ == '__main__': - app.run_forever(after_start=main()) - """ - try: - aio.run(self.main_loop(after_start)) - except KeyboardInterrupt: - self.logger.info('Receiving Ctrl+C, exit') diff --git a/src/ndn/bin/nfdc/cmd_get_face.py b/src/ndn/bin/nfdc/cmd_get_face.py index 8854e0c..e816e9b 100644 --- a/src/ndn/bin/nfdc/cmd_get_face.py +++ b/src/ndn/bin/nfdc/cmd_get_face.py @@ -20,7 +20,7 @@ from ...encoding import Name, Component from ...encoding.tlv_model import tlv_encode, tlv_parse from ...app_support.nfd_mgmt import FaceStatusMsg, FaceQueryFilter, FaceQueryFilterValue, parse_response -from .utils import express_interest +from .utils import express_command def add_parser(subparsers): @@ -35,7 +35,7 @@ def execute(args: argparse.Namespace): async def list_face(): try: - data = await express_interest(app, "/localhost/nfd/faces/list") + data = await express_command(app, "/localhost/nfd/faces/list") msg = tlv_parse(FaceStatusMsg, data) # TODO: Should calculate the length instead of using a fixed number print(f'{"FaceID":7}{"RemoteURI":<30}\t{"LocalURI":<30}') @@ -47,7 +47,7 @@ async def list_face(): async def inspect_face(face_id, face_uri): async def exec_query(): - data = await express_interest(app, data_name) + data = await express_command(app, data_name) if not data: return False elif data[0] == 0x65: diff --git a/src/ndn/bin/nfdc/cmd_get_route.py b/src/ndn/bin/nfdc/cmd_get_route.py index d178889..fdd0fa8 100644 --- a/src/ndn/bin/nfdc/cmd_get_route.py +++ b/src/ndn/bin/nfdc/cmd_get_route.py @@ -20,7 +20,7 @@ from ...encoding import Name from ...encoding.tlv_model import tlv_parse from ...app_support.nfd_mgmt import FibStatus, RibStatus -from .utils import express_interest +from .utils import express_command def add_parser(subparsers): @@ -36,9 +36,9 @@ def execute(args: argparse.Namespace): async def list_route(): try: - fib_data = await express_interest(app, "/localhost/nfd/fib/list") + fib_data = await express_command(app, "/localhost/nfd/fib/list") fib_msg = tlv_parse(FibStatus, fib_data) - rib_data = await express_interest(app, "/localhost/nfd/rib/list") + rib_data = await express_command(app, "/localhost/nfd/rib/list") rib_msg = tlv_parse(RibStatus, rib_data) # TODO: Should calculate the length instead of using a fixed number print('Forwarding Table (FIB)') diff --git a/src/ndn/bin/nfdc/cmd_get_status.py b/src/ndn/bin/nfdc/cmd_get_status.py index b0e1f42..51bd168 100644 --- a/src/ndn/bin/nfdc/cmd_get_status.py +++ b/src/ndn/bin/nfdc/cmd_get_status.py @@ -20,7 +20,7 @@ from ...app import NDNApp from ...encoding.tlv_model import tlv_parse from ...app_support.nfd_mgmt import GeneralStatus -from .utils import express_interest +from .utils import express_command def add_parser(subparsers): @@ -33,7 +33,7 @@ def execute(_args: argparse.Namespace): async def after_start(): try: - data = await express_interest(app, "/localhost/nfd/status/general") + data = await express_command(app, "/localhost/nfd/status/general") msg = tlv_parse(GeneralStatus, data) diff --git a/src/ndn/bin/nfdc/cmd_get_strategy.py b/src/ndn/bin/nfdc/cmd_get_strategy.py index 3d242e6..1a67dd3 100644 --- a/src/ndn/bin/nfdc/cmd_get_strategy.py +++ b/src/ndn/bin/nfdc/cmd_get_strategy.py @@ -20,7 +20,7 @@ from ...encoding import Name from ...encoding.tlv_model import tlv_parse from ...app_support.nfd_mgmt import StrategyChoiceMsg -from .utils import express_interest +from .utils import express_command def add_parser(subparsers): @@ -36,7 +36,7 @@ def execute(args: argparse.Namespace): async def list_strategy(): try: - data = await express_interest(app, "/localhost/nfd/strategy-choice/list") + data = await express_command(app, "/localhost/nfd/strategy-choice/list") msg = tlv_parse(StrategyChoiceMsg, data) for s in msg.strategy_choices: s_prefix = Name.to_str(s.name) diff --git a/src/ndn/bin/nfdc/cmd_new_face.py b/src/ndn/bin/nfdc/cmd_new_face.py index fdab7f8..2329fa3 100644 --- a/src/ndn/bin/nfdc/cmd_new_face.py +++ b/src/ndn/bin/nfdc/cmd_new_face.py @@ -18,7 +18,7 @@ import argparse from ...app import NDNApp from ...app_support.nfd_mgmt import parse_response, make_command_v2 -from .utils import express_interest +from .utils import express_command def add_parser(subparsers): @@ -39,7 +39,7 @@ def execute(args: argparse.Namespace): async def create_face(): cmd = make_command_v2('faces', 'create', uri=uri) - res = await express_interest(app, cmd) + res = await express_command(app, cmd) msg = parse_response(res) print(f'{msg["status_code"]} {msg["status_text"]}') app.shutdown() diff --git a/src/ndn/bin/nfdc/cmd_new_route.py b/src/ndn/bin/nfdc/cmd_new_route.py index 332b418..ca69a35 100644 --- a/src/ndn/bin/nfdc/cmd_new_route.py +++ b/src/ndn/bin/nfdc/cmd_new_route.py @@ -18,7 +18,7 @@ import argparse from ...app import NDNApp from ...app_support.nfd_mgmt import make_command_v2, parse_response -from .utils import express_interest +from .utils import express_command def add_parser(subparsers): @@ -39,7 +39,7 @@ async def register_route(): try: fid = int(face_id) cmd = make_command_v2('rib', 'register', name=route, face_id=fid) - res = await express_interest(app, cmd) + res = await express_command(app, cmd) msg = parse_response(res) print(f'{msg["status_code"]} {msg["status_text"]}') finally: diff --git a/src/ndn/bin/nfdc/cmd_remove_face.py b/src/ndn/bin/nfdc/cmd_remove_face.py index 368773d..1a3cfee 100644 --- a/src/ndn/bin/nfdc/cmd_remove_face.py +++ b/src/ndn/bin/nfdc/cmd_remove_face.py @@ -21,7 +21,7 @@ from ...encoding.tlv_model import tlv_encode, tlv_parse from ...app_support.nfd_mgmt import FaceStatusMsg, FaceQueryFilter, FaceQueryFilterValue, parse_response, \ make_command_v2 -from .utils import express_interest +from .utils import express_command def add_parser(subparsers): @@ -38,7 +38,7 @@ def execute(args: argparse.Namespace): async def remove_face(fid): print(f'Removing face {fid} ...', end='') cmd = make_command_v2('faces', 'destroy', face_id=fid) - res = await express_interest(app, cmd) + res = await express_command(app, cmd) msg = parse_response(res) print(f'\t{msg["status_code"]} {msg["status_text"]}') @@ -50,7 +50,7 @@ async def run_with_fid(fid): async def run_with_uri(uri): async def try_remove(): - data = await express_interest(app, data_name) + data = await express_command(app, data_name) if not data: return False elif data[0] == 0x65: diff --git a/src/ndn/bin/nfdc/cmd_remove_route.py b/src/ndn/bin/nfdc/cmd_remove_route.py index 8c274c5..6860a92 100644 --- a/src/ndn/bin/nfdc/cmd_remove_route.py +++ b/src/ndn/bin/nfdc/cmd_remove_route.py @@ -18,7 +18,7 @@ import argparse from ...app import NDNApp from ...app_support.nfd_mgmt import parse_response, make_command_v2 -from .utils import express_interest +from .utils import express_command def add_parser(subparsers): @@ -42,7 +42,7 @@ async def remove_route(): else: fid = int(face_id) cmd = make_command_v2('rib', 'unregister', name=route, face_id=fid) - res = await express_interest(app, cmd) + res = await express_command(app, cmd) msg = parse_response(res) print(f'{msg["status_code"]} {msg["status_text"]}') finally: diff --git a/src/ndn/bin/nfdc/cmd_remove_strategy.py b/src/ndn/bin/nfdc/cmd_remove_strategy.py index 686be39..633ff88 100644 --- a/src/ndn/bin/nfdc/cmd_remove_strategy.py +++ b/src/ndn/bin/nfdc/cmd_remove_strategy.py @@ -18,7 +18,7 @@ import argparse from ...app import NDNApp from ...app_support.nfd_mgmt import parse_response, make_command_v2 -from .utils import express_interest +from .utils import express_command def add_parser(subparsers): @@ -35,7 +35,7 @@ def execute(args: argparse.Namespace): async def remove_strategy(): try: cmd = make_command_v2('strategy-choice', 'unset', name=prefix) - res = await express_interest(app, cmd) + res = await express_command(app, cmd) msg = parse_response(res) print(f'{msg["status_code"]} {msg["status_text"]}') finally: diff --git a/src/ndn/bin/nfdc/cmd_set_strategy.py b/src/ndn/bin/nfdc/cmd_set_strategy.py index 14ab239..2694022 100644 --- a/src/ndn/bin/nfdc/cmd_set_strategy.py +++ b/src/ndn/bin/nfdc/cmd_set_strategy.py @@ -18,7 +18,7 @@ import argparse from ...app import NDNApp from ...app_support.nfd_mgmt import parse_response, make_command_v2 -from .utils import express_interest +from .utils import express_command def add_parser(subparsers): @@ -51,7 +51,7 @@ def execute(args: argparse.Namespace): async def remove_strategy(): try: cmd = make_command_v2('strategy-choice', 'set', name=prefix, strategy=strategy) - res = await express_interest(app, cmd) + res = await express_command(app, cmd) msg = parse_response(res) print(f'{msg["status_code"]} {msg["status_text"]}') finally: diff --git a/src/ndn/bin/nfdc/utils.py b/src/ndn/bin/nfdc/utils.py index 7cd30e7..59b5e59 100644 --- a/src/ndn/bin/nfdc/utils.py +++ b/src/ndn/bin/nfdc/utils.py @@ -20,7 +20,7 @@ from ...types import InterestNack, InterestTimeout, InterestCanceled, ValidationFailure -async def express_interest(app: NDNApp, name): +async def express_command(app: NDNApp, name): try: _, data, context = await app.express( name, validator=pass_all, app_param=b'', signer=DigestSha256Signer(True), diff --git a/src/ndn/name_tree.py b/src/ndn/name_tree.py index ea98cca..2864f17 100644 --- a/src/ndn/name_tree.py +++ b/src/ndn/name_tree.py @@ -15,12 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # ----------------------------------------------------------------------------- -import asyncio as aio -import dataclasses as dc -from hashlib import sha256 from pygtrie import Trie -from .encoding import InterestParam, FormalName, BinaryStr -from .types import InterestNack, Validator, Route, DataTuple +from .encoding import FormalName class NameTrie(Trie): @@ -31,65 +27,3 @@ def _path_from_key(self, key: FormalName) -> FormalName: def _key_from_path(self, path: FormalName) -> FormalName: return path - - -@dc.dataclass -class PendingIntEntry: - future: aio.Future - lifetime: int - can_be_prefix: bool - must_be_fresh: bool - implicit_sha256: BinaryStr = b'' - - -class InterestTreeNode: - pending_list: list[PendingIntEntry] - - def __init__(self): - self.pending_list = [] - - def append_interest(self, future: aio.Future, param: InterestParam, implicit_sha256: BinaryStr): - self.pending_list.append( - PendingIntEntry(future, param.lifetime, - param.can_be_prefix, param.must_be_fresh, implicit_sha256)) - - def nack_interest(self, nack_reason: int) -> bool: - for entry in self.pending_list: - entry.future.set_exception(InterestNack(nack_reason)) - return True - - def satisfy(self, data: DataTuple, is_prefix: bool) -> bool: - unsatisfied_entries = [] - raw_packet = data[4] - for entry in self.pending_list: - if entry.can_be_prefix or not is_prefix: - if len(entry.implicit_sha256) > 0: - data_sha256 = sha256(raw_packet).digest() - passed = data_sha256 == entry.implicit_sha256 - else: - passed = True - else: - passed = False - if passed: - entry.future.set_result(data) - else: - unsatisfied_entries.append(entry) - if unsatisfied_entries: - self.pending_list = unsatisfied_entries - return False - else: - return True - - def timeout(self, future: aio.Future): - self.pending_list = [ele for ele in self.pending_list if ele.future is not future] - return not self.pending_list - - def cancel(self): - for entry in self.pending_list: - entry.future.cancel() - - -class PrefixTreeNode: - callback: Route = None - validator: Validator | None = None - extra_param: dict = None diff --git a/src/ndn/schema/__init__.py b/src/ndn/schema/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/ndn/schema/policy.py b/src/ndn/schema/policy.py deleted file mode 100644 index 1da2171..0000000 --- a/src/ndn/schema/policy.py +++ /dev/null @@ -1,133 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -import abc -from ..encoding import SignaturePtrs, FormalName, InterestParam, BinaryStr -from ..encoding.signer import Signer -from ..types import Validator - - -class Policy: - """ - Policy is an annotation attached to a node. - """ - def __init__(self): - self.node = None - - -class Cache(Policy, metaclass=abc.ABCMeta): - """ - Cache policy determines how Data packets are stored. - """ - @abc.abstractmethod - async def search(self, match, name: FormalName, param: InterestParam): - pass - - @abc.abstractmethod - async def save(self, match, name: FormalName, packet: BinaryStr): - pass - - -class InterestValidator(Policy, metaclass=abc.ABCMeta): - """ - InterestValidator policy describes how to verify an Interest packet. - """ - @abc.abstractmethod - async def validate(self, match, sig_ptrs: SignaturePtrs) -> bool: - pass - - -class DataValidator(Policy, metaclass=abc.ABCMeta): - """ - DataValidator policy describes how to verify a Data packet. - """ - @abc.abstractmethod - def get_validator(self, match) -> Validator: - pass - - -class Signing(Policy, metaclass=abc.ABCMeta): - """ - Signing policy gives a signer used to sign a packet. - When a user uses signing policy, he needs to specify whether its - :class:`InterestSigning` or :class:`DataSigning`. - """ - @abc.abstractmethod - async def get_signer(self, match) -> Signer: - pass - - -class InterestSigning(Signing, metaclass=abc.ABCMeta): - """ - InterestSigning policy is a type used to indicate the Interest signer. - Used as the type argument of set_policy. - """ - pass - - -class DataSigning(Signing, metaclass=abc.ABCMeta): - """ - DataSigning policy is a type used to indicate the Data signer. - Used as the type argument of set_policy. - """ - pass - - -class Encryption(Policy, metaclass=abc.ABCMeta): - """ - Encryption policy encrypts and decrypts content. - When a user uses encryption policy, he needs to specify whether its - :class:`InterestEncryption` or :class:`DataEncryption`. - """ - @abc.abstractmethod - async def decrypt(self, match, content: BinaryStr) -> BinaryStr | None: - pass - - @abc.abstractmethod - async def encrypt(self, match, content: BinaryStr) -> BinaryStr | None: - pass - - -class InterestEncryption(Encryption, metaclass=abc.ABCMeta): - """ - InterestSigning policy is a type used to indicate the Interest encryption policy. - Used as the type argument of set_policy. - """ - pass - - -class DataEncryption(Encryption, metaclass=abc.ABCMeta): - """ - DataEncryption policy is a type used to indicate the Data encryption policy. - Used as the type argument of set_policy. - """ - pass - - -class LocalOnly(Policy): - """ - LocalOnly means the Data should be stored in the local storage. - It prevents the node from sending Interest packets. - """ - pass - - -class Register(Policy): - """ - Register policy indicates the node should be registered as a prefix in the forwarder. - """ - pass diff --git a/src/ndn/schema/schema_tree.py b/src/ndn/schema/schema_tree.py deleted file mode 100644 index ad8c1fc..0000000 --- a/src/ndn/schema/schema_tree.py +++ /dev/null @@ -1,597 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -import asyncio as aio -from typing import Any -from dataclasses import dataclass -from ..encoding import is_binary_str, FormalName, NonStrictName, Name, Component, \ - SignaturePtrs, InterestParam, BinaryStr, MetaInfo, parse_data, TypeNumber -from ..app import NDNApp -from ..security import sha256_digest_checker, DigestSha256Signer -from ..utils import gen_nonce -from .util import norm_pattern -from . import policy - - -class NodeExistsError(Exception): - """ - Raised when trying to create a node which already exists. - """ - pattern: str - - def __init__(self, pattern: str): - self.pattern = pattern - - -class LocalResourceNotExistError(Exception): - """ - Raised when trying to fetch a local resource that does not exist. - Used only when :class:`LocalOnly` is attached to the node. - """ - name: FormalName - - def __init__(self, name: FormalName): - self.name = name - - -class Node: - """ - Node represents a node in the static namespace tree. - - :ivar policies: policies attached to this node - :vartype policies: Dict[Type[policy.Policy], policy.Policy] - :ivar prefix: the prefix of the root node of the tree. Generally not set for other nodes. - :vartype prefix: :any:`FormalName` - :ivar ~.app: the :any:`NDNApp` this static tree is attached to. Only available at the root. - :vartype ~.app: Optional[NDNApp] - """ - policies: dict[type[policy.Policy], policy.Policy] - prefix: FormalName - app: NDNApp | None - - def __init__(self, parent=None): - self.parent = parent - # Efficiency is not considered at this draft - self.children = {} - self.matches = {} - self.policies = {} - self.prefix = [] - self.app = None - - # def make_namespace(self, prefix: NonStrictName): - # ret = Node() - # self[prefix] = ret - # return ret - - # ====== Functions operating on children ====== - - def exist(self, key): - """ - If it has a child with specified name component or nme pattern. - - :param key: a name component (bytes) or a patten (tuple). - :return: whether the child node exists - """ - if is_binary_str(key): - return bytes(key) in self.children - else: - return key[:2] in self.matches - - def _get(self, key): - if is_binary_str(key): - return self.children[bytes(key)] - else: - return self.matches[key[:2]][1] - - def _set(self, key, val): - if is_binary_str(key): - self.children[bytes(key)] = val - else: - self.matches[key[:2]] = (key[2], val) - return val - - def __getitem__(self, key: str): - """ - Get a node in the subtree rooted at this node. - If any node on the path does not exist, this function will create it. - - :param key: the path from this node to the destination. Can contain both components and patterns. - :return: the desinated node. - """ - key_lst = norm_pattern(key) - cur = self - for k in key_lst: - try: - cur = cur._get(k) - except KeyError: - cur = cur._set(k, Node(cur)) - return cur - - def __setitem__(self, key: str, value): - """ - Set a node in the subtree rooted at this node. - If any node on the path does not exist, this function will create it. - - :param key: the path from this node to the destination. Can contain both components and patterns. - :param value: the destinated node. - :return: the same as ``value`` - """ - key_lst = norm_pattern(key) - cur = self - for k in key_lst[:-1]: - try: - cur = cur._get(k) - except KeyError: - cur = cur._set(k, Node(cur)) - if cur.exist(key_lst[-1]): - raise NodeExistsError(key) - cur._set(key_lst[-1], value) - value.parent = cur - return value - - def _match_step(self, comp: bytes, env, policies): - policies.update(self.policies) - chd = self.children.get(comp, None) - if chd is not None: - return chd - typ = Component.get_type(comp) - match = self.matches.get((0, typ), None) - if match is not None: - env[match[0]] = Component.get_value(comp) - return match[1] - else: - return None - - def match(self, name: NonStrictName): - """ - Start from this node, go the path that matches with the name, - and return the node it reaches when it cannot go further. - - :param name: an NDN name. - :return: a :class:`MatchedNode`, which contains the destination node and variables matched. - """ - if self.parent is not None: - raise ValueError('Node.match() should be called from root') - env = {} - policies = {} - cur = self - name = Name.normalize(name) - if self.prefix: - if len(name) < len(self.prefix) or name[:len(self.prefix)] != self.prefix: - raise ValueError(f'The name f{Name.to_str(name)} does not match with ' - f'the prefix of this node {Name.to_str(self.prefix)}') - pos = len(self.prefix) - else: - pos = 0 - while pos < len(name): - nxt = cur._match_step(bytes(name[pos]), env, policies) - if not nxt: - break - else: - cur = nxt - pos += 1 - if pos is None: - pos = len(name) - policies.update(cur.policies) - return MatchedNode(root=self, node=cur, name=name, pos=pos, env=env, policies=policies) - - # TODO: Apply - - # ====== Functions operating on policies ====== - - def get_policy(self, typ: type[policy.Policy]): - """ - Get the policy of specified type that applies to this node. - It can be attached to this node or a parent of this node. - - :param typ: a policy type - :return: the policy. None if there does not exist one. - """ - ret = None - cur = self - while ret is None and cur is not None: - ret = cur.policies.get(typ, None) - cur = cur.parent - return ret - - def set_policy(self, typ: type[policy.Policy], value: policy.Policy): - """ - Attach a policy to this node. - - :param typ: the policy type. - :param value: the policy to be attached to this node. - """ - if not isinstance(value, typ): - raise TypeError(f'The policy {value} is not of type {typ}') - self.policies[typ] = value - value.node = self - - # ====== Functions on registration ====== - - async def attach(self, app: NDNApp, prefix: NonStrictName): - r""" - Attach this node to a specified :any:`NDNApp`, register all name prefixes. - This node becomes the root node of the application static tree. - ``prefix`` is the prefix of the tree, which will be prepended to all names under this tree. - For example, if ``prefix='/a/blog'``, then the node with path ``/articles`` from this node - will become ``/a/blog/articles``. - - .. warning:: - - The way to register prefixes is still under discussion. - Currently, we register the nodes that we can reach without going through a pattern. - Also, there is no ``detach`` function yet, and no means to change the static tree after it's attached. - - :param app: the :any:`NDNApp` to be attached to. - :param prefix: the prefix of the static tree. - :return: whether succeeded or not. - """ - prefix = Name.normalize(prefix) - self.app = app - return await self.on_register(self, app, prefix, cached=False) - - # async def detach(self, app: NDNApp): - # raise NotImplementedError('TODO: Not supported yet. Please reset NDNApp.') - - async def on_register(self, root, app: NDNApp, prefix: FormalName, cached: bool): - """ - Called when the root node ``root`` is attached to ``app``, and the :meth:`attach` wants to - register prefixed under the subtree rooted at this node. - - :param root: the root of the static tree. - :param app: the :any:`NDNApp` to be attached to. - :param prefix: the prefix of the static tree. - :param cached: If there is a cache policy that applies to this node. - :return: whether succeeded or not. - """ - # If there is a register policy - if policy.Register in self.policies: - return await app.register(prefix, root._on_interest_root, root._int_validator, True) - # If it is cached with a match or being leaf - cached = cached or policy.Cache in self.policies - if cached: - if self.matches or not self.children: - return await app.register(prefix, root._on_interest_root, root._int_validator, True) - # O/w enumerate its children - for comp, chd in self.children.items(): - if not await chd.on_register(root, app, prefix + [comp], cached=cached): - return False - return True - - async def _int_validator(self, name: FormalName, sig_ptrs: SignaturePtrs) -> bool: - match = self.match(name) - validate_policy = match.policies.get(policy.InterestValidator, None) - if validate_policy is None: - return await sha256_digest_checker(name, sig_ptrs) - if isinstance(validate_policy, policy.InterestValidator): - return await validate_policy.validate(match, sig_ptrs) - else: - raise TypeError(f'The InterestValidator policy is of wrong type. Name={Name.to_str(name)}') - - def _on_interest_root(self, name: FormalName, param: InterestParam, - app_param: BinaryStr | None, raw_packet: BinaryStr): - match = self.match(name) - aio.create_task(match.on_interest(param, app_param, raw_packet)) - - # ====== Functions on Interest & Data processing (For overriding) ====== - - async def process_int(self, match, param: InterestParam, app_param: BinaryStr | None, raw_packet: BinaryStr): - """ - Processing an incoming Interest packet. Specific node type can override this function to have customized - processing pipeline. - - .. note:: - - This function will not be called if the Interest packet is satisfied with a cached Data packet. - - :param match: the matched node object of this node. - :param param: the parameters of the Interest packet. - :param app_param: the ApplicationParameters of the Interest packet. - :param raw_packet: the raw Interest packet. - """ - pass - - async def process_data(self, match, meta_info: MetaInfo, content: BinaryStr | None, raw_packet: BinaryStr): - """ - Processing an incoming Data packet. Specific node type can override this function to have customized - processing pipeline. By default it returns the content. - - :param match: the matched node object of this node. - :param meta_info: the MetaInfo of the Data packet. - :param content: the content of the Data packet. - :param raw_packet: the raw Data packet. - :return: a tuple, whose first element is data content after processing, and second is a ``dict[str, Any]`` - containing metadata. - """ - # Override this function to customize the processing - meta_data = { - **match.env, - 'content_type': meta_info.content_type, - 'freshness_period': meta_info.freshness_period, - 'final_block_id': meta_info.final_block_id - } - return content, meta_data - - async def need(self, match, **kwargs): - """ - Consume an object corresponding to this node. Specific node type can override this function to have customized - processing pipeline. For example, a SegmentedNode can do reassembly here. - By default it sends an Interest packet to fetch a Data. - - :param match: the matched node object of this node. - :param kwargs: other arguments from user input. - :return: This is defined by the node type. By default it returns what :meth:`process_data` returns. - That is, a tuple of contect and metadata dict. - """ - return await match.express(**kwargs) - - async def provide(self, match, content, **kwargs): - """ - Produce an object corresponding to this node, and make all generated Data packets available. - Specific node type can override this function to have customized processing pipeline. - For example, a SegmentedNode can do segmentation here. - By default it makes a Data packet out of content and put it into the cache. - - :param match: the matched node object of this node. - :param content: the content of the object. - :param kwargs: other arguments from user input. - """ - return await match.put_data(content, **kwargs) - - -@dataclass -class MatchedNode: - r""" - MatchedNode represents a matched static tree node. - That is, a node with all name patterns on the path from the root to it assigned to some value. - For example, if the tree contains a node N on the path ``/a//``, - and the user use the Name ``/a/x/y`` to match, - then a matched node (N, {'b': 'x', 'c': 'y'}) will be returned. - - :ivar root: the root of the static tree. - :vartype root: Node - :ivar node: the matched node of the static tree. - :vartype node: Node - :ivar name: the name used to match. - :vartype name: :any:`FormalName` - :ivar pos: an integer indicating the length the name is matched. Generally, it equals the length of ``name``. - :vartype pos: int - :ivar env: a dict containing the value all pattern variables matched on the path. - :vartype env: Dict[str, Any] - :ivar policies: a dict collecting all policies that apply to this node. - For each type of policy, the one attached on the nearst ancestor is collected here. - :vartype policies: Dict[Type[policy.Policy], policy.Policy] - """ - root: Node - node: Node - name: FormalName - pos: int - env: dict[str, Any] - policies: dict[type[policy.Policy], policy.Policy] - - def finer_match(self, new_name: FormalName): - """ - Do a finer match based on current match. ``new_name`` must include current ``name`` as its prefix. - For example, if the current match name is ``/a/b`` and we want to get the matched node for ``/a/b/c``, - then we can call finer_match with ``/a/b/c``. - - :param new_name: the new name to be matched. Must include current ``name`` as its prefix. - :return: the new matched node. - """ - name_len = len(self.name) - if self.pos < name_len: - # match = self.finer_match(data_name[name_len:]) - return MatchedNode(root=self.root, node=self.node, name=new_name, pos=self.pos, - env=self.env, policies=self.policies) - - env = self.env.copy() - policies = self.policies.copy() - pos = None - cur = self.node - for i in range(name_len, len(new_name)): - comp = new_name[i] - nxt = cur._match_step(bytes(comp), env, policies) - if not nxt: - pos = i - break - else: - cur = nxt - if pos is None: - pos = len(new_name) - policies.update(cur.policies) - return MatchedNode(root=self.root, node=cur, name=new_name, pos=pos, env=env, policies=policies) - - async def on_interest(self, param: InterestParam, app_param: BinaryStr | None, raw_packet: BinaryStr): - """ - Called when an Interest packet comes. - It looks up the cache and returns a Data packet if it exists. - Otherwise, it decrypts ApplicationParameters and calls the node's ``process_int`` function. - - :param param: the parameters of the incoming Interest. - :param app_param: the ApplicationParameters of the Interest. - :param raw_packet: the raw Interest packet. - """ - # Cache search - cache_policy = self.policies.get(policy.Cache, None) - if cache_policy and isinstance(cache_policy, policy.Cache): - data_raw = await cache_policy.search(self, self.name, param) - if data_raw is not None: - self.root.app.put_raw_packet(data_raw) - return - # By design, we do not cache Interest - # Decrypt app_param - if app_param: - ac_policy = self.policies.get(policy.InterestEncryption, None) - if ac_policy and isinstance(ac_policy, policy.InterestEncryption): - app_param = await ac_policy.decrypt(self, app_param) - # Process Interest - await self.node.process_int(self, param, app_param, raw_packet) - - async def on_data(self, meta_info: MetaInfo, content: BinaryStr | None, raw_packet: BinaryStr): - """ - Called when a Data packet comes. - It saves the Data packet into the cache, decrypts the content, and calls - the node's ``process_data`` function. - - :param meta_info: the MetaInfo of the incoming Data packet. - :param content: the content of the Data. - :param raw_packet: the raw Data packet. - :return: whatever ``process_data`` returns. - """ - # Cache save - if policy.LocalOnly not in self.policies: - cache_policy = self.policies.get(policy.Cache, None) - if cache_policy and isinstance(cache_policy, policy.Cache): - # aio.ensure_future(cache_policy.save(self, self.name, raw_packet)) - # self.name may change after this time point, so we have to wait until its finish - await cache_policy.save(self, self.name, raw_packet) - # Decrypt content - if content is not None: - ac_policy = self.policies.get(policy.DataEncryption, None) - if ac_policy and isinstance(ac_policy, policy.DataEncryption): - content = await ac_policy.decrypt(self, content) - # Process Data - return await self.node.process_data(self, meta_info, content, raw_packet) - - async def express(self, app_param: BinaryStr | None = None, **kwargs): - """ - Try to fetch the data, called by the node's need function. - It will search the local cache, and examines the local resource. - If the corresponding Data cannot be found in the two places, - it encrypts the app_param and expresses the Interest. - - .. note:: - - This function only sends out an Interest packet when the Data is not cached locally. - - :param app_param: the ApplicationParameter of the Interest. - :param kwargs: other parameters of the Interest. - :return: whatever ``process_data`` returns. - Generally this function is only called at the default node, - so the return value is a tuple of the content and a dict containing metadata. - """ - if 'nonce' not in kwargs: - kwargs['nonce'] = gen_nonce() - param = InterestParam.from_dict(kwargs) - - # Cache search - cache_policy = self.policies.get(policy.Cache, None) - if cache_policy and isinstance(cache_policy, policy.Cache): - data_raw = await cache_policy.search(self, self.name, param) - if data_raw is not None: - with_tl = (data_raw[0] == TypeNumber.DATA) - data_name, meta_info, content, _ = parse_data(data_raw, with_tl=with_tl) - return await self.finer_match(data_name).on_data(meta_info, content, data_raw) - # Local only? - local_policy = self.policies.get(policy.LocalOnly, None) - if local_policy: - raise LocalResourceNotExistError(self.name) - # Encrypt app_param - if app_param is not None: - ac_policy = self.policies.get(policy.InterestEncryption, None) - if ac_policy and isinstance(ac_policy, policy.InterestEncryption): - app_param = await ac_policy.encrypt(self, app_param) - # Get validator TODO: How can we pass information out? - validate_policy = self.policies.get(policy.DataValidator, None) - if validate_policy and isinstance(validate_policy, policy.DataValidator): - validator = validate_policy.get_validator(self) - else: - validator = sha256_digest_checker # Change this if possible - # Get signer - signer_policy = self.policies.get(policy.InterestSigning, None) - if signer_policy and isinstance(signer_policy, policy.InterestSigning): - signer = signer_policy.get_signer(self) - elif app_param is not None: - signer = DigestSha256Signer() - else: - signer = None - # Express interest - data = await self.root.app.express_interest(self.name, app_param, validator, need_raw_packet=True, - interest_param=param, signer=signer) - data_name, meta_info, content, data_raw = data - return await self.finer_match(data_name).on_data(meta_info, content, data_raw) - - def need(self, **kwargs): - """ - Consume an object corresponding to this node. Specific node type may have customized - processing pipeline. For example, a SegmentedNode can do reassembly here. - By default it sends an Interest packet to fetch a Data. - - MatchedNode's ``need`` simply calls the node's ``need`` function. - - :param kwargs: arguments from user input. - :return: the object needed, whose format is defined by specific node type. - By default, it returns a tuple of the content and a dict of metadata. - """ - return self.node.need(self, **kwargs) - - def provide(self, content, **kwargs): - """ - Produce an object corresponding to this node, and make all generated Data packets available. - Specific node type may have customized processing pipeline. - For example, a SegmentedNode can do segmentation here. - By default it makes a Data packet out of content and put it into the cache. - - MatchedNode's ``provide`` simply calls the node's ``provide`` function. - - :param content: the content of the object. - :param kwargs: other arguments from user input. Defined by specific node type. - """ - return self.node.provide(self, content, **kwargs) - - async def put_data(self, content: BinaryStr | None = None, send_packet: bool = False, **kwargs): - """ - Generate the Data packet out of content. - This function encrypts the content, encodes and signs the packet, saves it into the cache, - and optionally sends it to the face. - This function is called by the node's ``provide`` function. - - :param content: the Data content. - :param send_packet: whether sends the Data packet to the face. - :param kwargs: other arguments generating the Data packet. - """ - meta_info = MetaInfo.from_dict(kwargs) - data_name = self.name - # Encrypt content - if content is not None: - ac_policy = self.policies.get(policy.DataEncryption, None) - if ac_policy and isinstance(ac_policy, policy.DataEncryption): - content = await ac_policy.encrypt(self, content) - # Get signer - signer_policy = self.policies.get(policy.DataSigning, None) - if signer_policy and isinstance(signer_policy, policy.DataSigning): - signer = signer_policy.get_signer(self) - else: - signer = self.root.app.keychain.get_signer(kwargs) - # Prepare Data packet - raw_packet = self.root.app.prepare_data(data_name, content, meta_info=meta_info, signer=signer) - # Cache save - cache_policy = self.policies.get(policy.Cache, None) - if cache_policy and isinstance(cache_policy, policy.Cache): - # aio.ensure_future(cache_policy.save(self, self.name, raw_packet)) - await cache_policy.save(self, self.name, raw_packet) - # face.put - if send_packet: - self.root.app.put_raw_packet(raw_packet) - - def app(self) -> NDNApp: - """ - The :any:`NDNApp` the static tree is attached to. - - :return: the :any:`NDNApp`. - """ - return self.root.app diff --git a/src/ndn/schema/simple_cache.py b/src/ndn/schema/simple_cache.py deleted file mode 100644 index cde4b3f..0000000 --- a/src/ndn/schema/simple_cache.py +++ /dev/null @@ -1,69 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -import logging -from ..encoding import FormalName, Name, BinaryStr, InterestParam -from ..name_tree import NameTrie -from .schema_tree import MatchedNode -from . import policy - - -class MemoryCache: - """ - MemoryCache is a simple cache class that supports searching and storing Data packets in the memory. - """ - def __init__(self): - self.data = NameTrie() - - async def search(self, name: FormalName, param: InterestParam): - """ - Search for the data packet that satisfying an Interest packet with name specified. - - :param name: the Interest name. - :param param: the parameters of the Interest. Not used in current implementation. - :return: a raw Data packet or None. - """ - try: - return next(self.data.itervalues(prefix=name, shallow=True)) - except KeyError: - logging.getLogger(__name__).debug('Cache miss: %s', Name.to_str(name)) - return None - - async def save(self, name: FormalName, packet: BinaryStr): - """ - Save a Data packet with name into the memory storage. - - :param name: the Data name. - :param packet: the raw Data packet. - """ - logging.getLogger(__name__).debug('Cache save: %s', Name.to_str(name)) - self.data[name] = bytes(packet) - - -class MemoryCachePolicy(policy.Cache): - """ - MemoryCachePolicy stores Data packets in memory. - """ - def __init__(self, cache): - super().__init__() - self.cache = cache - - async def search(self, match: MatchedNode, name: FormalName, param: InterestParam): - return await self.cache.search(name, param) - - async def save(self, match: MatchedNode, name: FormalName, packet: BinaryStr): - await self.cache.save(name, packet) diff --git a/src/ndn/schema/simple_node.py b/src/ndn/schema/simple_node.py deleted file mode 100644 index 7d6122c..0000000 --- a/src/ndn/schema/simple_node.py +++ /dev/null @@ -1,179 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -# TODO: Change these names -from .schema_tree import Node -from .util import norm_pattern -from ..encoding import Name, Component, TlvModel, NameField, ContentType -from ..types import InterestTimeout -from ..utils import timestamp - - -class LocalResource(Node): - """ - LocalResource is a custom node that preloads some data. - When need() is called, it returns the loaded data directly. - This node type does not interact with the network. - """ - def __init__(self, parent=None, data=None): - super().__init__(parent) - self.data = data - - async def on_register(self, root, app, prefix, cached: bool): - self.prefix = prefix - self.app = app - return await app.register(prefix, root._on_interest_root, root._int_validator, True) - - async def need(self, match, **kwargs): - return self.data - - async def provide(self, match, content, **kwargs): - self.data = content - - -class SegmentedNode(Node): - """ - SegmentedNode represents a segmented object. - The segmented object is composed with multiple Data packets, - whose name have a suffix "/seg=seg_no" attached to the object's name. - The ``provide`` function handles segmentation, and the ``need`` function handles reassembly. - - .. note:: - - Currently, the fetching pipeline is a simple one-by-one pipeline. - where only one Interest will be in-flight at one time. - """ - SEGMENT_PATTERN = norm_pattern('')[0] - SEGMENT_SIZE = 4400 - - def __init__(self, parent=None, timeout=4000, retry_times=3, segment_size=SEGMENT_SIZE): - super().__init__(parent) - self._set(self.SEGMENT_PATTERN, Node()) - self.timeout = timeout - self.retry_times = retry_times - self.segment_size = segment_size - - async def retry(self, submatch, must_be_fresh): - trial_times = 0 - while True: - try: - return await submatch.need(must_be_fresh=must_be_fresh, lifetime=self.timeout, can_be_prefix=False) - except InterestTimeout: - trial_times += 1 - if trial_times >= self.retry_times: - raise - - async def need(self, match, **kwargs): - if match.pos < len(match.name): - raise ValueError(f'{Name.to_str(match.name)} does not match with the structure') - subname = match.name + [None] - must_be_fresh = kwargs.get('must_be_fresh', True) - contents = [] - cur = 0 - while True: - subname[-1] = Component.from_segment(cur) - submatch = match.finer_match(subname) - segment, meta_data = await self.retry(submatch, must_be_fresh) - contents.append(segment) - if meta_data['final_block_id'] == subname[-1]: - break - cur += 1 - ret = b''.join(contents) - meta_data_ret = { - **match.env, - 'content_type': meta_data['content_type'], - 'block_count': cur + 1, - 'freshness_period': meta_data['freshness_period'] - } - return ret, meta_data_ret - - async def provide(self, match, content, **kwargs): - seg_cnt = (len(content) + self.segment_size - 1) // self.segment_size - subname = match.name + [None] - final_block_id = Component.from_segment(seg_cnt - 1) - for i in range(seg_cnt): - subname[-1] = Component.from_segment(i) - submatch = match.finer_match(subname) - kwargs['final_block_id'] = final_block_id - await submatch.provide(content[i*self.segment_size:(i+1)*self.segment_size], **kwargs) - - async def process_int(self, match, param, app_param, raw_packet): - if match.pos == len(match.name): - submatch = match.finer_match(match.name + [Component.from_segment(0)]) - return await submatch.on_interest(param, None, raw_packet) - - -class RDRNode(Node): - """ - RDRNode represents a versioned and segmented object whose encoding follows the RDR protocol. - Its ``provide`` function generates the metadata packet, and ``need`` function handles version discovery. - """ - class MetaDataValue(TlvModel): - name = NameField() - - class MetaData(Node): - VERSION_PATTERN = norm_pattern('')[0] - FRESHNESS_PERIOD = 10 - - def __init__(self, parent=None): - super().__init__(parent) - self._set(self.VERSION_PATTERN, Node(self)) - - def make_metadata(self, match): - metadata = RDRNode.MetaDataValue() - metadata.name = match.name[:-1] + [Component.from_version(self.parent.timestamp)] - return metadata.encode() - - async def process_int(self, match, param, app_param, raw_packet): - if match.pos == len(match.name) and self.parent.timestamp is not None and param.can_be_prefix: - metaname = match.name + [Component.from_version(timestamp())] - submatch = match.finer_match(metaname) - await submatch.put_data(self.make_metadata(match), send_packet=True, - freshness_period=self.FRESHNESS_PERIOD) - - async def need(self, match, **kwargs): - if self.parent.timestamp is None: - return await super().need(match, **kwargs) - else: - meta_info = { - **match.env, - 'content_type': ContentType.BLOB, - 'freshness_period': self.FRESHNESS_PERIOD, - 'final_block_id': None - } - return self.make_metadata(match), meta_info - - def __init__(self, parent=None, **kwargs): - super().__init__(parent) - self['/32=metadata'] = RDRNode.MetaData(self) - self[''] = SegmentedNode(self, **kwargs) - self.timestamp = None - - async def need(self, match, **kwargs): - submatch = match.finer_match(match.name + [Component.from_str('32=metadata')]) - lifetime = kwargs.get('lifetime', None) - meta_int_param = {'lifetime': lifetime} if lifetime else {} - metadata_val, _ = await submatch.need(must_be_fresh=True, can_be_prefix=True, **meta_int_param) - metadata = RDRNode.MetaDataValue.parse(metadata_val, ignore_critical=True) - - submatch = match.finer_match(metadata.name) - return await submatch.need(**kwargs) - - async def provide(self, match, content, **kwargs): - self.timestamp = timestamp() - submatch = match.finer_match(match.name + [Component.from_version(self.timestamp)]) - await submatch.provide(content, **kwargs) diff --git a/src/ndn/schema/simple_trust.py b/src/ndn/schema/simple_trust.py deleted file mode 100644 index 8f14479..0000000 --- a/src/ndn/schema/simple_trust.py +++ /dev/null @@ -1,132 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -import logging -from typing import Any -from collections.abc import Callable -from Cryptodome.PublicKey import ECC, RSA -from Cryptodome.Signature import DSS, pkcs1_15 -from Cryptodome.Hash import SHA256 -from ..encoding import SignaturePtrs, FormalName, Name, SignatureType -from ..types import Validator, NetworkError, InterestTimeout, InterestNack, ValidationFailure -from .schema_tree import Node -from . import policy - - -Checker = Callable[[dict[str, Any], dict[str, Any]], bool] - - -class SignedBy(policy.DataValidator, policy.InterestValidator): - r""" - SignedBy policy represents the trust schema, - specifying the key used to signed the Interest or Data packet. - It does the follows: - - - Match the key used to sign the packet in the static tree. - The real key must match the node specified by ``key``. - Otherwise, the validation fails. - - Call the checker ``subject_to`` with two matching variable dict. - Fail if the checker returns ``False``. - - Call the ``need`` function of the matched key node to get the public key. - Fail if the key cannot be fetched. - - Verify the signature. - - .. note:: - - Theoretically, SignedBy should also give the signer used to sign outgoing packets. - However, this function is missing in current implementation. - - For example, - - .. code-block:: python3 - - # This checker checks the Author of Data is the same as the Author of the key. - def check_author(data_env, key_env): - return data_env['Author'] == key_env['Author'] - - root = Node() - root['/author//KEY//self/'] = Node() - root['/blog///'] = Node() - # The Data "/blog///" should be signed by - # the key "/author//KEY/" with the same author. - root['/blog///'].set_policy( - policy.DataValidator, - SignedBy(root['/author//KEY/'], subject_to=check_author)) - """ - def __init__(self, key: Node, subject_to: Checker = None): - super().__init__() - self.key = key - self.subject_to = subject_to - self.logger = logging.getLogger(__name__) - - def get_validator(self, match) -> Validator: - def validator(name: FormalName, sig_ptrs: SignaturePtrs): - return self.validate(match.finer_match(name), sig_ptrs) - return validator - - async def validate(self, match, sig_ptrs: SignaturePtrs) -> bool: - # Check key name - if sig_ptrs.signature_info is None or sig_ptrs.signature_info.key_locator is None: - self.logger.info('%s => Not signed', Name.to_str(match.name)) - return False - key_name = sig_ptrs.signature_info.key_locator.name - if not key_name: - self.logger.info('%s => Not signed', Name.to_str(match.name)) - return False - key_match = match.root.match(key_name) - if key_match.node is not self.key: - self.logger.info('%s => The key name %s mismatch', Name.to_str(match.name), Name.to_str(key_name)) - return False - if self.subject_to and not self.subject_to(match.env, key_match.env): - self.logger.info('%s => The key name %s mismatch', Name.to_str(match.name), Name.to_str(key_name)) - return False - # Get key_bits - try: - key_bits, _ = await key_match.need(must_be_fresh=True, can_be_prefix=True) - except (NetworkError, InterestNack, InterestTimeout) as e: - self.logger.info('%s => Unable to fetch the key %s due to %s', Name.to_str(match.name), Name.to_str(key_name), e) - return False - except ValidationFailure: - self.logger.info('%s => The key %s cannot be verified', Name.to_str(match.name), Name.to_str(key_name)) - return False - # Import key - sig_type = sig_ptrs.signature_info.signature_type - key_bits = bytes(key_bits) - try: - if sig_type == SignatureType.SHA256_WITH_RSA: - pub_key = RSA.import_key(key_bits) - verifier = pkcs1_15.new(pub_key) - elif sig_type == SignatureType.SHA256_WITH_ECDSA: - pub_key = ECC.import_key(key_bits) - verifier = DSS.new(pub_key, 'fips-186-3', 'der') - else: - self.logger.info('%s => Unrecognized signature type %s', Name.to_str(match.name), sig_type) - return False - except (ValueError, IndexError, TypeError): - self.logger.info('%s => The key %s is malformed', Name.to_str(match.name), Name.to_str(key_name)) - return False - # Verify signature - h = SHA256.new() - for content in sig_ptrs.signature_covered_part: - h.update(content) - try: - verifier.verify(h, bytes(sig_ptrs.signature_value_buf)) - except ValueError: - self.logger.info('%s => Unable to verify the signature', Name.to_str(match.name)) - return False - self.logger.debug('%s => Verification passed', Name.to_str(match.name)) - return True diff --git a/src/ndn/schema/util.py b/src/ndn/schema/util.py deleted file mode 100644 index 141dfe5..0000000 --- a/src/ndn/schema/util.py +++ /dev/null @@ -1,63 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -from ..encoding import Name, Component, BinaryStr - -NamePattern = list[BinaryStr | tuple[int, int, str]] -r""" -NamePattern is a list containing mixed name components and varaible patterns. -A variable pattern is a capturing pattern that matches with exactly one name component. -It is a tuple containing 3 variables: - -- The 1st element is reserved and always 0. This is a quick and dirty solution in this PoC implementation - It will be used if we want to support multiple name components matching patterns. -- The 2nd element is the TLV type of the name component to be matched. -- The 3rd element is the name of the pattern variable. -""" - - -def norm_pattern(name: str) -> NamePattern: - """ - This function returns a normalized name pattern from a string, just like normalizing a name. - - :param name: the name pattern string. - :return: normalized name pattern. - """ - ret = Name.normalize(name)[:] - for i, comp in enumerate(ret): - comp_type = Component.get_type(comp) - comp_value = Component.get_value(comp) - if comp_type == Component.TYPE_GENERIC and comp_value[0] == b'<'[0] and comp_value[-1] == b'>'[0]: - content = bytes(comp_value[1:-1]).decode() - eq_sgn = content.find(':') - if eq_sgn >= 0: - type_str = content[:eq_sgn] - if type_str == 'sha256digest': - type_val = Component.TYPE_IMPLICIT_SHA256 - elif type_str == 'params-sha256': - type_val = Component.TYPE_PARAMETERS_SHA256 - elif type_str in Component.ALTERNATE_URI_STR: - type_val = Component.ALTERNATE_URI_STR[type_str] - else: - type_val = int(type_str) - content = content[eq_sgn+1:] - else: - type_val = Component.TYPE_GENERIC - if not content: - raise ValueError('Pattern variable name cannot be empty') - ret[i] = (0, type_val, content) - return ret diff --git a/src/ndn/security/validator/__init__.py b/src/ndn/security/validator/__init__.py index 7f9fc73..353695c 100644 --- a/src/ndn/security/validator/__init__.py +++ b/src/ndn/security/validator/__init__.py @@ -1,9 +1,9 @@ -from .digest_validator import sha256_digest_checker, params_sha256_checker, union_checker +from .digest_validator import sha256_digest_checker, params_sha256_checker from .known_key_validator import verify_rsa, verify_ecdsa, verify_hmac, \ EccChecker, RsaChecker, HmacChecker, verify_ed25519, Ed25519Checker -__all__ = ['sha256_digest_checker', 'params_sha256_checker', 'union_checker', +__all__ = ['sha256_digest_checker', 'params_sha256_checker', 'verify_ecdsa', 'verify_rsa', 'verify_hmac', 'EccChecker', 'RsaChecker', 'HmacChecker', 'verify_ed25519', 'Ed25519Checker'] diff --git a/src/ndn/security/validator/cascade_validator.py b/src/ndn/security/validator/cascade_validator.py deleted file mode 100644 index 48bf87d..0000000 --- a/src/ndn/security/validator/cascade_validator.py +++ /dev/null @@ -1,122 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -import abc -import logging -from typing import Any -from collections.abc import Coroutine -from Cryptodome.PublicKey import ECC, RSA -from ...encoding import FormalName, BinaryStr, SignatureType, Name, parse_data, SignaturePtrs -from ...app import NDNApp, Validator, ValidationFailure, InterestTimeout, InterestNack -from .known_key_validator import verify_rsa, verify_hmac, verify_ecdsa - - -class PublicKeyStorage(abc.ABC): - @abc.abstractmethod - def load(self, name: FormalName) -> bytes | None: - pass - - @abc.abstractmethod - def save(self, name: FormalName, key_bits: bytes): - pass - - -class EmptyKeyStorage(PublicKeyStorage): - def load(self, name: FormalName) -> bytes | None: - return None - - def save(self, name: FormalName, key_bits: bytes): - return - - -class MemoryKeyStorage(PublicKeyStorage): - _cache: dict[bytes, bytes] - - def __init__(self): - self._cache = {} - - def load(self, name: FormalName) -> bytes | None: - return self._cache.get(Name.to_bytes(name), None) - - def save(self, name: FormalName, key_bits: bytes): - self._cache[Name.to_bytes(name)] = key_bits - - -class CascadeChecker: - app: NDNApp - next_level: Validator - storage: PublicKeyStorage | None - anchor_key: bytes - anchor_name: FormalName - - @staticmethod - def _verify_sig(pub_key_bits, sig_ptrs) -> bool: - if sig_ptrs.signature_info.signature_type == SignatureType.HMAC_WITH_SHA256: - verify_hmac(pub_key_bits, sig_ptrs) - elif sig_ptrs.signature_info.signature_type == SignatureType.SHA256_WITH_RSA: - pub_key = RSA.import_key(bytes(pub_key_bits)) - return verify_rsa(pub_key, sig_ptrs) - elif sig_ptrs.signature_info.signature_type == SignatureType.SHA256_WITH_ECDSA: - pub_key = ECC.import_key(bytes(pub_key_bits)) - return verify_ecdsa(pub_key, sig_ptrs) - else: - return False - - def __init__(self, app: NDNApp, trust_anchor: BinaryStr, storage: PublicKeyStorage = MemoryKeyStorage()): - self.app = app - self.next_level = self - self.storage = storage - cert_name, _, key_bits, sig_ptrs = parse_data(trust_anchor) - self.anchor_name = [bytes(c) for c in cert_name] # Copy the name in case - self.anchor_key = bytes(key_bits) - if not self._verify_sig(self.anchor_key, sig_ptrs): - raise ValueError('Trust anchor is not properly self-signed') - self.logger = logging.getLogger(__name__) - - async def validate(self, name: FormalName, sig_ptrs: SignaturePtrs) -> bool: - if (not sig_ptrs.signature_info or not sig_ptrs.signature_info.key_locator - or not sig_ptrs.signature_info.key_locator.name): - return False - # Obtain public key - cert_name = sig_ptrs.signature_info.key_locator.name - self.logger.debug('Verifying %s <- %s ...', Name.to_str(name), Name.to_str(cert_name)) - if cert_name == self.anchor_name: - self.logger.debug('Use trust anchor.') - key_bits = self.anchor_key - else: - if key_bits := self.storage.load(cert_name): - self.logger.debug('Use cached public key.') - else: - self.logger.debug('Cascade fetching public key ...') - # Try to fetch - try: - _, _, key_bits = await self.app.express_interest( - name=cert_name, must_be_fresh=True, can_be_prefix=False, - validator=self.next_level) - except (ValidationFailure, InterestTimeout, InterestNack): - self.logger.debug('Public key not valid.') - return False - self.logger.debug('Public key fetched.') - if key_bits: - self.storage.save(cert_name, key_bits) - # Validate signature - if not key_bits: - return False - return self._verify_sig(key_bits, sig_ptrs) - - def __call__(self, name: FormalName, sig_ptrs: SignaturePtrs) -> Coroutine[Any, None, bool]: - return self.validate(name, sig_ptrs) diff --git a/src/ndn/security/validator/digest_validator.py b/src/ndn/security/validator/digest_validator.py index f88581b..a6f99e9 100644 --- a/src/ndn/security/validator/digest_validator.py +++ b/src/ndn/security/validator/digest_validator.py @@ -18,7 +18,7 @@ import logging from hashlib import sha256 from ...encoding import FormalName, SignatureType, Name, SignaturePtrs -from ...types import Validator, ValidResult +from ...types import ValidResult async def sha256_digest_checker(name: FormalName, sig: SignaturePtrs, _context=None) -> ValidResult: @@ -52,15 +52,3 @@ async def params_sha256_checker(name: FormalName, sig: SignaturePtrs) -> bool: ret = sha256_algo.digest() == sig_value logging.getLogger(__name__).debug('Interest params-sha256 check %s -> %s', Name.to_str(name), ret) return ret - - -def union_checker(*args) -> Validator: - async def wrapper(name: FormalName, sig: SignaturePtrs, context) -> ValidResult: - for checker in args: - result = await checker(name, sig, context) - if result in (ValidResult.FAIL, ValidResult.TIMEOUT): - return result - if result is ValidResult.ALLOW_BYPASS: - return result - return ValidResult.PASS - return wrapper diff --git a/src/ndn/security/validator/known_key_validator.py b/src/ndn/security/validator/known_key_validator.py index 2a21d8c..86c3a89 100644 --- a/src/ndn/security/validator/known_key_validator.py +++ b/src/ndn/security/validator/known_key_validator.py @@ -20,7 +20,7 @@ from Cryptodome.PublicKey import ECC, RSA from Cryptodome.Signature import DSS, pkcs1_15, eddsa from ...encoding import FormalName, BinaryStr, NonStrictName, SignaturePtrs, Name, SignatureType -from ...types import Validator, ValidResult +from ...types import ValidResult from ...app_support.security_v2 import parse_certificate @@ -66,7 +66,7 @@ def _verify(cls, pub_key_bits, sig_ptrs) -> bool: pass @classmethod - def from_key(cls, key_name: NonStrictName, pub_key_bits: BinaryStr) -> Validator: + def from_key(cls, key_name: NonStrictName, pub_key_bits: BinaryStr): key_name = Name.normalize(key_name) async def validator(_name: FormalName, sig_ptrs: SignaturePtrs, _context=None) -> ValidResult: @@ -81,7 +81,7 @@ async def validator(_name: FormalName, sig_ptrs: SignaturePtrs, _context=None) - return validator @classmethod - def from_cert(cls, certificate: BinaryStr) -> Validator: + def from_cert(cls, certificate: BinaryStr): cert = parse_certificate(certificate) key_name = cert.name[:-2] key_bits = cert.content diff --git a/src/ndn/types.py b/src/ndn/types.py index cf7f785..ab5fae0 100644 --- a/src/ndn/types.py +++ b/src/ndn/types.py @@ -16,16 +16,7 @@ # limitations under the License. # ----------------------------------------------------------------------------- from enum import Enum -from typing import Any -from collections.abc import Callable, Coroutine -from .encoding import FormalName, MetaInfo, BinaryStr, InterestParam, SignaturePtrs - - -Route = Callable[[FormalName, InterestParam, BinaryStr | None], None] -r"""An OnInterest callback function for a route.""" - -Validator = Callable[[FormalName, SignaturePtrs], Coroutine[Any, None, bool]] -r"""A validator used to validate an Interest or Data packet.""" +from .encoding import FormalName, MetaInfo, BinaryStr, SignaturePtrs # For internal use. = (FormalName, MetaInfo, Content, SigPtrs, RawPacket) DataTuple = tuple[FormalName, MetaInfo, BinaryStr | None, SignaturePtrs, BinaryStr] @@ -77,7 +68,7 @@ class ValidResult(Enum): """ Validation result returned by a validator. Most of them are designed for the union checker, which chains multiple checkers in order. - For NDNApp (v2), only PASS and ALLOW_BYPASS are considered as True. + For NDNApp, only PASS and ALLOW_BYPASS are considered successful. """ FAIL = -2 diff --git a/tests/integration/app_v2_test.py b/tests/integration/app_v2_test.py deleted file mode 100644 index 22fbc28..0000000 --- a/tests/integration/app_v2_test.py +++ /dev/null @@ -1,268 +0,0 @@ -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2022 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -import abc -import asyncio as aio -import pytest -from ndn import appv2 as app -from ndn import security as sec -from ndn import encoding as enc -from ndn import types -from ndn.transport.dummy_face import DummyFace - - -class NDNAppTestSuite: - app = None - signer = None - - def test_main(self): - aio.run(self.comain()) - - async def comain(self): - face = DummyFace(self.face_proc) - self.signer = sec.DigestSha256Signer() - self.app = app.NDNApp(face) - face.app = self.app - await self.app.main_loop(self.app_main()) - - @abc.abstractmethod - async def face_proc(self, face: DummyFace): - pass - - @abc.abstractmethod - async def app_main(self): - pass - - -class TestConsumerBasic(NDNAppTestSuite): - async def face_proc(self, face: DummyFace): - await face.consume_output(b'\x050\x07(\x08\x07example\x08\x07testApp\x08\nrandomData' - b'\x38\x08\x00\x00\x01m\xa4\xf3\xffm\x12\x00\x0c\x02\x17p') - await face.input_packet(b'\x06B\x07(\x08\x07example\x08\x07testApp\x08\nrandomData' - b'\x38\x08\x00\x00\x01m\xa4\xf3\xffm\x14\x07\x18\x01\x00\x19\x02\x03\xe8' - b'\x15\rHello, world!') - - async def app_main(self): - name = '/example/testApp/randomData/t=1570430517101' - data_name, content, pkt_context = await self.app.express( - name, app.pass_all, must_be_fresh=True, can_be_prefix=False, lifetime=6000, nonce=None) - assert data_name == enc.Name.from_str(name) - assert pkt_context['meta_info'].freshness_period == 1000 - assert content == b'Hello, world!' - - -class TestInterestCancel(NDNAppTestSuite): - async def face_proc(self, face: DummyFace): - await face.consume_output(b'\x05\x15\x07\x0f\x08\rnot important\x0c\x02\x0f\xa0') - - async def app_main(self): - with pytest.raises(types.InterestCanceled): - await self.app.express('not important', app.pass_all, nonce=None) - - -class TestInterestNack(NDNAppTestSuite): - async def face_proc(self, face: DummyFace): - await face.consume_output(b'\x05)\x07\x1f\x08\tlocalhost\x08\x03nfd\x08\x05faces\x08\x06events' - b'\x21\x00\x12\x00\x0c\x02\x03\xe8') - await face.input_packet(b'\x64\x36\xfd\x03\x20\x05\xfd\x03\x21\x01\x96' - b'\x50\x2b\x05)\x07\x1f\x08\tlocalhost\x08\x03nfd\x08\x05faces\x08\x06events' - b'\x21\x00\x12\x00\x0c\x02\x03\xe8') - - async def app_main(self): - with pytest.raises(types.InterestNack) as nack: - await self.app.express('/localhost/nfd/faces/events', app.pass_all, nonce=None, lifetime=1000, - must_be_fresh=True, can_be_prefix=True) - assert nack.value.reason == 150 - - -class TestInterestTimeout(NDNAppTestSuite): - async def face_proc(self, face: DummyFace): - await face.consume_output(b'\x05\x14\x07\x0f\x08\rnot important\x0c\x01\x0a') - await aio.sleep(0.05) - - async def app_main(self): - with pytest.raises(types.InterestTimeout): - await self.app.express('not important', app.pass_all, nonce=None, lifetime=10) - - -class TestDataValidationFalure(NDNAppTestSuite): - @staticmethod - async def validator(_name, _sig, _context) -> types.ValidResult: - await aio.sleep(0.003) - return types.ValidResult.FAIL - - async def face_proc(self, face: DummyFace): - await face.consume_output(b'\x05\x1b\x07\x10\x08\x03not\x08\timportant\n\x04\x00\x00\x00\x00\x0c\x01\xfa') - await face.input_packet(b'\x06\x1d\x07\x10\x08\x03not\x08\timportant\x14\x03\x18\x01\x00\x15\x04test') - - async def app_main(self): - with pytest.raises(types.ValidationFailure) as e: - await self.app.express('/not/important', validator=self.validator, nonce=0, lifetime=250) - assert e.value.name == enc.Name.from_str('/not/important') - assert e.value.content == b'test' - assert e.value.result == types.ValidResult.FAIL - - -class TestInterestCanBePrefix(NDNAppTestSuite): - async def face_proc(self, face: DummyFace): - await face.consume_output(b'\x05\x0a\x07\x05\x08\x03not\x0c\x01\xfa' - b'\x05\x0c\x07\x05\x08\x03not\x21\x00\x0c\x01\xfa' - b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\xfa') - await face.input_packet(b'\x06\x1d\x07\x10\x08\x03not\x08\timportant\x14\x03\x18\x01\x00\x15\x04test') - await aio.sleep(0.4) - - async def app_main(self): - future1 = self.app.express('/not', app.pass_all, nonce=None, lifetime=250, can_be_prefix=False) - future2 = self.app.express('/not', app.pass_all, nonce=None, lifetime=250, can_be_prefix=True) - future3 = self.app.express('/not/important', app.pass_all, nonce=None, lifetime=250, can_be_prefix=False) - name2, content2, _ = await future3 - name1, content1, _ = await future2 - with pytest.raises(types.InterestTimeout): - await future1 - assert name1 == enc.Name.from_str('/not/important') - assert content1 == b'test' - assert name2 == enc.Name.from_str('/not/important') - assert content2 == b'test' - - -class TestRoute(NDNAppTestSuite): - async def face_proc(self, face: DummyFace): - await face.ignore_output(0) - await face.input_packet(b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\xfa') - await face.consume_output(b'\x06\x24\x07\x10\x08\x03not\x08\timportant\x14\x03\x18\x01\x00\x15\x04test' - b'\x16\x03\x1b\x01\xc8\x17\x00') - - async def app_main(self): - @self.app.route('/not') - def on_interest(name, _app_param, reply: app.ReplyFunc, _context): - data = self.app.make_data(name, b'test', signer=sec.NullSigner()) - assert reply(data) - - -class TestInvalidInterest(NDNAppTestSuite): - @staticmethod - async def validator(_name, _sig, _context) -> types.ValidResult: - await aio.sleep(0.003) - return types.ValidResult.FAIL - - async def face_proc(self, face: DummyFace): - await face.ignore_output(0) - await face.input_packet(b'\x05`\x072\x08\x03not\x08\timportant' - b'\x02 E\x8a\xeaxI}[\xb1\xcd\xf0\x01\xbe' - b'\xdb\xe9\x03\x085\xb1g+K\xa8jK,\xd0\xad' - b')\x07\x83\x96\xbb\x0c\x01\xfa$\x00,\x03' - b'\x1b\x01\x00. !\x93!zG[%\xcfs\xe89\\\x8f' - b'^\xd3\xa4\xb9\x13\xaa\x7f\xa6?\xd7\x13aVyS\xdc\x1dW\xea') - await aio.sleep(0.005) - - async def app_main(self): - @self.app.route('/not', validator=self.validator) - def on_interest(_name, _app_param, _reply: app.ReplyFunc, _context): - raise ValueError('This test fails') - - -class TestRoute2(NDNAppTestSuite): - async def face_proc(self, face: DummyFace): - await face.ignore_output(0) - await face.input_packet(b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\xfa') - await face.consume_output(b'\x06\x24\x07\x10\x08\x03not\x08\timportant\x14\x03\x18\x01\x00\x15\x04test' - b'\x16\x03\x1b\x01\xc8\x17\x00') - - async def app_main(self): - @self.app.route('/not') - def on_interest(name, _app_param, reply: app.ReplyFunc, context): - assert context['raw_packet'] == b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\xfa' - assert not context['sig_ptrs'].signature_info - reply(self.app.make_data(name, b'test', signer=sec.NullSigner())) - - -class TestConsumerRawPacket(NDNAppTestSuite): - async def face_proc(self, face: DummyFace): - await face.consume_output(b'\x050\x07(\x08\x07example\x08\x07testApp\x08\nrandomData' - b'\x38\x08\x00\x00\x01m\xa4\xf3\xffm\x12\x00\x0c\x02\x17p') - await face.input_packet(b'\x06B\x07(\x08\x07example\x08\x07testApp\x08\nrandomData' - b'\x38\x08\x00\x00\x01m\xa4\xf3\xffm\x14\x07\x18\x01\x00\x19\x02\x03\xe8' - b'\x15\rHello, world!') - - async def app_main(self): - name = '/example/testApp/randomData/t=1570430517101' - _, _, pkt_context = await self.app.express( - name, validator=app.pass_all, - must_be_fresh=True, can_be_prefix=False, lifetime=6000, nonce=None, need_raw_packet=True) - raw = pkt_context['raw_packet'] - assert (raw == b'\x06\x42\x07(\x08\x07example\x08\x07testApp\x08\nrandomData' - b'\x38\x08\x00\x00\x01m\xa4\xf3\xffm\x14\x07\x18\x01\x00\x19\x02\x03\xe8' - b'\x15\rHello, world!') - - -class TestCongestionMark(NDNAppTestSuite): - async def face_proc(self, face: DummyFace): - await face.ignore_output(0) - await face.input_packet(b'\x64\x1e\xfd\x03\x40\x01\x01\x50\x17' - b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\xfa') - await face.consume_output(b'\x06\x24\x07\x10\x08\x03not\x08\timportant\x14\x03\x18\x01\x00\x15\x04test' - b'\x16\x03\x1b\x01\xc8\x17\x00', timeout=0.5) - - async def app_main(self): - @self.app.route('/not') - def on_interest(name, _app_param, reply: app.ReplyFunc, context): - assert context['raw_packet'] == b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\xfa' - assert not context['sig_ptrs'].signature_info - reply(self.app.make_data(name, b'test', signer=sec.NullSigner())) - - -class TestImplicitSha256(NDNAppTestSuite): - async def face_proc(self, face: DummyFace): - await face.consume_output(b'\x05\x2d\x07\x28\x08\x04test\x01\x20' - b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' - b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' - b'\x0c\x01\xfa' - b'\x05\x2d\x07\x28\x08\x04test\x01\x20' - b'\x54\x88\xf2\xc1\x1b\x56\x6d\x49\xe9\x90\x4f\xb5\x2a\xa6\xf6\xf9' - b'\xe6\x6a\x95\x41\x68\x10\x9c\xe1\x56\xee\xa2\xc9\x2c\x57\xe4\xc2' - b'\x0c\x01\xfa') - await face.input_packet(b'\x06\x13\x07\x06\x08\x04test\x14\x03\x18\x01\x00\x15\x04test') - await aio.sleep(0.4) - - async def app_main(self): - fut1 = self.app.express( - '/test/sha256digest=FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', - validator=app.pass_all, nonce=None, lifetime=250) - fut2 = self.app.express( - '/test/sha256digest=5488f2c11b566d49e9904fb52aa6f6f9e66a954168109ce156eea2c92c57e4c2', - validator=app.pass_all, nonce=None, lifetime=250) - name2, content2, _ = await fut2 - with pytest.raises(types.InterestTimeout): - await fut1 - assert name2 == enc.Name.from_str('/test') - assert content2 == b'test' - - -class TestPitToken(NDNAppTestSuite): - async def face_proc(self, face: DummyFace): - await face.ignore_output(0) - await face.input_packet(b'\x64\x1f\x62\x04\x01\x02\x03\x04\x50\x17' - b'\x05\x15\x07\x10\x08\x03not\x08\timportant\x0c\x01\xfa') - await face.consume_output(b'\x64\x2e\x62\x04\x01\x02\x03\x04\x50\x26' - b'\x06\x24\x07\x10\x08\x03not\x08\timportant\x14\x03\x18\x01\x00\x15\x04test' - b'\x16\x03\x1b\x01\xc8\x17\x00') - - async def app_main(self): - @self.app.route('/not') - def on_interest(name, _app_param, reply: app.ReplyFunc, _context): - data = self.app.make_data(name, b'test', signer=sec.NullSigner()) - assert reply(data) From 91449992bc28580dc810dd6d784ce3b9c681ed4c Mon Sep 17 00:00:00 2001 From: Xinyu Ma Date: Wed, 7 Oct 2026 23:34:18 -0700 Subject: [PATCH 6/8] encoding: remove legacy TLV descriptor API Co-authored-by: Cursor --- src/ndn/app_support/security_v2.py | 2 +- src/ndn/encoding/tlv_model.py | 1184 +--------------------------- tests/encoding/tlv_model_test.py | 433 ++-------- 3 files changed, 74 insertions(+), 1545 deletions(-) diff --git a/src/ndn/app_support/security_v2.py b/src/ndn/app_support/security_v2.py index ccd5e6d..1b1f994 100644 --- a/src/ndn/app_support/security_v2.py +++ b/src/ndn/app_support/security_v2.py @@ -111,7 +111,7 @@ class CertificateV2Value(DataPacketValue): class SafeBag: certificate_v2: Optional[bytes] = dc.field( default=None, metadata={'tlv_type': TypeNumber.DATA}) - # We do not use ModelField due to 2 reasons: + # Keep the certificate as encoded bytes instead of a nested dataclass: # 1. The encoded length of CertificateV2 is unknown. # 2. Generally we already have an encoded certificate when exporting a # SafeBag. diff --git a/src/ndn/encoding/tlv_model.py b/src/ndn/encoding/tlv_model.py index b7266c5..86e2ff7 100644 --- a/src/ndn/encoding/tlv_model.py +++ b/src/ndn/encoding/tlv_model.py @@ -15,1157 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # ----------------------------------------------------------------------------- -import abc -import struct -from enum import Enum, Flag -from collections.abc import Iterable -from functools import reduce -from .tlv_type import BinaryStr, VarBinaryStr, is_binary_str -from .tlv_var import write_tl_num, parse_tl_num, get_tl_num_size -from .name import Name, Component - - -__all__ = ['DecodeError', 'TlvModel', 'ProcedureArgument', 'OffsetMarker', 'UintField', 'BoolField', - 'NameField', 'BytesField', 'ModelField', 'RepeatedField', 'IncludeBase', 'IncludeBaseError', - 'MapField'] - - -class DecodeError(Exception): - """ - Raised when there is a critical field (Type is odd) that is unrecognized, redundant or out-of-order. - """ - pass - - -class IncludeBaseError(Exception): - """ - Raised when IncludeBase is used to include a non-base class. - """ - pass - - -class IncludeBase: - """ - Include all fields from a base class. - """ - def __init__(self, base): - self.base = base - - -class TlvModelMeta(abc.ABCMeta): - """ - Metaclass for TlvModel, used to collect fields. - """ - def __new__(mcs, name, bases, attrs): - cls = super().__new__(mcs, name, bases, attrs) - - # Collect encoded fields - cls._encoded_fields = [] - index_dict = {} - for field_name in cls.__dict__: - if not field_name.startswith('__'): - field_obj = getattr(cls, field_name) - if isinstance(field_obj, Field): - field_obj.name = field_name - if field_name not in index_dict: - cls._encoded_fields.append(field_obj) - index_dict[field_name] = len(cls._encoded_fields) - 1 - else: - cls._encoded_fields[index_dict[field_name]] = field_obj - elif isinstance(field_obj, IncludeBase): - if field_obj.base not in bases: - raise IncludeBaseError(f"{field_obj.base} is not one of {name}'s base classes") - if not issubclass(field_obj.base, TlvModel): - raise IncludeBaseError(f"{field_obj.base} is not a TlvModel") - for field in field_obj.base._encoded_fields: - if field.name not in index_dict: - cls._encoded_fields.append(field) - index_dict[field.name] = len(cls._encoded_fields) - 1 - else: - cls._encoded_fields[index_dict[field.name]] = field - - return cls - - -class Field(metaclass=abc.ABCMeta): - """ - Field of :class:`TlvModel`. - A field with value ``None`` will be omitted in encoding TLV. - There is no required field in a :class:`TlvModel`, i.e. any Field can be ``None``. - - :ivar name: The name of the field - :vartype name: str - - :ivar type_num: The Type number used in TLV encoding - :vartype type_num: int - - :ivar default: The default value used for parsing and encoding. - - - If this field is absent during parsing, ``default`` is used to fill in this field. - - If this field is not explicitly assigned to None before encoding, - ``default`` is used. - """ - def __init__(self, type_num: int, default=None): - """ - Initialize a TLV field. - - :param type_num: Type number. - :param default: default value used for parsing and encoding. - """ - self.name = None - self.type_num = type_num - self.default = default - - def __get__(self, instance, owner): - """ - Get the value of this field in a specific instance. - Simply call :meth:`get_value` if ``instance`` is not ``None``. - - :param instance: the instance that this field is being accessed through. - :param owner: the owner class of this field. - :return: the value of this field. - """ - if instance is None: - return self - return self.get_value(instance) - - def __set__(self, instance, value): - """ - Set the value of this field. - - :param instance: the instance whose field is being set. - :param value: the new value. - """ - instance.__dict__[self.name] = value - - def get_value(self, instance): - """ - Get the value of this field in a specific instance. - Most fields use ``instance.__dict__`` to access the value. - - :param instance: the instance that this field is being accessed through. - :return: the value of this field. - """ - return instance.__dict__.get(self.name, self.default) - - @abc.abstractmethod - def encoded_length(self, val, markers: dict) -> int: - r""" - Preprocess value and get encoded length of this field. - The function may use ``markers[f'{self.name}##encoded_length']`` to store the length with TL. - Other marker variables starting with ``f'{self.name}##'`` may also be used. - Generally, marker variables are only used to store temporary values and avoid duplicated calculation. - One field should not access to another field's marker by its name. - - This function may also use other marker variables. However, in that case, - this field must be unique in a TlvModel. Usage of marker variables should follow - the name convention defined by specific TlvModel. - - :param val: value of this field - :param markers: encoding marker variables - :return: encoded length with TL. - It is expected as the exact length when encoding this field. - The only exception is ``SignatureValueField`` (invisible to application developer). - """ - pass - - @abc.abstractmethod - def encode_into(self, val, markers: dict, wire: VarBinaryStr, offset: int) -> int: - """ - Encode this field into wire. Must be called after :meth:`encoded_length`. - - :param val: value of this field - :param markers: encoding marker variables - :param wire: buffer to encode - :param offset: offset of this field in wire - :return: encoded length with TL. - It is expected to be the same as :meth:`encoded_length` returns. - """ - pass - - @abc.abstractmethod - def parse_from(self, instance, markers: dict, wire: BinaryStr, offset: int, length: int, offset_btl: int): - """ - Parse the value of this field from an encoded wire. - - :param instance: the instance to parse into. - :param markers: encoding marker variables. Only used in special cases. - :param wire: the TLV encoded wire. - :param offset: the offset of this field's Value in ``wire``. - :param length: the Length of this field's Value. - :param offset_btl: the offset of this field's TLV. - - .. code-block:: python3 - - assert offset == (offset_btl - + get_tl_num_size(self.type_num) - + get_tl_num_size(length)) - - :return: the value. - """ - pass - - def skipping_process(self, markers: dict, wire: BinaryStr, offset: int): - """ - Called when this field does not occur in ``wire`` and thus be skipped. - - :param markers: encoding marker variables. - :param wire: the TLV encoded wire. - :param offset: the offset where this field should have been if it occurred. - """ - pass - - -class ProcedureArgument(Field): - """ - A marker variable used during encoding or parsing. - It does not have a value. - Instead, it provides a way to access a specific variable in ``markers``. - """ - def __init__(self, default=None): - super().__init__(-1, default) - - def encoded_length(self, val, markers: dict) -> int: - return 0 - - def encode_into(self, val, markers: dict, wire: VarBinaryStr, offset: int) -> int: - return 0 - - def parse_from(self, instance, markers: dict, wire: BinaryStr, offset: int, length: int, offset_btl: int): - pass - - def __get__(self, instance, owner): - """ - :return: itself. - """ - return self - - def __set__(self, instance, value): - """ - This is not allowed and will raise a :class:`TypeError` if called. - """ - raise TypeError('ProcedureArgument can only be set via set_arg()') - - def get_arg(self, markers: dict): - """ - Get its value from ``markers`` - - :param markers: the markers dict. - :return: its value. - """ - return markers.get(f'{self.name}##args', self.default) - - def set_arg(self, markers: dict, val): - """ - Set its value in ``markers``. - - :param markers: the markers dict. - :param val: the new value. - """ - markers[f'{self.name}##args'] = val - - -class OffsetMarker(ProcedureArgument): - """ - A marker variable that records its position in TLV wire in terms of offset. - """ - def encode_into(self, val, markers: dict, wire: VarBinaryStr, offset: int) -> int: - self.set_arg(markers, offset) - return 0 - - def skipping_process(self, markers: dict, wire: BinaryStr, offset: int): - self.set_arg(markers, offset) - - -class UintField(Field): - """ - NonNegativeInteger field. - - Type: :class:`int` - - Its Length is 1, 2, 4 or 8 when present. - - :ivar fixed_len: the fixed value for Length if it's not ``None``. - Only 1, 2, 4 and 8 are acceptable. - :vartype fixed_len: int - :ivar val_base_type: the base type of the value of the field. - Can be int (default), an Enum or a Flag type. - """ - def __init__(self, type_num: int, default=None, fixed_len: int = None, - val_base_type=int): - super().__init__(type_num, default) - if fixed_len not in {None, 1, 2, 4, 8}: - raise ValueError("Uint's length should be 1, 2, 4, 8 or None") - if not issubclass(val_base_type, (Flag, Enum, int)): - raise TypeError("Uint's base class should be int, an Enum, or a Flag") - self.fixed_len = fixed_len - self.val_base_type = val_base_type - - def __set__(self, instance, value): - """ - Set the value of this uint field. - Will try to convert ``value`` into ``int``. - - :param instance: the instance whose field is being set. - :param value: the new value. - """ - if not isinstance(value, int) and value is not None: - if isinstance(value, (Flag, Enum)): - value = value.value - else: - raise TypeError(f"Cannot convert {value} into a uint field.") - instance.__dict__[self.name] = value - - def __get__(self, instance, owner): - """ - Get the value of this uint field in a specific instance. - Convert the value into the given ``val_base_type``. - - :param instance: the instance that this field is being accessed through. - :param owner: the owner class of this field. - :return: the value of this field. - """ - if instance is None: - return self - value = self.get_value(instance) - if value is not None: - return self.val_base_type(value) - else: - return None - - def encoded_length(self, val, markers: dict) -> int: - if val is None: - return 0 - if not isinstance(val, int) or val < 0: - raise TypeError(f'{self.name}=f{val} is not a legal uint') - tl_size = get_tl_num_size(self.type_num) + 1 - if self.fixed_len is not None: - ret = self.fixed_len - else: - if val <= 0xFF: - ret = 1 - elif val <= 0xFFFF: - ret = 2 - elif val <= 0xFFFFFFFF: - ret = 4 - else: - ret = 8 - if val >= 0x100 ** ret: - raise ValueError(f'{val} cannot be encoded into {ret} bytes') - markers[f'{self.name}##encoded_length'] = ret - return ret + tl_size - - def encode_into(self, val, markers: dict, wire: VarBinaryStr, offset: int) -> int: - if val is None: - return 0 - tl_size = get_tl_num_size(self.type_num) + 1 - length = markers[f'{self.name}##encoded_length'] - offset += write_tl_num(self.type_num, wire, offset) - if length == 1: - struct.pack_into('!BB', wire, offset, 1, val) - elif length == 2: - struct.pack_into('!BH', wire, offset, 2, val) - elif length == 4: - struct.pack_into('!BI', wire, offset, 4, val) - else: - struct.pack_into('!BQ', wire, offset, 8, val) - return length + tl_size - - def parse_from(self, instance, markers: dict, wire: BinaryStr, offset: int, length: int, offset_btl: int): - if length == 1: - return struct.unpack_from('!B', wire, offset)[0] - elif length == 2: - return struct.unpack_from('!H', wire, offset)[0] - elif length == 4: - return struct.unpack_from('!I', wire, offset)[0] - elif length == 8: - return struct.unpack_from('!Q', wire, offset)[0] - else: - raise ValueError("Uint's length should be 1, 2, 4 or 8") - - -class BoolField(Field): - """ - Boolean field. - - Type: :class:`bool` - - Its Length is always 0. - When present, its Value is ``True``. - When absent, its Value is ``None``, which is equivalent to ``False``. - - .. note:: - The default value is always ``None``. - """ - def encoded_length(self, val, markers: dict) -> int: - tl_size = get_tl_num_size(self.type_num) + 1 - return tl_size if val else 0 - - def encode_into(self, val, markers: dict, wire: VarBinaryStr, offset: int) -> int: - if val: - tl_size = get_tl_num_size(self.type_num) + 1 - offset += write_tl_num(self.type_num, wire, offset) - wire[offset] = 0 - return tl_size - else: - return 0 - - def parse_from(self, instance, markers: dict, wire: BinaryStr, offset: int, length: int, offset_btl: int): - return True - - -class SignatureValueField(Field): - def __init__(self, - type_num: int, - signer: ProcedureArgument, - covered_part: ProcedureArgument, - starting_point: OffsetMarker, - value_buffer: ProcedureArgument, - shrink_len: ProcedureArgument): - super().__init__(type_num) - self.signer = signer - self.covered_part = covered_part - self.starting_point = starting_point - self.value_buffer = value_buffer - self.shrink_len = shrink_len - - def encoded_length(self, val, markers: dict) -> int: - signer = self.signer.get_arg(markers) - if signer is None: - return 0 - else: - sig_value_len = signer.get_signature_value_size() - length = 1 + get_tl_num_size(sig_value_len) + sig_value_len - markers[f'{self.name}##encoded_length'] = sig_value_len - return length - - def encode_into(self, val, markers: dict, wire: VarBinaryStr, offset: int) -> int: - signer = self.signer.get_arg(markers) - if signer is None: - return 0 - else: - sig_cover_start = self.starting_point.get_arg(markers) - if sig_cover_start is not None: - sig_cover_part = self.covered_part.get_arg(markers) - sig_cover_part.append(wire[sig_cover_start:offset]) - - origin_offset = offset - sig_value_len = markers[f'{self.name}##encoded_length'] - offset += write_tl_num(self.type_num, wire, offset) - markers[f'{self.name}##wire_length'] = wire[offset:offset+1] - offset += write_tl_num(sig_value_len, wire, offset) - self.value_buffer.set_arg(markers, wire[offset:offset + sig_value_len]) - offset += sig_value_len - return offset - origin_offset - - def calculate_signature(self, markers: dict): - signer = self.signer.get_arg(markers) - if signer is not None: - sig_value_len = markers[f'{self.name}##encoded_length'] - real_len = signer.write_signature_value(self.value_buffer.get_arg(markers), - self.covered_part.get_arg(markers)) - self.shrink_len.set_arg(markers, sig_value_len - real_len) - if real_len != sig_value_len: - if sig_value_len >= 253: - raise ValueError(f'Long signatrue with flexible length is not supported: {sig_value_len} >= 253') - markers[f'{self.name}##wire_length'][0] = real_len - - def parse_from(self, instance, markers: dict, wire: BinaryStr, offset: int, length: int, offset_btl: int): - sig_buffer = memoryview(wire)[offset:offset+length] - self.value_buffer.set_arg(markers, sig_buffer) - - sig_cover_start = self.starting_point.get_arg(markers) - if sig_cover_start is not None: - sig_cover_part = self.covered_part.get_arg(markers) - sig_cover_part.append(wire[sig_cover_start:offset_btl]) - - return sig_buffer - - -class InterestNameField(Field): - def __init__(self, - need_digest: ProcedureArgument, - signature_covered_part: ProcedureArgument, - digest_buffer: ProcedureArgument, - default=None): - super().__init__(Name.TYPE_NAME, default) - self.need_digest = need_digest - self.sig_covered_part = signature_covered_part - self.digest_buffer = digest_buffer - - def encoded_length(self, val, markers: dict) -> int: - digest_pos = None - need_digest = self.need_digest.get_arg(markers) - name = val - if is_binary_str(name): - # Decode it if it's binary name - # This makes appending the digest component easier - name = Name.decode(name)[0] - elif isinstance(name, str): - name = Name.from_str(name) - elif isinstance(name, Iterable): - # clone to prevent the list being modified - name = list(name) - # From here on, name must be in List[Component, str] - if not isinstance(name, list): - raise TypeError('invalid type for name') - # Check every component - for i, comp in enumerate(name): - # If it's string, encode it first - if isinstance(comp, str): - name[i] = Component.from_str(Component.escape_str(comp)) - comp = name[i] - # And then check the type - if is_binary_str(comp): - typ = Component.get_type(comp) - if typ == Component.TYPE_INVALID: - raise TypeError('invalid type for name component') - elif typ == Component.TYPE_PARAMETERS_SHA256: - # Params Sha256 can occur at most once - if need_digest and digest_pos is None: - digest_pos = i - else: - raise ValueError('unnecessary ParametersSha256DigestComponent in name') - else: - raise TypeError('invalid type for name component') - markers[f'{self.name}##digest_pos'] = digest_pos - markers[f'{self.name}##preprocessed_name'] = name - - length = reduce(lambda x, y: x + len(y), name, 0) - if need_digest and digest_pos is None: - length += 34 - markers[f'{self.name}##encoded_length'] = length - return 1 + get_tl_num_size(length) + length - - def encode_into(self, val, markers: dict, wire: VarBinaryStr, offset: int) -> int: - origin_offset = offset - name_len = markers[f'{self.name}##encoded_length'] - name = markers[f'{self.name}##preprocessed_name'] - digest_pos = markers[f'{self.name}##digest_pos'] - need_digest = self.need_digest.get_arg(markers) - sig_cover_part = self.sig_covered_part.get_arg(markers) - digest_buf = None - - offset += write_tl_num(self.type_num, wire, offset) - offset += write_tl_num(name_len, wire, offset) - cover_start = offset # Signature covers the name - for i, comp in enumerate(name): - wire[offset:offset + len(comp)] = comp - if i == digest_pos: - # except the Digest component - if offset > cover_start: - sig_cover_part.append(wire[cover_start:offset]) - digest_buf = wire[offset + 2:offset + 34] - cover_start = offset + 34 - offset += len(comp) - if offset > cover_start: - sig_cover_part.append(wire[cover_start:offset]) - if need_digest and digest_pos is None: - markers[f'{self.name}##preprocessed_name'].append(wire[offset:offset+34]) - # If digest component does not exist, append one - offset += write_tl_num(Component.TYPE_PARAMETERS_SHA256, wire, offset) - offset += write_tl_num(32, wire, offset) - digest_buf = wire[offset:offset + 32] - offset += 32 - - if need_digest: - self.digest_buffer.set_arg(markers, digest_buf) - return offset - origin_offset - - def get_final_name(self, markers): - return markers[f'{self.name}##preprocessed_name'] - - def parse_from(self, instance, markers: dict, wire: BinaryStr, offset: int, length: int, offset_btl: int): - name = Name.decode(wire, offset_btl)[0] - sig_cover_part = self.sig_covered_part.get_arg(markers) - for ele in name: - typ = Component.get_type(ele) - if typ == Component.TYPE_PARAMETERS_SHA256: - self.digest_buffer.set_arg(markers, Component.get_value(ele)) - else: - sig_cover_part.append(ele) - return name - - -class NameField(Field): - """ - NDN Name field. Its Type is always :any:`Name.TYPE_NAME`. - - Type: :any:`NonStrictName` - """ - def __init__(self, default=None, type_number=Name.TYPE_NAME): - super().__init__(type_number, default) - - def encoded_length(self, val, markers: dict) -> int: - if val is None: - return 0 - name = val - if isinstance(name, str): - name = Name.from_str(name) - elif not is_binary_str(name): - if isinstance(name, Iterable): - name = list(name) - for i, comp in enumerate(name): - if isinstance(comp, str): - name[i] = Component.from_str(Component.escape_str(comp)) - elif not is_binary_str(comp): - raise TypeError('invalid type for name component') - else: - raise TypeError('invalid type for name') - - if isinstance(name, list): - ret = Name.encoded_length(name) - else: - ret = len(name) - markers[f'{self.name}##preprocessed_name'] = name - markers[f'{self.name}##encoded_length_with_tl'] = ret - return ret - - def encode_into(self, val, markers: dict, wire: VarBinaryStr, offset: int) -> int: - if val is None: - return 0 - name = markers[f'{self.name}##preprocessed_name'] - name_len_with_tl = markers[f'{self.name}##encoded_length_with_tl'] - if isinstance(name, list): - Name.encode(name, wire, offset) - else: - wire[offset:offset + name_len_with_tl] = name - return name_len_with_tl - - def parse_from(self, instance, markers: dict, wire: BinaryStr, offset: int, length: int, offset_btl: int): - return Name.decode(wire, offset_btl)[0] - - -class BytesField(Field): - r""" - Field for ``*OCTET``. - - Type: :any:`BinaryStr` - - :ivar is_string: If the value is a UTF-8 string. False by default. - - .. note:: - Do not assign it with a :class:`str` if ``is_string`` is False. - """ - def __init__(self, type_num: int, default=None, is_string: bool = False): - super().__init__(type_num, default) - self.is_string = is_string - - def __set__(self, instance, value): - instance.__dict__[self.name] = value - - def __get__(self, instance, owner): - if instance is None: - return self - value = self.get_value(instance) - return value - - def encoded_length(self, val, markers: dict) -> int: - if val is None: - return 0 - tl_size = get_tl_num_size(self.type_num) + get_tl_num_size(len(val)) - return tl_size + len(val) - - def encode_into(self, val, markers: dict, wire: VarBinaryStr, offset: int) -> int: - if val is None: - return 0 - else: - if isinstance(val, str): - val = val.encode('utf-8') - origin_offset = offset - offset += write_tl_num(self.type_num, wire, offset) - offset += write_tl_num(len(val), wire, offset) - wire[offset:offset+len(val)] = val - offset += len(val) - return offset - origin_offset - - def parse_from(self, instance, markers: dict, wire: BinaryStr, offset: int, length: int, offset_btl: int): - ret = memoryview(wire)[offset:offset+length] - if self.is_string: - return bytes(ret).decode('utf-8') - else: - return ret - - -class TlvModel(metaclass=TlvModelMeta): - r""" - Used to describe a TLV format. - - :ivar _encoded_fields: a list of :any:`Field` in order. - :vartype _encoded_fields: List[Field] - """ - _encoded_fields: list[Field] - - def __repr__(self): - values = ', '.join(f'{field.name}={field.__get__(self, None).__repr__()}' for field in self._encoded_fields) - return f'{self.__class__.__name__}({values})' - - def __eq__(self, other): - """ - Compare two TlvModels - - :param other: the other TlvModel to compare with. - :return: whether all Fields are equal. - """ - for field in self._encoded_fields: - if field.get_value(self) != field.get_value(other): - return False - return True - - def asdict(self, dict_factory=dict): - """ - Return a dict to represent this TlvModel. - - :param dict_factory: class of dict. - :return: the dict. - """ - result = [] - for field in self._encoded_fields: - if isinstance(field, ModelField): - result.append((field.name, field.__get__(self, None).asdict())) - elif isinstance(field, RepeatedField): - result.append((field.name, field.aslist(self))) - elif isinstance(field, MapField): - result.append((field.name, field.asdict(self))) - elif isinstance(field, BytesField): - val = field.__get__(self, None) - if isinstance(val, str): - result.append((field.name, val)) - else: - # memoryview, bytearray, bytes - result.append((field.name, bytes(val))) - else: - result.append((field.name, field.__get__(self, None))) - return dict_factory(result) - - def encoded_length(self, markers: dict | None = None) -> int: - """ - Get the encoded Length of this TlvModel. - - :param markers: encoding marker variables. - :return: the encoded Length. - """ - if markers is None: - markers = {} - ret = 0 - for field in self._encoded_fields: - ret += field.encoded_length(field.get_value(self), markers) - markers['##encoded_length'] = ret - return ret - - def encode(self, - wire: VarBinaryStr = None, - offset: int = 0, - markers: dict | None = None) -> VarBinaryStr: - r""" - Encode the TlvModel. - - :param wire: the buffer to contain the encoded wire. - A new :class:`bytearray` will be created if it's ``None``. - :param offset: the starting offset. - :param markers: encoding marker variables. - :return: wire. - - :raises ValueError: some field is assigned with improper value. - :raises TypeError: some field is assigned with value of wrong type. - :raises IndexError: wire does not have enough length. - :raises struct.error: a negative number is assigned to any non-negative integer field. - """ - if markers is None: - markers = {} - if '##encoded_length' in markers: - length = markers['##encoded_length'] - else: - length = self.encoded_length(markers) - if wire is None: - wire = bytearray(length) - wire_view = memoryview(wire) - for field in self._encoded_fields: - offset += field.encode_into(field.get_value(self), markers, wire_view, offset) - return wire - - @classmethod - def parse(cls, wire: BinaryStr, markers: dict | None = None, ignore_critical: bool = False): - """ - Parse a TlvModel from TLV encoded wire. - - :param wire: the TLV encoded wire. - :param markers: encoding marker variables. - :param ignore_critical: whether to ignore unknown critical fields. - :return: parsed TlvModel. - - :raises DecodeError: a critical field is unrecognized, redundant or out-of-order. - :raises IndexError: the Length of a field exceeds the size of wire. - """ - if markers is None: - markers = {} - offset = 0 - field_pos = 0 - ret = cls() - ret.__dict__ = {} # Clean default values created in __init__ - while offset < len(wire): - # Read TL - offset_btl = offset - typ, size_typ = parse_tl_num(wire, offset) - offset += size_typ - length, size_len = parse_tl_num(wire, offset) - offset += size_len - # Search for field - i = field_pos - while i < len(ret._encoded_fields): - if ret._encoded_fields[i].type_num == typ: - break - i += 1 - if i < len(ret._encoded_fields): - # If found - # First process skipped fields - for j in range(field_pos, i): - ret._encoded_fields[j].skipping_process(markers, wire, offset_btl) - # Parse that field - cur_field = ret._encoded_fields[i] - val = cur_field.parse_from(ret, markers, wire, offset, length, offset_btl) - cur_field.__set__(ret, val) - # Set next field - if isinstance(cur_field, RepeatedField): - field_pos = i - elif isinstance(cur_field, MapField): - # Parse the value part for a map - field_pos = i - offset += length - - offset_btl = offset - typ, size_typ = parse_tl_num(wire, offset) - offset += size_typ - length, size_len = parse_tl_num(wire, offset) - offset += size_len - - val = cur_field.parse_value(ret, markers, wire, offset, length, offset_btl) - cur_field.__set__(ret, val) - else: - field_pos = i + 1 - else: - # If not found - if (typ & 1) == 1 and not ignore_critical: - raise DecodeError(f'a critical field of type {typ} is unrecognized, redundant or out-of-order') - offset += length - return ret - - -class ModelField(Field): - r""" - Field for nested TlvModel. - - Type: :any:`TlvModel` - - :ivar model_type: the type of its value. - :vartype model_type: :any:`TlvModelMeta` - - :ivar ignore_critical: whether to ignore critical fields (whose Types are odd). - :vartype ignore_critical: :class:`bool` - """ - def __init__(self, - type_num: int, - model_type: type[TlvModel], - copy_in_fields: list[ProcedureArgument] = None, - copy_out_fields: list[ProcedureArgument] = None, - ignore_critical: bool = False): - # default should be None here to prevent unintended modification - super().__init__(type_num, None) - self.model_type = model_type - self.copy_in_fields = copy_in_fields if copy_in_fields else {} - self.copy_out_fields = copy_out_fields if copy_out_fields else {} - self.ignore_critical = ignore_critical - - def encoded_length(self, val, markers: dict) -> int: - if val is None: - return 0 - if not isinstance(val, self.model_type): - raise TypeError(f'{self.name}=f{val} is of type {self.model_type}') - copy_fields = {f.name for f in self.copy_in_fields} - inner_markers = {k: v - for k, v in markers.items() - if k.split('##')[0] in copy_fields} - length = val.encoded_length(inner_markers) - markers[f'{self.name}##inner_markers'] = inner_markers - markers[f'{self.name}##encoded_length'] = length - return get_tl_num_size(self.type_num) + get_tl_num_size(length) + length - - def encode_into(self, val, markers: dict, wire: VarBinaryStr, offset: int) -> int: - if val is None: - return 0 - else: - inner_markers = markers[f'{self.name}##inner_markers'] - length = markers[f'{self.name}##encoded_length'] - - origin_offset = offset - offset += write_tl_num(self.type_num, wire, offset) - offset += write_tl_num(length, wire, offset) - val.encode(wire, offset, inner_markers) - offset += length - return offset - origin_offset - - def parse_from(self, instance, markers: dict, wire: BinaryStr, offset: int, length: int, offset_btl: int): - inner_markers = {} - val = self.model_type.parse(memoryview(wire)[offset:offset+length], inner_markers, self.ignore_critical) - copy_fields = {f.name for f in self.copy_out_fields} - for k, v in inner_markers.items(): - if k.split('##')[0] in copy_fields: - markers[k] = v - return val - - -class RepeatedField(Field): - r""" - Field for an array of a specific type. - All elements will be directly encoded into TLV wire in order, sharing the same Type. - The ``type_num`` of ``element_type`` is used. - - Type: :class:`list` - - :vartype element_type: :any:`Field` - :ivar element_type: the type of elements in the list. - - .. warning:: - - Please always create a new :any:`Field` instance. - Don't use an existing one. - """ - def __init__(self, element_type: Field): - # default should be None here to prevent unintended modification - super().__init__(element_type.type_num, None) - self.element_type = element_type - - def get_value(self, instance): - if self.name not in instance.__dict__: - instance.__dict__[self.name] = [] - return instance.__dict__[self.name] - - def encoded_length(self, val, markers: dict) -> int: - if not val: - return 0 - - ret = 0 - # Different from ModelField, here changing the name is allowed - # Because self.element_type is always a new field instance - # ModelField share a ModelClass with others, and also - # subfields under a model do not use its name prefix so - # there may be conflicts - for i, ele in enumerate(val): - self.element_type.name = f'{self.name}[{i}]' - ret += self.element_type.encoded_length(ele, markers) - - return ret # TL is not included here - - def encode_into(self, val, markers: dict, wire: VarBinaryStr, offset: int) -> int: - if val is None: - return 0 - else: - origin_offset = offset - for i, ele in enumerate(val): - self.element_type.name = f'{self.name}[{i}]' - offset += self.element_type.encode_into(ele, markers, wire, offset) - return offset - origin_offset - - def parse_from(self, instance, markers: dict, wire: BinaryStr, offset: int, length: int, offset_btl: int): - lst = self.get_value(instance) - self.element_type.name = f'{self.name}[{len(lst)}]' - new_ele = self.element_type.parse_from(instance, markers, wire, offset, length, offset_btl) - lst.append(new_ele) - return lst - - def aslist(self, instance): - ret = [] - for x in self.__get__(instance, None): - if isinstance(x, TlvModel): - ret.append(x.asdict()) - elif isinstance(x, memoryview): - ret.append(bytes(x)) - else: - ret.append(x) - return ret - - -class MapField(Field): - r""" - Field for an unordered string or int map of a specific type. - All elements will be directly encoded into TLV wire in order, sharing the same Type. - The ``type_num`` of ``element_type`` is used. - - Type: :class:`list` - - :vartype value_type: :any:`Field` - :ivar value_type: the type of values in the dict. - - .. warning:: - - Please always create a new :any:`Field` instance. - Don't use an existing one. - """ - - def __init__(self, key_type: Field, value_type: Field): - # default should be None here to prevent unintended modification - if not isinstance(key_type, BytesField) and not isinstance(key_type, UintField): - raise TypeError('MapField only supports string and uint to be keys') - super().__init__(key_type.type_num, None) - self.key_type = key_type - self.value_type = value_type - - def get_value(self, instance): - if self.name not in instance.__dict__: - instance.__dict__[self.name] = {} - return instance.__dict__[self.name] - - def encoded_length(self, val, markers: dict) -> int: - if not val: - return 0 - - ret = 0 - for i, (key, val) in enumerate(val.items()): - self.key_type.name = f'{self.name}[{i}#k]' - ret += self.key_type.encoded_length(key, markers) - self.value_type.name = f'{self.name}[{i}#v]' - ret += self.value_type.encoded_length(val, markers) - - return ret - - def encode_into(self, val, markers: dict, wire: VarBinaryStr, offset: int) -> int: - if val is None: - return 0 - else: - origin_offset = offset - for i, (key, val) in enumerate(val.items()): - self.key_type.name = f'{self.name}[{i}#k]' - offset += self.key_type.encode_into(key, markers, wire, offset) - self.value_type.name = f'{self.name}[{i}#v]' - offset += self.value_type.encode_into(val, markers, wire, offset) - return offset - origin_offset - - def parse_from(self, instance, markers: dict, wire: BinaryStr, offset: int, length: int, offset_btl: int): - # parse_from only parses keys and will not update the value - dct = self.get_value(instance) - self.key_type.name = f'{self.name}[{len(dct)}#k]' - new_key = self.key_type.parse_from(instance, markers, wire, offset, length, offset_btl) - markers[f'{self.name}#last_key'] = new_key - return dct - - def parse_value(self, instance, markers: dict, wire: BinaryStr, offset: int, length: int, offset_btl: int): - # parse_value parses the value associated with the key last parsed. - dct = self.get_value(instance) - last_key = markers.get(f'{self.name}#last_key') - self.value_type.name = f'{self.name}[{len(dct)}#v]' - val = self.value_type.parse_from(instance, markers, wire, offset, length, offset_btl) - dct[last_key] = val - return dct - - def asdict(self, instance): - ret = {} - for key, val in self.__get__(instance, None).items(): - if isinstance(val, TlvModel): - ret[key] = val.asdict() - elif isinstance(val, memoryview): - ret[key] = bytes(val) - else: - ret[key] = val - return ret - - -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -""" -Dataclass-based TLV encoding/decoding (v2 API). - -Usage:: - - from dataclasses import dataclass, field - from typing import List, Optional - from ndn.encoding import tlv_encode, tlv_parse, NDNName - - @dataclass - class Inner: - value: int = field(default=None, metadata={'tlv_type': 0x01}) - - @dataclass - class Outer: - name: NDNName = field(default=None, metadata={'tlv_type': 0x07}) - count: int = field(default=None, metadata={'tlv_type': 0x0a}) - payload: bytes = field(default=None, metadata={'tlv_type': 0x15}) - sub: Inner = field(default=None, metadata={'tlv_type': 0x16}) - tags: List[bytes] = field(default_factory=list, - metadata={'tlv_type': 0x17}) - - wire = tlv_encode(obj) - obj = tlv_parse(Outer, wire) - -Field-kind inference from Python annotation -------------------------------------------- -+--------------------------------------------+----------+------------------+ -| Annotation | Kind | Old equivalent | -+============================================+==========+==================+ -| int / Enum / Flag subclass | uint | UintField | -+--------------------------------------------+----------+------------------+ -| bool | bool | BoolField | -+--------------------------------------------+----------+------------------+ -| bytes / bytearray / memoryview | bytes | BytesField | -+--------------------------------------------+----------+------------------+ -| str | str | BytesField | -| | | (is_string=True) | -+--------------------------------------------+----------+------------------+ -| NDNName (sentinel) | name | NameField | -+--------------------------------------------+----------+------------------+ -| Any @dataclass type | model | ModelField | -+--------------------------------------------+----------+------------------+ -| List[T] | repeated | RepeatedField | -+--------------------------------------------+----------+------------------+ -| Dict[K, V] | map | MapField | -+--------------------------------------------+----------+------------------+ -| None + field_type='offset_marker' | (zero) | OffsetMarker | -+--------------------------------------------+----------+------------------+ -| bytes + field_type='sig_value' | (special)| SignatureValue | -+--------------------------------------------+----------+------------------+ -| NDNName + field_type='interest_name' | (special)| InterestNameField| -+--------------------------------------------+----------+------------------+ - -Supported metadata keys ------------------------ -``'tlv_type'`` int TLV type number (required except for offset_marker) -``'fixed_len'`` int Force uint value width: 1, 2, 4, or 8 bytes -``'ignore_critical' bool Suppress DecodeError for nested model parsing -``'field_type'`` str Explicit kind override when inference is insufficient - -For **map** fields (``Dict[K, V]``): -``'val_tlv_type'`` int TLV type for map values (required) - -For **sig_value** fields: -``'cover_start'`` str Name of the offset_marker field where sig coverage begins -``'digest_cover_start' str Same or different offset_marker; where digest coverage begins -``'digest_cover_end'`` str Offset_marker after sig_value; where digest coverage ends - -Signature machinery markers (set by caller before tlv_encode / tlv_parse): -``markers['##signer']`` Signer instance; absent means unsigned -``markers['##need_digest']`` True ⟹ insert/compute ParametersSha256DigestComponent - -Signature machinery markers (set by tlv_encode / tlv_parse internally): -``markers['##sig_covered_part']`` list[memoryview | bytes]: regions covered by sig -``markers['##sig_value_buf']`` writable memoryview into the placeholder bytes -``markers['##shrink_len']`` int: bytes trimmed from end after sig finalization -``markers['##digest_buf']`` writable memoryview into the digest component value -``markers[fname]`` int: recorded byte offset for each offset_marker field -""" +"""Dataclass-based TLV encoding and decoding.""" import dataclasses import struct import typing @@ -1179,7 +29,11 @@ class Outer: from .name import Name, Component -__all__ += [ +class DecodeError(Exception): + """Raised for malformed or unrecognized critical TLV fields.""" + + +__all__ = [ 'tlv_encode', 'tlv_parse', 'NDNName', 'DecodeError', 'tlv_get_arg', 'tlv_set_arg', ] @@ -1196,16 +50,14 @@ class NDNName: """ Sentinel annotation type that marks a field as an NDN Name. - Use it wherever you would have used :class:`~ndn.encoding.NameField` in - the old metaclass API:: + Use it for fields containing encoded NDN names:: name: NDNName = field(default=None, metadata={'tlv_type': 0x07}) # repeated Names: names: List[NDNName] = field(default_factory=list, metadata={'tlv_type': 0x07}) - The actual runtime value is :any:`FormalName` (a list of encoded - component bytes), exactly as returned by the old NameField. + The runtime value is :any:`FormalName`, a list of encoded components. """ @@ -1365,8 +217,7 @@ def _encoded_length_interest_name(fname: str, val, metadata: dict, """ Size pass for an Interest Name field. - Mirrors ``InterestNameField.encoded_length``. If ``markers['##need_digest']`` - is truthy and the name does not already contain a + If ``markers['##need_digest']`` is truthy and the name does not already contain a ``ParametersSha256DigestComponent``, 34 extra bytes are reserved for one. """ if val is None: @@ -1419,8 +270,8 @@ def _encode_into_interest_name(fname: str, val, metadata: dict, markers: dict, """ Write pass for an Interest Name field. - Mirrors ``InterestNameField.encode_into``. Appends non-digest name - components to ``markers['##sig_covered_part']`` (wire slices) and stores + Appends non-digest name components to ``markers['##sig_covered_part']`` + (wire slices) and stores the writable digest-value buffer in ``markers['##digest_buf']``. """ if val is None: @@ -1540,10 +391,8 @@ def _encoded_length_field(fname: str, val, spec: _FieldSpec, markers: dict) -> i """ Compute the encoded byte count of one TLV field (T + L + V). - Intermediate values are cached in *markers* under ``fname##...`` keys, - exactly mirroring the convention used by the v1 :class:`~ndn.encoding.Field` - subclasses. Returns 0 when the field is absent (*val* is ``None``/falsy - for bool). + Intermediate values are cached in *markers* under ``fname##...`` keys. + Returns 0 when the field is absent (*val* is ``None``/falsy for bool). """ kind = spec.kind # Zero-wire kinds: handled before looking up tlv_type. @@ -1565,7 +414,7 @@ def _encoded_length_field(fname: str, val, spec: _FieldSpec, markers: dict) -> i type_num = spec.tlv_type - # BoolField: present if truthy, absent otherwise + # Boolean TLVs are present if truthy and absent otherwise. if kind == 'bool': return (get_tl_num_size(type_num) + 1) if val else 0 @@ -1840,7 +689,7 @@ def _make_default_instance(cls): Uses ``object.__new__`` to bypass ``__init__``, then sets each field: - ``field(default=X)`` → X - ``field(default_factory=F)`` → F() - - no default → None (same behaviour as old TlvModel.parse) + - no default → None """ obj = object.__new__(cls) for f in dataclasses.fields(cls): @@ -1864,8 +713,7 @@ def _parse_value(fname: str, spec: _FieldSpec, :param wire: memoryview of the full wire buffer. :param offset: byte offset of V within *wire*. :param length: byte length of V. - :param offset_btl: byte offset of the TLV's T field within *wire* - (used by NameField to pass to ``Name.decode``). + :param offset_btl: byte offset of the TLV's T field within *wire*. :param ignore_critical: forwarded to nested ``tlv_parse`` calls. :return: the parsed Python value. """ diff --git a/tests/encoding/tlv_model_test.py b/tests/encoding/tlv_model_test.py index bf8625c..65db5a9 100644 --- a/tests/encoding/tlv_model_test.py +++ b/tests/encoding/tlv_model_test.py @@ -15,214 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # ----------------------------------------------------------------------------- -from enum import Enum, Flag -from ndn.encoding import TlvModel, NameField, UintField, BytesField, BoolField, Component, \ - RepeatedField, ModelField, Name, IncludeBase, MapField - - -class TestEncodeDecode: - def test_basic(self): - class Model(TlvModel): - name = NameField() - int_val = UintField(0x03) - str_val = BytesField(0x02) - bool_val = BoolField(0x01) - - model = Model() - model.name = ['test', Component.from_str('name')] - model.int_val = 0 - assert model.encode() == b'\x07\x0c\x08\x04test\x08\x04name\x03\x01\x00' - - model = Model.parse(b'\x07\x0c\x08\x04test\x08\x04name\x03\x01\x00') - assert model.name == Name.from_str('/test/name') - assert model.int_val == 0 - assert not model.bool_val - - model.name = 'test/name' - model.str_val = b'str' - model.bool_val = True - assert model.encode() == b'\x07\x0c\x08\x04test\x08\x04name\x03\x01\x00\x02\x03str\x01\x00' - - model = Model.parse(b'\x07\x0c\x08\x04test\x08\x04name\x03\x01\x00\x02\x03str\x01\x00') - assert model.str_val == b'str' - assert model.bool_val - - def test_repeat(self): - class WordArray(TlvModel): - words = RepeatedField(UintField(0x01, fixed_len=2)) - - array = WordArray() - array.words = [i for i in range(3)] - assert array.encode() == b'\x01\x02\x00\x00\x01\x02\x00\x01\x01\x02\x00\x02' - - array = WordArray.parse(b'\x01\x02\x00\x00\x01\x02\x00\x01\x01\x02\x00\x02') - assert array.words == [0, 1, 2] - - def test_map(self): - class ArgList(TlvModel): - params = MapField(BytesField(0x85, is_string=True), BytesField(0x87)) - - arg_list = ArgList() - arg_list.params = { - 'key1': b'val1', - 'key2': b'val2' - } - # The following line is OK because Python 3.9+ dicts are ordered - assert arg_list.encode() == b'\x85\x04key1\x87\x04val1\x85\x04key2\x87\x04val2' - arg_list = ArgList.parse(b'\x85\x04key1\x87\x04val1\x85\x04key2\x87\x04val2') - assert len(arg_list.params) == 2 - assert bytes(arg_list.params['key1']) == b'val1' - assert bytes(arg_list.params['key2']) == b'val2' - - def test_nested(self): - class Inner(TlvModel): - val = UintField(0x01) - - class Outer(TlvModel): - val = ModelField(0x02, Inner) - - obj = Outer() - obj.val = Inner() - obj.val.val = 255 - assert obj.encode() == b'\x02\x03\x01\x01\xFF' - - obj = Outer.parse(b'\x02\x03\x01\x01\xFF') - assert obj.val.val == 255 - - def test_derivation(self): - class Base(TlvModel): - m2 = UintField(0x02) - - class Derived(Base): - m1 = UintField(0x01) - _base = IncludeBase(Base) - m3 = UintField(0x03) - - obj = Derived() - obj.m1, obj.m2, obj.m3 = range(1, 4) - assert obj.encode() == b'\x01\x01\x01\x02\x01\x02\x03\x01\x03' - - obj = Derived.parse(b'\x01\x01\x01\x02\x01\x02\x03\x01\x03') - assert obj.m1 == 1 - assert obj.m2 == 2 - assert obj.m3 == 3 - - def test_override(self): - class A1(TlvModel): - m1 = UintField(0x01) - - class A2(A1): - _a1 = IncludeBase(A1) - m2 = UintField(0x02) - - class B1(TlvModel): - a = ModelField(0x03, A1) - - class B2(B1): - IncludeBase(B1) - a = ModelField(0x03, A2) - - obj = B2() - obj.a = A2() - obj.a.m1 = 1 - obj.a.m2 = 2 - assert obj.encode() == b'\x03\x06\x01\x01\x01\x02\x01\x02' - - obj = B2.parse(b'\x03\x06\x01\x01\x01\x02\x01\x02') - assert obj.a.m1 == 1 - assert obj.a.m2 == 2 - - def test_diamond(self): - class A(TlvModel): - m1 = UintField(0x01) - - class B1(A): - _base = IncludeBase(A) - m1 = UintField(0x02) - m4 = UintField(0x04) - - class B2(A): - _base = IncludeBase(A) - m1 = UintField(0x03) - m5 = UintField(0x05) - - class D(B1, B2): - _b2 = IncludeBase(B2) - _b1 = IncludeBase(B1) - - obj = D() - obj.m1, obj.m2, obj.m4, obj.m5 = 1, 2, 4, 5 - assert obj.encode() == b'\x02\x01\x01\x05\x01\x05\x04\x01\x04' - - obj = D.parse(b'\x02\x01\x01\x05\x01\x05\x04\x01\x04') - assert obj.m1 == 1 - assert obj.m4 == 4 - assert obj.m5 == 5 - - -class TestAsDict: - def test_asdict(self): - class EnumVal(Enum): - E1 = 1 - E2 = 2 - - class FlagVal(Flag): - F1 = 1 - F2 = 2 - - class WordArray(TlvModel): - words = RepeatedField(UintField(0x04, fixed_len=2)) - - class Model(TlvModel): - name = NameField() - int_val = UintField(0x03) - bytes_val = BytesField(0x02) - bool_val = BoolField(0x01) - array = ModelField(0x05, WordArray) - flag_val = UintField(0x06, val_base_type=FlagVal) - enum_arr = RepeatedField(UintField(0x07, val_base_type=EnumVal)) - str_val = BytesField(0x08, is_string=True) - str_arr = RepeatedField(BytesField(0x09, is_string=True)) - - obj = Model() - obj.name = '/test/name' - obj.int_val = 0 - obj.bytes_val = b'\x00' - obj.array = WordArray() - obj.array.words = [1, 2, 3] - obj.flag_val = FlagVal.F1 | FlagVal.F2 - obj.enum_arr = [EnumVal.E1, EnumVal.E2] - obj.str_val = 'वरुण' - obj.str_arr = ['あいう', 'utf-8'] - assert obj.asdict() == {'name': '/test/name', - 'int_val': 0, - 'bytes_val': b'\x00', - 'bool_val': None, - 'array': {'words': [1, 2, 3]}, - 'flag_val': FlagVal.F1 | FlagVal.F2, - 'enum_arr': [EnumVal.E1, EnumVal.E2], - 'str_val': 'वरुण', - 'str_arr': ['あいう', 'utf-8']} - - -# ----------------------------------------------------------------------------- -# Copyright (C) 2019-2020 The python-ndn authors -# -# This file is part of python-ndn. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ----------------------------------------------------------------------------- -"""Tests for the dataclass-based TLV v2 API (tlv_encode / tlv_parse).""" +"""Tests for the dataclass-based TLV API.""" from dataclasses import dataclass, field from enum import IntEnum, IntFlag from hashlib import sha256 @@ -231,9 +24,7 @@ class Model(TlvModel): from ndn.encoding import ( tlv_encode, tlv_parse, NDNName, DecodeError, tlv_get_arg, tlv_set_arg, - # v1 equivalents used for binary-compatibility checks - TlvModel, UintField, BoolField, BytesField, NameField, ModelField, - RepeatedField, Name, Signer, + Name, Signer, ) @@ -263,11 +54,11 @@ class _RepeatedModel: # --------------------------------------------------------------------------- -# TestUintField +# Unsigned integer tests # --------------------------------------------------------------------------- -class TestUintField: - """UintField: variable-width and fixed-width non-negative integers.""" +class TestUint: + """Variable-width and fixed-width non-negative integers.""" def test_min_width_1_byte(self): @dataclass @@ -352,7 +143,7 @@ class M: assert tlv_parse(M, wire).x == 42 -class TestUintFieldEnum: +class TestUintEnum: """IntEnum and IntFlag auto-conversion on parse.""" def test_intenum_roundtrip(self): @@ -401,11 +192,11 @@ class M: # --------------------------------------------------------------------------- -# TestBoolField +# Boolean tests # --------------------------------------------------------------------------- -class TestBoolField: - """BoolField: 0-length TLV present when truthy, absent otherwise.""" +class TestBool: + """Zero-length TLV present when truthy, absent otherwise.""" def test_present(self): @dataclass @@ -446,11 +237,11 @@ class M: # --------------------------------------------------------------------------- -# TestBytesField +# Byte-string tests # --------------------------------------------------------------------------- -class TestBytesField: - """BytesField: raw bytes and UTF-8 strings.""" +class TestBytes: + """Raw bytes and UTF-8 strings.""" def test_bytes_roundtrip(self): @dataclass @@ -526,10 +317,10 @@ class M: # --------------------------------------------------------------------------- -# TestNameField +# Name tests # --------------------------------------------------------------------------- -class TestNameField: +class TestName: """NDNName: NDN Name TLV via string, FormalName list, or binary.""" def test_from_string(self): @@ -586,11 +377,11 @@ class M: # --------------------------------------------------------------------------- -# TestModelField +# Nested model tests # --------------------------------------------------------------------------- -class TestModelField: - """ModelField: nested dataclass, recursively encoded.""" +class TestNestedModel: + """Nested dataclass, recursively encoded.""" def test_basic_nested(self): wire = tlv_encode(_Outer(inner=_Inner(val=255))) @@ -644,11 +435,11 @@ class Outer: # --------------------------------------------------------------------------- -# TestRepeatedField +# Repeated value tests # --------------------------------------------------------------------------- -class TestRepeatedField: - """RepeatedField: multiple TLVs of the same type, no outer wrapper.""" +class TestRepeated: + """Multiple TLVs of the same type, without an outer wrapper.""" def test_uint_elements(self): wire = tlv_encode(_RepeatedUint(words=[0, 1, 2])) @@ -784,7 +575,7 @@ class M: # --------------------------------------------------------------------------- class TestInheritance: - """Dataclass inheritance: parent fields come first (no IncludeBase needed).""" + """Dataclass inheritance places parent fields first.""" def test_parent_fields_encoded_first(self): @dataclass @@ -919,137 +710,48 @@ def test_default_factory_list_preserved_if_absent(self): # --------------------------------------------------------------------------- -# TestBinaryCompatibility +# Golden wire compatibility # --------------------------------------------------------------------------- -class TestBinaryCompatibility: - """Byte-for-byte compatibility with the v1 TlvModel metaclass API.""" - - def test_uint_compat(self): - class V1(TlvModel): - sig_type = UintField(0x1b, fixed_len=1) - nonce = UintField(0x26) - - @dataclass - class V2: - sig_type: int = field(default=None, - metadata={'tlv_type': 0x1b, 'fixed_len': 1}) - nonce: int = field(default=None, metadata={'tlv_type': 0x26}) - - v1 = V1(); v1.sig_type = 3; v1.nonce = 42 - assert bytes(v1.encode()) == bytes(tlv_encode(V2(sig_type=3, nonce=42))) - - def test_bool_compat(self): - class V1(TlvModel): - flag = BoolField(0x12) - count = UintField(0x0a) - - @dataclass - class V2: - flag: bool = field(default=None, metadata={'tlv_type': 0x12}) - count: int = field(default=None, metadata={'tlv_type': 0x0a}) - - for flag_val in (True, False, None): - v1 = V1(); v1.flag = flag_val; v1.count = 5 - v2 = V2(flag=flag_val, count=5) - assert bytes(v1.encode()) == bytes(tlv_encode(v2)) - - def test_bytes_compat(self): - class V1(TlvModel): - raw = BytesField(0x15) - label = BytesField(0x16, is_string=True) - - @dataclass - class V2: - raw: bytes = field(default=None, metadata={'tlv_type': 0x15}) - label: str = field(default=None, metadata={'tlv_type': 0x16}) - - v1 = V1(); v1.raw = b'\x01\x02\x03'; v1.label = 'hi' - v2 = V2(raw=b'\x01\x02\x03', label='hi') - assert bytes(v1.encode()) == bytes(tlv_encode(v2)) - - def test_name_compat(self): - class V1(TlvModel): - name = NameField() - +class TestGoldenWireCompatibility: + def test_scalar_wire_formats(self): @dataclass - class V2: - name: NDNName = field(default=None, metadata={'tlv_type': 0x07}) + class Scalars: + sig_type: int = field(default=None, metadata={'tlv_type': 0x1b, 'fixed_len': 1}) + nonce: int = field(default=None, metadata={'tlv_type': 0x26}) + flag: bool = field(default=None, metadata={'tlv_type': 0x28}) + raw: bytes = field(default=None, metadata={'tlv_type': 0x2a}) - v1 = V1(); v1.name = '/foo/bar' - v2 = V2(name='/foo/bar') - assert bytes(v1.encode()) == bytes(tlv_encode(v2)) - - def test_model_compat(self): - class V1Inner(TlvModel): - val = UintField(0x01) - - class V1Outer(TlvModel): - inner = ModelField(0x10, V1Inner) + wire = bytes(tlv_encode(Scalars(sig_type=3, nonce=42, flag=True, raw=b'hi'))) + assert wire == bytes.fromhex('1b010326012a28002a026869') + def test_name_and_nested_wire_formats(self): @dataclass - class V2Inner: + class Inner: val: int = field(default=None, metadata={'tlv_type': 0x01}) @dataclass - class V2Outer: - inner: V2Inner = field(default=None, metadata={'tlv_type': 0x10}) - - v1 = V1Outer(); v1.inner = V1Inner(); v1.inner.val = 99 - v2 = V2Outer(inner=V2Inner(val=99)) - assert bytes(v1.encode()) == bytes(tlv_encode(v2)) - - def test_repeated_uint_compat(self): - class V1(TlvModel): - words = RepeatedField(UintField(0x01, fixed_len=2)) - - v1 = V1(); v1.words = [0, 1, 2] - v2 = _RepeatedUint(words=[0, 1, 2]) - assert bytes(v1.encode()) == bytes(tlv_encode(v2)) - - def test_repeated_model_compat(self): - class V1Inner(TlvModel): - val = UintField(0x01) - - class V1Rep(TlvModel): - items = RepeatedField(ModelField(0x10, V1Inner)) - - v1 = V1Rep() - r1 = V1Inner(); r1.val = 10 - r2 = V1Inner(); r2.val = 20 - v1.items = [r1, r2] - - v2 = _RepeatedModel(items=[_Inner(val=10), _Inner(val=20)]) - assert bytes(v1.encode()) == bytes(tlv_encode(v2)) - - def test_parse_interop(self): - """Wire produced by v1 can be parsed by v2 and vice-versa.""" - class V1(TlvModel): - name = NameField() - count = UintField(0x0a) - - @dataclass - class V2: - name: NDNName = field(default=None, metadata={'tlv_type': 0x07}) - count: int = field(default=None, metadata={'tlv_type': 0x0a}) - - v1 = V1(); v1.name = '/test'; v1.count = 7 - wire_from_v1 = bytes(v1.encode()) + class Outer: + name: NDNName = field(default=None, metadata={'tlv_type': 0x07}) + inner: Inner = field(default=None, metadata={'tlv_type': 0x10}) - p = tlv_parse(V2, wire_from_v1) - assert Name.to_str(p.name) == '/test' - assert p.count == 7 + wire = bytes(tlv_encode(Outer(name='/foo/bar', inner=Inner(val=99)))) + assert wire == bytes.fromhex('070a0803666f6f08036261721003010163') + parsed = tlv_parse(Outer, wire) + assert Name.to_str(parsed.name) == '/foo/bar' + assert parsed.inner.val == 99 - v2 = V2(name='/test', count=7) - wire_from_v2 = bytes(tlv_encode(v2)) + def test_repeated_wire_formats(self): + uint_wire = bytes(tlv_encode(_RepeatedUint(words=[0, 1, 2]))) + assert uint_wire == bytes.fromhex('010200000102000101020002') - p2 = V1.parse(wire_from_v2) - assert Name.to_str(p2.name) == '/test' - assert p2.count == 7 + model_wire = bytes(tlv_encode( + _RepeatedModel(items=[_Inner(val=10), _Inner(val=20)]))) + assert model_wire == bytes.fromhex('100301010a1003010114') # --------------------------------------------------------------------------- -# MapField tests +# Map tests # --------------------------------------------------------------------------- @dataclass @@ -1109,35 +811,15 @@ def test_str_model_map_roundtrip(self): assert p.entries['x'].value == 7 assert p.entries['y'].value == 99 - def test_v1_compat_wire(self): - """v2 map encoding must be byte-for-byte identical to v1 MapField.""" - from ndn.encoding import MapField, BytesField - - class V1Map(TlvModel): - entries = MapField(BytesField(0x21, is_string=True), BytesField(0x23)) - - v1 = V1Map() - v1.entries['alpha'] = b'\x01\x02' - v1.entries['beta'] = b'\x03' - v1_wire = bytes(v1.encode()) - - v2 = _StrBytesMap(entries={'alpha': b'\x01\x02', 'beta': b'\x03'}) - v2_wire = bytes(tlv_encode(v2)) - - assert v1_wire == v2_wire - - def test_v1_produced_wire_parsed_by_v2(self): - from ndn.encoding import MapField, BytesField - - class V1Map(TlvModel): - entries = MapField(BytesField(0x21, is_string=True), BytesField(0x23)) - - v1 = V1Map() - v1.entries['hello'] = b'\xde\xad' - wire = bytes(v1.encode()) + def test_golden_wire(self): + obj = _StrBytesMap(entries={'alpha': b'\x01\x02', 'beta': b'\x03'}) + wire = bytes(tlv_encode(obj)) + assert wire == bytes.fromhex('2105616c70686123020102210462657461230103') - p = tlv_parse(_StrBytesMap, wire) - assert bytes(p.entries['hello']) == b'\xde\xad' + def test_golden_wire_is_parsed(self): + wire = bytes.fromhex('210568656c6c6f2302dead') + parsed = tlv_parse(_StrBytesMap, wire) + assert bytes(parsed.entries['hello']) == b'\xde\xad' def test_bytes_values_are_memoryview_zero_copy(self): obj = _StrBytesMap(entries={'k': b'\xca\xfe'}) @@ -1316,10 +998,9 @@ def test_data_signature_verifies(self): enc_markers = {'##signer': signer} wire = tlv_encode(obj, markers=enc_markers) - enc_covered = enc_markers['##sig_covered_part'] parse_markers = {} - p = tlv_parse(_DataValue, wire, markers=parse_markers) + tlv_parse(_DataValue, wire, markers=parse_markers) parse_covered = parse_markers.get('##sig_covered_part', []) sig_buf = parse_markers['##sig_value_buf'] From 12e89f2adc9cc423c6f95878287220cbe0bf40ae Mon Sep 17 00:00:00 2001 From: Xinyu Ma Date: Wed, 7 Oct 2026 23:36:07 -0700 Subject: [PATCH 7/8] docs: document breaking dataclass and app APIs Co-authored-by: Cursor --- CHANGELOG.rst | 6 + docs/_static/schema-example1-policy.svg | 1 - docs/_static/schema-example1-schema.svg | 1 - docs/index.rst | 2 - docs/src/app.rst | 93 +++--------- docs/src/appv2.rst | 26 ---- docs/src/encoding/encoding.rst | 58 ++------ docs/src/encoding/tlv_model.rst | 48 ++----- docs/src/examples/basic_app.rst | 78 +++++----- docs/src/examples/tlv_model.rst | 182 ++++++++---------------- docs/src/lvs/package.rst | 2 +- docs/src/schema/custom_node.rst | 5 - docs/src/schema/ex1.rst | 88 ------------ docs/src/schema/policies.rst | 20 --- docs/src/schema/schema.rst | 48 ------- docs/src/schema/schema_tree.rst | 5 - docs/src/schema/utils.rst | 5 - docs/src/security/security.rst | 22 +-- 18 files changed, 153 insertions(+), 537 deletions(-) delete mode 100644 docs/_static/schema-example1-policy.svg delete mode 100644 docs/_static/schema-example1-schema.svg delete mode 100644 docs/src/appv2.rst delete mode 100644 docs/src/schema/custom_node.rst delete mode 100644 docs/src/schema/ex1.rst delete mode 100644 docs/src/schema/policies.rst delete mode 100644 docs/src/schema/schema.rst delete mode 100644 docs/src/schema/schema_tree.rst delete mode 100644 docs/src/schema/utils.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fb05136..2edbc8f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,12 @@ Changelog ========= +Next (breaking) ++++++++++++++++ +* Replace the descriptor-based TLV model with dataclass models and ``tlv_encode``/``tlv_parse``. +* Make the PIT-token-aware application API canonical at ``ndn.app`` and remove ``ndn.appv2``. +* Remove the legacy application API, Name Tree Schema, dispatcher, segment fetcher, and cascade validator. + 0.4-1 (2023-08-21) ++++++++++++++++++ * Update dependencies: drop cryptography. diff --git a/docs/_static/schema-example1-policy.svg b/docs/_static/schema-example1-policy.svg deleted file mode 100644 index 803a00d..0000000 --- a/docs/_static/schema-example1-policy.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/_static/schema-example1-schema.svg b/docs/_static/schema-example1-schema.svg deleted file mode 100644 index f1029d7..0000000 --- a/docs/_static/schema-example1-schema.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index f1d3dea..787b8f5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,10 +15,8 @@ Table Of Contents src/readme src/installation src/app - src/appv2 src/encoding/encoding src/security/security - src/schema/schema src/lvs/lvs src/misc src/examples/examples diff --git a/docs/src/app.rst b/docs/src/app.rst index b7e7aab..c0d8240 100644 --- a/docs/src/app.rst +++ b/docs/src/app.rst @@ -4,82 +4,23 @@ Introduction ------------ -The :mod:`ndn.app` package contains the class :any:`NDNApp` , -which connects an NDN application and an NFD node. - -:any:`NDNApp` provides the functionalities similar to application Face in ndn-cxx, which include: - -+ Establish a connection to an NFD node. -+ Express Interests and handle the Data coming back. -+ Register and unregister a route with an Interest handling function. - -This package does not support PIT token. -To use PIT token, consider using :mod:`ndn.appv2` package. - -.. _label-keyword-arguments: - -Keyword Arguments ------------------ - -Some functions which create a Interest or Data packet accept a ``kwargs``, -which can be used to support diversity in arguments provided to create a packet. - -MetaInfo -~~~~~~~~ - -These arguments are used to fill in the MetaInfo field of a Data packet. - -+ **meta_info** (:any:`MetaInfo`) - the MetaInfo field of Data. - All other related parameters will be ignored. -+ **content_type** (*int*) - :any:`ContentType`. ``ContentType.BLOB`` by default. -+ **freshness_period** (*int*) - FreshnessPeriod in milliseconds. ``None`` by default. -+ **final_block_id** (:any:`BinaryStr`) - FinalBlockId. It should be an encoded :any:`Component`. - ``None`` by default. - -InterestParameters -~~~~~~~~~~~~~~~~~~ - -These arguments are used to fill in fields of an Interest packet. - -+ **interest_param** (:any:`InterestParam`) - a dataclass containing all parameters. - All other related parameters will be ignored. -+ **can_be_prefix** (*bool*) - CanBePrefix. ``False`` by default. -+ **must_be_fresh** (*bool*) - MustBeFresh. ``False`` by default. -+ **nonce** (*int*) - Nonce. A random number will be generated by default. - To omit Nonce, please explicitly pass ``None`` to this argument. -+ **lifetime** (*int*) - InterestLifetime in milliseconds. ``4000`` by default. - - .. warning:: - On Windows, a too small number may cause a memory failure of the NameTrie. Currently, ``>=10`` is safe. -+ **hop_limit** (*int*) - HopLimit. ``None`` by default. -+ **forwarding_hint** (*list[NonStrictName]*) - see :any:`InterestParam`. - -Signature -~~~~~~~~~ - -These arguments are used to decide how the Interest or Data packet is signed and by which Signer. -Supported arguments are different with each Keychain. -Only those supported by the default Keychain are listed here. -If there is a conflict, the earlier an argument is listed the higher priority it has. - -.. note:: - Only Interests with ApplicationParameters are signed. - ``b''`` can be used if that field is not needed by the application. - -+ **signer** (*Signer*) - the Signer used to sign this packet. - All other related parameters will be ignored. The Keychain will not be used. -+ **no_signature** (*bool*) - not signed. Not recommended. -+ **digest_sha256** (*bool*) - using SHA-256 digest to protect integrity only. ``False`` by default. -+ **cert** (:any:`NonStrictName`) - using the speficied Certificate to sign this packet. - The Key name will be derived from the certificate name. -+ **key** - using the specified Key to sign this packet. - Either a Key object or the :any:`NonStrictName` of a Key is acceptable. - KeyLocator will be set to the default Certificate name of this Key unless specified. -+ **identity** - using the default Key of the specified Identity to sign this packet. - Either an Identity object or the :any:`NonStrictName` of an Identity is acceptable. - The default Identity will be used if all of the above arguments are omitted. -+ **key_locator** (:any:`NonStrictName`) - using the specified KeyLocator Name regardless of which - Key is used. +The :mod:`ndn.app` package contains :class:`NDNApp`, the canonical asyncio +application API. It connects to an NDN forwarder and provides: + +* Interest expression and Data validation. +* Interest handlers with PIT-token-aware reply callbacks. +* Prefix registration and unregistration. +* Signed NFD management commands. + +Consumer code calls :meth:`NDNApp.express` and receives ``(name, content, +context)``. The context contains parsed metadata, signature pointers, the raw +packet, and the deadline. Producer handlers receive ``(name, app_param, reply, +context)`` and should send encoded Data through ``reply`` so PIT tokens are +preserved. + +The application does not own a keychain. Use :meth:`NDNApp.default_keychain` +when the default client configuration is desired, and pass an explicit signer +to :meth:`NDNApp.express` or :meth:`NDNApp.make_data`. Reference --------- diff --git a/docs/src/appv2.rst b/docs/src/appv2.rst deleted file mode 100644 index 698a75b..0000000 --- a/docs/src/appv2.rst +++ /dev/null @@ -1,26 +0,0 @@ -:mod:`ndn.appv2` package -======================== - -Introduction ------------- - -The :mod:`ndn.appv2` package contains ``NDNApp`` class. -It offers the functionalities similar to an application face in other NDN libraries. -Main features include: - -+ Establish a connection to NDN forwarder. -+ Consumer: express Interests and receive the reply Data. -+ Producer: attach Interest handler function to a name prefix, to handle incoming Interests. -+ Producer: register and unregister prefixes in the forwarder. - -This package is a rewrite from :mod:`ndn.app` package. -Major differences from that package are: - -+ Initial support for PIT token. -+ Send signed Interests for NFD management commands. - -Reference ---------- - -.. automodule:: ndn.appv2 - :members: diff --git a/docs/src/encoding/encoding.rst b/docs/src/encoding/encoding.rst index 8844799..0714721 100644 --- a/docs/src/encoding/encoding.rst +++ b/docs/src/encoding/encoding.rst @@ -4,66 +4,34 @@ Introduction ------------ -The :mod:`ndn.encoding` package contains classes and functions -that help to encode and decode NDN Name, NameComponent, Data and Interest. +The :mod:`ndn.encoding` package encodes and decodes TLV values, NDN names, +Interest packets, and Data packets. Its main parts are: -There are three parts of this package: - -1. **TLV elements**: process TLV variables, Names and NameComponents. - -2. **TlvModel**: design a general way to describe a TLV format. - A TLV object can be described with a class derived from :any:`TlvModel`, - with members of type :any:`Field`. - -3. **NDN Packet Fotmat v0.3**: functions used to encode and parse - Interest and Data packets in - `NDN Packet Format Spec 0.3 `_. +1. TLV number, Name, and NameComponent primitives. +2. Dataclass TLV models encoded with :func:`tlv_encode` and parsed with + :func:`tlv_parse`. +3. NDN Packet Format 0.3 helpers for Interests and Data. .. _label-different-names: :any:`FormalName` and :any:`NonStrictName` ------------------------------------------ -To increase the flexibility, API in ``python-ndn`` accepts Name arguments in a wide range of formats, -i.e. :any:`NonStrictName`, but returns an unified form, :any:`FormalName`. - -A Component is a NameComponent encoded in TLV format. +APIs accept :any:`NonStrictName` values in several forms but return the +canonical :any:`FormalName`, a list of encoded NameComponents. .. code-block:: python3 component = b'\x08\x09component' - -A :any:`FormalName` is a list of encoded Components. - -.. code-block:: python3 - formal_name = [bytearray(b'\x08\x06formal'), b'\x08\x04name'] - -A :any:`NonStrictName` is any of below: - -- A URI string. - - .. code-block:: python3 - - casual_name_1 = "/non-strict/8=name" - -- A list or iterator of Components, in the form of either encoded TLV or URI string. - - .. code-block:: python3 - - casual_name_2 = [bytearray(b'\x08\x0anon-strict'), 'name'] - casual_name_3 = (f'{x}' for x in range(3)) - -- An encoded Name of type :class:`bytes`, :class:`bytearray` or :class:`memoryview`. - - .. code-block:: python3 - - casual_name_4 = b'\x07\x12\x08\x0anon-strict\x08\x04name' + casual_name_1 = '/non-strict/8=name' + casual_name_2 = [bytearray(b'\x08\x0anon-strict'), 'name'] + casual_name_3 = b'\x07\x12\x08\x0anon-strict\x08\x04name' Customized TLV Models --------------------- -See :doc:`../examples/tlv_model` +See :doc:`../examples/tlv_model`. Reference --------- @@ -72,5 +40,5 @@ Reference TLV Variables Name and Component - TLV Model + Dataclass TLV Model NDN Packet Format 0.3 diff --git a/docs/src/encoding/tlv_model.rst b/docs/src/encoding/tlv_model.rst index aea77e1..8d4daec 100644 --- a/docs/src/encoding/tlv_model.rst +++ b/docs/src/encoding/tlv_model.rst @@ -1,47 +1,19 @@ -TLV Model -========= +Dataclass TLV Model +=================== .. automodule:: ndn.encoding.tlv_model - .. autoexception:: DecodeError - :members: +Public API +---------- - .. autoexception:: IncludeBaseError - :members: +.. autofunction:: tlv_encode - .. autoclass:: IncludeBase - :members: +.. autofunction:: tlv_parse - .. autoclass:: Field - :members: __get__, __set__, encode_into, encoded_length, get_value, parse_from, skipping_process +.. autoclass:: NDNName - .. autoclass:: ProcedureArgument - :members: __get__, __set__, get_arg, set_arg - :exclude-members: encoded_length, encoded_into, parse_from +.. autofunction:: tlv_get_arg - .. autoclass:: OffsetMarker - :exclude-members: encoded_length, encoded_into, parse_from, skipping_process +.. autofunction:: tlv_set_arg - .. autoclass:: UintField - :exclude-members: encoded_length, encoded_into, parse_from - - .. autoclass:: BoolField - :exclude-members: encoded_length, encoded_into, parse_from - - .. autoclass:: NameField - :exclude-members: encoded_length, encoded_into, parse_from - - .. autoclass:: BytesField - :exclude-members: encoded_length, encoded_into, parse_from - - .. autoclass:: ModelField - :exclude-members: encoded_length, encoded_into, parse_from - - .. autoclass:: RepeatedField - :exclude-members: encoded_length, encoded_into, parse_from - - .. autoclass:: TlvModelMeta - :members: - - .. autoclass:: TlvModel - :members: __eq__, asdict, encode, encoded_length, parse +.. autoexception:: DecodeError diff --git a/docs/src/examples/basic_app.rst b/docs/src/examples/basic_app.rst index 933fc70..086eae6 100644 --- a/docs/src/examples/basic_app.rst +++ b/docs/src/examples/basic_app.rst @@ -2,79 +2,73 @@ Basic Applications ================== Connect to NFD -~~~~~~~~~~~~~~ - -NDNApp connects to an NFD node and provides interface to express and process Interests. -The following code initializes an NDNApp instance with default configuration. +-------------- .. code-block:: python3 from ndn.app import NDNApp - app = NDNApp() - app.run_forever() -If there is a main function for the application, use the ``after_start`` argument. - -.. code-block:: python3 - - from ndn.app import NDNApp app = NDNApp() async def main(): - # Do something - app.shutdown() # Close the connection and shutdown + # Application startup work goes here. + app.shutdown() app.run_forever(after_start=main()) Consumer -~~~~~~~~ +-------- -A consumer can use ``express_interest`` to express an Interest. -If a Data is received and validated, it returns the Name, MetaInfo and Content of Data. -Otherwise, an exception is thrown. +A consumer calls :meth:`NDNApp.express` with a validator. The returned context +contains ``meta_info``, ``sig_ptrs``, and ``raw_packet``. .. code-block:: python3 + from ndn.app import NDNApp, pass_all from ndn.encoding import Name + from ndn.types import InterestNack, InterestTimeout, ValidationFailure + + app = NDNApp() async def main(): try: - data_name, meta_info, content = await app.express_interest( - # Interest Name + data_name, content, context = await app.express( '/example/testApp/randomData', + validator=pass_all, must_be_fresh=True, - can_be_prefix=False, - # Interest lifetime in ms - lifetime=6000) - # Print out Data Name, MetaInfo and its conetnt. - print(f'Received Data Name: {Name.to_str(data_name)}') - print(meta_info) + lifetime=6000, + ) + print(Name.to_str(data_name)) + print(context['meta_info']) print(bytes(content) if content else None) - except InterestNack as e: - # A NACK is received - print(f'Nacked with reason={e.reason}') + except InterestNack as exc: + print(f'Nacked with reason={exc.reason}') except InterestTimeout: - # Interest times out - print(f'Timeout') - except InterestCanceled: - # Connection to NFD is broken - print(f'Canceled') + print('Timeout') except ValidationFailure: - # Validation failure - print(f'Data failed to validate') + print('Data failed to validate') finally: app.shutdown() Producer -~~~~~~~~ +-------- -A producer can call ``route`` to register a permanent route. -Route registration can be done before application is started. -NDNApp will automatically announce that route to the NFD node. +Interest handlers are synchronous callbacks. Use the supplied ``reply`` +function to preserve the incoming PIT token. .. code-block:: python3 - @app.route('/example/testApp') - def on_interest(name, interest_param, application_param): - app.put_data(name, content=b'content', freshness_period=10000) + from ndn.app import NDNApp + from ndn.security import DigestSha256Signer + app = NDNApp() + + @app.route('/example/testApp') + def on_interest(name, app_param, reply, context): + packet = app.make_data( + name, + content=b'content', + signer=DigestSha256Signer(), + freshness_period=10000, + ) + reply(packet) diff --git a/docs/src/examples/tlv_model.rst b/docs/src/examples/tlv_model.rst index 038db80..e8e3de1 100644 --- a/docs/src/examples/tlv_model.rst +++ b/docs/src/examples/tlv_model.rst @@ -1,141 +1,73 @@ Customized TLV Models ===================== -Encoding --------- - -``python-ndn`` provides a descriptive way to define a specific TLV format, called TLV model. -Every object can be described by a class derived from :any:`TlvModel`. -Elements of a TLV object is expressed as an instance variable of :any:`Field`. -Fields are encoded in order. - -.. code-block:: python3 - - from ndn.encoding import * - - class Model(TlvModel): # Model = [Name] [IntVal] [StrVal] [BoolVal] - name = NameField() # Name = NAME-TYPE TLV-LENGTH ... - int_val = UintField(0x03) # IntVal = INT-VAL-TYPE TLV-LENGTH nonNegativeInteger - str_val = BytesField(0x02) # StrVal = STR-VAL-TYPE TLV-LENGTH *OCTET - bool_val = BoolField(0x01) # BoolVal = BOOL-VAL-TYPE 0 - - model = Model() - model.name = '/name' - model.str_val = b'bit string' - assert model.encode() == b'\x07\x06\x08\x04name\x02\nbit string' - - model = Model.parse(b'\x07\x06\x08\x04name\x02\nbit string') - assert model.str_val == b'bit string' - -There is *no required* fields in a TLV model. -Every :any:`Field` is ``None`` by default, which means it will not be encoded. - -Nested Model ------------- - -``python-ndn`` allows a TLV model to be a field (:any:`ModelField`) of another TLV model, -which enables a hierarchical structure. -Also, a TLV model does not contain the outer Type and Length. -This can be solved by encapsulating it into another TLV model. - -.. code-block:: python3 - - class Inner(TlvModel): # Inner = [Val1] - val1 = UintField(0x01) # Val1 = 1 TLV-LENGTH nonNegativeInteger - - class Outer(TlvModel): # Outer = [Val2] - val2 = ModelField(0x02, Inner) # Val2 = 2 TLV-LENGTH Inner - - obj = Outer() - obj.val2 = Inner() - obj.val2.val1 = 255 - assert obj.encode() == b'\x02\x03\x01\x01\xFF' - -Repeated Model --------------- +TLV models are Python dataclasses. Type annotations determine how values are +encoded, while ``dataclasses.field`` metadata supplies TLV type numbers and +special encoding options. -:any:`RepeatedField` is an array of a specific type of field. -When encoding, elements are encoded in order. +Encoding and parsing +-------------------- .. code-block:: python3 - class WordArray(TlvModel): # WordArray = *Words - words = RepeatedField(UintField(0x01, fixed_len=2)) # Words = 1 2 2OCTET + from dataclasses import dataclass, field + from ndn.encoding import NDNName, Name, tlv_encode, tlv_parse - array = WordArray() - array.words = [i for i in range(3)] - assert array.encode() == b'\x01\x02\x00\x00\x01\x02\x00\x01\x01\x02\x00\x02' + @dataclass + class Model: + name: NDNName = field(default=None, metadata={'tlv_type': 0x07}) + int_val: int = field(default=None, metadata={'tlv_type': 0x03}) + str_val: bytes = field(default=None, metadata={'tlv_type': 0x02}) + bool_val: bool = field(default=False, metadata={'tlv_type': 0x01}) -Derivation ----------- + wire = tlv_encode(Model(name='/name', str_val=b'bit string')) + parsed = tlv_parse(Model, wire) + assert Name.to_str(parsed.name) == '/name' + assert bytes(parsed.str_val) == b'bit string' -To avoid duplication, a :any:`TlvModel` can extend 1 or more other TlvModels. -However, to indicate the locations of base classes in the TLV encoded wire, -there must be an field for every base class to explicitly include its base class. -These fields must have the value :any:`IncludeBase`. -TlvModel instances' Include fields cannot be assigned, and will be ignored during encoding and parsing. +``None`` values are omitted. Boolean fields are encoded as zero-length TLVs +when true and are omitted when false. -.. code-block:: python3 - - class Base(TlvModel): # Base = [M2] - m2 = UintField(0x02) - - class Derived(Base): # Derived = [M1] [M2] [M3] - m1 = UintField(0x01) - _base = IncludeBase(Base) - m3 = UintField(0x03) - - obj = Derived() - obj.m1, obj.m2, obj.m3 = range(1, 4) - assert obj.encode() == b'\x01\x01\x01\x02\x01\x02\x03\x01\x03' - -Overriding ----------- +Nested and repeated values +-------------------------- -The derived class can override fields of its base classes. -To override a field, declare a field with the same name *after* the including. -Overriding fields will be encoded in their *original* places, irrelevant to the order of declaration. +Dataclass annotations also describe nested models, repeated fields, and maps. .. code-block:: python3 - class A1(TlvModel): # A1 = [M1] - m1 = UintField(0x01) - - class A2(A1): # A2 = [M1] [M2] - _a1 = IncludeBase(A1) - m2 = UintField(0x02) - - class B1(TlvModel): # B1 = [X] [A1] [Y] - x = UintField(0x0a) - a = ModelField(0x03, A1) - y = UintField(0x0b) - - class B2(B1): # B2 = [X] [A2] [Y] - IncludeBase(B1) - a = ModelField(0x03, A2) - -Parsing -------- - -A TlvModel can be parsed from a wire. All fields are parsed in order. -Out of order or unknown fields are ignored if they are non-critical. -An unknown critical field leads to :any:`DecodeError` . - -.. code-block:: python3 - - from ndn.encoding import * - - class Model(TlvModel): # Model = [Name] [IntVal] [StrVal] [BoolVal] - name = NameField() # Name = NAME-TYPE TLV-LENGTH ... - int_val = UintField(0x03) # IntVal = INT-VAL-TYPE TLV-LENGTH nonNegativeInteger - str_val = BytesField(0x02) # StrVal = STR-VAL-TYPE TLV-LENGTH *OCTET - bool_val = BoolField(0x01) # BoolVal = BOOL-VAL-TYPE 0 - - model = Model.parse(b'\x07\x06\x08\x04name\x02\nbit string') - assert Name.to_str(model.name) == '/name' - assert model.str_val == b'bit string' - -Signature ---------- + @dataclass + class Inner: + value: int = field(default=None, metadata={'tlv_type': 0x01}) + + @dataclass + class Outer: + inner: Inner = field(default=None, metadata={'tlv_type': 0x02}) + words: list[int] = field( + default_factory=list, + metadata={'tlv_type': 0x03, 'fixed_len': 2}, + ) + labels: dict[str, bytes] = field( + default_factory=dict, + metadata={'tlv_type': 0x21, 'val_tlv_type': 0x23}, + ) + + wire = tlv_encode(Outer( + inner=Inner(255), + words=[0, 1, 2], + labels={'key': b'value'}, + )) + parsed = tlv_parse(Outer, wire) + assert parsed.inner.value == 255 + +Dataclass inheritance places base-class fields before subclass fields. Unknown +critical TLVs raise :class:`DecodeError`; unknown non-critical TLVs are skipped. +The codec returns binary values as zero-copy ``memoryview`` slices where +possible. + +Metadata +-------- -Please contact the developer if you have to have a Signature field in your model. +Common metadata keys are ``tlv_type``, ``fixed_len``, ``ignore_critical``, +``val_tlv_type``, and ``field_type``. ``field_type`` is reserved for special +fields such as offset markers, signature values, and Interest names used by +the packet-format implementation. diff --git a/docs/src/lvs/package.rst b/docs/src/lvs/package.rst index b1bcd6b..81b8df1 100644 --- a/docs/src/lvs/package.rst +++ b/docs/src/lvs/package.rst @@ -14,4 +14,4 @@ .. autoclass:: LvsModelError :members: -.. autonewtypedata:: ndn.app_support.light_versec.checker.UserFn \ No newline at end of file +.. autodata:: ndn.app_support.light_versec.checker.UserFn \ No newline at end of file diff --git a/docs/src/schema/custom_node.rst b/docs/src/schema/custom_node.rst deleted file mode 100644 index a8625ac..0000000 --- a/docs/src/schema/custom_node.rst +++ /dev/null @@ -1,5 +0,0 @@ -Custom Nodes -============ - -.. automodule:: ndn.schema.simple_node - :members: diff --git a/docs/src/schema/ex1.rst b/docs/src/schema/ex1.rst deleted file mode 100644 index 8c326b7..0000000 --- a/docs/src/schema/ex1.rst +++ /dev/null @@ -1,88 +0,0 @@ -1 - File Sharing -================ - -Assume that Alice has several devices and wants to share some files among them. -To simplify the case, we assume that all devices have Alice's key, which can be used as the trust anchor. -A file may be large, so segmentation is needed. - -Design -~~~~~~ - -First, let's start with the namespace design. -There are two kinds of object in the system, one is the key, the other is the file. - -For the file, an option is `RDR protocol `_. -RDR protocol handles the version discovery and segmentation. -There is no need to know the implementation details, -since NTScheme allows we use an existing protocols as a black box. -In short, RDR has: - -- A metadata packet that contains a version number of the content. -- A series of data packets containing segmented data content. - -For the key, we can use a single Data packet to contain the certificate. - -.. note:: - - This example is only used for demo, which is different from the real-world scenario. - - - RDR is not necessary in this scenario, since there is only one version for each file. - - In real world, Alice may want to have a trust anchor instead of sharing a single key. - -The whole namespace design is shown as follows: - -.. image:: /_static/schema-example1-schema.svg - :align: center - :width: 40% - -In the figure, ``/file/`` is the file object -and ``//KEY//self/`` represents the certificate. -Here, ```` is a pattern variable that matches exactly one name cpmponent. -The real names may be ``/file/foo.txt`` and ``/Alice/KEY/%29/self/%F6``. -Also, note that ``/file/`` is an object composed of multiple data packets, -which are managed by :any:`RDRNode` and not exposed to the programmer. - -Then, let's move to the policies part. -We want to ensure the following requirements: - -- All data packets are stored in memory, so if another node requests this file, - the current node can serve it. This applies to both the producer -- which loads - the file from the disk and create packets, and the consumer -- which receives - the file from another node. -- Data packets of the file must be signed by Alice's key. - The certificate can be preloaded into memory when the program starts. - -Let's attach these two policies onto the namespace schema tree we have: - -.. image:: /_static/schema-example1-policy.svg - :align: center - :width: 50% - -The :any:`MemoryCachePolicy` indicates all data packets are stored in memory. -And :any:`SignedBy` requires data packets with prefix ``/file/`` -to be signed by key ``//KEY/``. -We can add restrictions, such as ``IDName == 'Alice'``, to limit the identity. - -Coding -~~~~~~ - -With NTSchema, we can translate our design into code directly: - -.. code-block:: python3 - - # Make schema tree - root = Node() - root['//KEY//self/'] = Node() - root['/file/'] = RDRNode() - - # Set policies - id_name = Name.Component.get_value(app.keychain.default_identity().name[0]) - cache = MemoryCache() - root.set_policy(policy.Cache, MemoryCachePolicy(cache)) - root['/file/'].set_policy( - policy.DataValidator, - SignedBy(root['//KEY/'], - subject_to=lambda _, vars: vars['IDName'] == id_name)) - -The full source code can be found in -`examples/rdrnode.py `_. diff --git a/docs/src/schema/policies.rst b/docs/src/schema/policies.rst deleted file mode 100644 index f05eea8..0000000 --- a/docs/src/schema/policies.rst +++ /dev/null @@ -1,20 +0,0 @@ -Policies -======== - -Policy Types -~~~~~~~~~~~~ - -.. automodule:: ndn.schema.policy - :members: - -Trust Policies -~~~~~~~~~~~~~~ - -.. automodule:: ndn.schema.simple_trust - :members: - -Cache Policies -~~~~~~~~~~~~~~ - -.. automodule:: ndn.schema.simple_cache - :members: diff --git a/docs/src/schema/schema.rst b/docs/src/schema/schema.rst deleted file mode 100644 index 3bce5af..0000000 --- a/docs/src/schema/schema.rst +++ /dev/null @@ -1,48 +0,0 @@ -:mod:`ndn.schema` package -============================ - -.. warning:: - - Name Tree Schema (NTSchema) is experimental and capricious. - The current implementation is treated as a proof-of-concept demo. - -Introduction ------------- - -The :mod:`ndn.schema` package provides an implementation of Name Tree Schema, an application framework that -organizes application functionalities by the applciation namespace. -Modularized NDN libraries can be developed based on it, and -application developers can use those libraries as building blocks. - -The core concept of NTSchema is the namespace schema tree. -The schema tree is a tree structure that contains all possible naming conventions of an application. -Different from a tree of names, its edge may be a pattern variable instead of a specific name component. -For example, the path ``//KEY/`` can be used to represents a naming convention of a key, -where specific keys -- like ``/Alice/KEY/%01`` and ``/Bob/KEY/%c2`` match with it. - -Two main components of NTSchema are custom nodes and policies. -In the schema tree, every node represents a namespace. -After matching with a specific name, a node can be used to produce and consume data. -For example, if we call ``matched_node = tree.match('/Alice/KEY/%01')``, it will return a matching of node -``//KEY/`` with variable setting ``Identity='Alice', KeyID=\x01``. -Then we call ``matched_node.provide(key_data)``, it will generate the key with data ``key_data`` and make it available. -When we call ``key_data = matched_node.need()``, it will try to fetch the key. -A custom node will have customized pipeline to handle ``provide`` and ``need`` function calls. -Policies are annotations attached to nodes, that specifies user-defined policies that are security, storage, etc. - -Examples --------- - -.. toctree:: - - 1 - File Sharing - -Reference ---------- - -.. toctree:: - - Namespace Schema Tree - Utils - Custom Nodes - Policies diff --git a/docs/src/schema/schema_tree.rst b/docs/src/schema/schema_tree.rst deleted file mode 100644 index 1830ab6..0000000 --- a/docs/src/schema/schema_tree.rst +++ /dev/null @@ -1,5 +0,0 @@ -Namespace Schema Tree -===================== - -.. automodule:: ndn.schema.schema_tree - :members: diff --git a/docs/src/schema/utils.rst b/docs/src/schema/utils.rst deleted file mode 100644 index b185183..0000000 --- a/docs/src/schema/utils.rst +++ /dev/null @@ -1,5 +0,0 @@ -Utils -===== - -.. automodule:: ndn.schema.util - :members: diff --git a/docs/src/security/security.rst b/docs/src/security/security.rst index 0ebec21..2928946 100644 --- a/docs/src/security/security.rst +++ b/docs/src/security/security.rst @@ -4,12 +4,13 @@ Introduction ------------ -The :mod:`ndn.security` package provides basic tools for security use. +The :mod:`ndn.security` package provides signers, validators, keychains, and +TPM integrations. Signer ------ -A :any:`Signer` is a class used to sign a packet during encoding. +A :class:`Signer` supplies signature metadata and computes a packet signature. .. autoclass:: ndn.encoding.Signer :members: @@ -17,29 +18,32 @@ A :any:`Signer` is a class used to sign a packet during encoding. Validator --------- -A :any:`Validator` is a async function called to validate an Interest or Data packet. -It takes 2 arguments: a :any:`FormalName` and a :any:`SignaturePtrs`, -and returns whether the packet is validated. +An application validator is an async callable with three arguments: a +:class:`FormalName`, :class:`SignaturePtrs`, and packet-context dictionary. It +returns :class:`ValidResult`. ``PASS`` and ``ALLOW_BYPASS`` accept a packet; +``FAIL`` and ``TIMEOUT`` reject it. + +The digest and known-key validator factories exported from +:mod:`ndn.security` follow this contract. Keychain -------- -A :any:`Keychain` is a class which contains Identities, Keys associated with Identities and associated Certificates. +A :class:`Keychain` contains identities, their keys, and certificates. .. autoclass:: ndn.security.keychain.Keychain :members: - KeychainDigest ~~~~~~~~~~~~~~ + .. automodule:: ndn.security.keychain.keychain_digest :members: - KeychainSqlite3 ~~~~~~~~~~~~~~~ -This is the default Keychain. +This is the default persistent keychain. .. automodule:: ndn.security.keychain.keychain_sqlite3 :members: From 39afa3fc00112b26cedda92b892253c67f3aafa2 Mon Sep 17 00:00:00 2001 From: Xinyu Ma Date: Thu, 8 Oct 2026 13:01:18 -0700 Subject: [PATCH 8/8] style: use Python 3.11 typing syntax Co-authored-by: Cursor --- src/ndn/app.py | 18 +- src/ndn/app_support/light_versec/binary.py | 38 ++-- src/ndn/app_support/nfd_mgmt.py | 187 +++++++++--------- src/ndn/app_support/security_v2.py | 21 +- src/ndn/app_support/svs/tlv.py | 11 +- src/ndn/encoding/ndn_format_0_3.py | 75 ++++--- src/ndn/encoding/ndn_format_0_3_2017.py | 77 ++++---- src/ndn/encoding/ndnlp_v2.py | 35 ++-- src/ndn/encoding/tlv_model.py | 22 ++- src/ndn/platform/osx.py | 3 +- src/ndn/platform/windows.py | 5 +- src/ndn/security/keychain/keychain_sqlite3.py | 2 +- src/ndn/types.py | 2 +- tests/encoding/tlv_model_test.py | 2 +- 14 files changed, 248 insertions(+), 250 deletions(-) diff --git a/src/ndn/app.py b/src/ndn/app.py index 7c88d48..a79fb2c 100644 --- a/src/ndn/app.py +++ b/src/ndn/app.py @@ -60,12 +60,12 @@ The function should use the provided ``reply`` callback to reply with Data, which can handle PIT token properly. -.. function:: (name: FormalName, app_param: Optional[BinaryStr], reply: ReplyFunc, context: PktContext) -> None +.. function:: (name: FormalName, app_param: BinaryStr | None, reply: ReplyFunc, context: PktContext) -> None :param name: Interest name. :type name: :any:`FormalName` :param app_param: Interest ApplicationParameters value, or None if absent. - :type app_param: Optional[:any:`BinaryStr`] + :type app_param: :any:`BinaryStr` | None :param reply: continuation function to respond with Data. :type reply: :any:`ReplyFunc` :param context: packet handler context. @@ -319,9 +319,9 @@ def make_data(name: enc.NonStrictName, content: enc.BinaryStr | None, :param name: the Name. :type name: :any:`NonStrictName` :param content: the Content. - :type content: Optional[:any:`BinaryStr`] + :type content: :any:`BinaryStr` | None :param signer: the Signer used to sign the packet. - :type signer: Optional[:any:`Signer`] + :type signer: :any:`Signer` | None :param kwargs: arguments for :any:`MetaInfo`. :return: TLV encoded Data packet. """ @@ -477,7 +477,7 @@ def attach_handler(self, name: enc.NonStrictName, handler: IntHandler, Otherwise, signed Interests are passed to the validator. Those failing the validation are dropped silently. Those passing the validation are passed to the handler function. - :type validator: Optional[:any:`Validator`] + :type validator: :any:`Validator` | None """ name = enc.Name.normalize(name) node = self._fib.setdefault(name, PrefixTreeNode()) @@ -617,12 +617,12 @@ def express(self, name: enc.NonStrictName, validator: Validator, :param app_param: Interest ApplicationParameters value. If this is not None, a signed Interest is sent. NDNApp does not support sending parameterized Interests that are not signed. - :type app_param: Optional[:any:`BinaryStr`] + :type app_param: :any:`BinaryStr` | None :param signer: Signer for Interest signing. This is required if `app_param` is specified. - :type signer: Optional[:any:`Signer`] + :type signer: :any:`Signer` | None :param kwargs: arguments for :any:`InterestParam`. :return: A tuple of (Name, Content, PacketContext) after ``await``. - :rtype: Coroutine[Any, None, Tuple[:any:`FormalName`, Optional[:any:`BinaryStr`], :any:`PktContext`]] + :rtype: Coroutine[Any, None, tuple[:any:`FormalName`, :any:`BinaryStr` | None, :any:`PktContext`]] The following exceptions may be raised by ``express``: @@ -664,7 +664,7 @@ def route(self, name: enc.NonStrictName, validator: Validator | None = None): :param name: name prefix. :type name: :any:`NonStrictName` :param validator: validator for signed Interests. See :any:`attach_handler` for details. - :type validator: Optional[:any:`Validator`] + :type validator: :any:`Validator` | None :examples: .. code-block:: python3 diff --git a/src/ndn/app_support/light_versec/binary.py b/src/ndn/app_support/light_versec/binary.py index 58e760d..32b2714 100644 --- a/src/ndn/app_support/light_versec/binary.py +++ b/src/ndn/app_support/light_versec/binary.py @@ -21,7 +21,7 @@ # limitations under the License. # ----------------------------------------------------------------------------- import dataclasses as dc -from typing import Optional +from typing import Self from ...encoding import BinaryStr from ...encoding.tlv_model import tlv_encode, tlv_parse @@ -69,16 +69,16 @@ class TypeNumber: @dc.dataclass class UserFnArg: # A given component - value: Optional[bytes] = dc.field( + value: bytes | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.COMPONENT_VALUE}) # Referring to a previous matched pattern - tag: Optional[int] = dc.field( + tag: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.PATTERN_TAG}) @dc.dataclass class UserFnCall: - fn_id: Optional[str] = dc.field( + fn_id: str | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.USER_FN_ID}) args: list[UserFnArg] = dc.field( default_factory=list, metadata={'tlv_type': TypeNumber.FN_ARGS}) @@ -87,13 +87,13 @@ class UserFnCall: @dc.dataclass class ConstraintOption: # Equal to a given NameComponent value - value: Optional[bytes] = dc.field( + value: bytes | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.COMPONENT_VALUE}) # Equal to another pattern - tag: Optional[int] = dc.field( + tag: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.PATTERN_TAG}) # Decide by a user function call - fn: Optional[UserFnCall] = dc.field( + fn: UserFnCall | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.USER_FN_CALL}) @@ -105,9 +105,9 @@ class PatternConstraint: @dc.dataclass class PatternEdge: - dest: Optional[int] = dc.field( + dest: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.NODE_ID}) - tag: Optional[int] = dc.field( + tag: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.PATTERN_TAG}) cons_sets: list[PatternConstraint] = dc.field( default_factory=list, metadata={'tlv_type': TypeNumber.CONSTRAINT}) @@ -115,17 +115,17 @@ class PatternEdge: @dc.dataclass class ValueEdge: - dest: Optional[int] = dc.field( + dest: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.NODE_ID}) - value: Optional[bytes] = dc.field( + value: bytes | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.COMPONENT_VALUE}) @dc.dataclass class Node: - id: Optional[int] = dc.field( + id: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.NODE_ID}) - parent: Optional[int] = dc.field( + parent: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.PARENT_ID}) rule_name: list[str] = dc.field( default_factory=list, metadata={'tlv_type': TypeNumber.IDENTIFIER}) @@ -139,19 +139,19 @@ class Node: @dc.dataclass class TagSymbol: - tag: Optional[int] = dc.field( + tag: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.PATTERN_TAG}) - ident: Optional[str] = dc.field( + ident: str | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.IDENTIFIER}) @dc.dataclass class LvsModel: - version: Optional[int] = dc.field( + version: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.VERSION}) - start_id: Optional[int] = dc.field( + start_id: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.NODE_ID}) - named_pattern_cnt: Optional[int] = dc.field( + named_pattern_cnt: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.NAMED_PATTERN_NUM}) nodes: list[Node] = dc.field( default_factory=list, metadata={'tlv_type': TypeNumber.NODE}) @@ -162,5 +162,5 @@ def encode(self) -> bytearray: return tlv_encode(self) @classmethod - def parse(cls, wire: BinaryStr) -> 'LvsModel': + def parse(cls, wire: BinaryStr) -> Self: return tlv_parse(cls, wire) diff --git a/src/ndn/app_support/nfd_mgmt.py b/src/ndn/app_support/nfd_mgmt.py index 4119df4..0dcf8a2 100644 --- a/src/ndn/app_support/nfd_mgmt.py +++ b/src/ndn/app_support/nfd_mgmt.py @@ -18,7 +18,6 @@ """NFD management protocol models using the dataclass TLV API.""" import dataclasses as dc import struct -from typing import Optional from enum import Enum, Flag from ..transport.face import Face @@ -96,106 +95,106 @@ class Strategy: @dc.dataclass class ControlParametersValue: name: NDNName = _name() - face_id: Optional[int] = _tlv(0x69) - uri: Optional[str] = _tlv(0x72) - local_uri: Optional[str] = _tlv(0x81) - origin: Optional[int] = _tlv(0x6f) - cost: Optional[int] = _tlv(0x6a) - capacity: Optional[int] = _tlv(0x83) - count: Optional[int] = _tlv(0x84) - base_congestion_mark_interval: Optional[int] = _tlv(0x87) - default_congestion_threshold: Optional[int] = _tlv(0x88) - mtu: Optional[int] = _tlv(0x89) - flags: Optional[int] = _tlv(0x6c) - mask: Optional[int] = _tlv(0x70) - strategy: Optional[Strategy] = _tlv(0x6b) - expiration_period: Optional[int] = _tlv(0x6d) - face_persistency: Optional[FacePersistency] = _tlv(0x85) + face_id: int | None = _tlv(0x69) + uri: str | None = _tlv(0x72) + local_uri: str | None = _tlv(0x81) + origin: int | None = _tlv(0x6f) + cost: int | None = _tlv(0x6a) + capacity: int | None = _tlv(0x83) + count: int | None = _tlv(0x84) + base_congestion_mark_interval: int | None = _tlv(0x87) + default_congestion_threshold: int | None = _tlv(0x88) + mtu: int | None = _tlv(0x89) + flags: int | None = _tlv(0x6c) + mask: int | None = _tlv(0x70) + strategy: Strategy | None = _tlv(0x6b) + expiration_period: int | None = _tlv(0x6d) + face_persistency: FacePersistency | None = _tlv(0x85) @dc.dataclass class ControlParameters: - cp: Optional[ControlParametersValue] = _tlv(0x68) + cp: ControlParametersValue | None = _tlv(0x68) @dc.dataclass class ControlResponse: - status_code: Optional[int] = _tlv(0x66) - status_text: Optional[str] = _tlv(0x67) - body: Optional[ControlParametersValue] = _tlv(0x68) + status_code: int | None = _tlv(0x66) + status_text: str | None = _tlv(0x67) + body: ControlParametersValue | None = _tlv(0x68) @dc.dataclass class FaceEventNotificationValue: - face_event_kind: Optional[FaceEventKind] = _tlv(0xc1) - face_id: Optional[int] = _tlv(0x69) - uri: Optional[str] = _tlv(0x72) - local_uri: Optional[str] = _tlv(0x81) - face_scope: Optional[FaceScope] = _tlv(0x84) - face_persistency: Optional[FacePersistency] = _tlv(0x85) - link_type: Optional[FaceLinkType] = _tlv(0x86) - flags: Optional[FaceFlags] = _tlv(0x6c) + face_event_kind: FaceEventKind | None = _tlv(0xc1) + face_id: int | None = _tlv(0x69) + uri: str | None = _tlv(0x72) + local_uri: str | None = _tlv(0x81) + face_scope: FaceScope | None = _tlv(0x84) + face_persistency: FacePersistency | None = _tlv(0x85) + link_type: FaceLinkType | None = _tlv(0x86) + flags: FaceFlags | None = _tlv(0x6c) @dc.dataclass class FaceEventNotification: - event: Optional[FaceEventNotificationValue] = _tlv(0xc0) + event: FaceEventNotificationValue | None = _tlv(0xc0) @dc.dataclass class GeneralStatus: - nfd_version: Optional[str] = _tlv(0x80) - start_timestamp: Optional[int] = _tlv(0x81) - current_timestamp: Optional[int] = _tlv(0x82) - n_name_tree_entries: Optional[int] = _tlv(0x83) - n_fib_entries: Optional[int] = _tlv(0x84) - n_pit_entries: Optional[int] = _tlv(0x85) - n_measurement_entries: Optional[int] = _tlv(0x86) - n_cs_entries: Optional[int] = _tlv(0x87) - n_in_interests: Optional[int] = _tlv(0x90) - n_in_data: Optional[int] = _tlv(0x91) - n_in_nacks: Optional[int] = _tlv(0x97) - n_out_interests: Optional[int] = _tlv(0x92) - n_out_data: Optional[int] = _tlv(0x93) - n_out_nacks: Optional[int] = _tlv(0x98) - n_satisfied_interests: Optional[int] = _tlv(0x99) - n_unsatisfied_interests: Optional[int] = _tlv(0x9a) + nfd_version: str | None = _tlv(0x80) + start_timestamp: int | None = _tlv(0x81) + current_timestamp: int | None = _tlv(0x82) + n_name_tree_entries: int | None = _tlv(0x83) + n_fib_entries: int | None = _tlv(0x84) + n_pit_entries: int | None = _tlv(0x85) + n_measurement_entries: int | None = _tlv(0x86) + n_cs_entries: int | None = _tlv(0x87) + n_in_interests: int | None = _tlv(0x90) + n_in_data: int | None = _tlv(0x91) + n_in_nacks: int | None = _tlv(0x97) + n_out_interests: int | None = _tlv(0x92) + n_out_data: int | None = _tlv(0x93) + n_out_nacks: int | None = _tlv(0x98) + n_satisfied_interests: int | None = _tlv(0x99) + n_unsatisfied_interests: int | None = _tlv(0x9a) # The following comes from DNMP's extension to NFD mgmt protocol: # https://github.com/pollere/DNMP-v2/blob/c4359ae1af03824ec1ee8cd27a7d52c9151fa813/formats/forwarder-status.proto # It does not show up in the standard protocol: # https://redmine.named-data.net/projects/nfd/wiki/ForwarderStatus - n_fragmentation_errors: Optional[int] = _tlv(0xc8) - n_out_over_mtu: Optional[int] = _tlv(0xc9) - n_in_lp_invalid: Optional[int] = _tlv(0xca) - n_reassembly_timeouts: Optional[int] = _tlv(0xcb) - n_in_net_invalid: Optional[int] = _tlv(0xcc) - n_acknowledged: Optional[int] = _tlv(0xcd) - n_retransmitted: Optional[int] = _tlv(0xce) - n_retx_exhausted: Optional[int] = _tlv(0xcf) - n_congestion_marked: Optional[int] = _tlv(0xd0) + n_fragmentation_errors: int | None = _tlv(0xc8) + n_out_over_mtu: int | None = _tlv(0xc9) + n_in_lp_invalid: int | None = _tlv(0xca) + n_reassembly_timeouts: int | None = _tlv(0xcb) + n_in_net_invalid: int | None = _tlv(0xcc) + n_acknowledged: int | None = _tlv(0xcd) + n_retransmitted: int | None = _tlv(0xce) + n_retx_exhausted: int | None = _tlv(0xcf) + n_congestion_marked: int | None = _tlv(0xd0) @dc.dataclass class FaceStatus: - face_id: Optional[int] = _tlv(0x69) - uri: Optional[str] = _tlv(0x72) - local_uri: Optional[str] = _tlv(0x81) - expiration_period: Optional[int] = _tlv(0x6d) - face_scope: Optional[FaceScope] = _tlv(0x84) - face_persistency: Optional[FacePersistency] = _tlv(0x85) - link_type: Optional[FaceLinkType] = _tlv(0x86) - base_congestion_mark_interval: Optional[int] = _tlv(0x87) - default_congestion_threshold: Optional[int] = _tlv(0x88) - mtu: Optional[int] = _tlv(0x89) - n_in_interests: Optional[int] = _tlv(0x90) - n_in_data: Optional[int] = _tlv(0x91) - n_in_nacks: Optional[int] = _tlv(0x97) - n_out_interests: Optional[int] = _tlv(0x92) - n_out_data: Optional[int] = _tlv(0x93) - n_out_nacks: Optional[int] = _tlv(0x98) - n_in_bytes: Optional[int] = _tlv(0x94) - n_out_bytes: Optional[int] = _tlv(0x95) - flags: Optional[FaceFlags] = _tlv(0x6c) + face_id: int | None = _tlv(0x69) + uri: str | None = _tlv(0x72) + local_uri: str | None = _tlv(0x81) + expiration_period: int | None = _tlv(0x6d) + face_scope: FaceScope | None = _tlv(0x84) + face_persistency: FacePersistency | None = _tlv(0x85) + link_type: FaceLinkType | None = _tlv(0x86) + base_congestion_mark_interval: int | None = _tlv(0x87) + default_congestion_threshold: int | None = _tlv(0x88) + mtu: int | None = _tlv(0x89) + n_in_interests: int | None = _tlv(0x90) + n_in_data: int | None = _tlv(0x91) + n_in_nacks: int | None = _tlv(0x97) + n_out_interests: int | None = _tlv(0x92) + n_out_data: int | None = _tlv(0x93) + n_out_nacks: int | None = _tlv(0x98) + n_in_bytes: int | None = _tlv(0x94) + n_out_bytes: int | None = _tlv(0x95) + flags: FaceFlags | None = _tlv(0x6c) @dc.dataclass @@ -205,27 +204,27 @@ class FaceStatusMsg: @dc.dataclass class FaceQueryFilterValue: - face_id: Optional[int] = _tlv(0x69) - uri_scheme: Optional[str] = _tlv(0x83) - uri: Optional[str] = _tlv(0x72) - local_uri: Optional[str] = _tlv(0x81) - face_scope: Optional[FaceScope] = _tlv(0x84) - face_persistency: Optional[FacePersistency] = _tlv(0x85) - link_type: Optional[FaceLinkType] = _tlv(0x86) + face_id: int | None = _tlv(0x69) + uri_scheme: str | None = _tlv(0x83) + uri: str | None = _tlv(0x72) + local_uri: str | None = _tlv(0x81) + face_scope: FaceScope | None = _tlv(0x84) + face_persistency: FacePersistency | None = _tlv(0x85) + link_type: FaceLinkType | None = _tlv(0x86) @dc.dataclass class FaceQueryFilter: - face_query_filter: Optional[FaceQueryFilterValue] = _tlv(0x96) + face_query_filter: FaceQueryFilterValue | None = _tlv(0x96) @dc.dataclass class Route: - face_id: Optional[int] = _tlv(0x69) - origin: Optional[int] = _tlv(0x6f) - cost: Optional[int] = _tlv(0x6a) - flags: Optional[RouteFlags] = _tlv(0x6c) - expiration_period: Optional[int] = _tlv(0x6d) + face_id: int | None = _tlv(0x69) + origin: int | None = _tlv(0x6f) + cost: int | None = _tlv(0x6a) + flags: RouteFlags | None = _tlv(0x6c) + expiration_period: int | None = _tlv(0x6d) @dc.dataclass @@ -241,8 +240,8 @@ class RibStatus: @dc.dataclass class NextHopRecord: - face_id: Optional[int] = _tlv(0x69) - cost: Optional[int] = _tlv(0x6a) + face_id: int | None = _tlv(0x69) + cost: int | None = _tlv(0x6a) @dc.dataclass @@ -259,7 +258,7 @@ class FibStatus: @dc.dataclass class StrategyChoice: name: NDNName = _name() - strategy: Optional[Strategy] = _tlv(0x6b) + strategy: Strategy | None = _tlv(0x6b) @dc.dataclass @@ -269,11 +268,11 @@ class StrategyChoiceMsg: @dc.dataclass class CsInfo: - capacity: Optional[int] = _tlv(0x83) - flags: Optional[int] = _tlv(0x6c) - n_cs_entries: Optional[int] = _tlv(0x87) - n_hits: Optional[int] = _tlv(0x81) - n_misses: Optional[int] = _tlv(0x82) + capacity: int | None = _tlv(0x83) + flags: int | None = _tlv(0x6c) + n_cs_entries: int | None = _tlv(0x87) + n_hits: int | None = _tlv(0x81) + n_misses: int | None = _tlv(0x82) def make_command(module, command, face: Face | None = None, **kwargs): diff --git a/src/ndn/app_support/security_v2.py b/src/ndn/app_support/security_v2.py index 1b1f994..1587015 100644 --- a/src/ndn/app_support/security_v2.py +++ b/src/ndn/app_support/security_v2.py @@ -17,7 +17,6 @@ # ----------------------------------------------------------------------------- import dataclasses as dc from datetime import datetime, timedelta, UTC -from typing import Optional from ..utils import timestamp from ..encoding import ( @@ -58,10 +57,10 @@ class SecurityV2TypeNumber: @dc.dataclass class DescriptionEntry: - description_key: Optional[bytes] = dc.field( + description_key: bytes | None = dc.field( default=None, metadata={ 'tlv_type': SecurityV2TypeNumber.DESCRIPTION_KEY}) - description_value: Optional[bytes] = dc.field( + description_value: bytes | None = dc.field( default=None, metadata={ 'tlv_type': SecurityV2TypeNumber.DESCRIPTION_VALUE}) @@ -75,32 +74,32 @@ class AdditionalDescription: @dc.dataclass class CertificateV2Extension: - additional_description: Optional[AdditionalDescription] = dc.field( + additional_description: AdditionalDescription | None = dc.field( default=None, metadata={ 'tlv_type': SecurityV2TypeNumber.ADDITIONAL_DESCRIPTION}) @dc.dataclass class ValidityPeriod: - not_before: Optional[bytes] = dc.field( + not_before: bytes | None = dc.field( default=None, metadata={'tlv_type': SecurityV2TypeNumber.NOT_BEFORE}) - not_after: Optional[bytes] = dc.field( + not_after: bytes | None = dc.field( default=None, metadata={'tlv_type': SecurityV2TypeNumber.NOT_AFTER}) @dc.dataclass class CertificateV2SignatureInfo(SignatureInfo): - validity_period: Optional[ValidityPeriod] = dc.field( + validity_period: ValidityPeriod | None = dc.field( default=None, metadata={ 'tlv_type': SecurityV2TypeNumber.VALIDITY_PERIOD}) - additional_description: Optional[AdditionalDescription] = dc.field( + additional_description: AdditionalDescription | None = dc.field( default=None, metadata={ 'tlv_type': SecurityV2TypeNumber.ADDITIONAL_DESCRIPTION}) @dc.dataclass class CertificateV2Value(DataPacketValue): - signature_info: Optional[CertificateV2SignatureInfo] = dc.field( + signature_info: CertificateV2SignatureInfo | None = dc.field( default=None, metadata={ 'tlv_type': TypeNumber.SIGNATURE_INFO, 'ignore_critical': True, @@ -109,13 +108,13 @@ class CertificateV2Value(DataPacketValue): @dc.dataclass class SafeBag: - certificate_v2: Optional[bytes] = dc.field( + certificate_v2: bytes | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.DATA}) # Keep the certificate as encoded bytes instead of a nested dataclass: # 1. The encoded length of CertificateV2 is unknown. # 2. Generally we already have an encoded certificate when exporting a # SafeBag. - encrypted_key_bag: Optional[bytes] = dc.field( + encrypted_key_bag: bytes | None = dc.field( default=None, metadata={ 'tlv_type': SecurityV2TypeNumber.ENCRYPTED_KEY_BAG}) diff --git a/src/ndn/app_support/svs/tlv.py b/src/ndn/app_support/svs/tlv.py index 5d08918..2f163f1 100644 --- a/src/ndn/app_support/svs/tlv.py +++ b/src/ndn/app_support/svs/tlv.py @@ -16,7 +16,6 @@ # limitations under the License. # ----------------------------------------------------------------------------- import dataclasses as dc -from typing import Optional from ... import encoding as enc @@ -26,7 +25,7 @@ @dc.dataclass class StateVecEntry: node_id: enc.NDNName = dc.field(default=None, metadata={'tlv_type': enc.Name.TYPE_NAME}) - seq_no: Optional[int] = dc.field(default=None, metadata={'tlv_type': 0xcc}) + seq_no: int | None = dc.field(default=None, metadata={'tlv_type': 0xcc}) @dc.dataclass @@ -36,21 +35,21 @@ class StateVec: @dc.dataclass class StateVecWrapper: - val: Optional[StateVec] = dc.field(default=None, metadata={'tlv_type': 0xc9}) + val: StateVec | None = dc.field(default=None, metadata={'tlv_type': 0xc9}) @dc.dataclass class MappingEntry: - seq_no: Optional[int] = dc.field(default=None, metadata={'tlv_type': 0xcc}) + seq_no: int | None = dc.field(default=None, metadata={'tlv_type': 0xcc}) app_name: enc.NDNName = dc.field(default=None, metadata={'tlv_type': enc.Name.TYPE_NAME}) @dc.dataclass class MappingData: node_id: enc.NDNName = dc.field(default=None, metadata={'tlv_type': enc.Name.TYPE_NAME}) - entries: Optional[MappingEntry] = dc.field(default=None, metadata={'tlv_type': 0xce}) + entries: MappingEntry | None = dc.field(default=None, metadata={'tlv_type': 0xce}) @dc.dataclass class MappingDataWrapper: - val: Optional[MappingEntry] = dc.field(default=None, metadata={'tlv_type': 0xcd}) + val: MappingEntry | None = dc.field(default=None, metadata={'tlv_type': 0xcd}) diff --git a/src/ndn/encoding/ndn_format_0_3.py b/src/ndn/encoding/ndn_format_0_3.py index 0b5f513..fafa13d 100644 --- a/src/ndn/encoding/ndn_format_0_3.py +++ b/src/ndn/encoding/ndn_format_0_3.py @@ -4,7 +4,6 @@ # ----------------------------------------------------------------------------- """NDN Packet Format v0.3 models using the dataclass TLV API.""" import dataclasses as dc -from typing import Optional from .name import Name, Component from .signer import Signer @@ -74,22 +73,22 @@ class SignatureType: class KeyLocator: name: NDNName = dc.field( default=None, metadata={'tlv_type': TypeNumber.NAME}) - key_digest: Optional[bytes] = dc.field( + key_digest: bytes | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.KEY_DIGEST}) @dc.dataclass class SignatureInfo: - signature_type: Optional[int] = dc.field( + signature_type: int | None = dc.field( default=None, metadata={ 'tlv_type': TypeNumber.SIGNATURE_TYPE, 'fixed_len': 1}) - key_locator: Optional[KeyLocator] = dc.field( + key_locator: KeyLocator | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.KEY_LOCATOR}) - signature_nonce: Optional[int] = dc.field( + signature_nonce: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.SIGNATURE_NONCE}) - signature_time: Optional[int] = dc.field( + signature_time: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.SIGNATURE_TIME}) - signature_seq_num: Optional[int] = dc.field( + signature_seq_num: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.SIGNATURE_SEQ_NUM}) @@ -121,24 +120,24 @@ class InterestPacketValue: default=False, metadata={'tlv_type': TypeNumber.CAN_BE_PREFIX}) must_be_fresh: bool = dc.field( default=False, metadata={'tlv_type': TypeNumber.MUST_BE_FRESH}) - forwarding_hint: Optional[Links] = dc.field( + forwarding_hint: Links | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.FORWARDING_HINT}) - nonce: Optional[int] = dc.field(default=None, metadata={ + nonce: int | None = dc.field(default=None, metadata={ 'tlv_type': TypeNumber.NONCE, 'fixed_len': 4}) - lifetime: Optional[int] = dc.field( + lifetime: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.INTEREST_LIFETIME}) - hop_limit: Optional[int] = dc.field(default=None, metadata={ + hop_limit: int | None = dc.field(default=None, metadata={ 'tlv_type': TypeNumber.HOP_LIMIT, 'fixed_len': 1}) _sig_cover_start: None = dc.field( default=None, metadata={'field_type': 'offset_marker'}) _digest_cover_start: None = dc.field( default=None, metadata={'field_type': 'offset_marker'}) - application_parameters: Optional[bytes] = dc.field( + application_parameters: bytes | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.APPLICATION_PARAMETERS}) - signature_info: Optional[SignatureInfo] = dc.field( + signature_info: SignatureInfo | None = dc.field( default=None, metadata={ 'tlv_type': TypeNumber.INTEREST_SIGNATURE_INFO}) - signature_value: Optional[bytes] = dc.field(default=None, metadata={ + signature_value: bytes | None = dc.field(default=None, metadata={ 'tlv_type': TypeNumber.INTEREST_SIGNATURE_VALUE, 'field_type': 'sig_value', 'cover_start': '_sig_cover_start', @@ -151,23 +150,23 @@ class InterestPacketValue: @dc.dataclass class InterestPacket: - interest: Optional[InterestPacketValue] = dc.field( + interest: InterestPacketValue | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.INTEREST}) @dc.dataclass(init=False) class MetaInfo: - content_type: Optional[int] = dc.field( + content_type: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.CONTENT_TYPE}) - freshness_period: Optional[int] = dc.field( + freshness_period: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.FRESHNESS_PERIOD}) - final_block_id: Optional[bytes] = dc.field( + final_block_id: bytes | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.FINAL_BLOCK_ID}) def __init__(self, - content_type: Optional[int] = ContentType.BLOB, - freshness_period: Optional[int] = None, - final_block_id: Optional[BinaryStr] = None): + content_type: int | None = ContentType.BLOB, + freshness_period: int | None = None, + final_block_id: BinaryStr | None = None): self.content_type = content_type self.freshness_period = freshness_period self.final_block_id = final_block_id @@ -187,13 +186,13 @@ class DataPacketValue: default=None, metadata={'field_type': 'offset_marker'}) name: NDNName = dc.field( default='/', metadata={'tlv_type': TypeNumber.NAME}) - meta_info: Optional[MetaInfo] = dc.field( + meta_info: MetaInfo | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.META_INFO}) - content: Optional[bytes] = dc.field( + content: bytes | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.CONTENT}) - signature_info: Optional[SignatureInfo] = dc.field(default=None, metadata={ + signature_info: SignatureInfo | None = dc.field(default=None, metadata={ 'tlv_type': TypeNumber.SIGNATURE_INFO, 'ignore_critical': True}) - signature_value: Optional[bytes] = dc.field(default=None, metadata={ + signature_value: bytes | None = dc.field(default=None, metadata={ 'tlv_type': TypeNumber.SIGNATURE_VALUE, 'field_type': 'sig_value', 'cover_start': '_sig_cover_start', @@ -202,7 +201,7 @@ class DataPacketValue: @dc.dataclass class DataPacket: - data: Optional[DataPacketValue] = dc.field( + data: DataPacketValue | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.DATA}) @@ -210,9 +209,9 @@ class DataPacket: class InterestParam: can_be_prefix: bool = False must_be_fresh: bool = False - nonce: Optional[int] = None - lifetime: Optional[int] = 4000 - hop_limit: Optional[int] = None + nonce: int | None = None + lifetime: int | None = 4000 + hop_limit: int | None = None forwarding_hint: list[NonStrictName] = dc.field(default_factory=list) @staticmethod @@ -226,15 +225,15 @@ def from_dict(kwargs): @dc.dataclass class SignaturePtrs: - signature_info: Optional[SignatureInfo] = None + signature_info: SignatureInfo | None = None signature_covered_part: list[BinaryStr] = dc.field(default_factory=list) - signature_value_buf: Optional[BinaryStr] = None + signature_value_buf: BinaryStr | None = None digest_covered_part: list[BinaryStr] = dc.field(default_factory=list) - digest_value_buf: Optional[BinaryStr] = None + digest_value_buf: BinaryStr | None = None -Interest = tuple[FormalName, InterestParam, Optional[BinaryStr], SignaturePtrs] -Data = tuple[FormalName, MetaInfo, Optional[BinaryStr], SignaturePtrs] +Interest = tuple[FormalName, InterestParam, BinaryStr | None, SignaturePtrs] +Data = tuple[FormalName, MetaInfo, BinaryStr | None, SignaturePtrs] def _wrap_tlv(type_num: int, value: BinaryStr) -> VarBinaryStr: @@ -252,8 +251,8 @@ def _wrap_tlv(type_num: int, value: BinaryStr) -> VarBinaryStr: def make_interest(name: NonStrictName, interest_param: InterestParam, - app_param: Optional[BinaryStr] = None, - signer: Optional[Signer] = None, + app_param: BinaryStr | None = None, + signer: Signer | None = None, need_final_name: bool = False): value = InterestPacketValue( name=name, @@ -289,8 +288,8 @@ def make_interest(name: NonStrictName, def make_data(name: NonStrictName, meta_info: MetaInfo, - content: Optional[BinaryStr] = None, - signer: Optional[Signer] = None) -> VarBinaryStr: + content: BinaryStr | None = None, + signer: Signer | None = None) -> VarBinaryStr: value = DataPacketValue(name=name, meta_info=meta_info, content=content) if signer is not None: value.signature_info = SignatureInfo() diff --git a/src/ndn/encoding/ndn_format_0_3_2017.py b/src/ndn/encoding/ndn_format_0_3_2017.py index 45a3933..db57021 100644 --- a/src/ndn/encoding/ndn_format_0_3_2017.py +++ b/src/ndn/encoding/ndn_format_0_3_2017.py @@ -4,7 +4,6 @@ # ----------------------------------------------------------------------------- """NDN Packet Format v0.3 (2017 forwarding hint) dataclass models.""" import dataclasses as dc -from typing import Optional from .name import Name, Component from .signer import Signer @@ -73,22 +72,22 @@ class SignatureType: class KeyLocator: name: NDNName = dc.field( default=None, metadata={'tlv_type': TypeNumber.NAME}) - key_digest: Optional[bytes] = dc.field( + key_digest: bytes | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.KEY_DIGEST}) @dc.dataclass class SignatureInfo: - signature_type: Optional[int] = dc.field( + signature_type: int | None = dc.field( default=None, metadata={ 'tlv_type': TypeNumber.SIGNATURE_TYPE, 'fixed_len': 1}) - key_locator: Optional[KeyLocator] = dc.field( + key_locator: KeyLocator | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.KEY_LOCATOR}) - signature_nonce: Optional[int] = dc.field( + signature_nonce: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.SIGNATURE_NONCE}) - signature_time: Optional[int] = dc.field( + signature_time: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.SIGNATURE_TIME}) - signature_seq_num: Optional[int] = dc.field( + signature_seq_num: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.SIGNATURE_SEQ_NUM}) @@ -108,7 +107,7 @@ def write_signature_info(signer: Signer, signature_info: SignatureInfo) -> None: @dc.dataclass class Delegation: - preference: Optional[int] = dc.field( + preference: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.PREFERENCE}) delegation: NDNName = dc.field( default=None, metadata={'tlv_type': TypeNumber.NAME}) @@ -128,24 +127,24 @@ class InterestPacketValue: default=False, metadata={'tlv_type': TypeNumber.CAN_BE_PREFIX}) must_be_fresh: bool = dc.field( default=False, metadata={'tlv_type': TypeNumber.MUST_BE_FRESH}) - forwarding_hint: Optional[Links] = dc.field( + forwarding_hint: Links | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.FORWARDING_HINT}) - nonce: Optional[int] = dc.field(default=None, metadata={ + nonce: int | None = dc.field(default=None, metadata={ 'tlv_type': TypeNumber.NONCE, 'fixed_len': 4}) - lifetime: Optional[int] = dc.field( + lifetime: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.INTEREST_LIFETIME}) - hop_limit: Optional[int] = dc.field(default=None, metadata={ + hop_limit: int | None = dc.field(default=None, metadata={ 'tlv_type': TypeNumber.HOP_LIMIT, 'fixed_len': 1}) _sig_cover_start: None = dc.field( default=None, metadata={'field_type': 'offset_marker'}) _digest_cover_start: None = dc.field( default=None, metadata={'field_type': 'offset_marker'}) - application_parameters: Optional[bytes] = dc.field( + application_parameters: bytes | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.APPLICATION_PARAMETERS}) - signature_info: Optional[SignatureInfo] = dc.field( + signature_info: SignatureInfo | None = dc.field( default=None, metadata={ 'tlv_type': TypeNumber.INTEREST_SIGNATURE_INFO}) - signature_value: Optional[bytes] = dc.field(default=None, metadata={ + signature_value: bytes | None = dc.field(default=None, metadata={ 'tlv_type': TypeNumber.INTEREST_SIGNATURE_VALUE, 'field_type': 'sig_value', 'cover_start': '_sig_cover_start', @@ -158,23 +157,23 @@ class InterestPacketValue: @dc.dataclass class InterestPacket: - interest: Optional[InterestPacketValue] = dc.field( + interest: InterestPacketValue | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.INTEREST}) @dc.dataclass(init=False) class MetaInfo: - content_type: Optional[int] = dc.field( + content_type: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.CONTENT_TYPE}) - freshness_period: Optional[int] = dc.field( + freshness_period: int | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.FRESHNESS_PERIOD}) - final_block_id: Optional[bytes] = dc.field( + final_block_id: bytes | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.FINAL_BLOCK_ID}) def __init__(self, - content_type: Optional[int] = ContentType.BLOB, - freshness_period: Optional[int] = None, - final_block_id: Optional[BinaryStr] = None): + content_type: int | None = ContentType.BLOB, + freshness_period: int | None = None, + final_block_id: BinaryStr | None = None): self.content_type = content_type self.freshness_period = freshness_period self.final_block_id = final_block_id @@ -194,13 +193,13 @@ class DataPacketValue: default=None, metadata={'field_type': 'offset_marker'}) name: NDNName = dc.field( default='/', metadata={'tlv_type': TypeNumber.NAME}) - meta_info: Optional[MetaInfo] = dc.field( + meta_info: MetaInfo | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.META_INFO}) - content: Optional[bytes] = dc.field( + content: bytes | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.CONTENT}) - signature_info: Optional[SignatureInfo] = dc.field(default=None, metadata={ + signature_info: SignatureInfo | None = dc.field(default=None, metadata={ 'tlv_type': TypeNumber.SIGNATURE_INFO, 'ignore_critical': True}) - signature_value: Optional[bytes] = dc.field(default=None, metadata={ + signature_value: bytes | None = dc.field(default=None, metadata={ 'tlv_type': TypeNumber.SIGNATURE_VALUE, 'field_type': 'sig_value', 'cover_start': '_sig_cover_start', @@ -209,7 +208,7 @@ class DataPacketValue: @dc.dataclass class DataPacket: - data: Optional[DataPacketValue] = dc.field( + data: DataPacketValue | None = dc.field( default=None, metadata={'tlv_type': TypeNumber.DATA}) @@ -217,9 +216,9 @@ class DataPacket: class InterestParam: can_be_prefix: bool = False must_be_fresh: bool = False - nonce: Optional[int] = None - lifetime: Optional[int] = 4000 - hop_limit: Optional[int] = None + nonce: int | None = None + lifetime: int | None = 4000 + hop_limit: int | None = None forwarding_hint: list[tuple[int, NonStrictName]] = dc.field(default_factory=list) @staticmethod @@ -233,15 +232,15 @@ def from_dict(kwargs): @dc.dataclass class SignaturePtrs: - signature_info: Optional[SignatureInfo] = None + signature_info: SignatureInfo | None = None signature_covered_part: list[BinaryStr] = dc.field(default_factory=list) - signature_value_buf: Optional[BinaryStr] = None + signature_value_buf: BinaryStr | None = None digest_covered_part: list[BinaryStr] = dc.field(default_factory=list) - digest_value_buf: Optional[BinaryStr] = None + digest_value_buf: BinaryStr | None = None -Interest = tuple[FormalName, InterestParam, Optional[BinaryStr], SignaturePtrs] -Data = tuple[FormalName, MetaInfo, Optional[BinaryStr], SignaturePtrs] +Interest = tuple[FormalName, InterestParam, BinaryStr | None, SignaturePtrs] +Data = tuple[FormalName, MetaInfo, BinaryStr | None, SignaturePtrs] def _wrap_tlv(type_num: int, value: BinaryStr) -> VarBinaryStr: @@ -259,8 +258,8 @@ def _wrap_tlv(type_num: int, value: BinaryStr) -> VarBinaryStr: def make_interest(name: NonStrictName, interest_param: InterestParam, - app_param: Optional[BinaryStr] = None, - signer: Optional[Signer] = None, + app_param: BinaryStr | None = None, + signer: Signer | None = None, need_final_name: bool = False): value = InterestPacketValue( name=name, @@ -298,8 +297,8 @@ def make_interest(name: NonStrictName, def make_data(name: NonStrictName, meta_info: MetaInfo, - content: Optional[BinaryStr] = None, - signer: Optional[Signer] = None) -> VarBinaryStr: + content: BinaryStr | None = None, + signer: Signer | None = None) -> VarBinaryStr: value = DataPacketValue(name=name, meta_info=meta_info, content=content) if signer is not None: value.signature_info = SignatureInfo() diff --git a/src/ndn/encoding/ndnlp_v2.py b/src/ndn/encoding/ndnlp_v2.py index 35f7a1f..8678740 100644 --- a/src/ndn/encoding/ndnlp_v2.py +++ b/src/ndn/encoding/ndnlp_v2.py @@ -4,7 +4,6 @@ # ----------------------------------------------------------------------------- """NDNLPv2 models using the dataclass TLV API.""" import dataclasses as dc -from typing import Optional from .tlv_model import DecodeError from .tlv_model import tlv_encode, tlv_parse @@ -48,57 +47,57 @@ class NackReason: @dc.dataclass class NetworkNack: - nack_reason: Optional[int] = dc.field( + nack_reason: int | None = dc.field( default=None, metadata={'tlv_type': LpTypeNumber.NACK_REASON}) @dc.dataclass class CachePolicy: - cache_policy_type: Optional[int] = dc.field( + cache_policy_type: int | None = dc.field( default=None, metadata={'tlv_type': LpTypeNumber.CACHE_POLICY_TYPE}) @dc.dataclass class LpPacketValue: - frag_index: Optional[int] = dc.field( + frag_index: int | None = dc.field( default=None, metadata={'tlv_type': LpTypeNumber.FRAG_INDEX}) - frag_count: Optional[int] = dc.field( + frag_count: int | None = dc.field( default=None, metadata={'tlv_type': LpTypeNumber.FRAG_COUNT}) - pit_token: Optional[bytes] = dc.field( + pit_token: bytes | None = dc.field( default=None, metadata={'tlv_type': LpTypeNumber.PIT_TOKEN}) - nack: Optional[NetworkNack] = dc.field( + nack: NetworkNack | None = dc.field( default=None, metadata={ 'tlv_type': LpTypeNumber.NACK, 'ignore_critical': False}) - incoming_face_id: Optional[int] = dc.field( + incoming_face_id: int | None = dc.field( default=None, metadata={'tlv_type': LpTypeNumber.INCOMING_FACE_ID}) - next_hop_face_id: Optional[int] = dc.field( + next_hop_face_id: int | None = dc.field( default=None, metadata={'tlv_type': LpTypeNumber.NEXT_HOP_FACE_ID}) - cache_policy: Optional[CachePolicy] = dc.field( + cache_policy: CachePolicy | None = dc.field( default=None, metadata={ 'tlv_type': LpTypeNumber.CACHE_POLICY, 'ignore_critical': False}) - congestion_mark: Optional[int] = dc.field( + congestion_mark: int | None = dc.field( default=None, metadata={'tlv_type': LpTypeNumber.CONGESTION_MARK}) - tx_sequence: Optional[bytes] = dc.field( + tx_sequence: bytes | None = dc.field( default=None, metadata={'tlv_type': LpTypeNumber.TX_SEQUENCE}) - ack: Optional[bytes] = dc.field( + ack: bytes | None = dc.field( default=None, metadata={'tlv_type': LpTypeNumber.ACK}) non_discovery: bool = dc.field( default=False, metadata={'tlv_type': LpTypeNumber.NON_DISCOVERY}) - prefix_announcement: Optional[bytes] = dc.field( + prefix_announcement: bytes | None = dc.field( default=None, metadata={'tlv_type': LpTypeNumber.PREFIX_ANNOUNCEMENT}) - fragment: Optional[bytes] = dc.field( + fragment: bytes | None = dc.field( default=None, metadata={'tlv_type': LpTypeNumber.FRAGMENT}) @dc.dataclass class LpPacket: - lp_packet: Optional[LpPacketValue] = dc.field( + lp_packet: LpPacketValue | None = dc.field( default=None, metadata={'tlv_type': LpTypeNumber.LP_PACKET}) def parse_lp_packet(wire: BinaryStr, with_tl: bool = True - ) -> tuple[Optional[int], Optional[BinaryStr]]: + ) -> tuple[int | None, BinaryStr | None]: ret = parse_lp_packet_v2(wire, with_tl) reason = ret.nack.nack_reason if ret.nack is not None else None return reason, ret.fragment @@ -115,7 +114,7 @@ def parse_lp_packet_v2(wire: BinaryStr, with_tl: bool = True) -> LpPacketValue: def parse_network_nack( wire: BinaryStr, - with_tl: bool = True) -> tuple[Optional[int], Optional[BinaryStr]]: + with_tl: bool = True) -> tuple[int | None, BinaryStr | None]: if with_tl: wire = parse_and_check_tl(wire, LpTypeNumber.LP_PACKET) ret = tlv_parse(LpPacketValue, wire, ignore_critical=True) diff --git a/src/ndn/encoding/tlv_model.py b/src/ndn/encoding/tlv_model.py index 86e2ff7..dca581d 100644 --- a/src/ndn/encoding/tlv_model.py +++ b/src/ndn/encoding/tlv_model.py @@ -16,6 +16,8 @@ # limitations under the License. # ----------------------------------------------------------------------------- """Dataclass-based TLV encoding and decoding.""" +from __future__ import annotations + import dataclasses import struct import typing @@ -54,7 +56,7 @@ class NDNName: name: NDNName = field(default=None, metadata={'tlv_type': 0x07}) # repeated Names: - names: List[NDNName] = field(default_factory=list, + names: list[NDNName] = field(default_factory=list, metadata={'tlv_type': 0x07}) The runtime value is :any:`FormalName`, a list of encoded components. @@ -66,7 +68,7 @@ class NDNName: # --------------------------------------------------------------------------- def _unwrap_optional(annotation): - """Return T for Optional[T] = Union[T, None]; otherwise return unchanged.""" + """Return T for T | None; otherwise return the annotation unchanged.""" if typing.get_origin(annotation) in (typing.Union, UnionType): args = [a for a in typing.get_args(annotation) if a is not type(None)] if len(args) == 1: @@ -117,14 +119,14 @@ def _infer_kind(annotation, metadata: dict) -> str: def _element_annotation(annotation): - """Extract T from List[T]; falls back to bytes.""" + """Extract T from list[T]; fall back to bytes.""" annotation = _unwrap_optional(annotation) args = typing.get_args(annotation) return args[0] if args else bytes def _map_annotations(annotation): - """Extract (K, V) from Dict[K, V]; falls back to (str, bytes).""" + """Extract (K, V) from dict[K, V]; fall back to (str, bytes).""" annotation = _unwrap_optional(annotation) args = typing.get_args(annotation) if len(args) == 2: @@ -156,11 +158,11 @@ class _FieldSpec: kind: str metadata: typing.Mapping annotation: typing.Any - tlv_type: typing.Optional[int] - enum_cls: typing.Optional[type] = None - elem: typing.Optional['_FieldSpec'] = None - key: typing.Optional['_FieldSpec'] = None - val: typing.Optional['_FieldSpec'] = None + tlv_type: int | None + enum_cls: type | None = None + elem: _FieldSpec | None = None + key: _FieldSpec | None = None + val: _FieldSpec | None = None def _make_spec(name: str, annotation, metadata) -> _FieldSpec: @@ -179,7 +181,7 @@ def _make_spec(name: str, annotation, metadata) -> _FieldSpec: enum_cls, elem, key, val) -_SCHEMA_CACHE: 'weakref.WeakKeyDictionary[type, tuple[_FieldSpec, ...]]' = weakref.WeakKeyDictionary() +_SCHEMA_CACHE: weakref.WeakKeyDictionary[type, tuple[_FieldSpec, ...]] = weakref.WeakKeyDictionary() def _get_schema(cls) -> tuple[_FieldSpec, ...]: diff --git a/src/ndn/platform/osx.py b/src/ndn/platform/osx.py index fd840c9..2c05035 100644 --- a/src/ndn/platform/osx.py +++ b/src/ndn/platform/osx.py @@ -19,6 +19,7 @@ import sys import asyncio as aio from ctypes import cdll, c_void_p, c_ubyte, POINTER, c_int32, c_ulong, c_uint16 +from typing import Self from .general import Platform if sys.platform == 'darwin': from ..contrib.cocoapy import cf, CFIndex, CFRange, CFAllocatorRef @@ -101,7 +102,7 @@ def __getattr__(self, item): def __setattr__(self, key, value): self._dict[key] = value - def __enter__(self): + def __enter__(self) -> Self: if len(self._dict) > 0: raise RuntimeError('Re-enter a ReleaseGuard') return self diff --git a/src/ndn/platform/windows.py b/src/ndn/platform/windows.py index 09bb0a9..2ef92f6 100644 --- a/src/ndn/platform/windows.py +++ b/src/ndn/platform/windows.py @@ -19,6 +19,7 @@ import socket import asyncio as aio import ctypes as c +from typing import Self from .general import Platform @@ -178,11 +179,11 @@ def __getattr__(self, idx): def __setattr__(self, idx, value): self._list[idx] = value - def __iadd__(self, defer): + def __iadd__(self, defer) -> Self: self._list.append(defer) return self - def __enter__(self): + def __enter__(self) -> Self: if len(self._list) > 0: raise RuntimeError('Re-enter a ReleaseGuard') return self diff --git a/src/ndn/security/keychain/keychain_sqlite3.py b/src/ndn/security/keychain/keychain_sqlite3.py index fa8f3f7..1e3cd0d 100644 --- a/src/ndn/security/keychain/keychain_sqlite3.py +++ b/src/ndn/security/keychain/keychain_sqlite3.py @@ -686,7 +686,7 @@ def new_key(self, id_name: NonStrictName, key_type: str = 'ec', **kwargs) -> Key :Keyword Arguments: + **key_size** (:class:`int`) - key size in bit. - + **key_id** (Union[:any:`BinaryStr`, :class:`str`]) - a one-Component ID of the Key. + + **key_id** (:any:`BinaryStr` | :class:`str`) - a one-Component ID of the Key. + **key_id_type** (:class:`str`) - the method to generate the ID if *key_id* is not specified. Can be ``random`` or ``sha256``. diff --git a/src/ndn/types.py b/src/ndn/types.py index ab5fae0..95cc0c1 100644 --- a/src/ndn/types.py +++ b/src/ndn/types.py @@ -96,7 +96,7 @@ class ValidationFailure(Exception): :ivar meta_info: the MetaInfo. :vartype meta_info: :any:`MetaInfo` :ivar content: the Content of Data. - :vartype content: Optional[:any:`BinaryStr`] + :vartype content: :any:`BinaryStr` | None :ivar sig_ptrs: the signature pointers of Data :vartype sig_ptrs: :any:`SignaturePtrs` :ivar result: the reason of failure. diff --git a/tests/encoding/tlv_model_test.py b/tests/encoding/tlv_model_test.py index 65db5a9..cc3eb20 100644 --- a/tests/encoding/tlv_model_test.py +++ b/tests/encoding/tlv_model_test.py @@ -362,7 +362,7 @@ class M: assert tlv_encode(M(name=None)) == b'' def test_repeated_names(self): - """List[NDNName] — multiple Name TLVs with the same type number.""" + """list[NDNName] — multiple Name TLVs with the same type number.""" @dataclass class M: names: list[NDNName] = field(default_factory=list,