Skip to content

Commit 3f555e7

Browse files
committed
fix: keep key= distinct from a valueless key in TXT records
RFC 6763 6.4: a key with no '=' is a boolean attribute; `key=` is present with an empty value. Collapsing both to None makes them indistinguishable, so a received `key=` re-encodes as a bare `key`. Regression in #1225 (first released in 0.80.0), which replaced explicit branching on the separator with `value or None`. Restores the behaviour #226 introduced.
1 parent fce7094 commit 3f555e7

2 files changed

Lines changed: 71 additions & 3 deletions

File tree

src/zeroconf/_services/info.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -463,10 +463,15 @@ def _unpack_text_into_properties(self) -> None:
463463
length = text[index]
464464
index += 1
465465
key_value = text[index : index + length]
466-
key_sep_value = key_value.partition(b"=")
467-
key = key_sep_value[0]
466+
key, sep, value = key_value.partition(b"=")
468467
if key not in properties:
469-
properties[key] = key_sep_value[2] or None
468+
# RFC 6763 section 6.4 distinguishes a key with no '=' (a
469+
# boolean attribute: present, no value) from `key=` (present
470+
# with an empty value). Testing the separator rather than the
471+
# value keeps them apart; `value or None` collapsed both to
472+
# None, so re-serialising a received `key=` emitted a bare
473+
# `key`.
474+
properties[key] = value if sep else None
470475
index += length
471476

472477
self._properties = properties

tests/services/test_info.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,69 @@ def test_service_info_duplicate_properties_txt_records(self):
630630
assert info.properties[b"ci"] == b"2"
631631
zc.close()
632632

633+
def test_service_info_empty_value_txt_record(self):
634+
"""Verify `key=` stays distinct from a valueless `key`.
635+
636+
RFC 6763 section 6.4 defines a key with no '=' as a boolean attribute
637+
(present, no value), and `key=` as present with an empty value. If both
638+
decode to None they are indistinguishable, and re-encoding a received
639+
`key=` emits a bare `key` -- a different record than the one received.
640+
"""
641+
zc = r.Zeroconf(interfaces=["127.0.0.1"])
642+
service_name = "name._type._tcp.local."
643+
service_type = "_type._tcp.local."
644+
service_server = "ash-1.local."
645+
text = b"\x03rm=\x02rs\x05ve=05"
646+
info = ServiceInfo(
647+
service_type,
648+
service_name,
649+
22,
650+
0,
651+
0,
652+
{"path": "/~paulsm/"},
653+
service_server,
654+
addresses=[socket.inet_aton("10.0.1.2")],
655+
)
656+
info.async_update_records(
657+
zc,
658+
r.current_time_millis(),
659+
[
660+
r.RecordUpdate(
661+
r.DNSText(
662+
service_name,
663+
const._TYPE_TXT,
664+
const._CLASS_IN | const._CLASS_UNIQUE,
665+
120,
666+
text,
667+
),
668+
None,
669+
)
670+
],
671+
)
672+
assert info.properties[b"rm"] == b""
673+
assert info.properties[b"rs"] is None
674+
assert info.properties[b"ve"] == b"05"
675+
676+
# The string-facing API must preserve the distinction too.
677+
assert info.decoded_properties["rm"] == ""
678+
assert info.decoded_properties["rs"] is None
679+
assert info.decoded_properties["ve"] == "05"
680+
681+
# Re-encoding the decoded properties must reproduce the received rdata.
682+
assert (
683+
ServiceInfo(
684+
service_type,
685+
service_name,
686+
22,
687+
0,
688+
0,
689+
info.properties,
690+
service_server,
691+
).text
692+
== text
693+
)
694+
zc.close()
695+
633696

634697
def test_multiple_addresses():
635698
type_ = "_http._tcp.local."

0 commit comments

Comments
 (0)