From: Bob Halley Date: Sun, 13 Nov 2005 01:54:29 +0000 (+0000) Subject: Preliminary Unicode support X-Git-Tag: v1.4.0~27 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f4562106aad27e0f1c0049d62d91cb586603df91;p=thirdparty%2Fdnspython.git Preliminary Unicode support --- diff --git a/ChangeLog b/ChangeLog index b4cef236..29f89646 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,11 @@ +2005-11-12 Bob Halley + + * dns/name.py: Preliminary Unicode support has been added for + domain names. Running dns.name.from_text() on a Unicode string + will now encode each label using the IDN ACE encoding. The + to_unicode() method may be used to convert a dns.name.Name with + IDN ACE labels back into a Unicode string. + 2005-10-31 Bob Halley * (Version 1.3.5 released) diff --git a/dns/message.py b/dns/message.py index 335d4fe1..0995299c 100644 --- a/dns/message.py +++ b/dns/message.py @@ -438,7 +438,7 @@ class Message(object): if keyname is None: self.keyname = self.keyring.keys()[0] else: - if isinstance(keyname, str): + if isinstance(keyname, (str, unicode)): keyname = dns.name.from_text(keyname) self.keyname = keyname self.fudge = fudge @@ -954,7 +954,7 @@ def make_query(qname, rdtype, rdclass = dns.rdataclass.IN): @type rdclass: int @rtype: dns.message.Message object""" - if isinstance(qname, str): + if isinstance(qname, (str, unicode)): qname = dns.name.from_text(qname) if isinstance(rdtype, str): rdtype = dns.rdatatype.from_text(rdtype) diff --git a/dns/name.py b/dns/name.py index 09e2c6e6..9f7b6d1b 100644 --- a/dns/name.py +++ b/dns/name.py @@ -22,10 +22,12 @@ """ import cStringIO -import string import struct import sys +if sys.hexversion >= 0x02030000: + import encodings.idna + import dns.exception NAMERELN_NONE = 0 @@ -321,7 +323,28 @@ class Name(object): l = self.labels[:-1] else: l = self.labels - s = string.join(map(_escapify, l), '.') + s = '.'.join(map(_escapify, l)) + return s + + def to_unicode(self, omit_final_dot = False): + """Convert name to Unicode text format. + + IDN ACE lables are converted to Unicode. + + @param omit_final_dot: If True, don't emit the final dot (denoting the + root label) for absolute names. The default is False. + @rtype: string + """ + + if len(self.labels) == 0: + return u'@' + if len(self.labels) == 1 and self.labels[0] == '': + return u'.' + if omit_final_dot and self.is_absolute(): + l = self.labels[:-1] + else: + l = self.labels + s = u'.'.join([encodings.idna.ToUnicode(_escapify(x)) for x in l]) return s def to_digestable(self, origin=None): @@ -508,13 +531,78 @@ class Name(object): root = Name(['']) empty = Name([]) +def from_unicode(text, origin = root): + """Convert unicode text into a Name object. + + Lables are encoded in IDN ACE form. + + @rtype: dns.name.Name object + """ + + if not isinstance(text, unicode): + raise ValueError, "input to from_unicode() must be a unicode string" + if not (origin is None or isinstance(origin, Name)): + raise ValueError, "origin must be a Name or None" + labels = [] + label = u'' + escaping = False + edigits = 0 + total = 0 + if text == u'@': + text = u'' + if text: + if text == u'.': + return Name(['']) # no Unicode "u" on this constant! + for c in text: + if escaping: + if edigits == 0: + if c.isdigit(): + total = int(c) + edigits += 1 + else: + label += c + escaping = False + else: + if not c.isdigit(): + raise BadEscape + total *= 10 + total += int(c) + edigits += 1 + if edigits == 3: + escaping = False + label += chr(total) + elif c == u'.' or c == u'\u3002' or \ + c == u'\uff0e' or c == u'\uff61': + if len(label) == 0: + raise EmptyLabel + labels.append(encodings.idna.ToASCII(label)) + label = u'' + elif c == u'\\': + escaping = True + edigits = 0 + total = 0 + else: + label += c + if escaping: + raise BadEscape + if len(label) > 0: + labels.append(encodings.idna.ToASCII(label)) + else: + labels.append('') + if (len(labels) == 0 or labels[-1] != '') and not origin is None: + labels.extend(list(origin.labels)) + return Name(labels) + def from_text(text, origin = root): """Convert text into a Name object. @rtype: dns.name.Name object """ if not isinstance(text, str): - raise ValueError, "input to from_text() must be a byte string" + if isinstance(text, unicode) and sys.hexversion >= 0x02030000: + return from_unicode(text, origin) + else: + raise ValueError, "input to from_text() must be a string" if not (origin is None or isinstance(origin, Name)): raise ValueError, "origin must be a Name or None" labels = [] diff --git a/dns/query.py b/dns/query.py index 943d14e9..38bbcafd 100644 --- a/dns/query.py +++ b/dns/query.py @@ -263,7 +263,7 @@ def xfr(where, zone, rdtype=dns.rdatatype.AXFR, rdclass=dns.rdataclass.IN, The default is 0. @type source_port: int""" - if isinstance(zone, str): + if isinstance(zone, (str, unicode)): zone = dns.name.from_text(zone) q = dns.message.make_query(zone, rdtype, rdclass) if not keyring is None: diff --git a/dns/resolver.py b/dns/resolver.py index 44032386..4b58e79e 100644 --- a/dns/resolver.py +++ b/dns/resolver.py @@ -474,7 +474,7 @@ class Resolver(object): @raises NoNameservers: no non-broken nameservers are available to answer the question.""" - if isinstance(qname, str): + if isinstance(qname, (str, unicode)): qname = dns.name.from_text(qname, None) if isinstance(rdtype, str): rdtype = dns.rdatatype.from_text(rdtype) @@ -642,7 +642,7 @@ def zone_for_name(name, rdclass=dns.rdataclass.IN, tcp=False, resolver=None): @type resolver: dns.resolver.Resolver object or None @rtype: dns.name.Name""" - if isinstance(name, str): + if isinstance(name, (str, unicode)): name = dns.name.from_text(name, dns.name.root) if resolver is None: resolver = get_default_resolver() diff --git a/dns/rrset.py b/dns/rrset.py index 82638e26..f9b87367 100644 --- a/dns/rrset.py +++ b/dns/rrset.py @@ -115,7 +115,7 @@ def from_text_list(name, ttl, rdclass, rdtype, text_rdatas): @rtype: dns.rrset.RRset object """ - if isinstance(name, str): + if isinstance(name, (str, unicode)): name = dns.name.from_text(name, None) if isinstance(rdclass, str): rdclass = dns.rdataclass.from_text(rdclass) @@ -144,7 +144,7 @@ def from_rdata_list(name, ttl, rdatas): @rtype: dns.rrset.RRset object """ - if isinstance(name, str): + if isinstance(name, (str, unicode)): name = dns.name.from_text(name, None) if len(rdatas) == 0: diff --git a/dns/update.py b/dns/update.py index 4595c968..c513647a 100644 --- a/dns/update.py +++ b/dns/update.py @@ -44,7 +44,7 @@ class Update(dns.message.Message): """ super(Update, self).__init__() self.flags |= dns.opcode.to_flags(dns.opcode.UPDATE) - if isinstance(zone, str): + if isinstance(zone, (str, unicode)): zone = dns.name.from_text(zone) else: zone = zone.copy() @@ -80,7 +80,7 @@ class Update(dns.message.Message): - ttl, rdtype, string...""" - if isinstance(name, str): + if isinstance(name, (str, unicode)): name = dns.name.from_text(name, None) if isinstance(args[0], dns.rdataset.Rdataset): for rds in args: @@ -130,7 +130,7 @@ class Update(dns.message.Message): - rdtype, [string...]""" - if isinstance(name, str): + if isinstance(name, (str, unicode)): name = dns.name.from_text(name, None) if len(args) == 0: rrset = self.find_rrset(self.authority, name, dns.rdataclass.ANY, @@ -188,7 +188,7 @@ class Update(dns.message.Message): - rdtype, string...""" - if isinstance(name, str): + if isinstance(name, (str, unicode)): name = dns.name.from_text(name, None) if len(args) == 0: rrset = self.find_rrset(self.answer, name, @@ -216,7 +216,7 @@ class Update(dns.message.Message): """Require that an owner name (and optionally an rdata type) does not exist as a prerequisite to the execution of the update.""" - if isinstance(name, str): + if isinstance(name, (str, unicode)): name = dns.name.from_text(name, None) if rdtype is None: rrset = self.find_rrset(self.answer, name, diff --git a/dns/zone.py b/dns/zone.py index c875a31c..085a6a12 100644 --- a/dns/zone.py +++ b/dns/zone.py @@ -102,7 +102,7 @@ class Zone(object): return not self.__eq__(other) def _validate_name(self, name): - if isinstance(name, str): + if isinstance(name, (str, unicode)): name = dns.name.from_text(name, None) elif not isinstance(name, dns.name.Name): raise KeyError, \ @@ -543,7 +543,7 @@ class _MasterReader(object): def __init__(self, tok, origin, rdclass, relativize, zone_factory=Zone, allow_include=False): - if isinstance(origin, str): + if isinstance(origin, (str, unicode)): origin = dns.name.from_text(origin) self.tok = tok self.current_origin = origin diff --git a/tests/name.py b/tests/name.py index cc7ced5f..07e2c66f 100644 --- a/tests/name.py +++ b/tests/name.py @@ -611,5 +611,40 @@ class NameTestCase(unittest.TestCase): n.parent() self.failUnlessRaises(dns.name.NoParent, bad) + def testFromUnicode1(self): + n = dns.name.from_text(u'foo.bar') + self.failUnless(n.labels == ('foo', 'bar', '')) + + def testFromUnicode2(self): + n = dns.name.from_text(u'foo\u1234bar.bar') + self.failUnless(n.labels == ('xn--foobar-r5z', 'bar', '')) + + def testFromUnicodeAlternateDot1(self): + n = dns.name.from_text(u'foo\u3002bar') + self.failUnless(n.labels == ('foo', 'bar', '')) + + def testFromUnicodeAlternateDot2(self): + n = dns.name.from_text(u'foo\uff0ebar') + self.failUnless(n.labels == ('foo', 'bar', '')) + + def testFromUnicodeAlternateDot3(self): + n = dns.name.from_text(u'foo\uff61bar') + self.failUnless(n.labels == ('foo', 'bar', '')) + + def testToUnicode1(self): + n = dns.name.from_text(u'foo.bar') + s = n.to_unicode() + self.failUnless(s == u'foo.bar.') + + def testToUnicode2(self): + n = dns.name.from_text(u'foo\u1234bar.bar') + s = n.to_unicode() + self.failUnless(s == u'foo\u1234bar.bar.') + + def testToUnicode3(self): + n = dns.name.from_text('foo.bar') + s = n.to_unicode() + self.failUnless(s == u'foo.bar.') + if __name__ == '__main__': unittest.main()