From: Eugen Goebel Date: Sat, 27 Jun 2026 23:43:55 +0000 (+0200) Subject: Render unnamed flag bits in to_text() instead of dropping them (#1276) X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=715fb880973ec56528c9ff238ec230be02067596;p=thirdparty%2Fdnspython.git Render unnamed flag bits in to_text() instead of dropping them (#1276) Closes #1264. dns.flags.to_text() and dns.flags.edns_to_text() iterated only the named enum members, so any set bit without a named flag (for example the EDNS 0x2000 bit) was silently dropped from the text representation. Such bits are now rendered as FLAGn, where n is the bit position, and from_text() and edns_from_text() parse that form back, so the conversions round-trip. Adds tests in tests/test_flags.py and a whatsnew entry. Fixes [#1264]. Co-authored-by: Eugen Goebel --- diff --git a/dns/flags.py b/dns/flags.py index 8eb2bc83..2757e62e 100644 --- a/dns/flags.py +++ b/dns/flags.py @@ -54,15 +54,32 @@ def _from_text(text: str, enum_class: Any) -> int: flags = 0 tokens = text.split() for t in tokens: - flags |= enum_class[t.upper()] + token = t.upper() + if token.startswith("FLAG") and token[4:].isdigit(): + # An unnamed flag, rendered by _to_text() as FLAGn (see below). + flags |= 1 << int(token[4:]) + else: + flags |= enum_class[token] return flags def _to_text(flags: int, enum_class: Any) -> str: text_flags = [] + known = 0 for k, v in enum_class.__members__.items(): + known |= int(v) if flags & v != 0: text_flags.append(k) + # Render any set bits that do not have a named flag as FLAGn, where n is + # the bit position, so that they are not silently dropped. These tokens + # round-trip through _from_text(). + unknown = flags & ~known + bit = 0 + while unknown: + if unknown & 1: + text_flags.append(f"FLAG{bit}") + unknown >>= 1 + bit += 1 return " ".join(text_flags) diff --git a/doc/whatsnew.rst b/doc/whatsnew.rst index 7deac03f..76dc0710 100644 --- a/doc/whatsnew.rst +++ b/doc/whatsnew.rst @@ -14,6 +14,11 @@ TBD only Python 3" update, also quite some time ago. It is now renamed to "type" (again) so it matches the Python 3 code it is overriding. +* dns.flags.to_text() and dns.flags.edns_to_text() no longer silently drop set + bits that have no named flag. Such bits are now rendered as ``FLAGn``, where n + is the bit position, and dns.flags.from_text() / edns_from_text() parse that form + back, so the conversions round-trip. See issue #1264. + 2.8.0 ----- diff --git a/tests/test_flags.py b/tests/test_flags.py index 27cf03bf..be5342bd 100644 --- a/tests/test_flags.py +++ b/tests/test_flags.py @@ -57,6 +57,23 @@ class FlagsTestCase(unittest.TestCase): flags = dns.flags.QR | dns.flags.AA | dns.flags.RD | dns.flags.RA self.assertEqual(dns.flags.to_text(flags), "QR AA RD RA") + def test_to_text_unknown_flag(self): + # An unknown flag bit must not be silently dropped (issue #1264). + self.assertEqual(dns.flags.to_text(0x8000 | 0x0001), "QR FLAG0") + + def test_edns_to_text_unknown_flag(self): + # EDNS flags behave the same: the unnamed 0x2000 bit is rendered. + self.assertEqual(dns.flags.edns_to_text(0x2000), "FLAG13") + self.assertEqual(dns.flags.edns_to_text(0x8000 | 0x2000), "DO FLAG13") + + def test_unknown_flag_roundtrip(self): + for value in [0x0001, 0x8000 | 0x0001, 0x0400 | 0x0008]: + self.assertEqual(dns.flags.from_text(dns.flags.to_text(value)), value) + for value in [0x2000, 0x8000 | 0x2000, 0x4000 | 0x2000]: + self.assertEqual( + dns.flags.edns_from_text(dns.flags.edns_to_text(value)), value + ) + def test_rcode_badvers(self): rcode = dns.rcode.BADVERS self.assertEqual(rcode.value, 16)