diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 9ef1084e2..97d1a9b2e 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@particle/device-constants": "^3.5.0", - "binary-version-reader": "^2.4.0", + "binary-version-reader": "^2.5.0", "chalk": "^2.4.2", "cli-progress": "^3.12.0", "cli-spinner": "^0.2.10", @@ -1403,9 +1403,9 @@ } }, "node_modules/binary-version-reader": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/binary-version-reader/-/binary-version-reader-2.4.0.tgz", - "integrity": "sha512-ZWPL+plZHFYO5mSucewjXUo/acy0/+3yPsQ+Rv8FpBUfJlXCtUdNN7v1v27FjSuGc+uuUldHV8YDz8RHhy9LvA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/binary-version-reader/-/binary-version-reader-2.5.0.tgz", + "integrity": "sha512-e4AowD7zIksZnYGbHmeoWmDWnb6F33mUvDuK/4xICJaDQbvUYjfTnDY1xtPvvpTXRO8IUzdDzVK0HaGihJs6Og==", "dependencies": { "archiver": "^5.3.1", "buffer-crc32": "^0.2.5", @@ -1421,7 +1421,7 @@ "npm": "8.x" }, "peerDependencies": { - "@particle/device-constants": "^3.2.0" + "@particle/device-constants": "^3.5.0" } }, "node_modules/bl": { @@ -10321,9 +10321,9 @@ } }, "binary-version-reader": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/binary-version-reader/-/binary-version-reader-2.4.0.tgz", - "integrity": "sha512-ZWPL+plZHFYO5mSucewjXUo/acy0/+3yPsQ+Rv8FpBUfJlXCtUdNN7v1v27FjSuGc+uuUldHV8YDz8RHhy9LvA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/binary-version-reader/-/binary-version-reader-2.5.0.tgz", + "integrity": "sha512-e4AowD7zIksZnYGbHmeoWmDWnb6F33mUvDuK/4xICJaDQbvUYjfTnDY1xtPvvpTXRO8IUzdDzVK0HaGihJs6Og==", "requires": { "archiver": "^5.3.1", "buffer-crc32": "^0.2.5", diff --git a/package.json b/package.json index 6351726be..f7651fe99 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ ], "dependencies": { "@particle/device-constants": "^3.5.0", - "binary-version-reader": "^2.4.0", + "binary-version-reader": "^2.5.0", "chalk": "^2.4.2", "cli-progress": "^3.12.0", "cli-spinner": "^0.2.10", diff --git a/src/cli/binary.js b/src/cli/binary.js index 503fd9327..45158d322 100644 --- a/src/cli/binary.js +++ b/src/cli/binary.js @@ -28,5 +28,27 @@ module.exports = ({ commandProcessor, root }) => { } }); + commandProcessor.createCommand(binary, 'list-assets', 'Lists assets present in an application binary', { + params: '', + handler: (args) => { + const BinaryCommand = require('../cmd/binary'); + return new BinaryCommand().listAssetsFromApplication(args.params.file); + }, + examples: { + '$0 $command app-with-assets.bin': 'Show the list of assets in the application binary' + } + }); + + commandProcessor.createCommand(binary, 'strip-assets', 'Remove assets from application binary', { + params: '', + handler: (args) => { + const BinaryCommand = require('../cmd/binary'); + return new BinaryCommand().stripAssetsFromApplication(args.params.file); + }, + examples: { + '$0 $command app-with-assets.bin': 'Remove assets from the application binary' + } + }); + return binary; }; diff --git a/src/cmd/binary.js b/src/cmd/binary.js index acdbf7b98..778586d43 100644 --- a/src/cmd/binary.js +++ b/src/cmd/binary.js @@ -26,12 +26,22 @@ License along with this program; if not, see . */ const fs = require('fs-extra'); +const os = require('os'); const path = require('path'); const VError = require('verror'); const chalk = require('chalk'); -const { HalModuleParser: Parser, unpackApplicationAndAssetBundle, isAssetValid, createProtectedModule, ModuleInfo } = require('binary-version-reader'); +const { + HalModuleParser: Parser, + unpackApplicationAndAssetBundle, + isAssetValid, + createProtectedModule, + ModuleInfo, + listModuleExtensions, + removeModuleExtensions +} = require('binary-version-reader'); const utilities = require('../lib/utilities'); const ensureError = utilities.ensureError; +const filenameNoExt = utilities.filenameNoExt; const INVALID_SUFFIX_SIZE = 65535; const DEFAULT_PRODUCT_ID = 65535; @@ -43,7 +53,8 @@ class BinaryCommand { async inspectBinary(file) { await this._checkFile(file); const extractedFiles = await this._extractApplicationFiles(file); - const parsedAppInfo = await this._parseApplicationBinary(extractedFiles.application); + const parsedAppInfo = await this._parseBinary(extractedFiles.application); + await this._showInspectOutput(parsedAppInfo); const assets = extractedFiles.assets; await this._verifyBundle(parsedAppInfo, assets); } @@ -81,6 +92,61 @@ class BinaryCommand { } } + async listAssetsFromApplication(file) { + await this._checkFile(file); + const extractedFile = await this._extractApplicationFiles(file); + const parsedAppInfo = await this._parseBinary(extractedFile.application); + + const assets = await listModuleExtensions({ + module: parsedAppInfo.fileBuffer, + exts: [ModuleInfo.ModuleInfoExtension.ASSET_DEPENDENCY] + }); + + //if no assets, print no assets + if (assets.length === 0) { + throw new Error('No assets found'); + } + + console.log('Assets found in ' + path.basename(file) + ':'); + for (const asset of assets) { + console.log(' ' + chalk.bold(asset.name) + ' (' + asset.hash + ')'); + } + console.log(os.EOL); + + return assets; + + } + + async stripAssetsFromApplication(file) { + // Verify that the file exists and that it has assets + this._checkFile(file); + const extractedFile = await this._extractApplicationFiles(file); + const parsedAppInfo = await this._parseBinary(extractedFile.application); + + const assets = await listModuleExtensions({ + module: parsedAppInfo.fileBuffer, + exts: [ModuleInfo.ModuleInfoExtension.ASSET_DEPENDENCY] + }); + + //if no assets, print no assets + if (assets.length === 0) { + throw new Error('No assets found'); + } + + // Remove assets + const appWithAssetsRemoved = await removeModuleExtensions({ + module: parsedAppInfo.fileBuffer, + exts: [ModuleInfo.ModuleInfoExtension.ASSET_DEPENDENCY] + }); + + // Provide the path of the new application binary file with assets removed + const outputFile = filenameNoExt(file) + '-no-assets.bin'; + await fs.writeFile(outputFile, appWithAssetsRemoved); + console.log('Application binary without assets saved to ' + outputFile); + console.log(os.EOL); + return outputFile; + } + async _checkFile(file) { try { await fs.access(file); @@ -110,24 +176,15 @@ class BinaryCommand { } } - async _parseApplicationBinary(applicationBinary) { - const parser = new Parser(); - let fileInfo; - try { - fileInfo = await parser.parseBuffer({ filename: applicationBinary.name, fileBuffer: applicationBinary.data }); - } catch (err) { - throw new VError(ensureError(err), `Could not parse ${applicationBinary.name}`); - } - - const filename = path.basename(fileInfo.filename); - if (fileInfo.suffixInfo.suffixSize === INVALID_SUFFIX_SIZE){ + async _showInspectOutput(appInfo) { + const filename = path.basename(appInfo.filename); + if (appInfo.suffixInfo.suffixSize === INVALID_SUFFIX_SIZE){ throw new VError(`${filename} does not contain inspection information`); } console.log(chalk.bold(filename)); - this._showCrc(fileInfo); - this._showPlatform(fileInfo); - this._showModuleInfo(fileInfo); - return fileInfo; + this._showCrc(appInfo); + this._showPlatform(appInfo); + this._showModuleInfo(appInfo); } async _parseBinary(binary) { @@ -241,6 +298,8 @@ class BinaryCommand { + chalk.bold(fileInfo.prefixInfo.dep2ModuleVersion.toString())); } } + + } module.exports = BinaryCommand; diff --git a/src/cmd/binary.test.js b/src/cmd/binary.test.js index 0ad8edf0d..a9dea44be 100644 --- a/src/cmd/binary.test.js +++ b/src/cmd/binary.test.js @@ -3,7 +3,7 @@ const { expect } = require('../../test/setup'); const path = require('path'); const fs = require('fs-extra'); const { PATH_FIXTURES_THIRDPARTY_OTA_DIR, PATH_FIXTURES_BINARIES_DIR } = require('../../test/lib/env'); -describe('Binary Inspect', () => { +describe.only('Binary Inspect', () => { let binaryCommand; beforeEach(async () => { @@ -104,35 +104,6 @@ describe('Binary Inspect', () => { }); }); - describe('_parseApplicationBinary', () => { - it('parses a .bin file', async () => { - const name = 'argon_stroby.bin'; - const data = await fs.readFile(path.join(PATH_FIXTURES_BINARIES_DIR, name)); - const applicationBinary = { name, data }; - - const res = await binaryCommand._parseApplicationBinary(applicationBinary); - - expect(path.basename(res.filename)).to.equal('argon_stroby.bin'); - expect(res.crc.ok).to.equal(true); - expect(res).to.have.property('prefixInfo'); - expect(res).to.have.property('suffixInfo'); - }); - - it('errors if the binary is not valid', async () => { - const applicationBinary = { name: 'junk', data: Buffer.from('junk') }; - - let error; - try { - await binaryCommand._parseApplicationBinary(applicationBinary); - } catch (_error) { - error = _error; - } - - expect(error).to.be.an.instanceof(Error); - expect(error.message).to.match(/Could not parse junk/); - }); - }); - describe('_parseBinary', () => { it('parses a .bin file', async () => { const name = 'argon_stroby.bin'; @@ -166,7 +137,7 @@ describe('Binary Inspect', () => { it('verifies bundle with asset info', async () => { const zipPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'bundle.zip'); const res = await binaryCommand._extractApplicationFiles(zipPath); - const parsedBinaryInfo = await binaryCommand._parseApplicationBinary(res.application); + const parsedBinaryInfo = await binaryCommand._parseBinary(res.application); const verify = await binaryCommand._verifyBundle(parsedBinaryInfo, res.assets); @@ -251,5 +222,75 @@ describe('Binary Inspect', () => { expect(error.message).to.equal('Device protection feature is not supported for this binary.'); }); }); + + describe('listAssetsFromApplication', () => { + it('lists assets from a bundle', async () => { + const zipPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'bundle.zip'); + + const assets = await binaryCommand.listAssetsFromApplication(zipPath); + + expect(assets).to.have.lengthOf(3); + expect(assets.map(a => a.name)).to.eql(['cat.txt', 'house.txt', 'water.txt']); + }); + + it('lists assets from an application binary', async () => { + const binPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'app-with-assets.bin'); + + const assets = await binaryCommand.listAssetsFromApplication(binPath); + + expect(assets).to.have.lengthOf(3); + expect(assets.map(a => a.name)).to.eql(['cat.txt', 'house.txt', 'water.txt']); + }); + + it('lists assets from a binary which does not have assets', async () => { + const binPath = path.join(PATH_FIXTURES_BINARIES_DIR, 'argon_stroby.bin'); + + let error; + try { + await binaryCommand.listAssetsFromApplication(binPath); + } catch (e) { + error = e; + } + + expect(error).to.be.an.instanceof(Error); + expect(error.message).to.equal('No assets found'); + }); + }); + + describe('stripAssetsFromApplication', () => { + it('strips assets from a binary', async () => { + const binPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'app-with-assets.bin'); + + const res = await binaryCommand.stripAssetsFromApplication(binPath); + + expect(res).to.equal(PATH_FIXTURES_THIRDPARTY_OTA_DIR + '/app-with-assets-no-assets.bin'); + + await fs.remove(res); + }); + + it('strips assets from a bundle', async () => { + const zipPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'bundle.zip'); + + const res = await binaryCommand.stripAssetsFromApplication(zipPath); + + expect(res).to.equal(PATH_FIXTURES_THIRDPARTY_OTA_DIR + '/bundle-no-assets.bin'); + + await fs.remove(res); + }); + + it('errors if binary has no assets', async () => { + const binPath = path.join(PATH_FIXTURES_BINARIES_DIR, 'argon_stroby.bin'); + + let error; + try { + await binaryCommand.stripAssetsFromApplication(binPath); + } catch (e) { + error = e; + } + + expect(error).to.be.an.instanceof(Error); + expect(error.message).to.equal('No assets found'); + }); + }); }); diff --git a/test/__fixtures__/third_party_ota/app-with-assets.bin b/test/__fixtures__/third_party_ota/app-with-assets.bin new file mode 100644 index 000000000..948dabe32 Binary files /dev/null and b/test/__fixtures__/third_party_ota/app-with-assets.bin differ diff --git a/test/e2e/binary.e2e.js b/test/e2e/binary.e2e.js index 33337afc2..46fd8394b 100644 --- a/test/e2e/binary.e2e.js +++ b/test/e2e/binary.e2e.js @@ -7,7 +7,7 @@ const { } = require('../lib/env'); -describe('Binary Commands', () => { +describe.only('Binary Commands', () => { const help = [ 'Inspect binaries', 'Usage: particle binary ', @@ -260,5 +260,93 @@ describe('Binary Commands', () => { } }); }); + + describe('Binary Subcommand to list assets', () => { + it('Lists assets present in an application binary bundle', async () => { + const bin = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'bundle.zip'); + const args = ['binary', 'list-assets', bin]; + const expected = [ + 'Assets found in bundle.zip:', + ' cat.txt (b0f0d8ff8cc965a7b70b07e0c6b4c028f132597196ae9c70c620cb9e41344106)', + ' house.txt (a78fb0e7df9977ffd3102395254ae92dd332b46a616e75ff4701e75f91dd60d3)', + ' water.txt (3b0c25d6b8af66da115b30018ae94fbe3f04ac056fa60d1150131128baf8c591)' + ]; + + const { stdout, stderr, exitCode } = await cli.run(args); + + expect(stdout.split('\n')).to.include.members(expected); + expect(stderr).to.equal(''); + expect(exitCode).to.equal(0); + }); + + it('Lists assets present in an application binary', async () => { + const bin = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'app-with-assets.bin'); + const args = ['binary', 'list-assets', bin]; + const expected = [ + 'Assets found in app-with-assets.bin:', + ' cat.txt (b0f0d8ff8cc965a7b70b07e0c6b4c028f132597196ae9c70c620cb9e41344106)', + ' house.txt (a78fb0e7df9977ffd3102395254ae92dd332b46a616e75ff4701e75f91dd60d3)', + ' water.txt (3b0c25d6b8af66da115b30018ae94fbe3f04ac056fa60d1150131128baf8c591)' + ]; + + const { stdout, stderr, exitCode } = await cli.run(args); + + expect(stdout.split('\n')).to.include.members(expected); + expect(stderr).to.equal(''); + expect(exitCode).to.equal(0); + }); + + it('errors for binary without assets', async () => { + const bin = path.join(PATH_FIXTURES_BINARIES_DIR, 'argon_stroby.bin'); + const args = ['binary', 'list-assets', bin]; + + const { stdout, stderr, exitCode } = await cli.run(args); + + expect(stdout).to.equal('No assets found'); + expect(stderr).to.equal(''); + expect(exitCode).to.equal(1); + }); + }); + + describe('Binary Subcommand to strip assets', () => { + it('Removes assets from application binary', async () => { + try { + const bin = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'bundle.zip'); + const args = ['binary', 'strip-assets', bin]; + const { stdout, stderr, exitCode } = await cli.run(args); + + expect(stdout).to.include('Application binary without assets saved to'); + expect(stderr).to.equal(''); + expect(exitCode).to.equal(0); + } finally { + await fs.remove(path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'bundle-no-assets.bin')); + } + }); + + it ('Removes assets from application binary', async () => { + try { + const bin = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'app-with-assets.bin'); + const args = ['binary', 'strip-assets', bin]; + const { stdout, stderr, exitCode } = await cli.run(args); + + expect(stdout).to.include('Application binary without assets saved to'); + expect(stderr).to.equal(''); + expect(exitCode).to.equal(0); + } finally { + await fs.remove(path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'app-with-assets-no-assets.bin')); + } + }); + + it('errors for binary without assets', async () => { + const bin = path.join(PATH_FIXTURES_BINARIES_DIR, 'argon_stroby.bin'); + const args = ['binary', 'strip-assets', bin]; + + const { stdout, stderr, exitCode } = await cli.run(args); + + expect(stdout).to.equal('No assets found'); + expect(stderr).to.equal(''); + expect(exitCode).to.equal(1); + }); + }); });