Skip to content

Commit 221dd16

Browse files
committed
Phase 1: write side of the new core
MpqArchiveWriter builds an archive in memory and writes it when told to. Saving is explicit (P1-2). The pre-2.0 JMpqEditor rebuilt the archive as a side effect of close(), so a missed flag or a throw on the way out could rewrite a file that was only meant to be read. Reading is MpqArchive, writing is MpqArchiveWriter, and the write happens where the caller asks for it. Format version is chosen, not inherited (P1-6). MpqWriteOptions accepts 0 or 1 and rejects the rest at construction, rather than emitting a 208-byte header carrying 32 bytes of meaning as the old writer did for a version 2 source. Verbatim copies are conditional on sector size. A file keeps its stored bytes only when the target archive keeps the source's sector size, because a sector offset table is expressed in that sector size. This is the bug the golden harness found in the old recompression path, encoded as a rule the writer applies rather than a case it can forget. MpqWriteOptions carries the P1-8 extension points: an explicit hash table capacity and extra unused block slots, enough for protection tooling to emit a maximised table without that being a concern of this library. Also listfile and prefix policy, since a Warcraft III map stops loading if its 512-byte prefix is dropped. save(Path) stages to a sibling file and moves it into place, so an interrupted save cannot leave a half-written archive where a working one used to be. It cannot target an archive that is still open: a mapped file cannot be replaced on Windows. The first version of this claimed otherwise and the test caught it, so the javadoc now spells out the build-then-write pattern instead. Verification: the writer preserves 179 files across all fixtures on the copy path, and the same 179 when re-encoding into a different sector size, which is the case the old writer corrupted. Plus reproducible output, explicit format selection, table capacity control, prefix handling, atomic save, and rejection of the internal names it generates. Self-review before pushing: block table sizing was int * int, and unlike the hash table its capacity is caller-influenced through extraBlockEntries, so it could overflow.
1 parent d4e3058 commit 221dd16

7 files changed

Lines changed: 1540 additions & 0 deletions

File tree

src/main/java/org/inwc3/jmpq/MpqArchive.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,39 @@ private void readNames() {
387387
}
388388
}
389389

390+
/**
391+
* The bytes preceding the archive header.
392+
* <p>
393+
* Warcraft III maps carry a 512-byte prefix before the archive proper, and
394+
* a rebuild that means to stay loadable has to keep it.
395+
*
396+
* @return the prefix, empty when the archive starts at offset 0.
397+
* @throws IOException if the prefix cannot be read.
398+
*/
399+
byte[] prefixBytes() throws IOException {
400+
final long length = header.headerOffset();
401+
if (length <= 0) {
402+
return new byte[0];
403+
}
404+
if (length > Integer.MAX_VALUE - 8) {
405+
throw new JMpqException("Archive is preceded by " + length
406+
+ " bytes, too many to carry over.");
407+
}
408+
return source.bytes(0, (int) length);
409+
}
410+
411+
/**
412+
* The stored bytes of a file with any encryption removed, for a verbatim
413+
* copy into another archive of the same sector size.
414+
*
415+
* @param entry the file to copy.
416+
* @return exactly {@link MpqFileEntry#compressedSize()} bytes.
417+
* @throws IOException if the data is damaged.
418+
*/
419+
byte[] storedBytesDecrypted(MpqFileEntry entry) throws IOException {
420+
return reader.storedBytesDecrypted(entry);
421+
}
422+
390423
/**
391424
* Releases the archive. For a file-backed archive the file is fully
392425
* released by the time this returns.

0 commit comments

Comments
 (0)