Skip to content

Commit 84e9d58

Browse files
committed
feat: DNSADDR protocol completed
1 parent 2185a3a commit 84e9d58

File tree

16 files changed

+833
-813
lines changed

16 files changed

+833
-813
lines changed

multiaddr/codecs/cid.py

Lines changed: 74 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
from typing import Dict, List
1+
import logging
22

33
import base58
44
import cid
5-
import varint
65

76
from ..codecs import CodecBase
8-
from . import LENGTH_PREFIXED_VAR_SIZE
97
from ..exceptions import BinaryParseError
8+
from . import LENGTH_PREFIXED_VAR_SIZE
9+
10+
logger = logging.getLogger(__name__)
1011

1112
SIZE = LENGTH_PREFIXED_VAR_SIZE
1213
IS_PATH = False
1314

1415

1516
# Spec: https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#string-representation
16-
CIDv0_PREFIX_TO_LENGTH: Dict[str, List[int]] = {
17+
CIDv0_PREFIX_TO_LENGTH: dict[str, list[int]] = {
1718
# base58btc prefixes for valid lengths 1 - 42 with the identity "hash" function
1819
"12": [5, 12, 19, 23, 30, 41, 52, 56],
1920
"13": [9, 16, 34, 45],
@@ -75,8 +76,15 @@ def _is_binary_cidv0_multihash(buf: bytes) -> bool:
7576
"""Check if the given bytes represent a CIDv0 multihash."""
7677
try:
7778
# CIDv0 is just a base58btc encoded multihash
78-
decoded = base58.b58decode(base58.b58encode(buf).decode("ascii"))
79-
return len(decoded) == len(buf) and decoded == buf
79+
# The first byte is the hash function code, second byte is the length
80+
if len(buf) < 2:
81+
return False
82+
hash_code = buf[0]
83+
length = buf[1]
84+
if len(buf) != length + 2: # +2 for the hash code and length bytes
85+
return False
86+
# For CIDv0, we only support sha2-256 (0x12) and identity (0x00)
87+
return hash_code in (0x12, 0x00)
8088
except Exception:
8189
return False
8290

@@ -85,62 +93,84 @@ class Codec(CodecBase):
8593
SIZE = SIZE
8694
IS_PATH = IS_PATH
8795

88-
def to_bytes(self, proto, value: str) -> bytes:
96+
def to_bytes(self, proto, string: str) -> bytes:
8997
"""Convert a CID string to its binary representation."""
90-
if not value:
98+
if not string:
9199
raise ValueError("CID string cannot be empty")
92100

101+
logger.debug(f"[DEBUG CID to_bytes] Input value: {string}")
102+
93103
# First try to parse as CIDv0 (base58btc encoded multihash)
94104
try:
95-
decoded = base58.b58decode(value)
105+
decoded = base58.b58decode(string)
96106
if _is_binary_cidv0_multihash(decoded):
97-
# Add length prefix for CIDv0
98-
return varint.encode(len(decoded)) + decoded
99-
except Exception:
100-
pass
107+
logger.debug(f"[DEBUG CID to_bytes] Parsed as CIDv0: {decoded.hex()}")
108+
# Do not add length prefix here; the framework handles it
109+
return decoded
110+
except Exception as e:
111+
logger.debug(f"[DEBUG CID to_bytes] Failed to parse as CIDv0: {e}")
101112

102113
# If not CIDv0, try to parse as CIDv1
103114
try:
104-
parsed = cid.make_cid(value)
105-
# Add length prefix for CIDv1
106-
return varint.encode(len(parsed.buffer)) + parsed.buffer
107-
except ValueError:
108-
raise ValueError(f"Invalid CID: {value}")
115+
parsed = cid.make_cid(string)
116+
117+
# Do not add length prefix here; the framework handles it
118+
if not isinstance(parsed.buffer, bytes):
119+
raise ValueError("CID buffer must be bytes")
120+
return parsed.buffer
121+
except ValueError as e:
122+
logger.debug(f"[DEBUG CID to_bytes] Failed to parse as CIDv1: {e}")
123+
raise ValueError(f"Invalid CID: {string}")
109124

110125
def to_string(self, proto, buf: bytes) -> str:
111126
"""Convert a binary CID to its string representation."""
112127
if not buf:
113128
raise ValueError("CID buffer cannot be empty")
114129

130+
logger.debug(f"[DEBUG CID to_string] Input buffer: {buf.hex()}")
131+
logger.debug(f"[DEBUG CID to_string] Protocol: {proto.name}")
132+
115133
expected_codec = PROTO_NAME_TO_CIDv1_CODEC.get(proto.name)
134+
logger.debug(f"[DEBUG CID to_string] Expected codec: {expected_codec}")
116135

117136
try:
118-
if _is_binary_cidv0_multihash(buf): # CIDv0
119-
if not expected_codec:
120-
# Simply encode as base58btc as there is nothing better to do
121-
return base58.b58encode(buf).decode("ascii")
122-
123-
# "Implementations SHOULD display peer IDs using the first (raw
124-
# base58btc encoded multihash) format until the second format is
125-
# widely supported."
126-
return base58.b58encode(buf).decode("ascii")
127-
else: # CIDv1+
128-
parsed = cid.from_bytes(buf)
129-
130-
# Ensure CID has correct codec for protocol
131-
if expected_codec and parsed.codec != expected_codec:
132-
raise ValueError(
133-
'"{0}" multiaddr CIDs must use the "{1}" multicodec'.format(
134-
proto.name, expected_codec
135-
)
137+
# First try to parse as CIDv0
138+
if _is_binary_cidv0_multihash(buf):
139+
result = base58.b58encode(buf).decode("ascii")
140+
logger.debug(f"[DEBUG CID to_string] Parsed as CIDv0: {result}")
141+
return result
142+
143+
# If not CIDv0, try to parse as CIDv1
144+
parsed = cid.from_bytes(buf)
145+
logger.debug(f"[DEBUG CID to_string] Parsed as CIDv1: {parsed}")
146+
147+
# Ensure CID has correct codec for protocol
148+
if expected_codec and parsed.codec != expected_codec:
149+
raise ValueError(
150+
'"{}" multiaddr CIDs must use the "{}" multicodec'.format(
151+
proto.name, expected_codec
136152
)
137-
138-
# "Implementations SHOULD display peer IDs using the first (raw
139-
# base58btc encoded multihash) format until the second format is
140-
# widely supported."
141-
if expected_codec and _is_binary_cidv0_multihash(parsed.multihash):
142-
return base58.b58encode(parsed.multihash).decode("ascii")
143-
144-
return parsed.encode("base32").decode("ascii")
153+
)
154+
155+
# For peer IDs (p2p/ipfs), always try to use CIDv0 format if possible
156+
if expected_codec:
157+
# Try to convert to CIDv0 format
158+
try:
159+
# Extract the multihash bytes
160+
multihash = parsed.multihash
161+
logger.debug(f"[DEBUG CID to_string] Extracted multihash: {multihash.hex()}")
162+
# Check if it's a valid CIDv0 multihash
163+
if _is_binary_cidv0_multihash(multihash):
164+
result = base58.b58encode(multihash).decode("ascii")
165+
logger.debug(f"[DEBUG CID to_string] Converted to CIDv0: {result}")
166+
return result
167+
except Exception as e:
168+
logger.debug(f"[DEBUG CID to_string] Failed to convert to CIDv0: {e}")
169+
170+
# If we can't convert to CIDv0, use base32 CIDv1 format
171+
result = parsed.encode("base32").decode("ascii")
172+
logger.debug(f"[DEBUG CID to_string] Using CIDv1 format: {result}")
173+
return result
145174
except Exception as e:
175+
logger.debug(f"[DEBUG CID to_string] Error: {e}")
146176
raise BinaryParseError(str(e), buf, proto.name, e) from e

multiaddr/codecs/domain.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import idna
2-
from . import CodecBase, LENGTH_PREFIXED_VAR_SIZE
2+
33
from ..exceptions import BinaryParseError
4+
from . import LENGTH_PREFIXED_VAR_SIZE, CodecBase
45

56
SIZE = LENGTH_PREFIXED_VAR_SIZE # Variable size for length-prefixed values
67
IS_PATH = False
@@ -10,19 +11,21 @@ class Codec(CodecBase):
1011
SIZE = SIZE
1112
IS_PATH = IS_PATH
1213

13-
def to_bytes(self, proto, value: str) -> bytes:
14-
"""Convert a domain name string to its binary representation (UTF-8), validating with IDNA."""
15-
if not value:
14+
def to_bytes(self, proto, string: str) -> bytes:
15+
"""Convert a domain name string to its binary representation (UTF-8),
16+
validating with IDNA."""
17+
if not string:
1618
raise ValueError("Domain name cannot be empty")
1719
try:
1820
# Validate using IDNA, but store as UTF-8
19-
idna.encode(value, uts46=True)
20-
return value.encode("utf-8")
21+
idna.encode(string, uts46=True)
22+
return string.encode("utf-8")
2123
except idna.IDNAError as e:
22-
raise ValueError(f"Invalid domain name: {str(e)}")
24+
raise ValueError(f"Invalid domain name: {e!s}")
2325

2426
def to_string(self, proto, buf: bytes) -> str:
25-
"""Convert a binary domain name to its string representation (UTF-8), validating with IDNA."""
27+
"""Convert a binary domain name to its string representation (UTF-8),
28+
validating with IDNA."""
2629
if not buf:
2730
raise ValueError("Domain name buffer cannot be empty")
2831
try:
@@ -31,7 +34,7 @@ def to_string(self, proto, buf: bytes) -> str:
3134
idna.encode(value, uts46=True)
3235
return value
3336
except (UnicodeDecodeError, idna.IDNAError) as e:
34-
raise BinaryParseError(f"Invalid domain name encoding: {str(e)}", buf, proto.name, e)
37+
raise BinaryParseError(f"Invalid domain name encoding: {e!s}", buf, proto.name, e)
3538

3639

3740
def to_bytes(proto, string):

multiaddr/codecs/fspath.py

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import os
1+
import logging
22
import urllib.parse
3-
from . import CodecBase, LENGTH_PREFIXED_VAR_SIZE
3+
44
from ..exceptions import BinaryParseError
5+
from . import LENGTH_PREFIXED_VAR_SIZE, CodecBase
6+
7+
logger = logging.getLogger(__name__)
58

69
SIZE = LENGTH_PREFIXED_VAR_SIZE
710
IS_PATH = True
@@ -11,47 +14,59 @@ class Codec(CodecBase):
1114
SIZE = SIZE
1215
IS_PATH = IS_PATH
1316

14-
def to_bytes(self, proto, value: str) -> bytes:
17+
def to_bytes(self, proto, string: str) -> bytes:
1518
"""Convert a filesystem path to its binary representation."""
16-
if not value:
19+
logger.debug(f"[DEBUG fspath.to_bytes] input value: {string}")
20+
if not string:
1721
raise ValueError("Path cannot be empty")
1822

1923
# Normalize path separators
20-
value = value.replace("\\", "/")
21-
22-
# Remove leading/trailing slashes
23-
value = value.strip("/")
24-
24+
string = string.replace("\\", "/")
25+
26+
# Remove leading/trailing slashes but preserve path components
27+
string = string.strip("/")
28+
2529
# Handle empty path after normalization
26-
if not value:
30+
if not string:
2731
raise ValueError("Path cannot be empty after normalization")
28-
32+
2933
# URL decode to handle special characters
30-
value = urllib.parse.unquote(value)
31-
34+
string = urllib.parse.unquote(string)
35+
3236
# Encode as UTF-8
33-
return value.encode("utf-8")
37+
encoded = string.encode("utf-8")
38+
logger.debug(f"[DEBUG fspath.to_bytes] encoded bytes: {encoded}")
39+
return encoded
3440

3541
def to_string(self, proto, buf: bytes) -> str:
3642
"""Convert a binary filesystem path to its string representation."""
43+
logger.debug(f"[DEBUG fspath.to_string] input bytes: {buf}")
3744
if not buf:
3845
raise ValueError("Path buffer cannot be empty")
3946

4047
try:
4148
# Decode from UTF-8
4249
value = buf.decode("utf-8")
43-
50+
logger.debug(f"[DEBUG fspath.to_string] decoded value: {value}")
51+
4452
# Normalize path separators
4553
value = value.replace("\\", "/")
46-
47-
# Remove leading/trailing slashes
54+
55+
# Remove leading/trailing slashes but preserve path components
4856
value = value.strip("/")
49-
57+
5058
# Handle empty path after normalization
5159
if not value:
5260
raise ValueError("Path cannot be empty after normalization")
53-
61+
5462
# URL encode special characters
55-
return urllib.parse.quote(value)
63+
result = urllib.parse.quote(value)
64+
logger.debug(f"[DEBUG fspath.to_string] output string: {result}")
65+
66+
# Add leading slash for Unix socket paths
67+
if proto.name == "unix":
68+
result = "/" + result
69+
70+
return result
5671
except UnicodeDecodeError as e:
57-
raise BinaryParseError(f"Invalid UTF-8 encoding: {str(e)}", buf, proto.name, e)
72+
raise BinaryParseError(f"Invalid UTF-8 encoding: {e!s}", buf, proto.name, e)

multiaddr/codecs/ip4.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import netaddr
22

33
from ..codecs import CodecBase
4+
from ..exceptions import BinaryParseError
45

56
SIZE = 32
67
IS_PATH = False
@@ -14,4 +15,7 @@ def to_bytes(self, proto, string):
1415
return netaddr.IPAddress(string, version=4).packed
1516

1617
def to_string(self, proto, buf):
17-
return str(netaddr.IPAddress(int.from_bytes(buf, byteorder='big'), version=4))
18+
try:
19+
return str(netaddr.IPAddress(int.from_bytes(buf, byteorder='big'), version=4))
20+
except (ValueError, netaddr.AddrFormatError):
21+
raise BinaryParseError("Invalid IPv4 address bytes", buf, "ip4")

0 commit comments

Comments
 (0)