Completed
Push — master ( ef95c0...e1ca39 )
by John
06:17
created

parse_tcl_check()   A

Complexity

Conditions 1

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 15
ccs 4
cts 4
cp 1
crap 1
rs 9.4285
c 0
b 0
f 0
1
#!/usr/bin/env python3
2 1
"""This module is used for network connections; APIs, downloading, etc."""
3
4 1
import os  # filesystem read
5 1
try:
6 1
    from defusedxml import ElementTree  # safer XML parsing
7
except (ImportError, AttributeError):
8
    from xml.etree import ElementTree  # XML parsing
9 1
import re  # regexes
10 1
import concurrent.futures  # multiprocessing/threading
11 1
import glob  # pem file lookup
12 1
import hashlib  # salt
13 1
import time  # salt
14 1
import random  # salt
15 1
import requests  # downloading
16
from bs4 import BeautifulSoup  # scraping
17 1
from bbarchivist import utilities  # parse filesize
18 1
from bbarchivist.bbconstants import SERVERS  # lookup servers
19 1
20
__author__ = "Thurask"
21
__license__ = "WTFPL v2"
22 1
__copyright__ = "2015-2017 Thurask"
23
24
25
def grab_pem():
26 1
    """
27 1
    Work with either local cacerts or system cacerts.
28 1
    """
29 1
    try:
30
        pemfile = glob.glob(os.path.join(os.getcwd(), "cacert.pem"))[0]
31 1
    except IndexError:
32
        return requests.certs.where()  # no local cacerts
33
    else:
34 1
        return os.path.abspath(pemfile)  # local cacerts
35
36
37
def pem_wrapper(method):
38
    """
39
    Decorator to set REQUESTS_CA_BUNDLE.
40
41 1
    :param method: Method to use.
42
    :type method: function
43
    """
44
    def wrapper(*args, **kwargs):
45 1
        """
46 1
        Set REQUESTS_CA_BUNDLE before doing function.
47 1
        """
48
        os.environ["REQUESTS_CA_BUNDLE"] = grab_pem()
49
        return method(*args, **kwargs)
50 1
    return wrapper
51
52
53
def generic_session(session=None):
54
    """
55
    Create a Requests session object on the fly, if need be.
56
57 1
    :param session: Requests session object, created if this is None.
58 1
    :type session: requests.Session()
59
    """
60
    sess = requests.Session() if session is None else session
61 1
    return sess
62
63
64
def generic_soup_parser(url, session=None):
65
    """
66
    Get a BeautifulSoup HTML parser for some URL.
67
68
    :param url: The URL to check.
69
    :type url: str
70
71 1
    :param session: Requests session object, default is created on the fly.
72 1
    :type session: requests.Session()
73 1
    """
74 1
    session = generic_session(session)
75
    req = session.get(url)
76
    soup = BeautifulSoup(req.content, "html.parser")
77 1
    return soup
78 1
79
80
@pem_wrapper
81
def get_length(url, session=None):
82
    """
83
    Get content-length header from some URL.
84
85
    :param url: The URL to check.
86
    :type url: str
87
88 1
    :param session: Requests session object, default is created on the fly.
89 1
    :type session: requests.Session()
90 1
    """
91 1
    session = generic_session(session)
92 1
    if url is None:
93 1
        return 0
94 1
    try:
95 1
        heads = session.head(url)
96 1
        fsize = heads.headers['content-length']
97
        return int(fsize)
98
    except requests.ConnectionError:
99 1
        return 0
100 1
101
102
@pem_wrapper
103
def download(url, output_directory=None, session=None):
104
    """
105
    Download file from given URL.
106
107
    :param url: URL to download from.
108
    :type url: str
109
110
    :param output_directory: Download folder. Default is local.
111
    :type output_directory: str
112
113 1
    :param session: Requests session object, default is created on the fly.
114 1
    :type session: requests.Session()
115 1
    """
116 1
    session = generic_session(session)
117 1
    output_directory = utilities.dirhandler(output_directory, os.getcwd())
118 1
    lfname = url.split('/')[-1]
119 1
    sname = utilities.stripper(lfname)
120
    fname = os.path.join(output_directory, lfname)
121
    download_writer(url, fname, lfname, sname, session)
122 1
    remove_empty_download(fname)
123
124
125
def remove_empty_download(fname):
126
    """
127
    Remove file if it's empty.
128
129 1
    :param fname: File path.
130 1
    :type fname: str
131
    """
132
    if os.stat(fname).st_size == 0:
133 1
        os.remove(fname)
134
135
136
def download_writer(url, fname, lfname, sname, session=None):
137
    """
138
    Download file and write to disk.
139
140
    :param url: URL to download from.
141
    :type url: str
142
143
    :param fname: File path.
144
    :type fname: str
145
146
    :param lfname: Long filename.
147
    :type lfname: str
148
149
    :param sname: Short name, for printing to screen.
150
    :type sname: str
151
152 1
    :param session: Requests session object, default is created on the fly.
153 1
    :type session: requests.Session()
154 1
    """
155 1
    with open(fname, "wb") as file:
156 1
        req = session.get(url, stream=True)
157 1
        clength = req.headers['content-length']
158 1
        fsize = utilities.fsizer(clength)
159 1
        if req.status_code == 200:  # 200 OK
160
            print("DOWNLOADING {0} [{1}]".format(sname, fsize))
161 1
            for chunk in req.iter_content(chunk_size=1024):
162
                file.write(chunk)
163
        else:
164 1
            print("ERROR: HTTP {0} IN {1}".format(req.status_code, lfname))
165
166
167
def download_bootstrap(urls, outdir=None, workers=5, session=None):
168
    """
169
    Run downloaders for each file in given URL iterable.
170
171
    :param urls: URLs to download.
172
    :type urls: list
173
174
    :param outdir: Download folder. Default is handled in :func:`download`.
175
    :type outdir: str
176
177
    :param workers: Number of worker processes. Default is 5.
178
    :type workers: int
179
180 1
    :param session: Requests session object, default is created on the fly.
181 1
    :type session: requests.Session()
182 1
    """
183 1
    workers = len(urls) if len(urls) < workers else workers
184 1
    spinman = utilities.SpinManager()
185 1
    with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as xec:
186 1
        try:
187
            spinman.start()
188
            for url in urls:
189
                xec.submit(download, url, outdir, session)
190 1
        except (KeyboardInterrupt, SystemExit):
191 1
            xec.shutdown()
192 1
            spinman.stop()
193
    spinman.stop()
194
    utilities.spinner_clear()
195 1
    utilities.line_begin()
196 1
197
198
@pem_wrapper
199
def availability(url, session=None):
200
    """
201
    Check HTTP status code of given URL.
202
    200 or 301-308 is OK, else is not.
203
204
    :param url: URL to check.
205
    :type url: str
206
207 1
    :param session: Requests session object, default is created on the fly.
208 1
    :type session: requests.Session()
209 1
    """
210 1
    session = generic_session(session)
211 1
    try:
212 1
        avlty = session.head(url)
213 1
        status = int(avlty.status_code)
214
        return status == 200 or 300 < status <= 308
215
    except requests.ConnectionError:
216 1
        return False
217
218
219
def clean_availability(results, server):
220
    """
221
    Clean availability for autolookup script.
222
223
    :param results: Result dict.
224
    :type results: dict(str: str)
225
226 1
    :param server: Server, key for result dict.
227 1
    :type server: str
228 1
    """
229 1
    marker = "PD" if server == "p" else server.upper()
230
    rel = results[server.lower()]
231
    avail = marker if rel != "SR not in system" and rel is not None else "  "
232 1
    return rel, avail
233 1
234
235
@pem_wrapper
236
def tcl_check(curef, session=None):
237
    """
238
    Check TCL server for updates.
239
240
    :param curef: PRD of the phone variant to check.
241
    :type curef: str
242
243
    :param session: Requests session object, default is created on the fly.
244
    :type session: requests.Session()
245
    """
246 1
    sess = generic_session(session)
247 1
    geturl = "http://g2master-us-east.tctmobile.com/check.php"
248
    params = {"id": "543212345000000", "curef": curef, "fv": "AAM481", "mode": 4, "type": "Firmware", "cltp": 2010, "cktp": 2, "rtd": 1, "chnl": 2}
249 1
    req = sess.get(geturl, params=params)
250 1
    if req.status_code == 200:
251 1
        return(req.text)
0 ignored issues
show
Unused Code Coding Style introduced by
There is an unnecessary parenthesis after return.
Loading history...
252 1
    else:
253 1
        return None
254 1
255 1
256 1
def parse_tcl_check(data):
257 1
    """
258
    Extract version and file info from TCL update server response.
259
260 1
    :param data: The data to parse.
261
    :type data: str
262
    """
263
    root = ElementTree.fromstring(data)
264
    tv = root.find("VERSION").find("TV").text
0 ignored issues
show
Coding Style Naming introduced by
The name tv does not conform to the variable naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
265
    fwid = root.find("FIRMWARE").find("FW_ID").text
266
    fileinfo = root.find("FIRMWARE").find("FILESET").find("FILE")
267
    filename = fileinfo.find("FILENAME").text
268
    filesize = fileinfo.find("SIZE").text
269
    filehash = fileinfo.find("CHECKSUM").text
270 1
    return tv, fwid, filename, filesize, filehash
271
272
273 1
def tcl_salt():
274 1
    """
275
    Generate salt value for TCL server tools.
276
    """
277
    millis = round(time.time() * 1000)
278
    tail = "{0:06d}".format(random.randint(0, 999999))
279
    return "{0}{1}".format(str(millis), tail)
280
281
282
def vkhash(curef, tv, fwid, salt):
0 ignored issues
show
Coding Style Naming introduced by
The name tv does not conform to the argument naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
283
    """
284
    Generate hash from TCL update server variables.
285
286
    :param curef: PRD of the phone variant to check.
287
    :type curef: str
288
289
    :param tv: Target software version.
290
    :type tv: str
291
292
    :param fwid: Firmware ID for desired download file.
293
    :type fwid: str
294
295
    :param salt: Salt hash.
296 1
    :type salt: str
297 1
    """
298 1
    vdkey = "1271941121281905392291845155542171963889169361242115412511417616616958244916823523421516924614377131161951402261451161002051042011757216713912611682532031591181861081836612643016596231212872211620511861302106446924625728571011411121471811641125920123641181975581511602312222261817375462445966911723844130106116313122624220514"
299 1
    query = "id={0}&salt={1}&curef={2}&fv={3}&tv={4}&type={5}&fw_id={6}&mode={7}&cltp={8}{9}".format("543212345000000", salt, curef, "AAM481", tv, "Firmware", fwid, 4, 2010, vdkey)
300 1
    engine = hashlib.sha1()
301 1
    engine.update(bytes(query, "utf-8"))
302 1
    return engine.hexdigest()
303 1
304 1
305 1
@pem_wrapper
306 1
def tcl_download_request(curef, tv, fwid, salt, vkh, session=None):
0 ignored issues
show
Coding Style Naming introduced by
The name tv does not conform to the argument naming conventions ([a-z_][a-z0-9_]{2,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
307 1
    """
308 1
    Check TCL server for download URLs.
309 1
310 1
    :param curef: PRD of the phone variant to check.
311 1
    :type curef: str
312 1
313 1
    :param tv: Target software version.
314 1
    :type tv: str
315 1
316 1
    :param fwid: Firmware ID for desired download file.
317 1
    :type fwid: str
318 1
319 1
    :param salt: Salt hash.
320 1
    :type salt: str
321 1
322 1
    :param vkh: VDKey-based hash.
323 1
    :type vkh: str
324 1
325 1
    :param session: Requests session object, default is created on the fly.
326 1
    :type session: requests.Session()
327 1
    """
328 1
    sess = generic_session(session)
329 1
    posturl = "http://g2master-us-east.tctmobile.com/download_request.php"
330 1
    params = {"id": "543212345000000", "curef": curef, "fv": "AAM481", "mode": 4, "type": "Firmware", "tv": tv, "fw_id": fwid, "salt": salt, "vk": vkh, "cltp": 2010}
331 1
    req = sess.post(posturl, data=params)
332 1
    if req.status_code == 200:
333 1
        return req.text
334 1
    else:
335
        return None
336
337 1
338
def parse_tcl_download_request(body):
339
    """
340
    Extract file URL from TCL update server response.
341
342
    :param data: The data to parse.
343
    :type data: str
344 1
    """
345 1
    root = ElementTree.fromstring(body)
346 1
    slave = root.find("SLAVE_LIST").find("SLAVE").text
347
    dlurl = root.find("FILE_LIST").find("FILE").find("DOWNLOAD_URL").text
348
    return "http://{0}{1}".format(slave, dlurl)
349 1
350
351
@pem_wrapper
352
def carrier_checker(mcc, mnc, session=None):
353
    """
354
    Query BlackBerry World to map a MCC and a MNC to a country and carrier.
355
356
    :param mcc: Country code.
357
    :type mcc: int
358
359
    :param mnc: Network code.
360
    :type mnc: int
361
362
    :param session: Requests session object, default is created on the fly.
363
    :type session: requests.Session()
364
    """
365 1
    session = generic_session(session)
366 1
    url = "http://appworld.blackberry.com/ClientAPI/checkcarrier?homemcc={0}&homemnc={1}&devicevendorid=-1&pin=0".format(mcc, mnc)
367
    user_agent = {'User-agent': 'AppWorld/5.1.0.60'}
368 1
    req = session.get(url, headers=user_agent)
369 1
    root = ElementTree.fromstring(req.text)
370 1
    for child in root:
371
        if child.tag == "country":
372
            country = child.get("name")
373 1
        if child.tag == "carrier":
374
            carrier = child.get("name")
375
    return country, carrier
376
377
378
def return_npc(mcc, mnc):
379
    """
380
    Format MCC and MNC into a NPC.
381
382
    :param mcc: Country code.
383
    :type mcc: int
384
385
    :param mnc: Network code.
386
    :type mnc: int
387
    """
388
    return "{0}{1}30".format(str(mcc).zfill(3), str(mnc).zfill(3))
389 1
390 1
391 1
@pem_wrapper
392 1
def carrier_query(npc, device, upgrade=False, blitz=False, forced=None, session=None):
393 1
    """
394 1
    Query BlackBerry servers, check which update is out for a carrier.
395 1
396 1
    :param npc: MCC + MNC (see `func:return_npc`)
397 1
    :type npc: int
398 1
399
    :param device: Hexadecimal hardware ID.
400
    :type device: str
401 1
402
    :param upgrade: Whether to use upgrade files. False by default.
403
    :type upgrade: bool
404
405
    :param blitz: Whether or not to create a blitz package. False by default.
406
    :type blitz: bool
407
408
    :param forced: Force a software release.
409
    :type forced: str
410
411 1
    :param session: Requests session object, default is created on the fly.
412 1
    :type session: requests.Session()
413 1
    """
414 1
    session = generic_session(session)
415 1
    upg = "upgrade" if upgrade else "repair"
416 1
    forced = "latest" if forced is None else forced
417 1
    url = "https://cs.sl.blackberry.com/cse/updateDetails/2.2/"
418 1
    query = '<?xml version="1.0" encoding="UTF-8"?>'
419 1
    query += '<updateDetailRequest version="2.2.1" authEchoTS="1366644680359">'
420 1
    query += "<clientProperties>"
421 1
    query += "<hardware>"
422 1
    query += "<pin>0x2FFFFFB3</pin><bsn>1128121361</bsn>"
423
    query += "<imei>004401139269240</imei>"
424
    query += "<id>0x{0}</id>".format(device)
425 1
    query += "</hardware>"
426 1
    query += "<network>"
427
    query += "<homeNPC>0x{0}</homeNPC>".format(npc)
428
    query += "<iccid>89014104255505565333</iccid>"
429
    query += "</network>"
430
    query += "<software>"
431
    query += "<currentLocale>en_US</currentLocale>"
432
    query += "<legalLocale>en_US</legalLocale>"
433
    query += "</software>"
434
    query += "</clientProperties>"
435
    query += "<updateDirectives>"
436
    query += '<allowPatching type="REDBEND">true</allowPatching>'
437
    query += "<upgradeMode>{0}</upgradeMode>".format(upg)
438
    query += "<provideDescriptions>false</provideDescriptions>"
439
    query += "<provideFiles>true</provideFiles>"
440 1
    query += "<queryType>NOTIFICATION_CHECK</queryType>"
441 1
    query += "</updateDirectives>"
442 1
    query += "<pollType>manual</pollType>"
443 1
    query += "<resultPackageSetCriteria>"
444 1
    query += '<softwareRelease softwareReleaseVersion="{0}" />'.format(forced)
445 1
    query += "<releaseIndependent>"
446 1
    query += '<packageType operation="include">application</packageType>'
447 1
    query += "</releaseIndependent>"
448 1
    query += "</resultPackageSetCriteria>"
449 1
    query += "</updateDetailRequest>"
450 1
    header = {"Content-Type": "text/xml;charset=UTF-8"}
451 1
    req = session.post(url, headers=header, data=query)
452 1
    return parse_carrier_xml(req.text, blitz)
453 1
454 1
455 1
def carrier_swver_get(root):
456 1
    """
457 1
    Get software release from carrier XML.
458 1
459 1
    :param root: ElementTree we're barking up.
460 1
    :type root: xml.etree.ElementTree.ElementTree
461
    """
462
    for child in root.iter("softwareReleaseMetadata"):
463 1
        swver = child.get("softwareReleaseVersion")
464
    return swver
465
466
467
def carrier_child_fileappend(child, files, baseurl, blitz=False):
468
    """
469
    Append bar file links to a list from a child element.
470
471
    :param child: Child element in use.
472
    :type child: xml.etree.ElementTree.Element
473
474
    :param files: Filelist.
475
    :type files: list(str)
476 1
477 1
    :param baseurl: Base URL, URL minus the filename.
478 1
    :type baseurl: str
479 1
480 1
    :param blitz: Whether or not to create a blitz package. False by default.
481 1
    :type blitz: bool
482
    """
483 1
    if not blitz:
484 1
        files.append(baseurl + child.get("path"))
485
    else:
486
        if child.get("type") not in ["system:radio", "system:desktop", "system:os"]:
487 1
            files.append(baseurl + child.get("path"))
488
    return files
489
490
491
def carrier_child_finder(root, files, baseurl, blitz=False):
492
    """
493
    Extract filenames, radio and OS from child elements.
494 1
495 1
    :param root: ElementTree we're barking up.
496 1
    :type root: xml.etree.ElementTree.ElementTree
497 1
498
    :param files: Filelist.
499 1
    :type files: list(str)
500 1
501
    :param baseurl: Base URL, URL minus the filename.
502
    :type baseurl: str
503 1
504
    :param blitz: Whether or not to create a blitz package. False by default.
505
    :type blitz: bool
506
    """
507
    osver = radver = ""
508
    for child in root.iter("package"):
509
        files = carrier_child_fileappend(child, files, baseurl, blitz)
510 1
        if child.get("type") == "system:radio":
511 1
            radver = child.get("version")
512 1
        elif child.get("type") == "system:desktop":
513 1
            osver = child.get("version")
514 1
        elif child.get("type") == "system:os":
515 1
            osver = child.get("version")
516 1
    return osver, radver, files
517
518
519 1
def parse_carrier_xml(data, blitz=False):
520
    """
521
    Parse the response to a carrier update request and return the juicy bits.
522
523
    :param data: The data to parse.
524
    :type data: xml
525
526
    :param blitz: Whether or not to create a blitz package. False by default.
527
    :type blitz: bool
528
    """
529
    root = ElementTree.fromstring(data)
530
    sw_exists = root.find('./data/content/softwareReleaseMetadata')
531
    swver = "N/A" if sw_exists is None else ""
532 1
    if sw_exists is not None:
533 1
        swver = carrier_swver_get(root)
534 1
    files = []
535
    package_exists = root.find('./data/content/fileSets/fileSet')
536
    osver = radver = ""
537
    if package_exists is not None:
538
        baseurl = "{0}/".format(package_exists.get("url"))
539
        osver, radver, files = carrier_child_finder(root, files, baseurl, blitz)
540
    return(swver, osver, radver, files)
541 1
542 1
543 1
@pem_wrapper
544 1
def sr_lookup(osver, server, session=None):
545 1
    """
546 1
    Software release lookup, with choice of server.
547
    :data:`bbarchivist.bbconstants.SERVERLIST` for server list.
548
549
    :param osver: OS version to lookup, 10.x.y.zzzz.
550
    :type osver: str
551 1
552 1
    :param server: Server to use.
553
    :type server: str
554
555
    :param session: Requests session object, default is created on the fly.
556
    :type session: requests.Session()
557
    """
558
    query = '<?xml version="1.0" encoding="UTF-8"?>'
559
    query += '<srVersionLookupRequest version="2.0.0"'
560
    query += ' authEchoTS="1366644680359">'
561
    query += '<clientProperties><hardware>'
562
    query += '<pin>0x2FFFFFB3</pin><bsn>1140011878</bsn>'
563
    query += '<imei>004402242176786</imei><id>0x8D00240A</id>'
564
    query += '<isBootROMSecure>true</isBootROMSecure>'
565
    query += '</hardware>'
566
    query += '<network>'
567
    query += '<vendorId>0x0</vendorId><homeNPC>0x60</homeNPC>'
568 1
    query += '<currentNPC>0x60</currentNPC><ecid>0x1</ecid>'
569 1
    query += '</network>'
570 1
    query += '<software><currentLocale>en_US</currentLocale>'
571 1
    query += '<legalLocale>en_US</legalLocale>'
572 1
    query += '<osVersion>{0}</osVersion>'.format(osver)
573 1
    query += '<omadmEnabled>false</omadmEnabled>'
574 1
    query += '</software></clientProperties>'
575 1
    query += '</srVersionLookupRequest>'
576 1
    reqtext = sr_lookup_poster(query, server, session)
577 1
    packtext = sr_lookup_xmlparser(reqtext)
578 1
    return packtext
579 1
580 1
581 1
def sr_lookup_poster(query, server, session=None):
582 1
    """
583 1
    Post the XML payload for a software release lookup.
584 1
585 1
    :param query: XML payload.
586 1
    :type query: str
587 1
588 1
    :param server: Server to use.
589 1
    :type server: str
590 1
591 1
    :param session: Requests session object, default is created on the fly.
592
    :type session: requests.Session()
593
    """
594 1
    session = generic_session(session)
595 1
    header = {"Content-Type": "text/xml;charset=UTF-8"}
596
    try:
597
        req = session.post(server, headers=header, data=query, timeout=1)
598
    except (requests.exceptions.Timeout, requests.exceptions.ConnectionError):
599
        reqtext = "SR not in system"
600
    else:
601
        reqtext = req.text
602
    return reqtext
603
604
605 1
def sr_lookup_xmlparser(reqtext):
606
    """
607 1
    Take the text of a software lookup request response and parse it as XML.
608 1
609 1
    :param reqtext: Response text, hopefully XML formatted.
610 1
    :type reqtext: str
611 1
    """
612 1
    try:
613 1
        root = ElementTree.fromstring(reqtext)
614 1
    except ElementTree.ParseError:
615 1
        packtext = "SR not in system"
616 1
    else:
617 1
        packtext = sr_lookup_extractor(root)
618 1
    return packtext
619 1
620
621
def sr_lookup_extractor(root):
622 1
    """
623
    Take an ElementTree and extract a software release from it.
624
625
    :param root: ElementTree we're barking up.
626
    :type root: xml.etree.ElementTree.ElementTree
627
    """
628
    reg = re.compile(r"(\d{1,4}\.)(\d{1,4}\.)(\d{1,4}\.)(\d{1,4})")
629
    packages = root.findall('./data/content/')
630
    for package in packages:
631
        if package.text is not None:
632 1
            match = reg.match(package.text)
633 1
            packtext = package.text if match else "SR not in system"
634 1
            return packtext
635
636
637 1
def sr_lookup_bootstrap(osv, session=None, no2=False):
638
    """
639
    Run lookups for each server for given OS.
640
641
    :param osv: OS to check.
642
    :type osv: str
643
644 1
    :param session: Requests session object, default is created on the fly.
645 1
    :type session: requests.Session()
646 1
647 1
    :param no2: Whether to skip Alpha2/Beta2 servers. Default is false.
648 1
    :type no2: bool
649 1
    """
650 1
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as xec:
651 1
        try:
652 1
            results = {
653 1
                "p": None,
654 1
                "a1": None,
655 1
                "a2": None,
656 1
                "b1": None,
657 1
                "b2": None
658 1
            }
659 1
            if no2:
660 1
                del results["a2"]
661 1
                del results["b2"]
662 1
            for key in results:
663 1
                results[key] = xec.submit(sr_lookup, osv, SERVERS[key], session).result()
664 1
            return results
665 1
        except KeyboardInterrupt:
666 1
            xec.shutdown(wait=False)
667 1
668 1
669 1
@pem_wrapper
670 1
def available_bundle_lookup(mcc, mnc, device, session=None):
671 1
    """
672 1
    Check which software releases were ever released for a carrier.
673
674 1
    :param mcc: Country code.
675 1
    :type mcc: int
676 1
677 1
    :param mnc: Network code.
678
    :type mnc: int
679
680 1
    :param device: Hexadecimal hardware ID.
681 1
    :type device: str
682
683
    :param session: Requests session object, default is created on the fly.
684
    :type session: requests.Session()
685
    """
686
    session = generic_session(session)
687
    server = "https://cs.sl.blackberry.com/cse/availableBundles/1.0.0/"
688
    npc = return_npc(mcc, mnc)
689
    query = '<?xml version="1.0" encoding="UTF-8"?>'
690
    query += '<availableBundlesRequest version="1.0.0" '
691 1
    query += 'authEchoTS="1366644680359">'
692 1
    query += '<deviceId><pin>0x2FFFFFB3</pin></deviceId>'
693 1
    query += '<clientProperties><hardware><id>0x{0}</id>'.format(device)
694 1
    query += '<isBootROMSecure>true</isBootROMSecure></hardware>'
695 1
    query += '<network><vendorId>0x0</vendorId><homeNPC>0x{0}</homeNPC>'.format(npc)
696 1
    query += '<currentNPC>0x{0}</currentNPC></network><software>'.format(npc)
697 1
    query += '<currentLocale>en_US</currentLocale>'
698 1
    query += '<legalLocale>en_US</legalLocale>'
699
    query += '<osVersion>10.0.0.0</osVersion>'
700 1
    query += '<radioVersion>10.0.0.0</radioVersion></software>'
701 1
    query += '</clientProperties><updateDirectives><bundleVersionFilter>'
702 1
    query += '</bundleVersionFilter></updateDirectives>'
703
    query += '</availableBundlesRequest>'
704
    header = {"Content-Type": "text/xml;charset=UTF-8"}
705 1
    req = session.post(server, headers=header, data=query)
706
    root = ElementTree.fromstring(req.text)
707
    package = root.find('./data/content')
708
    bundlelist = [child.attrib["version"] for child in package]
709
    return bundlelist
710
711
712
@pem_wrapper
713
def ptcrb_scraper(ptcrbid, session=None):
714
    """
715
    Get the PTCRB results for a given device.
716
717
    :param ptcrbid: Numerical ID from PTCRB (end of URL).
718
    :type ptcrbid: str
719 1
720
    :param session: Requests session object, default is created on the fly.
721 1
    :type session: requests.Session()
722
    """
723 1
    baseurl = "https://ptcrb.com/vendor/complete/view_complete_request_guest.cfm?modelid={0}".format(
724
        ptcrbid)
725 1
    sess = generic_session(session)
726 1
    sess.headers.update({"Referer": "https://ptcrb.com/vendor/complete/complete_request.cfm"})
727
    soup = generic_soup_parser(baseurl, sess)
728
    text = soup.get_text()
729 1
    text = text.replace("\r\n", " ")
730
    prelimlist = re.findall("OS .+[^\\n]", text, re.IGNORECASE)
731
    if not prelimlist:  # Priv
732
        prelimlist = re.findall(r"[A-Z]{3}[0-9]{3}[\s]", text)
733
    cleanlist = []
734
    for item in prelimlist:
735
        if not item.endswith("\r\n"):  # they should hire QC people...
736
            cleanlist.append(ptcrb_item_cleaner(item))
737
    return cleanlist
738
739
740
def space_pad(instring, minlength):
741
    """
742
    Pad a string with spaces until it's the minimum length.
743
744
    :param instring: String to pad.
745 1
    :type instring: str
746 1
747 1
    :param minlength: Pad while len(instring) < minlength.
748 1
    :type minlength: int
749
    """
750 1
    while len(instring) < minlength:
751 1
        instring += " "
752
    return instring
753
754 1
755
def ptcrb_item_cleaner(item):
756
    """
757
    Cleanup poorly formatted PTCRB entries written by an intern.
758
759
    :param item: The item to clean.
760
    :type item: str
761
    """
762
    item = item.replace("<td>", "")
763
    item = item.replace("</td>", "")
764
    item = item.replace("\n", "")
765
    item = item.replace(" (SR", ", SR")
766
    item = re.sub(r"\s?\((.*)$", "", item)
767
    item = re.sub(r"\sSV.*$", "", item)
768
    item = item.replace(")", "")
769
    item = item.replace(". ", ".")
770 1
    item = item.replace(";", "")
771 1
    item = item.replace("version", "Version")
772 1
    item = item.replace("Verison", "Version")
773 1
    if item.count("OS") > 1:
774 1
        templist = item.split("OS")
775 1
        templist[0] = "OS"
776
        item = "".join([templist[0], templist[1]])
777 1
    item = item.replace("SR", "SW Release")
778 1
    item = item.replace(" Version:", ":")
779
    item = item.replace("Version ", " ")
780
    item = item.replace(":1", ": 1")
781 1
    item = item.replace(", ", " ")
782
    item = item.replace("Software", "SW")
783
    item = item.replace("  ", " ")
784
    item = item.replace("OS ", "OS: ")
785
    item = item.replace("Radio ", "Radio: ")
786
    item = item.replace("Release ", "Release: ")
787
    spaclist = item.split(" ")
788
    if len(spaclist) > 1:
789
        spaclist[1] = space_pad(spaclist[1], 11)
790
        spaclist[3] = space_pad(spaclist[3], 11)
791
    else:
792
        spaclist.insert(0, "OS:")
793
    item = " ".join(spaclist)
794
    item = item.strip()
795
    return item
796
797
798 1
@pem_wrapper
799 1
def kernel_scraper(utils=False, session=None):
800 1
    """
801 1
    Scrape BlackBerry's GitHub kernel repo for available branches.
802 1
803 1
    :param utils: Check android-utils repo instead of android-linux-kernel. Default is False.
804 1
    :type utils: bool
805
806
    :param session: Requests session object, default is created on the fly.
807 1
    :type session: requests.Session()
808
    """
809
    repo = "android-utils" if utils else "android-linux-kernel"
810
    kernlist = []
811
    sess = generic_session(session)
812
    for page in range(1, 10):
813
        url = "https://github.com/blackberry/{0}/branches/all?page={1}".format(repo, page)
814
        soup = generic_soup_parser(url, sess)
815
        if soup.find("div", {"class": "no-results-message"}):
816
            break
817
        else:
818
            text = soup.get_text()
819
            kernlist.extend(re.findall(r"msm[0-9]{4}\/[A-Z0-9]{6}", text, re.IGNORECASE))
820 1
    return kernlist
821
822
823
def root_generator(folder, build, variant="common"):
824 1
    """
825 1
    Generate roots for the SHAxxx hash lookup URLs.
826 1
827 1
    :param folder: Dictionary of variant: loader name pairs.
828 1
    :type folder: dict(str: str)
829 1
830 1
    :param build: Build to check, 3 letters + 3 numbers.
831 1
    :type build: str
832 1
833
    :param variant: Autoloader variant. Default is "common".
834
    :type variant: str
835 1
    """
836
    #Priv specific
837
    privx = "bbfoundation/hashfiles_priv/{0}".format(folder[variant])
838
    #DTEK50 specific
839
    dtek50x = "bbSupport/DTEK50" if build[:3] == "AAF" else "bbfoundation/hashfiles_priv/dtek50"
840
    #DTEK60 specific
841
    dtek60x = dtek50x  # still uses dtek50 folder, for some reason
842 1
    #Pack it up
843 1
    roots = {"Priv": privx, "DTEK50": dtek50x, "DTEK60": dtek60x}
844
    return roots
845 1
846 1
847
def make_droid_skeleton_bbm(method, build, device, variant="common"):
848
    """
849 1
    Make an Android autoloader/hash URL, on the BB Mobile site.
850
851
    :param method: None for regular OS links, "sha256/512" for SHA256 or 512 hash.
852
    :type method: str
853
854
    :param build: Build to check, 3 letters + 3 numbers.
855
    :type build: str
856
857
    :param device: Device to check.
858
    :type device: str
859
860
    :param variant: Autoloader variant. Default is "common".
861
    :type variant: str
862
    """
863
    devices = {"KEYone": "qc8953"}
864
    base = "bbry_{2}_autoloader_user-{0}-{1}".format(variant, build.upper(), devices[device])
865 1
    if method is None:
866 1
        skel = "http://54.247.87.13/softwareupgrade/BBM/{0}.zip".format(base)
867 1
    else:
868 1
        skel = "http://54.247.87.13/softwareupgrade/BBM/{0}.{1}sum".format(base, method.lower())
869 1
    return skel
870 1
871 1
872 1
def make_droid_skeleton_og(method, build, device, variant="common"):
873 1
    """
874
    Make an Android autoloader/hash URL, on the original site.
875
876 1
    :param method: None for regular OS links, "sha256/512" for SHA256 or 512 hash.
877
    :type method: str
878
879
    :param build: Build to check, 3 letters + 3 numbers.
880
    :type build: str
881
882
    :param device: Device to check.
883
    :type device: str
884
885
    :param variant: Autoloader variant. Default is "common".
886 1
    :type variant: str
887 1
    """
888
    folder = {"vzw-vzw": "verizon", "na-att": "att", "na-tmo": "tmo", "common": "default"}
889
    devices = {"Priv": "qc8992", "DTEK50": "qc8952_64_sfi", "DTEK60": "qc8996"}
890 1
    roots = root_generator(folder, build, variant)
891
    base = "bbry_{2}_autoloader_user-{0}-{1}".format(variant, build.upper(), devices[device])
892
    if method is None:
893
        skel = "https://bbapps.download.blackberry.com/Priv/{0}.zip".format(base)
894
    else:
895
        skel = "https://ca.blackberry.com/content/dam/{1}/{0}.{2}sum".format(base, roots[device], method.lower())
896
    return skel
897 1
898
899
def make_droid_skeleton(method, build, device, variant="common"):
900 1
    """
901
    Make an Android autoloader/hash URL.
902
903
    :param method: None for regular OS links, "sha256/512" for SHA256 or 512 hash.
904
    :type method: str
905
906
    :param build: Build to check, 3 letters + 3 numbers.
907 1
    :type build: str
908 1
909
    :param device: Device to check.
910
    :type device: str
911 1
912
    :param variant: Autoloader variant. Default is "common".
913
    :type variant: str
914
    """
915
    # No Aurora
916
    oglist = ("Priv", "DTEK50", "DTEK60")  # BlackBerry
917
    bbmlist = ("KEYone")   # BB Mobile
918 1
    if device in oglist:
919 1
        skel = make_droid_skeleton_og(method, build, device, variant)
920
    elif device in bbmlist:
921
        skel = make_droid_skeleton_bbm(method, build, device, variant)
922 1
    return skel
923 1
924
925
def bulk_droid_skeletons(devs, build, method=None):
926
    """
927
    Prepare list of Android autoloader/hash URLs.
928
929
    :param devs: List of devices.
930 1
    :type devs: list(str)
931 1
932 1
    :param build: Build to check, 3 letters + 3 numbers.
933
    :type build: str
934
935 1
    :param method: None for regular OS links, "sha256/512" for SHA256 or 512 hash.
936
    :type method: str
937
    """
938
    carrier_variants = {
939
        "Priv": ("common", "vzw-vzw", "na-tmo", "na-att"),
940
        "KEYone": ("common", "usa-vzw", "usa-sprint", "global-att")  # verify this
941
    }
942 1
    common_variants = ("common", )  # for single-variant devices
943 1
    carrier_devices = ("Priv", )  # add KEYone when verified
944 1
    skels = []
945 1
    for dev in devs:
946 1
        varlist = carrier_variants[dev] if dev in carrier_devices else common_variants
947 1
        for var in varlist:
948
            skel = make_droid_skeleton(method, build, dev, var)
949
            skels.append(skel)
950 1
    return skels
951
952
953
def prepare_droid_list(device):
954
    """
955
    Convert single devices to a list, if necessary.
956
957 1
    :param device: Device to check.
958 1
    :type device: str
959 1
    """
960 1
    if isinstance(device, list):
961 1
        devs = device
962 1
    else:
963
        devs = [device]
964
    return devs
965 1
966
967
def droid_scanner(build, device, method=None, session=None):
968
    """
969
    Check for Android autoloaders on BlackBerry's site.
970
971
    :param build: Build to check, 3 letters + 3 numbers.
972
    :type build: str
973
974
    :param device: Device to check.
975
    :type device: str
976
977
    :param method: None for regular OS links, "sha256/512" for SHA256 or 512 hash.
978 1
    :type method: str
979 1
980 1
    :param session: Requests session object, default is created on the fly.
981 1
    :type session: requests.Session()
982 1
    """
983
    devs = prepare_droid_list(device)
984
    skels = bulk_droid_skeletons(devs, build, method)
985 1
    with concurrent.futures.ThreadPoolExecutor(max_workers=len(skels)) as xec:
986
        results = []
987
        for skel in skels:
988
            avail = xec.submit(availability, skel, session)
989
            if avail.result():
990
                results.append(skel)
991
    return results if results else None
992 1
993 1
994 1
def chunker(iterable, inc):
995
    """
996
    Convert an iterable into a list of inc sized lists.
997 1
998
    :param iterable: Iterable to chunk.
999
    :type iterable: list/tuple/string
1000
1001
    :param inc: Increment; how big each chunk is.
1002
    :type inc: int
1003
    """
1004 1
    chunks = [iterable[x:x+inc] for x in range(0, len(iterable), inc)]
1005 1
    return chunks
1006 1
1007 1
1008
def unicode_filter(intext):
1009
    """
1010 1
    Remove Unicode crap.
1011 1
1012
    :param intext: Text to filter.
1013
    :type intext: str
1014
    """
1015
    return intext.replace("\u2013", "").strip()
1016
1017
1018
def table_header_filter(ptag):
1019
    """
1020
    Validate p tag, to see if it's relevant.
1021 1
1022 1
    :param ptag: P tag.
1023 1
    :type ptag: bs4.element.Tag
1024 1
    """
1025 1
    valid = ptag.find("b") and "BlackBerry" in ptag.text and not "experts" in ptag.text
1026 1
    return valid
1027
1028
1029 1
def table_headers(pees):
1030
    """
1031
    Generate table headers from list of p tags.
1032
1033
    :param pees: List of p tags.
1034
    :type pees: list(bs4.element.Tag)
1035
    """
1036 1
    bolds = [x.text for x in pees if table_header_filter(x)]
1037 1
    return bolds
1038 1
1039
1040
@pem_wrapper
1041 1
def loader_page_scraper(session=None):
1042
    """
1043
    Return scraped autoloader pages.
1044
1045
    :param session: Requests session object, default is created on the fly.
1046
    :type session: requests.Session()
1047
    """
1048 1
    session = generic_session(session)
1049 1
    loader_page_scraper_og(session)
1050
    loader_page_scraper_bbm(session)
1051
1052 1
1053
def loader_page_scraper_og(session=None):
1054
    """
1055
    Return scraped autoloader page, original site.
1056
1057
    :param session: Requests session object, default is created on the fly.
1058
    :type session: requests.Session()
1059 1
    """
1060 1
    url = "https://ca.blackberry.com/support/smartphones/Android-OS-Reload.html"
1061
    soup = generic_soup_parser(url, session)
1062
    tables = soup.find_all("table")
1063 1
    headers = table_headers(soup.find_all("p"))
1064
    for idx, table in enumerate(tables):
1065
        loader_page_chunker_og(idx, table, headers)
1066
1067
1068
def loader_page_scraper_bbm(session=None):
1069
    """
1070 1
    Return scraped autoloader page, new site.
1071 1
1072
    :param session: Requests session object, default is created on the fly.
1073
    :type session: requests.Session()
1074 1
    """
1075 1
    url = "https://www.blackberrymobile.com/support/reload-software/"
1076
    soup = generic_soup_parser(url, session)
1077
    ulls = soup.find_all("ul", {"class": re.compile("list-two special-.")})[1:]
1078
    print("~~~BlackBerry KEYone~~~")
1079
    for ull in ulls:
1080
        loader_page_chunker_bbm(ull)
1081
1082
1083
def loader_page_chunker_og(idx, table, headers):
1084
    """
1085
    Given a loader page table, chunk it into lists of table cells.
1086
1087
    :param idx: Index of enumerating tables.
1088 1
    :type idx: int
1089 1
1090 1
    :param table: HTML table tag.
1091 1
    :type table: bs4.element.Tag
1092 1
1093
    :param headers: List of table headers.
1094 1
    :type headers: list(str)
1095 1
    """
1096
    print("~~~{0}~~~".format(headers[idx]))
1097
    chunks = chunker(table.find_all("td"), 4)
1098 1
    for chunk in chunks:
1099
        loader_page_printer(chunk)
1100
    print(" ")
1101
1102
1103
def loader_page_chunker_bbm(ull):
1104
    """
1105
    Given a loader page list, chunk it into lists of list items.
1106
1107
    :param ull: HTML unordered list tag.
1108 1
    :type ull: bs4.element.Tag
1109 1
    """
1110 1
    chunks = chunker(ull.find_all("li"), 3)
1111 1
    for chunk in chunks:
1112 1
        loader_page_printer(chunk)
1113
1114
1115 1
def loader_page_printer(chunk):
1116
    """
1117
    Print individual cell texts given a list of table cells.
1118
1119
    :param chunk: List of td tags.
1120
    :type chunk: list(bs4.element.Tag)
1121
    """
1122
    key = unicode_filter(chunk[0].text)
1123
    ver = unicode_filter(chunk[1].text)
1124
    link = unicode_filter(chunk[2].find("a")["href"])
1125
    print("{0}\n    {1}: {2}".format(key, ver, link))
1126
1127
1128
@pem_wrapper
1129
def base_metadata(url, session=None):
1130
    """
1131 1
    Get BBNDK metadata, base function.
1132 1
1133 1
    :param url: URL to check.
1134 1
    :type url: str
1135 1
1136 1
    :param session: Requests session object, default is created on the fly.
1137 1
    :type session: requests.Session()
1138
    """
1139
    session = generic_session(session)
1140 1
    req = session.get(url)
1141
    data = req.content
1142
    entries = data.split(b"\n")
1143
    metadata = [entry.split(b",")[1].decode("utf-8") for entry in entries if entry]
1144
    return metadata
1145
1146
1147
def ndk_metadata(session=None):
1148
    """
1149
    Get BBNDK target metadata.
1150
1151
    :param session: Requests session object, default is created on the fly.
1152
    :type session: requests.Session()
1153 1
    """
1154 1
    data = base_metadata("http://downloads.blackberry.com/upr/developers/update/bbndk/metadata", session)
1155 1
    metadata = [entry for entry in data if entry.startswith(("10.0", "10.1", "10.2"))]
1156
    return metadata
1157
1158
1159
def sim_metadata(session=None):
1160 1
    """
1161
    Get BBNDK simulator metadata.
1162
1163
    :param session: Requests session object, default is created on the fly.
1164
    :type session: requests.Session()
1165
    """
1166
    metadata = base_metadata("http://downloads.blackberry.com/upr/developers/update/bbndk/simulator/simulator_metadata", session)
1167 1
    return metadata
1168 1
1169 1
1170 1
def runtime_metadata(session=None):
1171
    """
1172
    Get BBNDK runtime metadata.
1173 1
1174
    :param session: Requests session object, default is created on the fly.
1175
    :type session: requests.Session()
1176
    """
1177
    metadata = base_metadata("http://downloads.blackberry.com/upr/developers/update/bbndk/runtime/runtime_metadata", session)
1178
    return metadata
1179
1180
1181
def series_generator(osversion):
1182
    """
1183 1
    Generate series/branch name from OS version.
1184 1
1185 1
    :param osversion: OS version.
1186 1
    :type osversion: str
1187 1
    """
1188
    splits = osversion.split(".")
1189
    return "BB{0}_{1}_{2}".format(*splits[0:3])
1190 1
1191
1192
@pem_wrapper
1193
def devalpha_urls(osversion, skel, session=None):
1194
    """
1195
    Check individual Dev Alpha autoloader URLs.
1196
1197 1
    :param osversion: OS version.
1198 1
    :type osversion: str
1199 1
1200 1
    :param skel: Individual skeleton format to try.
1201
    :type skel: str
1202
1203
    :param session: Requests session object, default is created on the fly.
1204
    :type session: requests.Session()
1205
    """
1206
    session = generic_session(session)
1207
    url = "http://downloads.blackberry.com/upr/developers/downloads/{0}{1}.exe".format(skel, osversion)
1208
    req = session.head(url)
1209
    if req.status_code == 200:
1210
        finals = (url, req.headers["content-length"])
1211
    else:
1212
        finals = ()
1213
    return finals
1214
1215
1216
def devalpha_urls_serieshandler(osversion, skeletons):
1217
    """
1218
    Process list of candidate Dev Alpha autoloader URLs.
1219
1220
    :param osversion: OS version.
1221
    :type osversion: str
1222
1223
    :param skeletons: List of skeleton formats to try.
1224
    :type skeletons: list
1225
    """
1226
    skels = skeletons
1227
    for idx, skel in enumerate(skeletons):
1228
        if "<SERIES>" in skel:
1229
            skels[idx] = skel.replace("<SERIES>", series_generator(osversion))
1230
    return skels
1231
1232
1233
def devalpha_urls_bulk(osversion, skeletons, xec, session=None):
1234
    """
1235
    Construct list of valid Dev Alpha autoloader URLs.
1236
1237
    :param osversion: OS version.
1238
    :type osversion: str
1239
1240
    :param skeletons: List of skeleton formats to try.
1241
    :type skeletons: list
1242
1243
    :param xec: ThreadPoolExecutor instance.
1244
    :type xec: concurrent.futures.ThreadPoolExecutor
1245
1246
    :param session: Requests session object, default is created on the fly.
1247
    :type session: requests.Session()
1248
    """
1249
    finals = {}
1250
    skels = devalpha_urls_serieshandler(osversion, skeletons)
1251
    for skel in skels:
1252
        final = xec.submit(devalpha_urls, osversion, skel, session).result()
1253
        if final:
1254
            finals[final[0]] = final[1]
1255
    return finals
1256
1257
1258
def devalpha_urls_bootstrap(osversion, skeletons, session=None):
1259
    """
1260
    Get list of valid Dev Alpha autoloader URLs.
1261
1262
    :param osversion: OS version.
1263
    :type osversion: str
1264
1265
    :param skeletons: List of skeleton formats to try.
1266
    :type skeletons: list
1267
1268
    :param session: Requests session object, default is created on the fly.
1269
    :type session: requests.Session()
1270
    """
1271
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as xec:
1272
        try:
1273
            return devalpha_urls_bulk(osversion, skeletons, xec, session)
1274
        except KeyboardInterrupt:
1275
            xec.shutdown(wait=False)
1276
1277
1278
def dev_dupe_dicter(finals):
1279
    """
1280
    Prepare dictionary to clean duplicate autoloaders.
1281
1282
    :param finals: Dict of URL:content-length pairs.
1283
    :type finals: dict(str: str)
1284
    """
1285
    revo = {}
1286
    for key, val in finals.items():
1287
        revo.setdefault(val, set()).add(key)
1288
    return revo
1289
1290
1291
def dev_dupe_remover(finals, dupelist):
1292
    """
1293
    Filter dictionary of autoloader entries.
1294
1295
    :param finals: Dict of URL:content-length pairs.
1296
    :type finals: dict(str: str)
1297
1298
    :param dupelist: List of duplicate URLs.
1299
    :type duplist: list(str)
1300
    """
1301
    for dupe in dupelist:
1302
        for entry in dupe:
1303
            if "DevAlpha" in entry:
1304
                del finals[entry]
1305
    return finals
1306
1307
1308
def dev_dupe_cleaner(finals):
1309
    """
1310
    Clean duplicate autoloader entries.
1311
1312
    :param finals: Dict of URL:content-length pairs.
1313
    :type finals: dict(str: str)
1314
    """
1315
    revo = dev_dupe_dicter(finals)
1316
    dupelist = [val for key, val in revo.items() if len(val) > 1]
1317
    finals = dev_dupe_remover(finals, dupelist)
1318
    return finals
1319