# -*- encoding: utf-8 -*-
"""
libpydhcpserver.dhcp_types.rfc
==============================
Provides a number of convenience-classes and methods for working with RFC
extensions to the DHCP spec.
Legal
-----
This file is part of libpydhcpserver.
libpydhcpserver is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
(C) Neil Tallim, 2014 <flan@uguu.ca>
"""
try:
    from types import StringTypes
except ImportError: #py3k
    StringTypes = (str,)
    
from conversion import (intToList, longToList)
from ipv4 import IPv4
[docs]def rfc3046_decode(s):
    """
    Extracts sub-options from an RFC3046 option (82).
    
    :param sequence s: The option's raw data.
    :return dict: The sub-options, as byte-lists, keyed by ID.
    """
    sub_options = {}
    while s:
        id = s.pop(0)
        length = s.pop(0)
        sub_options[id] = s[:length]
        s = s[length:]
    return sub_options
     
[docs]def rfc3925_decode(s, identifier_size=4):
    """
    Extracts sub-options from an RFC3925 option (124, 125 (with
    identifier_size=1)).
    
    You probably want to use :func:`rfc3925_125_decode` when dealing
    with option 125 specifically.
    
    :param sequence s: The option's raw data.
    :param int identifier_size: The width of the identifier in bytes.
    :return dict: A dictionary of data as strings keyed by ID numbers.
    """
    data = {}
    while s:
        enterprise_number = conversion.listToNumber(s[:identifier_size])
        payload_size = s[identifier_size]
        payload = s[1 + identifier_size:1 + identifier_size + payload_size]
        s = s[1 + identifier_size + payload_size:]
        data[enterprise_number] = payload
    return data
     
[docs]def rfc3925_125_decode(l):
    """
    Extracts sub-options from an RFC3925 option (125).
    
    :param sequence s: The option's raw data.
    :return dict: A dictionary of data as dictionaries mapping data as strings
        keyed by ID numbers.
    """
    value = rfc3925_decode(value, identifier_size=4)
    for i in value:
        value[i] = rfc3925_decode(value[i], identifier_size=1)
    return value
     
def _rfc1035Parse(domain_name):
    """
    Splits an FQDN on dots, outputting data like
    ['g', 'o', 'o', 'g', 'l', 'e', 2, 'c', 'a', 0], in conformance with
    RFC1035.
    
    :param str domain_name: The FQDN to be converted.
    :return list: The converted FQDN.
    """
    bytes = []
    for fragment in domain_name.split('.'):
        bytes += [len(fragment)] + [ord(c) for c in fragment]
    return bytes + [0]
    
    
[docs]class RFC(object):
    """
    A generic special RFC object, used to simplify the process of setting
    complex options.
    """
    _value = None #: The bytes associated with this object.
    
[docs]    def getValue(self):
        """
        Provides the type's data as a list of bytes.
        
        Note that this is the actual internal list; modifications to the list
        will be persistent.
        
        :return list: A list of bytes.
        """
        return self._value
         
    def __repr__(self):
        return "<%(name)s : %(value)r>" % {
         'name': self.__class__.__name__,
         'value': self._value,
        }
        
    def __nonzero__(self):
        return 1
        
    def __cmp__(self, other):
        """
        If ``other`` is an RFC instance, their values are compared. Otherwise,
        the internal list's value is compared directly to ``other``.
        """
        if isinstance(other, RFC):
            return cmp(self._value, other.getValue())
        return cmp(self._value, other)
         
[docs]class rfc1035_plus(RFC):
    def __init__(self, data):
        """
        Formats FQDNs as an RFC1035-formatted sequence.
        
        :param str data: The comma-delimited FQDNs to process.
        """
        self._value = []
        for token in (tok for tok in (t.strip() for t in data.split(',')) if tok):
            self._value += _rfc1035Parse(token)
             
[docs]class rfc2610_78(RFC):
    def __init__(self, mandatory, data):
        """
        Formats native IPv4s as encoded IPv4 addresses.
        
        :param bool mandatory: True if the IPv4 addresses have to be respected.
        :param str data: The comma-delimited IPv4s to process.
        """
        self._value = [int(mandatory)]
        for token in (tok for tok in (t.strip() for t in data.split(',')) if tok):
            self._value.extend(IPv4(token))
 
[docs]class rfc2610_79(RFC):
    def __init__(self, mandatory, data):
        """
        Formats scope-list data.
        
        :param bool mandatory: True if the scope-list has to be respected.
        :param str data: The scope-list to process.
        """
        self._value = [int(mandatory)] + [ord(c) for c in data.encode('utf-8')]
 
[docs]class rfc3361_120(RFC):
    def __init__(self, data):
        """
        Formats the given data into multiple IPv4 addresses or
        RFC1035-formatted strings.
        
        :param str data: The comma-delimited IPv4s or FQDNs to process.
        :except ValueError: Both IPv4s and FQDNs were provided.
        """
        ip_4_mode = False
        dns_mode = False
        
        self._value = []
        for token in (tok for tok in (t.strip() for t in data.split(',')) if tok):
            try:
                self._value.extend(IPv4(token))
                ip_4_mode = True
            except ValueError:
                self._value += _rfc1035Parse(token)
                dns_mode = True
                
        if ip_4_mode == dns_mode:
            raise ValueError("'%(data)s contains both IPv4 and DNS-based entries" % {
             'data': data,
            })
            
        self._value.insert(0, int(ip_4_mode))
 
[docs]class rfc3397_119(rfc1035_plus): pass
 
[docs]class rfc3442_121(RFC):
    def __init__(self, classless_addresses):
        """
        Accepts a sequence of CIDR entries and routers to produce classless
        static route byte-blocks.
        
        You'll probably want to put (("0.0.0.0", 0), packet.getOption("router"))
        at the start to ensure the default gateway is set as expected.
        
        :param classless_addresses: ((network IPv4, CIDR-mask), router IPv4)
            elements, like (("169.254.0.0", 16), "0.0.0.0").
        :except ValueError: An illegal address or mask was encountered.
        """
        self._value = []
        for ((ip, mask), router) in classless_addresses:
            ip = IPv4(ip)
            router = IPv4(router)
            if not 0 <= mask <= 32:
                raise ValueError("CIDR mask %(mask)i is not between 0 and 32" % {
                    'mask': mask,
                })
            width = mask / 8
            if mask % 8:
                width += 1
                
            self._value.append(mask)
            if width:
                self._value.extend(tuple(ip)[:width])
            self._value.extend(router)
             
[docs]class rfc3925_124(RFC):
    def __init__(self, data):
        """
        Sets `vendor_class` data.
        
        :param dict data: A dictionary of data-strings keyed by ID-ints.
        """
        self._value = []
        for (enterprise_number, payload) in sorted(data.items()):
            self._value += longToList(enterprise_number)
            self._value.append(chr(len(payload)))
            self._value += payload
 
[docs]class rfc3925_125(RFC):
    def __init__(self, data):
        """
        Sets `vendor_specific` data.
        
        :param dict data: A dictionary of dictionaries of data-strings, keyed
            by ID-ints at both levels.
        """
        self._value = []
        for (enterprise_number, payload) in sorted(data.items()):
            self._value += longToList(enterprise_number)
            
            subdata = []
            for (subopt_code, subpayload) in sorted(payload.items()):
                subdata.append(chr(subopt_code))
                subdata.append(chr(len(subpayload)))
                subdata += subpayload
                
            self._value.append(chr(len(subdata)))
            self._value += subdata
 
[docs]class rfc4174_83(RFC):
    def __init__(self, isns_functions, dd_access, admin_flags, isns_security, ips):
        """
        Sets iSNS configuration parameters.
        
        :param int isns_functions: A sixteen-bit value.
        :param int dd_access: A sixteen-bit value.
        :param int admin_flags: A sixteen-bit value.
        :param int isns_security: A thirty-two-bit value.
        :param str ips: Comma-delimited IPv4s to be processed.
        """
        isns_functions = intToList(isns_functions)
        dd_access = intToList(dd_access)
        admin_flags = intToList(admin_flags)
        isns_security = longToList(isns_security)
        
        self._value = isns_functions + dd_access + admin_flags + isns_security
        for token in [tok for tok in [t.strip() for t in ips.split(',')] if tok]:
            self._value.extend(IPv4(token))
 
[docs]class rfc4280_88(rfc1035_plus): pass
 
[docs]class rfc5223_137(rfc1035_plus): pass
 
[docs]class rfc5678_139(RFC):
    def __init__(self, values):
        """
        Formats the given data into multiple IPv4 addresses associated with
        sub-option codes.
        
        :param sequence values: A sequence of (code:int, IPv4s:string) elements.
        """
        self._value = []
        for (code, addresses) in values:
            self._value.append(code)
            for token in [tok for tok in [address.strip() for address in addresses.split(',')] if tok]:
                self._value.extend(IPv4(token))
                 
[docs]class rfc5678_140(RFC):
    def __init__(self, values):
        """
        Formats the given data into multiple RFC1035-formatted strings
        associated with sub-option codes.
        
        :param sequence values: A sequence of (code:int, FQDNs:string)
            elements.
        """
        self._value = []
        for (code, addresses) in values:
            self._value.append(code)
            self._value += rfc1035_plus(addresses).getValue()