From 8144eee68e7b11c8c000ac336e24d90f21ec5c77 Mon Sep 17 00:00:00 2001 From: AlCalzone Date: Wed, 6 Nov 2024 14:34:43 +0100 Subject: [PATCH] feat: add `tryUnzipFirmwareFile` utility (#7372) --- docs/api/node.md | 59 ++++++++++++++++++++++++++++-- packages/core/package.json | 1 + packages/core/src/util/firmware.ts | 44 +++++++++++++++++++--- yarn.lock | 8 ++++ 4 files changed, 103 insertions(+), 9 deletions(-) diff --git a/docs/api/node.md b/docs/api/node.md index 9dfd1a5ec3d4..0f7301aa12ad 100644 --- a/docs/api/node.md +++ b/docs/api/node.md @@ -394,6 +394,10 @@ extractFirmware(rawData: Buffer, format: FirmwareFileFormat): Firmware - `"hec"` - An encrypted Intel HEX firmware file - `"gecko"` - A binary gecko bootloader firmware file with `.gbl` extension +If successful, `extractFirmware` returns an `Firmware` object which can be passed to the `updateFirmware` method. + +If no firmware data can be extracted, the method will throw. + > [!ATTENTION] At the moment, only some `.exe` files contain `firmwareTarget` information. **All** other formats only contain the firmware `data`. > This means that the `firmwareTarget` property usually needs to be provided, unless it is `0`. @@ -406,10 +410,6 @@ guessFirmwareFileFormat(filename: string, rawData: Buffer): FirmwareFileFormat - `filename`: The name of the firmware file (including the extension) - `rawData`: A buffer containing the original firmware update file -If successful, `extractFirmware` returns an `Firmware` object which can be passed to the `updateFirmware` method. - -If no firmware data can be extracted, the method will throw. - Example usage: ```ts @@ -437,6 +437,57 @@ try { } ``` +In some cases, the firmware update file has to be extracted from a ZIP archive first. Z-Wave JS provides a utility method to do so, which must be used instead of `guessFirmwareFileFormat`: + +```ts +tryUnzipFirmwareFile(zipData: Uint8Array): { + filename: string; + format: FirmwareFileFormat; + rawData: Uint8Array; +} | undefined; +``` + +If the given ZIP archive contains a compatible firmware update file, the method returns an object with the following properties: + +- `filename`: The name of the unzipped firmware file. +- `format`: The guessed format of the unzipped firmware file (see `guessFirmwareFileFormat` above) +- `rawData`: A buffer containing the unzipped firmware update file. + +Otherwise `undefined` is returned. + +The unzipped firmware file can then be passed to `extractFirmware` to get the firmware data. Example usage: + +```ts +// Unzip the firmware archive +const unzippedFirmware = tryUnzipFirmwareFile(zipData); +if (!unzippedFirmware) { + // No firmware file found in the ZIP archive, abort update +} + +const { filename, format, rawData } = unzippedFirmware; +// Extract the firmware from a given firmware file +let actualFirmware: Firmware; +try { + actualFirmware = extractFirmware(rawData, format); +} catch (e) { + // handle the error, then abort the update +} + +if (actualFirmware.firmwareTarget == undefined) { + actualFirmware.firmwareTarget = getFirmwareTargetSomehow(); +} + +// try the update +try { + const result = await this.driver.controller.nodes + .get(nodeId)! + .updateFirmware([actualFirmware]); + // check result +} catch (e) { + // handle error +} +``` + ### `abortFirmwareUpdate` ```ts diff --git a/packages/core/package.json b/packages/core/package.json index 4ea5afef47a6..a0a4489203d8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -94,6 +94,7 @@ "alcalzone-shared": "^5.0.0", "ansi-colors": "^4.1.3", "dayjs": "^1.11.13", + "fflate": "^0.8.2", "logform": "^2.6.1", "nrf-intel-hex": "^1.4.0", "reflect-metadata": "^0.2.2", diff --git a/packages/core/src/util/firmware.ts b/packages/core/src/util/firmware.ts index c0063a226893..36d3d6fdc45c 100644 --- a/packages/core/src/util/firmware.ts +++ b/packages/core/src/util/firmware.ts @@ -1,5 +1,6 @@ import { getErrorMessage, isUint8Array } from "@zwave-js/shared"; import { Bytes } from "@zwave-js/shared/safe"; +import { unzipSync } from "fflate"; import * as crypto from "node:crypto"; import { ZWaveError, ZWaveErrorCodes } from "../error/ZWaveError.js"; import type { Firmware, FirmwareFileFormat } from "./_Types.js"; @@ -53,11 +54,44 @@ export function guessFirmwareFileFormat( .equals(firmwareIndicators.hec) ) { return "hec"; - } else { - throw new ZWaveError( - "Could not detect firmware format", - ZWaveErrorCodes.Invalid_Firmware_File, - ); + } + + throw new ZWaveError( + "Could not detect firmware format", + ZWaveErrorCodes.Invalid_Firmware_File, + ); +} + +/** + * Given the contents of a ZIP archive with a compatible firmware file, + * this function extracts the firmware data and guesses the firmware format + * using {@link guessFirmwareFileFormat}. + * + * @returns An object containing the filename, guessed format and unzipped data + * of the firmware file from the ZIP archive, or `undefined` if no compatible + * firmware file could be extracted. + */ +export function tryUnzipFirmwareFile(zipData: Uint8Array): { + filename: string; + format: FirmwareFileFormat; + rawData: Uint8Array; +} | undefined { + // Extract files we can work with + const unzipped = unzipSync(zipData, { + filter: (file) => { + return /\.(hex|exe|ex_|ota|otz|hec|gbl|bin)$/.test(file.name); + }, + }); + if (Object.keys(unzipped).length === 1) { + // Exactly one file was extracted, inspect that + const filename = Object.keys(unzipped)[0]; + const rawData = unzipped[filename]; + try { + const format = guessFirmwareFileFormat(filename, rawData); + return { filename, format, rawData }; + } catch { + return; + } } } diff --git a/yarn.lock b/yarn.lock index a1439ddb29f0..a89e48cfc663 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2633,6 +2633,7 @@ __metadata: ansi-colors: "npm:^4.1.3" dayjs: "npm:^1.11.13" del-cli: "npm:^6.0.0" + fflate: "npm:^0.8.2" logform: "npm:^2.6.1" nrf-intel-hex: "npm:^1.4.0" reflect-metadata: "npm:^0.2.2" @@ -5250,6 +5251,13 @@ __metadata: languageName: node linkType: hard +"fflate@npm:^0.8.2": + version: 0.8.2 + resolution: "fflate@npm:0.8.2" + checksum: 10/2bd26ba6d235d428de793c6a0cd1aaa96a06269ebd4e21b46c8fd1bd136abc631acf27e188d47c3936db090bf3e1ede11d15ce9eae9bffdc4bfe1b9dc66ca9cb + languageName: node + linkType: hard + "figures@npm:^2.0.0": version: 2.0.0 resolution: "figures@npm:2.0.0"