@microbit/microbit-universal-hex
Version:
Create micro:bit Universal Hexes.
359 lines • 14.3 kB
JavaScript
/**
* Generate and process Intel Hex records.
* @packageDocumentation
*
* (c) 2020 Micro:bit Educational Foundation and contributors.
* SPDX-License-Identifier: MIT
*/
import * as utils from './utils';
/** Values for the Record Type field, including Universal Hex custom types. */
var RecordType;
(function (RecordType) {
RecordType[RecordType["Data"] = 0] = "Data";
RecordType[RecordType["EndOfFile"] = 1] = "EndOfFile";
RecordType[RecordType["ExtendedSegmentAddress"] = 2] = "ExtendedSegmentAddress";
RecordType[RecordType["StartSegmentAddress"] = 3] = "StartSegmentAddress";
RecordType[RecordType["ExtendedLinearAddress"] = 4] = "ExtendedLinearAddress";
RecordType[RecordType["StartLinearAddress"] = 5] = "StartLinearAddress";
RecordType[RecordType["BlockStart"] = 10] = "BlockStart";
RecordType[RecordType["BlockEnd"] = 11] = "BlockEnd";
RecordType[RecordType["PaddedData"] = 12] = "PaddedData";
RecordType[RecordType["CustomData"] = 13] = "CustomData";
RecordType[RecordType["OtherData"] = 14] = "OtherData";
})(RecordType || (RecordType = {}));
/**
* The maximum data bytes per record is 0xFF, 16 and 32 bytes are the two most
* common lengths, but DAPLink doesn't support more than 32 bytes.
*/
var RECORD_DATA_MAX_BYTES = 32;
/**
* Constants for the record character lengths.
*/
var START_CODE_STR = ':';
var START_CODE_INDEX = 0;
var START_CODE_STR_LEN = START_CODE_STR.length;
var BYTE_COUNT_STR_INDEX = START_CODE_INDEX + START_CODE_STR_LEN;
var BYTE_COUNT_STR_LEN = 2;
var ADDRESS_STR_INDEX = BYTE_COUNT_STR_INDEX + BYTE_COUNT_STR_LEN;
var ADDRESS_STR_LEN = 4;
var RECORD_TYPE_STR_INDEX = ADDRESS_STR_INDEX + ADDRESS_STR_LEN;
var RECORD_TYPE_STR_LEN = 2;
var DATA_STR_INDEX = RECORD_TYPE_STR_INDEX + RECORD_TYPE_STR_LEN;
var DATA_STR_LEN_MIN = 0;
var CHECKSUM_STR_LEN = 2;
var MIN_RECORD_STR_LEN = START_CODE_STR_LEN +
BYTE_COUNT_STR_LEN +
ADDRESS_STR_LEN +
RECORD_TYPE_STR_LEN +
DATA_STR_LEN_MIN +
CHECKSUM_STR_LEN;
var MAX_RECORD_STR_LEN = MIN_RECORD_STR_LEN - DATA_STR_LEN_MIN + RECORD_DATA_MAX_BYTES * 2;
/**
* Checks if a given number is a valid Record type.
*
* @param recordType Number to check
* @returns True if it's a valid Record type.
*/
function isRecordTypeValid(recordType) {
// Checking ranges is more efficient than object key comparison
// This also allow us use a const enum (compilation replaces it by literals)
if ((recordType >= RecordType.Data &&
recordType <= RecordType.StartLinearAddress) ||
(recordType >= RecordType.BlockStart && recordType <= RecordType.OtherData)) {
return true;
}
return false;
}
/**
* Calculates the Intel Hex checksum.
*
* This is basically the LSB of the two's complement of the sum of all bytes.
*
* @param dataBytes A byte array to calculate the checksum into.
* @returns Checksum byte.
*/
function calcChecksumByte(dataBytes) {
var sum = dataBytes.reduce(function (accumulator, currentValue) { return accumulator + currentValue; }, 0);
return -sum & 0xff;
}
/**
* Creates an Intel Hex record with normal or custom record types.
*
* @param address - The two least significant bytes for the data address.
* @param recordType - Record type, could be one of the standard types or any
* of the custom types created for forming a Universal Hex.
* @param dataBytes - Byte array with the data to include in the record.
* @returns A string with the Intel Hex record.
*/
function createRecord(address, recordType, dataBytes) {
if (address < 0 || address > 0xffff) {
throw new Error("Record (" + recordType + ") address out of range: " + address);
}
var byteCount = dataBytes.length;
if (byteCount > RECORD_DATA_MAX_BYTES) {
throw new Error("Record (" + recordType + ") data has too many bytes (" + byteCount + ").");
}
if (!isRecordTypeValid(recordType)) {
throw new Error("Record type '" + recordType + "' is not valid.");
}
var recordContent = utils.concatUint8Arrays([
new Uint8Array([byteCount, address >> 8, address & 0xff, recordType]),
dataBytes,
]);
var recordContentStr = utils.byteArrayToHexStr(recordContent);
var checksumStr = utils.byteToHexStrFast(calcChecksumByte(recordContent));
return "" + START_CODE_STR + recordContentStr + checksumStr;
}
/**
* Check if an Intel Hex record conforms to the following rules:
* - Correct length of characters
* - Starts with a colon
*
* TODO: Apply more rules.
*
* @param iHexRecord - Single Intel Hex record to check.
* @returns A boolean indicating if the record is valid.
*/
function validateRecord(iHexRecord) {
if (iHexRecord.length < MIN_RECORD_STR_LEN) {
throw new Error("Record length too small: " + iHexRecord);
}
if (iHexRecord.length > MAX_RECORD_STR_LEN) {
throw new Error("Record length is too large: " + iHexRecord);
}
if (iHexRecord[0] !== ':') {
throw new Error("Record does not start with a \":\": " + iHexRecord);
}
return true;
}
/**
* Retrieves the Record Type form an Intel Hex record line.
*
* @param iHexRecord Intel hex record line without line terminator.
* @returns The RecordType value.
*/
function getRecordType(iHexRecord) {
validateRecord(iHexRecord);
var recordTypeCharStart = START_CODE_STR_LEN + BYTE_COUNT_STR_LEN + ADDRESS_STR_LEN;
var recordTypeStr = iHexRecord.slice(recordTypeCharStart, recordTypeCharStart + RECORD_TYPE_STR_LEN);
var recordType = parseInt(recordTypeStr, 16);
if (!isRecordTypeValid(recordType)) {
throw new Error("Record type '" + recordTypeStr + "' from record '" + iHexRecord + "' is not valid.");
}
return recordType;
}
/**
* Retrieves the data field from a record.
*
* @param iHexRecord Intel Hex record string.
* @returns The record Data in a byte array.
*/
function getRecordData(iHexRecord) {
try {
// The only thing after the Data bytes is the Checksum (2 characters)
return utils.hexStrToBytes(iHexRecord.slice(DATA_STR_INDEX, -2));
}
catch (e) {
throw new Error("Could not parse Intel Hex record \"" + iHexRecord + "\": " + e.message);
}
}
/**
* Parses an Intel Hex record into an Record object with its respective fields.
*
* @param iHexRecord Intel hex record line without line terminator.
* @returns New object with the Record interface.
*/
function parseRecord(iHexRecord) {
validateRecord(iHexRecord);
var recordBytes;
try {
recordBytes = utils.hexStrToBytes(iHexRecord.substring(1));
}
catch (e) {
throw new Error("Could not parse Intel Hex record \"" + iHexRecord + "\": " + e.message);
}
var byteCountIndex = 0;
var byteCount = recordBytes[0];
var addressIndex = byteCountIndex + BYTE_COUNT_STR_LEN / 2;
var address = (recordBytes[addressIndex] << 8) + recordBytes[addressIndex + 1];
var recordTypeIndex = addressIndex + ADDRESS_STR_LEN / 2;
var recordType = recordBytes[recordTypeIndex];
var dataIndex = recordTypeIndex + RECORD_TYPE_STR_LEN / 2;
var checksumIndex = dataIndex + byteCount;
var data = recordBytes.slice(dataIndex, checksumIndex);
var checksum = recordBytes[checksumIndex];
var totalLength = checksumIndex + CHECKSUM_STR_LEN / 2;
if (recordBytes.length > totalLength) {
throw new Error("Parsed record \"" + iHexRecord + "\" is larger than indicated by the byte count." +
("\n\tExpected: " + totalLength + "; Length: " + recordBytes.length + "."));
}
return {
byteCount: byteCount,
address: address,
recordType: recordType,
data: data,
checksum: checksum,
};
}
/**
* Creates an End Of File Intel Hex record.
*
* @returns End of File record with new line.
*/
function endOfFileRecord() {
// No need to use createRecord(), this record is always the same
return ':00000001FF';
}
/**
* Creates an Extended Linear Address record from a 4 byte address.
*
* @param address - Full 32 bit address.
* @returns The Extended Linear Address Intel Hex record.
*/
function extLinAddressRecord(address) {
if (address < 0 || address > 0xffffffff) {
throw new Error("Address '" + address + "' for Extended Linear Address record is out of range.");
}
return createRecord(0, RecordType.ExtendedLinearAddress, new Uint8Array([(address >> 24) & 0xff, (address >> 16) & 0xff]));
}
/**
* Creates a Block Start (custom) Intel Hex Record.
*
* @param boardId Board ID to embed into the record, 0 to 0xFFF.
* @returns A Block Start (custom) Intel Hex record.
*/
function blockStartRecord(boardId) {
if (boardId < 0 || boardId > 0xffff) {
throw new Error('Board ID out of range when creating Block Start record.');
}
return createRecord(0, RecordType.BlockStart, new Uint8Array([(boardId >> 8) & 0xff, boardId & 0xff, 0xc0, 0xde]));
}
/**
* Create Block End (custom) Intel Hex Record.
*
* The Data field in this Record will be ignored and can be used for padding.
*
* @param padBytesLen Number of bytes to add to the Data field.
* @returns A Block End (custom) Intel Hex record.
*/
function blockEndRecord(padBytesLen) {
// This function is called very often with the same arguments, so cache
// those results for better performance
switch (padBytesLen) {
case 0x4:
// Common for blocks that have full Data records with 0x10 bytes and a
// single Extended Linear Address record
return ':0400000BFFFFFFFFF5';
case 0x0c:
// The most common padding, when a block has 10 full (0x10) Data records
return ':0C00000BFFFFFFFFFFFFFFFFFFFFFFFFF5';
default:
// Input sanitation will be done in createRecord, no need to do it here too
var recordData = new Uint8Array(padBytesLen).fill(0xff);
return createRecord(0, RecordType.BlockEnd, recordData);
}
}
/**
* Create a Padded Data (custom) Intel Hex Record.
* This record is used to add padding data, to be ignored by DAPLink, to be able
* to create blocks of 512-bytes.
*
* @param padBytesLen Number of bytes to add to the Data field.
* @returns A Padded Data (custom) Intel Hex record.
*/
function paddedDataRecord(padBytesLen) {
// Input sanitation will be done in createRecord, no need to do it here too
var recordData = new Uint8Array(padBytesLen).fill(0xff);
return createRecord(0, RecordType.PaddedData, recordData);
}
/**
* Changes the record type of a Record to a Custom Data type.
*
* The data field is kept, but changing the record type will trigger the
* checksum to be updated as well.
*
* @param iHexRecord Intel hex record line without line terminator.
* @returns A Custom Data Intel Hex record with the same data field.
*/
function convertRecordTo(iHexRecord, recordType) {
var oRecord = parseRecord(iHexRecord);
var recordContent = new Uint8Array(oRecord.data.length + 4);
recordContent[0] = oRecord.data.length;
recordContent[1] = oRecord.address >> 8;
recordContent[2] = oRecord.address & 0xff;
recordContent[3] = recordType;
recordContent.set(oRecord.data, 4);
var recordContentStr = utils.byteArrayToHexStr(recordContent);
var checksumStr = utils.byteToHexStrFast(calcChecksumByte(recordContent));
return "" + START_CODE_STR + recordContentStr + checksumStr;
}
/**
* Converts and Extended Segment Linear Address record to an Extended Linear
* Address record.
*
* @throws {Error} When the record does not contain exactly 2 bytes.
* @throws {Error} When the Segmented Address is not a multiple of 0x1000.
*
* @param iHexRecord Intel hex record line without line terminator.
*/
function convertExtSegToLinAddressRecord(iHexRecord) {
var segmentAddress = getRecordData(iHexRecord);
if (segmentAddress.length !== 2 ||
segmentAddress[0] & 0xf || // Only process multiples of 0x1000
segmentAddress[1] !== 0) {
throw new Error("Invalid Extended Segment Address record " + iHexRecord);
}
var startAddress = segmentAddress[0] << 12;
return extLinAddressRecord(startAddress);
}
/**
* Separates an Intel Hex file (string) into an array of Record strings.
*
* @param iHexStr Intel Hex file as a string.
* @returns Array of Records in string format.
*/
function iHexToRecordStrs(iHexStr) {
// For some reason this is quicker than .split(/\r?\n/)
// Up to x200 faster in Chrome (!) and x1.5 faster in Firefox
var output = iHexStr.replace(/\r/g, '').split('\n');
// Boolean filter removes all falsy values as some of these files contain
// multiple empty lines we want to remove
return output.filter(Boolean);
}
/**
* Iterates through the beginning of an array of Intel Hex records to find the
* longest record data field length.
*
* Once it finds 12 records at the maximum size found so far (starts at 16
* bytes) it will stop iterating.
*
* This is useful to identify the expected max size of the data records for an
* Intel Hex, and then be able to generate new custom records of the same size.
*
* @param iHexRecords Array of Intel Hex Records.
* @returns Number of data bytes in a full record.
*/
function findDataFieldLength(iHexRecords) {
var maxDataBytes = 16;
var maxDataBytesCount = 0;
for (var _i = 0, iHexRecords_1 = iHexRecords; _i < iHexRecords_1.length; _i++) {
var record = iHexRecords_1[_i];
var dataBytesLength = (record.length - MIN_RECORD_STR_LEN) / 2;
if (dataBytesLength > maxDataBytes) {
maxDataBytes = dataBytesLength;
maxDataBytesCount = 0;
}
else if (dataBytesLength === maxDataBytes) {
maxDataBytesCount++;
}
if (maxDataBytesCount > 12) {
break;
}
}
if (maxDataBytes > RECORD_DATA_MAX_BYTES) {
throw new Error("Intel Hex record data size is too large: " + maxDataBytes);
}
return maxDataBytes;
}
export { MAX_RECORD_STR_LEN, RecordType, createRecord, getRecordType, getRecordData, parseRecord, endOfFileRecord, extLinAddressRecord, blockStartRecord, blockEndRecord, paddedDataRecord, convertRecordTo, convertExtSegToLinAddressRecord, iHexToRecordStrs, findDataFieldLength, };
//# sourceMappingURL=ihex.js.map