Repository: twisted/twisted

Issue:
Twisted Names Error when loading Bind style zone file
Hello Team,

When using twisted names module as `twistd -n dns --bindzone example.com`, we encounter the error:
```
  File "/usr/local/lib/python3.10/dist-packages/twisted/names/authority.py", line 315, in loadFile
    self.origin = nativeString(fp.basename() + b".")
TypeError: can only concatenate str (not "bytes") to str
```
https://github.com/twisted/twisted/blob/c9ae7d730aad4036bb0147f5ce70e6fea46dbb51/src/twisted/names/authority.py#L315

`fb.basename()` returns a string and Python complains about it as the other argument is bytes. 

Thanks.

Discussion:
@glyph: Hi @SivaKesava1 — thanks for the bug report, sorry it's been so long with no response.

We should definitely fix this, it's an embarrassing failure to have out-of-the-box functionality like this not even covered by tests.  Most uses of twisted.names are programmatic, but that's no excuse.  That said, I don't know if I have bandwidth to fix this at the moment — but this would be a great first contribution, as it should be straightforward enough to add some type annotations and a unit test for this.

Solution patch:
diff --git a/src/twisted/names/authority.py b/src/twisted/names/authority.py
index 33df6c0068..d6043d92a7 100644
--- a/src/twisted/names/authority.py
+++ b/src/twisted/names/authority.py
@@ -307,12 +307,11 @@ class BindAuthority(FileAuthority):
         Load records from C{filename}.
 
         @param filename: file to read from
-        @type filename: L{bytes}
         """
         fp = FilePath(filename)
         # Not the best way to set an origin. It can be set using $ORIGIN
         # though.
-        self.origin = nativeString(fp.basename() + b".")
+        self.origin = fp.asTextMode().basename() + "."
 
         lines = fp.getContent().splitlines(True)
         lines = self.stripComments(lines)


Test patch:
diff --git a/src/twisted/names/test/test_names.py b/src/twisted/names/test/test_names.py
index 8cd46ff3b0..c91fa0fc79 100644
--- a/src/twisted/names/test/test_names.py
+++ b/src/twisted/names/test/test_names.py
@@ -21,6 +21,7 @@ from twisted.internet.testing import (
     waitUntilAllDisconnected,
 )
 from twisted.names import authority, client, common, dns, server
+from twisted.names.authority import BindAuthority
 from twisted.names.client import Resolver
 from twisted.names.dns import SOA, Message, Query, Record_A, Record_SOA, RRHeader
 from twisted.names.error import DomainError
@@ -1211,23 +1212,58 @@ class BindAuthorityTests(unittest.TestCase):
     Tests for L{twisted.names.authority.BindAuthority}.
     """
 
-    def loadBindString(self, s):
+    def loadBindString(self, s: bytes, asText: bool = True) -> authority.BindAuthority:
         """
         Create a new L{twisted.names.authority.BindAuthority} from C{s}.
 
         @param s: A string with BIND zone data.
-        @type s: bytes
+        @type s: L{bytes}
+
+        @param asText: If True, treat path as text file, else as binary.
+        @type asText: L{bool}
 
         @return: a new bind authority
         @rtype: L{twisted.names.authority.BindAuthority}
         """
-        fp = FilePath(self.mktemp().encode("ascii"))
+        if asText:
+            path = self.mktemp()
+        else:
+            path = FilePath(self.mktemp()).asBytesMode().path
+        # Convert path to FilePath which handles both str and bytes
+        fp = FilePath(path)
         fp.setContent(s)
 
         return authority.BindAuthority(fp.path)
 
-    def setUp(self):
-        self.auth = self.loadBindString(sampleBindZone)
+    def setUp(self) -> None:
+        self.auth = self.loadBindString(sampleBindZone, asText=True)
+
+    def test_loadBindZonePathAsString(self) -> None:
+        """
+        L{BindAuthority} loads a BIND zone with filepath as type string.
+        """
+        authority = self.loadBindString(sampleBindZone, asText=True)
+        self.assertIsInstance(
+            authority, BindAuthority, "Loaded object is not a BindAuthority"
+        )
+
+        self.assertTrue(authority.records, "No records were loaded from the BIND zone")
+
+        self.assertIsInstance(authority.records, dict, "Records is not a dictionary")
+
+    def test_loadBindZonePathAsBytes(self) -> None:
+        """
+        L{BindAuthority} loads a BIND zone with filepath as type bytes.
+        """
+        authority = self.loadBindString(sampleBindZone, asText=False)
+
+        self.assertIsInstance(
+            authority, BindAuthority, "Loaded object is not a BindAuthority"
+        )
+
+        self.assertTrue(authority.records, "No records were loaded from the BIND zone")
+
+        self.assertIsInstance(authority.records, dict, "Records is not a dictionary")
 
     def test_ttl(self):
         """