/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import {
  Bytes,
  DataReader,
  Endian,
  hashAlgorithmForId,
  InternalError,
  Logger,
  MatterError
} from "@matter/general";
import { TlvOtaImageHeader } from "./OtaImageHeader.js";
const logger = Logger.get("OtaImageReader");
class OtaImageError extends MatterError {
  constructor(message, options) {
    super(message, options);
  }
}
async function computeOtaDigest(crypto, data, algorithm) {
  try {
    return Bytes.of(await crypto.computeHash(data, algorithm));
  } catch (cause) {
    throw new OtaImageError(`Cannot compute ${algorithm} OTA digest`, { cause });
  }
}
class OtaImageReader {
  #crypto;
  #streamReader;
  #totalSize;
  #headerSize;
  #headerData;
  #fullFileChecksum;
  #fullFileChecksumType = "SHA-256";
  /** Read only the OTA image header from the stream and returns the  parsed header data. */
  static async header(streamReader) {
    const reader = new OtaImageReader(streamReader);
    await reader.#processHeader(false);
    if (reader.#headerData === void 0) {
      throw new InternalError("OTA header not read");
    }
    return reader.#headerData;
  }
  /** Read and validate the full OTA image file from the stream and returns the header data on success. */
  static async file(streamReader, crypto, expectedTotalSize, options) {
    const reader = new OtaImageReader(streamReader, crypto);
    if (options?.checksumType !== void 0) {
      reader.#fullFileChecksumType = options.checksumType;
    }
    const { headerBytes, remainingData } = await reader.#processHeader(options?.calculateFullChecksum ?? true);
    if (reader.#headerData === void 0) {
      throw new InternalError("OTA header not read");
    }
    await reader.#processPayloadDigest(remainingData, headerBytes, options?.calculateFullChecksum ?? true);
    if (options?.expectedChecksum && reader.#fullFileChecksum !== options.expectedChecksum) {
      throw new OtaImageError(
        `OTA full file checksum mismatch: expected "${options.expectedChecksum}", got "${reader.#fullFileChecksum}"`
      );
    }
    if (expectedTotalSize !== void 0) {
      expectedTotalSize = BigInt(expectedTotalSize);
      if (reader.totalSize !== expectedTotalSize) {
        if (options?.expectedChecksum) {
          logger.warn(
            `OTA file size mismatch, but checksum matched: expected ${expectedTotalSize}, got ${reader.totalSize}`
          );
        } else {
          throw new OtaImageError(
            `OTA file size mismatch: expected ${expectedTotalSize}, got ${reader.totalSize}`
          );
        }
      }
    }
    return reader.#headerData;
  }
  /**
   * Read and validate OTA file, extracting payload to a writable stream.
   * Returns the header information after successful validation and extraction.
   */
  static async extractPayload(streamReader, payloadWriter, crypto, expectedTotalSize) {
    const reader = new OtaImageReader(streamReader, crypto);
    const { remainingData } = await reader.#processHeader(false);
    if (reader.#headerData === void 0) {
      throw new InternalError("OTA header not read");
    }
    await reader.#extractAndValidatePayload(remainingData, payloadWriter);
    if (expectedTotalSize !== void 0) {
      expectedTotalSize = BigInt(expectedTotalSize);
      if (reader.totalSize !== expectedTotalSize) {
        throw new OtaImageError(
          `OTA file size mismatch: expected ${expectedTotalSize}, got ${reader.totalSize}`
        );
      }
    }
    return reader.#headerData;
  }
  constructor(streamReader, crypto) {
    this.#streamReader = streamReader;
    this.#crypto = crypto;
  }
  get totalSize() {
    return this.#totalSize;
  }
  /** Process and read the OTA image header from the stream. */
  async #processHeader(collectHeaderBytes = false) {
    if (this.#headerData !== void 0) {
      throw new InternalError("OTA header already read.");
    }
    let headerReader = void 0;
    const allHeaderBytes = [];
    while (true) {
      const { value, done } = await this.#streamReader.read();
      if (value === void 0 || done) {
        throw new OtaImageError("OTA file ended unexpectedly");
      }
      if (collectHeaderBytes) {
        allHeaderBytes.push(value);
      }
      if (headerReader !== void 0) {
        headerReader = new DataReader(Bytes.concat(headerReader.remainingBytes, value), Endian.Little);
      } else {
        headerReader = new DataReader(value, Endian.Little);
      }
      if (this.#headerSize === void 0 && headerReader.remainingBytesCount >= 16) {
        const fileIdentifier = headerReader.readUInt32();
        if (fileIdentifier !== 468644126) {
          throw new OtaImageError("Invalid OTA file identifier");
        }
        this.#totalSize = headerReader.readUInt64();
        this.#headerSize = headerReader.readUInt32();
      }
      if (this.#headerData === void 0 && this.#headerSize !== void 0 && headerReader.remainingBytesCount >= this.#headerSize) {
        const data = headerReader.readByteArray(this.#headerSize);
        this.#headerData = TlvOtaImageHeader.decode(data);
        const remainingBytes = headerReader.remainingBytes;
        let headerBytes = void 0;
        if (collectHeaderBytes && allHeaderBytes.length > 0) {
          const totalHeaderAndRemaining = Bytes.of(Bytes.concat(...allHeaderBytes));
          const headerOnlyLength = totalHeaderAndRemaining.byteLength - remainingBytes.byteLength;
          headerBytes = totalHeaderAndRemaining.subarray(0, headerOnlyLength);
        }
        return { headerBytes, remainingData: remainingBytes };
      }
    }
  }
  /** Process and validate the OTA image payload digest from the stream. */
  async #processPayloadDigest(initialPayloadData, headerBytes, calculateFullChecksum = false) {
    if (this.#headerData === void 0) {
      throw new InternalError("OTA header not read");
    }
    if (this.#crypto === void 0) {
      throw new InternalError("No crypto implementation provided");
    }
    const { imageDigestType, imageDigest, payloadSize } = this.#headerData;
    let readPayloadSize = 0n;
    const streamReader = this.#streamReader;
    const payloadIterator = async function* (headerBytes2) {
      if (headerBytes2 !== void 0) {
        yield headerBytes2;
      }
      if (initialPayloadData !== void 0) {
        readPayloadSize += BigInt(initialPayloadData.byteLength);
        yield initialPayloadData;
      }
      while (true) {
        const { value, done } = await streamReader.read();
        if (value === void 0 || done) {
          break;
        }
        readPayloadSize += BigInt(value.byteLength);
        yield value;
      }
    };
    if (calculateFullChecksum && headerBytes !== void 0) {
      const fullChecksum = await computeOtaDigest(
        this.#crypto,
        payloadIterator(headerBytes),
        this.#fullFileChecksumType
      );
      this.#fullFileChecksum = Bytes.toBase64(fullChecksum);
      if (readPayloadSize !== BigInt(payloadSize)) {
        throw new OtaImageError(`OTA payload size mismatch: expected ${payloadSize}, got ${readPayloadSize}`);
      }
    } else {
      const digestAlgorithm = hashAlgorithmForId(imageDigestType);
      if (digestAlgorithm === void 0) {
        throw new OtaImageError(`Unsupported OTA image digest type: ${imageDigestType}`);
      }
      const hashBytes = await computeOtaDigest(this.#crypto, payloadIterator(), digestAlgorithm);
      if (readPayloadSize !== BigInt(payloadSize)) {
        throw new OtaImageError(`OTA payload size mismatch: expected ${payloadSize}, got ${readPayloadSize}`);
      }
      if (!Bytes.areEqual(hashBytes, imageDigest)) {
        throw new OtaImageError(
          `OTA file digest mismatch "${Bytes.toHex(hashBytes)}" vs. "${Bytes.toHex(imageDigest)}"`
        );
      }
    }
  }
  /** Extract the OTA image payload to a writable stream while validating its digest. */
  async #extractAndValidatePayload(initialPayloadData, payloadWriter) {
    if (this.#headerData === void 0) {
      throw new InternalError("OTA header not read");
    }
    if (this.#crypto === void 0) {
      throw new InternalError("No crypto implementation provided");
    }
    const { imageDigestType, imageDigest, payloadSize } = this.#headerData;
    let readPayloadSize = 0n;
    const streamReader = this.#streamReader;
    const iterator = async function* () {
      if (initialPayloadData !== void 0) {
        readPayloadSize += BigInt(initialPayloadData.byteLength);
        await payloadWriter.write(initialPayloadData);
        yield initialPayloadData;
      }
      while (true) {
        const { value, done } = await streamReader.read();
        if (value === void 0 || done) {
          break;
        }
        readPayloadSize += BigInt(value.byteLength);
        await payloadWriter.write(value);
        yield value;
      }
    };
    const digestAlgorithm = hashAlgorithmForId(imageDigestType);
    if (digestAlgorithm === void 0) {
      throw new OtaImageError(`Unsupported OTA image digest type: ${imageDigestType}`);
    }
    const hashBytes = await computeOtaDigest(this.#crypto, iterator(), digestAlgorithm);
    if (readPayloadSize !== BigInt(payloadSize)) {
      throw new OtaImageError(`OTA payload size mismatch: expected ${payloadSize}, got ${readPayloadSize}`);
    }
    if (!Bytes.areEqual(hashBytes, imageDigest)) {
      throw new OtaImageError(
        `OTA file digest mismatch "${Bytes.toHex(hashBytes)}" vs. "${Bytes.toHex(imageDigest)}"`
      );
    }
    await payloadWriter.close();
  }
}
export {
  OtaImageError,
  OtaImageReader
};
//# sourceMappingURL=OtaImageReader.js.map
