diff --git a/lib/gui/models/store.js b/lib/gui/models/store.js index 57e52adb..2914407f 100644 --- a/lib/gui/models/store.js +++ b/lib/gui/models/store.js @@ -22,6 +22,7 @@ const redux = require('redux'); const persistState = require('redux-localstorage'); const uuidV4 = require('uuid/v4'); const constraints = require('../../shared/drive-constraints'); +const supportedFormats = require('../../shared/supported-formats'); const errors = require('../../shared/errors'); const release = require('../../shared/release'); const fileExtensions = require('../../shared/file-extensions'); @@ -341,24 +342,38 @@ const storeReducer = (state = DEFAULT_STATE, action) => { }); } - if (!_.isString(action.data.extension)) { + if (_.some([ + !_.isString(action.data.extension), + !_.includes(supportedFormats.getAllExtensions(), action.data.extension) + ])) { throw errors.createError({ title: `Invalid image extension: ${action.data.extension}` }); } - if (fileExtensions.getLastFileExtension(action.data.path) !== action.data.extension) { + const lastImageExtension = fileExtensions.getLastFileExtension(action.data.path); + + if (lastImageExtension !== action.data.extension) { if (!action.data.archiveExtension) { throw errors.createError({ title: 'Missing image archive extension' }); } - if (!_.isString(action.data.archiveExtension)) { + if (_.some([ + !_.isString(action.data.archiveExtension), + !_.includes(supportedFormats.getAllExtensions(), action.data.archiveExtension) + ])) { throw errors.createError({ title: `Invalid image archive extension: ${action.data.archiveExtension}` }); } + + if (lastImageExtension !== action.data.archiveExtension) { + throw errors.createError({ + title: `Image archive extension mismatch: ${action.data.archiveExtension} and ${lastImageExtension}` + }); + } } if (!action.data.size) { diff --git a/lib/shared/file-extensions.js b/lib/shared/file-extensions.js index 0b3b8d29..ed2be4d7 100644 --- a/lib/shared/file-extensions.js +++ b/lib/shared/file-extensions.js @@ -31,13 +31,13 @@ const _ = require('lodash'); * console.log(extensions); * > [ 'img', 'gz' ] */ -exports.getFileExtensions = (filePath) => { +exports.getFileExtensions = _.memoize((filePath) => { return _.chain(filePath) .split('.') .tail() .map(_.toLower) .value(); -}; +}); /** * @summary Get the last file extension @@ -65,7 +65,7 @@ exports.getLastFileExtension = (filePath) => { * @returns {(String|Undefined)} penultimate extension * * @example - * const extension = fileExtensions.getLastFileExtension('path/to/foo.img.gz'); + * const extension = fileExtensions.getPenultimateFileExtension('path/to/foo.img.gz'); * console.log(extension); * > [ 'img' ] */ diff --git a/tests/gui/models/selection-state.spec.js b/tests/gui/models/selection-state.spec.js index e1256d05..012c7fcf 100644 --- a/tests/gui/models/selection-state.spec.js +++ b/tests/gui/models/selection-state.spec.js @@ -512,6 +512,73 @@ describe('Browser: SelectionState', function() { }).to.throw('Invalid image archive extension: 1'); }); + it('should throw if the archive extension doesn\'t match the last path extension in a compressed image', function() { + m.chai.expect(function() { + SelectionStateModel.setImage({ + path: 'foo.img.xz', + extension: 'img', + archiveExtension: 'gz', + size: { + original: 999999999, + final: { + estimation: false, + value: 999999999 + } + } + }); + }).to.throw('Image archive extension mismatch: gz and xz'); + }); + + it('should throw if the extension is not recognised in an uncompressed image', function() { + m.chai.expect(function() { + SelectionStateModel.setImage({ + path: 'foo.ifg', + extension: 'ifg', + size: { + original: 999999999, + final: { + estimation: false, + value: 999999999 + } + } + }); + }).to.throw('Invalid image extension: ifg'); + }); + + it('should throw if the extension is not recognised in a compressed image', function() { + m.chai.expect(function() { + SelectionStateModel.setImage({ + path: 'foo.ifg.gz', + extension: 'ifg', + archiveExtension: 'gz', + size: { + original: 999999999, + final: { + estimation: false, + value: 999999999 + } + } + }); + }).to.throw('Invalid image extension: ifg'); + }); + + it('should throw if the archive extension is not recognised', function() { + m.chai.expect(function() { + SelectionStateModel.setImage({ + path: 'foo.img.ifg', + extension: 'img', + archiveExtension: 'ifg', + size: { + original: 999999999, + final: { + estimation: false, + value: 999999999 + } + } + }); + }).to.throw('Invalid image archive extension: ifg'); + }); + it('should throw if no size', function() { m.chai.expect(function() { SelectionStateModel.setImage({ diff --git a/tests/shared/file-extensions.spec.js b/tests/shared/file-extensions.spec.js index 240aa817..fdeb0437 100644 --- a/tests/shared/file-extensions.spec.js +++ b/tests/shared/file-extensions.spec.js @@ -1,5 +1,5 @@ /* - * Copyright 2016 resin.io + * Copyright 2017 resin.io * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,62 +29,66 @@ describe('Shared: fileExtensions', function() { // No extension { file: 'path/to/filename', - extension: [] + extensions: [] }, // Type: 'archive' { file: 'path/to/filename.zip', - extension: [ 'zip' ] + extensions: [ 'zip' ] }, { file: 'path/to/filename.etch', - extension: [ 'etch' ] + extensions: [ 'etch' ] }, // Type: 'compressed' { file: 'path/to/filename.img.gz', - extension: [ 'img', 'gz' ] + extensions: [ 'img', 'gz' ] }, { file: 'path/to/filename.img.bz2', - extension: [ 'img', 'bz2' ] + extensions: [ 'img', 'bz2' ] }, { file: 'path/to/filename.img.xz', - extension: [ 'img', 'xz' ] + extensions: [ 'img', 'xz' ] + }, + { + file: 'path/to/filename.img.xz.gz', + extensions: [ 'img', 'xz', 'gz' ] }, // Type: 'image' { file: 'path/to/filename.img', - extension: [ 'img' ] + extensions: [ 'img' ] }, { file: 'path/to/filename.iso', - extension: [ 'iso' ] + extensions: [ 'iso' ] }, { file: 'path/to/filename.dsk', - extension: [ 'dsk' ] + extensions: [ 'dsk' ] }, { file: 'path/to/filename.hddimg', - extension: [ 'hddimg' ] + extensions: [ 'hddimg' ] }, { file: 'path/to/filename.raw', - extension: [ 'raw' ] + extensions: [ 'raw' ] }, { file: 'path/to/filename.dmg', - extension: [ 'dmg' ] + extensions: [ 'dmg' ] } ], (testCase) => { - it(`should return ${testCase.extension} for ${testCase.file}`, function() { - m.chai.expect(fileExtensions.getFileExtensions(testCase.file)).to.deep.equal(testCase.extension); + it(`should return ${testCase.extensions} for ${testCase.file}`, function() { + m.chai.expect(fileExtensions.getFileExtensions(testCase.file)).to.deep.equal(testCase.extensions); }); }); @@ -100,7 +104,7 @@ describe('Shared: fileExtensions', function() { describe('.getLastFileExtension()', function() { - it('should return undefined in the file path has no extension', function() { + it('should return undefined if the file path has no extension', function() { m.chai.expect(fileExtensions.getLastFileExtension('foo')).to.be.undefined; }); @@ -108,7 +112,7 @@ describe('Shared: fileExtensions', function() { m.chai.expect(fileExtensions.getLastFileExtension('foo.img')).to.equal('img'); }); - it('should return the last extension if there two extensions', function() { + it('should return the last extension if there are two extensions', function() { m.chai.expect(fileExtensions.getLastFileExtension('foo.img.gz')).to.equal('gz'); }); @@ -128,7 +132,7 @@ describe('Shared: fileExtensions', function() { m.chai.expect(fileExtensions.getPenultimateFileExtension('foo.img')).to.be.undefined; }); - it('should return the first extension if there are two extensions', function() { + it('should return the penultimate extension if there are two extensions', function() { m.chai.expect(fileExtensions.getPenultimateFileExtension('foo.img.gz')).to.equal('img'); });