author | Jan Keromnes <janx@linux.com> |
Tue, 11 Mar 2014 17:01:29 -0400 | |
changeset 173134 | b5fec3059a699719ca69a37dfb9bbefb1841f7f6 |
parent 173133 | 7a6b4371379fdd624bb41d6d5fbda1257bafc699 |
child 173135 | 0dd61eada6c925e48f8949c6858915ed7f8709d5 |
push id | 26391 |
push user | cbook@mozilla.com |
push date | Wed, 12 Mar 2014 11:20:34 +0000 |
treeherder | mozilla-central@a56837cfc67c [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | Mossop |
bugs | 974498 |
milestone | 30.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
new file mode 100644 --- /dev/null +++ b/toolkit/modules/ZipUtils.jsm @@ -0,0 +1,223 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +this.EXPORTED_SYMBOLS = [ "ZipUtils" ]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", + "resource://gre/modules/osfile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + + +// The maximum amount of file data to buffer at a time during file extraction +const EXTRACTION_BUFFER = 1024 * 512; + + +/** + * Asynchronously writes data from an nsIInputStream to an OS.File instance. + * The source stream and OS.File are closed regardless of whether the operation + * succeeds or fails. + * Returns a promise that will be resolved when complete. + * + * @param aPath + * The name of the file being extracted for logging purposes. + * @param aStream + * The source nsIInputStream. + * @param aFile + * The open OS.File instance to write to. + */ +function saveStreamAsync(aPath, aStream, aFile) { + let deferred = Promise.defer(); + + // Read the input stream on a background thread + let sts = Cc["@mozilla.org/network/stream-transport-service;1"]. + getService(Ci.nsIStreamTransportService); + let transport = sts.createInputTransport(aStream, -1, -1, true); + let input = transport.openInputStream(0, 0, 0) + .QueryInterface(Ci.nsIAsyncInputStream); + let source = Cc["@mozilla.org/binaryinputstream;1"]. + createInstance(Ci.nsIBinaryInputStream); + source.setInputStream(input); + + let data = Uint8Array(EXTRACTION_BUFFER); + + function readFailed(error) { + try { + aStream.close(); + } + catch (e) { + logger.error("Failed to close JAR stream for " + aPath); + } + + aFile.close().then(function() { + deferred.reject(error); + }, function(e) { + logger.error("Failed to close file for " + aPath); + deferred.reject(error); + }); + } + + function readData() { + try { + let count = Math.min(source.available(), data.byteLength); + source.readArrayBuffer(count, data.buffer); + + aFile.write(data, { bytes: count }).then(function() { + input.asyncWait(readData, 0, 0, Services.tm.currentThread); + }, readFailed); + } + catch (e if e.result == Cr.NS_BASE_STREAM_CLOSED) { + deferred.resolve(aFile.close()); + } + catch (e) { + readFailed(e); + } + } + + input.asyncWait(readData, 0, 0, Services.tm.currentThread); + + return deferred.promise; +} + + +this.ZipUtils = { + + /** + * Asynchronously extracts files from a ZIP file into a directory. + * Returns a promise that will be resolved when the extraction is complete. + * + * @param aZipFile + * The source ZIP file that contains the add-on. + * @param aDir + * The nsIFile to extract to. + */ + extractFilesAsync: function ZipUtils_extractFilesAsync(aZipFile, aDir) { + let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. + createInstance(Ci.nsIZipReader); + + try { + zipReader.open(aZipFile); + } + catch (e) { + return Promise.reject(e); + } + + return Task.spawn(function() { + // Get all of the entries in the zip and sort them so we create directories + // before files + let entries = zipReader.findEntries(null); + let names = []; + while (entries.hasMore()) + names.push(entries.getNext()); + names.sort(); + + for (let name of names) { + let entryName = name; + let zipentry = zipReader.getEntry(name); + let path = OS.Path.join(aDir.path, ...name.split("/")); + + if (zipentry.isDirectory) { + try { + yield OS.File.makeDir(path); + } + catch (e) { + dump("extractFilesAsync: failed to create directory " + path + "\n"); + throw e; + } + } + else { + let options = { unixMode: zipentry.permissions | FileUtils.PERMS_FILE }; + try { + let file = yield OS.File.open(path, { truncate: true }, options); + if (zipentry.realSize == 0) + yield file.close(); + else + yield saveStreamAsync(path, zipReader.getInputStream(entryName), file); + } + catch (e) { + dump("extractFilesAsync: failed to extract file " + path + "\n"); + throw e; + } + } + } + + zipReader.close(); + }).then(null, (e) => { + zipReader.close(); + throw e; + }); + }, + + /** + * Extracts files from a ZIP file into a directory. + * + * @param aZipFile + * The source ZIP file that contains the add-on. + * @param aDir + * The nsIFile to extract to. + */ + extractFiles: function ZipUtils_extractFiles(aZipFile, aDir) { + function getTargetFile(aDir, entry) { + let target = aDir.clone(); + entry.split("/").forEach(function(aPart) { + target.append(aPart); + }); + return target; + } + + let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. + createInstance(Ci.nsIZipReader); + zipReader.open(aZipFile); + + try { + // create directories first + let entries = zipReader.findEntries("*/"); + while (entries.hasMore()) { + let entryName = entries.getNext(); + let target = getTargetFile(aDir, entryName); + if (!target.exists()) { + try { + target.create(Ci.nsIFile.DIRECTORY_TYPE, + FileUtils.PERMS_DIRECTORY); + } + catch (e) { + dump("extractFiles: failed to create target directory for extraction file = " + target.path + "\n"); + } + } + } + + entries = zipReader.findEntries(null); + while (entries.hasMore()) { + let entryName = entries.getNext(); + let target = getTargetFile(aDir, entryName); + if (target.exists()) + continue; + + zipReader.extract(entryName, target); + try { + target.permissions |= FileUtils.PERMS_FILE; + } + catch (e) { + dump("Failed to set permissions " + aPermissions.toString(8) + " on " + target.path + "\n"); + } + } + } + finally { + zipReader.close(); + } + } + +};
--- a/toolkit/modules/moz.build +++ b/toolkit/modules/moz.build @@ -46,16 +46,17 @@ EXTRA_JS_MODULES += [ 'sessionstore/XPathGenerator.jsm', 'ShortcutUtils.jsm', 'Sntp.jsm', 'SpatialNavigation.jsm', 'Sqlite.jsm', 'Task.jsm', 'TelemetryTimestamps.jsm', 'Timer.jsm', + 'ZipUtils.jsm', ] EXTRA_PP_JS_MODULES += [ 'CertUtils.jsm', 'ResetProfile.jsm', 'Services.jsm', 'Troubleshoot.jsm', 'UpdateChannel.jsm',
new file mode 100644 --- /dev/null +++ b/toolkit/modules/tests/xpcshell/test_ZipUtils.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const ARCHIVE = "zips/zen.zip"; +const SUBDIR = "zen"; +const ENTRIES = ["beyond.txt", "waterwood.txt"]; + +Components.utils.import("resource://gre/modules/ZipUtils.jsm"); +Components.utils.import("resource://gre/modules/FileUtils.jsm"); + + +const archive = do_get_file(ARCHIVE, false); +const dir = do_get_profile().clone(); +dir.append("test_ZipUtils"); + +function run_test() { + run_next_test(); +} + +function ensureExtracted(target) { + target.append(SUBDIR); + do_check_true(target.exists()); + + for (let i = 0; i < ENTRIES.length; i++) { + let entry = target.clone(); + entry.append(ENTRIES[i]); + do_print("ENTRY " + entry.path); + do_check_true(entry.exists()); + } +} + + +add_task(function test_extractFiles() { + let target = dir.clone(); + target.append("test_extractFiles"); + + try { + ZipUtils.extractFiles(archive, target); + } catch(e) { + do_throw("Failed to extract synchronously!"); + } + + ensureExtracted(target); +}); + +add_task(function test_extractFilesAsync() { + let target = dir.clone(); + target.append("test_extractFilesAsync"); + target.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, + FileUtils.PERMS_DIRECTORY); + + yield ZipUtils.extractFilesAsync(archive, target).then( + function success() { + do_print("SUCCESS"); + ensureExtracted(target); + }, + function failure() { + do_print("FAILURE"); + do_throw("Failed to extract asynchronously!"); + } + ); +});
--- a/toolkit/modules/tests/xpcshell/xpcshell.ini +++ b/toolkit/modules/tests/xpcshell/xpcshell.ini @@ -1,15 +1,16 @@ [DEFAULT] head = tail = support-files = propertyLists/bug710259_propertyListBinary.plist propertyLists/bug710259_propertyListXML.plist chromeappsstore.sqlite + zips/zen.zip [test_AsyncShutdown.js] [test_DeferredTask.js] [test_dict.js] [test_FileUtils.js] [test_Http.js] [test_Log.js] [test_PermissionsUtils.js] @@ -17,9 +18,9 @@ support-files = [test_Promise.js] [test_propertyListsUtils.js] [test_readCertPrefs.js] [test_Services.js] [test_sqlite.js] [test_task.js] [test_TelemetryTimestamps.js] [test_timer.js] - +[test_ZipUtils.js]
new file mode 100644 index 0000000000000000000000000000000000000000..372010a3cc72306dd133841b48cd4f65337944d2 GIT binary patch literal 1058 zc$^FHW@h1H0D;qG=`IXNfQ3PZp(-^`KQx4sfw}j*WN;r4msW5yFtWU021<j70HAIW zpjr-wtw7z9I{H2*fV>+(%m>w-lv<gem!elvQ35t64QNIhra7{Gd$X7vMOuGXc3oZ? z7_SieW^r%AUW@W_wrkb~_NU5{a)OQ=YUogs@JJS(XtJqS|M9Lj(L1s$E#&W8#-8Hw z=9y9^AvN1ean;RjF?wD9gG*;H|K*IVaFvKGl2fZ|JLvsj$=aBX9kRmex`n%I?wT); zo)`W)|B77`*W(Vu+$AgC<`|ewvs$#HPGtY<qL0CKa*gvodKF(1P^(z3YRi7l^mKUP z`Shpn*56v+^;xoFb-$Tx>Xio@{emaW$XY)kLTTOK#@Zz}OOG%k!ecuyJdPUk>eMkZ zFuVt1L1=iCCzhlZmFMRpf+PzV9EF%c;v0B2OT<xN-}m4eet~bQ-<~-7GoF9z^WM+d z|N4cykFB=<;b>#=6zE-(aZSd|YtaPW-3M7dW^PMQyMHLFSt-OhwDjuttKp_o&Y9lo z`xwIZben<BidFxPxa<^o``hyNuYW75E9Ll~9Z>Gx<fykbGvSMeqR1&tkGg3NnoeO8 zg6GvQ5q<klXWpMz@v+D5-v0NBdqeC5|5&316GE5jFeR$=Hg0=kXtDiior2l?{p!C< z#QObjUypB8@0q9eSSu}Iite&M(@$+ZzvQ&5eeLmU_x5z!hrix^`AS=l*E+AK62jHY z7wck5POtv=X_xhmm&=W>9o=&N|K$az(%q)YnRqGp^6q@*cp)ucP`o>@RQh~);kNlK zno~k<D{TGx;AUV(vW=RRNC>;fk1i*sQ`z!ti)PQyEwfuwA9JKT^ZT??+dPNJu-Oe= zYCdxnYL?FB+iDV^KjpN^dL8B0Tlx2>ZM<olwSM}?9~*3cUOPAKz=!vu>My(aIehYp zo(7dInLF!)E&C($0B=SnIc8iLLjuYYVEF3@q9NIY6_QObvIxi|T$u!75-@!*Y-zNE snuMHXSRq*kkC~VW4cSaTMh0xjjun#ZFpOno18HRj!mmJDoEgLe0A|{NZU6uP
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm +++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm @@ -18,16 +18,18 @@ Components.utils.import("resource://gre/ XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", "resource://gre/modules/addons/AddonRepository.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser", "resource://gre/modules/ChromeManifestParser.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils", + "resource://gre/modules/ZipUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils", "resource://gre/modules/PermissionsUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); @@ -107,19 +109,16 @@ const KEY_APP_SYSTEM_USER = const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions"; const XPI_PERMISSION = "install"; const RDFURI_INSTALL_MANIFEST_ROOT = "urn:mozilla:install-manifest"; const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#"; const TOOLKIT_ID = "toolkit@mozilla.org"; -// The maximum amount of file data to buffer at a time during file extraction -const EXTRACTION_BUFFER = 1024 * 512; - // The value for this is in Makefile.in #expand const DB_SCHEMA = __MOZ_EXTENSIONS_DB_SCHEMA__; // Properties that exist in the install manifest const PROP_METADATA = ["id", "version", "type", "internalName", "updateURL", "updateKey", "optionsURL", "optionsType", "aboutURL", "iconURL", "icon64URL"]; const PROP_LOCALE_SINGLE = ["name", "description", "creator", "homepageURL"]; @@ -1124,209 +1123,16 @@ function getTemporaryFile() { let random = Math.random().toString(36).replace(/0./, '').substr(-3); file.append("tmp-" + random + ".xpi"); file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); return file; } /** - * Asynchronously writes data from an nsIInputStream to an OS.File instance. - * The source stream and OS.File are closed regardless of whether the operation - * succeeds or fails. - * Returns a promise that will be resolved when complete. - * - * @param aPath - * The name of the file being extracted for logging purposes. - * @param aStream - * The source nsIInputStream. - * @param aFile - * The open OS.File instance to write to. - */ -function saveStreamAsync(aPath, aStream, aFile) { - let deferred = Promise.defer(); - - // Read the input stream on a background thread - let sts = Cc["@mozilla.org/network/stream-transport-service;1"]. - getService(Ci.nsIStreamTransportService); - let transport = sts.createInputTransport(aStream, -1, -1, true); - let input = transport.openInputStream(0, 0, 0) - .QueryInterface(Ci.nsIAsyncInputStream); - let source = Cc["@mozilla.org/binaryinputstream;1"]. - createInstance(Ci.nsIBinaryInputStream); - source.setInputStream(input); - - let data = Uint8Array(EXTRACTION_BUFFER); - - function readFailed(error) { - try { - aStream.close(); - } - catch (e) { - logger.error("Failed to close JAR stream for " + aPath); - } - - aFile.close().then(function() { - deferred.reject(error); - }, function(e) { - logger.error("Failed to close file for " + aPath); - deferred.reject(error); - }); - } - - function readData() { - try { - let count = Math.min(source.available(), data.byteLength); - source.readArrayBuffer(count, data.buffer); - - aFile.write(data, { bytes: count }).then(function() { - input.asyncWait(readData, 0, 0, Services.tm.currentThread); - }, readFailed); - } - catch (e if e.result == Cr.NS_BASE_STREAM_CLOSED) { - deferred.resolve(aFile.close()); - } - catch (e) { - readFailed(e); - } - } - - input.asyncWait(readData, 0, 0, Services.tm.currentThread); - - return deferred.promise; -} - -/** - * Asynchronously extracts files from a ZIP file into a directory. - * Returns a promise that will be resolved when the extraction is complete. - * - * @param aZipFile - * The source ZIP file that contains the add-on. - * @param aDir - * The nsIFile to extract to. - */ -function extractFilesAsync(aZipFile, aDir) { - let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. - createInstance(Ci.nsIZipReader); - - try { - zipReader.open(aZipFile); - } - catch (e) { - return Promise.reject(e); - } - - return Task.spawn(function() { - // Get all of the entries in the zip and sort them so we create directories - // before files - let entries = zipReader.findEntries(null); - let names = []; - while (entries.hasMore()) - names.push(entries.getNext()); - names.sort(); - - for (let name of names) { - let entryName = name; - let zipentry = zipReader.getEntry(name); - let path = OS.Path.join(aDir.path, ...name.split("/")); - - if (zipentry.isDirectory) { - try { - yield OS.File.makeDir(path); - } - catch (e) { - logger.error("extractFilesAsync: failed to create directory " + path, e); - throw e; - } - } - else { - let options = { unixMode: zipentry.permissions | FileUtils.PERMS_FILE }; - try { - let file = yield OS.File.open(path, { truncate: true }, options); - if (zipentry.realSize == 0) - yield file.close(); - else - yield saveStreamAsync(path, zipReader.getInputStream(entryName), file); - } - catch (e) { - logger.error("extractFilesAsync: failed to extract file " + path, e); - throw e; - } - } - } - - zipReader.close(); - }).then(null, (e) => { - zipReader.close(); - throw e; - }); -} - -/** - * Extracts files from a ZIP file into a directory. - * - * @param aZipFile - * The source ZIP file that contains the add-on. - * @param aDir - * The nsIFile to extract to. - */ -function extractFiles(aZipFile, aDir) { - function getTargetFile(aDir, entry) { - let target = aDir.clone(); - entry.split("/").forEach(function(aPart) { - target.append(aPart); - }); - return target; - } - - let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]. - createInstance(Ci.nsIZipReader); - zipReader.open(aZipFile); - - try { - // create directories first - let entries = zipReader.findEntries("*/"); - while (entries.hasMore()) { - var entryName = entries.getNext(); - let target = getTargetFile(aDir, entryName); - if (!target.exists()) { - try { - target.create(Ci.nsIFile.DIRECTORY_TYPE, - FileUtils.PERMS_DIRECTORY); - } - catch (e) { - logger.error("extractFiles: failed to create target directory for " + - "extraction file = " + target.path, e); - } - } - } - - entries = zipReader.findEntries(null); - while (entries.hasMore()) { - let entryName = entries.getNext(); - let target = getTargetFile(aDir, entryName); - if (target.exists()) - continue; - - zipReader.extract(entryName, target); - try { - target.permissions |= FileUtils.PERMS_FILE; - } - catch (e) { - logger.warn("Failed to set permissions " + aPermissions.toString(8) + " on " + - target.path, e); - } - } - } - finally { - zipReader.close(); - } -} - -/** * Verifies that a zip file's contents are all signed by the same principal. * Directory entries and anything in the META-INF directory are not checked. * * @param aZip * A nsIZipReader to check * @param aPrincipal * The nsIPrincipal to compare against * @return true if all the contents that should be signed were signed by the @@ -2420,17 +2226,17 @@ var XPIProvider = { targetDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); } catch (e) { logger.error("Failed to create staging directory for add-on " + addon.id, e); continue; } try { - extractFiles(stagedXPI, targetDir); + ZipUtils.extractFiles(stagedXPI, targetDir); } catch (e) { logger.error("Failed to extract staged XPI for add-on " + addon.id + " in " + aLocation.name, e); } } else { try { @@ -5546,17 +5352,17 @@ AddonInstall.prototype = { // First stage the file regardless of whether restarting is necessary if (this.addon.unpack || Prefs.getBoolPref(PREF_XPI_UNPACK, false)) { logger.debug("Addon " + this.addon.id + " will be installed as " + "an unpacked directory"); stagedAddon.append(this.addon.id); yield removeAsync(stagedAddon); yield OS.File.makeDir(stagedAddon.path); - yield extractFilesAsync(this.file, stagedAddon); + yield ZipUtils.extractFilesAsync(this.file, stagedAddon); installedUnpacked = 1; } else { logger.debug("Addon " + this.addon.id + " will be installed as " + "a packed xpi"); stagedAddon.append(this.addon.id + ".xpi"); yield removeAsync(stagedAddon); yield OS.File.copy(this.file.path, stagedAddon.path);