Completed
Push — master ( 66b0f5...21ba8d )
by John
02:44
created

root_generator()   B

Complexity

Conditions 2

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

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