/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2024. All rights reserved.
 */
package zip4cj.io.inputstream

let emptyArrayList = ArrayList<ExtraDataRecord>()

public class HeaderReader {
    private var zipModel: ?ZipModel = None
    private let rawIO = RawIO()
    private let intBuff = Array<Byte>(4, repeat: 0)

    public func readAllHeaders(zip4cjRaf: RandomAccessFile, zip4cjConfig: Zip4cjConfig) {
        if (zip4cjRaf.length == 0) {
            return ZipModel()
        }

        if (zip4cjRaf.length < InternalZipConstants.ENDHDR) {
            throw ZipException(
                "Zip file size less than minimum expected zip file size. Probably not a zip file or a corrupted zip file"
            )
        }

        let myzipModel = ZipModel()
        this.zipModel = myzipModel
        try {
            myzipModel.setEndOfCentralDirectoryRecord(readEndOfCentralDirectoryRecord(zip4cjRaf, rawIO, zip4cjConfig))
        } catch (e: ZipException) {
            throw e
        } catch (e: Exception) {
            throw ZipException("Zip headers not found. Probably not a zip file or a corrupted zip file")
        }
        if (myzipModel.getEndOfCentralDirectoryRecord().getTotalNumberOfEntriesInCentralDirectory() == 0) {
            return myzipModel
        }
        // If file is Zip64 format, Zip64 headers have to be read before reading central directory
        myzipModel.setZip64EndOfCentralDirectoryLocator(
            readZip64EndOfCentralDirectoryLocator(zip4cjRaf, rawIO,
            myzipModel.getEndOfCentralDirectoryRecord().getOffsetOfEndOfCentralDirectory()))

        if (myzipModel.isZip64Format()) {
            myzipModel.setZip64EndOfCentralDirectoryRecord(readZip64EndCentralDirRec(zip4cjRaf, rawIO))
            if (myzipModel.getZip64EndOfCentralDirectoryRecord().getNumberOfThisDisk() > 0) {
                myzipModel.setSplitArchive(true)
            } else {
                myzipModel.setSplitArchive(false)
            }
        }
        myzipModel.setCentralDirectory(readCentralDirectory(zip4cjRaf, rawIO))

        return zipModel.getOrThrow()
    }

    private func readEndOfCentralDirectoryRecord(
        zip4cjRaf: RandomAccessFile,
        rawIO: RawIO,
        zip4cjConfig: Zip4cjConfig
    ): EndOfCentralDirectoryRecord {
        var offsetEndOfCentralDirectory = locateOffsetOfEndOfCentralDirectory(zip4cjRaf)
        seekInCurrentPart(zip4cjRaf, offsetEndOfCentralDirectory + 4)

        var endOfCentralDirectoryRecord = EndOfCentralDirectoryRecord()
        endOfCentralDirectoryRecord.setSignature(HeaderSignature.END_OF_CENTRAL_DIRECTORY)
        endOfCentralDirectoryRecord.setNumberOfThisDisk(rawIO.readShortLittleEndian(zip4cjRaf))
        endOfCentralDirectoryRecord.setNumberOfThisDiskStartOfCentralDir(rawIO.readShortLittleEndian(zip4cjRaf))
        endOfCentralDirectoryRecord.setTotalNumberOfEntriesInCentralDirectoryOnThisDisk(
            rawIO.readShortLittleEndian(zip4cjRaf))
        endOfCentralDirectoryRecord.setTotalNumberOfEntriesInCentralDirectory(
            Int64(rawIO.readShortLittleEndian(zip4cjRaf)))
        endOfCentralDirectoryRecord.setSizeOfCentralDirectory(rawIO.readIntLittleEndian(zip4cjRaf))
        endOfCentralDirectoryRecord.setOffsetOfEndOfCentralDirectory(offsetEndOfCentralDirectory)

        zip4cjRaf.readFully(intBuff)
        endOfCentralDirectoryRecord.setOffsetOfStartOfCentralDirectory(rawIO.readLongLittleEndian(intBuff, 0))

        var commentLength = Int64(rawIO.readShortLittleEndian(zip4cjRaf))
        endOfCentralDirectoryRecord.setComment(readZipComment(zip4cjRaf, commentLength))

        zipModel.getOrThrow().setSplitArchive(endOfCentralDirectoryRecord.getNumberOfThisDisk() > 0)
        return endOfCentralDirectoryRecord
    }

    private func readCentralDirectory(zip4cjRaf: RandomAccessFile, rawIO: RawIO): CentralDirectory {
        var centralDirectory = CentralDirectory()
        var fileHeaders = ArrayList<FileHeader>()

        var offSetStartCentralDir = HeaderUtil.getOffsetStartOfCentralDirectory(zipModel.getOrThrow())
        var centralDirEntryCount = getNumberOfEntriesInCentralDirectory(zipModel.getOrThrow())
        zip4cjRaf.seek(offSetStartCentralDir)

        var shortBuff = Array<Byte>(2, repeat: 0)
        var intBuff = Array<Byte>(4, repeat: 0)

        for (i in 0..centralDirEntryCount) {
            var fileHeader = FileHeader()
            if (Int64(rawIO.readIntLittleEndian(zip4cjRaf)) != HeaderSignature.CENTRAL_DIRECTORY.getValue()) {
                throw ZipException("Expected central directory entry not found (#${i+1})")
            }
            fileHeader.setSignature(HeaderSignature.CENTRAL_DIRECTORY)
            fileHeader.setVersionMadeBy(rawIO.readShortLittleEndian(zip4cjRaf))
            fileHeader.setVersionNeededToExtract(rawIO.readShortLittleEndian(zip4cjRaf))

            var generalPurposeFlags = Array<Byte>(2, repeat: 0)
            zip4cjRaf.readFully(generalPurposeFlags)
            fileHeader.setEncrypted(BitUtils.isBitSet(generalPurposeFlags[0], 0))
            fileHeader.setDataDescriptorExists(BitUtils.isBitSet(generalPurposeFlags[0], 3))
            fileHeader.setFileNameUTF8Encoded(BitUtils.isBitSet(generalPurposeFlags[1], 3))
            fileHeader.setGeneralPurposeFlag(generalPurposeFlags.clone())

            fileHeader.setCompressionMethod(
                CompressionMethod.getCompressionMethodFromCode(rawIO.readShortLittleEndian(zip4cjRaf)))
            fileHeader.setLastModifiedTime(Int64(rawIO.readIntLittleEndian(zip4cjRaf)))

            zip4cjRaf.readFully(intBuff)
            fileHeader.setCrc(rawIO.readLongLittleEndian(intBuff, 0))

            fileHeader.setCompressedSize(rawIO.readLongLittleEndian(zip4cjRaf, 4))
            fileHeader.setUncompressedSize(rawIO.readLongLittleEndian(zip4cjRaf, 4))

            var fileNameLength = Int64(rawIO.readShortLittleEndian(zip4cjRaf))
            fileHeader.setFileNameLength(fileNameLength)
            fileHeader.setExtraFieldLength(rawIO.readShortLittleEndian(zip4cjRaf))
            var fileCommentLength = rawIO.readShortLittleEndian(zip4cjRaf)
            fileHeader.setFileCommentLength(fileCommentLength)

            fileHeader.setDiskNumberStart(Int64(rawIO.readShortLittleEndian(zip4cjRaf)))

            zip4cjRaf.readFully(shortBuff)
            fileHeader.setInternalFileAttributes(shortBuff.clone())

            zip4cjRaf.readFully(intBuff)
            fileHeader.setExternalFileAttributes(intBuff.clone())

            zip4cjRaf.readFully(intBuff)
            fileHeader.setOffsetLocalHeader(rawIO.readLongLittleEndian(intBuff, 0))

            if (fileNameLength > 0) {
                var fileNameBuff = Array<Byte>(fileNameLength, repeat: 0)
                zip4cjRaf.readFully(fileNameBuff)
                var fileName = HeaderUtil.decodeStringWithCharset(fileNameBuff, fileHeader.isFileNameUTF8Encoded())
                fileHeader.setFileName(fileName)
            } else {
                throw ZipException("Invalid entry name in file header")
            }
            fileHeader.setDirectory(
                isDirectory(fileHeader.getExternalFileAttributes().getOrThrow(), fileHeader.getFileName()))
            readExtraDataRecords(zip4cjRaf, fileHeader)
            readZip64ExtendedInfo(fileHeader, rawIO)
            readAesExtraDataRecord(fileHeader, rawIO)
            if (fileCommentLength > 0) {
                let fileCommentBuff = Array<Byte>(Int64(fileCommentLength), repeat: 0)
                zip4cjRaf.readFully(fileCommentBuff)
                fileHeader.setFileComment(
                    HeaderUtil.decodeStringWithCharset(fileCommentBuff, fileHeader.isFileNameUTF8Encoded()))
            }
            if (fileHeader.isEncrypted()) {
                if (let Some(_) <- fileHeader.getAesExtraDataRecord()) {
                    fileHeader.setEncryptionMethod(EncryptionMethod.ZIP_STANDARD)
                } else {
                    fileHeader.setEncryptionMethod(EncryptionMethod.AES)
                }
            }
            fileHeaders.add(fileHeader)
        }

        centralDirectory.setFileHeaders(fileHeaders)

        var digitalSignature = DigitalSignature()
        if (Int64(rawIO.readIntLittleEndian(zip4cjRaf)) == HeaderSignature.DIGITAL_SIGNATURE.getValue()) {
            digitalSignature.setSignature(HeaderSignature.DIGITAL_SIGNATURE)
            digitalSignature.setSizeOfData(rawIO.readShortLittleEndian(zip4cjRaf))

            if (digitalSignature.getSizeOfData() > 0) {
                var signatureDataBuff = Array<Byte>(Int64(digitalSignature.getSizeOfData()), repeat: 0)
                zip4cjRaf.readFully(signatureDataBuff)
                digitalSignature.setSignatureData(String.fromUtf8(signatureDataBuff))
            }
        }

        return centralDirectory
    }

    private func readExtraDataRecords(zip4cjRaf: RandomAccessFile, fileHeader: FileHeader) {
        var extraFieldLength: Int64 = Int64(fileHeader.getExtraFieldLength())

        if (extraFieldLength <= 0) {
            return
        }
        fileHeader.setExtraDataRecords(readExtraDataRecords(zip4cjRaf, extraFieldLength))
    }

    private func readExtraDataRecords(inputStream: InputStream, localFileHeader: LocalFileHeader) {
        var extraFieldLength: Int64 = Int64(localFileHeader.getExtraFieldLength())
        if (extraFieldLength <= 0) {
            return
        }
        localFileHeader.setExtraDataRecords(readExtraDataRecords(inputStream, extraFieldLength))
    }

    private func readExtraDataRecords(zip4cjRaf: RandomAccessFile, extraFieldLength: Int64): ?ArrayList<ExtraDataRecord> {
        var extraFieldBuf = Array<Byte>(1, repeat: 0)
        if (extraFieldLength < 4) {
            if (extraFieldLength > 0) {
                // zip4cjRaf.skipBytes(extraFieldLength)

                for (_ in 0..extraFieldLength) {
                    zip4cjRaf.read(extraFieldBuf)
                }
            }

            return None
        }

        extraFieldBuf = Array<Byte>(extraFieldLength, repeat: 0)
        zip4cjRaf.read(extraFieldBuf)

        try {
            return parseExtraDataRecords(extraFieldBuf, extraFieldLength)
        } catch (e: Exception) {
            // Ignore any errors when parsing extra data records
            return emptyArrayList
        }
    }

    private func readExtraDataRecords(inputStream: InputStream, extraFieldLength: Int64): ?ArrayList<ExtraDataRecord> {
        var extraFieldBuf = Array<Byte>(1, repeat: 0)
        if (extraFieldLength < 4) {
            if (extraFieldLength > 0) {
                for (_ in 0..extraFieldLength) {
                    inputStream.read(extraFieldBuf)
                }
            }

            return None
        }

        extraFieldBuf = Array<Byte>(extraFieldLength, repeat: 0)
        Zip4cjUtil.readFully(inputStream, extraFieldBuf)

        try {
            return parseExtraDataRecords(extraFieldBuf, extraFieldLength)
        } catch (e: Exception) {
            // Ignore any errors when parsing extra data records
            return emptyArrayList
        }
    }

    private func parseExtraDataRecords(extraFieldBuf: Array<Byte>, extraFieldLength: Int64): ArrayList<ExtraDataRecord> {
        var counter = 0
        var extraDataRecords = ArrayList<ExtraDataRecord>()
        while (counter < extraFieldLength) {
            var extraDataRecord = ExtraDataRecord()
            var header = rawIO.readShortLittleEndian(extraFieldBuf, counter)
            extraDataRecord.setHeader(Int64(header))
            counter += 2

            var sizeOfRec = rawIO.readShortLittleEndian(extraFieldBuf, counter)
            extraDataRecord.setSizeOfData(Int64(sizeOfRec))
            counter += 2

            if (sizeOfRec > 0) {
                var data = Array<Byte>(Int64(sizeOfRec), repeat: 0)
                ArrayCopy(extraFieldBuf, counter, data, 0, Int64(sizeOfRec))
                extraDataRecord.setData(data)
            }
            counter += Int64(sizeOfRec)
            extraDataRecords.add(extraDataRecord)
        }
        return if (extraDataRecords.size > 0) {
            extraDataRecords
        } else {
            emptyArrayList
        }
    }

    private func readZip64EndOfCentralDirectoryLocator(
        zip4cjRaf: RandomAccessFile,
        rawIO: RawIO,
        offsetEndOfCentralDirectoryRecord: Int64
    ): ?Zip64EndOfCentralDirectoryLocator {
        let zip64EndOfCentralDirectoryLocator = Zip64EndOfCentralDirectoryLocator()
        setFilePointerToReadZip64EndCentralDirLoc(zip4cjRaf, offsetEndOfCentralDirectoryRecord)

        var signature = rawIO.readIntLittleEndian(zip4cjRaf)
        if (signature == Int32(HeaderSignature.ZIP64_END_CENTRAL_DIRECTORY_LOCATOR.getValue())) {
            zipModel.getOrThrow().setZip64Format(true)
            zip64EndOfCentralDirectoryLocator.setSignature(HeaderSignature.ZIP64_END_CENTRAL_DIRECTORY_LOCATOR)
        } else {
            zipModel.getOrThrow().setZip64Format(false)
            return None
        }

        zip64EndOfCentralDirectoryLocator.setNumberOfDiskStartOfZip64EndOfCentralDirectoryRecord(
            rawIO.readIntLittleEndian(zip4cjRaf))
        zip64EndOfCentralDirectoryLocator.setOffsetZip64EndOfCentralDirectoryRecord(
            rawIO.readLongLittleEndian(zip4cjRaf))
        zip64EndOfCentralDirectoryLocator.setTotalNumberOfDiscs(rawIO.readIntLittleEndian(zip4cjRaf))

        return zip64EndOfCentralDirectoryLocator
    }

    private func readZip64EndCentralDirRec(zip4cjRaf: RandomAccessFile, rawIO: RawIO): Zip64EndOfCentralDirectoryRecord {
        let offSetStartOfZip64CentralDir = zipModel.getOrThrow().getZip64EndOfCentralDirectoryLocator().getOrThrow().
            getOffsetZip64EndOfCentralDirectoryRecord()

        if (offSetStartOfZip64CentralDir < 0) {
            throw ZipException("invalid offset for start of end of central directory record")
        }

        zip4cjRaf.seek(offSetStartOfZip64CentralDir)

        var zip64EndOfCentralDirectoryRecord = Zip64EndOfCentralDirectoryRecord()

        var signature: Int32 = rawIO.readIntLittleEndian(zip4cjRaf)
        if (Int64(signature) != HeaderSignature.ZIP64_END_CENTRAL_DIRECTORY_RECORD.getValue()) {
            throw ZipException("invalid signature for zip64 end of central directory record")
        }
        zip64EndOfCentralDirectoryRecord.setSignature(HeaderSignature.ZIP64_END_CENTRAL_DIRECTORY_RECORD)
        zip64EndOfCentralDirectoryRecord.setSizeOfZip64EndCentralDirectoryRecord(rawIO.readLongLittleEndian(zip4cjRaf))
        zip64EndOfCentralDirectoryRecord.setVersionMadeBy(rawIO.readShortLittleEndian(zip4cjRaf))
        zip64EndOfCentralDirectoryRecord.setVersionNeededToExtract(rawIO.readShortLittleEndian(zip4cjRaf))
        zip64EndOfCentralDirectoryRecord.setNumberOfThisDisk(rawIO.readIntLittleEndian(zip4cjRaf))
        zip64EndOfCentralDirectoryRecord.setNumberOfThisDiskStartOfCentralDirectory(
            rawIO.readIntLittleEndian(zip4cjRaf))
        zip64EndOfCentralDirectoryRecord.setTotalNumberOfEntriesInCentralDirectoryOnThisDisk(
            rawIO.readLongLittleEndian(zip4cjRaf))
        zip64EndOfCentralDirectoryRecord.setTotalNumberOfEntriesInCentralDirectory(
            rawIO.readLongLittleEndian(zip4cjRaf))
        zip64EndOfCentralDirectoryRecord.setSizeOfCentralDirectory(rawIO.readLongLittleEndian(zip4cjRaf))
        zip64EndOfCentralDirectoryRecord.setOffsetStartCentralDirectoryWRTStartDiskNumber(
            rawIO.readLongLittleEndian(zip4cjRaf))

        //zip64 extensible data sector
        //44 is the size of fixed variables in this record
        let extDataSecSize = zip64EndOfCentralDirectoryRecord.getSizeOfZip64EndCentralDirectoryRecord() - 44
        if (extDataSecSize > 0) {
            let extDataSecRecBuf = Array<Byte>(extDataSecSize, repeat: 0)
            zip4cjRaf.readFully(extDataSecRecBuf)
            zip64EndOfCentralDirectoryRecord.setExtensibleDataSector(extDataSecRecBuf)
        }

        return zip64EndOfCentralDirectoryRecord
    }

    private func readZip64ExtendedInfo(fileHeader: FileHeader, rawIO: RawIO) {
        if (fileHeader.getExtraDataRecords().isNone() || fileHeader.getExtraDataRecords().getOrThrow().size <= 0) {
            return
        }
        if (let Some(zip64ExtendedInfo) <- readZip64ExtendedInfo(fileHeader.getExtraDataRecords().getOrThrow(), rawIO,
            fileHeader.getUncompressedSize(), fileHeader.getCompressedSize(), fileHeader.getOffsetLocalHeader(),
            fileHeader.getDiskNumberStart())) {
            fileHeader.setZip64ExtendedInfo(zip64ExtendedInfo)
            if (-1 != zip64ExtendedInfo.getUncompressedSize()) {
                fileHeader.setUncompressedSize(zip64ExtendedInfo.getUncompressedSize())
            }

            if (-1 != zip64ExtendedInfo.getCompressedSize()) {
                fileHeader.setCompressedSize(zip64ExtendedInfo.getCompressedSize())
            }

            if (-1 != zip64ExtendedInfo.getOffsetLocalHeader()) {
                fileHeader.setOffsetLocalHeader(zip64ExtendedInfo.getOffsetLocalHeader())
            }
            if (-1 != zip64ExtendedInfo.getDiskNumberStart()) {
                fileHeader.setDiskNumberStart(Int64(zip64ExtendedInfo.getDiskNumberStart()))
            }
        } else {
            return
        }
    }

    private func readZip64ExtendedInfo(localFileHeaderOpt: ?LocalFileHeader, rawIO: RawIO): Unit {
        if (localFileHeaderOpt.isNone()) {
            throw ZipException("file header is null in reading Zip64 Extended Info")
        }
        let localFileHeader = localFileHeaderOpt.getOrThrow()
        if (localFileHeader.getExtraDataRecords().isNone() || localFileHeader.getExtraDataRecords().getOrThrow().size <=
            0) {
            return
        }

        var zip64ExtendedInfo = readZip64ExtendedInfo(localFileHeader.getExtraDataRecords().getOrThrow(), rawIO,
            localFileHeader.getUncompressedSize(), localFileHeader.getCompressedSize(), 0, 0)

        if (zip64ExtendedInfo.isNone()) {
            return
        }

        localFileHeader.setZip64ExtendedInfo(zip64ExtendedInfo.getOrThrow())

        if (zip64ExtendedInfo.getOrThrow().getUncompressedSize() != -1) {
            localFileHeader.setUncompressedSize(zip64ExtendedInfo.getOrThrow().getUncompressedSize())
        }

        if (zip64ExtendedInfo.getOrThrow().getCompressedSize() != -1) {
            localFileHeader.setCompressedSize(zip64ExtendedInfo.getOrThrow().getCompressedSize())
        }
    }

    private func readZip64ExtendedInfo(
        extraDataRecords: ArrayList<ExtraDataRecord>,
        rawIO: RawIO,
        uncompressedSize: Int64,
        compressedSize: Int64,
        offsetLocalHeader: Int64,
        diskNumberStart: Int64
    ): ?Zip64ExtendedInfo {
        for (i in 0..extraDataRecords.size) {
            let extraDataRecord = extraDataRecords[i]
            if (HeaderSignature.ZIP64_EXTRA_FIELD_SIGNATURE.getValue() == extraDataRecord.getHeader()) {
                var zip64ExtendedInfo = Zip64ExtendedInfo()
                var extraData = extraDataRecord.getData().getOrThrow()

                if (extraDataRecord.getSizeOfData() <= 0) {
                    return None
                }

                var counter = 0
                if (counter < extraDataRecord.getSizeOfData() && uncompressedSize == InternalZipConstants.
                    ZIP_64_SIZE_LIMIT) {
                    zip64ExtendedInfo.setUncompressedSize(rawIO.readLongLittleEndian(extraData, counter))
                    counter += 8
                }

                if (counter < extraDataRecord.getSizeOfData() && compressedSize == InternalZipConstants.
                    ZIP_64_SIZE_LIMIT) {
                    zip64ExtendedInfo.setCompressedSize(rawIO.readLongLittleEndian(extraData, counter))
                    counter += 8
                }

                if (counter < extraDataRecord.getSizeOfData() && offsetLocalHeader == InternalZipConstants.
                    ZIP_64_SIZE_LIMIT) {
                    zip64ExtendedInfo.setOffsetLocalHeader(rawIO.readLongLittleEndian(extraData, counter))
                    counter += 8
                }

                if (counter < extraDataRecord.getSizeOfData() && diskNumberStart == InternalZipConstants.
                    ZIP_64_NUMBER_OF_ENTRIES_LIMIT) {
                    zip64ExtendedInfo.setDiskNumberStart(rawIO.readIntLittleEndian(extraData, counter))
                }

                return zip64ExtendedInfo
            }
        }
        return None
    }

    private func setFilePointerToReadZip64EndCentralDirLoc(
        zip4cjRaf: RandomAccessFile,
        offsetEndOfCentralDirectoryRecord: Int64
    ): Unit {
        /* Now the file pointer is at the end of signature of Central Dir Rec
        Seek back with the following values
        4 -> total number of disks
        8 -> relative offset of the zip64 end of central directory record
        4 -> number of the disk with the start of the zip64 end of central directory
        4 -> zip64 end of central dir locator signature
        Refer to Appnote for more information */
        seekInCurrentPart(zip4cjRaf, offsetEndOfCentralDirectoryRecord - 4 - 8 - 4 - 4)
    }

    public func readLocalFileHeader(inputStream: InputStream): ?LocalFileHeader {
        var localFileHeader = LocalFileHeader()
        let intBuff = Array<Byte>(4, repeat: 0)

        //signature
        var sig = rawIO.readIntLittleEndian(inputStream)
        if (sig == Int32(HeaderSignature.TEMPORARY_SPANNING_MARKER.getValue())) {
            sig = rawIO.readIntLittleEndian(inputStream)
        }
        if (sig != Int32(HeaderSignature.LOCAL_FILE_HEADER.getValue())) {
            return None
        }
        localFileHeader.setSignature(HeaderSignature.LOCAL_FILE_HEADER)
        localFileHeader.setVersionNeededToExtract(rawIO.readShortLittleEndian(inputStream))

        let generalPurposeFlags = Array<Byte>(2, repeat: 0)
        if (Zip4cjUtil.readFully(inputStream, generalPurposeFlags) != 2) {
            throw ZipException("Could not read enough bytes for generalPurposeFlags")
        }
        localFileHeader.setEncrypted(BitUtils.isBitSet(generalPurposeFlags[0], 0))
        localFileHeader.setDataDescriptorExists(BitUtils.isBitSet(generalPurposeFlags[0], 3))
        localFileHeader.setFileNameUTF8Encoded(BitUtils.isBitSet(generalPurposeFlags[1], 3))
        localFileHeader.setGeneralPurposeFlag(generalPurposeFlags.clone())

        localFileHeader.setCompressionMethod(
            CompressionMethod.getCompressionMethodFromCode(rawIO.readShortLittleEndian(inputStream)))
        localFileHeader.setLastModifiedTime(Int64(rawIO.readIntLittleEndian(inputStream)))

        Zip4cjUtil.readFully(inputStream, intBuff)
        localFileHeader.setCrc(rawIO.readLongLittleEndian(intBuff, 0))

        localFileHeader.setCompressedSize(rawIO.readLongLittleEndian(inputStream, 4))
        localFileHeader.setUncompressedSize(rawIO.readLongLittleEndian(inputStream, 4))

        var fileNameLength = rawIO.readShortLittleEndian(inputStream)
        localFileHeader.setFileNameLength(Int64(fileNameLength))
        localFileHeader.setExtraFieldLength(rawIO.readShortLittleEndian(inputStream))

        if (fileNameLength > 0) {
            let fileNameBuf = Array<Byte>(Int64(fileNameLength), repeat: 0)
            Zip4cjUtil.readFully(inputStream, fileNameBuf)

            var fileName = HeaderUtil.decodeStringWithCharset(fileNameBuf, localFileHeader.isFileNameUTF8Encoded())
            localFileHeader.setFileName(fileName)
            localFileHeader.setDirectory(fileName.endsWith("/") || fileName.endsWith("\\"))
        } else {
            throw ZipException("Invalid entry name in local file header")
        }
        this.readExtraDataRecords(inputStream, localFileHeader)
        this.readZip64ExtendedInfo(localFileHeader, rawIO)
        this.readAesExtraDataRecord(localFileHeader, rawIO)

        if (localFileHeader.isEncrypted()) {
            if (EncryptionMethod.AES == localFileHeader.getEncryptionMethod()) {
                //Do nothing
            } else {
                if (BitUtils.isBitSet(localFileHeader.getGeneralPurposeFlag().getOrThrow()[0], 6)) {
                    localFileHeader.setEncryptionMethod(EncryptionMethod.ZIP_STANDARD_VARIANT_STRONG)
                } else {
                    localFileHeader.setEncryptionMethod(EncryptionMethod.ZIP_STANDARD)
                }
            }
        }

        return localFileHeader
    }

    public func readDataDescriptor(inputStream: InputStream, isZip64Format: Bool): DataDescriptor {

        let dataDescriptors = DataDescriptor()
        let intBuff = Array<Byte>(4, repeat:0)
        Zip4cjUtil.readFully(inputStream, intBuff)
        var sigOrCrc = rawIO.readLongLittleEndian(intBuff, 0)
        //According to zip specification, presence of extra data record header signature is optional.
        //If this signature is present, read it and read the next 4 bytes for crc
        //If signature not present, assign the read 4 bytes for crc
        if (sigOrCrc == HeaderSignature.EXTRA_DATA_RECORD.getValue()) {
            dataDescriptors.setSignature(HeaderSignature.EXTRA_DATA_RECORD)
            Zip4cjUtil.readFully(inputStream, intBuff)
            dataDescriptors.setCrc(rawIO.readLongLittleEndian(intBuff, 0))
        } else {
            dataDescriptors.setCrc(sigOrCrc)
        }
        if (isZip64Format) {
            dataDescriptors.setCompressedSize(rawIO.readLongLittleEndian(inputStream))
            dataDescriptors.setUncompressedSize(rawIO.readLongLittleEndian(inputStream))
        } else {
            dataDescriptors.setCompressedSize(Int64(rawIO.readIntLittleEndian(inputStream)))
            dataDescriptors.setUncompressedSize(Int64(rawIO.readIntLittleEndian(inputStream)))
        }
        return dataDescriptors
    }

    private func readAesExtraDataRecord(fileHeader: AbstractFileHeader, rawIO: RawIO) {
        if (let Some(fileHead) <- fileHeader.getExtraDataRecords()) {
            if (fileHead.size <= 0) {
                return
            }

            let aesExtraDataRecord: ?AESExtraDataRecord = readAesExtraDataRecord(fileHead, rawIO)
            if (let Some(v) <- aesExtraDataRecord) {
                fileHeader.setAesExtraDataRecord(v)
                fileHeader.setEncryptionMethod(EncryptionMethod.AES)
            }
        } else {
            return
        }
    }

    private func readAesExtraDataRecord(extraDataRecords: ArrayList<ExtraDataRecord>, rawIO: RawIO): ?AESExtraDataRecord {
        
        for (i in 0..extraDataRecords.size) {
            let extraDataRecord = extraDataRecords[i]
            if (extraDataRecord.getHeader() == HeaderSignature.AES_EXTRA_DATA_RECORD.getValue()) {
                var aesExtraDataRecordBytes = extraDataRecord.getData()
                if (aesExtraDataRecordBytes == None || aesExtraDataRecordBytes.getOrThrow().size != 7) {
                    throw ZipException("corrupt AES extra data records")
                }
                let aesExtraDataRecord = AESExtraDataRecord()
                aesExtraDataRecord.setSignature(HeaderSignature.AES_EXTRA_DATA_RECORD)
                aesExtraDataRecord.setDataSize(extraDataRecord.getSizeOfData())

                var aesData = extraDataRecord.getData().getOrThrow()
                aesExtraDataRecord.setAesVersion(
                    AesVersion.getFromVersionNumber(rawIO.readShortLittleEndian(aesData, 0)))
                var vendorIDBytes = Array<Byte>(2, repeat: 0)
                ArrayCopy(aesData, 2, vendorIDBytes, 0, 2)
                aesExtraDataRecord.setVendorID(String.fromUtf8(vendorIDBytes))
                aesExtraDataRecord.setAesKeyStrength(
                    AesKeyStrength.getAesKeyStrengthFromRawCode(Int32(aesData[4] & 0xFF)).getOrThrow())
                aesExtraDataRecord.setCompressionMethod(
                    CompressionMethod.getCompressionMethodFromCode(rawIO.readShortLittleEndian(aesData, 5)))
                return aesExtraDataRecord
            }
        }

        return None
    }

    private func getNumberOfEntriesInCentralDirectory(zipModel: ZipModel): Int64 {
        if (zipModel.isZip64Format()) {
            return zipModel.getZip64EndOfCentralDirectoryRecord().getTotalNumberOfEntriesInCentralDirectory()
        }
        return zipModel.getEndOfCentralDirectoryRecord().getTotalNumberOfEntriesInCentralDirectory()
    }

    private func locateOffsetOfEndOfCentralDirectory(randomAccessFile: RandomAccessFile): Int64 {
        var zipFileSize = randomAccessFile.length
        if (zipFileSize < InternalZipConstants.ENDHDR) {
            throw ZipException("Zip file size less than size of zip headers. Probably not a zip file.")
        }

        seekInCurrentPart(randomAccessFile, zipFileSize - InternalZipConstants.ENDHDR)
        if (Int64(rawIO.readIntLittleEndian(randomAccessFile)) == HeaderSignature.END_OF_CENTRAL_DIRECTORY.getValue()) {
            return zipFileSize - InternalZipConstants.ENDHDR
        }

        return locateOffsetOfEndOfCentralDirectoryByReverseSeek(randomAccessFile)
    }

    private func locateOffsetOfEndOfCentralDirectoryByReverseSeek(randomAccessFile: RandomAccessFile): Int64 {
        var currentFilePointer = randomAccessFile.length - InternalZipConstants.ENDHDR
        // reverse seek for a maximum of MAX_COMMENT_SIZE bytes
        var numberOfBytesToRead = if (randomAccessFile.length < InternalZipConstants.MAX_COMMENT_SIZE) {
            randomAccessFile.length
        } else {
            InternalZipConstants.MAX_COMMENT_SIZE
        }
        while (numberOfBytesToRead > 0 && currentFilePointer > 0) {
            currentFilePointer--
            seekInCurrentPart(randomAccessFile, currentFilePointer)
            if (Int64(rawIO.readIntLittleEndian(randomAccessFile)) == HeaderSignature.END_OF_CENTRAL_DIRECTORY.getValue()) {
                return currentFilePointer
            }
            numberOfBytesToRead--
        }
        throw ZipException("Zip headers not found. Probably not a zip file")
    }

    private func seekInCurrentPart(randomAccessFile: RandomAccessFile, pos: Int64): Unit {
        if (randomAccessFile is NumberedSplitRandomAccessFile) {
            (randomAccessFile as NumberedSplitRandomAccessFile).getOrThrow().seekInCurrentPart(pos)
        } else {
            randomAccessFile.seek(pos)
        }
    }

    private func readZipComment(raf: RandomAccessFile, commentLength: Int64): ?String {
        if (commentLength <= 0) {
            return None
        }

        try {
            let commentBuf = Array<Byte>(commentLength, repeat: 0)
            raf.readFully(commentBuf)
            return HeaderUtil.decodeStringWithCharset(commentBuf, false)
        } catch (e: Exception) {
            // Ignore any exception and set comment to null if comment cannot be read
            return None
        }
    }

    public func isDirectory(externalFileAttributes: Array<Byte>, fileName: ?String): Bool {
        // first check if DOS attributes are set (lower order bytes from external attributes). If yes, check if the 4th bit
        // which represents a directory is set. If UNIX attributes are set (higher order two bytes), check for the 6th bit
        // in 4th byte which  represents a directory flag.
        if (externalFileAttributes.size <= 0 ) {
            throw IndexOutOfBoundsException("Index 0 out of bounds for length 0")
        }
        if (externalFileAttributes[0] != 0 && BitUtils.isBitSet(externalFileAttributes[0], 4)) {
            return true
        } else if (externalFileAttributes[3] != 0 && BitUtils.isBitSet(externalFileAttributes[3], 6)) {
            return true
        }

        return fileName != None && (fileName.getOrThrow().endsWith("/") || fileName.getOrThrow().endsWith("\\"))
    }
}