diff --git a/package.json b/package.json index a767695..a52c66d 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "formik": "^2.4.5", "lodash": "^4.17.21", "memoize-one": "^6.0.0", - "nrf-intel-hex": "^1.4.0", "primeflex": "^3.3.1", "primereact": "^10.9.8", "react": "^19.2.6", @@ -552,4 +551,4 @@ "workspace", "ui" ] -} \ No newline at end of file +} diff --git a/src/common/intel-hex.spec.ts b/src/common/intel-hex.spec.ts new file mode 100644 index 0000000..cc8356f --- /dev/null +++ b/src/common/intel-hex.spec.ts @@ -0,0 +1,55 @@ +/******************************************************************************** + * Copyright (C) 2026 Arm Limited and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License, version 2 + * with the GNU Classpath Exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import 'mocha'; +import { expect } from 'chai'; +import { IntelHEX } from './intel-hex'; + +describe('intel-hex', () => { + it('round-trips memory above 0x80000000', () => { + const block: IntelHEX.MemoryBlock = { + address: 0x800B5DC0n, + bytes: Uint8Array.from({ length: 20 }, (_, index) => index) + }; + + const encoded = IntelHEX.encode(block); + const decoded = IntelHEX.decode(encoded); + + expect(decoded).to.have.length(1); + expect(decoded[0].address).to.equal(block.address); + expect(Array.from(decoded[0].bytes)).to.deep.equal(Array.from(block.bytes)); + }); + + it('splits records when data crosses a 64 KiB boundary', () => { + const block: IntelHEX.MemoryBlock = { + address: 0x0000FFF8n, + bytes: Uint8Array.from({ length: 32 }, (_, index) => index) + }; + + const encoded = IntelHEX.encode(block); + const decoded = IntelHEX.decode(encoded); + + expect(decoded).to.have.length(2); + expect(decoded[0].address).to.equal(0x0000FFF8n); + expect(decoded[0].bytes).to.have.length(8); + expect(decoded[1].address).to.equal(0x00010000n); + expect(decoded[1].bytes).to.have.length(24); + }); + + it('rejects invalid checksums', () => { + expect(() => IntelHEX.decode(':020000040001F8\n:00000001FF\n')).to.throw('Invalid Intel HEX checksum'); + }); +}); diff --git a/src/common/intel-hex.ts b/src/common/intel-hex.ts index 3ea4a45..3e2b200 100644 --- a/src/common/intel-hex.ts +++ b/src/common/intel-hex.ts @@ -16,7 +16,55 @@ import { URI, Utils } from 'vscode-uri'; +const DATA_RECORD = 0x00; +const END_OF_FILE_RECORD = 0x01; +const EXTENDED_SEGMENT_ADDRESS_RECORD = 0x02; +const START_SEGMENT_ADDRESS_RECORD = 0x03; +const EXTENDED_LINEAR_ADDRESS_RECORD = 0x04; +const START_LINEAR_ADDRESS_RECORD = 0x05; + +const MAX_INTEL_HEX_ADDRESS = 0xffff_ffffn; +const MAX_DATA_LENGTH = 0xff; +const DEFAULT_DATA_RECORD_LENGTH = 0x10; + +interface MutableMemoryBlock { + address: bigint; + bytes: number[]; +} + +function encodeHexByte(value: number): string { + return value.toString(16).toUpperCase().padStart(2, '0'); +} + +function parseHexByte(value: string, context: string): number { + const parsed = Number.parseInt(value, 16); + if (Number.isNaN(parsed)) { + throw new Error(`Invalid ${context}: '${value}'`); + } + return parsed; +} + +function createRecord(recordType: number, address: number, data: ArrayLike): string { + const bytes = [data.length, (address >> 8) & 0xff, address & 0xff, recordType, ...Array.from(data)]; + const checksum = (-bytes.reduce((sum, current) => sum + current, 0)) & 0xff; + return `:${bytes.map(encodeHexByte).join('')}${encodeHexByte(checksum)}`; +} + +function appendBytes(block: MutableMemoryBlock | undefined, address: bigint, data: number[]): MutableMemoryBlock { + if (!block || block.address + BigInt(block.bytes.length) !== address) { + return { address, bytes: [...data] }; + } + + block.bytes.push(...data); + return block; +} + export namespace IntelHEX { + export interface MemoryBlock { + address: bigint; + bytes: Uint8Array; + } + export namespace FileExtensions { export const All = [ // General @@ -40,4 +88,134 @@ export namespace IntelHEX { 'Intel HEX Files': IntelHEX.FileExtensions.All, 'All Files': ['*'] }; + + export function encode(blocks: MemoryBlock | readonly MemoryBlock[]): string { + const normalizedBlocks = Array.isArray(blocks) ? blocks : [blocks]; + const records: string[] = []; + let currentUpperAddress: number | undefined; + + for (const block of normalizedBlocks) { + if (block.address < 0n) { + throw new Error('Intel HEX addresses must be non-negative'); + } + if (block.address > MAX_INTEL_HEX_ADDRESS) { + throw new Error(`Intel HEX addresses must fit in 32 bits: ${block.address}`); + } + if (block.bytes.length === 0) { + continue; + } + + const blockEnd = block.address + BigInt(block.bytes.length - 1); + if (blockEnd > MAX_INTEL_HEX_ADDRESS) { + throw new Error(`Intel HEX data exceeds 32-bit address space: ${blockEnd}`); + } + + let offset = 0; + while (offset < block.bytes.length) { + const absoluteAddress = block.address + BigInt(offset); + const upperAddress = Number((absoluteAddress >> 16n) & 0xffffn); + if (currentUpperAddress !== upperAddress) { + records.push(createRecord(EXTENDED_LINEAR_ADDRESS_RECORD, 0, [upperAddress >> 8, upperAddress & 0xff])); + currentUpperAddress = upperAddress; + } + + const lowerAddress = Number(absoluteAddress & 0xffffn); + const remainingInSegment = 0x10000 - lowerAddress; + const chunkLength = Math.min(DEFAULT_DATA_RECORD_LENGTH, remainingInSegment, block.bytes.length - offset, MAX_DATA_LENGTH); + const chunk = block.bytes.slice(offset, offset + chunkLength); + records.push(createRecord(DATA_RECORD, lowerAddress, chunk)); + offset += chunkLength; + } + } + + records.push(createRecord(END_OF_FILE_RECORD, 0, [])); + return `${records.join('\n')}\n`; + } + + export function decode(content: string): MemoryBlock[] { + const blocks: MutableMemoryBlock[] = []; + let currentBlock: MutableMemoryBlock | undefined; + let baseAddress = 0n; + let sawEof = false; + + const lines = content.split(/\r?\n/); + lines.forEach((rawLine, index) => { + const line = rawLine.trim(); + if (line.length === 0) { + return; + } + if (sawEof) { + throw new Error(`Unexpected data after EOF record on line ${index + 1}`); + } + if (!line.startsWith(':')) { + throw new Error(`Invalid Intel HEX record on line ${index + 1}: missing ':' prefix`); + } + if (line.length < 11 || (line.length - 1) % 2 !== 0) { + throw new Error(`Invalid Intel HEX record length on line ${index + 1}`); + } + + const bytes: number[] = []; + for (let cursor = 1; cursor < line.length; cursor += 2) { + bytes.push(parseHexByte(line.slice(cursor, cursor + 2), `hex byte on line ${index + 1}`)); + } + + const recordLength = bytes[0]; + const expectedLength = recordLength + 5; + if (bytes.length !== expectedLength) { + throw new Error(`Invalid Intel HEX byte count on line ${index + 1}`); + } + + const checksum = bytes.reduce((sum, value) => sum + value, 0) & 0xff; + if (checksum !== 0) { + throw new Error(`Invalid Intel HEX checksum on line ${index + 1}`); + } + + const recordAddress = (bytes[1] << 8) | bytes[2]; + const recordType = bytes[3]; + const data = bytes.slice(4, bytes.length - 1); + + switch (recordType) { + case DATA_RECORD: { + const absoluteAddress = baseAddress + BigInt(recordAddress); + currentBlock = appendBytes(currentBlock, absoluteAddress, data); + if (!blocks.includes(currentBlock)) { + blocks.push(currentBlock); + } + break; + } + case END_OF_FILE_RECORD: + if (recordLength !== 0 || recordAddress !== 0) { + throw new Error(`Invalid EOF record on line ${index + 1}`); + } + sawEof = true; + break; + case EXTENDED_SEGMENT_ADDRESS_RECORD: + if (recordLength !== 2 || recordAddress !== 0) { + throw new Error(`Invalid extended segment address record on line ${index + 1}`); + } + baseAddress = (BigInt((data[0] << 8) | data[1])) << 4n; + currentBlock = undefined; + break; + case EXTENDED_LINEAR_ADDRESS_RECORD: + if (recordLength !== 2 || recordAddress !== 0) { + throw new Error(`Invalid extended linear address record on line ${index + 1}`); + } + baseAddress = (BigInt((data[0] << 8) | data[1])) << 16n; + currentBlock = undefined; + break; + case START_SEGMENT_ADDRESS_RECORD: + case START_LINEAR_ADDRESS_RECORD: + currentBlock = undefined; + break; + default: + throw new Error(`Unsupported Intel HEX record type 0x${recordType.toString(16).toUpperCase().padStart(2, '0')} on line ${index + 1}`); + } + }); + + if (!sawEof) { + throw new Error('Invalid Intel HEX file: missing EOF record'); + } + + return blocks.map(block => ({ address: block.address, bytes: Uint8Array.from(block.bytes) })); + } }; diff --git a/src/plugin/memory-storage.ts b/src/plugin/memory-storage.ts index f645172..51b0f6d 100644 --- a/src/plugin/memory-storage.ts +++ b/src/plugin/memory-storage.ts @@ -14,7 +14,6 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import MemoryMap from 'nrf-intel-hex'; import * as vscode from 'vscode'; import { URI, Utils } from 'vscode-uri'; import { isVariablesContext } from '../common/external-views'; @@ -81,8 +80,8 @@ export class MemoryStorage { try { const memoryResponse = await memoryProvider.readMemory(readArgs); const memory = createMemoryFromRead(memoryResponse); - const memoryMap = new MemoryMap({ [Number(memory.address)]: memory.bytes }); - await vscode.workspace.fs.writeFile(outputFile, new TextEncoder().encode(memoryMap.asHexString())); + const encodedContent = IntelHEX.encode({ address: memory.address, bytes: memory.bytes }); + await vscode.workspace.fs.writeFile(outputFile, new TextEncoder().encode(encodedContent)); } catch (error) { if (error instanceof Error) { vscode.window.showErrorMessage(`Could not write memory to '${vscode.workspace.asRelativePath(outputFile)}': ${error.message}`); @@ -176,13 +175,13 @@ export class MemoryStorage { } try { const byteContent = await vscode.workspace.fs.readFile(options.uri); - const memoryMap = MemoryMap.fromHex(new TextDecoder().decode(byteContent)); let memoryReference: string | undefined; let count: number | undefined; - for (const [address, memory] of memoryMap) { - memoryReference = toHexStringWithRadixMarker(address); - count = memory.length; - const data = bytesToStringMemory(memory); + const memoryBlocks = IntelHEX.decode(new TextDecoder().decode(byteContent)); + for (const memory of memoryBlocks) { + memoryReference = toHexStringWithRadixMarker(memory.address); + count = memory.bytes.length; + const data = bytesToStringMemory(memory.bytes); await memoryProvider.writeMemory({ memoryReference, data }); } await vscode.window.showInformationMessage(`Memory from '${vscode.workspace.asRelativePath(options.uri)}' applied.`); diff --git a/yarn.lock b/yarn.lock index 497943d..9240d37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3723,11 +3723,6 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -nrf-intel-hex@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/nrf-intel-hex/-/nrf-intel-hex-1.4.0.tgz#f14d5d89a09437407536652ca3a377cef915be9e" - integrity sha512-q3+GGRIpe0VvCjUP1zaqW5rk6IpCZzhD0lu7Sguo1bgWwFcA9kZRjsaKUb0jBQMnefyOl5o0BBGAxvqMqYx8Sg== - nth-check@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"