From 13c69918e1f1bff13e7881bac5580958e4af266f Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 16 Feb 2023 11:18:09 -0600 Subject: [PATCH] firmware/installPybricksDialog: add basic metadata check This will catch some errors. Mainly this is to ensure we show an error if someone tries to use, e.g. a NXT firmware file that isn't supported in Pybricks Code yet. Error message isn't great but we don't expect this to be hit often, if ever, by non-developers. --- src/firmware/installPybricksDialog/hooks.ts | 4 ++ .../installPybricksDialog/index.test.ts | 64 +++++++++++++++++++ src/firmware/installPybricksDialog/index.ts | 29 ++++++++- 3 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 src/firmware/installPybricksDialog/index.test.ts diff --git a/src/firmware/installPybricksDialog/hooks.ts b/src/firmware/installPybricksDialog/hooks.ts index 69c1c683..e3c5999f 100644 --- a/src/firmware/installPybricksDialog/hooks.ts +++ b/src/firmware/installPybricksDialog/hooks.ts @@ -14,6 +14,7 @@ import { useIsMounted } from 'usehooks-ts'; import { alertsShowAlert } from '../../alerts/actions'; import { Hub } from '../../components/hubPicker'; import { ensureError } from '../../utils'; +import { validateMetadata } from './'; export type FirmwareData = { firmwareZip: ArrayBuffer; @@ -175,6 +176,9 @@ export function useCustomFirmware(zipFile: File | undefined) { const reader = await FirmwareReader.load(firmwareZip); const licenseText = await reader.readReadMeOss(); const metadata = await reader.readMetadata(); + + validateMetadata(metadata); + const data = { firmwareZip, licenseText, diff --git a/src/firmware/installPybricksDialog/index.test.ts b/src/firmware/installPybricksDialog/index.test.ts new file mode 100644 index 00000000..b4cbd01d --- /dev/null +++ b/src/firmware/installPybricksDialog/index.test.ts @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2023 The Pybricks Authors + +import { validateMetadata } from '.'; + +describe('validateMetadata', () => { + it('should accept 1.x firmware metadata', () => { + expect(() => + validateMetadata({ + 'metadata-version': '1.0.0', + 'device-id': 0x40, + 'firmware-version': '3.0.0', + 'checksum-type': 'sum', + 'max-firmware-size': 100000, + 'mpy-abi-version': 6, + 'mpy-cross-options': ['-mno-unicode'], + 'user-mpy-offset': 90000, + }), + ).not.toThrow(); + }); + + it('should accept 2.x firmware metadata', () => { + expect(() => + validateMetadata({ + 'metadata-version': '2.0.0', + 'device-id': 0x40, + 'firmware-version': '3.0.0', + 'checksum-type': 'sum', + 'checksum-size': 100000, + 'hub-name-offset': 100000 - 16, + 'hub-name-size': 16, + }), + ).not.toThrow(); + }); + + it('should reject 3.x firmware metadata', () => { + expect(() => + validateMetadata({ + // @ts-expect-error: testing bad data + 'metadata-version': '3.0.0', + 'device-id': 0x40, + 'firmware-version': '3.0.0', + 'checksum-type': 'sum', + 'checksum-size': 100000, + 'hub-name-offset': 100000 - 16, + 'hub-name-size': 16, + }), + ).toThrow(/"metadata-version"/); + }); + + it('should reject unsupported device id', () => { + expect(() => + validateMetadata({ + 'metadata-version': '2.0.0', + 'device-id': 0xe0, + 'firmware-version': '3.0.0', + 'checksum-type': 'sum', + 'checksum-size': 100000, + 'hub-name-offset': 100000 - 16, + 'hub-name-size': 16, + }), + ).toThrow(/"device-id"/); + }); +}); diff --git a/src/firmware/installPybricksDialog/index.ts b/src/firmware/installPybricksDialog/index.ts index 2323fb34..c8e9acd4 100644 --- a/src/firmware/installPybricksDialog/index.ts +++ b/src/firmware/installPybricksDialog/index.ts @@ -1,5 +1,8 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2022 The Pybricks Authors +// Copyright (c) 2022-2023 The Pybricks Authors + +import { FirmwareMetadata, HubType } from '@pybricks/firmware'; +import * as semver from 'semver'; const encoder = new TextEncoder(); @@ -18,3 +21,27 @@ export function validateHubName(hubName: string): boolean { // user earlier for better UX. return encoded.length < 16; } + +const supportHubs: readonly HubType[] = [ + HubType.MoveHub, + HubType.CityHub, + HubType.TechnicHub, + HubType.PrimeHub, + HubType.EssentialHub, +]; + +export function validateMetadata(metadata: FirmwareMetadata) { + if (semver.satisfies(metadata['metadata-version'], '^1')) { + if (!supportHubs.includes(metadata['device-id'])) { + throw new Error(`"unsupported "device-id": ${metadata['device-id']}`); + } + } else if (semver.satisfies(metadata['metadata-version'], '^2')) { + if (!supportHubs.includes(metadata['device-id'])) { + throw new Error(`"unsupported "device-id": ${metadata['device-id']}`); + } + } else { + throw new Error( + `unsupported "metadata-version": "${metadata['metadata-version']}"`, + ); + } +}