Skip to content
Draft
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
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -552,4 +551,4 @@
"workspace",
"ui"
]
}
}
55 changes: 55 additions & 0 deletions src/common/intel-hex.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
178 changes: 178 additions & 0 deletions src/common/intel-hex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>): 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
Expand All @@ -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) }));
}
};
15 changes: 7 additions & 8 deletions src/plugin/memory-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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.`);
Expand Down
5 changes: 0 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading