diff --git a/src/main/java/systems/crigges/jmpq3/JMpqEditor.java b/src/main/java/systems/crigges/jmpq3/JMpqEditor.java index bbbe910..992eff9 100644 --- a/src/main/java/systems/crigges/jmpq3/JMpqEditor.java +++ b/src/main/java/systems/crigges/jmpq3/JMpqEditor.java @@ -209,6 +209,40 @@ private void readAttributesFile() { } } + /** + * For use when the MPQ is missing a (listfile) + * Adds this custom listfile into the MPQ and uses it + * for rebuilding purposes. + * If this is not a full listfile, the end result will be missing files. + * + * @param externalListfilePath Path to a file containing listfile entries + */ + public void setExternalListfile(File externalListfilePath) { + if(!canWrite) { + log.warn("The mpq was opened as readonly, setting an external listfile will have no effect."); + return; + } + if(!externalListfilePath.exists()) { + log.warn("External MPQ File: " + externalListfilePath.getAbsolutePath() + + " does not exist and will not be used"); + return; + } + try { + // Read and apply listfile + listFile = new Listfile(Files.readAllBytes(externalListfilePath.toPath())); + checkListfileEntries(); + // Operation succeeded and added a listfile so we can now write properly. + // (as long as it wasn't read-only to begin with) + } catch (Exception ex) { + log.warn("Could not apply external listfile: " + externalListfilePath.getAbsolutePath()); + // The value of canWrite is not changed intentionally + } + } + + /** + * Reads an internal Listfile name called (listfile) + * and applies that as the archive's listfile. + */ private void readListFile() { if (hasFile("(listfile)")) { try { @@ -216,18 +250,7 @@ private void readListFile() { tempFile.deleteOnExit(); extractFile("(listfile)", tempFile); listFile = new Listfile(Files.readAllBytes(tempFile.toPath())); - int hiddenFiles = (hasFile("(attributes)") ? 2 : 1) + (hasFile("(signature)") ? 1 : 0); - if (canWrite) { - if (listFile.getFiles().size() >= blockTable.getAllVaildBlocks().size() - hiddenFiles) { - log.warn("mpq's listfile is incomplete. Blocks without listfile entry will be discarded"); - } - for (String fileName : listFile.getFiles()) { - if (!hasFile(fileName)) { - log.warn("listfile entry does not exist in archive and will be discarded: " + fileName); - } - } - listFile.getFileMap().entrySet().removeIf(file -> !hasFile(file.getValue())); - } + checkListfileEntries(); } catch (Exception e) { log.warn("Extracting the mpq's listfile failed. It cannot be rebuild.", e); } @@ -237,6 +260,37 @@ private void readListFile() { } } + /** + * Performs verification to see if we know all the blocks of this file. + * Prints warnings if we don't know all blocks. + * + * @throws JMpqException If retrieving valid blocks fails + */ + private void checkListfileEntries() throws JMpqException { + int hiddenFiles = (hasFile("(attributes)") ? 2 : 1) + (hasFile("(signature)") ? 1 : 0); + if (canWrite) { + checkListfileCompleteness(hiddenFiles); + } + } + + /** + * Checks listfile for completeness against block table + * + * @param hiddenFiles Num. hidden files + * @throws JMpqException If retrieving valid blocks fails + */ + private void checkListfileCompleteness(int hiddenFiles) throws JMpqException { + if (listFile.getFiles().size() <= blockTable.getAllVaildBlocks().size() - hiddenFiles) { + log.warn("mpq's listfile is incomplete. Blocks without listfile entry will be discarded"); + } + for (String fileName : listFile.getFiles()) { + if (!hasFile(fileName)) { + log.warn("listfile entry does not exist in archive and will be discarded: " + fileName); + } + } + listFile.getFileMap().entrySet().removeIf(file -> !hasFile(file.getValue())); + } + private void readBlockTable() throws IOException { ByteBuffer blockBuffer = ByteBuffer.allocate(blockSize * 16).order(ByteOrder.LITTLE_ENDIAN); fc.position(headerOffset + blockPos); @@ -1046,4 +1100,13 @@ public String toString() { return "JMpqEditor [headerSize=" + headerSize + ", archiveSize=" + archiveSize + ", formatVersion=" + formatVersion + ", discBlockSize=" + discBlockSize + ", hashPos=" + hashPos + ", blockPos=" + blockPos + ", hashSize=" + hashSize + ", blockSize=" + blockSize + ", hashMap=" + hashTable + "]"; } + + /** + * Returns an unmodifiable collection of all Listfile entries + * + * @return Listfile entries + */ + public Collection getListfileEntries() { + return Collections.unmodifiableCollection(listFile.getFiles()); + } } diff --git a/src/test/java/systems/crigges/jmpq3test/MpqTests.java b/src/test/java/systems/crigges/jmpq3test/MpqTests.java index 4c3a089..6bba7ce 100644 --- a/src/test/java/systems/crigges/jmpq3test/MpqTests.java +++ b/src/test/java/systems/crigges/jmpq3test/MpqTests.java @@ -20,8 +20,6 @@ import java.nio.file.StandardCopyOption; import java.util.*; -import static systems.crigges.jmpq3.HashTable.calculateFileKey; - /** * Created by Frotty on 06.03.2017. */ @@ -136,6 +134,18 @@ public void testRebuild() throws IOException { } } + @Test + public void testExternalListfile() throws Exception { + File mpq = getFile("mpqs/normalMap.w3x"); + File listFile = getFile("listfile.txt"); + JMpqEditor mpqEditor = new JMpqEditor(mpq, MPQOpenOption.FORCE_V0); + if(mpqEditor.isCanWrite()) { + mpqEditor.deleteFile("(listfile)"); + } + mpqEditor.setExternalListfile(listFile); + Assert.assertTrue(mpqEditor.getListfileEntries().contains("war3map.w3a")); + } + @Test public void testRecompressBuild() throws IOException { File[] mpqs = getMpqs(); diff --git a/src/test/resources/listfile.txt b/src/test/resources/listfile.txt new file mode 100644 index 0000000..89043f5 --- /dev/null +++ b/src/test/resources/listfile.txt @@ -0,0 +1,6 @@ +scripts\war3map.j +war3map.j +war3map.w3u +war3map.w3t +war3map.w3a +customFile.j \ No newline at end of file