diff --git a/bun.lockb b/bun.lockb index da4ddfd..6762f2a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 405f8cb..d55b653 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,8 @@ "@trivago/prettier-plugin-sort-imports": "^4.3.0", "@types/jest": "29.5.12", "@types/node": "^20.14.2", - "@typescript-eslint/eslint-plugin": "^7.12.0", - "@typescript-eslint/parser": "^7.12.0", + "@typescript-eslint/eslint-plugin": "^7.13.0", + "@typescript-eslint/parser": "^7.13.0", "eslint": "^9.4.0", "eslint-config-prettier": "^9.1.0", "eslint-config-standard-with-typescript": "^39.0.0", @@ -52,7 +52,7 @@ "eslint-plugin-promise": "^6.2.0", "eslint-plugin-tsdoc": "^0.3.0", "eslint-plugin-unused-imports": "^4.0.0", - "prettier": "^3.3.1", + "prettier": "^3.3.2", "ts-jest": "^29.1.4", "ts-node": "^10.9.2", "typescript": "^5.4.5" diff --git a/src/byteStream.test.ts b/src/byteStream.test.ts new file mode 100644 index 0000000..63b2ae8 --- /dev/null +++ b/src/byteStream.test.ts @@ -0,0 +1,113 @@ +/****************************************************************************** + * (c) 2018 - 2024 Zondax AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ +import { Buffer } from 'buffer' + +import { ByteStream } from './byteStream' +import { LedgerError } from './consts' +import { ResponseError } from './responseError' + +describe('ByteStream', () => { + let byteStream: ByteStream + + beforeEach(() => { + byteStream = new ByteStream(Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05])) + }) + + test('getCompleteBuffer should return a complete buffer', () => { + expect(byteStream.getCompleteBuffer()).toEqual(Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05])) + }) + + test('getAvailableBuffer should return the available buffer after some bytes are read', () => { + byteStream.readBytes(3) + expect(byteStream.getAvailableBuffer()).toEqual(Buffer.from([0x04, 0x05])) + }) + + test('readBytes should return the correct bytes and increase offset', () => { + const readBuffer = byteStream.readBytes(2) + expect(readBuffer).toEqual(Buffer.from([0x01, 0x02])) + expect(byteStream.readBytes(1)).toEqual(Buffer.from([0x03])) + }) + + test('skipBytes should increase the offset correctly', () => { + byteStream.skipBytes(2) + expect(byteStream.readBytes(1)).toEqual(Buffer.from([0x03])) + }) + + test('resetOffset should reset the offset to zero', () => { + byteStream.readBytes(3) + byteStream.resetOffset() + expect(byteStream.readBytes(2)).toEqual(Buffer.from([0x01, 0x02])) + }) + + test('readBytes should throw an error when reading beyond the buffer length', () => { + expect(() => byteStream.readBytes(10)).toThrow(new ResponseError(LedgerError.UnknownError, 'Attempt to read beyond buffer length')) + }) + + test('skipBytes should throw an error when skipping beyond the buffer length', () => { + expect(() => byteStream.skipBytes(10)).toThrow(new ResponseError(LedgerError.UnknownError, 'Attempt to skip beyond buffer length')) + }) + + test('appendUint8 should correctly append a byte to the buffer', () => { + byteStream.appendUint8(0x06) + expect(byteStream.getCompleteBuffer()).toEqual(Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x06])) + }) + + test('appendUint16 should correctly append a two-byte integer to the buffer', () => { + byteStream.appendUint16(0x0708) + expect(byteStream.getCompleteBuffer()).toEqual(Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x08, 0x07])) + }) + + test('appendUint32 should correctly append a four-byte integer to the buffer', () => { + byteStream.appendUint32(0x090a0b0c) + expect(byteStream.getCompleteBuffer()).toEqual(Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x0c, 0x0b, 0x0a, 0x09])) + }) + + test('appendUint64 should correctly append an eight-byte integer to the buffer', () => { + byteStream = new ByteStream() + byteStream.appendUint64(BigInt('0x0102030405060708')) + expect(byteStream.readBytes(8)).toEqual(Buffer.from([8, 7, 6, 5, 4, 3, 2, 1])) + }) + + test('readBytesAt should return the correct bytes from a given offset', () => { + const readBuffer = byteStream.readBytesAt(2, 1) + expect(readBuffer).toEqual(Buffer.from([0x02, 0x03])) + }) + + test('readBytesAt should throw an error when reading beyond the buffer length', () => { + expect(() => byteStream.readBytesAt(10, 1)).toThrow(new ResponseError(LedgerError.UnknownError, 'Attempt to read beyond buffer length')) + }) + + test('insertBytesAt should correctly insert bytes at a given offset', () => { + byteStream.insertBytesAt(Buffer.from([0x06, 0x07]), 2) + expect(byteStream.getCompleteBuffer()).toEqual(Buffer.from([0x01, 0x02, 0x06, 0x07, 0x03, 0x04, 0x05])) + }) + + test('insertBytesAt should expand the buffer if necessary', () => { + byteStream.insertBytesAt(Buffer.from([0x08, 0x09]), 10) + expect(byteStream.getCompleteBuffer()).toEqual(Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x09])) + }) + + test('writeBytesAt should correctly write bytes at a given offset and advance the write offset', () => { + byteStream.writeBytesAt(Buffer.from([0x0a, 0x0b]), 1) + expect(byteStream.getCompleteBuffer()).toEqual(Buffer.from([0x01, 0x0a, 0x0b, 0x04, 0x05])) + expect(byteStream.readBytes(5)).toEqual(Buffer.from([0x01, 0x0a, 0x0b, 0x04, 0x05])) + }) + + test('writeBytesAt should expand the buffer if necessary', () => { + byteStream.writeBytesAt(Buffer.from([0x0c, 0x0d]), 10) + expect(byteStream.getCompleteBuffer()).toEqual(Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0d])) + }) +}) diff --git a/src/byteStream.ts b/src/byteStream.ts new file mode 100644 index 0000000..649d9a2 --- /dev/null +++ b/src/byteStream.ts @@ -0,0 +1,246 @@ +/****************************************************************************** + * (c) 2018 - 2024 Zondax AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ +import { LedgerError } from './consts' +import { ResponseError } from './responseError' + +/** + * Class representing a byte stream for reading and writing data. + */ +export class ByteStream { + private readOffset = 0 + private writeOffset = 0 + protected internalBuffer: Buffer + + constructor(buffer?: Buffer) { + this.internalBuffer = buffer ? Buffer.from(buffer) : Buffer.alloc(0) + this.readOffset = 0 + this.writeOffset = this.internalBuffer.length + } + + /** + * Writes a single byte (Uint8) to the buffer at the current write offset, then advances the write offset. + * If the write offset is at the buffer's end, the buffer is expanded. + * @param value The byte to write. + */ + appendUint8(value: number) { + const byteBuffer = Buffer.from([value]) + this.appendBytes(byteBuffer) + } + + /** + * Writes a two-byte unsigned integer (Uint16) to the buffer at the current write offset in little-endian format, then advances the write offset. + * If the write offset is at the buffer's end, the buffer is expanded. + * @param value The two-byte unsigned integer to write. + */ + appendUint16(value: number) { + const byteBuffer = Buffer.alloc(2) + byteBuffer.writeUInt16LE(value, 0) + this.appendBytes(byteBuffer) + } + + /** + * Writes a four-byte unsigned integer (Uint32) to the buffer at the current write offset in little-endian format, then advances the write offset. + * If the write offset is at the buffer's end, the buffer is expanded. + * @param value The four-byte unsigned integer to write. + */ + appendUint32(value: number) { + const byteBuffer = Buffer.alloc(4) + byteBuffer.writeUInt32LE(value, 0) + this.appendBytes(byteBuffer) + } + + /** + * Writes an eight-byte unsigned integer (Uint64) to the buffer at the current write offset in little-endian format, then advances the write offset. + * If the write offset is at the buffer's end, the buffer is expanded. + * @param value The eight-byte unsigned integer to write. + */ + appendUint64(value: bigint) { + const byteBuffer = Buffer.alloc(8) + byteBuffer.writeBigUInt64LE(value, 0) + this.appendBytes(byteBuffer) + } + + /** + * Reads a specified number of bytes from the current read offset, then advances the read offset. + * @param length The number of bytes to read. + * @returns A buffer containing the read bytes. + * @throws Error if attempting to read beyond the buffer length. + */ + readBytes(length: number): Buffer { + if (this.readOffset + length > this.internalBuffer.length) { + throw new ResponseError(LedgerError.UnknownError, 'Attempt to read beyond buffer length') + } + const response = this.internalBuffer.subarray(this.readOffset, this.readOffset + length) + this.readOffset += length + return response + } + + /** + * Reads a specified number of bytes from a given offset without changing the current read offset. + * @param length The number of bytes to read. + * @param offset The offset from which to read the bytes. + * @returns A buffer containing the read bytes. + * @throws Error if attempting to read beyond the buffer length. + */ + readBytesAt(length: number, offset: number): Buffer { + if (offset + length > this.internalBuffer.length) { + throw new ResponseError(LedgerError.UnknownError, 'Attempt to read beyond buffer length') + } + return this.internalBuffer.subarray(offset, offset + length) + } + + /** + * Writes data to the buffer at the current write offset, then advances the write offset. + * If the data exceeds the buffer length, the buffer is expanded. + * @param data The data to write. + */ + appendBytes(data: Buffer) { + if (this.writeOffset + data.length > this.internalBuffer.length) { + const newBuffer = Buffer.alloc(this.writeOffset + data.length) + this.internalBuffer.copy(newBuffer, 0, 0, this.writeOffset) + this.internalBuffer = newBuffer + } + data.copy(this.internalBuffer, this.writeOffset) + this.writeOffset += data.length + } + + /** + * Inserts data into the buffer at the specified offset without changing the current write offset. + * Expands the buffer if necessary. + * @param data The data to insert. + * @param offset The offset at which to insert the data. + */ + insertBytesAt(data: Buffer, offset: number) { + if (offset > this.internalBuffer.length) { + const padding = Buffer.alloc(offset - this.internalBuffer.length, 0) + this.internalBuffer = Buffer.concat([this.internalBuffer, padding, data]) + } else { + const before = this.internalBuffer.subarray(0, offset) + const after = this.internalBuffer.subarray(offset) + this.internalBuffer = Buffer.concat([before, data, after]) + } + } + + /** + * Writes data to the buffer at the specified offset and advances the write offset from that point. + * Expands the buffer if the data exceeds the buffer length. + * @param data The data to write. + * @param offset The offset at which to write the data. + */ + writeBytesAt(data: Buffer, offset: number) { + if (offset + data.length > this.internalBuffer.length) { + const newBuffer = Buffer.alloc(offset + data.length) + this.internalBuffer.copy(newBuffer, 0, 0, offset) + this.internalBuffer = newBuffer + } + data.copy(this.internalBuffer, offset) + this.writeOffset = offset + data.length + } + + /** + * Advances the current read offset by a specified number of bytes. + * @param length The number of bytes to skip. + * @throws Error if attempting to skip beyond the buffer length. + */ + skipBytes(length: number) { + if (this.readOffset + length > this.internalBuffer.length) { + throw new ResponseError(LedgerError.UnknownError, 'Attempt to skip beyond buffer length') + } + this.readOffset += length + } + + clear() { + this.internalBuffer = Buffer.alloc(0) + this.readOffset = 0 + this.writeOffset = 0 + } + + /** + * Resets the current read and write offsets to zero. + */ + resetOffset() { + this.readOffset = 0 + this.writeOffset = 0 + } + + /** + * Returns a new buffer containing all bytes of the internal buffer. + */ + getCompleteBuffer(): Buffer { + return Buffer.from(this.internalBuffer) + } + + /** + * Returns a new buffer containing the bytes from the current read offset to the end of the internal buffer. + */ + getAvailableBuffer(): Buffer { + return Buffer.from(this.internalBuffer.subarray(this.readOffset)) + } + + /** + * Returns the remaining length of the buffer from the current read offset. + * @returns The remaining length of the buffer. + */ + length(): number { + return this.internalBuffer.length - this.readOffset + } + + /** + * Returns the total capacity of the internal buffer, irrespective of the current read or write offset. + * @returns The total length of the internal buffer. + */ + capacity(): number { + return this.internalBuffer.length + } + + /** + * Returns the current read offset. + * @returns The current read offset. + */ + getReadOffset(): number { + return this.readOffset + } + + /** + * Returns the current write offset. + * @returns The current write offset. + */ + getWriteOffset(): number { + return this.writeOffset + } + + /** + * Sets the read offset to a specified value. + * @param offset The new read offset. + */ + setReadOffset(offset: number) { + if (offset < 0 || offset > this.internalBuffer.length) { + throw new ResponseError(LedgerError.UnknownError, 'Invalid read offset') + } + this.readOffset = offset + } + + /** + * Sets the write offset to a specified value. + * @param offset The new write offset. + */ + setWriteOffset(offset: number) { + if (offset < 0 || offset > this.internalBuffer.length) { + throw new ResponseError(LedgerError.UnknownError, 'Invalid write offset') + } + this.writeOffset = offset + } +} diff --git a/src/common.test.ts b/src/common.test.ts index 4ba2b59..450469e 100644 --- a/src/common.test.ts +++ b/src/common.test.ts @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. *****************************************************************************/ -import { errorCodeToString, processErrorResponse } from './common' +import { processErrorResponse } from './common' import { LedgerError } from './consts' +import { errorCodeToString } from './errors' import { ResponseError } from './responseError' describe('errorCodeToString', () => { diff --git a/src/common.ts b/src/common.ts index 5aa2afd..2a030df 100644 --- a/src/common.ts +++ b/src/common.ts @@ -13,27 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. *****************************************************************************/ -import { ERROR_DESCRIPTION_OVERRIDE, LedgerError } from './consts' +import { LedgerError } from './consts' +import { errorCodeToString } from './errors' import { ResponsePayload } from './payload' import { ResponseError } from './responseError' -/** - * Converts a Ledger error code to a human-readable string. - * - * @param returnCode - The Ledger error code to convert. - * @returns A string describing the error code. - */ -export function errorCodeToString(returnCode: LedgerError): string { - const returnCodeStr = returnCode.toString(16).toUpperCase() - let errDescription = `Unknown Return Code: 0x${returnCodeStr}` - - if (returnCode in ERROR_DESCRIPTION_OVERRIDE) { - errDescription = ERROR_DESCRIPTION_OVERRIDE[returnCode] - } - - return errDescription -} - /** * Checks if a value is a dictionary (i.e., a plain object). * diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..6511c8e --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,33 @@ +/****************************************************************************** + * (c) 2018 - 2024 Zondax AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ +import { ERROR_DESCRIPTION_OVERRIDE, LedgerError } from './consts' + +/** + * Converts a Ledger error code to a human-readable string. + * + * @param returnCode - The Ledger error code to convert. + * @returns A string describing the error code. + */ +export function errorCodeToString(returnCode: LedgerError): string { + const returnCodeStr = returnCode.toString(16).toUpperCase() + let errDescription = `Unknown Return Code: 0x${returnCodeStr}` + + if (returnCode in ERROR_DESCRIPTION_OVERRIDE) { + errDescription = ERROR_DESCRIPTION_OVERRIDE[returnCode] + } + + return errDescription +} diff --git a/src/payload.ts b/src/payload.ts index faf8d76..6914136 100644 --- a/src/payload.ts +++ b/src/payload.ts @@ -13,67 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. *****************************************************************************/ -import { LedgerError } from './consts' -import { ResponseError } from './responseError' - -export class ResponsePayload { - private offset = 0 - private readonly internalBuffer: Buffer +import { ByteStream } from './byteStream' +/** + * Class representing a payload response for reading data. + */ +export class ResponsePayload extends ByteStream { constructor(payload: Buffer) { - this.internalBuffer = payload - this.offset = 0 - } - - /** - * Returns a new buffer containing all bytes of the original payload. - */ - getCompleteBuffer(): Buffer { - return Buffer.from(this.internalBuffer) - } - - /** - * Returns a new buffer containing the bytes from the current offset to the end of the payload. - */ - getAvailableBuffer(): Buffer { - return Buffer.from(this.internalBuffer.subarray(this.offset)) - } - - length(): number { - return this.internalBuffer.length - this.offset - } - - /** - * Reads a specified number of bytes from the current offset, then advances the offset. - * @param length The number of bytes to read. - * @returns A buffer containing the read bytes. - * @throws Error if attempting to read beyond the buffer length. - */ - readBytes(length: number): Buffer { - if (this.offset + length > this.internalBuffer.length) { - throw new ResponseError(LedgerError.UnknownError, 'Attempt to read beyond buffer length') - } - const response = this.internalBuffer.subarray(this.offset, this.offset + length) - this.skipBytes(length) - return response - } - - /** - * Advances the current offset by a specified number of bytes. - * @param length The number of bytes to skip. - * @throws Error if attempting to skip beyond the buffer length. - */ - skipBytes(length: number) { - if (this.offset + length > this.internalBuffer.length) { - throw new ResponseError(LedgerError.UnknownError, 'Attempt to skip beyond buffer length') - } - this.offset += length - } - - /** - * Resets the current offset to zero. - */ - resetOffset() { - this.offset = 0 + super(payload) } } diff --git a/src/responseError.ts b/src/responseError.ts index a43b64d..8c0819d 100644 --- a/src/responseError.ts +++ b/src/responseError.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. *****************************************************************************/ -import { errorCodeToString } from './common' +import { errorCodeToString } from './errors' /** * Class representing a response error.