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:
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.py b/src/ndn/app.py
index 6342c3d..a79fb2c 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: 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: :any:`BinaryStr` | None
+ :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: :any:`BinaryStr` | None
+ :param signer: the Signer used to sign the packet.
+ :type signer: :any:`Signer` | None
+ :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 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: :any:`Validator` | None
+ """
+ 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`
- :param content: the Content.
- :type content: Optional[:any:`BinaryStr`]
- :param kwargs: :ref:`label-keyword-arguments`.
- :return: TLV encoded Data packet.
"""
- 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.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.
- :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`]]]
-
- The following exception is raised by ``express_interest``:
+ :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: :any:`BinaryStr` | None
+ :param signer: Signer for Interest signing. This is required if `app_param` is specified.
+ :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`, :any:`BinaryStr` | None, :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 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: :any:`Validator` | None
+
+ :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()
- async def main_loop(self, after_start: Awaitable = None) -> bool:
+ 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/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/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/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/binary.py b/src/ndn/app_support/light_versec/binary.py
index 8f88df8..32b2714 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 Self
+
+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: bytes | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.COMPONENT_VALUE})
# Referring to a previous matched pattern
- tag = enc.UintField(TypeNumber.PATTERN_TAG)
+ tag: int | None = 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: 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})
-class ConstraintOption(enc.TlvModel):
+@dc.dataclass
+class ConstraintOption:
# Equal to a given NameComponent value
- value = enc.BytesField(TypeNumber.COMPONENT_VALUE)
+ value: bytes | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.COMPONENT_VALUE})
# Equal to another pattern
- tag = enc.UintField(TypeNumber.PATTERN_TAG)
+ tag: int | None = 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: UserFnCall | None = 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: int | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.NODE_ID})
+ 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})
+
+
+@dc.dataclass
+class ValueEdge:
+ dest: int | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.NODE_ID})
+ value: bytes | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.COMPONENT_VALUE})
+
+
+@dc.dataclass
+class Node:
+ id: int | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.NODE_ID})
+ 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})
+ 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: int | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.PATTERN_TAG})
+ ident: str | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.IDENTIFIER})
+
+
+@dc.dataclass
+class LvsModel:
+ version: int | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.VERSION})
+ start_id: int | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.NODE_ID})
+ 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})
+ 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) -> Self:
+ return tlv_parse(cls, wire)
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/nfd_mgmt.py b/src/ndn/app_support/nfd_mgmt.py
index a7b6989..0dcf8a2 100644
--- a/src/ndn/app_support/nfd_mgmt.py
+++ b/src/ndn/app_support/nfd_mgmt.py
@@ -15,13 +15,16 @@
# 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 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 +65,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: 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: ControlParametersValue | None = _tlv(0x68)
+
+
+@dc.dataclass
+class ControlResponse:
+ status_code: int | None = _tlv(0x66)
+ status_text: str | None = _tlv(0x67)
+ body: ControlParametersValue | None = _tlv(0x68)
+
+
+@dc.dataclass
+class FaceEventNotificationValue:
+ 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: FaceEventNotificationValue | None = _tlv(0xc0)
+
+
+@dc.dataclass
+class GeneralStatus:
+ 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 = 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: 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)
+
-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: 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)
-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: 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)
-class FaceQueryFilter(TlvModel):
- face_query_filter = ModelField(0x96, FaceQueryFilterValue)
+@dc.dataclass
+class FaceQueryFilter:
+ face_query_filter: FaceQueryFilterValue | None = _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: 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)
-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: int | None = _tlv(0x69)
+ cost: int | None = _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: Strategy | None = _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: 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):
@@ -240,8 +285,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 +313,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/security_v2.py b/src/ndn/app_support/security_v2.py
index b382b33..1587015 100644
--- a/src/ndn/app_support/security_v2.py
+++ b/src/ndn/app_support/security_v2.py
@@ -15,12 +15,26 @@
# 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 ..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 +55,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: bytes | None = dc.field(
+ default=None, metadata={
+ 'tlv_type': SecurityV2TypeNumber.DESCRIPTION_KEY})
+ description_value: bytes | None = 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: AdditionalDescription | None = 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: bytes | None = dc.field(
+ default=None, metadata={'tlv_type': SecurityV2TypeNumber.NOT_BEFORE})
+ not_after: bytes | None = 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: ValidityPeriod | None = dc.field(
+ default=None, metadata={
+ 'tlv_type': SecurityV2TypeNumber.VALIDITY_PERIOD})
+ additional_description: AdditionalDescription | None = 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: CertificateV2SignatureInfo | None = dc.field(
+ default=None, metadata={
+ 'tlv_type': TypeNumber.SIGNATURE_INFO,
+ 'ignore_critical': True,
+ })
+
+
+@dc.dataclass
+class SafeBag:
+ 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: bytes | None = dc.field(
+ default=None, metadata={
+ 'tlv_type': SecurityV2TypeNumber.ENCRYPTED_KEY_BAG})
-class SafeBag(TlvModel):
- certificate_v2 = BytesField(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)
+@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 +171,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/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/app_support/svs/sync.py b/src/ndn/app_support/svs/sync.py
index 9012326..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
@@ -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..2f163f1 100644
--- a/src/ndn/app_support/svs/tlv.py
+++ b/src/ndn/app_support/svs/tlv.py
@@ -15,34 +15,41 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# -----------------------------------------------------------------------------
+import dataclasses as dc
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: int | None = 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: StateVec | None = 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: 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})
-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: MappingEntry | None = dc.field(default=None, metadata={'tlv_type': 0xce})
-class MappingDataWrapper(enc.TlvModel):
- val = enc.ModelField(0xcd, MappingEntry)
+@dc.dataclass
+class MappingDataWrapper:
+ val: MappingEntry | None = dc.field(default=None, metadata={'tlv_type': 0xcd})
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 2da16c3..e816e9b 100644
--- a/src/ndn/bin/nfdc/cmd_get_face.py
+++ b/src/ndn/bin/nfdc/cmd_get_face.py
@@ -16,10 +16,11 @@
# 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
-from .utils import express_interest
+from .utils import express_command
def add_parser(subparsers):
@@ -34,8 +35,8 @@ def execute(args: argparse.Namespace):
async def list_face():
try:
- data = await express_interest(app, "/localhost/nfd/faces/list")
- msg = FaceStatusMsg.parse(data)
+ 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}')
print(f'{"------":7}{"---------":<30}\t{"--------":<30}')
@@ -46,14 +47,14 @@ 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:
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..fdd0fa8 100644
--- a/src/ndn/bin/nfdc/cmd_get_route.py
+++ b/src/ndn/bin/nfdc/cmd_get_route.py
@@ -16,10 +16,11 @@
# 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
-from .utils import express_interest
+from .utils import express_command
def add_parser(subparsers):
@@ -35,10 +36,10 @@ 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)
- rib_data = await express_interest(app, "/localhost/nfd/rib/list")
- rib_msg = RibStatus.parse(rib_data)
+ fib_data = await express_command(app, "/localhost/nfd/fib/list")
+ fib_msg = tlv_parse(FibStatus, fib_data)
+ 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)')
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..51bd168 100644
--- a/src/ndn/bin/nfdc/cmd_get_status.py
+++ b/src/ndn/bin/nfdc/cmd_get_status.py
@@ -17,9 +17,10 @@
# -----------------------------------------------------------------------------
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
+from .utils import express_command
def add_parser(subparsers):
@@ -32,9 +33,9 @@ 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 = 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..1a67dd3 100644
--- a/src/ndn/bin/nfdc/cmd_get_strategy.py
+++ b/src/ndn/bin/nfdc/cmd_get_strategy.py
@@ -16,10 +16,11 @@
# 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
-from .utils import express_interest
+from .utils import express_command
def add_parser(subparsers):
@@ -35,8 +36,8 @@ def execute(args: argparse.Namespace):
async def list_strategy():
try:
- data = await express_interest(app, "/localhost/nfd/strategy-choice/list")
- msg = StrategyChoiceMsg.parse(data)
+ 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)
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..2329fa3 100644
--- a/src/ndn/bin/nfdc/cmd_new_face.py
+++ b/src/ndn/bin/nfdc/cmd_new_face.py
@@ -16,9 +16,9 @@
# 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
+from .utils import express_command
def add_parser(subparsers):
@@ -38,8 +38,8 @@ def execute(args: argparse.Namespace):
uri = uri + ":6363"
async def create_face():
- cmd = make_command_v2('faces', 'create', uri=uri.encode())
- res = await express_interest(app, cmd)
+ cmd = make_command_v2('faces', 'create', uri=uri)
+ 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 b929fe2..ca69a35 100644
--- a/src/ndn/bin/nfdc/cmd_new_route.py
+++ b/src/ndn/bin/nfdc/cmd_new_route.py
@@ -16,9 +16,9 @@
# 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
+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 b615071..1a3cfee 100644
--- a/src/ndn/bin/nfdc/cmd_remove_face.py
+++ b/src/ndn/bin/nfdc/cmd_remove_face.py
@@ -16,11 +16,12 @@
# 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, \
make_command_v2
-from .utils import express_interest
+from .utils import express_command
def add_parser(subparsers):
@@ -37,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"]}')
@@ -49,14 +50,14 @@ 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:
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/src/ndn/bin/nfdc/cmd_remove_route.py b/src/ndn/bin/nfdc/cmd_remove_route.py
index 6a44375..6860a92 100644
--- a/src/ndn/bin/nfdc/cmd_remove_route.py
+++ b/src/ndn/bin/nfdc/cmd_remove_route.py
@@ -16,9 +16,9 @@
# 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
+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 3fafeca..633ff88 100644
--- a/src/ndn/bin/nfdc/cmd_remove_strategy.py
+++ b/src/ndn/bin/nfdc/cmd_remove_strategy.py
@@ -16,9 +16,9 @@
# 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
+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 d617690..2694022 100644
--- a/src/ndn/bin/nfdc/cmd_set_strategy.py
+++ b/src/ndn/bin/nfdc/cmd_set_strategy.py
@@ -16,9 +16,9 @@
# 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
+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 ce086de..59b5e59 100644
--- a/src/ndn/bin/nfdc/utils.py
+++ b/src/ndn/bin/nfdc/utils.py
@@ -15,12 +15,12 @@
# 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
-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/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/encoding/ndn_format_0_3.py b/src/ndn/encoding/ndn_format_0_3.py
index a0d773d..fafa13d 100644
--- a/src/ndn/encoding/ndn_format_0_3.py
+++ b/src/ndn/encoding/ndn_format_0_3.py
@@ -1,51 +1,31 @@
# -----------------------------------------------------------------------------
# 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 .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 +35,6 @@ class TypeNumber:
APPLICATION_PARAMETERS = 0x24
INTEREST_SIGNATURE_INFO = 0x2c
INTEREST_SIGNATURE_VALUE = 0x2e
-
META_INFO = 0x14
CONTENT = 0x15
SIGNATURE_INFO = 0x16
@@ -63,31 +42,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 +60,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,210 +69,144 @@ 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: bytes | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.KEY_DIGEST})
+
+
+@dc.dataclass
+class SignatureInfo:
+ signature_type: int | None = dc.field(
+ default=None, metadata={
+ 'tlv_type': TypeNumber.SIGNATURE_TYPE, 'fixed_len': 1})
+ key_locator: KeyLocator | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.KEY_LOCATOR})
+ signature_nonce: int | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.SIGNATURE_NONCE})
+ signature_time: int | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.SIGNATURE_TIME})
+ signature_seq_num: int | None = 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: Links | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.FORWARDING_HINT})
+ nonce: int | None = dc.field(default=None, metadata={
+ 'tlv_type': TypeNumber.NONCE, 'fixed_len': 4})
+ lifetime: int | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.INTEREST_LIFETIME})
+ 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: bytes | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.APPLICATION_PARAMETERS})
+ signature_info: SignatureInfo | None = dc.field(
+ default=None, metadata={
+ 'tlv_type': TypeNumber.INTEREST_SIGNATURE_INFO})
+ signature_value: bytes | None = 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: InterestPacketValue | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.INTEREST})
+
+
+@dc.dataclass(init=False)
+class MetaInfo:
+ content_type: int | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.CONTENT_TYPE})
+ freshness_period: int | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.FRESHNESS_PERIOD})
+ final_block_id: bytes | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.FINAL_BLOCK_ID})
def __init__(self,
- content_type: int = ContentType.BLOB,
+ content_type: int | None = ContentType.BLOB,
freshness_period: int | None = None,
- final_block_id: BinaryStr = None):
+ final_block_id: BinaryStr | None = 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: MetaInfo | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.META_INFO})
+ content: bytes | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.CONTENT})
+ signature_info: SignatureInfo | None = dc.field(default=None, metadata={
+ 'tlv_type': TypeNumber.SIGNATURE_INFO, 'ignore_critical': True})
+ signature_value: bytes | None = 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: DataPacketValue | None = 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
@@ -331,36 +216,19 @@ class InterestParam:
@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`
-
- :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_covered_part: list[BinaryStr] = dc.field(default_factory=list)
signature_value_buf: BinaryStr | None = None
- digest_covered_part: list[BinaryStr] | None = dc.field(default_factory=list)
+ digest_covered_part: list[BinaryStr] = dc.field(default_factory=list)
digest_value_buf: BinaryStr | None = None
@@ -368,143 +236,107 @@ class 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,
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
+ 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..db57021 100644
--- a/src/ndn/encoding/ndn_format_0_3_2017.py
+++ b/src/ndn/encoding/ndn_format_0_3_2017.py
@@ -1,51 +1,31 @@
# -----------------------------------------------------------------------------
# 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 .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 +35,6 @@ class TypeNumber:
APPLICATION_PARAMETERS = 0x24
INTEREST_SIGNATURE_INFO = 0x2c
INTEREST_SIGNATURE_VALUE = 0x2e
-
META_INFO = 0x14
CONTENT = 0x15
SIGNATURE_INFO = 0x16
@@ -63,31 +42,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 +60,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,215 +68,152 @@ 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: bytes | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.KEY_DIGEST})
+
+
+@dc.dataclass
+class SignatureInfo:
+ signature_type: int | None = dc.field(
+ default=None, metadata={
+ 'tlv_type': TypeNumber.SIGNATURE_TYPE, 'fixed_len': 1})
+ key_locator: KeyLocator | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.KEY_LOCATOR})
+ signature_nonce: int | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.SIGNATURE_NONCE})
+ signature_time: int | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.SIGNATURE_TIME})
+ signature_seq_num: int | None = 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: int | None = 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: Links | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.FORWARDING_HINT})
+ nonce: int | None = dc.field(default=None, metadata={
+ 'tlv_type': TypeNumber.NONCE, 'fixed_len': 4})
+ lifetime: int | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.INTEREST_LIFETIME})
+ 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: bytes | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.APPLICATION_PARAMETERS})
+ signature_info: SignatureInfo | None = dc.field(
+ default=None, metadata={
+ 'tlv_type': TypeNumber.INTEREST_SIGNATURE_INFO})
+ signature_value: bytes | None = 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: InterestPacketValue | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.INTEREST})
+
+
+@dc.dataclass(init=False)
+class MetaInfo:
+ content_type: int | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.CONTENT_TYPE})
+ freshness_period: int | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.FRESHNESS_PERIOD})
+ final_block_id: bytes | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.FINAL_BLOCK_ID})
def __init__(self,
- content_type: int = ContentType.BLOB,
+ content_type: int | None = ContentType.BLOB,
freshness_period: int | None = None,
- final_block_id: BinaryStr = None):
+ final_block_id: BinaryStr | None = 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
+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: MetaInfo | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.META_INFO})
+ content: bytes | None = dc.field(
+ default=None, metadata={'tlv_type': TypeNumber.CONTENT})
+ signature_info: SignatureInfo | None = dc.field(default=None, metadata={
+ 'tlv_type': TypeNumber.SIGNATURE_INFO, 'ignore_critical': True})
+ signature_value: bytes | None = dc.field(default=None, metadata={
+ 'tlv_type': TypeNumber.SIGNATURE_VALUE,
+ 'field_type': 'sig_value',
+ 'cover_start': '_sig_cover_start',
+ })
- :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: DataPacketValue | None = 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
@@ -335,36 +223,19 @@ class InterestParam:
@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`
-
- :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_covered_part: list[BinaryStr] = dc.field(default_factory=list)
signature_value_buf: BinaryStr | None = None
- digest_covered_part: list[BinaryStr] | None = dc.field(default_factory=list)
+ digest_covered_part: list[BinaryStr] = dc.field(default_factory=list)
digest_value_buf: BinaryStr | None = None
@@ -372,144 +243,111 @@ class 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,
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
+ 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..8678740 100644
--- a/src/ndn/encoding/ndnlp_v2.py
+++ b/src/ndn/encoding/ndnlp_v2.py
@@ -1,26 +1,20 @@
# -----------------------------------------------------------------------------
# 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 .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 +45,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: int | None = dc.field(
+ default=None, metadata={'tlv_type': LpTypeNumber.NACK_REASON})
+
+
+@dc.dataclass
+class CachePolicy:
+ cache_policy_type: int | None = dc.field(
+ default=None, metadata={'tlv_type': LpTypeNumber.CACHE_POLICY_TYPE})
+
+
+@dc.dataclass
+class LpPacketValue:
+ frag_index: int | None = dc.field(
+ default=None, metadata={'tlv_type': LpTypeNumber.FRAG_INDEX})
+ frag_count: int | None = dc.field(
+ default=None, metadata={'tlv_type': LpTypeNumber.FRAG_COUNT})
+ pit_token: bytes | None = dc.field(
+ default=None, metadata={'tlv_type': LpTypeNumber.PIT_TOKEN})
+ nack: NetworkNack | None = dc.field(
+ default=None, metadata={
+ 'tlv_type': LpTypeNumber.NACK, 'ignore_critical': False})
+ incoming_face_id: int | None = dc.field(
+ default=None, metadata={'tlv_type': LpTypeNumber.INCOMING_FACE_ID})
+ next_hop_face_id: int | None = dc.field(
+ default=None, metadata={'tlv_type': LpTypeNumber.NEXT_HOP_FACE_ID})
+ cache_policy: CachePolicy | None = dc.field(
+ default=None, metadata={
+ 'tlv_type': LpTypeNumber.CACHE_POLICY, 'ignore_critical': False})
+ congestion_mark: int | None = dc.field(
+ default=None, metadata={'tlv_type': LpTypeNumber.CONGESTION_MARK})
+ tx_sequence: bytes | None = dc.field(
+ default=None, metadata={'tlv_type': LpTypeNumber.TX_SEQUENCE})
+ 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: bytes | None = dc.field(
+ default=None, metadata={'tlv_type': LpTypeNumber.PREFIX_ANNOUNCEMENT})
+ fragment: bytes | None = dc.field(
+ default=None, metadata={'tlv_type': LpTypeNumber.FRAGMENT})
+
+
+@dc.dataclass
+class LpPacket:
+ 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[int | None, BinaryStr | None]:
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[int | None, BinaryStr | None]:
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/src/ndn/encoding/tlv_model.py b/src/ndn/encoding/tlv_model.py
index 1cd0514..dca581d 100644
--- a/src/ndn/encoding/tlv_model.py
+++ b/src/ndn/encoding/tlv_model.py
@@ -15,1055 +15,916 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# -----------------------------------------------------------------------------
-import abc
+"""Dataclass-based TLV encoding and decoding."""
+from __future__ import annotations
+
+import dataclasses
import struct
+import typing
+import weakref
from enum import Enum, Flag
-from collections.abc import Iterable
-from functools import reduce
-from .tlv_type import BinaryStr, VarBinaryStr, is_binary_str
+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__ = ['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
+ """Raised for malformed or unrecognized critical TLV fields."""
-class IncludeBaseError(Exception):
- """
- Raised when IncludeBase is used to include a non-base class.
- """
- pass
+__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'})
-class IncludeBase:
- """
- Include all fields from a base class.
- """
- def __init__(self, base):
- self.base = base
+# ---------------------------------------------------------------------------
+# NDNName sentinel — used as a type annotation for NDN Name fields
+# ---------------------------------------------------------------------------
-class TlvModelMeta(abc.ABCMeta):
+class NDNName:
"""
- 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``.
+ Sentinel annotation type that marks a field as an NDN Name.
- :ivar name: The name of the field
- :vartype name: str
+ Use it for fields containing encoded NDN names::
- :ivar type_num: The Type number used in TLV encoding
- :vartype type_num: int
+ name: NDNName = field(default=None, metadata={'tlv_type': 0x07})
+ # repeated Names:
+ names: list[NDNName] = field(default_factory=list,
+ metadata={'tlv_type': 0x07})
- :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.
+ The runtime value is :any:`FormalName`, a list of encoded components.
"""
- 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.
+# ---------------------------------------------------------------------------
+# Annotation helpers
+# ---------------------------------------------------------------------------
- .. code-block:: python3
+def _unwrap_optional(annotation):
+ """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:
+ return args[0]
+ return annotation
- 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
+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'``.
-class ProcedureArgument(Field):
+ The ``'field_type'`` metadata key overrides automatic inference.
"""
- 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``.
+ 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]; 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]; fall 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: 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:
+ 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, ...]:
"""
- def __init__(self, default=None):
- super().__init__(-1, default)
+ Return the TLV field specs of dataclass *cls* in declaration order.
- def encoded_length(self, val, markers: dict) -> int:
- return 0
+ 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.
- def encode_into(self, val, markers: dict, wire: VarBinaryStr, offset: int) -> int:
+ 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')
- 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
+ markers[f'{fname}##digest_pos'] = digest_pos
+ markers[f'{fname}##preprocessed_name'] = name
- 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()')
+ 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)
- def get_arg(self, markers: dict):
- """
- Get its value from ``markers``
+ markers[f'{fname}##name_value_len'] = comp_total
+ return get_tl_num_size(type_num) + get_tl_num_size(comp_total) + comp_total
- :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``.
+def _encode_into_interest_name(fname: str, val, metadata: dict, markers: dict,
+ wire: VarBinaryStr, offset: int) -> int:
+ """
+ Write pass for an Interest Name field.
- :param markers: the markers dict.
- :param val: the new value.
- """
- markers[f'{self.name}##args'] = val
+ 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']``).
-class OffsetMarker(ProcedureArgument):
+ Returns *shrink_size* (0 for fixed-length signature schemes like HMAC/EdDSA).
+ All offsets in *markers* are absolute positions within *mv*.
"""
- A marker variable that records its position in TLV wire in terms of offset.
+ 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:
"""
- def encode_into(self, val, markers: dict, wire: VarBinaryStr, offset: int) -> int:
- self.set_arg(markers, offset)
- return 0
+ Compute the encoded byte count of one TLV field (T + L + V).
- def skipping_process(self, markers: dict, wire: BinaryStr, offset: int):
- self.set_arg(markers, offset)
+ 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.
+ 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
-class UintField(Field):
- """
- NonNegativeInteger field.
+ if kind == 'interest_name':
+ return _encoded_length_interest_name(fname, val, spec.metadata, markers)
- Type: :class:`int`
+ type_num = spec.tlv_type
- Its Length is 1, 2, 4 or 8 when present.
+ # Boolean TLVs are present if truthy and absent otherwise.
+ if kind == 'bool':
+ return (get_tl_num_size(type_num) + 1) if val else 0
- :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
+ if val is None:
+ return 0
- def encoded_length(self, val, markers: dict) -> int:
- 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'{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
+ 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:
- if val <= 0xFF:
- ret = 1
- elif val <= 0xFFFF:
- ret = 2
- elif val <= 0xFFFFFFFF:
- ret = 4
+ 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:
- 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]
+ raise TypeError(f'{fname}: invalid name type')
+ if isinstance(name_val, list):
+ total_with_tl = Name.encoded_length(name_val)
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
+ 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
- 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:
+ 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
- def parse_from(self, instance, markers: dict, wire: BinaryStr, offset: int, length: int, offset_btl: int):
- return True
+ raise TypeError(f'Unknown field kind {kind!r} for {fname!r}')
-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 _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
- 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
+# ---------------------------------------------------------------------------
+# Encoding — pass 2: write bytes
+# ---------------------------------------------------------------------------
-class NameField(Field):
+def _encode_into_field(fname: str, val, spec: _FieldSpec,
+ markers: dict, wire: VarBinaryStr, offset: int) -> int:
"""
- NDN Name field. Its Type is always :any:`Name.TYPE_NAME`.
+ Write one TLV field into *wire* at *offset*.
- Type: :any:`NonStrictName`
+ *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.
"""
- def __init__(self, default=None, type_number=Name.TYPE_NAME):
- super().__init__(type_number, default)
+ kind = spec.kind
+ metadata = spec.metadata
+ # Zero-wire kinds: handled before looking up tlv_type.
+ if kind == 'offset_marker':
+ markers[fname] = offset
+ return 0
- def encoded_length(self, val, markers: dict) -> int:
- if val is None:
+ if kind == 'sig_value':
+ signer = markers.get('##signer')
+ if signer 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')
+ 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 isinstance(name, list):
- ret = Name.encoded_length(name)
+ 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:
- ret = len(name)
- markers[f'{self.name}##preprocessed_name'] = name
- markers[f'{self.name}##encoded_length_with_tl'] = ret
- return ret
+ 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
- def encode_into(self, val, markers: dict, wire: VarBinaryStr, offset: int) -> int:
- if val is None:
+ if kind == 'map':
+ if not val:
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
+ 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
- def parse_from(self, instance, markers: dict, wire: BinaryStr, offset: int, length: int, offset_btl: int):
- return Name.decode(wire, offset_btl)[0]
+ raise TypeError(f'Unknown field kind {kind!r} for {fname!r}')
-class BytesField(Field):
- r"""
- Field for ``*OCTET``.
+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)
- Type: :any:`BinaryStr`
- :ivar is_string: If the value is a UTF-8 string. False by default.
+# ---------------------------------------------------------------------------
+# Public encode entry point
+# ---------------------------------------------------------------------------
- .. note::
- Do not assign it with a :class:`str` if ``is_string`` is False.
+def tlv_encode(obj, wire=None, offset: int = 0, markers: dict = None):
"""
- def __init__(self, type_num: int, default=None, is_string: bool = False):
- super().__init__(type_num, default)
- self.is_string = is_string
+ Encode a dataclass TLV object.
- def __set__(self, instance, value):
- instance.__dict__[self.name] = value
+ **Allocating form** — ``tlv_encode(obj)``
+ Allocates a new :class:`bytearray`, fills it, and returns it.
- def __get__(self, instance, owner):
- if instance is None:
- return self
- value = self.get_value(instance)
- return value
+ **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.
- 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)
+ :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.
- 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')
+ Uses ``object.__new__`` to bypass ``__init__``, then sets each field:
+ - ``field(default=X)`` → X
+ - ``field(default_factory=F)`` → F()
+ - no default → None
+ """
+ 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:
- return ret
-
+ object.__setattr__(obj, f.name, None)
+ return obj
-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]
+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*.
+ :param ignore_critical: forwarded to nested ``tlv_parse`` calls.
+ :return: the parsed Python value.
"""
- _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
+ kind = spec.kind
+ if kind == 'bool':
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']
+ 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:
- 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
-
+ 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]
-class ModelField(Field):
- r"""
- Field for nested TlvModel.
+ if kind == 'model':
+ ignore = spec.metadata.get('ignore_critical', ignore_critical)
+ return tlv_parse(spec.annotation, wire[offset:offset + length], ignore)
- Type: :any:`TlvModel`
+ raise TypeError(f'Unknown kind {kind!r} for {fname!r}')
- :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 tlv_parse(cls, wire, ignore_critical: bool = False, markers: dict = None):
"""
- 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.
+ 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.
"""
- 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
+ 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
- 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
+ val = _parse_value(fname, spec,
+ mv, offset, length, offset_btl, ignore_critical)
+ object.__setattr__(obj, fname, val)
+ field_pos = i + 1
+ found = True
+ break
-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.
+ 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')
- Type: :class:`list`
+ offset += length
- :vartype value_type: :any:`Field`
- :ivar value_type: the type of values in the dict.
+ return obj
- .. 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
+# ---------------------------------------------------------------------------
+# Marker helpers (convenience wrappers for the markers dict)
+# ---------------------------------------------------------------------------
- def get_value(self, instance):
- if self.name not in instance.__dict__:
- instance.__dict__[self.name] = {}
- return instance.__dict__[self.name]
+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`.
- def encoded_length(self, val, markers: dict) -> int:
- if not val:
- return 0
+ Equivalent to ``markers.get(key, default)``.
+ """
+ return markers.get(key, default)
- 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 tlv_set_arg(markers: dict, key: str, val) -> None:
+ """
+ Write a value into the *markers* dict used by :func:`tlv_encode` /
+ :func:`tlv_parse`.
- 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
+ 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/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/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/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/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/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 7af61e0..a6f99e9 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 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
@@ -52,12 +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) -> bool:
- for checker in args:
- if not await checker(name, sig):
- return False
- return True
- return wrapper
diff --git a/src/ndn/security/validator/known_key_validator.py b/src/ndn/security/validator/known_key_validator.py
index aa63819..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
+from ...types import ValidResult
from ...app_support.security_v2 import parse_certificate
@@ -66,22 +66,22 @@ 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) -> 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
@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..95cc0c1 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
@@ -105,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/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/encoding/tlv_model_test.py b/tests/encoding/tlv_model_test.py
index ebb26ef..cc3eb20 100644
--- a/tests/encoding/tlv_model_test.py
+++ b/tests/encoding/tlv_model_test.py
@@ -15,191 +15,1180 @@
# 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']}
+"""Tests for the dataclass-based TLV API."""
+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,
+ 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})
+
+
+# ---------------------------------------------------------------------------
+# Unsigned integer tests
+# ---------------------------------------------------------------------------
+
+class TestUint:
+ """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 TestUintEnum:
+ """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
+
+
+# ---------------------------------------------------------------------------
+# Boolean tests
+# ---------------------------------------------------------------------------
+
+class TestBool:
+ """Zero-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
+
+
+# ---------------------------------------------------------------------------
+# Byte-string tests
+# ---------------------------------------------------------------------------
+
+class TestBytes:
+ """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
+
+
+# ---------------------------------------------------------------------------
+# Name tests
+# ---------------------------------------------------------------------------
+
+class TestName:
+ """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'
+
+
+# ---------------------------------------------------------------------------
+# Nested model tests
+# ---------------------------------------------------------------------------
+
+class TestNestedModel:
+ """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
+
+
+# ---------------------------------------------------------------------------
+# Repeated value tests
+# ---------------------------------------------------------------------------
+
+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]))
+ # 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 places parent fields first."""
+
+ 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 == []
+
+
+# ---------------------------------------------------------------------------
+# Golden wire compatibility
+# ---------------------------------------------------------------------------
+
+class TestGoldenWireCompatibility:
+ def test_scalar_wire_formats(self):
+ @dataclass
+ 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})
+
+ 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 Inner:
+ val: int = field(default=None, metadata={'tlv_type': 0x01})
+
+ @dataclass
+ class Outer:
+ name: NDNName = field(default=None, metadata={'tlv_type': 0x07})
+ inner: Inner = field(default=None, metadata={'tlv_type': 0x10})
+
+ 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
+
+ def test_repeated_wire_formats(self):
+ uint_wire = bytes(tlv_encode(_RepeatedUint(words=[0, 1, 2])))
+ assert uint_wire == bytes.fromhex('010200000102000101020002')
+
+ model_wire = bytes(tlv_encode(
+ _RepeatedModel(items=[_Inner(val=10), _Inner(val=20)])))
+ assert model_wire == bytes.fromhex('100301010a1003010114')
+
+
+# ---------------------------------------------------------------------------
+# Map 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_golden_wire(self):
+ obj = _StrBytesMap(entries={'alpha': b'\x01\x02', 'beta': b'\x03'})
+ wire = bytes(tlv_encode(obj))
+ assert wire == bytes.fromhex('2105616c70686123020102210462657461230103')
+
+ 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'})
+ 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)
+
+ parse_markers = {}
+ 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
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/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)
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/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'
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')
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