nl.salp.warcraft4j.casc.cdn.local.LocalIndexFileParser.java Source code

Java tutorial

Introduction

Here is the source code for nl.salp.warcraft4j.casc.cdn.local.LocalIndexFileParser.java

Source

/*
 * Licensed to the Warcraft4J Project under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The Warcraft4J Project licenses
 * this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package nl.salp.warcraft4j.casc.cdn.local;

import nl.salp.warcraft4j.casc.CascParsingException;
import nl.salp.warcraft4j.casc.FileKey;
import nl.salp.warcraft4j.casc.IndexEntry;
import nl.salp.warcraft4j.hash.JenkinsHash;
import nl.salp.warcraft4j.io.DataParsingException;
import nl.salp.warcraft4j.io.DataReader;
import nl.salp.warcraft4j.io.DataReadingException;
import nl.salp.warcraft4j.io.datatype.DataTypeFactory;
import nl.salp.warcraft4j.util.Checksum;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.ByteOrder;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;

import static java.lang.String.format;

/**
 * Parser for parsing a {@link LocalIndexFile}.
 *
 * @author Barre Dijkstra
 */
public class LocalIndexFileParser {
    /** The logger. */
    private static final Logger LOGGER = LoggerFactory.getLogger(LocalIndexFileParser.class);
    /** The size of an entry in bytes. */
    public static final int ENTRY_SIZE = 18;
    /** The path of the index file. */
    private final Path file;
    /** Function to get the the index file number from the file path. */
    private final Function<Path, Integer> fileNumberFunction;
    /** Function get the the index file version from the file path. */
    private final Function<Path, Integer> fileVersionFunction;

    /**
     * Create a new instance.
     *
     * @param file                The path of the index file to parse.
     * @param fileNumberFunction  The function to get the the index file number from the file path.
     * @param fileVersionFunction The function to get the the index file version from the file path.
     */
    public LocalIndexFileParser(Path file, Function<Path, Integer> fileNumberFunction,
            Function<Path, Integer> fileVersionFunction) {
        this.file = file;
        this.fileNumberFunction = fileNumberFunction;
        this.fileVersionFunction = fileVersionFunction;
    }

    /**
     * Read and parse a {@link LocalIndexFile}.
     *
     * @param reader The reader for the file.
     *
     * @return The parsed index file.
     *
     * @throws CascParsingException When the file is invalid.
     * @throws DataReadingException When reading the entry data failed.
     * @throws DataParsingException When parsing the entry data failed.
     */
    public LocalIndexFile parse(DataReader reader) throws DataReadingException, DataParsingException {
        LOGGER.trace("Parsing index file {}", file);
        validateHeader(reader);
        int dataHeaderLength = reader.readNext(DataTypeFactory.getInteger(), ByteOrder.LITTLE_ENDIAN);
        Checksum dataHeaderChecksum = new Checksum(reader.readNext(DataTypeFactory.getByteArray(4))); // ByteOrder.LITTLE_ENDIAN
        // TODO Validate header based on the checksum.
        IndexHeaderV2 header = parseHeader(reader);
        LOGGER.trace("Parsed header {}", header);
        reader.position((8 + dataHeaderLength + 0x0F) & 0xFFFFFFF0);

        int dataLength = reader.readNext(DataTypeFactory.getInteger(), ByteOrder.LITTLE_ENDIAN);
        Checksum dataChecksum = new Checksum(reader.readNext(DataTypeFactory.getByteArray(4))); // ByteOrder.LITTLE_ENDIAN
        // TODO Validate data based on the checksum.
        int entryCount = (dataLength / ENTRY_SIZE);
        LOGGER.trace("Parsing {} index file entries from {} bytes at position {}", entryCount, dataLength,
                reader.position());
        List<IndexEntry> entries = parseEntries(reader, entryCount);

        int fileNumber = fileNumberFunction.apply(file);
        int fileVersion = fileVersionFunction.apply(file);
        LOGGER.trace("Parsed index file {}, version {} with {} entries, expecting {} entries", fileNumber,
                fileVersion, entries.size(), entryCount);
        return new LocalIndexFile(file, fileNumber, fileVersion, entries);
    }

    /**
     * Validate the index file header.
     *
     * @param reader The reader to read the header data from.
     *
     * @throws CascParsingException When the header is invalid.
     * @throws DataReadingException When reading the entry data failed.
     * @throws DataParsingException When parsing the entry data failed.
     */
    private void validateHeader(DataReader reader)
            throws CascParsingException, DataReadingException, DataParsingException {
        long position = reader.position();

        int headerLen = (int) reader.readNext(DataTypeFactory.getUnsignedInteger(), ByteOrder.LITTLE_ENDIAN)
                .longValue();
        int hash = reader.readNext(DataTypeFactory.getInteger(), ByteOrder.LITTLE_ENDIAN);

        byte[] data = reader.readNext(DataTypeFactory.getByteArray(headerLen));

        int h2 = JenkinsHash.hashLittle2b(data, headerLen);
        if (hash != h2) {
            throw new CascParsingException(format("Invalid index header hash %X -> %X", hash, h2));
        }

        reader.position(position);
    }

    /**
     * Read and parse the index file header.
     *
     * @param reader The reader to read the header data from.
     *
     * @return The parsed index file header.
     *
     * @throws CascParsingException When the header is invalid.
     * @throws DataReadingException When reading the entry data failed.
     * @throws DataParsingException When parsing the entry data failed.
     */
    private IndexHeaderV2 parseHeader(DataReader reader)
            throws CascParsingException, DataReadingException, DataParsingException {
        int indexVersion = reader.readNext(DataTypeFactory.getUnsignedShort(), ByteOrder.LITTLE_ENDIAN);
        if (indexVersion != 0x07) {
            throw new CascParsingException(
                    format("Invalid index file header version 0x%02X, requires 0x07", indexVersion));
        }
        byte keyIndex = reader.readNext(DataTypeFactory.getByte());
        byte extraBytes = reader.readNext(DataTypeFactory.getByte());
        byte spanSizeBytes = reader.readNext(DataTypeFactory.getByte());
        byte spanOffsetBytes = reader.readNext(DataTypeFactory.getByte());
        byte keyBytes = reader.readNext(DataTypeFactory.getByte());
        byte segmentBits = reader.readNext(DataTypeFactory.getByte());
        long maxFileOffset = reader.readNext(DataTypeFactory.getLong(), ByteOrder.BIG_ENDIAN);
        if (extraBytes != 0x00 || spanSizeBytes != 0x04 || spanOffsetBytes != 0x05 || keyBytes != 0x09) {
            throw new CascParsingException("Invalid index file header");
        }
        return new IndexHeaderV2(indexVersion, keyIndex, extraBytes, spanSizeBytes, spanOffsetBytes, keyBytes,
                segmentBits, maxFileOffset);
    }

    /**
     * Read an parse all available {@link IndexEntry} instances.
     *
     * @param reader     The reader to read the entry data from.
     * @param entryCount The number of entries avialable.
     *
     * @return The parsed entries.
     *
     * @throws DataReadingException When reading the entry data failed.
     * @throws DataParsingException When parsing the entry data failed.
     */
    private List<IndexEntry> parseEntries(DataReader reader, int entryCount)
            throws DataReadingException, DataParsingException {
        Set<Checksum> keys = new HashSet<>();
        List<IndexEntry> entries = new ArrayList<>();
        for (int i = 0; i < entryCount; i++) {
            IndexEntry entry = parseEntry(reader);
            if (entry != null && entry.getFileKey() != null && keys.add(entry.getFileKey())) {
                entries.add(entry);
            } else if (entry != null && entry.getFileKey() != null) {
                LOGGER.trace("Skipping index entry for duplicate file key {}", entry.getFileKey());
            }
        }
        return entries;
    }

    /**
     * Read and parse an {@link IndexEntry}.
     *
     * @param reader The reader to read the entry data from.
     *
     * @return The parsed entry.
     *
     * @throws DataReadingException When reading the entry data failed.
     * @throws DataParsingException When parsing the entry data failed.
     */
    private IndexEntry parseEntry(DataReader reader) throws DataReadingException, DataParsingException {
        byte[] fileKey = reader.readNext(DataTypeFactory.getByteArray(9));
        short indexInfoHigh = reader.readNext(DataTypeFactory.getUnsignedByte());
        long indexInfoLow = reader.readNext(DataTypeFactory.getUnsignedInteger(), ByteOrder.BIG_ENDIAN);
        long fileSize = reader.readNext(DataTypeFactory.getUnsignedInteger(), ByteOrder.LITTLE_ENDIAN);
        IndexEntry entry = new LocalIndexEntry(new FileKey(fileKey), indexInfoHigh, indexInfoLow, fileSize);
        return entry;
    }

    /**
     * Header of the index file (version 2).
     */
    private static class IndexHeaderV2 {
        /** The index version, must be 0x07 for CASCv2. */
        private final int indexVersion;
        /** The file key index. */
        private final byte keyIndex;
        /** Empty byte */
        private final byte unknown1;
        /** The size of the field with the file size in bytes. */
        private final byte spanSizeBytes;
        /** The size of the field with the file offset in bytes. */
        private final byte spanOffsetBytes;
        /** The size of the file key in bytes. */
        private final byte keyBytes;
        /** Number of bits for the file offset (rest is archive index). */
        private final byte segmentBits;
        /** The maximum file offset. */
        private final long maxFileOffset;

        /**
         * @param indexVersion    The index version.
         * @param keyIndex        The file key index.
         * @param unknown1        Unknown byte.
         * @param spanSizeBytes   The size of the field with the file size in bytes.
         * @param spanOffsetBytes The size of the field with the file offset in bytes.
         * @param keyBytes        The size of the file key in bytes.
         * @param segmentBits     The number of bits for the file offset (rest is archive index).
         * @param maxFileOffset   The maximum file offset.
         */
        public IndexHeaderV2(int indexVersion, byte keyIndex, byte unknown1, byte spanSizeBytes,
                byte spanOffsetBytes, byte keyBytes, byte segmentBits, long maxFileOffset) {
            this.indexVersion = indexVersion;
            this.keyIndex = keyIndex;
            this.unknown1 = unknown1;
            this.spanSizeBytes = spanSizeBytes;
            this.spanOffsetBytes = spanOffsetBytes;
            this.keyBytes = keyBytes;
            this.segmentBits = segmentBits;
            this.maxFileOffset = maxFileOffset;
        }

        /**
         * Get the index version.
         *
         * @return The index version.
         */
        public int getIndexVersion() {
            return indexVersion;
        }

        /**
         * Get the file key index.
         *
         * @return The file key index.
         */
        public byte getKeyIndex() {
            return keyIndex;
        }

        /**
         * Get the first unknown data.
         *
         * @return The first unknown data.
         */
        public byte getUnknown1() {
            return unknown1;
        }

        /**
         * Get the size of the field with the file size in bytes.
         *
         * @return The size of the field with the file size in bytes.
         */
        public byte getSpanSizeBytes() {
            return spanSizeBytes;
        }

        /**
         * Get the size of the field with the file offset in bytes.
         *
         * @return The size of the field with the file offset in bytes.
         */
        public byte getSpanOffsetBytes() {
            return spanOffsetBytes;
        }

        /**
         * Get the size of the file key in bytes.
         *
         * @return The size of the file key in bytes.
         */
        public byte getKeyBytes() {
            return keyBytes;
        }

        /**
         * Get the number of bits for the file offset (rest is archive index).
         *
         * @return The number of bits.
         */
        public byte getSegmentBits() {
            return segmentBits;
        }

        /**
         * Get the maximum file offset.
         *
         * @return The maximum file offset.
         */
        public long getMaxFileOffset() {
            return maxFileOffset;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String toString() {
            return ToStringBuilder.reflectionToString(this);
        }
    }
}