// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.resource.key; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import org.infinity.resource.Writeable; import org.infinity.resource.Profile; import org.infinity.util.io.FileManager; import org.infinity.util.io.StreamUtils; /** * Provides information about the location of resource data within BIFF archives. */ public class BIFFEntry implements Writeable, Comparable<BIFFEntry> { // Location: Indicates where file might be found // Bit 0: Root folder (where the KEY file is located) // Bit 1: Cache directory // Bit 2: ??? (CD1 directory?) // Bit 3: CD2 directory // Bit 4: CD3 directory // Bit 5: CD4 directory // Bit 6: CD5 directory // Bit 7: CD6 directory // Bit 8: ??? (CD7 directory?) private int location; // supposed location of BIFF file private Path keyFile; // Full path to KEY file containing BIFF entry private Path biffFile; // Full path to BIFF file if available private String fileName; // Raw path to BIFF file as defined in KEY file private int index; // BIFF entry index in KEY file private int fileSize; // Resource size in bytes private int stringOffset; // Offset to BIFF filename in KEY file private char separatorChar; // path separator used to assemble BIFF path /** * Constructs a new BIFF entry. * @param keyFile The associated key file. * @param fileName Name and relative path to the BIFF file */ public BIFFEntry(Path keyFile, String fileName) { if (fileName == null) { throw new NullPointerException(); } this.separatorChar = '/'; this.fileName = fileName.replaceAll("[\\:]", "/"); // (normalized) relative path this.location = 1; // put into root folder this.index = -1; // not yet associated with KEY file } public BIFFEntry(Path keyFile, int index, ByteBuffer buffer, int offset) { updateBIFF(keyFile, index, buffer, offset); } //--------------------- Begin Interface Comparable --------------------- @Override public int compareTo(BIFFEntry o) { return fileName.compareTo(o.fileName); } //--------------------- End Interface Comparable --------------------- //--------------------- Begin Interface Writeable --------------------- @Override public void write(OutputStream os) throws IOException { StreamUtils.writeInt(os, fileSize); StreamUtils.writeInt(os, stringOffset); StreamUtils.writeShort(os, getFileNameLength()); StreamUtils.writeShort(os, (short)location); } //--------------------- End Interface Writeable --------------------- @Override public boolean equals(Object o) { if (this == o) { return true; } else if (o instanceof BIFFEntry) { BIFFEntry other = (BIFFEntry)o; boolean bRet = (keyFile == null && other.keyFile == null) || (keyFile != null && keyFile.equals(other.keyFile)); bRet &= (biffFile == null && other.biffFile == null) || (biffFile != null && biffFile.equals(other.biffFile)); bRet &= (fileSize == other.fileSize) && (stringOffset == other.stringOffset); return bRet; } else { return false; } } @Override public String toString() { return fileName; } /** Returns the KEY file containing this BIFF archive. */ public Path getKeyFile() { return keyFile; } /** Returns the relative file path to the BIFF file. */ public String getFileName() { return fileName; } /** Returns whether the referenced BIFF file exists in the game. */ public boolean exists() { return (biffFile != null && Files.isRegularFile(biffFile)); } /** Returns the absolute path to the BIFF file if it exists. */ public Path getPath() { return biffFile; } public int getIndex() { return index; } void setIndex(int newIndex) { this.index = newIndex; } /** * Returns the relative BIFF file path as found in the KEY file. * @param normalized Specify {@code true} to return the filename with default path separator * {@code '/'} or {@code false} to return the filename with the original * path separators. */ public String getFileName(boolean normalized) { if (normalized || separatorChar == '/') { return fileName; } else { return fileName.replace('/', separatorChar); } } public short getFileNameLength() { return (short)(fileName.length() + 1); } public int getFileSize() { return fileSize; } public void setFileSize(int fileSize) { this.fileSize = fileSize; } /** * Replaces the current data by the specified data. * @param keyFile KEY file containing this BIFF archive. * @param index BIFF entry index in KEY file. * @param buffer Buffered KEY file. * @param offset Start offset of BIFF entry data in KEY file. */ public void updateBIFF(Path keyFile, int index, ByteBuffer buffer, int offset) { if (keyFile == null || buffer == null) { throw new NullPointerException(); } this.keyFile = keyFile.toAbsolutePath(); this.index = index; this.fileSize = buffer.getInt(offset); this.stringOffset = buffer.getInt(offset + 4); short stringLength = buffer.getShort(offset + 8); this.location = buffer.getShort(offset + 10) & 0xffff; this.fileName = StreamUtils.readString(buffer, this.stringOffset, stringLength - 1); if (this.fileName.charAt(0) == '\\') { this.fileName = this.fileName.substring(1); } if (this.fileName.indexOf('\\') > 0) { this.separatorChar = '\\'; } else if (this.fileName.indexOf(':') > 0) { this.separatorChar = ':'; } else { this.separatorChar = '/'; } this.fileName = this.fileName.replace(this.separatorChar, '/'); this.biffFile = findBiffFile(this.keyFile.getParent(), this.location, this.fileName); } public int updateOffset(int newOffset) { this.stringOffset = newOffset; return getFileNameLength(); } public void writeString(OutputStream os) throws IOException { StreamUtils.writeString(os, getFileName(false), getFileNameLength()); } // Searches for the specified BIFF file based on root private static Path findBiffFile(Path root, int location, String fileName) { Path retVal = null; if (root != null && fileName != null) { List<Path> biffFolders = Profile.getProperty(Profile.Key.GET_GAME_BIFF_FOLDERS); if (biffFolders == null) { biffFolders = new ArrayList<>(); } else { // remove non-matching biff folder paths for (int idx = biffFolders.size() - 1; idx >= 0; idx--) { try { // if (!biffFolders.get(idx).getFileSystem().equals(root.getFileSystem())) { if (!biffFolders.get(idx).startsWith(root)) { biffFolders.remove(idx); } } catch (Throwable t) { biffFolders.remove(idx); } } } if (biffFolders.isEmpty()) { final String[] baseFolders = { "", "cache", "cd1", "cd2", "cd3", "cd4", "cd5", "cd6", "cd7", "cdall" }; for (final String folderName: baseFolders) { Path path = FileManager.resolve(root.resolve(folderName)); if (Files.isDirectory(path)) { biffFolders.add(path); } } } // Note: BIFF file may have extension ".cbf" String[] fileNames = { fileName, StreamUtils.replaceFileExtension(fileName, "cbf") }; for (final Path path: biffFolders) { for (final String biffName: fileNames) { retVal = FileManager.queryExisting(path, biffName); if (retVal != null) { break; } } if (retVal != null) { break; } } } return retVal; } }