de.nx42.maps4cim.header.HeaderParser.java Source code

Java tutorial

Introduction

Here is the source code for de.nx42.maps4cim.header.HeaderParser.java

Source

/**
 * maps4cim - a real world map generator for CiM 2
 * Copyright 2013 - 2014 Sebastian Straub
 *
 * Licensed 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 de.nx42.maps4cim.header;

import static de.nx42.maps4cim.header.CustomHeader.formatHeaderString;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.Arrays;

import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteSource;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.nx42.maps4cim.config.header.HeaderDef.BuildingSet;
import de.nx42.maps4cim.util.DateUtils;
import de.nx42.maps4cim.util.math.KMPMatch;

public class HeaderParser {

    private static Logger log = LoggerFactory.getLogger(HeaderParser.class);

    // public accessors

    /**
     * Takes a CiM 2 map file as argument, parses the header and returns a
     * CustomHeader-object representing the relevant contents of this header.
     * Expects an existing and fully accessible CiM 2 map file.
     * @param map the map to parse
     * @return the CustomHeader-object containing the data of this header
     * @throws ParseException if there is an error parsing the header
     * @throws IOException if there is an error reading the file's contents
     */
    public static CustomHeader parse(File map) throws ParseException, IOException {
        ByteSource bs = Files.asByteSource(map);
        return parse(bs);
    }

    /**
     * Takes a byte-array representing a full CiM 2 map (or at least the full
     * header of the map), parses the header and returns a CustomHeader-object
     * representing the relevant contents of this header.
     * @param map the map to parse
     * @return the CustomHeader-object containing the data of this header
     * @throws ParseException if there is an error parsing the header
     * @throws IOException if there is an error accessing the array-contents
     * (highly unlikely, this is owed to the InputStream-abstraction that is used)
     */
    public static CustomHeader parse(byte[] map) throws ParseException, IOException {
        ByteSource bs = ByteSource.wrap(map);
        return parse(bs);
    }

    /**
     * Takes any ByteSource representing a full CiM 2 map (or at least the full
     * header of the map), parses the header and returns a CustomHeader-object
     * representing the relevant contents of this header.
     * @param source the map to parse
     * @return the CustomHeader-object containing the data of this header
     * @throws ParseException if there is an error parsing the header
     * @throws IOException if there is an error accessing the array-contents
     */
    public static CustomHeader parse(ByteSource source) throws ParseException, IOException {
        InputStream is = null;
        try {
            is = source.openBufferedStream();
            byte[] relevant = getRelevantPart(is);
            return execute(relevant);
        } finally {
            if (is != null) {
                is.close();
            }
        }
    }

    // workers

    /**
     * Reads the input stream of a file, determines the end of the relevant
     * information and copies this (rather short) subset into a new byte-array
     * for faster access. Does not close the stream.
     * @param is an open input stream to the header to parse
     * @return a copy of the relevant portion of the header as byte-array
     * @throws IOException if anything goes wrong while accessing the stream
     */
    protected static byte[] getRelevantPart(InputStream is) throws IOException {
        // read until "Editor Player" and store as byte[]
        byte[] readUntil = formatHeaderString(CustomHeader.staticString05);
        int len = KMPMatch.indexOf(is, readUntil);
        byte[] header = new byte[len];
        is.read(header, 0, len);
        return header;
    }

    /**
     * Returns the first index after the end of the header
     * @param source the stream to find the end of the header in
     * @return the end of the header / start of the body
     * @throws IOException if the stream can't be read
     */
    public static int findEndOfHeader(ByteSource source) throws IOException {
        InputStream is = null;
        try {
            is = source.openBufferedStream();
            // find "GameState+SerializableTerrainData"
            int lastBlockStart = KMPMatch.indexOf(is, CustomHeader.formatHeaderString(CustomHeader.staticString06));
            // header length: how many multiples of 4096 (+256)
            int headerMulti = lastBlockStart / 4096;
            return headerMulti * 4096 + 256;
        } finally {
            if (is != null) {
                is.close();
            }
        }
    }

    /**
     * Actually parses the relevant part of the header and writes the results
     * into a new CustomHeader-object
     * @param header a byte-array containing at least the relevant part of the
     * map's header (can be retrieved via {@link HeaderParser#getRelevantPart(InputStream)})
     * @return the CustomHeader-object containing the data of this header
     * @throws ParseException if there is an error parsing the header
     */
    protected static CustomHeader execute(byte[] header) throws ParseException {
        CustomHeader ch = CustomHeader.newEmpty();

        // read intro
        int introEnd = readToString(header, 0);
        ch.intro = Arrays.copyOfRange(header, 0, introEnd);

        // read date/times
        int dateStart = readAfterGap(header, introEnd, 3);
        int dateEnd = readAfterBytes(header, dateStart, CustomHeader.staticBinary01)
                - CustomHeader.staticBinary01.length;

        // read 64 bit integers (date/time) until end is reached
        int dateAmount = (dateEnd - dateStart) / 8;
        long[] dateTimeStamps = new long[dateAmount];
        ByteArrayDataInput bin = ByteStreams.newDataInput(header, dateStart);

        for (int i = 0; i < dateAmount; i++) {
            dateTimeStamps[i] = bin.readLong();
        }

        // interpret dates
        if (dateAmount >= 6) {
            // current format (custom map)
            ch.unusedDate1 = DateUtils.ticksToDate(dateTimeStamps[0]);
            ch.unusedDate2 = DateUtils.ticksToDate(dateTimeStamps[1]);
            ch.lastSaved = DateUtils.ticksToDate(dateTimeStamps[2]);
            ch.mapCreated = DateUtils.ticksToDate(dateTimeStamps[3]);
            ch.workTime1 = dateTimeStamps[4];
            ch.workTime2 = dateTimeStamps[5];
        } else if (dateAmount >= 4) {
            // old format (campaign map), without mapCreated and workTime2
            ch.unusedDate1 = DateUtils.ticksToDate(dateTimeStamps[0]);
            ch.unusedDate2 = DateUtils.ticksToDate(dateTimeStamps[1]);
            ch.lastSaved = ch.mapCreated = DateUtils.ticksToDate(dateTimeStamps[2]);
            ch.workTime1 = ch.workTime2 = dateTimeStamps[3];
        } else {
            // fail, just write some default values
            log.warn("Can't read date & time values: unexpected format");
            ch.unusedDate1 = ch.unusedDate2 = CustomHeader.unusedDateDefault;
            ch.lastSaved = ch.mapCreated = DateUtils.getDateUTC(2013, 1, 1, 12, 0, 0);
        }

        // read map name
        int nameStart = readAfterString(header, dateEnd, CustomHeader.staticString02);
        ch.mapName = parseHeaderString(header, nameStart);

        // get minimap PNG length (watch out for euro update strings!)
        int mapNameEnd = readAfterString(header, nameStart);
        int beginPngLen = mapNameEnd;
        int firstStringAfterMapName = readToString(header, mapNameEnd);
        if (mapNameEnd == firstStringAfterMapName) {
            beginPngLen = readAfterString(header, firstStringAfterMapName);
        }

        // read minimap image
        ch.pngLength = Arrays.copyOfRange(header, beginPngLen, beginPngLen + 3);
        int beginPng = beginPngLen + 3;
        int pngLength = int24parse(ch.pngLength);
        int pngEnd = beginPng + pngLength;
        ch.png = Arrays.copyOfRange(header, beginPng, pngEnd);

        // find out if euro building style
        int firstStringAfterPng = readToString(header, pngEnd);

        String europeIdentifier = parseHeaderString(header, firstStringAfterPng);
        ch.buildingSet = europeIdentifier.contains("cim2.europe") ? BuildingSet.EUROPEAN : BuildingSet.AMERICAN;

        return ch;
    }

    // navigate within byte[]

    /**
     * Reads to the start of the next String
     * @param header the bytes to read from
     * @param off the offset to start the search at
     * @return the index position of the first byte of the next String
     */
    protected static int readToString(byte[] header, int off) {
        int limit = header.length - 3;
        int i = off;

        while (i < limit) {
            i = readAfterGap(header, i, 2);
            if (isStringAt(header, i - 2)) {
                return i - 2;
            }
            i++;
        }

        return -1;
    }

    /**
     * Decides, if there is a String at the specified offset
     * @param arr the array to search in
     * @param off the offset to start parsing at
     * @return true, iff there is a valid String (correct format and length)
     * at the specified offset
     */
    protected static boolean isStringAt(byte[] arr, int off) {
        if (arr[off] == 0 && arr[off + 1] == 0 && arr[off + 2] != 0 && arr[off + 3] == 0) {
            // Possible String detected (starts with 0,0,len,0)
            // now check if there is char, 0, char, 0, ... for full String length
            int limit = arr.length - 3;
            int len = arr[off + 2] & 0xFF;
            int firstChar = off + 3;
            for (int i = 0; i < len; i++) {
                int cur = firstChar + i * 2;
                if (cur > limit) {
                    // Array ended before String
                    return false;
                }
                if (!(arr[cur] == 0 && arr[cur + 1] != 0)) {
                    // (char, 0, ...) sequence not satisfied
                    return false;
                }
            }
            return true;
        } else {
            return false;
        }
    }

    /**
     * Reads to the end of the next String
     * @param header the bytes to read from
     * @param off the offset to start the search at
     * @return the index position of the first byte after the next String
     */
    protected static int readAfterString(byte[] header, int off) {
        int start = readToString(header, off);
        int len = header[start + 2] & 0xFF;

        return start + len * 2 + 3;
    }

    /**
     * Reads to the end of the next String that matches the specified String
     * @param header the bytes to read from
     * @param off the offset to start the search at
     * @param s the String to match
     * @return the index position of the first byte after the specified String
     */
    protected static int readAfterString(byte[] header, int off, String s) {
        return readAfterBytes(header, off, formatHeaderString(s));
    }

    /**
     * Reads to the end of the next occurrence of the specified bytes
     * @param header the bytes to read from
     * @param off the offset to start the search at
     * @param b the bytes to match
     * @return the index position of the first byte after the specified byte-array
     */
    protected static int readAfterBytes(byte[] header, int off, byte[] b) {
        int i = KMPMatch.indexOf(header, b, off);
        if (i >= 0) {
            return i + b.length;
        } else {
            return -1;
        }
    }

    /**
     * Reads to the start of the next gap with at least the specified length.
     * @param header the bytes to read from
     * @param off the offset to start the search at
     * @param len the minimum length of the gap
     * @return the index position of the first byte of the gap
     */
    protected static int readToGap(byte[] header, int off, int len) {
        return KMPMatch.indexOf(header, new byte[len], off);
    }

    /**
     * Reads to the end of the next gap with at least the specified length.
     * Will always read to the first non-zero byte.
     * @param header the bytes to read from
     * @param off the offset to start the search at
     * @param len the minimum length of the gap
     * @return the index position of the first non-zero byte after the specified gap
     */
    protected static int readAfterGap(byte[] header, int off, int len) {
        int i = readToGap(header, off, len) + len;
        if (i < 0) {
            return i;
        }

        while (i < header.length && header[i] == 0) {
            i++;
        }
        return i;
    }

    // parse datatypes

    /**
     * Converts a int24, stored in 3 bytes, into a default java int32
     * @param int24 the 24bit-integer to convert
     * @return 32bit representation of the int24 as primitive java int
     */
    protected static int int24parse(byte[] int24) {
        return int24[2] & 0xFF | (int24[1] & 0xFF) << 8 | (int24[0] & 0xFF) << 16;
    }

    // parse string that begins at offset, detect end automatically

    /**
     * Converts a String from the binary format that is used in the map format
     * back to a native java String. Reads from the specified offset to the
     * end of the String (String length is part of the binary String format).
     * This is basically the reverse function to
     * {@link CustomHeader#formatHeaderString(CharSequence)}.
     * @param b the byte-array that contains the desired string
     * @param off the 0-based offset where the String begins
     * @return the String which starts at the specified offset
     */
    protected static String parseHeaderString(byte[] b, int off) {
        int len = b[off + 2] & 0xFF;
        StringBuilder sb = new StringBuilder(len);
        for (int i = 0; i < len; i++) {
            sb.append((char) b[off + 4 + i * 2]);
        }
        return sb.toString();
    }

    /**
     *  Converts a String from the binary format that is used in the map format
     * back to a native java String. Reads from the start of the array to the
     * end of the String (not the array!)
     * @param b the byte-array that contains the desired string
     * @return the String which is represented by the specified array
     */
    protected static String parseHeaderString(byte[] b) {
        return parseHeaderString(b, 0);
    }

}