zerodds-wire 1.0.0rc7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zerodds_wire/__init__.py +107 -0
- zerodds_wire/encapsulation.py +111 -0
- zerodds_wire/endianness.py +48 -0
- zerodds_wire/error.py +72 -0
- zerodds_wire/float128.py +609 -0
- zerodds_wire/py.typed +0 -0
- zerodds_wire/reader.py +1084 -0
- zerodds_wire/topic_descriptor.py +164 -0
- zerodds_wire/typesupport.py +220 -0
- zerodds_wire/values.py +242 -0
- zerodds_wire/writer.py +1578 -0
- zerodds_wire-1.0.0rc7.dist-info/METADATA +26 -0
- zerodds_wire-1.0.0rc7.dist-info/RECORD +15 -0
- zerodds_wire-1.0.0rc7.dist-info/WHEEL +5 -0
- zerodds_wire-1.0.0rc7.dist-info/top_level.txt +1 -0
zerodds_wire/__init__.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 ZeroDDS Contributors
|
|
3
|
+
"""`zerodds_wire` — the Python wire runtime.
|
|
4
|
+
|
|
5
|
+
The runtime a generated Python encoder calls: `Writer` and `Reader`, the
|
|
6
|
+
alignment rules, and the framing forms. It is the counterpart of
|
|
7
|
+
`crates/wire`, not a binding to it — a Python probe that reached the Rust
|
|
8
|
+
core through ctypes would be byte-identical by construction and would
|
|
9
|
+
prove nothing about whether the two read the specification the same way.
|
|
10
|
+
|
|
11
|
+
This package makes no decision about a type. Which members are written,
|
|
12
|
+
in what order, with which header and which length code is the layout
|
|
13
|
+
plan's answer and arrives here as arguments. A runtime that decided any
|
|
14
|
+
of it would be a second place where the wire format lives.
|
|
15
|
+
|
|
16
|
+
Spec: DDS-XTypes 1.3 §7.4, DDSI-RTPS 2.5 §9.6.3.8 and §10.5.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from .encapsulation import Encoding, encapsulation_header, encapsulation_padding
|
|
22
|
+
from .endianness import Endianness
|
|
23
|
+
from .error import ImpossibleLength, NotUtf8, OutOfRange, Truncated, WireError
|
|
24
|
+
from .float128 import Float128, Inexact
|
|
25
|
+
from .reader import EmHeaderRead, PidRead, Reader, ReaderMark
|
|
26
|
+
# `utf8_octets` and `utf16_units` are exported because a generated codec
|
|
27
|
+
# names them: the shared walk emits both through
|
|
28
|
+
# `CodecTarget::runtime_static` where a bounded `string` or `wstring` is
|
|
29
|
+
# measured against its bound (§7.3.1.2.1.12.3). Without them here a
|
|
30
|
+
# generated module needed a second import line reaching into a submodule,
|
|
31
|
+
# which is a path the package's shape does not otherwise ask anyone to
|
|
32
|
+
# know.
|
|
33
|
+
from .typesupport import Discarded, Status, TryConstruct, utf8_octets, utf16_units
|
|
34
|
+
from .values import Mismatch, Missing, SampleError, Unknown
|
|
35
|
+
from .writer import (
|
|
36
|
+
KEY_HASH_LEN,
|
|
37
|
+
PID_EXTENDED,
|
|
38
|
+
PID_IGNORE,
|
|
39
|
+
PID_IMPL_EXTENSION,
|
|
40
|
+
PID_LIST_END,
|
|
41
|
+
PID_MUST_UNDERSTAND,
|
|
42
|
+
PID_SHORT_MAX_ID,
|
|
43
|
+
PID_SHORT_MAX_LEN,
|
|
44
|
+
DHeader,
|
|
45
|
+
EmHeader,
|
|
46
|
+
ParameterRole,
|
|
47
|
+
Pid,
|
|
48
|
+
PlCdr1Framing,
|
|
49
|
+
pid_flags_at,
|
|
50
|
+
bit_pattern_bool,
|
|
51
|
+
bit_pattern_f32,
|
|
52
|
+
bit_pattern_f64,
|
|
53
|
+
bit_pattern_int,
|
|
54
|
+
no_dheader,
|
|
55
|
+
Writer,
|
|
56
|
+
wstring_unit_width,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"DHeader",
|
|
61
|
+
"Float128",
|
|
62
|
+
"Inexact",
|
|
63
|
+
"EmHeader",
|
|
64
|
+
"EmHeaderRead",
|
|
65
|
+
"Encoding",
|
|
66
|
+
"Endianness",
|
|
67
|
+
"ImpossibleLength",
|
|
68
|
+
"KEY_HASH_LEN",
|
|
69
|
+
"Discarded",
|
|
70
|
+
"Mismatch",
|
|
71
|
+
"Missing",
|
|
72
|
+
"NotUtf8",
|
|
73
|
+
"OutOfRange",
|
|
74
|
+
"PID_EXTENDED",
|
|
75
|
+
"PID_IGNORE",
|
|
76
|
+
"PID_IMPL_EXTENSION",
|
|
77
|
+
"PID_LIST_END",
|
|
78
|
+
"PID_MUST_UNDERSTAND",
|
|
79
|
+
"PID_SHORT_MAX_ID",
|
|
80
|
+
"PID_SHORT_MAX_LEN",
|
|
81
|
+
"ParameterRole",
|
|
82
|
+
"Pid",
|
|
83
|
+
"PlCdr1Framing",
|
|
84
|
+
"pid_flags_at",
|
|
85
|
+
"PidRead",
|
|
86
|
+
"Reader",
|
|
87
|
+
"ReaderMark",
|
|
88
|
+
"SampleError",
|
|
89
|
+
"Status",
|
|
90
|
+
"Truncated",
|
|
91
|
+
"Unknown",
|
|
92
|
+
"TryConstruct",
|
|
93
|
+
"WireError",
|
|
94
|
+
"Writer",
|
|
95
|
+
"encapsulation_header",
|
|
96
|
+
"encapsulation_padding",
|
|
97
|
+
"bit_pattern_bool",
|
|
98
|
+
"bit_pattern_f32",
|
|
99
|
+
"bit_pattern_f64",
|
|
100
|
+
"bit_pattern_int",
|
|
101
|
+
"no_dheader",
|
|
102
|
+
"utf8_octets",
|
|
103
|
+
"utf16_units",
|
|
104
|
+
"wstring_unit_width",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
__version__ = "1.0.0-rc.7"
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 ZeroDDS Contributors
|
|
3
|
+
"""Encodings, and the four bytes that announce one.
|
|
4
|
+
|
|
5
|
+
DDSI-RTPS 2.5 §10.5 and XTypes 1.3 §7.4.1.1. Kept out of `writer.py`
|
|
6
|
+
because it frames a sample rather than being part of one: a KeyHash
|
|
7
|
+
holder is written by the same writer and carries no header at all.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import enum
|
|
13
|
+
|
|
14
|
+
from .endianness import Endianness
|
|
15
|
+
|
|
16
|
+
__all__ = ["Encoding", "encapsulation_header", "encapsulation_padding"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Encoding(enum.Enum):
|
|
20
|
+
"""Which encoding a plan and a stream are in.
|
|
21
|
+
|
|
22
|
+
The five of `zerodds_ast_layout::Encoding`. XCDR1 and XCDR2 differ in
|
|
23
|
+
their framing: XCDR1 has no delimiter header, and `@mutable` uses PID
|
|
24
|
+
headers there and EMHEADERs here.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
PLAIN_CDR1 = "plain-cdr1"
|
|
28
|
+
PL_CDR1 = "pl-cdr1"
|
|
29
|
+
PLAIN_CDR2 = "plain-cdr2"
|
|
30
|
+
DELIMITED_CDR = "delimited-cdr"
|
|
31
|
+
PL_CDR2 = "pl-cdr2"
|
|
32
|
+
|
|
33
|
+
def is_v1(self) -> bool:
|
|
34
|
+
"""Whether this is an XCDR version 1 encoding."""
|
|
35
|
+
return self in (Encoding.PLAIN_CDR1, Encoding.PL_CDR1)
|
|
36
|
+
|
|
37
|
+
def is_v2(self) -> bool:
|
|
38
|
+
"""Whether this is an XCDR version 2 encoding."""
|
|
39
|
+
return not self.is_v1()
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def max_alignment(self) -> int:
|
|
43
|
+
"""The largest alignment any primitive takes.
|
|
44
|
+
|
|
45
|
+
XTypes 1.3 §7.4.1: `MAXALIGN(VERSION2) = 4`,
|
|
46
|
+
`MAXALIGN(VERSION1) = 8`, and `ALIGN(N)` aligns to
|
|
47
|
+
`MIN(N, XCDR.maxalign)`. §7.6.8 repeats it for the key holder:
|
|
48
|
+
"the maximum alignment in PLAIN_CDR2 (i.e., 4)".
|
|
49
|
+
"""
|
|
50
|
+
return 8 if self.is_v1() else 4
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def parse(name: str) -> Encoding | None:
|
|
54
|
+
"""The command line's spelling, including the two shorthands.
|
|
55
|
+
|
|
56
|
+
`xcdr1` and `xcdr2` name the plain form, because that is what a
|
|
57
|
+
caller who has not thought about extensibility means. An unknown
|
|
58
|
+
name is `None` and never a default.
|
|
59
|
+
"""
|
|
60
|
+
if name in ("xcdr1", "plain-cdr1"):
|
|
61
|
+
return Encoding.PLAIN_CDR1
|
|
62
|
+
if name in ("xcdr2", "plain-cdr2"):
|
|
63
|
+
return Encoding.PLAIN_CDR2
|
|
64
|
+
for encoding in Encoding:
|
|
65
|
+
if encoding.value == name:
|
|
66
|
+
return encoding
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_IDENTIFIERS: dict[tuple[Encoding, Endianness], int] = {
|
|
71
|
+
(Encoding.PLAIN_CDR1, Endianness.BIG): 0x0000,
|
|
72
|
+
(Encoding.PLAIN_CDR1, Endianness.LITTLE): 0x0001,
|
|
73
|
+
(Encoding.PL_CDR1, Endianness.BIG): 0x0002,
|
|
74
|
+
(Encoding.PL_CDR1, Endianness.LITTLE): 0x0003,
|
|
75
|
+
(Encoding.PLAIN_CDR2, Endianness.BIG): 0x0006,
|
|
76
|
+
(Encoding.PLAIN_CDR2, Endianness.LITTLE): 0x0007,
|
|
77
|
+
(Encoding.DELIMITED_CDR, Endianness.BIG): 0x0008,
|
|
78
|
+
(Encoding.DELIMITED_CDR, Endianness.LITTLE): 0x0009,
|
|
79
|
+
(Encoding.PL_CDR2, Endianness.BIG): 0x000A,
|
|
80
|
+
(Encoding.PL_CDR2, Endianness.LITTLE): 0x000B,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def encapsulation_padding(length: int) -> int:
|
|
85
|
+
"""How many zero bytes bring `length` up to the next multiple of four.
|
|
86
|
+
|
|
87
|
+
XTypes 1.3 §7.4.1.1 pads the serialized data to a four-byte
|
|
88
|
+
boundary; §7.6.3.1.2 states the count in the encapsulation options.
|
|
89
|
+
"""
|
|
90
|
+
return (4 - (length & 3)) & 3
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def encapsulation_header(
|
|
94
|
+
encoding: Encoding, endianness: Endianness, padding: int = 0
|
|
95
|
+
) -> bytes:
|
|
96
|
+
"""The four-byte encapsulation header.
|
|
97
|
+
|
|
98
|
+
The identifier itself is always big-endian on the wire, whatever the
|
|
99
|
+
payload behind it is — it is what tells a reader which order to
|
|
100
|
+
switch to.
|
|
101
|
+
|
|
102
|
+
The two option bytes carry the payload's trailing padding count in
|
|
103
|
+
the low two bits of the second one. XTypes 1.3 §7.6.3.1.2: those
|
|
104
|
+
bits "shall be interpreted by the receiver to determine where the
|
|
105
|
+
serialized data exactly ended". Measured on the loopback wire on
|
|
106
|
+
2026-08-09: Cyclone, RTI and Fast DDS all pad the payload to four
|
|
107
|
+
and all three state the count — `@final struct One { octet a; }`
|
|
108
|
+
goes out as `0001 0003 ab000000` from every one of them.
|
|
109
|
+
"""
|
|
110
|
+
identifier = _IDENTIFIERS[(encoding, endianness)]
|
|
111
|
+
return bytes([identifier >> 8, identifier & 0xFF, 0, padding & 0x3])
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 ZeroDDS Contributors
|
|
3
|
+
"""Byte order.
|
|
4
|
+
|
|
5
|
+
The counterpart of `crates/wire/src/endianness.rs`. A separate module
|
|
6
|
+
because the writer and the reader both need it and neither should own it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import enum
|
|
12
|
+
|
|
13
|
+
__all__ = ["Endianness"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Endianness(enum.Enum):
|
|
17
|
+
"""Which end of a multi-byte value goes out first."""
|
|
18
|
+
|
|
19
|
+
LITTLE = "le"
|
|
20
|
+
BIG = "be"
|
|
21
|
+
|
|
22
|
+
def is_little(self) -> bool:
|
|
23
|
+
"""Whether the low byte goes out first."""
|
|
24
|
+
return self is Endianness.LITTLE
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def struct_prefix(self) -> str:
|
|
28
|
+
"""The `struct` module's byte-order character.
|
|
29
|
+
|
|
30
|
+
Explicit rather than `=` or `@`: the native order of the host is
|
|
31
|
+
not the order the encapsulation identifier announced, and a probe
|
|
32
|
+
that agreed with itself on one machine and not another would be
|
|
33
|
+
useless as a second opinion.
|
|
34
|
+
"""
|
|
35
|
+
return "<" if self is Endianness.LITTLE else ">"
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def parse(name: str) -> Endianness | None:
|
|
39
|
+
"""The command line's spelling, or `None` for anything else.
|
|
40
|
+
|
|
41
|
+
`None` and not a default: a typo in a wire-affecting flag has to
|
|
42
|
+
be loud, because the bytes it produces look plausible.
|
|
43
|
+
"""
|
|
44
|
+
if name in ("le", "little"):
|
|
45
|
+
return Endianness.LITTLE
|
|
46
|
+
if name in ("be", "big"):
|
|
47
|
+
return Endianness.BIG
|
|
48
|
+
return None
|
zerodds_wire/error.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2026 ZeroDDS Contributors
|
|
3
|
+
"""What can go wrong on the wire.
|
|
4
|
+
|
|
5
|
+
The variants of `crates/wire/src/error.rs`, as exceptions. Every one
|
|
6
|
+
names what was expected and where: a reader that returned a zero for a
|
|
7
|
+
truncated stream would hand the caller a value that was never sent, and
|
|
8
|
+
a writer that clamped an over-wide integer would put a different number
|
|
9
|
+
on the wire than the caller asked for.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"WireError",
|
|
16
|
+
"Truncated",
|
|
17
|
+
"ImpossibleLength",
|
|
18
|
+
"NotUtf8",
|
|
19
|
+
"OutOfRange",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class WireError(Exception):
|
|
24
|
+
"""Base of every wire failure."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Truncated(WireError):
|
|
28
|
+
"""The stream ended before the value did."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, at: int, needed: int, available: int) -> None:
|
|
31
|
+
super().__init__(f"at {at}: {needed} bytes needed, {available} available")
|
|
32
|
+
self.at = at
|
|
33
|
+
self.needed = needed
|
|
34
|
+
self.available = available
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ImpossibleLength(WireError):
|
|
38
|
+
"""A length field claims more than the stream holds.
|
|
39
|
+
|
|
40
|
+
Separate from `Truncated`: a length that cannot be true is a
|
|
41
|
+
malformed stream, not a short one, and allocating for it is how a
|
|
42
|
+
reader turns a bad message into an out-of-memory.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, at: int, claimed: int, available: int) -> None:
|
|
46
|
+
super().__init__(f"at {at}: a length of {claimed} with {available} bytes left")
|
|
47
|
+
self.at = at
|
|
48
|
+
self.claimed = claimed
|
|
49
|
+
self.available = available
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class NotUtf8(WireError):
|
|
53
|
+
"""A string held bytes that are not UTF-8."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, at: int) -> None:
|
|
56
|
+
super().__init__(f"at {at}: the string is not UTF-8")
|
|
57
|
+
self.at = at
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class OutOfRange(WireError):
|
|
61
|
+
"""A value outside what its type admits.
|
|
62
|
+
|
|
63
|
+
Raised by the writer as well as the reader, which is where Python
|
|
64
|
+
differs from the Rust runtime: an `int` there is `i32` or `u64` and
|
|
65
|
+
the compiler has already refused the value, while here it is
|
|
66
|
+
unbounded and the check has to happen at the point of the write.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self, at: int, expected: str) -> None:
|
|
70
|
+
super().__init__(f"at {at}: expected {expected}")
|
|
71
|
+
self.at = at
|
|
72
|
+
self.expected = expected
|