Files
zigbee-OTA/src/process_firmware_image.ts
dependabot[bot] 757710b4ee fix(ignore): bump typescript from 5.9.3 to 6.0.2 (#1112)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2026-04-14 20:35:31 +02:00

220 lines
7.9 KiB
TypeScript

import assert from "node:assert";
import {readdirSync, readFileSync, renameSync, rmSync, writeFileSync} from "node:fs";
import path from "node:path";
import {extract} from "tar";
import {
addImageToBase,
addImageToPrev,
BASE_IMAGES_DIR,
BASE_INDEX_MANIFEST_FILENAME,
findMatchImage,
getOutDir,
getParsedImageStatus,
ParsedImageStatus,
PREV_IMAGES_DIR,
PREV_INDEX_MANIFEST_FILENAME,
parseImageHeader,
readManifest,
TMP_DIR,
UPGRADE_FILE_IDENTIFIER,
writeManifest,
} from "./common.js";
import type {ExtraMetas} from "./types.js";
export enum ProcessFirmwareImageStatus {
Error = -1,
Success = 0,
RequestFailed = 1,
TarNoImage = 2,
}
async function tarExtract(filePath: string, outDir: string, tarImageFinder: (fileName: string) => boolean): Promise<string> {
let outFileName: string | undefined;
try {
console.log(`[${filePath}] Extracting TAR...`);
await extract({file: filePath, cwd: TMP_DIR});
for (const file of readdirSync(TMP_DIR)) {
const archiveFilePath = path.join(TMP_DIR, file);
if (tarImageFinder(file)) {
outFileName = file;
renameSync(archiveFilePath, path.join(outDir, outFileName));
} else {
rmSync(archiveFilePath, {force: true});
}
}
} catch (error) {
console.error(error);
// force throw below, just in case something crashed in-between this being assigned and the end of the try block
outFileName = undefined;
}
// always remove archive file once done
rmSync(filePath, {force: true});
if (!outFileName) {
throw new Error(`No image found in ${filePath}.`);
}
return outFileName;
}
export async function processFirmwareImage(
manufacturer: string,
firmwareFileName: string,
firmwareFileUrl: string,
extraMetas: ExtraMetas = {},
tar = false,
tarImageFinder?: (fileName: string) => boolean,
): Promise<ProcessFirmwareImageStatus> {
// throttle requests (this is done at the top to ensure always executed)
await new Promise((resolve) => setTimeout(resolve, 300));
let firmwareFilePath: string | undefined;
const logPrefix = `[${manufacturer}:${firmwareFileName}]`;
if (tar && !firmwareFileName.endsWith(".tar.gz")) {
// ignore non-archive
return ProcessFirmwareImageStatus.TarNoImage;
}
const prevManifest = readManifest(PREV_INDEX_MANIFEST_FILENAME);
const baseManifest = readManifest(BASE_INDEX_MANIFEST_FILENAME);
const baseOutDir = getOutDir(manufacturer, BASE_IMAGES_DIR);
const prevOutDir = getOutDir(manufacturer, PREV_IMAGES_DIR);
try {
const firmwareFile = await fetch(firmwareFileUrl);
if (!firmwareFile.ok || !firmwareFile.body) {
console.error(`${logPrefix} Invalid response from ${firmwareFileUrl} status=${firmwareFile.status}.`);
return ProcessFirmwareImageStatus.RequestFailed;
}
if (tar) {
assert(tarImageFinder, "No image finder function supplied for tar.");
const archiveBuffer = Buffer.from(await firmwareFile.arrayBuffer());
const archiveFilePath = path.join(baseOutDir, firmwareFileName);
writeFileSync(archiveFilePath, archiveBuffer);
try {
firmwareFileName = await tarExtract(archiveFilePath, baseOutDir, tarImageFinder);
} catch {
console.error(`${logPrefix} No image found for ${firmwareFileUrl}.`);
return ProcessFirmwareImageStatus.TarNoImage;
}
}
const firmwareBuffer = tar ? readFileSync(path.join(baseOutDir, firmwareFileName)) : Buffer.from(await firmwareFile.arrayBuffer());
// make sure to parse from the actual start of the "spec OTA" portion of the file (e.g. Ikea has non-spec meta before)
const parsedImage = parseImageHeader(firmwareBuffer.subarray(firmwareBuffer.indexOf(UPGRADE_FILE_IDENTIFIER)));
const [baseMatchIndex, baseMatch] = findMatchImage(parsedImage, baseManifest, extraMetas);
const statusToBase = getParsedImageStatus(parsedImage, baseMatch);
switch (statusToBase) {
case ParsedImageStatus.Older: {
// if prev doesn't have a match, move to prev
const [prevMatchIndex, prevMatch] = findMatchImage(parsedImage, prevManifest, extraMetas);
const statusToPrev = getParsedImageStatus(parsedImage, prevMatch);
switch (statusToPrev) {
case ParsedImageStatus.Older:
case ParsedImageStatus.Identical: {
console.log(
`${logPrefix} Base manifest has higher version and an equal or better match is already present in prev manifest. Ignoring.`,
);
break;
}
case ParsedImageStatus.Newer:
case ParsedImageStatus.New: {
addImageToPrev(
logPrefix,
statusToPrev === ParsedImageStatus.Newer,
prevManifest,
prevMatchIndex,
prevMatch!,
prevOutDir,
firmwareFileName,
manufacturer,
parsedImage,
firmwareBuffer,
firmwareFileUrl,
extraMetas,
() => {
firmwareFilePath = path.join(prevOutDir, firmwareFileName);
// write before adding to manifest, in case of failure (throw), manifest won't have a broken link
writeFileSync(firmwareFilePath, firmwareBuffer);
},
);
break;
}
}
break;
}
case ParsedImageStatus.Identical: {
console.log(`${logPrefix} Base manifest already has version ${parsedImage.fileVersion}. Ignoring.`);
break;
}
case ParsedImageStatus.Newer:
case ParsedImageStatus.New: {
addImageToBase(
logPrefix,
statusToBase === ParsedImageStatus.Newer,
prevManifest,
prevOutDir,
baseManifest,
baseMatchIndex,
baseMatch!,
baseOutDir,
firmwareFileName,
manufacturer,
parsedImage,
firmwareBuffer,
firmwareFileUrl,
extraMetas,
() => {
firmwareFilePath = path.join(baseOutDir, firmwareFileName);
// write before adding to manifest, in case of failure (throw), manifest won't have a broken link
writeFileSync(firmwareFilePath, firmwareBuffer);
},
);
break;
}
}
} catch (error) {
console.error(`${logPrefix} Failed to save firmware file ${firmwareFileName}: ${(error as Error).stack!}.`);
/* v8 ignore start */
if (firmwareFilePath) {
rmSync(firmwareFilePath, {force: true});
}
/* v8 ignore stop */
return ProcessFirmwareImageStatus.Error;
}
writeManifest(PREV_INDEX_MANIFEST_FILENAME, prevManifest);
writeManifest(BASE_INDEX_MANIFEST_FILENAME, baseManifest);
console.log(`Prev manifest has ${prevManifest.length} images.`);
console.log(`Base manifest has ${baseManifest.length} images.`);
return ProcessFirmwareImageStatus.Success;
}