-
Notifications
You must be signed in to change notification settings - Fork 24
Fix load hex file #175
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Fix load hex file #175
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assume it's callers responsibility to ensure it's only two digits? Some commentary would be helpful. |
||
| 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 | ||
|
|
@@ -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) })); | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: maybe rename to |
||
| 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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should that be accumulated? |
||
| const data = bytesToStringMemory(memory.bytes); | ||
| await memoryProvider.writeMemory({ memoryReference, data }); | ||
| } | ||
| await vscode.window.showInformationMessage(`Memory from '${vscode.workspace.asRelativePath(options.uri)}' applied.`); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Could extend the tests with some real world captures and test snapshots. Maybe even generated with the previous implementation as a reference.