Key.java
// <editor-fold defaultstate="collapsed" desc="license">
/*
* Copyright (c) 2009, Karl H. Beckers
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of the <ORGANIZATION> nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
**/
// </editor-fold>
package net.jarre_de_the.griffin.file;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import net.jarre_de_the.griffin.Util;
import net.jarre_de_the.griffin.types.data.DWordData;
import net.jarre_de_the.griffin.types.data.WordData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Key File
* <p>
* TODO: the header fields sometimes have getters/setters which use int ...
* which is too narrow
*
* @author charly4711
*/
public class Key extends PersistableNwnFile {
public static final String DEFAULT_FILE_TYPE = "KEY ";
public static final String DEFAULT_FILE_VERSION = "V1 ";
private static final Logger LOGGER = LoggerFactory.getLogger(Key.class);
private static final int YEAR_ZERO = 1900;
private List<FileEntry> files = new ArrayList<FileEntry>();
private List<KeyEntry> keys = new ArrayList<KeyEntry>();
public Key() {
setFileType(DEFAULT_FILE_TYPE);
setFileVersion(DEFAULT_FILE_VERSION);
}
/**
* Contructor to read a Key file from disk.
* <p>
* @param in File on disk.
* @throws IOException
*/
public Key(File in)
throws IOException {
parseFile(in);
}
private void parseFile(File inFile)
throws IOException {
try (RandomAccessFile in = new RandomAccessFile(inFile, "r")) {
// set the header fields
for (KeyHeaderField f : KeyHeaderField.values()) {
DWordData hf = DWordData.read(in);
getHeaderFields().put(f.field(), hf);
}
// go to the file table
in.seek(getFileTableOff());
for (int i = 0; i < getBifCount(); i++) {
files.add(new FileEntry(in));
}
// go to the key table
in.seek(getKeyTableOff());
for (int i = 0; i < getKeyCount(); i++) {
keys.add(new KeyEntry(in));
}
}
}
@Override
public byte[] persist() throws IOException {
ByteArrayOutputStream fileArray = new ByteArrayOutputStream();
ByteArrayOutputStream fileNameArray = new ByteArrayOutputStream();
ByteArrayOutputStream keyArray = new ByteArrayOutputStream();
DWordData[] header = new DWordData[KeyHeaderField.values().length];
// file type
DWordData hf = getHeaderFields().get(KeyHeaderField.FILE_TYPE.field());
if (hf == null) {
hf = new DWordData(Util.stringToBitField(DEFAULT_FILE_TYPE));
}
header[KeyHeaderField.FILE_TYPE.ordinal()] = hf;
LOGGER.debug("ft: ");
for (byte b : hf.getValueAsByteArray()) {
char c = (char) b;
LOGGER.debug("" + c);
}
LOGGER.debug("");
// file version
hf = getHeaderFields().get(KeyHeaderField.FILE_VERSION.field());
if (hf == null) {
hf = new DWordData(Util.stringToBitField(DEFAULT_FILE_VERSION));
}
header[KeyHeaderField.FILE_VERSION.ordinal()] = hf;
LOGGER.debug("fv: ");
for (byte b : hf.getValueAsByteArray()) {
char c = (char) b;
LOGGER.debug("" + c);
}
LOGGER.debug("");
header[KeyHeaderField.BIF_COUNT.ordinal()] = new DWordData(files.size());
header[KeyHeaderField.KEY_COUNT.ordinal()] = new DWordData(keys.size());
header[KeyHeaderField.FILE_TABLE_OFFSET.ordinal()] = new DWordData(64);
int fileNameOffset = 64 + (files.size() * FileEntry.LENGTH);
for (FileEntry fe : files) {
fileArray.write(new DWordData((int) (fe.getFileSize() & 0xFFFFFFFF)).getValueAsByteArray());
fileArray.write(new DWordData(fileNameArray.size() + fileNameOffset).getValueAsByteArray());
fileArray.write(new WordData((short) fe.getFileName().length()).getValueAsByteArray());
fileArray.write(new WordData((short) fe.getDrive()).getValueAsByteArray());
fileNameArray.write(fe.getFileName().getBytes(Util.CHARSET_US_ASCII));
fileNameArray.write((byte) 0);
}
header[KeyHeaderField.KEY_TABLE_OFFSET.ordinal()] = new DWordData(64 + fileArray.size() + fileNameArray.size());
for (KeyEntry key : keys) {
String resRefString = key.getResRef();
byte[] resRef = new byte[KeyEntry.RES_REF_LENGTH];
System.arraycopy(resRefString.getBytes(Util.CHARSET_US_ASCII), 0,
resRef, 0, Math.min(resRefString.length(), resRef.length));
keyArray.write(resRef);
keyArray.write(new WordData((short) key.getResourceType().id()).getValueAsByteArray());
// only variable resources for now
int resId = key.getBif() << 20;
resId |= key.bifRessourceTblIdx;
keyArray.write(new DWordData(resId).getValueAsByteArray());
}
header[KeyHeaderField.KEY_TABLE_OFFSET.ordinal()] = new DWordData(64 + fileArray.size() + fileNameArray.size());
header[KeyHeaderField.BUILD_YEAR.ordinal()] = new DWordData(getBuildYear() - YEAR_ZERO);
header[KeyHeaderField.BUILD_DAY.ordinal()] = new DWordData(getBuildDay());
header[KeyHeaderField.RESERVED1.ordinal()] = getHeaderFields().get(KeyHeaderField.RESERVED1.name());
header[KeyHeaderField.RESERVED2.ordinal()] = getHeaderFields().get(KeyHeaderField.RESERVED2.name());
header[KeyHeaderField.RESERVED3.ordinal()] = getHeaderFields().get(KeyHeaderField.RESERVED3.name());
header[KeyHeaderField.RESERVED4.ordinal()] = getHeaderFields().get(KeyHeaderField.RESERVED4.name());
header[KeyHeaderField.RESERVED5.ordinal()] = getHeaderFields().get(KeyHeaderField.RESERVED5.name());
header[KeyHeaderField.RESERVED6.ordinal()] = getHeaderFields().get(KeyHeaderField.RESERVED6.name());
header[KeyHeaderField.RESERVED7.ordinal()] = getHeaderFields().get(KeyHeaderField.RESERVED7.name());
header[KeyHeaderField.RESERVED8.ordinal()] = getHeaderFields().get(KeyHeaderField.RESERVED8.name());
ByteArrayOutputStream out = new ByteArrayOutputStream();
for (DWordData d : header) {
if (null != d) {
out.write(d.getValueAsByteArray());
} else {
out.write(new DWordData(0).getValueAsByteArray());
}
}
out.write(fileArray.toByteArray());
out.write(fileNameArray.toByteArray());
out.write(keyArray.toByteArray());
return out.toByteArray();
}
public KeyEntry findResource(String resRef) {
KeyEntry result = null;
for (KeyEntry ke : getKeyEntries()) {
if (ke.getResRef().equals(resRef)) {
result = ke;
}
}
return result;
}
@Override
public Key clone() throws CloneNotSupportedException {
Key clone = (Key) super.clone();
clone.setFileEntries(new ArrayList<FileEntry>());
clone.setKeyEntries(new ArrayList<KeyEntry>());
clone.setFileType(getFileType());
clone.setFileVersion(getFileVersion());
clone.setFileTableOff(getFileTableOff());
clone.setBuildYear(getBuildYear());
clone.setBuildDay(getBuildDay());
clone.setBifCount(getBifCount());
clone.setKeyCount(getKeyCount());
clone.setKeyTableOff(getKeyTableOff());
if (null != getKeyEntries()) {
for (Key.KeyEntry k : getKeyEntries()) {
clone.getKeyEntries().add(k.clone());
}
} else {
clone.setKeyEntries(null);
}
if (null != getFileEntries()) {
for (Key.FileEntry f : getFileEntries()) {
clone.getFileEntries().add(f.clone());
}
} else {
clone.setFileEntries(null);
}
return clone;
}
@Override
public boolean equals(Object compare) {
if (compare == this) {
return true;
}
if (!super.equals(compare)) {
return false;
}
if (!(compare instanceof Key)) {
return false;
}
Key file = (Key) compare;
return areNullablePropertiesEqual(this.getFileEntries(), file.getFileEntries())
&& areNullablePropertiesEqual(this.getKeyEntries(), file.getKeyEntries())
&& this.getBifCount() == file.getBifCount()
&& this.getBuildDay() == file.getBuildDay()
&& this.getBuildYear() == file.getBuildYear()
// && this.getFileTableOff() == file.getFileTableOff()
&& this.getKeyCount() == file.getKeyCount();
// && this.getKeyTableOff() == file.getKeyTableOff();
}
@Override
public int hashCode() {
int hash = super.hashCode();
hash = 41 * hash + Objects.hashCode(this.files);
return hash;
}
protected enum KeyHeaderField {
FILE_TYPE(CommonHeaderField.FILE_TYPE.field()),
FILE_VERSION(CommonHeaderField.FILE_VERSION.field()),
BIF_COUNT("bifCount"),
KEY_COUNT("keyCount"),
FILE_TABLE_OFFSET("fileTableOff"),
KEY_TABLE_OFFSET("keyTableOff"),
BUILD_YEAR("buildYear"),
BUILD_DAY("buildDay"),
RESERVED1("RESERVED1"),
RESERVED2("RESERVED2"),
RESERVED3("RESERVED3"),
RESERVED4("RESERVED4"),
RESERVED5("RESERVED5"),
RESERVED6("RESERVED6"),
RESERVED7("RESERVED7"),
RESERVED8("RESERVED8");
private final String field;
private KeyHeaderField(String field) {
this.field = field;
}
public String field() {
return field;
}
}
public long getBifCount() {
return getHeaderNumber(KeyHeaderField.BIF_COUNT.field(), getFileEntries());
}
private void setBifCount(long bifCount) {
getHeaderFields().put(KeyHeaderField.BIF_COUNT.field(),
new DWordData(bifCount));
}
public long getKeyCount() {
return getHeaderNumber(KeyHeaderField.KEY_COUNT.field(), getKeyEntries());
}
private void setKeyCount(long keyCount) {
getHeaderFields().put(KeyHeaderField.KEY_COUNT.field(),
new DWordData(keyCount));
}
public long getFileTableOff() {
return getHeaderNumber(KeyHeaderField.FILE_TABLE_OFFSET.field(), null);
}
private void setFileTableOff(long fileTableOff) {
getHeaderFields().put(KeyHeaderField.FILE_TABLE_OFFSET.field(),
new DWordData(fileTableOff));
}
public long getKeyTableOff() {
return getHeaderNumber(KeyHeaderField.KEY_TABLE_OFFSET.field(), null);
}
private void setKeyTableOff(long keyTableOff) {
getHeaderFields().put(KeyHeaderField.KEY_TABLE_OFFSET.field(),
new DWordData(keyTableOff));
}
public long getBuildYear() {
return getHeaderNumber(KeyHeaderField.BUILD_YEAR.field(), null) + YEAR_ZERO;
}
public void setBuildYear(long buildYear) {
getHeaderFields().put(KeyHeaderField.BUILD_YEAR.field(),
new DWordData(buildYear - YEAR_ZERO));
}
public long getBuildDay() {
return getHeaderNumber(KeyHeaderField.BUILD_DAY.field(), null);
}
public void setBuildDay(long labelCount) {
getHeaderFields().put(KeyHeaderField.BUILD_DAY.field(),
new DWordData(labelCount));
}
public List<FileEntry> getFileEntries() {
return files;
}
public void setFileEntries(List<FileEntry> fileEntries) {
files = fileEntries;
}
public List<KeyEntry> getKeyEntries() {
return keys;
}
public void setKeyEntries(List<KeyEntry> keyEntries) {
keys = keyEntries;
}
/**
* Key files don't have the same kind of fields GFF files have. Keep the
* entries here till we need them elsewhere.
*/
public static class FileEntry implements Cloneable {
private long fileSize = 0;
private int drive = 0;
private String fileName = "";
public static final int LENGTH = (DWordData.LENGTH * 2) + (WordData.LENGTH * 2);
public FileEntry() {
}
public FileEntry(RandomAccessFile in) throws IOException {
DWordData fs = DWordData.read(in);
fileSize = fs.getValueAsNumber();
DWordData fileNameOffset = DWordData.read(in);
WordData fileNameSize = WordData.read(in);
drive = WordData.read(in).getValueAsNumber();
// remember where we are
long pos = in.getFilePointer();
// then go to the file name
long lPos = fileNameOffset.getValueAsNumber();
in.seek(lPos);
byte[] buf = new byte[fileNameSize.getValueAsNumber()];
// and read the file name
in.readFully(buf);
// set it
fileName = Util.getNullTerminatedString(buf);
LOGGER.debug("Reading filename: \"" + fileName);
// go back to the previous position
in.seek(pos);
}
public long getFileSize() {
return fileSize;
}
public void setFileSize(long fileSize) {
this.fileSize = fileSize;
}
/**
* A number that represents which drives the BIF file is located in.
* Currently each bit represents a drive letter. e.g., bit 0 = HD0,
* which is the directory where the BioWare application was installed.
*/
public int getDrive() {
return drive;
}
public void setDrive(int drive) {
this.drive = drive;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
/**
* Overrides the {@code java.lang.Object} method to ensure we always get
* back an instance of {@code FileEntry} rather than just
* {@code Object}.
* <p>
* @return A deep copy of this object.
* @throws java.lang.CloneNotSupportedException
*/
@Override
public FileEntry clone() throws CloneNotSupportedException {
LOGGER.debug("Cloning object");
FileEntry copy = (FileEntry) super.clone();
copy.setDrive(this.getDrive());
copy.setFileName(this.getFileName());
copy.setFileSize(this.getFileSize());
return copy;
}
@Override
public boolean equals(Object compare) {
if (compare == this) {
return true;
}
if (!(compare instanceof FileEntry)) {
return false;
}
FileEntry fe = (FileEntry) compare;
if (fe.getDrive() == this.getDrive()
&& fe.getFileName().equals(this.getFileName())
&& fe.getFileSize() == this.getFileSize()) {
return true;
}
return false;
}
@Override
public int hashCode() {
int code = (int) this.getFileSize();
code = code << 16;
code |= (this.getDrive() | this.getFileName().length());
return code;
}
@Override
public String toString() {
return this.getClass().getSimpleName() + ": " + this.getFileName();
}
}
public static class KeyEntry implements Cloneable {
private String resRef = "";
private ResourceType resourceType = ResourceType.GFF;
private int bifRessourceTblIdx = 0;
private int bif = 0;
public static final int RES_REF_LENGTH = 16;
public static final int LENGTH = DWordData.LENGTH + WordData.LENGTH + RES_REF_LENGTH;
public KeyEntry() {
}
public KeyEntry(RandomAccessFile in) throws IOException {
byte[] buf = new byte[RES_REF_LENGTH];
// and read the res ref
in.readFully(buf);
// set it
resRef = Util.getNullTerminatedString(buf);
// the following is just the implementation for variable resources
// as per Bioware that's all that's implemented, right now
// TODO: add fixed resources
resourceType = ResourceType.getResourceTypeById(WordData.read(in).getValueAsNumber());
DWordData resId = DWordData.read(in);
int bf = resId.getValue();
int mask = Integer.parseInt("11111111111111111111", 2);
bifRessourceTblIdx = bf & mask;
bif = bf >>> 20;
}
public String getResRef() {
return resRef;
}
public void setResRef(String resRef) {
this.resRef = resRef;
}
public ResourceType getResourceType() {
return resourceType;
}
public void setResourceType(ResourceType resourceType) {
this.resourceType = resourceType;
}
public int getBifRessourceTblIdx() {
return bifRessourceTblIdx;
}
public void setBifRessourceTblIdx(int bifRessourceTblIdx) {
this.bifRessourceTblIdx = bifRessourceTblIdx;
}
public int getBif() {
return bif;
}
public void setBif(int bif) {
this.bif = bif;
}
/**
* Overrides the {@code java.lang.Object} method to ensure we always get
* back an instance of {@code FileEntry} rather than just
* {@code Object}.
* <p>
* @return A deep copy of this object.
* @throws java.lang.CloneNotSupportedException
*/
@Override
public KeyEntry clone() throws CloneNotSupportedException {
LOGGER.debug("Cloning object");
KeyEntry copy = (KeyEntry) super.clone();
copy.setBif(this.getBif());
copy.setResourceType(this.getResourceType());
copy.setResRef(this.getResRef());
copy.setBifRessourceTblIdx(this.getBifRessourceTblIdx());
return copy;
}
@Override
public boolean equals(Object compare) {
if (compare == this) {
return true;
}
if (!(compare instanceof KeyEntry)) {
return false;
}
KeyEntry ke = (KeyEntry) compare;
if (ke.getBif() == this.getBif()
&& ke.getResRef().equals(this.getResRef())
&& ke.getBifRessourceTblIdx() == this.getBifRessourceTblIdx()
&& ke.getResourceType().equals(this.getResourceType())) {
return true;
}
return false;
}
@Override
public int hashCode() {
int resId = getBif() << 20;
resId |= getBifRessourceTblIdx();
return resId;
}
@Override
public String toString() {
return this.getClass().getSimpleName() + ": " + this.resRef
+ "." + this.getResourceType().extension();
}
}
}