]> git.ipfire.org Git - thirdparty/dnspython.git/commitdiff
Render unnamed flag bits in to_text() instead of dropping them (#1276)
authorEugen Goebel <eugen-goebel@hotmail.de>
Sat, 27 Jun 2026 23:43:55 +0000 (01:43 +0200)
committerGitHub <noreply@github.com>
Sat, 27 Jun 2026 23:43:55 +0000 (16:43 -0700)
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 <eugen-goebel@users.noreply.github.com>
dns/flags.py
doc/whatsnew.rst
tests/test_flags.py

index 8eb2bc839b24d840a8717f53b38f0e15ac121fb8..2757e62e4605420f761a06cfd6b932afea380956 100644 (file)
@@ -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)
 
 
index 7deac03fe97cd954dfd8d20645df75781b2597c5..76dc07101dd5c7ee9f9fedb6706d5d3344a9024a 100644 (file)
@@ -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
 -----
 
index 27cf03bf23fc8af8f3bbaa3ce44391ef0a3c1a19..be5342bd421cfbfe467ebd21bb1ebca465034731 100644 (file)
@@ -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)