Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/sc 118580/binary inspect bundle #642

Merged
merged 13 commits into from
May 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
],
"dependencies": {
"@particle/device-constants": "^3.1.9",
"binary-version-reader": "^2.1.0",
"binary-version-reader": "^2.2.0",
"chalk": "^2.4.2",
"cli-spinner": "^0.2.10",
"cli-table": "^0.3.1",
Expand Down
59 changes: 59 additions & 0 deletions src/cmd/README_binary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# binary.js

## Overview

`binary.js` is a command module in the `particle-cli` tool that provides functionality related to inspecting binary files and/or bundles that may contain binary files and assets.

## Usage

To use the `binary.js` command module, you can run the following commands:

- `particle binary inspect <.bin file>`: Inspects a binary file and displays detailed information such as filename, crc, prefixInfo, suffixInfo, and other relevant metadata.

- `particle binary inspect <.bin file with baked-in dependencies>`: Inspects a binary file and displays detailed information such as filename, crc, prefixInfo, suffixInfo, and other relevant metadata such as TLV of the (asset) files.

- `particle binary inspect <.zip file>`: Extracts and inspects contents from the zip file

## Command-Line Options

- `--verbose` or `-v`: Increases how much logging to display

- `--quiet` or `-q`: Decreases how much logging to display

## Examples

1. Inspecting a binary file such as tinker.bin:

```
particle binary inspect p2_app.bin

> particle-cli@3.10.2 start
> node ./src/index.js binary inspect /path/to/p2_app.bin

p2_app.bin
CRC is ok (6e2abf80)
Compiled for p2
This is an application module number 1 at version 6
It depends on a system module number 1 at version 5302
```

2. Inspecting a zip file whose app firmware has baked-in asset dependencies:

```
$ npm start -- binary inspect /path/to/bundle.zip

> particle-cli@3.10.2 start
> node ./src/index.js binary inspect /path/to/bundle.zip

app.bin
CRC is ok (7fa30408)
Compiled for argon
This is an application module number 2 at version 6
It is firmware for product id 12 at version 3
It depends on a system module number 1 at version 4006
It depends on assets:
cat.txt (hash b0f0d8ff8cc965a7b70b07e0c6b4c028f132597196ae9c70c620cb9e41344106)
house.txt (hash a78fb0e7df9977ffd3102395254ae92dd332b46a616e75ff4701e75f91dd60d3)
water.txt (hash 3b0c25d6b8af66da115b30018ae94fbe3f04ac056fa60d1150131128baf8c591)
```

87 changes: 65 additions & 22 deletions src/cmd/binary.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ License along with this program; if not, see <http://www.gnu.org/licenses/>.
******************************************************************************
*/

const fs = require('fs');
const fs = require('fs-extra');
const path = require('path');
const VError = require('verror');
const chalk = require('chalk');
const Parser = require('binary-version-reader').HalModuleParser;
const { HalModuleParser: Parser, unpackApplicationAndAssetBundle, isAssetValid } = require('binary-version-reader');
const utilities = require('../lib/utilities');
const ensureError = utilities.ensureError;

Expand All @@ -38,30 +38,73 @@ const DEFAULT_PRODUCT_ID = 65535;
const DEFAULT_PRODUCT_VERSION = 65535;

class BinaryCommand {
inspectBinary(binaryFile){
return Promise.resolve().then(() => {
if (!fs.existsSync(binaryFile)){
throw new VError(`Binary file not found ${binaryFile}`);
}
async inspectBinary(file) {
await this._checkFile(file);
const extractedFiles = await this._extractFiles(file);
const parsedBinaryInfo = await this._parseApplicationBinary(extractedFiles.application);
await this._verifyBundle(parsedBinaryInfo, extractedFiles.assets);
}

const parser = new Parser();
async _checkFile(file) {
// TODO: what happens if this is removed? What kind of error do we get?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check what happens without the file existance check for binary inspect on a .bin and on a .zip? If the error is reasonable, remove this function. If not, keep it and remove the comment.

try {
await fs.access(file);
} catch (error) {
throw new Error(`File does not exist: ${file}`);
}
return true;
}

return parser.parseFile(binaryFile)
.catch(err => {
throw new VError(ensureError(err), `Could not parse ${binaryFile}`);
})
.then(fileInfo => {
if (fileInfo.suffixInfo.suffixSize === INVALID_SUFFIX_SIZE){
throw new VError(`${binaryFile} does not contain inspection information`);
}
async _extractFiles(file) {
if (utilities.getFilenameExt(file) === '.zip') {
return unpackApplicationAndAssetBundle(file);
} else if (utilities.getFilenameExt(file) === '.bin') {
const data = await fs.readFile(file);
return { application: { name: path.basename(file), data }, assets: [] };
} else {
throw new VError(`File must be a .bin or .zip file: ${file}`);
}
}

console.log(chalk.bold(path.basename(binaryFile)));
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){
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(fileInfo);
this._showPlatform(fileInfo);
this._showModuleInfo(fileInfo);
});
});
async _verifyBundle(binaryFileInfo, assets) {
if (binaryFileInfo.assets && assets.length){
console.log('It depends on assets:');
for (const assetInfo of binaryFileInfo.assets) {
const asset = assets.find((asset) => asset.name === assetInfo.name);
if (asset) {
const valid = isAssetValid(asset.data, assetInfo);

if (valid) {
console.log(' ' + chalk.bold(assetInfo.name) + ' (hash ' + assetInfo.hash + ')');
} else {
console.log(chalk.red(' ' + assetInfo.name + ' failed' + ' (hash should be ' + assetInfo.hash + ')'));
}
} else {
console.log(chalk.red(' ' + assetInfo.name + ' failed' + ' (hash should be ' + assetInfo.hash + ' but is not in the bundle)'));
}
}
}
return true;
}

_showCrc(fileInfo){
Expand Down
128 changes: 128 additions & 0 deletions src/cmd/binary.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
const BinaryCommand = require('../cmd/binary');
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', () => {
let binaryCommand;

beforeEach(async () => {
binaryCommand = new BinaryCommand();
});

describe('__checkFile', () => {
it('errors if file does not exist', async () => {
let error;

try {
await binaryCommand._checkFile('does-not-exist.bin');
} catch (_error) {
error = _error;
}

expect(error).to.be.an.instanceof(Error);
expect(error.message).to.equal('File does not exist: does-not-exist.bin');
});

it('returns nothing if file exists', async () => {
let res = false;
try {
res = await binaryCommand._checkFile(path.join(PATH_FIXTURES_BINARIES_DIR, 'argon_stroby.bin'));
} catch (err) {
// ignore error
}

expect(res).to.equal(true);
});
});

describe('_extractFiles', () => {
it('errors if file is not .zip or .bin', async () => {
let error;

try {
await binaryCommand._extractFiles('not-a-zip-or-bin-file');
} catch (_error) {
error = _error;
}

expect(error).to.be.an.instanceof(Error);
expect(error.message).to.equal('File must be a .bin or .zip file: not-a-zip-or-bin-file');
});

it('extracts a .zip file', async () => {
const zipPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'bundle.zip');

const binaryInfo = await binaryCommand._extractFiles(zipPath);

expect(binaryInfo).to.have.property('application').with.property('name', 'app.bin');
expect(binaryInfo).to.have.property('assets').with.lengthOf(3);
expect(binaryInfo.assets.map(a => a.name)).to.eql(['cat.txt', 'house.txt', 'water.txt']);
});

xit('errors out if the .zip file does not contain a .bin', async () => {
// TODO
});

it('extracts a .bin file', async () => {
const binPath = path.join(PATH_FIXTURES_BINARIES_DIR, 'argon_stroby.bin');

const binaryInfo = await binaryCommand._extractFiles(binPath);

expect(binaryInfo).to.have.property('application').with.property('name', 'argon_stroby.bin');
expect(binaryInfo).to.have.property('assets').with.lengthOf(0);
});

it('handles if zip file does not have a binary or assets', async () => {
const zipPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'invalid-bundle.zip');

const binaryInfo = await binaryCommand._extractFiles(zipPath);

expect(binaryInfo).to.have.property('application').with.property('name', 'app.txt');
expect(binaryInfo).to.have.property('assets').with.lengthOf(0);

});
});

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('_verifyBundle', () => {
it('verifies bundle with asset info', async () => {
const zipPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'bundle.zip');
const res = await binaryCommand._extractFiles(zipPath);
const parsedBinaryInfo = await binaryCommand._parseApplicationBinary(res.application);

const verify = await binaryCommand._verifyBundle(parsedBinaryInfo, res.assets);

expect(verify).to.equal(true);
});
});
});

3 changes: 1 addition & 2 deletions src/cmd/bundle.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,7 @@ describe('BundleCommands', () => {
expect(bundleFilename).to.eq(targetBundlePath);
});

// TODO: uncomment this test when binary version reader is able to create a bundle with 0 assets
xit('creates a bundle if there are no assets in the assets folder', async () => {
it('creates a bundle if there are no assets in the assets folder', async () => {
const binPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'zero_assets', 'app.bin');
const assetsPath = path.join(PATH_FIXTURES_THIRDPARTY_OTA_DIR, 'zero_assets', 'assets');
const args = {
Expand Down
Binary file not shown.
Binary file not shown.
Binary file added test/__fixtures__/third_party_ota/bundle.zip
Binary file not shown.
Binary file added test/__fixtures__/third_party_ota/invalid-bundle.zip
Binary file not shown.
Loading