chromium/ui/file_manager/file_manager/foreground/js/metadata/byte_reader.ts

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

export class ByteReader {
  private readonly view_: DataView;
  private pos_ = 0;
  private readonly seekStack_: number[] = [];
  private littleEndian_ = false;

  /**
   * @param arrayBuffer An array of buffers to be read from.
   * @param offset Offset to read bytes at.
   * @param length Number of bytes to read.
   */
  constructor(arrayBuffer: ArrayBuffer, offset = 0, length?: number) {
    length = length || (arrayBuffer.byteLength - offset);
    this.view_ = new DataView(arrayBuffer, offset, length);
  }

  /**
   * Throw an error if (0 > pos >= end) or if (pos + size > end).
   *
   * Static utility function.
   *
   * @param pos Position in the file.
   * @param size Number of bytes to read.
   * @param end Maximum position to read from.
   */
  static validateRead(pos: number, size: number, end: number) {
    if (pos < 0 || pos >= end) {
      throw new Error('Invalid read position');
    }

    if (pos + size > end) {
      throw new Error('Read past end of buffer');
    }
  }

  /**
   * Read as a sequence of characters, returning them as a single string.
   *
   * This is a static utility function.  There is a member function with the
   * same name which side-effects the current read position.
   *
   * @param dataView Data view instance.
   * @param pos Position in bytes to read from.
   * @param size Number of bytes to read.
   * @param end Maximum position to read from.
   * @return Read string.
   */
  static readString(
      dataView: DataView, pos: number, size: number, end?: number): string {
    ByteReader.validateRead(pos, size, end || dataView.byteLength);

    const codes = [];

    for (let i = 0; i < size; ++i) {
      codes.push(dataView.getUint8(pos + i));
    }

    return String.fromCharCode.apply(null, codes);
  }

  /**
   * Read as a sequence of characters, returning them as a single string.
   *
   * This is a static utility function.  There is a member function with the
   * same name which side-effects the current read position.
   *
   * @param dataView Data view instance.
   * @param pos Position in bytes to read from.
   * @param size Number of bytes to read.
   * @param end Maximum position to read from.
   * @return Read string.
   */
  static readNullTerminatedString(
      dataView: DataView, pos: number, size: number, end?: number): string {
    ByteReader.validateRead(pos, size, end || dataView.byteLength);

    const codes = [];

    for (let i = 0; i < size; ++i) {
      const code = dataView.getUint8(pos + i);
      if (code === 0) {
        break;
      }
      codes.push(code);
    }

    return String.fromCharCode.apply(null, codes);
  }

  /**
   * Read as a sequence of UTF16 characters, returning them as a single string.
   *
   * This is a static utility function.  There is a member function with the
   * same name which side-effects the current read position.
   *
   * @param dataView Data view instance.
   * @param pos Position in bytes to read from.
   * @param bom True if BOM should be parsed.
   * @param size Number of bytes to read.
   * @param end Maximum position to read from.
   * @return Read string.
   */
  static readNullTerminatedStringUtf16(
      dataView: DataView, pos: number, bom: boolean, size: number,
      end?: number): string {
    ByteReader.validateRead(pos, size, end || dataView.byteLength);

    let littleEndian = false;
    let start = 0;

    if (bom) {
      littleEndian = (dataView.getUint8(pos) === 0xFF);
      start = 2;
    }

    const codes = [];

    for (let i = start; i < size; i += 2) {
      const code = dataView.getUint16(pos + i, littleEndian);
      if (code === 0) {
        break;
      }
      codes.push(code);
    }

    return String.fromCharCode.apply(null, codes);
  }

  /**
   * Read as a sequence of bytes, returning them as a single base64 encoded
   * string.
   *
   * This is a static utility function.  There is a member function with the
   * same name which side-effects the current read position.
   *
   * @param dataView Data view instance.
   * @param pos Position in bytes to read from.
   * @param size Number of bytes to read.
   * @param end Maximum position to read from.
   * @return Base 64 encoded value.
   */
  static readBase64(
      dataView: DataView, pos: number, size: number, end?: number): string {
    ByteReader.validateRead(pos, size, end || dataView.byteLength);

    const rv: string[] = [];
    const chars: string[] = [];
    let padding = 0;

    for (let i = 0; i < size; /* incremented inside */) {
      let bits = dataView.getUint8(pos + (i++)) << 16;

      if (i < size) {
        bits |= dataView.getUint8(pos + (i++)) << 8;

        if (i < size) {
          bits |= dataView.getUint8(pos + (i++));
        } else {
          padding = 1;
        }
      } else {
        padding = 2;
      }

      chars[3] = BASE64_ALPHABET[bits & 63]!;
      chars[2] = BASE64_ALPHABET[(bits >> 6) & 63]!;
      chars[1] = BASE64_ALPHABET[(bits >> 12) & 63]!;
      chars[0] = BASE64_ALPHABET[(bits >> 18) & 63]!;

      rv.push.apply(rv, chars);
    }

    if (padding > 0) {
      rv[rv.length - 1] = '=';
    }
    if (padding > 1) {
      rv[rv.length - 2] = '=';
    }

    return rv.join('');
  }

  /**
   * Read as an image encoded in a data url.
   *
   * This is a static utility function.  There is a member function with the
   * same name which side-effects the current read position.
   *
   * @param dataView Data view instance.
   * @param pos Position in bytes to read from.
   * @param size Number of bytes to read.
   * @param end Maximum position to read from.
   * @return Image as a data url.
   */
  static readImage(dataView: DataView, pos: number, size: number, end?: number):
      string {
    end = end || dataView.byteLength;
    ByteReader.validateRead(pos, size, end);

    // Two bytes is enough to identify the mime type.
    const prefixToMime: Record<string, string> = {
      '\x89P': 'png',
      '\xFF\xD8': 'jpeg',
      'BM': 'bmp',
      'GI': 'gif',
    };

    const prefix = ByteReader.readString(dataView, pos, 2, end);
    const mime = prefixToMime[prefix] ||
        dataView.getUint16(pos, false).toString(16);  // For debugging.

    const b64 = ByteReader.readBase64(dataView, pos, size, end);
    return 'data:image/' + mime + ';base64,' + b64;
  }

  /**
   * Return true if the requested number of bytes can be read from the buffer.
   *
   * @param size Number of bytes to read.
   * @return True if allowed, false otherwise.
   */
  canRead(size: number): boolean {
    return this.pos_ + size <= this.view_.byteLength;
  }

  /**
   * Return true if the current position is past the end of the buffer.
   * @return True if EOF, otherwise false.
   */
  eof(): boolean {
    return this.pos_ >= this.view_.byteLength;
  }

  /**
   * Return true if the current position is before the beginning of the buffer.
   * @return True if BOF, otherwise false.
   */
  bof(): boolean {
    return this.pos_ < 0;
  }

  /**
   * Return true if the current position is outside the buffer.
   * @return True if outside, false if inside.
   */
  beof(): boolean {
    return this.pos_ >= this.view_.byteLength || this.pos_ < 0;
  }

  /**
   * Set the expected byte ordering for future reads.
   * @param order Byte order. Either LITTLE_ENDIAN or BIG_ENDIAN.
   */
  setByteOrder(order: ByteOrder) {
    this.littleEndian_ = order === ByteOrder.LITTLE_ENDIAN;
  }

  /**
   * Throw an error if the reader is at an invalid position, or if a read a read
   * of |size| would put it in one.
   *
   * You may optionally pass |end| to override what is considered to be the
   * end of the buffer.
   *
   * @param size Number of bytes to read.
   * @param end Maximum position to read from.
   */
  validateRead(size: number, end?: number) {
    if (typeof end === 'undefined') {
      end = this.view_.byteLength;
    }

    ByteReader.validateRead(this.pos_, size, end);
  }

  /**
   * @param width Number of bytes to read.
   * @param signed True if signed, false otherwise.
   * @param end Maximum position to read from.
   * @return Scalar value.
   */
  readScalar(
      width: keyof typeof WIDTH_TO_DATA_VIEW_METHOD, signed?: boolean,
      end?: number): number {
    this.validateRead(width, end);

    const method = WIDTH_TO_DATA_VIEW_METHOD[width][signed ? 1 : 0];

    let rv;
    if (method === 'getInt8' || method === 'getUint8') {
      rv = this.view_[method](this.pos_);
    } else {
      rv = this.view_[method](this.pos_, this.littleEndian_);
    }
    this.pos_ += width;
    return rv;
  }

  /**
   * Read as a sequence of characters, returning them as a single string.
   *
   * Adjusts the current position on success.  Throws an exception if the
   * read would go past the end of the buffer.
   *
   * @param size Number of bytes to read.
   * @param end Maximum position to read from.
   * @return String value.
   */
  readString(size: number, end?: number): string {
    const rv = ByteReader.readString(this.view_, this.pos_, size, end);
    this.pos_ += size;
    return rv;
  }

  /**
   * Read as a sequence of characters, returning them as a single string.
   *
   * Adjusts the current position on success.  Throws an exception if the
   * read would go past the end of the buffer.
   *
   * @param size Number of bytes to read.
   * @param end Maximum position to read from.
   * @return Null-terminated string value.
   */
  readNullTerminatedString(size: number, end?: number): string {
    const rv =
        ByteReader.readNullTerminatedString(this.view_, this.pos_, size, end);
    this.pos_ += rv.length;

    if (rv.length < size) {
      // If we've stopped reading because we found '0' but didn't hit size limit
      // then we should skip additional '0' character
      this.pos_++;
    }

    return rv;
  }

  /**
   * Read as a sequence of UTF16 characters, returning them as a single string.
   *
   * Adjusts the current position on success.  Throws an exception if the
   * read would go past the end of the buffer.
   *
   * @param bom True if BOM should be parsed.
   * @param size Number of bytes to read.
   * @param end Maximum position to read from.
   * @return Read string.
   */
  readNullTerminatedStringUtf16(bom: boolean, size: number, end?: number):
      string {
    const rv = ByteReader.readNullTerminatedStringUtf16(
        this.view_, this.pos_, bom, size, end);

    if (bom) {
      // If the BOM word was present advance the position.
      this.pos_ += 2;
    }

    this.pos_ += rv.length;

    if (rv.length < size) {
      // If we've stopped reading because we found '0' but didn't hit size limit
      // then we should skip additional '0' character
      this.pos_ += 2;
    }

    return rv;
  }

  /**
   * Read as a sequence of bytes, returning them as a single base64 encoded
   * string.
   *
   * Adjusts the current position on success.  Throws an exception if the
   * read would go past the end of the buffer.
   *
   * @param size Number of bytes to read.
   * @param end Maximum position to read from.
   * @return Base 64 encoded value.
   */
  readBase64(size: number, end: number|undefined): string {
    const rv = ByteReader.readBase64(this.view_, this.pos_, size, end);
    this.pos_ += size;
    return rv;
  }

  /**
   * Read an image returning it as a data url.
   *
   * Adjusts the current position on success.  Throws an exception if the
   * read would go past the end of the buffer.
   *
   * @param size Number of bytes to read.
   * @param end Maximum position to read from.
   * @return Image as a data url.
   */
  readImage(size: number, end?: number): string {
    const rv = ByteReader.readImage(this.view_, this.pos_, size, end);
    this.pos_ += size;
    return rv;
  }

  /**
   * Seek to a give position relative to seekStart.
   *
   * @param pos Position in bytes to seek to.
   * @param seekStart Relative position in bytes.
   * @param end Maximum position to seek to.
   */
  seek(pos: number, seekStart = SeekOrigin.SEEK_BEG, end?: number) {
    end = end || this.view_.byteLength;

    let newPos;
    if (seekStart === SeekOrigin.SEEK_CUR) {
      newPos = this.pos_ + pos;
    } else if (seekStart === SeekOrigin.SEEK_END) {
      newPos = end + pos;
    } else {
      newPos = pos;
    }

    if (newPos < 0 || newPos > this.view_.byteLength) {
      throw new Error('Seek outside of buffer: ' + (newPos - end));
    }

    this.pos_ = newPos;
  }

  /**
   * Seek to a given position relative to seekStart, saving the current
   * position.
   *
   * Recover the current position with a call to seekPop.
   *
   * @param pos Position in bytes to seek to.
   * @param seekStart Relative position in bytes.
   */
  pushSeek(pos: number, seekStart?: number) {
    const oldPos = this.pos_;
    this.seek(pos, seekStart);
    // Alter the seekStack_ after the call to seek(), in case it throws.
    this.seekStack_.push(oldPos);
  }

  /**
   * Undo a previous seekPush.
   */
  popSeek() {
    const lastSeek = this.seekStack_.pop();
    if (lastSeek !== undefined) {
      this.seek(lastSeek);
    }
  }

  /**
   * Return the current read position.
   * @return Current position in bytes.
   */
  tell(): number {
    return this.pos_;
  }
}

export enum ByteOrder {
  // Intel, 0x1234 is [0x34, 0x12]
  LITTLE_ENDIAN = 0,
  // Motorola, 0x1234 is [0x12, 0x34]
  BIG_ENDIAN = 1,
}

export enum SeekOrigin {
  // Seek relative to the beginning of the buffer.
  SEEK_BEG = 0,
  // Seek relative to the current position.
  SEEK_CUR = 1,
  // Seek relative to the end of the buffer.
  SEEK_END = 2,
}

const BASE64_ALPHABET =
    ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/')
        .split('');

const WIDTH_TO_DATA_VIEW_METHOD = {
  1: ['getUint8', 'getInt8'],
  2: ['getUint16', 'getInt16'],
  4: ['getUint32', 'getInt32'],
} as const;