import { EXIF_CODES, ExifError } from "../../errors/exif-error.js"; import type { ByteOrder, Rational } from "../../types.js"; /** * Low-level binary reader for TIFF/EXIF byte streams. * * Supports both big-endian and little-endian byte orders with runtime switching. * All read operations are bounds-checked and throw ExifError on overflow. */ export class ByteReader { protected byteOrder: ByteOrder; protected view: DataView; protected data: Uint8Array; constructor(data: Uint8Array, byteOrder: ByteOrder = "BE") { this.data = data; this.byteOrder = byteOrder; this.view = new DataView(data.buffer, data.byteOffset, data.byteLength); } setByteOrder(order: ByteOrder): void { this.byteOrder = order; } get length(): number { return this.data.byteLength; } protected assertBounds(offset: number, size: number): void { if (offset < 0 || offset + size > this.data.byteLength) { throw new ExifError( `Read out of bounds: offset=${offset}, size=${size}, length=${this.data.byteLength}`, EXIF_CODES.PARSE_ERROR ); } } readUint8(offset: number): number { this.assertBounds(offset, 1); return this.view.getUint8(offset); } readUint16(offset: number): number { this.assertBounds(offset, 2); return this.view.getUint16(offset, this.byteOrder === "LE"); } readUint32(offset: number): number { this.assertBounds(offset, 4); return this.view.getUint32(offset, this.byteOrder === "LE"); } readInt16(offset: number): number { this.assertBounds(offset, 2); return this.view.getInt16(offset, this.byteOrder === "LE"); } readInt32(offset: number): number { this.assertBounds(offset, 4); return this.view.getInt32(offset, this.byteOrder === "LE"); } readFloat32(offset: number): number { this.assertBounds(offset, 4); return this.view.getFloat32(offset, this.byteOrder === "LE"); } readFloat64(offset: number): number { this.assertBounds(offset, 8); return this.view.getFloat64(offset, this.byteOrder === "LE"); } readBytes(offset: number, length: number): Uint8Array { this.assertBounds(offset, length); return this.data.slice(offset, offset + length); } readString(offset: number, length: number): string { this.assertBounds(offset, length); const bytes = this.data.slice(offset, offset + length); let str = ""; for (let i = 0; i < bytes.length; i++) { const b = bytes[i] as number; if (b === 0) break; // null-terminate on first null str += String.fromCharCode(b); } return str; } readNullTermString(offset: number): string { let end = offset; while (end < this.data.byteLength && this.data[end] !== 0) { end++; } return this.readString(offset, end - offset); } readRational(offset: number): Rational { this.assertBounds(offset, 8); const numerator = this.readUint32(offset); const denominator = this.readUint32(offset + 4); return { numerator, denominator }; } readSRational(offset: number): Rational { this.assertBounds(offset, 8); const numerator = this.readInt32(offset); const denominator = this.readInt32(offset + 4); return { numerator, denominator }; } slice(start: number, end: number): ByteReader { const sliced = this.data.slice(start, end); return new ByteReader(sliced, this.byteOrder); } }