Completed
Push — master ( 983a39...13a39b )
by John
02:40
created

sr_lookup_bootstrap()   B

Complexity

Conditions 5

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.1502

Importance

Changes 0
Metric Value
cc 5
dl 0
loc 29
rs 8.0894
c 0
b 0
f 0
ccs 9
cts 11
cp 0.8182
crap 5.1502
1
#!/usr/bin/env python3
2 4
"""This module is used for network connections; APIs, downloading, etc."""
3
4 4
import os  # filesystem read
5 4
import xml.etree.ElementTree  # XML parsing
6 4
import re  # regexes
7 4
import concurrent.futures  # multiprocessing/threading
8 4
import glob  # pem file lookup
9 4
import requests  # downloading
10 4
from bs4 import BeautifulSoup  # scraping
11 4
from bbarchivist import utilities  # parse filesize
12 4
from bbarchivist.bbconstants import SERVERS  # lookup servers
13
14 4
__author__ = "Thurask"
15 4
__license__ = "WTFPL v2"
16 4
__copyright__ = "Copyright 2015-2016 Thurask"
17
18
19 4
def grab_pem():
20
    """
21
    Work with either local cacerts or system cacerts.
22
    """
23 4
    try:
24 4
        pemfile = glob.glob(os.path.join(os.getcwd(), "cacert.pem"))[0]
25 4
    except IndexError:
26 4
        return requests.certs.where()  # no local cacerts
27
    else:
28 4
        return os.path.abspath(pemfile)  # local cacerts
29
30
31 4
def pem_wrapper(method):
32
    """
33
    Decorator to set REQUESTS_CA_BUNDLE.
34
35
    :param method: Method to use.
36
    :type method: function
37
    """
38 4
    def wrapper(*args, **kwargs):
39
        """
40
        Set REQUESTS_CA_BUNDLE before doing function.
41
        """
42 4
        os.environ["REQUESTS_CA_BUNDLE"] = grab_pem()
43 4
        return method(*args, **kwargs)
44 4
    return wrapper
45
46
47 4
def generic_soup_parser(url, session=None):
48
    """
49
    Get a BeautifulSoup HTML parser for some URL.
50
51
    :param url: The URL to check.
52
    :type url: str
53
54
    :param session: Requests session object, default is created on the fly.
55
    :type session: requests.Session()
56
    """
57 4
    session = requests.Session() if session is None else session
58 4
    req = session.get(url)
59 4
    soup = BeautifulSoup(req.content, "html.parser")
60 4
    return soup
61
62
63 4
@pem_wrapper
64 4
def get_length(url, session=None):
65
    """
66
    Get content-length header from some URL.
67
68
    :param url: The URL to check.
69
    :type url: str
70
71
    :param session: Requests session object, default is created on the fly.
72
    :type session: requests.Session()
73
    """
74 4
    session = requests.Session() if session is None else session
75 4
    if url is None:
76 4
        return 0
77 4
    try:
78 4
        heads = session.head(url)
79 4
        fsize = heads.headers['content-length']
80 4
        return int(fsize)
81 4
    except requests.ConnectionError:
82 4
        return 0
83
84
85 4
@pem_wrapper
86 4
def download(url, output_directory=None, session=None):
87
    """
88
    Download file from given URL.
89
90
    :param url: URL to download from.
91
    :type url: str
92
93
    :param output_directory: Download folder. Default is local.
94
    :type output_directory: str
95
96
    :param session: Requests session object, default is created on the fly.
97
    :type session: requests.Session()
98
    """
99 4
    session = requests.Session() if session is None else session
100 4
    output_directory = os.getcwd() if output_directory is None else output_directory
101 4
    lfname = url.split('/')[-1]
102 4
    sname = utilities.stripper(lfname)
103 4
    fname = os.path.join(output_directory, lfname)
104 4
    with open(fname, "wb") as file:
105 4
        req = session.get(url, stream=True)
106 4
        clength = req.headers['content-length']
107 4
        fsize = utilities.fsizer(clength)
108 4
        if req.status_code == 200:  # 200 OK
109 4
            print("DOWNLOADING {0} [{1}]".format(sname, fsize))
110 4
            for chunk in req.iter_content(chunk_size=1024):
111 4
                file.write(chunk)
112
        else:
113 4
            print("ERROR: HTTP {0} IN {1}".format(req.status_code, lfname))
114 4
    if os.stat(fname).st_size == 0:
115 4
        os.remove(fname)
116
117
118 4
def download_bootstrap(urls, outdir=None, workers=5, session=None):
119
    """
120
    Run downloaders for each file in given URL iterable.
121
122
    :param urls: URLs to download.
123
    :type urls: list
124
125
    :param outdir: Download folder. Default is handled in :func:`download`.
126
    :type outdir: str
127
128
    :param workers: Number of worker processes. Default is 5.
129
    :type workers: int
130
131
    :param session: Requests session object, default is created on the fly.
132
    :type session: requests.Session()
133
    """
134 4
    workers = len(urls) if len(urls) < workers else workers
135 4
    spinman = utilities.SpinManager()
136 4
    with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as xec:
137 4
        try:
138 4
            spinman.start()
139 4
            for url in urls:
140 4
                xec.submit(download, url, outdir, session)
141
        except (KeyboardInterrupt, SystemExit):
142
            xec.shutdown()
143
            spinman.stop()
144 4
    spinman.stop()
145 4
    utilities.spinner_clear()
146 4
    utilities.line_begin()
147
148
149 4
@pem_wrapper
150 4
def availability(url, session=None):
151
    """
152
    Check HTTP status code of given URL.
153
    200 or 301-308 is OK, else is not.
154
155
    :param url: URL to check.
156
    :type url: str
157
158
    :param session: Requests session object, default is created on the fly.
159
    :type session: requests.Session()
160
    """
161 4
    session = requests.Session() if session is None else session
162 4
    try:
163 4
        avlty = session.head(url)
164 4
        status = int(avlty.status_code)
165 4
        return status == 200 or 300 < status <= 308
166 4
    except requests.ConnectionError:
167 4
        return False
168
169
170 4
def clean_availability(results, server):
171
    """
172
    Clean availability for autolookup script.
173
174
    :param results: Result dict.
175
    :type results: dict(str: str)
176
177
    :param server: Server, key for result dict.
178
    :type server: str
179
    """
180 4
    marker = "PD" if server == "p" else server.upper()
181 4
    rel = results[server.lower()]
182 4
    avail = marker if rel != "SR not in system" and rel is not None else "  "
183 4
    return rel, avail
184
185
186 4
@pem_wrapper
187 4
def carrier_checker(mcc, mnc, session=None):
188
    """
189
    Query BlackBerry World to map a MCC and a MNC to a country and carrier.
190
191
    :param mcc: Country code.
192
    :type mcc: int
193
194
    :param mnc: Network code.
195
    :type mnc: int
196
197
    :param session: Requests session object, default is created on the fly.
198
    :type session: requests.Session()
199
    """
200 4
    session = requests.Session() if session is None else session
201 4
    url = "http://appworld.blackberry.com/ClientAPI/checkcarrier?homemcc={0}&homemnc={1}&devicevendorid=-1&pin=0".format(
202
        mcc, mnc)
203 4
    user_agent = {'User-agent': 'AppWorld/5.1.0.60'}
204 4
    req = session.get(url, headers=user_agent)
205 4
    root = xml.etree.ElementTree.fromstring(req.text)
206 4
    for child in root:
207 4
        if child.tag == "country":
208 4
            country = child.get("name")
209 4
        if child.tag == "carrier":
210 4
            carrier = child.get("name")
211 4
    return country, carrier
212
213
214 4
def return_npc(mcc, mnc):
215
    """
216
    Format MCC and MNC into a NPC.
217
218
    :param mcc: Country code.
219
    :type mcc: int
220
221
    :param mnc: Network code.
222
    :type mnc: int
223
    """
224 4
    return "{0}{1}30".format(str(mcc).zfill(3), str(mnc).zfill(3))
225
226
227 4
@pem_wrapper
228 4
def carrier_query(npc, device, upgrade=False, blitz=False, forced=None, session=None):
229
    """
230
    Query BlackBerry servers, check which update is out for a carrier.
231
232
    :param npc: MCC + MNC (see `func:return_npc`)
233
    :type npc: int
234
235
    :param device: Hexadecimal hardware ID.
236
    :type device: str
237
238
    :param upgrade: Whether to use upgrade files. False by default.
239
    :type upgrade: bool
240
241
    :param blitz: Whether or not to create a blitz package. False by default.
242
    :type blitz: bool
243
244
    :param forced: Force a software release.
245
    :type forced: str
246
247
    :param session: Requests session object, default is created on the fly.
248
    :type session: requests.Session()
249
    """
250 4
    session = requests.Session() if session is None else session
251 4
    upg = "upgrade" if upgrade else "repair"
252 4
    forced = "latest" if forced is None else forced
253 4
    url = "https://cs.sl.blackberry.com/cse/updateDetails/2.2/"
254 4
    query = '<?xml version="1.0" encoding="UTF-8"?>'
255 4
    query += '<updateDetailRequest version="2.2.1" authEchoTS="1366644680359">'
256 4
    query += "<clientProperties>"
257 4
    query += "<hardware>"
258 4
    query += "<pin>0x2FFFFFB3</pin><bsn>1128121361</bsn>"
259 4
    query += "<imei>004401139269240</imei>"
260 4
    query += "<id>0x{0}</id>".format(device)
261 4
    query += "</hardware>"
262 4
    query += "<network>"
263 4
    query += "<homeNPC>0x{0}</homeNPC>".format(npc)
264 4
    query += "<iccid>89014104255505565333</iccid>"
265 4
    query += "</network>"
266 4
    query += "<software>"
267 4
    query += "<currentLocale>en_US</currentLocale>"
268 4
    query += "<legalLocale>en_US</legalLocale>"
269 4
    query += "</software>"
270 4
    query += "</clientProperties>"
271 4
    query += "<updateDirectives>"
272 4
    query += '<allowPatching type="REDBEND">true</allowPatching>'
273 4
    query += "<upgradeMode>{0}</upgradeMode>".format(upg)
274 4
    query += "<provideDescriptions>false</provideDescriptions>"
275 4
    query += "<provideFiles>true</provideFiles>"
276 4
    query += "<queryType>NOTIFICATION_CHECK</queryType>"
277 4
    query += "</updateDirectives>"
278 4
    query += "<pollType>manual</pollType>"
279 4
    query += "<resultPackageSetCriteria>"
280 4
    query += '<softwareRelease softwareReleaseVersion="{0}" />'.format(forced)
281 4
    query += "<releaseIndependent>"
282 4
    query += '<packageType operation="include">application</packageType>'
283 4
    query += "</releaseIndependent>"
284 4
    query += "</resultPackageSetCriteria>"
285 4
    query += "</updateDetailRequest>"
286 4
    header = {"Content-Type": "text/xml;charset=UTF-8"}
287 4
    req = session.post(url, headers=header, data=query)
288 4
    return parse_carrier_xml(req.text, blitz)
289
290
291 4
def carrier_swver_get(root):
292
    """
293
    Get software release from carrier XML.
294
295
    :param root: ElementTree we're barking up.
296
    :type root: xml.etree.ElementTree.ElementTree
297
    """
298 4
    for child in root.iter("softwareReleaseMetadata"):
299 4
        swver = child.get("softwareReleaseVersion")
300 4
    return swver
301
302
303 4
def carrier_child_fileappend(child, files, baseurl, blitz=False):
304
    """
305
    Append bar file links to a list from a child element.
306
307
    :param child: Child element in use.
308
    :type child: xml.etree.ElementTree.Element
309
310
    :param files: Filelist.
311
    :type files: list(str)
312
313
    :param baseurl: Base URL, URL minus the filename.
314
    :type baseurl: str
315
316
    :param blitz: Whether or not to create a blitz package. False by default.
317
    :type blitz: bool
318
    """
319 4
    if not blitz:
320 4
        files.append(baseurl + child.get("path"))
321
    else:
322
        if child.get("type") not in ["system:radio", "system:desktop", "system:os"]:
323
            files.append(baseurl + child.get("path"))
324 4
    return files
325
326
327 4
def carrier_child_finder(root, files, baseurl, blitz=False):
328
    """
329
    Extract filenames, radio and OS from child elements.
330
331
    :param root: ElementTree we're barking up.
332
    :type root: xml.etree.ElementTree.ElementTree
333
334
    :param files: Filelist.
335
    :type files: list(str)
336
337
    :param baseurl: Base URL, URL minus the filename.
338
    :type baseurl: str
339
340
    :param blitz: Whether or not to create a blitz package. False by default.
341
    :type blitz: bool
342
    """
343 4
    osver = radver = ""
344 4
    for child in root.iter("package"):
345 4
        files = carrier_child_fileappend(child, files, baseurl, blitz)
346 4
        if child.get("type") == "system:radio":
347 4
            radver = child.get("version")
348 4
        elif child.get("type") == "system:desktop":
349 4
            osver = child.get("version")
350
        elif child.get("type") == "system:os":
351
            osver = child.get("version")
352 4
    return osver, radver, files
353
354
355 4
def parse_carrier_xml(data, blitz=False):
356
    """
357
    Parse the response to a carrier update request and return the juicy bits.
358
359
    :param data: The data to parse.
360
    :type data: xml
361
362
    :param blitz: Whether or not to create a blitz package. False by default.
363
    :type blitz: bool
364
    """
365 4
    root = xml.etree.ElementTree.fromstring(data)
366 4
    sw_exists = root.find('./data/content/softwareReleaseMetadata')
367 4
    swver = "N/A" if sw_exists is None else ""
368 4
    if sw_exists is not None:
369 4
        swver = carrier_swver_get(root)
370 4
    files = []
371 4
    package_exists = root.find('./data/content/fileSets/fileSet')
372 4
    osver = radver = ""
373 4
    if package_exists is not None:
374 4
        baseurl = "{0}/".format(package_exists.get("url"))
375 4
        osver, radver, files = carrier_child_finder(root, files, baseurl, blitz)
376 4
    return(swver, osver, radver, files)
377
378
379 4
@pem_wrapper
380 4
def sr_lookup(osver, server, session=None):
381
    """
382
    Software release lookup, with choice of server.
383
    :data:`bbarchivist.bbconstants.SERVERLIST` for server list.
384
385
    :param osver: OS version to lookup, 10.x.y.zzzz.
386
    :type osver: str
387
388
    :param server: Server to use.
389
    :type server: str
390
391
    :param session: Requests session object, default is created on the fly.
392
    :type session: requests.Session()
393
    """
394 4
    session = requests.Session() if session is None else session
395 4
    reg = re.compile(r"(\d{1,4}\.)(\d{1,4}\.)(\d{1,4}\.)(\d{1,4})")
396 4
    query = '<?xml version="1.0" encoding="UTF-8"?>'
397 4
    query += '<srVersionLookupRequest version="2.0.0"'
398 4
    query += ' authEchoTS="1366644680359">'
399 4
    query += '<clientProperties><hardware>'
400 4
    query += '<pin>0x2FFFFFB3</pin><bsn>1140011878</bsn>'
401 4
    query += '<imei>004402242176786</imei><id>0x8D00240A</id>'
402 4
    query += '<isBootROMSecure>true</isBootROMSecure>'
403 4
    query += '</hardware>'
404 4
    query += '<network>'
405 4
    query += '<vendorId>0x0</vendorId><homeNPC>0x60</homeNPC>'
406 4
    query += '<currentNPC>0x60</currentNPC><ecid>0x1</ecid>'
407 4
    query += '</network>'
408 4
    query += '<software><currentLocale>en_US</currentLocale>'
409 4
    query += '<legalLocale>en_US</legalLocale>'
410 4
    query += '<osVersion>{0}</osVersion>'.format(osver)
411 4
    query += '<omadmEnabled>false</omadmEnabled>'
412 4
    query += '</software></clientProperties>'
413 4
    query += '</srVersionLookupRequest>'
414 4
    header = {"Content-Type": "text/xml;charset=UTF-8"}
415 4
    try:
416 4
        req = session.post(server, headers=header, data=query, timeout=1)
417 4
    except (requests.exceptions.Timeout, requests.exceptions.ConnectionError):
418 4
        return "SR not in system"
419 4
    try:
420 4
        root = xml.etree.ElementTree.fromstring(req.text)
421 4
    except xml.etree.ElementTree.ParseError:
422 4
        return "SR not in system"
423
    else:
424 4
        packages = root.findall('./data/content/')
425 4
        for package in packages:
426 4
            if package.text is not None:
427 4
                match = reg.match(package.text)
428 4
                if match:
429 4
                    return package.text
430
                else:
431 4
                    return "SR not in system"
432
433
434 4
def sr_lookup_bootstrap(osv, session=None, noalpha2=False):
435
    """
436
    Run lookups for each server for given OS.
437
438
    :param osv: OS to check.
439
    :type osv: str
440
441
    :param session: Requests session object, default is created on the fly.
442
    :type session: requests.Session()
443
444
    :param noalpha2: Whether to skip Alpha2 server. Default is false.
445
    :type noalpha2: bool
446
    """
447 4
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as xec:
448 4
        try:
449 4
            results = {
450
                "p": None,
451
                "a1": None,
452
                "a2": None,
453
                "b1": None,
454
                "b2": None
455
            }
456 4
            if noalpha2:
457 4
                del results["a2"]
458 4
            for key in results:
459 4
                results[key] = xec.submit(sr_lookup, osv, SERVERS[key], session).result()
460 4
            return results
461
        except KeyboardInterrupt:
462
            xec.shutdown(wait=False)
463
464
465 4
@pem_wrapper
466 4
def available_bundle_lookup(mcc, mnc, device, session=None):
467
    """
468
    Check which software releases were ever released for a carrier.
469
470
    :param mcc: Country code.
471
    :type mcc: int
472
473
    :param mnc: Network code.
474
    :type mnc: int
475
476
    :param device: Hexadecimal hardware ID.
477
    :type device: str
478
479
    :param session: Requests session object, default is created on the fly.
480
    :type session: requests.Session()
481
    """
482 4
    session = requests.Session() if session is None else session
483 4
    server = "https://cs.sl.blackberry.com/cse/availableBundles/1.0.0/"
484 4
    npc = return_npc(mcc, mnc)
485 4
    query = '<?xml version="1.0" encoding="UTF-8"?>'
486 4
    query += '<availableBundlesRequest version="1.0.0" '
487 4
    query += 'authEchoTS="1366644680359">'
488 4
    query += '<deviceId><pin>0x2FFFFFB3</pin></deviceId>'
489 4
    query += '<clientProperties><hardware><id>0x{0}</id>'.format(device)
490 4
    query += '<isBootROMSecure>true</isBootROMSecure></hardware>'
491 4
    query += '<network><vendorId>0x0</vendorId><homeNPC>0x{0}</homeNPC>'.format(npc)
492 4
    query += '<currentNPC>0x{0}</currentNPC></network><software>'.format(npc)
493 4
    query += '<currentLocale>en_US</currentLocale>'
494 4
    query += '<legalLocale>en_US</legalLocale>'
495 4
    query += '<osVersion>10.0.0.0</osVersion>'
496 4
    query += '<radioVersion>10.0.0.0</radioVersion></software>'
497 4
    query += '</clientProperties><updateDirectives><bundleVersionFilter>'
498 4
    query += '</bundleVersionFilter></updateDirectives>'
499 4
    query += '</availableBundlesRequest>'
500 4
    header = {"Content-Type": "text/xml;charset=UTF-8"}
501 4
    req = session.post(server, headers=header, data=query)
502 4
    root = xml.etree.ElementTree.fromstring(req.text)
503 4
    package = root.find('./data/content')
504 4
    bundlelist = [child.attrib["version"] for child in package]
505 4
    return bundlelist
506
507
508 4
@pem_wrapper
509
def ptcrb_scraper(ptcrbid):
510
    """
511
    Get the PTCRB results for a given device.
512
513
    :param ptcrbid: Numerical ID from PTCRB (end of URL).
514
    :type ptcrbid: str
515
    """
516 4
    baseurl = "https://ptcrb.com/vendor/complete/view_complete_request_guest.cfm?modelid={0}".format(
517
        ptcrbid)
518 4
    soup = generic_soup_parser(baseurl)
519 4
    text = soup.get_text()
520 4
    text = text.replace("\r\n", " ")
521 4
    prelimlist = re.findall("OS .+[^\\n]", text, re.IGNORECASE)
522 4
    if not prelimlist:  # Priv
523 4
        prelimlist = re.findall(r"[A-Z]{3}[0-9]{3}[\s]", text)
524 4
    cleanlist = []
525 4
    for item in prelimlist:
526 4
        if not item.endswith("\r\n"):  # they should hire QC people...
527 4
            cleanlist.append(ptcrb_item_cleaner(item))
528 4
    return cleanlist
529
530
531 4
def ptcrb_item_cleaner(item):
532
    """
533
    Cleanup poorly formatted PTCRB entries written by an intern.
534
535
    :param item: The item to clean.
536
    :type item: str
537
    """
538 4
    item = item.replace("<td>", "")
539 4
    item = item.replace("</td>", "")
540 4
    item = item.replace("\n", "")
541 4
    item = item.replace(" (SR", ", SR")
542 4
    item = re.sub(r"\s?\((.*)$", "", item)
543 4
    item = re.sub(r"\sSV.*$", "", item)
544 4
    item = item.replace(")", "")
545 4
    item = item.replace(". ", ".")
546 4
    item = item.replace(";", "")
547 4
    item = item.replace("version", "Version")
548 4
    item = item.replace("Verison", "Version")
549 4
    if item.count("OS") > 1:
550 4
        templist = item.split("OS")
551 4
        templist[0] = "OS"
552 4
        item = "".join([templist[0], templist[1]])
553 4
    item = item.replace("SR", "SW Release")
554 4
    item = item.replace(" Version:", ":")
555 4
    item = item.replace("Version ", " ")
556 4
    item = item.replace(":1", ": 1")
557 4
    item = item.replace(", ", " ")
558 4
    item = item.replace("Software", "SW")
559 4
    item = item.replace("  ", " ")
560 4
    item = item.replace("OS ", "OS: ")
561 4
    item = item.replace("Radio ", "Radio: ")
562 4
    item = item.replace("Release ", "Release: ")
563 4
    spaclist = item.split(" ")
564 4
    if len(spaclist) > 1:
565 4
        while len(spaclist[1]) < 11:
566 4
            spaclist[1] += " "
567 4
        while len(spaclist[3]) < 11:
568 4
            spaclist[3] += " "
569
    else:
570 4
        spaclist.insert(0, "OS:")
571 4
    item = " ".join(spaclist)
572 4
    item = item.strip()
573 4
    return item
574
575
576 4
@pem_wrapper
577 4
def kernel_scraper(utils=False):
578
    """
579
    Scrape BlackBerry's GitHub kernel repo for available branches.
580
581
    :param utils: Check android-utils repo instead of android-linux-kernel. Default is False.
582
    :type utils: bool
583
    """
584 4
    repo = "android-utils" if utils else "android-linux-kernel"
585 4
    kernlist = []
586 4
    sess = requests.Session()
587 4
    for page in range(1, 10):
588 4
        url = "https://github.com/blackberry/{0}/branches/all?page={1}".format(repo, page)
589 4
        soup = generic_soup_parser(url, sess)
590 4
        if soup.find("div", {"class": "no-results-message"}):
591 4
            break
592
        else:
593 4
            text = soup.get_text()
594 4
            kernlist.extend(re.findall(r"msm[0-9]{4}\/[A-Z0-9]{6}", text, re.IGNORECASE))
595 4
    return kernlist
596
597
598 4
def root_generator(folder, build, variant="common"):
599
    """
600
    Generate roots for the SHAxxx hash lookup URLs.
601
602
    :param folder: Dictionary of variant: loader name pairs.
603
    :type folder: dict(str: str)
604
605
    :param build: Build to check, 3 letters + 3 numbers.
606
    :type build: str
607
608
    :param variant: Autoloader variant. Default is "common".
609
    :type variant: str
610
    """
611
    #Priv specific
612 4
    privx = "bbfoundation/hashfiles_priv/{0}".format(folder[variant])
613
    #DTEK50 specific
614 4
    dtek50x = "bbSupport/DTEK50" if build[:3] == "AAF" else "bbfoundation/hashfiles_priv/dtek50"
615
    #DTEK60 specific
616 4
    dtek60x = dtek50x  # still uses dtek50 folder, for some reason
617
    #Pack it up
618 4
    roots = {"Priv": privx, "DTEK50": dtek50x, "DTEK60": dtek60x}
619 4
    return roots
620
621
622 4
def make_droid_skeleton(method, build, device, variant="common"):
623
    """
624
    Make an Android autoloader/hash URL.
625
626
    :param method: None for regular OS links, "sha256/512" for SHA256 or 512 hash.
627
    :type method: str
628
629
    :param build: Build to check, 3 letters + 3 numbers.
630
    :type build: str
631
632
    :param device: Device to check.
633
    :type device: str
634
635
    :param variant: Autoloader variant. Default is "common".
636
    :type variant: str
637
    """
638 4
    folder = {"vzw-vzw": "verizon", "na-att": "att", "na-tmo": "tmo", "common": "default"}
639 4
    devices = {"Priv": "qc8992", "DTEK50": "qc8952_64_sfi", "DTEK60": "qc8996"}
640 4
    roots = root_generator(folder, build, variant)
641 4
    base = "bbry_{2}_autoloader_user-{0}-{1}".format(variant, build.upper(), devices[device])
642 4
    if method is None:
643 4
        skel = "https://bbapps.download.blackberry.com/Priv/{0}.zip".format(base)
644
    else:
645 4
        skel = "http://ca.blackberry.com/content/dam/{1}/{0}.{2}sum".format(base, roots[device], method.lower())
646 4
    return skel
647
648
649 4
def bulk_droid_skeletons(devs, build, method=None):
650
    """
651
    Prepare list of Android autoloader/hash URLs.
652
653
    :param devs: List of devices.
654
    :type devs: list(str)
655
656
    :param build: Build to check, 3 letters + 3 numbers.
657
    :type build: str
658
659
    :param method: None for regular OS links, "sha256/512" for SHA256 or 512 hash.
660
    :type method: str
661
    """
662 4
    carrier_variants = ("common", "vzw-vzw", "na-tmo", "na-att")  # device variants
663 4
    common_variants = ("common", )  # no Americans
664 4
    carrier_devices = ("Priv", )  # may this list never expand in the future
665 4
    skels = []
666 4
    for dev in devs:
667 4
        varlist = carrier_variants if dev in carrier_devices else common_variants
668 4
        for var in varlist:
669 4
            skel = make_droid_skeleton(method, build, dev, var)
670 4
            skels.append(skel)
671 4
    return skels
672
673
674 4
def prepare_droid_list(device):
675
    """
676
    Convert single devices to a list, if necessary.
677
678
    :param device: Device to check.
679
    :type device: str
680
    """
681 4
    if isinstance(device, list):
682 4
        devs = device
683
    else:
684 4
        devs = [device]
685 4
    return devs
686
687
688 4
def droid_scanner(build, device, method=None, session=None):
689
    """
690
    Check for Android autoloaders on BlackBerry's site.
691
692
    :param build: Build to check, 3 letters + 3 numbers.
693
    :type build: str
694
695
    :param device: Device to check.
696
    :type device: str
697
698
    :param method: None for regular OS links, "sha256/512" for SHA256 or 512 hash.
699
    :type method: str
700
701
    :param session: Requests session object, default is created on the fly.
702
    :type session: requests.Session()
703
    """
704 4
    devs = prepare_droid_list(device)
705 4
    skels = bulk_droid_skeletons(devs, build, method)
706 4
    with concurrent.futures.ThreadPoolExecutor(max_workers=len(skels)) as xec:
707 4
        results = []
708 4
        for skel in skels:
709 4
            avail = xec.submit(availability, skel, session)
710 4
            if avail.result():
711 4
                results.append(skel)
712 4
    return results if results else None
713
714
715 4
def chunker(iterable, inc):
716
    """
717
    Convert an iterable into a list of inc sized lists.
718
719
    :param chunker: Iterable to chunk.
720
    :type chunker: list
721
722
    :param inc: Increment; how big each chunk is.
723
    :type inc: int
724
    """
725 4
    chunks = [iterable[x:x+inc] for x in range(0, len(iterable), inc)]
726 4
    return chunks
727
728
729 4
def unicode_filter(intext):
730
    """
731
    Remove Unicode crap.
732
733
    :param intext: Text to filter.
734
    :type intext: str
735
    """
736 4
    return intext.replace("\u2013", "").strip()
737
738
739 4
def table_header_filter(ptag):
740
    """
741
    Validate p tag, to see if it's relevant.
742
743
    :param ptag: P tag.
744
    :type ptag: bs4.element.Tag
745
    """
746 4
    valid = ptag.find("b") and "BlackBerry" in ptag.text and not "experts" in ptag.text
747 4
    return valid
748
749
750 4
def table_headers(pees):
751
    """
752
    Generate table headers from list of p tags.
753
754
    :param pees: List of p tags.
755
    :type pees: list(bs4.element.Tag)
756
    """
757 4
    bolds = [x.text for x in pees if table_header_filter(x)]
758 4
    return bolds
759
760
761 4
@pem_wrapper
762
def loader_page_scraper():
763
    """
764
    Return scraped autoloader page.
765
    """
766 4
    url = "http://ca.blackberry.com/content/blackberry-com/en_ca/support/smartphones/Android-OS-Reload.html"
767 4
    sess = requests.Session()
768 4
    soup = generic_soup_parser(url, sess)
769 4
    tables = soup.find_all("table")
770 4
    headers = table_headers(soup.find_all("p"))
771 4
    for idx, table in enumerate(tables):
772 4
        print("~~~{0}~~~".format(headers[idx]))
773 4
        chunks = chunker(table.find_all("td"), 4)
774 4
        for chunk in chunks:
775 4
            key = unicode_filter(chunk[0].text)
776 4
            ver = unicode_filter(chunk[1].text)
777 4
            link = unicode_filter(chunk[2].find("a")["href"])
778 4
            print("{0}\n    {1}: {2}".format(key, ver, link))
779 4
        print(" ")
780
781
782 4
@pem_wrapper
783 4
def base_metadata(url, session=None):
784
    """
785
    Get BBNDK metadata, base function.
786
787
    :param url: URL to check.
788
    :type url: str
789
790
    :param session: Requests session object, default is created on the fly.
791
    :type session: requests.Session()
792
    """
793 4
    session = requests.Session() if session is None else session
794 4
    req = session.get(url)
795 4
    data = req.content
796 4
    entries = data.split(b"\n")
797 4
    metadata = [entry.split(b",")[1].decode("utf-8") for entry in entries if entry]
798 4
    return metadata
799
800
801 4
def ndk_metadata(session=None):
802
    """
803
    Get BBNDK target metadata.
804
805
    :param session: Requests session object, default is created on the fly.
806
    :type session: requests.Session()
807
    """
808 4
    data = base_metadata("http://downloads.blackberry.com/upr/developers/update/bbndk/metadata", session)
809 4
    metadata = [entry for entry in data if entry.startswith(("10.0", "10.1", "10.2"))]
810 4
    return metadata
811
812
813 4
def sim_metadata(session=None):
814
    """
815
    Get BBNDK simulator metadata.
816
817
    :param session: Requests session object, default is created on the fly.
818
    :type session: requests.Session()
819
    """
820 4
    metadata = base_metadata("http://downloads.blackberry.com/upr/developers/update/bbndk/simulator/simulator_metadata", session)
821 4
    return metadata
822
823
824 4
def runtime_metadata(session=None):
825
    """
826
    Get BBNDK runtime metadata.
827
828
    :param session: Requests session object, default is created on the fly.
829
    :type session: requests.Session()
830
    """
831 4
    metadata = base_metadata("http://downloads.blackberry.com/upr/developers/update/bbndk/runtime/runtime_metadata", session)
832 4
    return metadata
833
834
835 4
def series_generator(osversion):
836
    """
837
    Generate series/branch name from OS version.
838
839
    :param osversion: OS version.
840
    :type osversion: str
841
    """
842 4
    splits = osversion.split(".")
843 4
    return "BB{0}_{1}_{2}".format(*splits[0:3])
844
845
846 4
@pem_wrapper
847 4
def devalpha_urls(osversion, skel, session=None):
848
    """
849
    Check individual Dev Alpha autoloader URLs.
850
851
    :param osversion: OS version.
852
    :type osversion: str
853
854
    :param skel: Individual skeleton format to try.
855
    :type skel: str
856
857
    :param session: Requests session object, default is created on the fly.
858
    :type session: requests.Session()
859
    """
860 4
    session = requests.Session() if session is None else session
861 4
    url = "http://downloads.blackberry.com/upr/developers/downloads/{0}{1}.exe".format(skel, osversion)
862 4
    req = session.head(url)
863 4
    if req.status_code == 200:
864 4
        finals = (url, req.headers["content-length"])
865
    else:
866 4
        finals = ()
867 4
    return finals
868
869
870 4
def devalpha_urls_bootstrap(osversion, skeletons, session=None):
871
    """
872
    Get list of valid Dev Alpha autoloader URLs.
873
874
    :param osversion: OS version.
875
    :type osversion: str
876
877
    :param skeletons: List of skeleton formats to try.
878
    :type skeletons: list
879
880
    :param session: Requests session object, default is created on the fly.
881
    :type session: requests.Session()
882
    """
883 4
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as xec:
884 4
        try:
885 4
            finals = {}
886 4
            skels = skeletons
887 4
            for idx, skel in enumerate(skeletons):
888 4
                if "<SERIES>" in skel:
889 4
                    skels[idx] = skel.replace("<SERIES>", series_generator(osversion))
890 4
            for skel in skels:
891 4
                final = xec.submit(devalpha_urls, osversion, skel, session).result()
892 4
                if final:
893 4
                    finals[final[0]] = final[1]
894 4
            return finals
895
        except KeyboardInterrupt:
896
            xec.shutdown(wait=False)
897
898
899 4
def dev_dupe_cleaner(finals):
900
    """
901
    Clean duplicate autoloader entries.
902
903
    :param finals: Dict of URL:content-length pairs.
904
    :type finals: dict(str: str)
905
    """
906 4
    revo = {}
907 4
    for key, val in finals.items():
908 4
        revo.setdefault(val, set()).add(key)
909 4
    dupelist = [val for key, val in revo.items() if len(val) > 1]
910 4
    for dupe in dupelist:
911 4
        for entry in dupe:
912 4
            if "DevAlpha" in entry:
913 4
                del finals[entry]
914
    return finals
915