From 6c930e2d8d94a3b5786e797de7038f676a3d3ae7 Mon Sep 17 00:00:00 2001 From: Jonas Hermsmeier Date: Sat, 15 Apr 2017 06:17:41 +0200 Subject: [PATCH] fix(image-stream): fix Apple disk image detection & reading (#1283) This fixes two things: The format detection, and a bug in `udif`. First, by categorizing the `.dmg` extension as compressed image, `.isSupportedImage()` would attempt to detect the format after stripping the extension, causing it to be misdetected. Second, `udif`'s ReadStream didn't add the `dataForkOffset` to its position when reading blocks, causing the wrong data to be read for some images, in turn causing zlib to error on invalid headers. Changes: - Classify `.dmg` as `type: 'image'` - Update `udif` to 0.8.0 Change-Type: patch Changelog-Entry: Fix Apple disk image detection & streaming Signed-off-by: Juan Cruz Viotti --- lib/image-stream/README.md | 15 +++++ lib/image-stream/supported.js | 16 +++-- npm-shrinkwrap.json | 4 +- tests/shared/supported-formats.spec.js | 90 +++++++++++++++++++++++++- 4 files changed, 117 insertions(+), 8 deletions(-) diff --git a/lib/image-stream/README.md b/lib/image-stream/README.md index 10424a1b..ec915299 100644 --- a/lib/image-stream/README.md +++ b/lib/image-stream/README.md @@ -58,4 +58,19 @@ These are the rules for handling archive images: The module throws an error if the above rules are not met. +Supported Formats +----------------- + +There are currently three image types in supported formats: `image`, `compressed` and `archive`. + +An extension tagged `image` describes a format which can be directly written to a device by its handler, +and an extension tagged `archive` denotes an archive containing an image, and will cause an archive handler +to open the archive and search for an image file. + +Note that when marking an extension as `compressed`, the filename will be stripped of that extension, +and the leftover extension examined to determine the uncompressed image format (i.e. `.img.gz -> .img`). + +As an archive (such as `.tar`) might be additionally compressed, this will allow for constructs such as +`.tar.gz` (a compressed archive, containing a file with an extension tagged as `image`) to be handled correctly. + [etcher-image-write]: https://github.com/resin-io-modules/etcher-image-write diff --git a/lib/image-stream/supported.js b/lib/image-stream/supported.js index 144f196f..b955871d 100644 --- a/lib/image-stream/supported.js +++ b/lib/image-stream/supported.js @@ -16,6 +16,14 @@ 'use strict'; +/** + * @summary Supported filename extensions + * @description + * NOTE: Extensions with type: 'compressed' will be stripped + * from filenames to determine the format of the uncompressed image. + * For details, see lib/image-stream/README.md + * @const {Array} + */ module.exports = [ { extension: 'zip', @@ -37,10 +45,6 @@ module.exports = [ extension: 'xz', type: 'compressed' }, - { - extension: 'dmg', - type: 'compressed' - }, { extension: 'img', type: 'image' @@ -60,5 +64,9 @@ module.exports = [ { extension: 'raw', type: 'image' + }, + { + extension: 'dmg', + type: 'image' } ]; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index d6a94228..b6ef5205 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -6772,9 +6772,9 @@ "dev": true }, "udif": { - "version": "0.7.0", + "version": "0.8.0", "from": "udif@latest", - "resolved": "https://registry.npmjs.org/udif/-/udif-0.7.0.tgz", + "resolved": "https://registry.npmjs.org/udif/-/udif-0.8.0.tgz", "dependencies": { "base64-js": { "version": "1.1.2", diff --git a/tests/shared/supported-formats.spec.js b/tests/shared/supported-formats.spec.js index 9bf39af2..69adf6a5 100644 --- a/tests/shared/supported-formats.spec.js +++ b/tests/shared/supported-formats.spec.js @@ -26,7 +26,7 @@ describe('Shared: SupportedFormats', function() { it('should return the supported compressed extensions', function() { const extensions = supportedFormats.getCompressedExtensions(); - m.chai.expect(extensions).to.deep.equal([ 'gz', 'bz2', 'xz', 'dmg' ]); + m.chai.expect(extensions).to.deep.equal([ 'gz', 'bz2', 'xz' ]); }); }); @@ -35,7 +35,7 @@ describe('Shared: SupportedFormats', function() { it('should return the supported non compressed extensions', function() { const extensions = supportedFormats.getNonCompressedExtensions(); - m.chai.expect(extensions).to.deep.equal([ 'img', 'iso', 'dsk', 'hddimg', 'raw' ]); + m.chai.expect(extensions).to.deep.equal([ 'img', 'iso', 'dsk', 'hddimg', 'raw', 'dmg' ]); }); }); @@ -64,6 +64,92 @@ describe('Shared: SupportedFormats', function() { describe('.isSupportedImage()', function() { + _.forEach([ + + // Type: 'archive' + 'path/to/filename.zip', + 'path/to/filename.etch', + + // Type: 'compressed' + 'path/to/filename.img.gz', + 'path/to/filename.img.bz2', + 'path/to/filename.img.xz', + + // Type: 'image' + 'path/to/filename.img', + 'path/to/filename.iso', + 'path/to/filename.dsk', + 'path/to/filename.hddimg', + 'path/to/filename.raw', + 'path/to/filename.dmg' + + ], (filename) => { + it(`should return true for ${filename}`, function() { + const isSupported = supportedFormats.isSupportedImage(filename); + m.chai.expect(isSupported).to.be.true; + }); + }); + + it('should return false if the file has no extension', function() { + const isSupported = supportedFormats.isSupportedImage('/path/to/foo'); + m.chai.expect(isSupported).to.be.false; + }); + + it('should return false if the extension is not included in .getAllExtensions()', function() { + const isSupported = supportedFormats.isSupportedImage('/path/to/foo.jpg'); + m.chai.expect(isSupported).to.be.false; + }); + + it('should return true if the extension is included in .getAllExtensions()', function() { + const nonCompressedExtension = _.first(supportedFormats.getNonCompressedExtensions()); + const imagePath = `/path/to/foo.${nonCompressedExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + m.chai.expect(isSupported).to.be.true; + }); + + it('should ignore casing when determining extension validity', function() { + const nonCompressedExtension = _.first(supportedFormats.getNonCompressedExtensions()); + const imagePath = `/path/to/foo.${_.toUpper(nonCompressedExtension)}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + m.chai.expect(isSupported).to.be.true; + }); + + it('should not consider an extension before a non compressed extension', function() { + const nonCompressedExtension = _.first(supportedFormats.getNonCompressedExtensions()); + const imagePath = `/path/to/foo.1234.${nonCompressedExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + m.chai.expect(isSupported).to.be.true; + }); + + it('should return true if the extension is supported and the file name includes dots', function() { + const nonCompressedExtension = _.first(supportedFormats.getNonCompressedExtensions()); + const imagePath = `/path/to/foo.1.2.3-bar.${nonCompressedExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + m.chai.expect(isSupported).to.be.true; + }); + + it('should return true if the extension is only a supported archive extension', function() { + const archiveExtension = _.first(supportedFormats.getArchiveExtensions()); + const imagePath = `/path/to/foo.${archiveExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + m.chai.expect(isSupported).to.be.true; + }); + + it('should return true if the extension is a supported one plus a supported compressed extensions', function() { + const nonCompressedExtension = _.first(supportedFormats.getNonCompressedExtensions()); + const compressedExtension = _.first(supportedFormats.getCompressedExtensions()); + const imagePath = `/path/to/foo.${nonCompressedExtension}.${compressedExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + m.chai.expect(isSupported).to.be.true; + }); + + it('should return false if the extension is an unsupported one plus a supported compressed extensions', function() { + const compressedExtension = _.first(supportedFormats.getCompressedExtensions()); + const imagePath = `/path/to/foo.jpg.${compressedExtension}`; + const isSupported = supportedFormats.isSupportedImage(imagePath); + m.chai.expect(isSupported).to.be.false; + }); + it('should return false if the file has no extension', function() { const isSupported = supportedFormats.isSupportedImage('/path/to/foo'); m.chai.expect(isSupported).to.be.false;