links-notation 0.22.0__tar.gz → 0.23.0__tar.gz
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.
- {links_notation-0.22.0/links_notation.egg-info → links_notation-0.23.0}/PKG-INFO +11 -11
- {links_notation-0.22.0 → links_notation-0.23.0}/links_notation/__init__.py +20 -0
- links_notation-0.23.0/links_notation/binary.py +637 -0
- {links_notation-0.22.0 → links_notation-0.23.0/links_notation.egg-info}/PKG-INFO +11 -11
- {links_notation-0.22.0 → links_notation-0.23.0}/links_notation.egg-info/SOURCES.txt +2 -0
- links_notation-0.23.0/links_notation.egg-info/requires.txt +16 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/pyproject.toml +8 -5
- links_notation-0.23.0/tests/test_binary.py +107 -0
- links_notation-0.22.0/links_notation.egg-info/requires.txt +0 -16
- {links_notation-0.22.0 → links_notation-0.23.0}/LICENSE +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/MANIFEST.in +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/README.md +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/README.ru.md +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/links_notation/comments.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/links_notation/format_config.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/links_notation/formatter.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/links_notation/link.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/links_notation/parser.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/links_notation/quotes.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/links_notation/stream_parser.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/links_notation.egg-info/dependency_links.txt +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/links_notation.egg-info/top_level.txt +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/setup.cfg +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_api.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_comments.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_edge_case_parser.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_empty_reference.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_format_config.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_indentation_consistency.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_indented_id_nested_values.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_indented_id_syntax.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_link.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_links_group.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_mixed_indentation_modes.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_multi_quote_parser.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_multiline_parser.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_multiline_quoted_string.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_nested_indentation.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_nested_parser.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_nested_self_reference.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_nesting_limit.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_single_line_parser.py +0 -0
- {links_notation-0.22.0 → links_notation-0.23.0}/tests/test_stream_parser.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: links-notation
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.23.0
|
|
4
4
|
Summary: Python implementation of the Links Notation parser
|
|
5
5
|
Author-email: "Link.Foundation" <drakonard@gmail.com>
|
|
6
6
|
License-Expression: Unlicense
|
|
@@ -22,18 +22,18 @@ Requires-Python: >=3.9
|
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Provides-Extra: test
|
|
25
|
-
Requires-Dist: pytest>=9.1; extra == "test"
|
|
26
|
-
Requires-Dist: pytest-timeout>=2.4; extra == "test"
|
|
25
|
+
Requires-Dist: pytest>=9.1.1; extra == "test"
|
|
26
|
+
Requires-Dist: pytest-timeout>=2.4.0; extra == "test"
|
|
27
27
|
Provides-Extra: lint
|
|
28
|
-
Requires-Dist: black>=26.
|
|
29
|
-
Requires-Dist: isort>=9.0; extra == "lint"
|
|
30
|
-
Requires-Dist: flake8>=7.
|
|
28
|
+
Requires-Dist: black>=26.10.0; extra == "lint"
|
|
29
|
+
Requires-Dist: isort>=9.0.2; extra == "lint"
|
|
30
|
+
Requires-Dist: flake8>=7.4.1; extra == "lint"
|
|
31
31
|
Provides-Extra: dev
|
|
32
|
-
Requires-Dist: pytest>=9.1; extra == "dev"
|
|
33
|
-
Requires-Dist: pytest-timeout>=2.4; extra == "dev"
|
|
34
|
-
Requires-Dist: black>=26.
|
|
35
|
-
Requires-Dist: isort>=9.0; extra == "dev"
|
|
36
|
-
Requires-Dist: flake8>=7.
|
|
32
|
+
Requires-Dist: pytest>=9.1.1; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-timeout>=2.4.0; extra == "dev"
|
|
34
|
+
Requires-Dist: black>=26.10.0; extra == "dev"
|
|
35
|
+
Requires-Dist: isort>=9.0.2; extra == "dev"
|
|
36
|
+
Requires-Dist: flake8>=7.4.1; extra == "dev"
|
|
37
37
|
Dynamic: license-file
|
|
38
38
|
|
|
39
39
|
# Links Notation Parser for Python
|
|
@@ -10,6 +10,17 @@ from importlib.metadata import PackageNotFoundError
|
|
|
10
10
|
from importlib.metadata import version as _distribution_version
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
|
+
from .binary import (
|
|
14
|
+
ArityRange,
|
|
15
|
+
BinaryLinoCodec,
|
|
16
|
+
BinaryLinoOptions,
|
|
17
|
+
DecodeLimits,
|
|
18
|
+
External,
|
|
19
|
+
LinksPacket,
|
|
20
|
+
Section,
|
|
21
|
+
)
|
|
22
|
+
from .binary import format_document as format_binary_document
|
|
23
|
+
from .binary import format_reference as format_binary_reference
|
|
13
24
|
from .comments import strip_comments
|
|
14
25
|
from .format_config import FormatConfig
|
|
15
26
|
from .formatter import format_links
|
|
@@ -47,6 +58,15 @@ def _read_version() -> str:
|
|
|
47
58
|
__version__ = _read_version()
|
|
48
59
|
|
|
49
60
|
__all__ = [
|
|
61
|
+
"ArityRange",
|
|
62
|
+
"BinaryLinoCodec",
|
|
63
|
+
"BinaryLinoOptions",
|
|
64
|
+
"DecodeLimits",
|
|
65
|
+
"External",
|
|
66
|
+
"LinksPacket",
|
|
67
|
+
"Section",
|
|
68
|
+
"format_binary_document",
|
|
69
|
+
"format_binary_reference",
|
|
50
70
|
"DEFAULT_MAX_DEPTH",
|
|
51
71
|
"Link",
|
|
52
72
|
"ParseError",
|
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
"""Binary links notation v1. See docs/protocol/binary-links-notation.md.
|
|
2
|
+
|
|
3
|
+
The packet layer is independent of the document mapping. Native Link models
|
|
4
|
+
are preserved; parse_document explicitly selects link-cli canonical groups.
|
|
5
|
+
All integers on the wire are unsigned 64-bit values.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import io
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from .link import Link
|
|
14
|
+
from .parser import Parser
|
|
15
|
+
|
|
16
|
+
U64 = (1 << 64) - 1
|
|
17
|
+
WIDTHS = (1, 2, 4, 8)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _require(condition, message):
|
|
21
|
+
if not condition:
|
|
22
|
+
raise ValueError(message)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _u64(value):
|
|
26
|
+
_require(isinstance(value, int) and not isinstance(value, bool) and 0 <= value <= U64, "expected uint64")
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class External:
|
|
32
|
+
"""Hybrid external reference; bare integers denote internal addresses."""
|
|
33
|
+
|
|
34
|
+
value: int
|
|
35
|
+
|
|
36
|
+
def __post_init__(self):
|
|
37
|
+
_u64(self.value)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class ArityRange:
|
|
42
|
+
min: int = 2
|
|
43
|
+
max: Optional[int] = 2
|
|
44
|
+
|
|
45
|
+
def __post_init__(self):
|
|
46
|
+
_require(1 <= _u64(self.min) <= U64 >> 4, "invalid minimum arity")
|
|
47
|
+
_require(self.max is None or _u64(self.max) >= self.min, "invalid maximum arity")
|
|
48
|
+
|
|
49
|
+
def contains(self, length):
|
|
50
|
+
return length >= self.min and (self.max is None or length <= self.max)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def fixed(self):
|
|
54
|
+
return self.min == self.max
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def parse(cls, text):
|
|
58
|
+
_require(re.fullmatch(r"[0-9]+(?:\.\.[0-9]*)?", text), "invalid arity")
|
|
59
|
+
parts = text.split("..")
|
|
60
|
+
return cls(int(parts[0]), int(parts[-1]) if parts[-1] else None)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class DecodeLimits:
|
|
65
|
+
"""Work budgets applied to both byte/model input and encoding."""
|
|
66
|
+
|
|
67
|
+
max_links: int = 1 << 22
|
|
68
|
+
max_references: int = 1 << 24
|
|
69
|
+
max_nodes: int = 1 << 22
|
|
70
|
+
max_string_bytes: int = 64 << 20
|
|
71
|
+
max_depth: int = 64
|
|
72
|
+
|
|
73
|
+
def __post_init__(self):
|
|
74
|
+
for value in vars(self).values():
|
|
75
|
+
_u64(value)
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def unlimited(cls):
|
|
79
|
+
"""For trusted input; caller must provide adequate memory/stack."""
|
|
80
|
+
return cls(U64, U64, U64, U64, U64)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class BinaryLinoOptions:
|
|
85
|
+
external_references: bool = False
|
|
86
|
+
arity: ArityRange = field(default_factory=ArityRange)
|
|
87
|
+
packed_widths: bool = False
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def of_packet(cls, packet):
|
|
91
|
+
lengths = [2] + [len(link) for _, link in packet.links()]
|
|
92
|
+
return cls(
|
|
93
|
+
packet.external_references,
|
|
94
|
+
ArityRange(min(lengths), max(lengths)),
|
|
95
|
+
len({section.width for section in packet.sections}) > 1,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class Section:
|
|
101
|
+
gap: int
|
|
102
|
+
arity: ArityRange
|
|
103
|
+
width: int
|
|
104
|
+
links: list
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _width(reference, external):
|
|
108
|
+
value = reference.value if isinstance(reference, External) else _u64(reference)
|
|
109
|
+
_require(not isinstance(reference, External) or external, "external reference disabled")
|
|
110
|
+
for width in WIDTHS:
|
|
111
|
+
bits = width * 8 - int(external or isinstance(reference, External))
|
|
112
|
+
if value < 1 << bits:
|
|
113
|
+
return width
|
|
114
|
+
raise ValueError("reference exceeds capacity")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _leb(out, value):
|
|
118
|
+
value = _u64(value)
|
|
119
|
+
while value >= 128:
|
|
120
|
+
out.append((value & 127) | 128)
|
|
121
|
+
value >>= 7
|
|
122
|
+
out.append(value)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class _Reader:
|
|
126
|
+
def __init__(self, stream):
|
|
127
|
+
self.stream = stream
|
|
128
|
+
|
|
129
|
+
def raw(self, count):
|
|
130
|
+
chunks = bytearray()
|
|
131
|
+
while len(chunks) < count:
|
|
132
|
+
part = self.stream.read(count - len(chunks))
|
|
133
|
+
_require(part, "unexpected end of packet")
|
|
134
|
+
chunks.extend(part)
|
|
135
|
+
return int.from_bytes(chunks, "little")
|
|
136
|
+
|
|
137
|
+
def leb(self):
|
|
138
|
+
value = 0
|
|
139
|
+
for shift in range(0, 64, 7):
|
|
140
|
+
byte = self.raw(1)
|
|
141
|
+
payload = byte & 127
|
|
142
|
+
_require(shift != 63 or payload <= 1, "LEB128 overflow")
|
|
143
|
+
value |= payload << shift
|
|
144
|
+
if byte < 128:
|
|
145
|
+
return value
|
|
146
|
+
raise ValueError("LEB128 overflow")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass
|
|
150
|
+
class LinksPacket:
|
|
151
|
+
external_references: bool = False
|
|
152
|
+
sections: list = field(default_factory=list)
|
|
153
|
+
|
|
154
|
+
def links(self):
|
|
155
|
+
result, address = [], 1
|
|
156
|
+
for section in self.sections:
|
|
157
|
+
address = _u64(address + _u64(section.gap))
|
|
158
|
+
for link in section.links:
|
|
159
|
+
result.append((address, list(link)))
|
|
160
|
+
address = _u64(address + 1)
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
def validate(self, limits=DecodeLimits()):
|
|
164
|
+
_require(len(self.sections) <= limits.max_links, "too many sections")
|
|
165
|
+
links = self.links()
|
|
166
|
+
_require(len(links) <= limits.max_links, "too many links")
|
|
167
|
+
references = 0
|
|
168
|
+
for section in self.sections:
|
|
169
|
+
_require(section.width in WIDTHS, "invalid reference width")
|
|
170
|
+
# Also validate mutable in-memory sections/packets.
|
|
171
|
+
ArityRange(section.arity.min, section.arity.max)
|
|
172
|
+
for link in section.links:
|
|
173
|
+
_require(section.arity.contains(len(link)), "link outside arity range")
|
|
174
|
+
references += len(link)
|
|
175
|
+
_require(references <= limits.max_references, "too many references")
|
|
176
|
+
for reference in link:
|
|
177
|
+
_require(_width(reference, self.external_references) <= section.width, "reference outside width")
|
|
178
|
+
return self
|
|
179
|
+
|
|
180
|
+
def to_bytes(self, limits=DecodeLimits()):
|
|
181
|
+
self.validate(limits)
|
|
182
|
+
compact = not self.sections or (
|
|
183
|
+
len(self.sections) == 1
|
|
184
|
+
and self.sections[0].gap == 5
|
|
185
|
+
and self.sections[0].arity == ArityRange()
|
|
186
|
+
and bool(self.sections[0].links)
|
|
187
|
+
)
|
|
188
|
+
out = bytearray([0x10 | int(self.external_references)])
|
|
189
|
+
if compact:
|
|
190
|
+
width = self.sections[0].width if self.sections else 1
|
|
191
|
+
out[0] |= WIDTHS.index(width) << 2
|
|
192
|
+
_leb(out, sum(len(s.links) for s in self.sections))
|
|
193
|
+
else:
|
|
194
|
+
out[0] |= 2
|
|
195
|
+
_leb(out, len(self.sections))
|
|
196
|
+
for section in self.sections:
|
|
197
|
+
_leb(
|
|
198
|
+
out,
|
|
199
|
+
section.arity.min << 4
|
|
200
|
+
| WIDTHS.index(section.width)
|
|
201
|
+
| (4 if section.gap else 0)
|
|
202
|
+
| (0 if section.arity.fixed else 8),
|
|
203
|
+
)
|
|
204
|
+
if section.gap:
|
|
205
|
+
_leb(out, section.gap)
|
|
206
|
+
if not section.arity.fixed:
|
|
207
|
+
_leb(out, 0 if section.arity.max is None else section.arity.max - section.arity.min)
|
|
208
|
+
_leb(out, len(section.links))
|
|
209
|
+
for section in self.sections:
|
|
210
|
+
for link in section.links:
|
|
211
|
+
if not section.arity.fixed:
|
|
212
|
+
_leb(out, len(link) - section.arity.min)
|
|
213
|
+
for reference in link:
|
|
214
|
+
if isinstance(reference, External):
|
|
215
|
+
value = (
|
|
216
|
+
(1 << (section.width * 8 - 1))
|
|
217
|
+
if reference.value == 0
|
|
218
|
+
else (1 << (section.width * 8)) - reference.value
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
value = reference
|
|
222
|
+
out.extend(value.to_bytes(section.width, "little"))
|
|
223
|
+
return bytes(out)
|
|
224
|
+
|
|
225
|
+
def write_to(self, stream, limits=DecodeLimits()):
|
|
226
|
+
data = self.to_bytes(limits)
|
|
227
|
+
offset = 0
|
|
228
|
+
while offset < len(data):
|
|
229
|
+
written = stream.write(data[offset:])
|
|
230
|
+
_require(written is not None and written > 0, "stream did not write data")
|
|
231
|
+
offset += written
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def read_from(cls, stream, limits=DecodeLimits()):
|
|
235
|
+
"""Read exactly one packet; None only at a clean end of stream."""
|
|
236
|
+
first = stream.read(1)
|
|
237
|
+
if not first:
|
|
238
|
+
return None
|
|
239
|
+
header = first[0]
|
|
240
|
+
_require(header & 0xF0 == 0x10, "unsupported binary version")
|
|
241
|
+
reader, sections, counts = _Reader(stream), [], []
|
|
242
|
+
if header & 2:
|
|
243
|
+
_require(header & 12 == 0, "explicit header width bits set")
|
|
244
|
+
count = reader.leb()
|
|
245
|
+
_require(count <= limits.max_links, "too many sections")
|
|
246
|
+
address, total = 1, 0
|
|
247
|
+
for _ in range(count):
|
|
248
|
+
shape = reader.leb()
|
|
249
|
+
gap = reader.leb() if shape & 4 else 0
|
|
250
|
+
minimum = shape >> 4
|
|
251
|
+
extra = reader.leb() if shape & 8 else None
|
|
252
|
+
maximum = minimum if extra is None else (None if extra == 0 else _u64(minimum + extra))
|
|
253
|
+
arity = ArityRange(minimum, maximum)
|
|
254
|
+
count = reader.leb()
|
|
255
|
+
address = _u64(address + gap + count)
|
|
256
|
+
total += count
|
|
257
|
+
_require(total <= limits.max_links, "too many links")
|
|
258
|
+
sections.append(Section(gap, arity, WIDTHS[shape & 3], []))
|
|
259
|
+
counts.append(count)
|
|
260
|
+
else:
|
|
261
|
+
count = reader.leb()
|
|
262
|
+
_u64(6 + count)
|
|
263
|
+
_require(count <= limits.max_links, "too many links")
|
|
264
|
+
if count:
|
|
265
|
+
sections.append(Section(5, ArityRange(), WIDTHS[(header >> 2) & 3], []))
|
|
266
|
+
counts.append(count)
|
|
267
|
+
references = 0
|
|
268
|
+
for section, count in zip(sections, counts):
|
|
269
|
+
for _ in range(count):
|
|
270
|
+
length = section.arity.min + (0 if section.arity.fixed else reader.leb())
|
|
271
|
+
_require(section.arity.contains(_u64(length)), "link outside arity range")
|
|
272
|
+
references += length
|
|
273
|
+
_require(references <= limits.max_references, "too many references")
|
|
274
|
+
link = []
|
|
275
|
+
for _ in range(length):
|
|
276
|
+
raw = reader.raw(section.width)
|
|
277
|
+
top = 1 << (section.width * 8 - 1)
|
|
278
|
+
link.append(
|
|
279
|
+
External(0 if raw == top else (1 << (section.width * 8)) - raw)
|
|
280
|
+
if header & 1 and raw >= top
|
|
281
|
+
else raw
|
|
282
|
+
)
|
|
283
|
+
section.links.append(link)
|
|
284
|
+
return cls(bool(header & 1), sections)
|
|
285
|
+
|
|
286
|
+
@classmethod
|
|
287
|
+
def from_bytes(cls, data, limits=DecodeLimits()):
|
|
288
|
+
stream = io.BytesIO(data)
|
|
289
|
+
packet = cls.read_from(stream, limits)
|
|
290
|
+
_require(packet is not None, "empty input")
|
|
291
|
+
_require(not stream.read(1), "trailing bytes")
|
|
292
|
+
return packet
|
|
293
|
+
|
|
294
|
+
@classmethod
|
|
295
|
+
def parse_links(cls, text):
|
|
296
|
+
links = []
|
|
297
|
+
for entry in text.split(";"):
|
|
298
|
+
if not entry.strip():
|
|
299
|
+
continue
|
|
300
|
+
address, refs = entry.split(":")
|
|
301
|
+
links.append(
|
|
302
|
+
(
|
|
303
|
+
_u64(int(address)),
|
|
304
|
+
[External(int(r[1:])) if r.startswith("#") else _u64(int(r)) for r in refs.split()],
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
return links
|
|
308
|
+
|
|
309
|
+
@classmethod
|
|
310
|
+
def pack(cls, external_references, links, packed_widths=False):
|
|
311
|
+
links = list(links)
|
|
312
|
+
needs, previous = [], 0
|
|
313
|
+
for address, refs in links:
|
|
314
|
+
_require(previous < _u64(address) < U64, "addresses must ascend and leave space for end")
|
|
315
|
+
_require(refs, "empty raw link")
|
|
316
|
+
previous = address
|
|
317
|
+
needs.append(max(_width(r, external_references) for r in refs))
|
|
318
|
+
|
|
319
|
+
def layout(packed):
|
|
320
|
+
if not links:
|
|
321
|
+
return []
|
|
322
|
+
widest, costs, opens, bests = max(needs), [float("inf")] * 8, [], []
|
|
323
|
+
for index, (address, refs) in enumerate(links):
|
|
324
|
+
best = min(range(8), key=lambda s: costs[s])
|
|
325
|
+
bests.append(best)
|
|
326
|
+
before = costs[best] if index else 0
|
|
327
|
+
next_costs, mask = [float("inf")] * 8, 0
|
|
328
|
+
for state in range(8):
|
|
329
|
+
width, variable = WIDTHS[state // 2], state % 2
|
|
330
|
+
if (not packed and width != widest) or width < needs[index]:
|
|
331
|
+
continue
|
|
332
|
+
opening = before + 2 + variable
|
|
333
|
+
continuing = (
|
|
334
|
+
costs[state]
|
|
335
|
+
if index
|
|
336
|
+
and links[index - 1][0] + 1 == address
|
|
337
|
+
and (variable or len(links[index - 1][1]) == len(refs))
|
|
338
|
+
else float("inf")
|
|
339
|
+
)
|
|
340
|
+
if continuing <= opening:
|
|
341
|
+
next_costs[state] = continuing + len(refs) * width + variable
|
|
342
|
+
else:
|
|
343
|
+
next_costs[state] = opening + len(refs) * width + variable
|
|
344
|
+
mask |= 1 << state
|
|
345
|
+
opens.append(mask)
|
|
346
|
+
costs = next_costs
|
|
347
|
+
result, end, state = [], len(links), min(range(8), key=lambda s: costs[s])
|
|
348
|
+
for index in range(len(links) - 1, -1, -1):
|
|
349
|
+
if opens[index] & (1 << state):
|
|
350
|
+
result.append((end - index, WIDTHS[state // 2]))
|
|
351
|
+
end, state = index, bests[index]
|
|
352
|
+
return list(reversed(result))
|
|
353
|
+
|
|
354
|
+
def packet(plan):
|
|
355
|
+
sections, index, address = [], 0, 1
|
|
356
|
+
for count, width in plan:
|
|
357
|
+
members = links[index : index + count]
|
|
358
|
+
lengths = [len(refs) for _, refs in members]
|
|
359
|
+
start = members[0][0]
|
|
360
|
+
sections.append(
|
|
361
|
+
Section(
|
|
362
|
+
start - address,
|
|
363
|
+
ArityRange(min(lengths), max(lengths)),
|
|
364
|
+
width,
|
|
365
|
+
[list(refs) for _, refs in members],
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
index, address = index + count, start + count
|
|
369
|
+
return cls(external_references, sections)
|
|
370
|
+
|
|
371
|
+
uniform = packet(layout(False))
|
|
372
|
+
if not packed_widths:
|
|
373
|
+
return uniform
|
|
374
|
+
packed = packet(layout(True))
|
|
375
|
+
return (
|
|
376
|
+
packed
|
|
377
|
+
if len(packed.to_bytes(DecodeLimits.unlimited())) < len(uniform.to_bytes(DecodeLimits.unlimited()))
|
|
378
|
+
else uniform
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class _Encoder:
|
|
383
|
+
def __init__(self, options):
|
|
384
|
+
self.options = options
|
|
385
|
+
self.doublets, self.tuples, self.created = [], [], {}
|
|
386
|
+
self.powers = [("i", 1)]
|
|
387
|
+
|
|
388
|
+
def link(self, items):
|
|
389
|
+
key = tuple(items)
|
|
390
|
+
if key not in self.created:
|
|
391
|
+
doublet = len(items) == 2 and all(kind != "t" for kind, _ in items)
|
|
392
|
+
target = self.doublets if doublet else self.tuples
|
|
393
|
+
node = ("d" if doublet else "t", len(target))
|
|
394
|
+
target.append(list(items))
|
|
395
|
+
self.created[key] = node
|
|
396
|
+
return self.created[key]
|
|
397
|
+
|
|
398
|
+
def chain(self, items):
|
|
399
|
+
tail = ("i", 0)
|
|
400
|
+
for head in reversed(items):
|
|
401
|
+
tail = self.link([head, tail])
|
|
402
|
+
return tail
|
|
403
|
+
|
|
404
|
+
def typed(self, marker, elements):
|
|
405
|
+
items = [("i", marker)] + elements
|
|
406
|
+
return (
|
|
407
|
+
self.link(items)
|
|
408
|
+
if len(items) != 2 and self.options.arity.contains(len(items))
|
|
409
|
+
else self.link([("i", marker), self.chain(elements)])
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
def list(self, elements):
|
|
413
|
+
if not elements:
|
|
414
|
+
return ("i", 0)
|
|
415
|
+
if len(elements) == 2 or self.options.arity.contains(len(elements)):
|
|
416
|
+
return self.link(elements)
|
|
417
|
+
return self.typed(4, elements)
|
|
418
|
+
|
|
419
|
+
def unary(self, value):
|
|
420
|
+
powers = []
|
|
421
|
+
for bit in range(63, -1, -1):
|
|
422
|
+
if value & (1 << bit):
|
|
423
|
+
while len(self.powers) <= bit:
|
|
424
|
+
self.powers.append(self.link([self.powers[-1], self.powers[-1]]))
|
|
425
|
+
powers.append(self.powers[bit])
|
|
426
|
+
if not powers:
|
|
427
|
+
return ("i", 0)
|
|
428
|
+
result = powers[-1]
|
|
429
|
+
for power in reversed(powers[:-1]):
|
|
430
|
+
result = self.link([power, result])
|
|
431
|
+
return result
|
|
432
|
+
|
|
433
|
+
def scalar(self, value):
|
|
434
|
+
return ("e", value) if self.options.external_references and value < 1 << 63 else self.unary(value)
|
|
435
|
+
|
|
436
|
+
def reference(self, text):
|
|
437
|
+
if re.fullmatch(r"0|[1-9][0-9]*", text) and len(text) <= 20 and int(text) <= U64:
|
|
438
|
+
value = int(text)
|
|
439
|
+
if self.options.external_references and value < 1 << 63:
|
|
440
|
+
return ("e", value)
|
|
441
|
+
return self.link([("i", 2), self.unary(value)])
|
|
442
|
+
return self.typed(3, [self.scalar(ord(c)) for c in text])
|
|
443
|
+
|
|
444
|
+
def encode_identified(self, node):
|
|
445
|
+
if node.id is not None and node.values:
|
|
446
|
+
identifier = self.reference(node.id)
|
|
447
|
+
return self.typed(5, [identifier] + [self.encode_identified(v) for v in node.values])
|
|
448
|
+
if node.id is not None:
|
|
449
|
+
return self.reference(node.id)
|
|
450
|
+
return self.list([self.encode_identified(v) for v in node.values])
|
|
451
|
+
|
|
452
|
+
def finish(self):
|
|
453
|
+
def resolve(node):
|
|
454
|
+
kind, value = node
|
|
455
|
+
return (
|
|
456
|
+
External(value)
|
|
457
|
+
if kind == "e"
|
|
458
|
+
else (value if kind == "i" else 6 + value + (len(self.doublets) if kind == "t" else 0))
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
links = [(6 + i, [resolve(n) for n in nodes]) for i, nodes in enumerate(self.doublets + self.tuples)]
|
|
462
|
+
return LinksPacket.pack(self.options.external_references, links, self.options.packed_widths)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class _Decoder:
|
|
466
|
+
def __init__(self, packet, limits):
|
|
467
|
+
packet.validate(limits)
|
|
468
|
+
self.limits = limits
|
|
469
|
+
self.links, self.unary = [], []
|
|
470
|
+
self.nodes, self.strings = limits.max_nodes, limits.max_string_bytes
|
|
471
|
+
for address, refs in packet.links():
|
|
472
|
+
_require(address == 6 + len(self.links), "document links must be contiguous from 6")
|
|
473
|
+
_require(all(isinstance(r, External) or r < address for r in refs), "document contains forward reference")
|
|
474
|
+
|
|
475
|
+
def value(r):
|
|
476
|
+
if isinstance(r, External):
|
|
477
|
+
return None
|
|
478
|
+
return r if r <= 1 else (self.unary[r - 6] if r >= 6 else None)
|
|
479
|
+
|
|
480
|
+
a, b = (value(refs[0]), value(refs[1])) if len(refs) == 2 else (None, None)
|
|
481
|
+
self.unary.append(a + b if a is not None and b is not None and a + b <= U64 else None)
|
|
482
|
+
self.links.append(refs)
|
|
483
|
+
|
|
484
|
+
def chain(self, tail):
|
|
485
|
+
result = []
|
|
486
|
+
while tail != 0:
|
|
487
|
+
_require(isinstance(tail, int) and tail >= 6, "broken element chain")
|
|
488
|
+
items = self.links[tail - 6]
|
|
489
|
+
_require(len(items) == 2, "broken element chain")
|
|
490
|
+
_require(len(result) < self.limits.max_nodes, "chain too long")
|
|
491
|
+
result.append(items[0])
|
|
492
|
+
tail = items[1]
|
|
493
|
+
return result
|
|
494
|
+
|
|
495
|
+
def number(self, reference):
|
|
496
|
+
if isinstance(reference, External):
|
|
497
|
+
return reference.value
|
|
498
|
+
if reference <= 1:
|
|
499
|
+
return reference
|
|
500
|
+
value = self.unary[reference - 6] if reference >= 6 else None
|
|
501
|
+
_require(value is not None, "expected unary number")
|
|
502
|
+
return value
|
|
503
|
+
|
|
504
|
+
def text(self, text):
|
|
505
|
+
self.strings -= len(text.encode("utf-8"))
|
|
506
|
+
_require(self.strings >= 0, "too many expanded string bytes")
|
|
507
|
+
return Link(text)
|
|
508
|
+
|
|
509
|
+
def decode(self, reference, depth):
|
|
510
|
+
_require(depth < self.limits.max_depth, "nesting limit exceeded")
|
|
511
|
+
self.nodes -= 1
|
|
512
|
+
_require(self.nodes >= 0, "too many expanded nodes")
|
|
513
|
+
if isinstance(reference, External):
|
|
514
|
+
return self.text(str(reference.value))
|
|
515
|
+
if reference == 0:
|
|
516
|
+
return Link()
|
|
517
|
+
_require(reference >= 6, "standalone marker")
|
|
518
|
+
items = self.links[reference - 6]
|
|
519
|
+
if isinstance(items[0], int) and 1 <= items[0] <= 5:
|
|
520
|
+
marker = items[0]
|
|
521
|
+
elements = items[1:] if len(items) != 2 or marker == 2 else self.chain(items[1])
|
|
522
|
+
if marker == 2:
|
|
523
|
+
_require(len(elements) == 1, "number needs one value")
|
|
524
|
+
return self.text(str(self.number(elements[0])))
|
|
525
|
+
if marker == 3:
|
|
526
|
+
chars, byte_count = [], 0
|
|
527
|
+
for e in elements:
|
|
528
|
+
p = self.number(e)
|
|
529
|
+
_require(p <= 0x10FFFF and not 0xD800 <= p <= 0xDFFF, "invalid Unicode scalar")
|
|
530
|
+
size = 1 if p <= 0x7F else 2 if p <= 0x7FF else 3 if p <= 0xFFFF else 4
|
|
531
|
+
_require(size <= self.strings - byte_count, "too many expanded string bytes")
|
|
532
|
+
byte_count += size
|
|
533
|
+
chars.append(chr(p))
|
|
534
|
+
return self.text("".join(chars))
|
|
535
|
+
if marker == 5:
|
|
536
|
+
_require(elements, "identified link needs id")
|
|
537
|
+
identifier = self.decode(elements[0], depth)
|
|
538
|
+
_require(identifier.id is not None and not identifier.values, "id must be reference")
|
|
539
|
+
return Link(identifier.id, [self.decode(e, depth + 1) for e in elements[1:]])
|
|
540
|
+
_require(marker == 4, "invalid typed marker")
|
|
541
|
+
else:
|
|
542
|
+
elements = items
|
|
543
|
+
return Link(None, [self.decode(e, depth + 1) for e in elements])
|
|
544
|
+
|
|
545
|
+
def document(self):
|
|
546
|
+
if not self.links:
|
|
547
|
+
return []
|
|
548
|
+
root = self.links[-1]
|
|
549
|
+
if len(root) == 2 and root[0] == 4:
|
|
550
|
+
elements = self.chain(root[1])
|
|
551
|
+
else:
|
|
552
|
+
_require(not (isinstance(root[0], int) and 1 <= root[0] <= 5), "root must be list")
|
|
553
|
+
elements = root
|
|
554
|
+
return [self.decode(e, 0) for e in elements]
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _canonical(node):
|
|
558
|
+
if node.id is None and len(node.values) == 1 and node.values[0].id is not None and not node.values[0].values:
|
|
559
|
+
return node.values[0]
|
|
560
|
+
return Link(node.id, [_canonical(v) for v in node.values])
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def format_reference(text):
|
|
564
|
+
if text and not text.startswith("#") and not any(c.isspace() or c in "\ufeff():\"'`" for c in text):
|
|
565
|
+
return text
|
|
566
|
+
choices = []
|
|
567
|
+
for quote in "'\"`":
|
|
568
|
+
if text.startswith(quote):
|
|
569
|
+
continue
|
|
570
|
+
longest = max([len(m.group()) for m in re.finditer(re.escape(quote) + "+", text)] or [0])
|
|
571
|
+
choices.append(((longest + 1) | 1, quote))
|
|
572
|
+
count, quote = min(choices, key=lambda item: item[0])
|
|
573
|
+
delimiter = quote * count
|
|
574
|
+
return delimiter + text + delimiter
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def format_document(document):
|
|
578
|
+
def nested(node, top=False):
|
|
579
|
+
if node.id is not None and not node.values:
|
|
580
|
+
return format_reference(node.id)
|
|
581
|
+
values = " ".join(nested(v) for v in node.values)
|
|
582
|
+
if node.id is not None:
|
|
583
|
+
return "(" + format_reference(node.id) + ": " + values + ")"
|
|
584
|
+
if len(node.values) == 1 and node.values[0].id is not None and not node.values[0].values:
|
|
585
|
+
return "((" + values + "))"
|
|
586
|
+
return values if top and len(node.values) >= 2 else "(" + values + ")"
|
|
587
|
+
|
|
588
|
+
return "\n".join(nested(n, True) for n in document)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
class BinaryLinoCodec:
|
|
592
|
+
def __init__(self, options=None, limits=None):
|
|
593
|
+
self.options = options or BinaryLinoOptions()
|
|
594
|
+
self.limits = limits or DecodeLimits()
|
|
595
|
+
|
|
596
|
+
def encode_packet(self, document):
|
|
597
|
+
_require(self.options.arity.contains(2), "arity must contain 2")
|
|
598
|
+
pending = [(node, 0) for node in document]
|
|
599
|
+
nodes, strings = 0, 0
|
|
600
|
+
while pending:
|
|
601
|
+
node, depth = pending.pop()
|
|
602
|
+
_require(isinstance(node, Link), "expected Link model")
|
|
603
|
+
_require(depth < self.limits.max_depth, "nesting limit exceeded")
|
|
604
|
+
nodes += 1 + int(node.id is not None and bool(node.values))
|
|
605
|
+
if node.id is not None:
|
|
606
|
+
_require(isinstance(node.id, str), "reference must be string")
|
|
607
|
+
strings += len(node.id.encode("utf-8"))
|
|
608
|
+
_require(nodes <= self.limits.max_nodes, "too many nodes")
|
|
609
|
+
_require(strings <= self.limits.max_string_bytes, "too many string bytes")
|
|
610
|
+
pending.extend((v, depth + 1) for v in node.values)
|
|
611
|
+
encoder = _Encoder(self.options)
|
|
612
|
+
if document:
|
|
613
|
+
encoder.list([encoder.encode_identified(node) for node in document])
|
|
614
|
+
return encoder.finish().validate(self.limits)
|
|
615
|
+
|
|
616
|
+
def encode(self, document):
|
|
617
|
+
return self.encode_packet(document).to_bytes(self.limits)
|
|
618
|
+
|
|
619
|
+
def decode_packet(self, packet):
|
|
620
|
+
return _Decoder(packet, self.limits).document()
|
|
621
|
+
|
|
622
|
+
def decode(self, data):
|
|
623
|
+
return self.decode_packet(LinksPacket.from_bytes(data, self.limits))
|
|
624
|
+
|
|
625
|
+
@staticmethod
|
|
626
|
+
def parse_document(text, parser=None):
|
|
627
|
+
return [_canonical(n) for n in (parser or Parser()).parse(text)]
|
|
628
|
+
|
|
629
|
+
@staticmethod
|
|
630
|
+
def format_document(document):
|
|
631
|
+
return format_document(document)
|
|
632
|
+
|
|
633
|
+
def encode_text(self, text, parser=None):
|
|
634
|
+
return self.encode(self.parse_document(text, parser))
|
|
635
|
+
|
|
636
|
+
def decode_text(self, data):
|
|
637
|
+
return format_document(self.decode(data))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: links-notation
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.23.0
|
|
4
4
|
Summary: Python implementation of the Links Notation parser
|
|
5
5
|
Author-email: "Link.Foundation" <drakonard@gmail.com>
|
|
6
6
|
License-Expression: Unlicense
|
|
@@ -22,18 +22,18 @@ Requires-Python: >=3.9
|
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Provides-Extra: test
|
|
25
|
-
Requires-Dist: pytest>=9.1; extra == "test"
|
|
26
|
-
Requires-Dist: pytest-timeout>=2.4; extra == "test"
|
|
25
|
+
Requires-Dist: pytest>=9.1.1; extra == "test"
|
|
26
|
+
Requires-Dist: pytest-timeout>=2.4.0; extra == "test"
|
|
27
27
|
Provides-Extra: lint
|
|
28
|
-
Requires-Dist: black>=26.
|
|
29
|
-
Requires-Dist: isort>=9.0; extra == "lint"
|
|
30
|
-
Requires-Dist: flake8>=7.
|
|
28
|
+
Requires-Dist: black>=26.10.0; extra == "lint"
|
|
29
|
+
Requires-Dist: isort>=9.0.2; extra == "lint"
|
|
30
|
+
Requires-Dist: flake8>=7.4.1; extra == "lint"
|
|
31
31
|
Provides-Extra: dev
|
|
32
|
-
Requires-Dist: pytest>=9.1; extra == "dev"
|
|
33
|
-
Requires-Dist: pytest-timeout>=2.4; extra == "dev"
|
|
34
|
-
Requires-Dist: black>=26.
|
|
35
|
-
Requires-Dist: isort>=9.0; extra == "dev"
|
|
36
|
-
Requires-Dist: flake8>=7.
|
|
32
|
+
Requires-Dist: pytest>=9.1.1; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-timeout>=2.4.0; extra == "dev"
|
|
34
|
+
Requires-Dist: black>=26.10.0; extra == "dev"
|
|
35
|
+
Requires-Dist: isort>=9.0.2; extra == "dev"
|
|
36
|
+
Requires-Dist: flake8>=7.4.1; extra == "dev"
|
|
37
37
|
Dynamic: license-file
|
|
38
38
|
|
|
39
39
|
# Links Notation Parser for Python
|
|
@@ -5,6 +5,7 @@ README.ru.md
|
|
|
5
5
|
pyproject.toml
|
|
6
6
|
setup.cfg
|
|
7
7
|
links_notation/__init__.py
|
|
8
|
+
links_notation/binary.py
|
|
8
9
|
links_notation/comments.py
|
|
9
10
|
links_notation/format_config.py
|
|
10
11
|
links_notation/formatter.py
|
|
@@ -18,6 +19,7 @@ links_notation.egg-info/dependency_links.txt
|
|
|
18
19
|
links_notation.egg-info/requires.txt
|
|
19
20
|
links_notation.egg-info/top_level.txt
|
|
20
21
|
tests/test_api.py
|
|
22
|
+
tests/test_binary.py
|
|
21
23
|
tests/test_comments.py
|
|
22
24
|
tests/test_edge_case_parser.py
|
|
23
25
|
tests/test_empty_reference.py
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = [
|
|
2
|
+
requires = [
|
|
3
|
+
"setuptools>=84.0.0; python_version >= '3.10'",
|
|
4
|
+
"setuptools>=82.0.1; python_version < '3.10'",
|
|
5
|
+
]
|
|
3
6
|
build-backend = "setuptools.build_meta"
|
|
4
7
|
|
|
5
8
|
[project]
|
|
6
9
|
name = "links-notation"
|
|
7
|
-
version = "0.
|
|
10
|
+
version = "0.23.0"
|
|
8
11
|
description = "Python implementation of the Links Notation parser"
|
|
9
12
|
readme = "README.md"
|
|
10
13
|
license = "Unlicense"
|
|
@@ -29,9 +32,9 @@ requires-python = ">=3.9"
|
|
|
29
32
|
dependencies = []
|
|
30
33
|
|
|
31
34
|
[project.optional-dependencies]
|
|
32
|
-
test = ["pytest>=9.1", "pytest-timeout>=2.4"]
|
|
33
|
-
lint = ["black>=26.
|
|
34
|
-
dev = ["pytest>=9.1", "pytest-timeout>=2.4", "black>=26.
|
|
35
|
+
test = ["pytest>=9.1.1", "pytest-timeout>=2.4.0"]
|
|
36
|
+
lint = ["black>=26.10.0", "isort>=9.0.2", "flake8>=7.4.1"]
|
|
37
|
+
dev = ["pytest>=9.1.1", "pytest-timeout>=2.4.0", "black>=26.10.0", "isort>=9.0.2", "flake8>=7.4.1"]
|
|
35
38
|
|
|
36
39
|
[project.urls]
|
|
37
40
|
Homepage = "https://github.com/link-foundation/links-notation"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Shared binary vectors and native text-model parity (issue 325)."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from links_notation import Link, Parser
|
|
8
|
+
from links_notation.binary import ArityRange, BinaryLinoCodec, BinaryLinoOptions, DecodeLimits, LinksPacket
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_string_budget_stops_decoding_before_later_scalars():
|
|
12
|
+
data = bytes.fromhex("13024605011001030000009fffffff9effffff0028ffff06")
|
|
13
|
+
with pytest.raises(ValueError, match="string bytes"):
|
|
14
|
+
BinaryLinoCodec(limits=DecodeLimits(max_string_bytes=1)).decode(data)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_shared_vectors():
|
|
18
|
+
count = 0
|
|
19
|
+
for line in (
|
|
20
|
+
(Path(__file__).parents[2] / "docs/protocol/binary-links-notation-vectors.txt").read_text().splitlines()
|
|
21
|
+
):
|
|
22
|
+
if not line or line.startswith("#"):
|
|
23
|
+
continue
|
|
24
|
+
fields = line.split("\t")
|
|
25
|
+
expected = bytes.fromhex(fields[-1])
|
|
26
|
+
if fields[0] == "document":
|
|
27
|
+
codec = BinaryLinoCodec(
|
|
28
|
+
BinaryLinoOptions(fields[2] == "external", ArityRange.parse(fields[3]), fields[4] == "packed")
|
|
29
|
+
)
|
|
30
|
+
document = codec.parse_document(fields[1].replace("\\n", "\n"))
|
|
31
|
+
assert codec.encode(document) == expected, line
|
|
32
|
+
assert codec.decode(expected) == document, line
|
|
33
|
+
else:
|
|
34
|
+
links = LinksPacket.parse_links(fields[1])
|
|
35
|
+
packet = LinksPacket.pack(fields[2] == "external", links, fields[3] == "packed")
|
|
36
|
+
assert packet.to_bytes() == expected, line
|
|
37
|
+
assert LinksPacket.from_bytes(expected).links() == links, line
|
|
38
|
+
count += 1
|
|
39
|
+
assert count == 96
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.parametrize(
|
|
43
|
+
"source",
|
|
44
|
+
[
|
|
45
|
+
"",
|
|
46
|
+
"()",
|
|
47
|
+
'""',
|
|
48
|
+
"a",
|
|
49
|
+
"(a)",
|
|
50
|
+
"((a))",
|
|
51
|
+
"(a b c)",
|
|
52
|
+
'("": () "" (b))',
|
|
53
|
+
"a:\n b\n c",
|
|
54
|
+
'"#x" "λ😀" "007" "18446744073709551615"',
|
|
55
|
+
"# comment\n(a # tail\n b)",
|
|
56
|
+
],
|
|
57
|
+
)
|
|
58
|
+
def test_native_text_model_round_trip(source):
|
|
59
|
+
document = Parser().parse(source)
|
|
60
|
+
for external in (False, True):
|
|
61
|
+
for arity in ("2", "2..3", "1.."):
|
|
62
|
+
for packed in (False, True):
|
|
63
|
+
codec = BinaryLinoCodec(BinaryLinoOptions(external, ArityRange.parse(arity), packed))
|
|
64
|
+
assert codec.decode(codec.encode(document)) == document
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_limits_and_malformed_packets():
|
|
68
|
+
document = [Link(None, [Link("abcdef")])]
|
|
69
|
+
bytes_ = BinaryLinoCodec().encode(document)
|
|
70
|
+
for limits in (
|
|
71
|
+
DecodeLimits(max_nodes=1),
|
|
72
|
+
DecodeLimits(max_string_bytes=2),
|
|
73
|
+
DecodeLimits(max_depth=1),
|
|
74
|
+
DecodeLimits(max_links=1),
|
|
75
|
+
DecodeLimits(max_references=1),
|
|
76
|
+
):
|
|
77
|
+
with pytest.raises(ValueError):
|
|
78
|
+
BinaryLinoCodec(limits=limits).decode(bytes_)
|
|
79
|
+
with pytest.raises(ValueError):
|
|
80
|
+
BinaryLinoCodec(limits=limits).encode(document)
|
|
81
|
+
for bytes_ in (b"", b"\x20", b"\x1e\x00", b"\x10\x01", b"\x10\x00\x00", b"\x10" + b"\xff" * 10):
|
|
82
|
+
with pytest.raises(ValueError):
|
|
83
|
+
BinaryLinoCodec().decode(bytes_)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_packet_stream_boundaries_uint64_and_every_truncated_prefix():
|
|
87
|
+
from io import BytesIO
|
|
88
|
+
|
|
89
|
+
for external, references in [(False, "0 18446744073709551615"), (True, "#0 #9223372036854775807")]:
|
|
90
|
+
links = LinksPacket.parse_links("6:" + references)
|
|
91
|
+
packet = LinksPacket.pack(external, links, True)
|
|
92
|
+
data = packet.to_bytes()
|
|
93
|
+
stream = BytesIO(data + data)
|
|
94
|
+
assert LinksPacket.read_from(stream).links() == links
|
|
95
|
+
assert stream.tell() == len(data)
|
|
96
|
+
assert LinksPacket.read_from(stream).links() == links
|
|
97
|
+
assert LinksPacket.read_from(stream) is None
|
|
98
|
+
for end in range(len(data)):
|
|
99
|
+
with pytest.raises(ValueError):
|
|
100
|
+
LinksPacket.from_bytes(data[:end])
|
|
101
|
+
document = [Link("leaf")]
|
|
102
|
+
for _ in range(70):
|
|
103
|
+
document = [Link(None, document)]
|
|
104
|
+
with pytest.raises(ValueError):
|
|
105
|
+
BinaryLinoCodec().encode(document)
|
|
106
|
+
codec = BinaryLinoCodec(limits=DecodeLimits(max_depth=80))
|
|
107
|
+
assert codec.decode(codec.encode(document)) == document
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{links_notation-0.22.0 → links_notation-0.23.0}/links_notation.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|