Skip to content

Commit ff7a008

Browse files
committed
Add zip writer strategy to convert ZipArchive to via std ZipOutputStream
1 parent 8c34fd2 commit ff7a008

File tree

10 files changed

+216
-18
lines changed

10 files changed

+216
-18
lines changed

src/main/java/software/coley/llzip/ZipCompressions.java

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package software.coley.llzip;
22

33
import software.coley.llzip.part.LocalFileHeader;
4+
import software.coley.llzip.strategy.DeflateDecompressor;
5+
6+
import java.io.IOException;
47

58
/**
69
* Constants for {@link LocalFileHeader#getCompressionMethod()}.
@@ -120,4 +123,96 @@ public interface ZipCompressions {
120123
* AE-x encryption marker.
121124
*/
122125
int AE_x = 99;
126+
127+
/**
128+
* @param method
129+
* Compression method value.
130+
*
131+
* @return Name of method.
132+
*/
133+
static String getName(int method) {
134+
switch (method) {
135+
case STRORED:
136+
return "STRORED";
137+
case SHRUNK:
138+
return "SHRUNK";
139+
case REDUCED_F1:
140+
return "REDUCED_F1";
141+
case REDUCED_F2:
142+
return "REDUCED_F2";
143+
case REDUCED_F3:
144+
return "REDUCED_F3";
145+
case REDUCED_F4:
146+
return "REDUCED_F4";
147+
case IMPLODED:
148+
return "IMPLODED";
149+
case RESERVED_TOKENIZING:
150+
return "RESERVED_TOKENIZING";
151+
case DEFLATED:
152+
return "DEFLATED";
153+
case DEFLATED_64:
154+
return "DEFLATED_64";
155+
case PKWARE_IMPLODING:
156+
return "PKWARE_IMPLODING";
157+
case PKWARE_RESERVED_11:
158+
return "PKWARE_RESERVED_11";
159+
case BZIP2:
160+
return "BZIP2";
161+
case PKWARE_RESERVED_13:
162+
return "PKWARE_RESERVED_13";
163+
case LZMA:
164+
return "LZMA";
165+
case PKWARE_RESERVED_15:
166+
return "PKWARE_RESERVED_15";
167+
case CMPSC:
168+
return "CMPSC";
169+
case PKWARE_RESERVED_17:
170+
return "PKWARE_RESERVED_17";
171+
case IBM_TERSE:
172+
return "IBM_TERSE";
173+
case IBM_LZ77:
174+
return "IBM_LZ77";
175+
case DEPRECATED_ZSTD:
176+
return "DEPRECATED_ZSTD";
177+
case ZSTANDARD:
178+
return "ZSTANDARD";
179+
case MP3:
180+
return "MP3";
181+
case XZ:
182+
return "XZ";
183+
case JPEG:
184+
return "JPEG";
185+
case WAVPACK:
186+
return "WAVPACK";
187+
case PPMD:
188+
return "PPMD";
189+
case AE_x:
190+
return "AE_x";
191+
default:
192+
return "Unknown[" + method + "]";
193+
}
194+
}
195+
196+
/**
197+
* @param header
198+
* Header with {@link LocalFileHeader#getFileData()} to decompress.
199+
*
200+
* @return Decompressed {@code byte[]}.
201+
*
202+
* @throws IOException
203+
* When the decompression failed.
204+
*/
205+
static byte[] decompress(LocalFileHeader header) throws IOException {
206+
int method = header.getCompressionMethod();
207+
switch (method) {
208+
case STRORED:
209+
return header.getFileData();
210+
case DEFLATED:
211+
return header.decompress(new DeflateDecompressor());
212+
default:
213+
// TODO: Support other decompressing techniques
214+
String methodName = getName(method);
215+
throw new IOException("Unsupported compression method: " + methodName);
216+
}
217+
}
123218
}

src/main/java/software/coley/llzip/part/CentralDirectoryFileHeader.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
*/
1515
public class CentralDirectoryFileHeader implements ZipPart, ZipRead {
1616
private transient int offset = -1;
17-
private transient LocalFileHeader linked;
17+
private transient LocalFileHeader linkedFileHeader;
1818
// Zip spec elements
1919
private int versionMadeBy;
2020
private int versionNeededToExtract;
@@ -78,16 +78,16 @@ public int offset() {
7878
/**
7979
* @return The file header associated with {@link #getRelativeOffsetOfLocalHeader()}. May be {@code null}.
8080
*/
81-
public LocalFileHeader getLinked() {
82-
return linked;
81+
public LocalFileHeader getLinkedFileHeader() {
82+
return linkedFileHeader;
8383
}
8484

8585
/**
86-
* @param linked
86+
* @param header
8787
* The file header associated with {@link #getRelativeOffsetOfLocalHeader()}. May be {@code null}.
8888
*/
89-
public void link(LocalFileHeader linked) {
90-
this.linked = linked;
89+
public void link(LocalFileHeader header) {
90+
this.linkedFileHeader = header;
9191
}
9292

9393
/**
@@ -453,15 +453,15 @@ public boolean equals(Object o) {
453453
internalFileAttributes == that.internalFileAttributes &&
454454
externalFileAttributes == that.externalFileAttributes &&
455455
relativeOffsetOfLocalHeader == that.relativeOffsetOfLocalHeader &&
456-
Objects.equals(linked, that.linked) &&
456+
Objects.equals(linkedFileHeader, that.linkedFileHeader) &&
457457
fileName.equals(that.fileName) &&
458458
Arrays.equals(extraField, that.extraField) &&
459459
fileComment.equals(that.fileComment);
460460
}
461461

462462
@Override
463463
public int hashCode() {
464-
int result = Objects.hash(offset, linked, versionMadeBy, versionNeededToExtract, generalPurposeBitFlag,
464+
int result = Objects.hash(offset, linkedFileHeader, versionMadeBy, versionNeededToExtract, generalPurposeBitFlag,
465465
compressionMethod, lastModFileTime, lastModFileDate, crc32, compressedSize, uncompressedSize,
466466
fileNameLength, extraFieldLength, fileCommentLength, diskNumberStart, internalFileAttributes,
467467
externalFileAttributes, relativeOffsetOfLocalHeader, fileName, fileComment);

src/main/java/software/coley/llzip/part/LocalFileHeader.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414
public class LocalFileHeader implements ZipPart, ZipRead {
1515
private transient int offset = -1;
16+
private transient CentralDirectoryFileHeader linkedDirectoryFileHeader;
1617
// Zip spec elements
1718
private int versionNeededToExtract;
1819
private int generalPurposeBitFlag;
@@ -74,6 +75,21 @@ public byte[] decompress(Decompressor decompressor) throws IOException {
7475
return decompressor.decompress(this, fileData);
7576
}
7677

78+
/**
79+
* @return The central directory file header this file is associated with.
80+
*/
81+
public CentralDirectoryFileHeader getLinkedDirectoryFileHeader() {
82+
return linkedDirectoryFileHeader;
83+
}
84+
85+
/**
86+
* @param directoryFileHeader
87+
* The central directory file header this file is associated with.
88+
*/
89+
public void link(CentralDirectoryFileHeader directoryFileHeader) {
90+
this.linkedDirectoryFileHeader = directoryFileHeader;
91+
}
92+
7793
/**
7894
* @return Zip software version.
7995
*/

src/main/java/software/coley/llzip/part/ZipRead.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public interface ZipRead {
1414
*/
1515
void read(byte[] data, int offset);
1616

17-
// TODO: Write conventions, then rename to 'ZipReadWrite' and tie into ZipWriterStrategy
18-
// - maybe transform primitive 'offset' to data type references and auto-compute offsets in output.
17+
// TODO: Write conventions, then rename to 'ZipReadWrite' and tie into a new ZipWriterStrategy
18+
// - similar to read, but with data-output-stream writes bytes to stream
19+
// - obviously requires offset data to be correct for a 'valid' output
20+
// - but still allows invalid output to facilitate crafting intentional malformed zips/jars
1921
}

src/main/java/software/coley/llzip/strategy/DefaultZipReaderStrategy.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public void read(ZipArchive zip, byte[] data) throws IOException {
5151
file.read(data, offset);
5252
zip.getParts().add(file);
5353
directory.link(file);
54+
file.link(directory);
5455
offsets.add(offset);
5556
} else {
5657
logger.warn("Central-Directory-File-Header's offset[{}] to Local-File-Header does not match the Local-File-Header magic!", offset);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package software.coley.llzip.strategy;
2+
3+
import software.coley.llzip.ZipArchive;
4+
import software.coley.llzip.ZipCompressions;
5+
import software.coley.llzip.part.CentralDirectoryFileHeader;
6+
import software.coley.llzip.part.LocalFileHeader;
7+
8+
import java.io.IOException;
9+
import java.io.OutputStream;
10+
import java.util.Optional;
11+
import java.util.zip.ZipEntry;
12+
import java.util.zip.ZipOutputStream;
13+
14+
/**
15+
* Uses the Java {@link ZipOutputStream} to recompute the zip file format.
16+
* The only used data in this case is {@link LocalFileHeader#getFileData()} and the file name
17+
* which can be either {@link CentralDirectoryFileHeader#getFileName()} or {@link LocalFileHeader#getFileName()}.
18+
*
19+
* @author Matt Coley
20+
*/
21+
public class JavaZipWriterStategy implements ZipWriterStrategy {
22+
@Override
23+
public void write(ZipArchive archive, OutputStream os) throws IOException {
24+
try (ZipOutputStream zos = new ZipOutputStream(os)) {
25+
for (LocalFileHeader fileHeader : archive.getLocalFiles()) {
26+
String name = Optional.ofNullable(fileHeader.getLinkedDirectoryFileHeader())
27+
.map(CentralDirectoryFileHeader::getFileName)
28+
.orElse(fileHeader.getFileName());
29+
zos.putNextEntry(new ZipEntry(name));
30+
zos.write(ZipCompressions.decompress(fileHeader));
31+
zos.closeEntry();
32+
}
33+
}
34+
}
35+
}

src/main/java/software/coley/llzip/strategy/JvmZipReaderStrategy.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ else if (Array.startsWith(data, jvmBaseOffset, ZipPatterns.CENTRAL_DIRECTORY_FIL
9090
file.read(data, offset);
9191
zip.getParts().add(file);
9292
directory.link(file);
93+
file.link(directory);
9394
offsets.add(offset);
9495
} catch (Exception ex) {
9596
logger.warn("Failed to read 'local file header' at offset[{}]", offset);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package software.coley.llzip.util;
2+
3+
import software.coley.llzip.part.CentralDirectoryFileHeader;
4+
import software.coley.llzip.part.LocalFileHeader;
5+
import software.coley.llzip.part.ZipPart;
6+
import software.coley.llzip.strategy.JavaZipWriterStategy;
7+
8+
import java.util.Comparator;
9+
import java.util.Optional;
10+
11+
/**
12+
* For sorting by {@link CentralDirectoryFileHeader#getFileName()} and {@link LocalFileHeader#getFileName()}.
13+
* This is intended to be used in cases like {@link JavaZipWriterStategy} where offset information is ignored anyways.
14+
*
15+
* @author Matt Coley
16+
*/
17+
public class NameComparator implements Comparator<ZipPart> {
18+
private final Comparator<ZipPart> fallback;
19+
20+
/**
21+
* @param fallback
22+
* Fallback comparator for
23+
*/
24+
public NameComparator(Comparator<ZipPart> fallback) {
25+
this.fallback = fallback;
26+
}
27+
28+
@Override
29+
public int compare(ZipPart o1, ZipPart o2) {
30+
if (o1 instanceof LocalFileHeader && o2 instanceof LocalFileHeader) {
31+
LocalFileHeader header1 = (LocalFileHeader) o1;
32+
LocalFileHeader header2 = (LocalFileHeader) o2;
33+
String name1 = Optional.ofNullable(header1.getLinkedDirectoryFileHeader())
34+
.map(CentralDirectoryFileHeader::getFileName)
35+
.orElse(header1.getFileName());
36+
String name2 = Optional.ofNullable(header2.getLinkedDirectoryFileHeader())
37+
.map(CentralDirectoryFileHeader::getFileName)
38+
.orElse(header2.getFileName());
39+
return name1.compareTo(name2);
40+
} else if (o1 instanceof CentralDirectoryFileHeader && o2 instanceof CentralDirectoryFileHeader) {
41+
CentralDirectoryFileHeader header1 = (CentralDirectoryFileHeader) o1;
42+
CentralDirectoryFileHeader header2 = (CentralDirectoryFileHeader) o2;
43+
String name1 = header1.getFileName();
44+
String name2 = header2.getFileName();
45+
return name1.compareTo(name2);
46+
}
47+
return fallback.compare(o1, o2);
48+
}
49+
}

src/test/java/software/coley/llzip/CompressionTests.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import software.coley.llzip.part.LocalFileHeader;
1010
import software.coley.llzip.strategy.Decompressor;
1111
import software.coley.llzip.strategy.DeflateDecompressor;
12-
import software.coley.llzip.strategy.JvmZipReaderStrategy;
1312

1413
import java.io.IOException;
1514
import java.nio.file.Files;
@@ -60,14 +59,14 @@ public void testDeflateJvmJar() {
6059
// The red herring class that most zip tools see
6160
CentralDirectoryFileHeader redHerringCentralDir = zip.getCentralDirectories().get(0);
6261
assertEquals("Hello.class", redHerringCentralDir.getFileName());
63-
assertNull( redHerringCentralDir.getLinked(), "The red herring central directory got linked");
62+
assertNull( redHerringCentralDir.getLinkedFileHeader(), "The red herring central directory got linked");
6463
byte[] redHerringClassData = zip.getLocalFiles().get(1).decompress(new DeflateDecompressor());
6564
assertDefinesString(redHerringClassData, "Hello world!");
6665
// The real class that gets run by the JVM
6766
CentralDirectoryFileHeader jvmCentralDir = zip.getCentralDirectories().get(3);
6867
assertEquals("Hello.class/", jvmCentralDir.getFileName());
69-
assertNotEquals("Hello.class/", jvmCentralDir.getLinked().getFileName());
70-
byte[] classData = jvmCentralDir.getLinked().decompress(new DeflateDecompressor());
68+
assertNotEquals("Hello.class/", jvmCentralDir.getLinkedFileHeader().getFileName());
69+
byte[] classData = jvmCentralDir.getLinkedFileHeader().decompress(new DeflateDecompressor());
7170
assertDefinesString(classData, "The secret code is: ROSE");
7271
} catch (IOException ex) {
7372
fail(ex);
@@ -84,13 +83,13 @@ public void testDeflateJvmJarWithGarbageHeader() {
8483
// The red herring class that most zip tools see
8584
CentralDirectoryFileHeader redHerringCentralDir = zip.getCentralDirectories().get(1);
8685
assertEquals("Hello\t.class", redHerringCentralDir.getFileName());
87-
byte[] redHerringClassData = redHerringCentralDir.getLinked().decompress(new DeflateDecompressor());
86+
byte[] redHerringClassData = redHerringCentralDir.getLinkedFileHeader().decompress(new DeflateDecompressor());
8887
assertDefinesString(redHerringClassData, "Hello world!");
8988
// The real class that gets run by the JVM
9089
CentralDirectoryFileHeader jvmCentralDir = zip.getCentralDirectories().get(0);
9190
assertEquals("Hello.class/", jvmCentralDir.getFileName());
92-
assertNotEquals("Hello.class/", jvmCentralDir.getLinked().getFileName());
93-
byte[] classData = jvmCentralDir.getLinked().decompress(new DeflateDecompressor());
91+
assertNotEquals("Hello.class/", jvmCentralDir.getLinkedFileHeader().getFileName());
92+
byte[] classData = jvmCentralDir.getLinkedFileHeader().decompress(new DeflateDecompressor());
9493
assertDefinesString(classData, "The secret code is: ROSE");
9594
} catch (IOException ex) {
9695
fail(ex);

src/test/java/software/coley/llzip/PartParseTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public void testJvmTrickJar() {
8989

9090
private static boolean hasFile(ZipArchive zip, String name) {
9191
return zip.getCentralDirectories().stream()
92-
.anyMatch(cdfh -> cdfh.getLinked() != null &&
92+
.anyMatch(cdfh -> cdfh.getLinkedFileHeader() != null &&
9393
cdfh.getFileName().equals(name));
9494
}
9595
}

0 commit comments

Comments
 (0)