1# Copyright (C) 2003-2007, 2009, 2010 Nominum, Inc.
2#
3# Permission to use, copy, modify, and distribute this software and its
4# documentation for any purpose with or without fee is hereby granted,
5# provided that the above copyright notice and this permission notice
6# appear in all copies.
7#
8# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
9# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
11# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
14# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
16"""IPv6 helper functions."""
17
18import re
19
20import dns.exception
21import dns.ipv4
22
23_leading_zero = re.compile(r'0+([0-9a-f]+)')
24
25def inet_ntoa(address):
26    """Convert a network format IPv6 address into text.
27
28    @param address: the binary address
29    @type address: string
30    @rtype: string
31    @raises ValueError: the address isn't 16 bytes long
32    """
33
34    if len(address) != 16:
35        raise ValueError("IPv6 addresses are 16 bytes long")
36    hex = address.encode('hex_codec')
37    chunks = []
38    i = 0
39    l = len(hex)
40    while i < l:
41        chunk = hex[i : i + 4]
42        # strip leading zeros.  we do this with an re instead of
43        # with lstrip() because lstrip() didn't support chars until
44        # python 2.2.2
45        m = _leading_zero.match(chunk)
46        if not m is None:
47            chunk = m.group(1)
48        chunks.append(chunk)
49        i += 4
50    #
51    # Compress the longest subsequence of 0-value chunks to ::
52    #
53    best_start = 0
54    best_len = 0
55    start = -1
56    last_was_zero = False
57    for i in xrange(8):
58        if chunks[i] != '0':
59            if last_was_zero:
60                end = i
61                current_len = end - start
62                if current_len > best_len:
63                    best_start = start
64                    best_len = current_len
65                last_was_zero = False
66        elif not last_was_zero:
67            start = i
68            last_was_zero = True
69    if last_was_zero:
70        end = 8
71        current_len = end - start
72        if current_len > best_len:
73            best_start = start
74            best_len = current_len
75    if best_len > 0:
76        if best_start == 0 and \
77           (best_len == 6 or
78            best_len == 5 and chunks[5] == 'ffff'):
79            # We have an embedded IPv4 address
80            if best_len == 6:
81                prefix = '::'
82            else:
83                prefix = '::ffff:'
84            hex = prefix + dns.ipv4.inet_ntoa(address[12:])
85        else:
86            hex = ':'.join(chunks[:best_start]) + '::' + \
87                  ':'.join(chunks[best_start + best_len:])
88    else:
89        hex = ':'.join(chunks)
90    return hex
91
92_v4_ending = re.compile(r'(.*):(\d+)\.(\d+)\.(\d+)\.(\d+)$')
93_colon_colon_start = re.compile(r'::.*')
94_colon_colon_end = re.compile(r'.*::$')
95
96def inet_aton(text):
97    """Convert a text format IPv6 address into network format.
98
99    @param text: the textual address
100    @type text: string
101    @rtype: string
102    @raises dns.exception.SyntaxError: the text was not properly formatted
103    """
104
105    #
106    # Our aim here is not something fast; we just want something that works.
107    #
108
109    if text == '::':
110        text = '0::'
111    #
112    # Get rid of the icky dot-quad syntax if we have it.
113    #
114    m = _v4_ending.match(text)
115    if not m is None:
116        text = "%s:%04x:%04x" % (m.group(1),
117                                 int(m.group(2)) * 256 + int(m.group(3)),
118                                 int(m.group(4)) * 256 + int(m.group(5)))
119    #
120    # Try to turn '::<whatever>' into ':<whatever>'; if no match try to
121    # turn '<whatever>::' into '<whatever>:'
122    #
123    m = _colon_colon_start.match(text)
124    if not m is None:
125        text = text[1:]
126    else:
127        m = _colon_colon_end.match(text)
128        if not m is None:
129            text = text[:-1]
130    #
131    # Now canonicalize into 8 chunks of 4 hex digits each
132    #
133    chunks = text.split(':')
134    l = len(chunks)
135    if l > 8:
136        raise dns.exception.SyntaxError
137    seen_empty = False
138    canonical = []
139    for c in chunks:
140        if c == '':
141            if seen_empty:
142                raise dns.exception.SyntaxError
143            seen_empty = True
144            for i in xrange(0, 8 - l + 1):
145                canonical.append('0000')
146        else:
147            lc = len(c)
148            if lc > 4:
149                raise dns.exception.SyntaxError
150            if lc != 4:
151                c = ('0' * (4 - lc)) + c
152            canonical.append(c)
153    if l < 8 and not seen_empty:
154        raise dns.exception.SyntaxError
155    text = ''.join(canonical)
156
157    #
158    # Finally we can go to binary.
159    #
160    try:
161        return text.decode('hex_codec')
162    except TypeError:
163        raise dns.exception.SyntaxError
164