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 <jviotti@openmailbox.org>
This commit is contained in:
Jonas Hermsmeier 2017-04-15 06:17:41 +02:00 committed by Juan Cruz Viotti
parent 847b41f49a
commit 6c930e2d8d
4 changed files with 117 additions and 8 deletions

View File

@ -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

View File

@ -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'
}
];

4
npm-shrinkwrap.json generated
View File

@ -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",

View File

@ -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;