Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow usage of external Listfile to rebuild MPQ. #36

Merged
merged 10 commits into from
Mar 22, 2021
Merged
101 changes: 89 additions & 12 deletions src/main/java/systems/crigges/jmpq3/JMpqEditor.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ public class JMpqEditor implements AutoCloseable {

/** If write operations are supported on the archive. */
private boolean canWrite;
/** If the archive was originally read-only */
private boolean openedAsReadOnly;
Copy link
Member

@Frotty Frotty May 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why add this superfluous variable?
It also wouldn't sound right to open the mpq as read-only and for it then to become writable.
The listfile function should probably only be available in edit mode.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like the "edit mode" is only available in the Constructor. If the (listfile) doesn't exist, it sets canWrite to "false" even if it was originally not opened as read-only. That's why the setExternalListfile method needs to set canWrite to "true". However I don't want to set canWrite to "true" if the archive was orignally opened as readOnly. Does this make sense? Maybe there's a better way of accomplishing this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could pass listfile in constructor i suppose to prevent setting canWrite to false.


/**
* Creates a new MPQ editor for the MPQ file at the specified path.
Expand All @@ -138,6 +140,7 @@ public class JMpqEditor implements AutoCloseable {
public JMpqEditor(Path mpqArchive, MPQOpenOption... openOptions) throws JMpqException {
// process open options
canWrite = !Arrays.asList(openOptions).contains(MPQOpenOption.READ_ONLY);
openedAsReadOnly = !canWrite;
legacyCompatibility = Arrays.asList(openOptions).contains(MPQOpenOption.FORCE_V0);
log.debug(mpqArchive.toString());
try {
Expand Down Expand Up @@ -209,25 +212,59 @@ 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(!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();
removeMissingFiles();
// Operation succeeded and added a listfile so we can now write properly.
// (as long as it wasn't read-only to begin with)
canWrite = !openedAsReadOnly;
} catch (Exception ex) {
log.warn("Could not apply external listfile: " + externalListfilePath.getAbsolutePath());
// The value of canWrite is not changed intentionally
}
}

/**
* Removes files from the listfile if they aren't
* actually in the map.
*/
private void removeMissingFiles() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

@zach-cloud zach-cloud May 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is. Good point, that's a much better way of doing this.
Huh.. interesting that this was already in the code.. I swear it didn't work properly without adding in this removeMissingFIles method. I'll test again.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it resolved?

Iterator<String> it = listFile.getFiles().iterator();
while(it.hasNext()) {
if(!hasFile(it.next())) {
it.remove();
}
}
}

/**
* Reads an internal Listfile name called (listfile)
* and applies that as the archive's listfile.
*/
private void readListFile() {
if (hasFile("(listfile)")) {
try {
File tempFile = File.createTempFile("list", "file", JMpqEditor.tempDir);
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);
}
Expand All @@ -237,6 +274,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);
Expand Down Expand Up @@ -1046,4 +1114,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<String> getListfileEntries() {
return Collections.unmodifiableCollection(listFile.getFiles());
}
}
14 changes: 12 additions & 2 deletions src/test/java/systems/crigges/jmpq3test/MpqTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions src/test/resources/listfile.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
scripts\war3map.j
war3map.j
war3map.w3u
war3map.w3t
war3map.w3a
customFile.j