SquirrelJME/SquirrelJME

View on GitHub
modules/zip/src/main/java/net/multiphasicapps/zip/blockreader/ZipBlockEntry.java

Summary

Maintainability
A
0 mins
Test Coverage
// -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
// ---------------------------------------------------------------------------
// SquirrelJME
//     Copyright (C) Stephanie Gawroriski <xer@multiphasicapps.net>
// ---------------------------------------------------------------------------
// SquirrelJME is under the Mozilla Public License Version 2.0.
// See license.mkd for licensing and copyright information.
// ---------------------------------------------------------------------------

package net.multiphasicapps.zip.blockreader;

import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import net.multiphasicapps.zip.IBM437CodePage;
import net.multiphasicapps.zip.ZipCompressionType;
import net.multiphasicapps.zip.ZipException;
import org.jetbrains.annotations.NotNull;

/**
 * This represents a single entry within a ZIP file which may be opened.
 *
 * @since 2016/12/30
 */
public final class ZipBlockEntry
{
    /** Maximum version. */
    private static final int _MAX_CENTRAL_DIR_VERSION =
        20;
    
    /** The version which made the ZIP */
    private static final int _CENTRAL_DIRECTORY_MADE_BY_VERSION_OFFSET =
        4;
    
    /** The offset to the version needed to extract. */
    private static final int _CENTRAL_DIRECTORY_EXTRACT_VERSION_OFFSET =
        6;
    
    /** The offset of the general purpose flags. */
    private static final int _CENTRAL_DIRECTORY_FLAG_OFFSET =
        8;
    
    /** The offset to the method of compression. */
    private static final int _CENTRAL_DIRECTORY_METHOD_OFFSET =
        10;
    
    /** The offset to the CRC for data integrity. */
    private static final int _CENTRAL_DIRECTORY_CRC_OFFSET =
        16;
    
    /** The offset to the compressed size. */
    private static final int _CENTRAL_DIRECTORY_COMPRESSED_OFFSET =
        20;
    
    /** The offset to the uncompressed size. */
    private static final int _CENTRAL_DIRECTORY_UNCOMPRESSED_OFFSET =
        24;
    
    /** The offset to the file name length. */
    private static final int _CENTRAL_DIRECTORY_NAME_LENGTH_OFFSET =
        28;
    
    /** The offset to the extra data length. */
    private static final int _CENTRAL_DIRECTORY_EXTRA_LENGTH_OFFSET =
        ZipBlockEntry._CENTRAL_DIRECTORY_NAME_LENGTH_OFFSET + 2;
    
    /** The offset to the comment length. */
    private static final int _CENTRAL_DIRECTORY_COMMENT_LENGTH_OFFSET =
        ZipBlockEntry._CENTRAL_DIRECTORY_EXTRA_LENGTH_OFFSET + 2;
    
    /** The relative offset to the local header. */
    private static final int _CENTRAL_DIRECTORY_LOCAL_HEADER_OFFSET =
        42;
    
    /** The minimum length of the central directory entry. */
    private static final int _CENTRAL_DIRECTORY_MIN_LENGTH =
        46;
    
    /** The local file header magic number. */
    private static final int _LOCAL_HEADER_MAGIC_NUMBER =
        0x04034B50;
    
    /** The offset to the file name length in the local header. */
    private static final int _LOCAL_HEADER_NAME_LENGTH_OFFSET =
        26;
    
    /** The offset to the comment length in the local header. */
    private static final int _LOCAL_HEADER_COMMENT_LENGTH_OFFSET =
        28;
    
    /** The local header minimum size. */
    private static final int _LOCAL_HEADER_MIN_LENGTH =
        30;
    
    /** General purpose flag: Is UTF-8 encoded filename/comment? */
    protected static final int GPF_ENCODING_UTF8 =
        (1 << 11);
    
    /** The owning reader. */
    protected final ZipBlockReader owner;
    
    /** The data accessor. */
    protected final BlockAccessor accessor;
    
    /** The position of this entry. */
    protected final long position;
    
    /** The name of this file. */
    private Reference<String> _name;
    
    /**
     * Initializes the block entry.
     *
     * @param __br The owning block reader.
     * @param __id The entry ID.
     * @throws NullPointerException On null arguments.
     * @since 2016/12/30
     */
    ZipBlockEntry(ZipBlockReader __br, int __id)
        throws NullPointerException
    {
        // Check
        if (__br == null)
            throw new NullPointerException("NARG");
        
        // Set
        this.owner = __br;
        this.accessor = __br._accessor;
        
        // Get position
        this.position = __br._offsets[__id];
    }
    
    /**
     * Returns {@code true} if the entry pertains to a directory.
     *
     * @return If it is a directory or not.
     * @throws IOException On read errors.
     * @throws ZipException If the ZIP is malformed.
     * @since 2017/01/03
     */
    public boolean isDirectory()
        throws IOException, ZipException
    {
        return this.__internalToString().endsWith("/");
    }
    
    /**
     * Returns the time this entry was last modified.
     *
     * @return The last modified time or {@code Long.MIN_VALUE} if it is not
     * valid.
     * @since 2018/03/06
     */
    public long lastModifiedTime()
    {
        return Long.MIN_VALUE;
    }
    
    /**
     * Returns the name of this entry.
     *
     * @return The entry name.
     * @since 2017/03/01
     */
    public String name()
    {
        return this.toString();
    }
    
    /**
     * Opens the input stream for this entry's data.
     *
     * @return The entry data.
     * @throws IOException On read errors.
     * @throws ZipException If it could not be opened.
     * @since 2016/12/30
     */
    public InputStream open()
        throws IOException, ZipException
    {
        /* {@squirreljme.error BF0a Cannot open the entry because it is a
        directory. (The name of the entry)} */
        String s;
        if (this.isDirectory())
            throw new ZipException(String.format("BF0a %s", this.toString()));
        
        ZipBlockReader owner = this.owner;
        BlockAccessor accessor = this.accessor;
        
        // Read central directory information
        byte[] data = this.__readCentralDir();
        
        // The version needed to extract should not have the upper byte set
        // but some archive writing software sets the upper byte to match the
        // OS with the version in the made by bit.
        int ver = __ArrayData__.readUnsignedShort(
            ZipBlockEntry._CENTRAL_DIRECTORY_EXTRACT_VERSION_OFFSET, data),
            made = __ArrayData__.readUnsignedShort(
                ZipBlockEntry._CENTRAL_DIRECTORY_MADE_BY_VERSION_OFFSET, data);
        if ((ver & 0xFF00) != 0 && (made & 0xFF00) == (ver & 0xFF00))
            ver &= 0xFF;
        
        /* {@squirreljme.error BF0c Cannot open the entry because it uses
        too new of a version. (The version number)} */
        if (ZipBlockEntry._MAX_CENTRAL_DIR_VERSION < ver)
            throw new ZipException(String.format("BF0c %d", ver));
        
        // Need these later to determine how much data is available and how it
        // is stored.
        int method = __ArrayData__.readUnsignedShort(
            ZipBlockEntry._CENTRAL_DIRECTORY_METHOD_OFFSET, data);
        int crc = __ArrayData__.readSignedInt(
            ZipBlockEntry._CENTRAL_DIRECTORY_CRC_OFFSET,
            data);
        long compressed = __ArrayData__.readUnsignedInt(
            ZipBlockEntry._CENTRAL_DIRECTORY_COMPRESSED_OFFSET, data);
        
        // Determine the offset to the local header which precedes the data
        // of the entry
        long lhoffset = owner._zipbaseaddr + __ArrayData__.readUnsignedInt(
            ZipBlockEntry._CENTRAL_DIRECTORY_LOCAL_HEADER_OFFSET, data);
        
        /* {@squirreljme.error BF0d Could not read the local file header from
        the ZIP file.} */
        byte[] header = new byte[ZipBlockEntry._LOCAL_HEADER_MIN_LENGTH];
        if (ZipBlockEntry._LOCAL_HEADER_MIN_LENGTH != accessor.read(lhoffset,
            header, 0, ZipBlockEntry._LOCAL_HEADER_MIN_LENGTH))
            throw new ZipException("BF0d");
        
        /* {@squirreljme.error BF0e The magic number for the local file header
        is not valid.} */
        if (__ArrayData__.readSignedInt(0, header) !=
            ZipBlockEntry._LOCAL_HEADER_MAGIC_NUMBER)
            throw new ZipException("BF0e");
        
        // Need to know the file name and comment lengths, since they may
        // differ in the local header for some reason
        int lhfnl = __ArrayData__.readUnsignedShort(
            ZipBlockEntry._LOCAL_HEADER_NAME_LENGTH_OFFSET, header),
            lhcml = __ArrayData__.readUnsignedShort(
                ZipBlockEntry._LOCAL_HEADER_COMMENT_LENGTH_OFFSET, header);
        
        // The base address of the data is after the local header position
        long database = lhoffset + ZipBlockEntry._LOCAL_HEADER_MIN_LENGTH +
            lhfnl + lhcml;
        
        // Get base stream before compression
        InputStream base = new __BlockAccessorRegionInputStream__(accessor,
            database, compressed);
        
        /* {@squirreljme.error BF0f Unknown compression method for entry. (The
        method identifier)} */
        ZipCompressionType ztype = ZipCompressionType.forMethod(method);
        if (ztype == null)
            throw new ZipException(String.format("BF0f %d", method));
        
        // Wrap input so it may be read
        InputStream algo = ztype.inputStream(base);
        
        // Need to calculate the CRC for the stream of data
        return new __CRCInputStream__(algo, crc);
    }
    
    /**
     * {@inheritDoc}
     * @since 2016/12/30
     */
    @Override
    public String toString()
    {
        try
        {
            return this.__internalToString();
        }
            
        /* {@squirreljme.error BF0g Could not read the name of the
        entry.} */
        catch (IOException e)
        {
            throw new RuntimeException("BF0g", e);
        }
    }
    
    /**
     * Returns the uncompressed size of this entry as it appears in the
     * central directory.
     *
     * @return The uncompressed size of the entry.
     * @throws IOException On read errors.
     * @throws ZipException If the Zip is not correctly formatted.
     * @since 2024/01/14
     */
    public long uncompressedSize()
        throws IOException, ZipException
    {
        // Read in central directory
        byte[] data = this.__readCentralDir();
        
        // Read the size from it
        return __ArrayData__.readUnsignedInt(
            ZipBlockEntry._CENTRAL_DIRECTORY_UNCOMPRESSED_OFFSET, data);
    }
    
    /**
     * Reads the central directory block.
     *
     * @return The resultant central directory entry.
     * @throws IOException On read errors.
     * @throws ZipException If the Zip is not correctly formatted.
     * @since 2024/01/14
     */
    @NotNull
    private byte[] __readCentralDir()
        throws IOException, ZipException
    {
        /* {@squirreljme.error BF0b Could not read the central
        directory data.} */
        byte[] data = new byte[ZipBlockEntry._CENTRAL_DIRECTORY_MIN_LENGTH];
        if (ZipBlockEntry._CENTRAL_DIRECTORY_MIN_LENGTH != this.accessor.read(
            this.position, data, 0,
            ZipBlockEntry._CENTRAL_DIRECTORY_MIN_LENGTH))
            throw new ZipException("BF0b");
        
        return data;
    }
    
    /**
     * Returns the internal representation of the string entry.
     *
     * @return The name of this entry.
     * @throws IOException On read errors.
     * @throws ZipException If the ZIP is malformed.
     * @since 2017/01/03
     */
    private String __internalToString()
        throws IOException, ZipException
    {
        Reference<String> ref = this._name;
        String rv;
        
        // Need to load it?
        if (ref == null || null == (rv = ref.get()))
        {
            BlockAccessor accessor = this.accessor;
            long position = this.position;
            
            /* {@squirreljme.error BF0h Could not read the central
            directory data.} */
            byte[] data = new byte[ZipBlockEntry._CENTRAL_DIRECTORY_MIN_LENGTH];
            if (ZipBlockEntry._CENTRAL_DIRECTORY_MIN_LENGTH != accessor.read(position,
                data, 0, ZipBlockEntry._CENTRAL_DIRECTORY_MIN_LENGTH))
                throw new ZipException("BF0h");
            
            // Read file name length
            int fnl = __ArrayData__.readUnsignedShort(
                ZipBlockEntry._CENTRAL_DIRECTORY_NAME_LENGTH_OFFSET, data);
            
            /* {@squirreljme.error BF0i Could not read the file name.} */
            byte[] rawname = new byte[fnl];
            if (fnl != accessor.read(
                position + ZipBlockEntry._CENTRAL_DIRECTORY_MIN_LENGTH, rawname, 0, fnl))
                throw new ZipException("BF0i");
            
            // UTF-8 Encoded?
            if ((__ArrayData__.readUnsignedShort(
                ZipBlockEntry._CENTRAL_DIRECTORY_FLAG_OFFSET, data) & ZipBlockEntry.GPF_ENCODING_UTF8) != 0)
                rv = new String(rawname, 0, fnl, "utf-8");
            
            // DOS codepage
            else
                rv = IBM437CodePage.toString(rawname, 0, fnl);
            
            // Store for later
            this._name = new WeakReference<>(rv);
        }
        
        return rv;
    }
}