package mwt.wow.mpq; import java.io.ByteArrayInputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Scanner; import java.util.regex.Pattern; public class MpqArchive implements ReadMpqArchive, ReadWriteMpqArchive { private static final String MPQ_SIGNATURE = "MPQ\032"; private final File file; int archiveOffset; private String signature; private int headerSize; private long archiveSize; private int formatVersion; int sectorSizeShift; private int unknown1; private long hashTableOffset; private long blockTableOffset; private int hashTableEntries; private int blockTableEntries; private long extBlockTableOffset; private Integer extAttrsVersion; private Integer extAttrsPresent; private final List<BlockTableEntry> blockTable = new ArrayList<BlockTableEntry>(); private HashTableEntry[] hashTable; RandomAccessFile randomAccessFile; private final boolean rw; static final int[] cryptTable; static { cryptTable = new int[0x500]; int seed = 0x00100001; int index1; int index2; int i; for (index1 = 0; index1 < 0x100; index1++) { for (index2 = index1, i = 0; i < 5; i++, index2 += 0x100) { int temp1, temp2; seed = (seed * 125 + 3) % 0x2aaaab; temp1 = (seed & 0xffff) << 0x10; seed = (seed * 125 + 3) % 0x2aaaab; temp2 = (seed & 0xffff); cryptTable[index2] = temp1 | temp2; } } } public MpqArchive(File file) throws IOException { this(file, false); } public MpqArchive(File file, boolean rw) throws IOException { this.file = file; this.rw = rw; openAndRead(); } public File getArchiveFile() { return file; } public void close() throws IOException { if (randomAccessFile != null) { randomAccessFile.close(); randomAccessFile = null; } } @Override protected void finalize() throws Throwable { super.finalize(); if (randomAccessFile != null) { System.err.println("MpqArchive was not closed (finalizer)"); close(); } } private int hashString(String string, int hashType) { int seed1 = 0x7fed7fed; int seed2 = 0xeeeeeeee; string = string.toUpperCase(); for (int i = 0; i < string.length(); i++) { int ch = string.charAt(i); seed1 = cryptTable[hashType * 0x100 + ch] ^ (seed1 + seed2); seed2 = ch + seed1 + seed2 + (seed2 << 5) + 3; } return seed1; } private void openAndRead() throws IOException { randomAccessFile = new RandomAccessFile(file, rw ? "rw" : "r"); archiveOffset = 0; if (randomAccessFile.length() == 0) { writeHeader(); } findArchiveOffset(); readHeader(); readBlockTable(); readHashTable(); } int getSectorSize() { return 512 * (1 << sectorSizeShift); } private void writeHeader() throws IOException { checkReadWrite(); if (signature == null) { // new file signature = MPQ_SIGNATURE; headerSize = 44; // ? archiveSize = 0; // ? formatVersion = 1; sectorSizeShift = 3; unknown1 = 0; hashTableOffset = 0; blockTableOffset = 0; hashTableEntries = 0; blockTableEntries = 0; extBlockTableOffset = 0; } FileWriter fileWriter = new FileWriter(randomAccessFile, archiveOffset); fileWriter.writeChar4(signature); fileWriter.writeInt32(headerSize); fileWriter.writeInt32((int) archiveSize); fileWriter.writeInt16(formatVersion); fileWriter.writeInt8(sectorSizeShift); fileWriter.writeInt8(unknown1); fileWriter.writeInt32((int) hashTableOffset); fileWriter.writeInt32((int) blockTableOffset); fileWriter.writeInt32(hashTableEntries); fileWriter.writeInt32(blockTableEntries); if (formatVersion >= 1) { fileWriter.writeInt32((int) extBlockTableOffset); fileWriter.writeInt32((int) (extBlockTableOffset >>> 32)); fileWriter.writeInt16((int) (hashTableOffset >>> 32)); fileWriter.writeInt16((int) (blockTableOffset >>> 32)); } fileWriter.close(); } private void checkReadWrite() { if (!rw) { throw new IllegalStateException("read only"); } } private void writeBlockTable() throws IOException { checkReadWrite(); assert blockTableEntries == blockTable.size(); if (formatVersion >= 1 && extBlockTableOffset != 0) { FileWriter extBlockTableWriter = new FileWriter(randomAccessFile, archiveOffset + extBlockTableOffset); for (BlockTableEntry entry : blockTable) { extBlockTableWriter.writeInt16((int) (entry.getBlockOffset() >>> 32)); } } FileWriter fileWriter = new FileWriter(randomAccessFile, archiveOffset + blockTableOffset); CryptWriter blockTableWriter = new CryptWriter(fileWriter, blockTableEntries * 16, hashString("(block table)", 3)); for (BlockTableEntry entry : blockTable) { blockTableWriter.writeInt32((int) entry.getBlockOffset()); blockTableWriter.writeInt32(entry.getBlockSize()); blockTableWriter.writeInt32(entry.getFileSize()); blockTableWriter.writeInt32(entry.getFlags()); } } private void writeHashTable() throws IOException { checkReadWrite(); FileWriter fileWriter = new FileWriter(randomAccessFile, archiveOffset + hashTableOffset); CryptWriter cryptWriter = new CryptWriter(fileWriter, hashTableEntries * 16, hashString("(hash table)", 3)); for (int i = 0; i < hashTableEntries; i++) { if (hashTable[i] == null) { hashTable[i] = new HashTableEntry(-1, -1, -1, -1, -1, -1); } cryptWriter.writeInt32(hashTable[i].getFilePathHashA()); cryptWriter.writeInt32(hashTable[i].getFilePathHashB()); cryptWriter.writeInt16(hashTable[i].getLanguage()); cryptWriter.writeInt8(hashTable[i].getPlatform()); cryptWriter.writeInt8(hashTable[i].getUnknown()); cryptWriter.writeInt32(hashTable[i].getFileBlockIndex()); } } public void saveMetadata() throws IOException { checkReadWrite(); writeAttributesFile(); long maxBlockEnd = headerSize; boolean extBlockTableNeeded = false; for (BlockTableEntry blockTableEntry : blockTable) { long blockEnd = blockTableEntry.getBlockOffset() + blockTableEntry.getBlockSize(); if (blockEnd > maxBlockEnd) maxBlockEnd = blockEnd; if (blockTableEntry.getBlockOffset() > 0xffffffffL) extBlockTableNeeded = true; } if (maxBlockEnd > hashTableOffset) { hashTableOffset = maxBlockEnd; } hashTableEntries = hashTable.length; maxBlockEnd = hashTableOffset + hashTableEntries * 16; if (maxBlockEnd > blockTableOffset) { blockTableOffset = maxBlockEnd; } blockTableEntries = blockTable.size(); maxBlockEnd = blockTableOffset + blockTableEntries * 16; if (extBlockTableNeeded) { if (maxBlockEnd > extBlockTableOffset) { extBlockTableOffset = maxBlockEnd; } maxBlockEnd = extBlockTableOffset + blockTableEntries * 2; } else { extBlockTableOffset = 0; } archiveSize = maxBlockEnd; writeHeader(); writeHashTable(); writeBlockTable(); } public boolean isNew() { assert (hashTable.length == 0) == blockTable.isEmpty(); return hashTable.length == 0; } public void initHashtable(int size) { if (!isNew()) throw new IllegalStateException(); if (Integer.bitCount(size) != 1) throw new IllegalArgumentException("size must be (1<<x)"); hashTable = new HashTableEntry[size]; hashTableEntries = size; blockTable.clear(); } private void findArchiveOffset() throws IOException { int offset = 0; while (offset < randomAccessFile.length()) { FileReader fileReader = new FileReader(randomAccessFile, offset); signature = fileReader.readChar4(); if (!signature.equals(MPQ_SIGNATURE)) { offset += 1024; continue; } archiveOffset = offset; return; } throw new IOException("no valid MPQ signature found"); } private void readHeader() throws IOException { FileReader fileReader = new FileReader(randomAccessFile, archiveOffset); signature = fileReader.readChar4(); if (!signature.equals(MPQ_SIGNATURE)) { throw new IOException("no valid MPQ signature found"); } headerSize = fileReader.readInt32(); archiveSize = fileReader.readInt32() & 0xffffffffL; formatVersion = fileReader.readInt16(); sectorSizeShift = fileReader.readInt8(); unknown1 = fileReader.readInt8(); hashTableOffset = fileReader.readInt32() & 0xffffffffL; blockTableOffset = fileReader.readInt32() & 0xffffffffL; hashTableEntries = fileReader.readInt32(); blockTableEntries = fileReader.readInt32(); if (formatVersion >= 1) { extBlockTableOffset = fileReader.readInt32() & 0xffffffffL; extBlockTableOffset |= (fileReader.readInt32() & 0xffffffffL) << 32; hashTableOffset |= (fileReader.readInt16() & 0xffffL) << 32; blockTableOffset |= (fileReader.readInt16() & 0xffffL) << 32; } fileReader.close(); } private void readBlockTable() throws IOException { blockTable.clear(); int[] highOffsetBits = new int[blockTableEntries]; if (formatVersion >= 1 && extBlockTableOffset != 0) { FileReader extBlockTableReader = new FileReader(randomAccessFile, archiveOffset + extBlockTableOffset); for (int i = 0; i < blockTableEntries; i++) { highOffsetBits[i] = extBlockTableReader.readInt16() & 0xffff; } } FileReader fileReader = new FileReader(randomAccessFile, archiveOffset + blockTableOffset); DecryptReader blockTableReader = new DecryptReader(fileReader, blockTableEntries * 16, hashString("(block table)", 3)); for (int i = 0; i < blockTableEntries; i++) { long blockOffset = blockTableReader.readInt32() & 0xffffffffL; blockOffset |= ((long) highOffsetBits[i]) << 32; int blockSize = blockTableReader.readInt32(); int fileSize = blockTableReader.readInt32(); int flags = blockTableReader.readInt32(); blockTable.add(new BlockTableEntry( blockOffset, blockSize, fileSize, flags)); } assert blockTableEntries == blockTable.size(); } private void readHashTable() throws IOException { FileReader fileReader = new FileReader(randomAccessFile, archiveOffset + hashTableOffset); DecryptReader decryptReader = new DecryptReader(fileReader, hashTableEntries * 16, hashString("(hash table)", 3)); hashTable = new HashTableEntry[hashTableEntries]; for (int i = 0; i < hashTableEntries; i++) { int filePathHashA = decryptReader.readInt32(); int filePathHashB = decryptReader.readInt32(); int language = decryptReader.readInt16(); int platform = decryptReader.readInt8(); int unknown = decryptReader.readInt8(); int fileBlockIndex = decryptReader.readInt32(); hashTable[i] = new HashTableEntry(filePathHashA, filePathHashB, language, platform, unknown, fileBlockIndex); } } @Override public String toString() { StringBuilder s = new StringBuilder(); s.append("File: ").append(file).append('\n'); s.append("Archive offset: ").append(archiveOffset).append('\n'); s.append("Signature: ").append(signature).append('\n'); s.append("Header size: ").append(headerSize).append('\n'); s.append("Archive size: ").append(archiveSize).append('\n'); s.append("Archive version: ").append(formatVersion).append('\n'); s.append("Sector size shift: ").append(sectorSizeShift).append('\n'); s.append("unknown1: ").append(unknown1).append('\n'); s.append("Hash table offset: ").append(hashTableOffset).append('\n'); s.append("Block table offset: ").append(blockTableOffset).append('\n'); s.append("Hash table entries: ").append(hashTableEntries).append('\n'); s.append("Block table entries: ").append(blockTableEntries) .append('\n'); if (formatVersion >= 1) { s.append("Extended block table offset: ").append( extBlockTableOffset).append('\n'); } for (BlockTableEntry block : blockTable) { s.append(block.toString()); } for (int i = 0; i < hashTable.length; i++) { HashTableEntry hashTableEntry = hashTable[i]; if (hashTableEntry.getFileBlockIndex() != -1) { s.append(hashTableEntry.toString()); } } return s.toString(); } private void checkClosed() { if (randomAccessFile == null) { throw new IllegalStateException("Archive is closed"); } } public MpqFile getFile(String filePath, Integer language, Integer platform) throws IOException { checkClosed(); int initEntry = hashString(filePath, 0) & (hashTableEntries - 1); HashTableEntry entry = hashTable[initEntry]; if (entry == null || entry.getFileBlockIndex() == -1) { return null; } int hashA = hashString(filePath, 1); int hashB = hashString(filePath, 2); int curEntry = initEntry; do { if (entry != null && entry.getFileBlockIndex() != -2) { if (entry.getFilePathHashA() == hashA && entry.getFilePathHashB() == hashB && (language == null || entry.getLanguage() == language) && (platform == null || entry.getPlatform() == platform)) { entry.setFilePath(filePath); blockTable.get(entry.getFileBlockIndex()).setFilePath(filePath); return new MpqFile(this, filePath, entry, blockTable .get(entry.getFileBlockIndex())); } } curEntry = (curEntry + 1) % hashTableEntries; entry = hashTable[curEntry]; } while (entry != null && entry.getFileBlockIndex() != -1 && curEntry != initEntry); return null; } public boolean deleteFile(String filePath, Integer language, Integer platform) { checkClosed(); checkReadWrite(); int initEntry = hashString(filePath, 0) & (hashTableEntries - 1); HashTableEntry entry = hashTable[initEntry]; if (entry == null || entry.getFileBlockIndex() == -1) { return false; } int hashA = hashString(filePath, 1); int hashB = hashString(filePath, 2); int curEntry = initEntry; boolean deletedSomething = false; do { if (entry != null && entry.getFileBlockIndex() != -2) { if (entry.getFilePathHashA() == hashA && entry.getFilePathHashB() == hashB && (language == null || entry.getLanguage() == language) && (platform == null || entry.getPlatform() == platform)) { int blockIndex = entry.getFileBlockIndex(); blockTable.set(blockIndex, new BlockTableEntry( blockTable.get(blockIndex).getBlockOffset(), blockTable.get(blockIndex).getBlockSize(), 0, 0)); int nextEntry = (curEntry + 1) % hashTableEntries; if (hashTable[nextEntry].getFileBlockIndex() == -1) { hashTable[curEntry] = new HashTableEntry(0, 0, 0, 0, 0, -2); } else { hashTable[curEntry] = new HashTableEntry(0, 0, 0, 0, 0, -1); } deletedSomething = true; } } curEntry = (curEntry + 1) % hashTableEntries; entry = hashTable[curEntry]; } while (entry != null && entry.getFileBlockIndex() != -1 && curEntry != initEntry); return deletedSomething; } private int findFreeHashTableEntry(int fileHash) { int initEntry = fileHash & (hashTableEntries - 1); HashTableEntry entry = hashTable[initEntry]; int curEntry = initEntry; do { if (entry == null || entry.getFileBlockIndex() == -1 || entry.getFileBlockIndex() == -2) { return curEntry; } curEntry = (curEntry + 1) % hashTableEntries; entry = hashTable[curEntry]; } while (curEntry != initEntry); return -1; } public boolean addFile(File newFile, String filePath, int language, int platform) throws IOException { checkClosed(); checkReadWrite(); long fileSize = newFile.length(); FileInputStream fileInputStream = new FileInputStream(newFile); try { return addFile(filePath, language, platform, fileInputStream, fileSize); } finally { fileInputStream.close(); } } public boolean addFile(String filePath, int language, int platform, InputStream inputStream, long fileSize) throws IOException { long maxBlockEnd = headerSize; for (BlockTableEntry blockTableEntry : blockTable) { long blockEnd = blockTableEntry.getBlockOffset() + blockTableEntry.getBlockSize(); if (blockEnd > maxBlockEnd) maxBlockEnd = blockEnd; } int hashTableEntry = findFreeHashTableEntry(hashString(filePath, 0)); if (hashTableEntry < 0) { return false; } int hashA = hashString(filePath, 1); int hashB = hashString(filePath, 2); MpqWriter mpqWriter = new MpqWriter(this, maxBlockEnd, fileSize); int sectorSize = getSectorSize(); for (int i = 0; i < (fileSize + sectorSize - 1) / sectorSize; i++) { long remaining = fileSize - i * sectorSize; byte[] data = new byte[(int) (remaining > sectorSize ? sectorSize : remaining)]; int datalen = inputStream.read(data); if (datalen != data.length) { throw new IOException("couldn't read input file"); } mpqWriter.writeSector(data); } BlockTableEntry blockTableEntry = mpqWriter.getBlockTableEntry(); blockTableEntry.setFilePath(filePath); blockTable.add(blockTableEntry); HashTableEntry entry = new HashTableEntry(hashA, hashB, language, platform, 0, blockTable.indexOf(blockTableEntry)); entry.setFilePath(filePath); hashTable[hashTableEntry] = entry; return true; } private boolean writeAttributesFile() throws IOException { checkClosed(); checkReadWrite(); String filePath = "(attributes)"; deleteFile(filePath, null, null); if (extAttrsVersion == null) { extAttrsVersion = 100; extAttrsPresent = 7; } int blocks = blockTable.size() + 1; int size = 8; int attrsPresent = extAttrsPresent; if ((attrsPresent & 1) != 0) { // CRC32 size += blocks * 4; } if ((attrsPresent & 2) != 0) { // filetime size += blocks * 8; } if ((attrsPresent & 4) != 0) { // MD5 size += blocks * 16; } ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN); buffer.putInt(extAttrsVersion); buffer.putInt(extAttrsPresent); if ((attrsPresent & 1) != 0) { for (int i = 0; i < blocks; i++) { int crc32; if (i < blockTable.size()) { Integer v = blockTable.get(i).getExtCRC32(); crc32 = v == null ? 0 : v; } else { crc32 = 0; } buffer.putInt(crc32); } } if ((attrsPresent & 2) != 0) { for (int i = 0; i < blocks; i++) { long filetime; if (i < blockTable.size()) { Long v = blockTable.get(i).getExtFiletime(); filetime = v == null ? 0L : v; } else { filetime = 0L; } buffer.putLong(filetime); } } if ((attrsPresent & 4) != 0) { for (int i = 0; i < blocks; i++) { byte[] md5 = null; if (i < blockTable.size()) { md5 = blockTable.get(i).getExtMD5(); } if (md5 == null) { md5 = new byte[16]; } for (byte b : md5) { buffer.put(b); } } } assert buffer.position() == buffer.limit(); ByteArrayInputStream inputStream = new ByteArrayInputStream(buffer .array(), buffer.arrayOffset(), size); return addFile(filePath, 0, 0, inputStream, size); } private static int isReadInt32(InputStream is) throws IOException { int c1 = is.read(); int c2 = is.read(); int c3 = is.read(); int c4 = is.read(); if (c4 == -1) throw new EOFException(); return c1 + c2 * 256 + c3 * 256 * 256 + c4 * 256 * 256 * 256; } public void readExtData() throws IOException { checkClosed(); MpqFile attrFile = getFile("(attributes)", 0, 0); if (attrFile != null) { InputStream inputStream = attrFile.getInputStream(); try { int version = isReadInt32(inputStream); extAttrsVersion = version; int attrsPresent = isReadInt32(inputStream); extAttrsPresent = attrsPresent; if ((attrsPresent & 1) != 0) { // CRC32 for (int i = 0; i < blockTableEntries; i++) { int crc32 = isReadInt32(inputStream); blockTable.get(i).setExtCRC32(crc32); } } if ((attrsPresent & 2) != 0) { // filetime for (int i = 0; i < blockTableEntries; i++) { int lowTime = isReadInt32(inputStream); int highTime = isReadInt32(inputStream); long extFiletime = (lowTime & 0xffffffffL) | ((highTime & 0xffffffffL) << 32); blockTable.get(i).setExtFiletime(extFiletime); } } if ((attrsPresent & 4) != 0) { // MD5 for (int i = 0; i < blockTableEntries; i++) { byte[] md5 = new byte[16]; for (int j = 0; j < 16; j++) { int b = inputStream.read(); if (b == -1) throw new EOFException(); md5[j] = (byte) b; } blockTable.get(i).setExtMD5(md5); } } } finally { inputStream.close(); } } MpqFile listFile = getFile("(listfile)", 0, 0); if (listFile != null) { InputStream inputStream = listFile.getInputStream(); try { Scanner scan = new Scanner(inputStream, "UTF-8"); scan.useDelimiter(Pattern.compile("[;\\r\\n]+")); while (scan.hasNext()) { String name = scan.next(); getFile(name, null, null); } scan.close(); } finally { inputStream.close(); } } } public Collection<String> listFileNames() { List<String> names = new ArrayList<String>(); for (HashTableEntry entry : hashTable) { if (entry != null && entry.getFileBlockIndex() != -1 && entry.getFileBlockIndex() != -2) { String name = entry.getFilePath(); if (name != null) { names.add(name); } } } return names; } public void dump(PrintStream out) { StringBuilder s = new StringBuilder(); s.append("File: ").append(file).append('\n'); s.append("Archive offset: ").append(archiveOffset).append('\n'); s.append("Signature: ").append(signature).append('\n'); s.append("Header size: ").append(headerSize).append('\n'); s.append("Archive size: ").append(archiveSize).append('\n'); s.append("Archive version: ").append(formatVersion).append('\n'); s.append("Sector size shift: ").append(sectorSizeShift).append('\n'); s.append("unknown1: ").append(unknown1).append('\n'); s.append("Hash table offset: ").append(hashTableOffset).append('\n'); s.append("Block table offset: ").append(blockTableOffset).append('\n'); s.append("Hash table entries: ").append(hashTableEntries).append('\n'); s.append("Block table entries: ").append(blockTableEntries) .append('\n'); if (formatVersion >= 1) { s.append("Extended block table offset: ").append( extBlockTableOffset).append('\n'); } out.print(s); for (BlockTableEntry block : blockTable) { out.print(block.toString()); } for (int i = 0; i < hashTable.length; i++) { HashTableEntry hashTableEntry = hashTable[i]; if (hashTableEntry.getFileBlockIndex() != -1) { out.print(hashTableEntry.toString()); } } } }