unpack.py (7679B)
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 codecs 6 from urllib.parse import urlparse 7 8 import mozpack.path as mozpath 9 from mozpack.chrome.manifest import ( 10 ManifestEntryWithRelPath, 11 ManifestResource, 12 is_manifest, 13 parse_manifest, 14 ) 15 from mozpack.copier import FileCopier, FileRegistry 16 from mozpack.files import BaseFinder, DeflatedFile, FileFinder, ManifestFile 17 from mozpack.mozjar import JarReader 18 from mozpack.packager import SimplePackager 19 from mozpack.packager.formats import FlatFormatter 20 21 22 class UnpackFinder(BaseFinder): 23 """ 24 Special Finder object that treats the source package directory as if it 25 were in the flat chrome format, whatever chrome format it actually is in. 26 27 This means that for example, paths like chrome/browser/content/... match 28 files under jar:chrome/browser.jar!/content/... in case of jar chrome 29 format. 30 31 The only argument to the constructor is a Finder instance or a path. 32 The UnpackFinder is populated with files from this Finder instance, 33 or with files from a FileFinder using the given path as its root. 34 """ 35 36 def __init__(self, source, omnijar_name=None, unpack_xpi=True, **kwargs): 37 if isinstance(source, BaseFinder): 38 assert not kwargs 39 self._finder = source 40 else: 41 self._finder = FileFinder(source, **kwargs) 42 self.base = self._finder.base 43 self.files = FileRegistry() 44 self.kind = "flat" 45 if omnijar_name: 46 self.omnijar = omnijar_name 47 else: 48 # Can't include globally because of bootstrapping issues. 49 from buildconfig import substs 50 51 self.omnijar = substs.get("OMNIJAR_NAME", "omni.ja") 52 self.jarlogs = {} 53 self.compressed = False 54 self._unpack_xpi = unpack_xpi 55 56 jars = set() 57 58 for p, f in self._finder.find("*"): 59 # Skip the precomplete file, which is generated at packaging time. 60 if p == "precomplete": 61 continue 62 base = mozpath.dirname(p) 63 # If the file matches the omnijar pattern, it is an omnijar. 64 # All the files it contains go in the directory containing the full 65 # pattern. Manifests are merged if there is a corresponding manifest 66 # in the directory. 67 if self._maybe_zip(f) and mozpath.match(p, "**/%s" % self.omnijar): 68 jar = self._open_jar(p, f) 69 if "chrome.manifest" in jar: 70 self.kind = "omni" 71 self._fill_with_jar(p[: -len(self.omnijar) - 1], jar) 72 continue 73 # If the file is a manifest, scan its entries for some referencing 74 # jar: urls. If there are some, the files contained in the jar they 75 # point to, go under a directory named after the jar. 76 if is_manifest(p): 77 m = self.files[p] if self.files.contains(p) else ManifestFile(base) 78 for e in parse_manifest( 79 self.base, p, codecs.getreader("utf-8")(f.open()) 80 ): 81 m.add(self._handle_manifest_entry(e, jars)) 82 if self.files.contains(p): 83 continue 84 f = m 85 # If we're unpacking packed addons and the file is a packed addon, 86 # unpack it under a directory named after the xpi. 87 if self._unpack_xpi and p.endswith(".xpi") and self._maybe_zip(f): 88 self._fill_with_jar(p[:-4], self._open_jar(p, f)) 89 continue 90 if p not in jars: 91 self.files.add(p, f) 92 93 def _fill_with_jar(self, base, jar): 94 for j in jar: 95 path = mozpath.join(base, j.filename) 96 if is_manifest(j.filename): 97 m = ( 98 self.files[path] 99 if self.files.contains(path) 100 else ManifestFile(mozpath.dirname(path)) 101 ) 102 for e in parse_manifest(None, path, j): 103 m.add(e) 104 if not self.files.contains(path): 105 self.files.add(path, m) 106 continue 107 else: 108 self.files.add(path, DeflatedFile(j)) 109 110 def _handle_manifest_entry(self, entry, jars): 111 jarpath = None 112 if ( 113 isinstance(entry, ManifestEntryWithRelPath) 114 and urlparse(entry.relpath).scheme == "jar" 115 ): 116 jarpath, entry = self._unjarize(entry, entry.relpath) 117 elif ( 118 isinstance(entry, ManifestResource) 119 and urlparse(entry.target).scheme == "jar" 120 ): 121 jarpath, entry = self._unjarize(entry, entry.target) 122 if jarpath: 123 # Don't defer unpacking the jar file. If we already saw 124 # it, take (and remove) it from the registry. If we 125 # haven't, try to find it now. 126 if self.files.contains(jarpath): 127 jar = self.files[jarpath] 128 self.files.remove(jarpath) 129 else: 130 jar = [f for p, f in self._finder.find(jarpath)] 131 assert len(jar) == 1 132 jar = jar[0] 133 if jarpath not in jars: 134 base = mozpath.splitext(jarpath)[0] 135 for j in self._open_jar(jarpath, jar): 136 self.files.add(mozpath.join(base, j.filename), DeflatedFile(j)) 137 jars.add(jarpath) 138 self.kind = "jar" 139 return entry 140 141 def _open_jar(self, path, file): 142 """ 143 Return a JarReader for the given BaseFile instance, keeping a log of 144 the preloaded entries it has. 145 """ 146 jar = JarReader(fileobj=file.open()) 147 self.compressed = max(self.compressed, jar.compression) 148 if jar.last_preloaded: 149 jarlog = list(jar.entries.keys()) 150 self.jarlogs[path] = jarlog[: jarlog.index(jar.last_preloaded) + 1] 151 return jar 152 153 def find(self, path): 154 for p in self.files.match(path): 155 yield p, self.files[p] 156 157 def _maybe_zip(self, file): 158 """ 159 Return whether the given BaseFile looks like a ZIP/Jar. 160 """ 161 header = file.open().read(8) 162 return len(header) == 8 and (header[0:2] == b"PK" or header[4:6] == b"PK") 163 164 def _unjarize(self, entry, relpath): 165 """ 166 Transform a manifest entry pointing to chrome data in a jar in one 167 pointing to the corresponding unpacked path. Return the jar path and 168 the new entry. 169 """ 170 base = entry.base 171 jar, relpath = urlparse(relpath).path.split("!", 1) 172 entry = ( 173 entry.rebase(mozpath.join(base, "jar:%s!" % jar)) 174 .move(mozpath.join(base, mozpath.splitext(jar)[0])) 175 .rebase(base) 176 ) 177 return mozpath.join(base, jar), entry 178 179 180 def unpack_to_registry(source, registry, omnijar_name=None): 181 """ 182 Transform a jar chrome or omnijar packaged directory into a flat package. 183 184 The given registry is filled with the flat package. 185 """ 186 finder = UnpackFinder(source, omnijar_name) 187 packager = SimplePackager(FlatFormatter(registry)) 188 for p, f in finder.find("*"): 189 packager.add(p, f) 190 packager.close() 191 192 193 def unpack(source, omnijar_name=None): 194 """ 195 Transform a jar chrome or omnijar packaged directory into a flat package. 196 """ 197 copier = FileCopier() 198 unpack_to_registry(source, copier, omnijar_name) 199 copier.copy(source, skip_if_older=False)