catalog.py (4321B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 import argparse 6 import plistlib 7 import ssl 8 import sys 9 from io import BytesIO 10 from urllib.request import urlopen 11 from xml.dom import minidom 12 13 import certifi 14 from mozpack.macpkg import Pbzx, uncpio, unxar 15 16 17 def get_english(dict, default=None): 18 english = dict.get("English") 19 if english is None: 20 english = dict.get("en", default) 21 return english 22 23 24 def get_content_at(url): 25 ssl_context = ssl.create_default_context(cafile=certifi.where()) 26 f = urlopen(url, context=ssl_context) 27 return f.read() 28 29 30 def get_plist_at(url): 31 return plistlib.loads(get_content_at(url)) 32 33 34 def show_package_content(url, digest=None, size=None): 35 package = get_content_at(url) 36 if size is not None and len(package) != size: 37 print(f"Package does not match size given in catalog: {url}", file=sys.stderr) 38 sys.exit(1) 39 # Ideally we'd check the digest, but it's not md5, sha1 or sha256... 40 # if digest is not None and hashlib.???(package).hexdigest() != digest: 41 # print(f"Package does not match digest given in catalog: {url}", file=sys.stderr) 42 # sys.exit(1) 43 for name, content in unxar(BytesIO(package)): 44 if name == "Payload": 45 for path, _, __ in uncpio(Pbzx(content)): 46 if path: 47 print(path.decode("utf-8")) 48 49 50 def show_product_info(product, package_id=None): 51 # An alternative here would be to look at the MetadataURLs in 52 # product["Packages"], but going with Distributions allows to 53 # only do one request. 54 dist = get_english(product.get("Distributions")) 55 data = get_content_at(dist) 56 dom = minidom.parseString(data.decode("utf-8")) 57 for pkg_ref in dom.getElementsByTagName("pkg-ref"): 58 if pkg_ref.childNodes: 59 if pkg_ref.hasAttribute("packageIdentifier"): 60 id = pkg_ref.attributes["packageIdentifier"].value 61 else: 62 id = pkg_ref.attributes["id"].value 63 64 if package_id and package_id != id: 65 continue 66 67 for child in pkg_ref.childNodes: 68 if child.nodeType != minidom.Node.TEXT_NODE: 69 continue 70 for p in product["Packages"]: 71 if p["URL"].endswith("/" + child.data): 72 if package_id: 73 show_package_content( 74 p["URL"], p.get("Digest"), p.get("Size") 75 ) 76 else: 77 print(id, p["URL"]) 78 79 80 def show_products(products, filter=None): 81 for key, product in products.items(): 82 metadata_url = product.get("ServerMetadataURL", "") 83 if metadata_url and (not filter or filter in metadata_url): 84 metadata = get_plist_at(metadata_url) 85 localization = get_english(metadata.get("localization", {}), {}) 86 title = localization.get("title", None) 87 version = metadata.get("CFBundleShortVersionString", None) 88 print(key, title, version) 89 90 91 def main(): 92 parser = argparse.ArgumentParser() 93 parser.add_argument( 94 "--catalog", 95 help="URL of the catalog", 96 default="https://swscan.apple.com/content/catalogs/others/index-15-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog", 97 ) 98 parser.add_argument( 99 "--filter", help="Only show entries with metadata url matching the filter" 100 ) 101 parser.add_argument( 102 "what", nargs="?", help="Show packages information about the given entry" 103 ) 104 args = parser.parse_args() 105 106 data = get_plist_at(args.catalog) 107 products = data["Products"] 108 109 if args.what: 110 if args.filter: 111 print( 112 "Cannot use --filter when showing verbose information about an entry", 113 file=sys.stderr, 114 ) 115 sys.exit(1) 116 product_id, _, package_id = args.what.partition("/") 117 show_product_info(products[product_id], package_id) 118 else: 119 show_products(products, args.filter) 120 121 122 if __name__ == "__main__": 123 main()