/* ** 2013 April 20 ** ** The author disclaims copyright to this source code. In place of ** a legal notice, here is a blessing: ** May you do good and not evil. ** May you find forgiveness for yourself and forgive others. ** May you share freely, never taking more than you give. */ package info.ata4.vpk; import info.ata4.util.io.NIOFileUtils; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.zip.CRC32; import org.apache.commons.io.FilenameUtils; /** * * @author Nico Bergemann <barracuda415 at yahoo.de> */ public class VPKEntry { private final File vpkFile; private ByteBuffer bb; private String type; private String name; private String dir; private long crc32; private int offset; private int size; private byte[] preload = new byte[0]; private boolean readOnly; VPKEntry(File vpkFile, boolean readOnly) { this.vpkFile = vpkFile; this.readOnly = readOnly; } /** * Returns the VPK archive file for this entry. * * @return VPK archive file */ public File getFile() { return vpkFile; } /** * Creates and returs a byte buffer for this entry. * * @return byte buffer containing the data of this entry * @throws IOException If the buffer creation caused an error */ public ByteBuffer getData() throws IOException { // if there's nothing defined, simply return an empty buffer if (size == 0 && preload.length == 0) { return ByteBuffer.allocate(0); } // don't create another buffer if a previous one was created if (bb != null && bb.capacity() == size) { return bb.duplicate(); } // return the preloaded data directly if there's no actual size for the // file if (size == 0 && preload.length > 0) { return ByteBuffer.wrap(preload); } if (size == 0 && preload.length > 0) { // data is fully preloaded, simply wrap the array bb = ByteBuffer.wrap(preload); } else if (size > 0 && preload.length > 0) { // concat preloaded and external data bb = ByteBuffer.allocateDirect(getDataSize()); bb.put(preload); NIOFileUtils.load(vpkFile, offset, size, bb); } else if (readOnly) { if (vpkFile.exists()) { // map the file directly bb = NIOFileUtils.openReadOnly(vpkFile, offset, size); } else { // can't create files in read-only mode throw new FileNotFoundException(); } } else { bb = NIOFileUtils.openReadWrite(vpkFile, offset, size); } bb.order(ByteOrder.LITTLE_ENDIAN); bb.rewind(); return bb.duplicate(); } /** * Checks the data integrity by comparing the saved CRC32 checksum with the * actual checksum. If no exception is thrown, the data is OK. * * @throws IOException on I/O errors or if the checksum mismatches */ public void checkData() throws IOException { long targetCrc = getCRC32(); long actualCrc = calcCRC32(); if (actualCrc != targetCrc) { throw new IOException(String.format("CRC32 checksum mismatch: got 0x%06x, expected 0x%06x", actualCrc, targetCrc)); } } /** * Calculates the actual CRC32 checksum for the data. * * @return actual calculated CRC32 checksum * @throws IOException If the checksum calculation caused an I/O error */ public long calcCRC32() throws IOException { CRC32 crc = new CRC32(); byte[] buf = new byte[4096]; ByteBuffer bbCheck = getData(); while (bbCheck.hasRemaining()) { int bsize = Math.min(buf.length, bbCheck.remaining()); bbCheck.get(buf, 0, bsize); crc.update(buf, 0, bsize); } return crc.getValue(); } /** * Updates the current target checksum from the current data. * * @throws IOException If the checksum calculation caused an I/O error */ void updateCRC32() throws IOException { setCRC32(calcCRC32()); } /** * Returns the file extension for this entry. * * @return file extension */ public String getType() { return type; } /** * Sets a new file extension for this entry. * * @param type new file extension */ void setType(String type) { this.type = type; } /** * Returns the name of this entry without its extension. * * @return entry name */ public String getName() { return name; } /** * Sets a new name for this entry. * * @param name new entry name */ void setName(String name) { this.name = name; } /** * Returns the directory path for this entry. * * @return directory path */ public String getDir() { return dir; } /** * Sets a new directory path for this entry. * * @param dir directory path */ void setDir(String dir) { this.dir = dir; } /** * Returns the target CRC32 checksum for this entry. * * @return CRC32 checksum */ public long getCRC32() { return crc32; } /** * Sets a new target CRC32 checksum for this entry. * * @param crc32 CRC32 checksum */ void setCRC32(long crc32) { this.crc32 = crc32; } /** * Returns the preload data for this entry. * * @return preload data array */ byte[] getPreloadData() { return preload; } /** * Sets the preload data for this entry. It should not be included with the * actual data buffer and no greater than 20k bytes. * * @param preload preload data array */ void setPreloadData(byte[] preload) { if (preload == null) { throw new NullPointerException(); } this.preload = preload; } /** * Returns the array size for the preload data. * * @return preload size in bytes */ public int getPreloadSize() { return preload.length; } /** * Allocates the preload data array to the specified size. * * @param preloadSize preload data size in bytes */ void setPreloadSize(int preloadSize) { preload = new byte[preloadSize]; } /** * Returns the data offset in the associated file for this entry. * * @return file offset */ public int getOffset() { return offset; } /** * Sets the data offset in the associated file for this entry. * * @param offset new file offset */ void setOffset(int offset) { this.offset = offset; } /** * Returns the external archive data size for this entry. * * @return data size */ public int getSize() { return size; } /** * Sets the new external archive data size for this entry. * * @param size new data size */ void setSize(int size) { this.size = size; } /** * Returns the full data size for this entry. It includes the size of the * preloaded data and the archive data. * * @return full data size */ public int getDataSize() { return size + preload.length; } /** * Returns the full path of this entry. * * @return entry path */ public String getPath() { return getDir() + getName() + "." + getType(); } /** * Sets the new full path for this entry. * * @param path new entry path */ void setPath(String path) { setType(FilenameUtils.getExtension(path)); setName(FilenameUtils.getBaseName(path)); setDir(FilenameUtils.getPath(path)); } public String toString(){ return this.name + "." + this.type; } }