1# meid.py - functions for handling Mobile Equipment Identifiers (MEIDs)
2#
3# Copyright (C) 2010-2017 Arthur de Jong
4#
5# This library is free software; you can redistribute it and/or
6# modify it under the terms of the GNU Lesser General Public
7# License as published by the Free Software Foundation; either
8# version 2.1 of the License, or (at your option) any later version.
9#
10# This library is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13# Lesser General Public License for more details.
14#
15# You should have received a copy of the GNU Lesser General Public
16# License along with this library; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18# 02110-1301 USA
19
20"""MEID (Mobile Equipment Identifier).
21
22The Mobile Equipment Identifier is used to identify a physical piece of
23CDMA mobile station equipment.
24
25>>> validate('AF 01 23 45 0A BC DE C')
26'AF0123450ABCDE'
27>>> validate('29360 87365 0070 3710 0')
28'AF0123450ABCDE'
29>>> validate('29360 87365 0070 3710 0', strip_check_digit=False)
30'AF0123450ABCDEC'
31>>> validate('29360 87365 0070 3710 1')
32Traceback (most recent call last):
33    ...
34InvalidChecksum: ...
35>>> format('af0123450abcDEC', add_check_digit=True)
36'AF 01 23 45 0A BC DE C'
37>>> format('af0123450abcDEC', format='dec', add_check_digit=True)
38'29360 87365 0070 3710 0'
39"""
40
41from stdnum.exceptions import *
42from stdnum.util import clean, isdigits
43
44
45_hex_alphabet = '0123456789ABCDEF'
46
47
48def _cleanup(number):
49    """Remove any grouping information from the number and removes surrounding
50    whitespace."""
51    return clean(number, ' -').strip().upper()
52
53
54def _ishex(number):
55    for x in number:
56        if x not in _hex_alphabet:
57            return False
58    return True
59
60
61def _parse(number):
62    number = _cleanup(number)
63    if len(number) in (14, 15):
64        # 14 or 15 digit hex representation
65        if not _ishex(number):
66            raise InvalidFormat()
67        return number[0:14], number[14:]
68    elif len(number) in (18, 19):
69        # 18-digit decimal representation
70        if not isdigits(number):
71            raise InvalidFormat()
72        return number[0:18], number[18:]
73    else:
74        raise InvalidLength()
75
76
77def calc_check_digit(number):
78    """Calculate the check digit for the number. The number should not
79    already have a check digit."""
80    # both the 18-digit decimal format and the 14-digit hex format
81    # containing only decimal digits should use the decimal Luhn check
82    from stdnum import luhn
83    if isdigits(number):
84        return luhn.calc_check_digit(number)
85    else:
86        return luhn.calc_check_digit(number, alphabet=_hex_alphabet)
87
88
89def compact(number, strip_check_digit=True):
90    """Convert the MEID number to the minimal (hexadecimal) representation.
91    This strips grouping information, removes surrounding whitespace and
92    converts to hexadecimal if needed. If the check digit is to be preserved
93    and conversion is done a new check digit is recalculated."""
94    # first parse the number
95    number, cd = _parse(number)
96    # strip check digit if needed
97    if strip_check_digit:
98        cd = ''
99    # convert to hex if needed
100    if len(number) == 18:
101        number = '%08X%06X' % (int(number[0:10]), int(number[10:18]))
102        if cd:
103            cd = calc_check_digit(number)
104    # put parts back together again
105    return number + cd
106
107
108def _bit_length(n):
109    """Return the number of bits necessary to store the number in binary."""
110    try:
111        return n.bit_length()
112    except AttributeError:  # pragma: no cover (Python 2.6 only)
113        import math
114        return int(math.log(n, 2)) + 1
115
116
117def validate(number, strip_check_digit=True):
118    """Check if the number is a valid MEID number. This converts the
119    representation format of the number (if it is decimal it is not converted
120    to hexadecimal)."""
121    from stdnum import luhn
122    # first parse the number
123    number, cd = _parse(number)
124    if len(number) == 18:
125        # decimal format can be easily determined
126        if cd:
127            luhn.validate(number + cd)
128        # convert to hex
129        manufacturer_code = int(number[0:10])
130        serial_num = int(number[10:18])
131        if _bit_length(manufacturer_code) > 32 or _bit_length(serial_num) > 24:
132            raise InvalidComponent()
133        number = '%08X%06X' % (manufacturer_code, serial_num)
134        cd = calc_check_digit(number)
135    elif isdigits(number):
136        # if the remaining hex format is fully decimal it is an IMEI number
137        from stdnum import imei
138        imei.validate(number + cd)
139    else:
140        # normal hex Luhn validation
141        if cd:
142            luhn.validate(number + cd, alphabet=_hex_alphabet)
143    if strip_check_digit:
144        cd = ''
145    return number + cd
146
147
148def is_valid(number):
149    """Check if the number is a valid MEID number."""
150    try:
151        return bool(validate(number))
152    except ValidationError:
153        return False
154
155
156def format(number, separator=' ', format=None, add_check_digit=False):
157    """Reformat the number to the standard presentation format. The separator
158    used can be provided. If the format is specified (either 'hex' or 'dec')
159    the number is reformatted in that format, otherwise the current
160    representation is kept. If add_check_digit is True a check digit will be
161    added if it is not present yet."""
162    # first parse the number
163    number, cd = _parse(number)
164    # format conversions if needed
165    if format == 'dec' and len(number) == 14:
166        # convert to decimal
167        number = '%010d%08d' % (int(number[0:8], 16), int(number[8:14], 16))
168        if cd:
169            cd = calc_check_digit(number)
170    elif format == 'hex' and len(number) == 18:
171        # convert to hex
172        number = '%08X%06X' % (int(number[0:10]), int(number[10:18]))
173        if cd:
174            cd = calc_check_digit(number)
175    # see if we need to add a check digit
176    if add_check_digit and not cd:
177        cd = calc_check_digit(number)
178    # split number according to format
179    if len(number) == 14:
180        number = [number[i * 2:i * 2 + 2]
181                  for i in range(7)] + [cd]
182    else:
183        number = (number[:5], number[5:10], number[10:14], number[14:], cd)
184    return separator.join(x for x in number if x)
185
186
187def to_binary(number):
188    """Convert the number to its binary representation (without the check
189    digit)."""
190    from binascii import a2b_hex
191    return a2b_hex(compact(number, strip_check_digit=True))
192
193
194def to_pseudo_esn(number):
195    """Convert the provided MEID to a pseudo ESN (pESN). The ESN is returned
196    in compact hexadecimal representation."""
197    import hashlib
198    # return the last 6 digits of the SHA1  hash prefixed with the reserved
199    # manufacturer code
200    return '80' + hashlib.sha1(to_binary(number)).hexdigest()[-6:].upper()
201