Skip to content
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

singleFileArchive: Add DataInputStream class to decode primitive types from a byte stream. #108

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions src/utils/datastream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/* eslint-disable max-classes-per-file */

/**
* Byte lengths for primitive types.
*/
const SHORT_BYTES = 2;
const INT_BYTES = 4;
const LONG_BYTES = 8;

/**
* EOF error thrown by DataInputStream.
*/
class DataInputStreamEOFError extends Error {
bufLen: number;

requiredLen: number;

/**
* @param bufLen The length of the buffer.
* @param requiredLen The required length of the buffer (i.e. what was needed for
* a successful read).
* @param message
*/
constructor (bufLen: number, requiredLen: number, message: string = "") {
let formattedMessage = `[bufLen=${bufLen}, requiredLen=${requiredLen}]`;
if ("" !== message) {
formattedMessage += ` ${message}`;
}
super(formattedMessage);
this.name = "DataInputStreamEOFError";
this.bufLen = bufLen;
this.requiredLen = requiredLen;
}
}

/**
* Decodes primitive types from a byte stream (similar to Java's DataInputStream class).
*/
class DataInputStream {
#dataView: DataView;

#isLittleEndian: boolean;

#byteIdx: number;

/**
* @param arrayBuffer Underlying array buffer.
* @param isLittleEndian Byte endianness.
*/
constructor (arrayBuffer: ArrayBufferLike, isLittleEndian: boolean = false) {
this.#dataView = new DataView(arrayBuffer);
this.#isLittleEndian = isLittleEndian;
this.#byteIdx = 0;
}

/**
* Seeks to the given index.
*
* @param idx
* @throws {Error} If encounter EOF while seeking.
*/
seek (idx: number): void {
if (idx > this.#dataView.byteLength) {
this.#byteIdx = this.#dataView.byteLength;
throw new DataInputStreamEOFError(this.#dataView.byteLength, idx);
Comment on lines +62 to +65
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix potential off-by-one error in the 'seek' method

The condition in the seek method should use >= instead of > to prevent seeking beyond the buffer length. Since indexing starts at zero, an index equal to the buffer length would be out of bounds and should trigger an EOF error.

Apply this diff to correct the condition:

- if (idx > this.#dataView.byteLength) {
+ if (idx >= this.#dataView.byteLength) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
seek (idx: number): void {
if (idx > this.#dataView.byteLength) {
this.#byteIdx = this.#dataView.byteLength;
throw new DataInputStreamEOFError(this.#dataView.byteLength, idx);
seek (idx: number): void {
if (idx >= this.#dataView.byteLength) {
this.#byteIdx = this.#dataView.byteLength;
throw new DataInputStreamEOFError(this.#dataView.byteLength, idx);

}
this.#byteIdx = idx;
}

/**
* Returns the current read offset in the stream.
*
* @return
*/
getPos (): number {
return this.#byteIdx;
}

/**
* Reads the specified amount of data.
*
* @param length
* @return
* @throws {Error} If encounter EOF while reading.
*/
readFully (length: number): Uint8Array {
const requiredLen: number = this.#byteIdx + length;
if (this.#dataView.byteLength < requiredLen) {
this.#byteIdx = this.#dataView.byteLength;
throw new DataInputStreamEOFError(this.#dataView.byteLength, requiredLen);
Comment on lines +87 to +90
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor repetitive EOF checks into a helper method

Multiple methods perform similar EOF checks and error handling. Refactoring this code into a private helper method would reduce duplication and enhance maintainability.

Consider adding a helper method:

private checkEOF(length: number): void {
  const requiredLen = this.#byteIdx + length;
  if (this.#dataView.byteLength < requiredLen) {
    this.#byteIdx = this.#dataView.byteLength;
    throw new DataInputStreamEOFError(this.#dataView.byteLength, requiredLen);
  }
}

Then update the read methods to use this helper. For example, in readUnsignedByte():

- const requiredLen: number = this.#byteIdx + 1;
- if (this.#dataView.byteLength < requiredLen) {
-   this.#byteIdx = this.#dataView.byteLength;
-   throw new DataInputStreamEOFError(this.#dataView.byteLength, requiredLen);
- }
+ this.checkEOF(1);

Also applies to: 106-110, 123-126, 139-142, 157-160, 175-178, 193-196, 211-214

}

const val: Uint8Array = new Uint8Array(this.#dataView.buffer, this.#byteIdx, length);
this.#byteIdx += length;

return val;
}

/**
* Reads an unsigned byte.
*
* @return
* @throws {Error} If encounter EOF while reading.
*/
readUnsignedByte (): number {
const requiredLen: number = this.#byteIdx + 1;
if (this.#dataView.byteLength < requiredLen) {
this.#byteIdx = this.#dataView.byteLength;
throw new DataInputStreamEOFError(this.#dataView.byteLength, requiredLen);
}

return this.#dataView.getUint8(this.#byteIdx++);
}

/**
* Reads a signed byte
*
* @return
* @throws {Error} If encounter EOF while reading.
*/
readSignedByte (): number {
const requiredLen: number = this.#byteIdx + 1;
if (this.#dataView.byteLength < requiredLen) {
this.#byteIdx = this.#dataView.byteLength;
throw new DataInputStreamEOFError(this.#dataView.byteLength, requiredLen);
}

return this.#dataView.getInt8(this.#byteIdx++);
}

/**
* Reads an unsigned short
*
* @return
* @throws {Error} If encounter EOF while reading.
*/
readUnsignedShort (): number {
const requiredLen: number = this.#byteIdx + SHORT_BYTES;
if (this.#dataView.byteLength < requiredLen) {
this.#byteIdx = this.#dataView.byteLength;
throw new DataInputStreamEOFError(this.#dataView.byteLength, requiredLen);
}
const val: number = this.#dataView.getUint16(this.#byteIdx, this.#isLittleEndian);
this.#byteIdx += SHORT_BYTES;

return val;
}

/**
* Reads a signed short
*
* @return
* @throws {Error} If encounter EOF while reading.
*/
readSignedShort (): number {
const requiredLen: number = this.#byteIdx + SHORT_BYTES;
if (this.#dataView.byteLength < requiredLen) {
this.#byteIdx = this.#dataView.byteLength;
throw new DataInputStreamEOFError(this.#dataView.byteLength, requiredLen);
}
const val: number = this.#dataView.getInt16(this.#byteIdx, this.#isLittleEndian);
this.#byteIdx += SHORT_BYTES;

return val;
}

/**
* Reads an int.
*
* @return
* @throws {Error} If encounter EOF while reading.
*/
readInt (): number {
const requiredLen: number = this.#byteIdx + INT_BYTES;
if (this.#dataView.byteLength < requiredLen) {
this.#byteIdx = this.#dataView.byteLength;
throw new DataInputStreamEOFError(this.#dataView.byteLength, requiredLen);
}
const val: number = this.#dataView.getInt32(this.#byteIdx, this.#isLittleEndian);
this.#byteIdx += INT_BYTES;

return val;
}

/**
* Reads a signed long int (64 bit).
*
* @return
* @throws {Error} If encounter EOF while reading.
*/
readSignedLong (): bigint {
const requiredLen = this.#byteIdx + LONG_BYTES;
if (this.#dataView.byteLength < requiredLen) {
this.#byteIdx = this.#dataView.byteLength;
throw new DataInputStreamEOFError(this.#dataView.byteLength, requiredLen);
}
const val: bigint = this.#dataView.getBigInt64(this.#byteIdx, this.#isLittleEndian);
this.#byteIdx += LONG_BYTES;

return val;
}

/**
* Reads an unsigned long int (64 bit).
*
* @return
* @throws {Error} If encounter EOF while reading.
*/
readUnsignedLong (): bigint {
const requiredLen = this.#byteIdx + LONG_BYTES;
if (this.#dataView.byteLength < requiredLen) {
this.#byteIdx = this.#dataView.byteLength;
throw new DataInputStreamEOFError(this.#dataView.byteLength, requiredLen);
}
const val: bigint = this.#dataView.getBigUint64(this.#byteIdx, this.#isLittleEndian);
this.#byteIdx += LONG_BYTES;

return val;
}
}

export {
DataInputStream,
DataInputStreamEOFError,
};
Loading