UNPKG

@microbit/microbit-fs

Version:

Manipulate files in a micro:bit MicroPython Intel Hex file.

485 lines 19.2 kB
/** * Filesystem management for MicroPython hex files. * * (c) 2019 Micro:bit Educational Foundation and the microbit-fs contributors. * SPDX-License-Identifier: MIT */ import * as microbitUh from '@microbit/microbit-universal-hex'; import { createMpFsBuilderCache, generateHexWithFiles, addIntelHexFiles, calculateFileSize, getIntelHexFiles, } from './micropython-fs-builder.js'; import { SimpleFile } from './simple-file.js'; import { areUint8ArraysEqual } from './common.js'; /** * The Board ID is used to identify the different targets from a Universal Hex. * In this case the target represents a micro:bit version. * For micro:bit V1 (v1.3, v1.3B and v1.5) the `boardId` is `0x9900`, and for * V2 `0x9903`. * This is being re-exported from the @microbit/microbit-universal-hex package. */ export var microbitBoardId = microbitUh.microbitBoardId; /** * Manage filesystem files in one or multiple MicroPython hex files. * * @public */ export class MicropythonFsHex { /** * File System manager constructor. * * At the moment it needs a MicroPython hex string without files included. * Multiple MicroPython images can be provided to generate a Universal Hex. * * @throws {Error} When any of the input iHex contains filesystem files. * @throws {Error} When any of the input iHex is not a valid MicroPython hex. * * @param intelHex - MicroPython Intel Hex string or an array of Intel Hex * strings with their respective board IDs. */ constructor(intelHex, { maxFsSize = 0 } = {}) { Object.defineProperty(this, "_uPyFsBuilderCache", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "_files", { enumerable: true, configurable: true, writable: true, value: {} }); Object.defineProperty(this, "_storageSize", { enumerable: true, configurable: true, writable: true, value: 0 }); const hexWithIdArray = Array.isArray(intelHex) ? intelHex : [ { hex: intelHex, boardId: 0x0000, }, ]; // Generate and store the MicroPython Builder caches let minFsSize = Infinity; hexWithIdArray.forEach((hexWithId) => { if (!hexWithId.hex) { throw new Error('Invalid MicroPython hex.'); } const builderCache = createMpFsBuilderCache(hexWithId.hex); const thisBuilderCache = { originalIntelHex: builderCache.originalIntelHex, originalMemMap: builderCache.originalMemMap, uPyEndAddress: builderCache.uPyEndAddress, uPyIntelHex: builderCache.uPyIntelHex, fsSize: builderCache.fsSize, boardId: hexWithId.boardId, }; this._uPyFsBuilderCache.push(thisBuilderCache); minFsSize = Math.min(minFsSize, thisBuilderCache.fsSize); }); this.setStorageSize(maxFsSize || minFsSize); // Check if there are files in any of the input hex this._uPyFsBuilderCache.forEach((builderCache) => { const hexFiles = getIntelHexFiles(builderCache.originalMemMap); if (Object.keys(hexFiles).length) { throw new Error('There are files in the MicropythonFsHex constructor hex file input.'); } }); } /** * Create a new file and add it to the file system. * * @throws {Error} When the file already exists. * @throws {Error} When an invalid filename is provided. * @throws {Error} When invalid file data is provided. * * @param filename - Name for the file. * @param content - File content to write. */ create(filename, content) { if (this.exists(filename)) { throw new Error('File already exists.'); } this.write(filename, content); } /** * Write a file into the file system. Overwrites a previous file with the * same name. * * @throws {Error} When an invalid filename is provided. * @throws {Error} When invalid file data is provided. * * @param filename - Name for the file. * @param content - File content to write. */ write(filename, content) { this._files[filename] = new SimpleFile(filename, content); } // eslint-disable-next-line @typescript-eslint/no-unused-vars append(filename, content) { if (!filename) { throw new Error('Invalid filename.'); } if (!this.exists(filename)) { throw new Error(`File "${filename}" does not exist.`); } // TODO: Implement this. throw new Error('Append operation not yet implemented.'); } /** * Read the text from a file. * * @throws {Error} When invalid file name is provided. * @throws {Error} When file is not in the file system. * * @param filename - Name of the file to read. * @returns Text from the file. */ read(filename) { if (!filename) { throw new Error('Invalid filename.'); } if (!this.exists(filename)) { throw new Error(`File "${filename}" does not exist.`); } return this._files[filename].getText(); } /** * Read the bytes from a file. * * @throws {Error} When invalid file name is provided. * @throws {Error} When file is not in the file system. * * @param filename - Name of the file to read. * @returns Byte array from the file. */ readBytes(filename) { if (!filename) { throw new Error('Invalid filename.'); } if (!this.exists(filename)) { throw new Error(`File "${filename}" does not exist.`); } return this._files[filename].getBytes(); } /** * Delete a file from the file system. * * @throws {Error} When invalid file name is provided. * @throws {Error} When the file doesn't exist. * * @param filename - Name of the file to delete. */ remove(filename) { if (!filename) { throw new Error('Invalid filename.'); } if (!this.exists(filename)) { throw new Error(`File "${filename}" does not exist.`); } delete this._files[filename]; } /** * Check if a file is already present in the file system. * * @param filename - Name for the file to check. * @returns True if it exists, false otherwise. */ exists(filename) { return this._files.hasOwnProperty(filename); } /** * Returns the size of a file in bytes. * * @throws {Error} When invalid file name is provided. * @throws {Error} When the file doesn't exist. * * @param filename - Name for the file to check. * @returns Size file size in bytes. */ size(filename) { if (!filename) { throw new Error(`Invalid filename: ${filename}`); } if (!this.exists(filename)) { throw new Error(`File "${filename}" does not exist.`); } return calculateFileSize(this._files[filename].filename, this._files[filename].getBytes()); } /** * @returns A list all the files in the file system. */ ls() { const files = []; Object.values(this._files).forEach((value) => files.push(value.filename)); return files; } /** * Sets a storage size limit. Must be smaller than available space in * MicroPython. * * @param {number} size - Size in bytes for the filesystem. */ setStorageSize(size) { let minFsSize = Infinity; this._uPyFsBuilderCache.forEach((builderCache) => { minFsSize = Math.min(minFsSize, builderCache.fsSize); }); if (size > minFsSize) { throw new Error('Storage size limit provided is larger than size available in the MicroPython hex.'); } this._storageSize = size; } /** * The available filesystem total size either calculated by the MicroPython * hex or the max storage size limit has been set. * * @returns Size of the filesystem in bytes. */ getStorageSize() { return this._storageSize; } /** * @returns The total number of bytes currently used by files in the file system. */ getStorageUsed() { return Object.values(this._files).reduce((accumulator, current) => accumulator + this.size(current.filename), 0); } /** * @returns The remaining storage of the file system in bytes. */ getStorageRemaining() { return this.getStorageSize() - this.getStorageUsed(); } /** * Read the files included in a MicroPython hex string and add them to this * instance. * * @throws {Error} When there are no files to import in the hex. * @throws {Error} When there is a problem reading the files from the hex. * @throws {Error} When a filename already exists in this instance (all other * files are still imported). * * @param intelHex - MicroPython hex string with files. * @param overwrite - Flag to overwrite existing files in this instance. * @param formatFirst - Erase all the previous files before importing. It only * erases the files after there are no error during hex file parsing. * @returns A filename list of added files. */ importFilesFromIntelHex(intelHex, { overwrite = false, formatFirst = false } = {}) { const files = getIntelHexFiles(intelHex); if (!Object.keys(files).length) { throw new Error('Intel Hex does not have any files to import'); } if (formatFirst) { this._files = {}; } const existingFiles = []; Object.keys(files).forEach((filename) => { if (!overwrite && this.exists(filename)) { existingFiles.push(filename); } else { this.write(filename, files[filename]); } }); // Only throw the error at the end so that all other files are imported if (existingFiles.length) { throw new Error(`Files "${existingFiles}" from hex already exists.`); } return Object.keys(files); } /** * Read the files included in a MicroPython Universal Hex string and add them * to this instance. * * @throws {Error} When there are no files to import from one of the hex. * @throws {Error} When the files in the individual hex are different. * @throws {Error} When there is a problem reading files from one of the hex. * @throws {Error} When a filename already exists in this instance (all other * files are still imported). * * @param universalHex - MicroPython Universal Hex string with files. * @param overwrite - Flag to overwrite existing files in this instance. * @param formatFirst - Erase all the previous files before importing. It only * erases the files after there are no error during hex file parsing. * @returns A filename list of added files. */ importFilesFromUniversalHex(universalHex, { overwrite = false, formatFirst = false } = {}) { if (!microbitUh.isUniversalHex(universalHex)) { throw new Error('Universal Hex provided is invalid.'); } const hexWithIds = microbitUh.separateUniversalHex(universalHex); const allFileGroups = []; hexWithIds.forEach((hexWithId) => { const fileGroup = getIntelHexFiles(hexWithId.hex); if (!Object.keys(fileGroup).length) { throw new Error(`Hex with ID ${hexWithId.boardId} from Universal Hex does not have any files to import`); } allFileGroups.push(fileGroup); }); // Ensure all hexes have the same files allFileGroups.forEach((fileGroup) => { // Create new array without this current group const compareFileGroups = allFileGroups.filter((v) => v !== fileGroup); // Check that all files in this group are in all the others for (const [fileName, fileContent] of Object.entries(fileGroup)) { compareFileGroups.forEach((compareGroup) => { if (!compareGroup.hasOwnProperty(fileName) || !areUint8ArraysEqual(compareGroup[fileName], fileContent)) { throw new Error('Mismatch in the different Hexes inside the Universal Hex'); } }); } }); // If we reached this point all file groups are the same and we can use any const files = allFileGroups[0]; if (formatFirst) { this._files = {}; } const existingFiles = []; Object.keys(files).forEach((filename) => { if (!overwrite && this.exists(filename)) { existingFiles.push(filename); } else { this.write(filename, files[filename]); } }); // Only throw the error at the end so that all other files are imported if (existingFiles.length) { throw new Error(`Files "${existingFiles}" from hex already exists.`); } return Object.keys(files); } /** * Read the files included in a MicroPython Universal or Intel Hex string and * add them to this instance. * * @throws {Error} When there are no files to import from the hex. * @throws {Error} When in the Universal Hex the files of the individual hexes * are different. * @throws {Error} When there is a problem reading files from one of the hex. * @throws {Error} When a filename already exists in this instance (all other * files are still imported). * * @param hexStr - MicroPython Intel or Universal Hex string with files. * @param overwrite - Flag to overwrite existing files in this instance. * @param formatFirst - Erase all the previous files before importing. It only * erases the files after there are no error during hex file parsing. * @returns A filename list of added files. */ importFilesFromHex(hexStr, options = {}) { return microbitUh.isUniversalHex(hexStr) ? this.importFilesFromUniversalHex(hexStr, options) : this.importFilesFromIntelHex(hexStr, options); } /** * Generate a new copy of the MicroPython Intel Hex with the files in the * filesystem included. * * @throws {Error} When a file doesn't have any data. * @throws {Error} When there are issues calculating file system boundaries. * @throws {Error} When there is no space left for a file. * @throws {Error} When the board ID is not found. * @throws {Error} When there are multiple MicroPython hexes and board ID is * not provided. * * @param boardId - When multiple MicroPython hex files are provided select * one via this argument. * * @returns A new string with MicroPython and the filesystem included. */ getIntelHex(boardId) { if (this.getStorageRemaining() < 0) { throw new Error('There is no storage space left.'); } const files = {}; Object.values(this._files).forEach((file) => { files[file.filename] = file.getBytes(); }); if (boardId === undefined) { if (this._uPyFsBuilderCache.length === 1) { return generateHexWithFiles(this._uPyFsBuilderCache[0], files); } else { throw new Error('The Board ID must be specified if there are multiple MicroPythons.'); } } for (const builderCache of this._uPyFsBuilderCache) { if (builderCache.boardId === boardId) { return generateHexWithFiles(builderCache, files); } } // If we reach this point we could not find the board ID throw new Error('Board ID requested not found.'); } /** * Generate a byte array of the MicroPython and filesystem data. * * @throws {Error} When a file doesn't have any data. * @throws {Error} When there are issues calculating file system boundaries. * @throws {Error} When there is no space left for a file. * @throws {Error} When the board ID is not found. * @throws {Error} When there are multiple MicroPython hexes and board ID is * not provided. * * @param boardId - When multiple MicroPython hex files are provided select * one via this argument. * * @returns A Uint8Array with MicroPython and the filesystem included. */ getIntelHexBytes(boardId) { if (this.getStorageRemaining() < 0) { throw new Error('There is no storage space left.'); } const files = {}; Object.values(this._files).forEach((file) => { files[file.filename] = file.getBytes(); }); if (boardId === undefined) { if (this._uPyFsBuilderCache.length === 1) { return addIntelHexFiles(this._uPyFsBuilderCache[0].originalMemMap, files, true); } else { throw new Error('The Board ID must be specified if there are multiple MicroPythons.'); } } for (const builderCache of this._uPyFsBuilderCache) { if (builderCache.boardId === boardId) { return addIntelHexFiles(builderCache.originalMemMap, files, true); } } // If we reach this point we could not find the board ID throw new Error('Board ID requested not found.'); } /** * Generate a new copy of a MicroPython Universal Hex with the files in the * filesystem included. * * @throws {Error} When a file doesn't have any data. * @throws {Error} When there are issues calculating file system boundaries. * @throws {Error} When there is no space left for a file. * @throws {Error} When this method is called without having multiple * MicroPython hexes. * * @returns A new Universal Hex string with MicroPython and filesystem. */ getUniversalHex() { if (this._uPyFsBuilderCache.length === 1) { throw new Error('MicropythonFsHex constructor must have more than one MicroPython ' + 'Intel Hex to generate a Universal Hex.'); } const iHexWithIds = []; this._uPyFsBuilderCache.forEach((builderCache) => { iHexWithIds.push({ hex: this.getIntelHex(builderCache.boardId), boardId: builderCache.boardId, }); }); return microbitUh.createUniversalHex(iHexWithIds); } } //# sourceMappingURL=micropython-fs-hex.js.map