Skip to content

Commit 84e953b

Browse files
committed
fix: Documentation, parsing, JVM format fixes
1 parent 3240a1b commit 84e953b

11 files changed

+214
-90
lines changed

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import software.coley.llzip.util.ByteData;
66

77
import java.io.IOException;
8-
import java.nio.ByteBuffer;
98

109
/**
1110
* Constants for {@link LocalFileHeader#getCompressionMethod()}.
@@ -16,7 +15,7 @@ public interface ZipCompressions {
1615
/**
1716
* The file is stored <i>(no compression)</i>.
1817
*/
19-
int STRORED = 0;
18+
int STORED = 0;
2019
/**
2120
* The file is Shrunk.
2221
*/
@@ -134,7 +133,7 @@ public interface ZipCompressions {
134133
*/
135134
static String getName(int method) {
136135
switch (method) {
137-
case STRORED:
136+
case STORED:
138137
return "STRORED";
139138
case SHRUNK:
140139
return "SHRUNK";
@@ -207,7 +206,7 @@ static String getName(int method) {
207206
static ByteData decompress(LocalFileHeader header) throws IOException {
208207
int method = header.getCompressionMethod();
209208
switch (method) {
210-
case STRORED:
209+
case STORED:
211210
return header.getFileData();
212211
case DEFLATED:
213212
return header.decompress(new DeflateDecompressor());
Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package software.coley.llzip.part;
22

3-
import software.coley.llzip.ZipPatterns;
43
import software.coley.llzip.util.ByteData;
5-
import software.coley.llzip.util.ByteDataUtil;
4+
5+
import java.util.NavigableSet;
6+
67

78
/**
89
* An extension of {@link LocalFileHeader} with adjustments to the file-data parse logic to support
@@ -12,16 +13,27 @@
1213
* @author Wolfie / win32kbase <i>(Reverse engineering JVM specific zip handling)</i>
1314
*/
1415
public class JvmLocalFileHeader extends LocalFileHeader {
16+
17+
private final NavigableSet<Long> offsets;
18+
19+
/**
20+
* @param offsets
21+
* Set containing all local file header offsets.
22+
*/
23+
public JvmLocalFileHeader(NavigableSet<Long> offsets) {
24+
this.offsets = offsets;
25+
}
26+
1527
@Override
1628
public void read(ByteData data, long offset) {
1729
super.read(data, offset);
18-
long start = offset + 30 + getFileNameLength() + getExtraFieldLength();
1930
// JVM file data reading does NOT use the compressed/uncompressed fields.
2031
// Instead, it scans data until the next header/EOF.
21-
long nextPk = ByteDataUtil.indexOf(data, offset + 1, ZipPatterns.PK);
22-
if (nextPk == -1L)
23-
nextPk = data.length();
24-
ByteData slice = data.slice(start, nextPk);
25-
setFileData(slice);
32+
offset += 30 + getFileNameLength() + getExtraFieldLength();
33+
Long nextOffset = offsets.ceiling(offset);
34+
if (nextOffset != null)
35+
setFileData(data.slice(offset, nextOffset));
36+
else
37+
setFileData(data.slice(offset, data.length()));
2638
}
2739
}

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import software.coley.llzip.util.ByteDataUtil;
77

88
import java.io.IOException;
9-
import java.nio.ByteBuffer;
9+
10+
import static software.coley.llzip.ZipCompressions.*;
1011

1112
/**
1213
* ZIP LocalFileHeader structure.
@@ -48,7 +49,13 @@ public void read(ByteData data, long offset) {
4849
extraFieldLength = ByteDataUtil.readWord(data, offset + 28);
4950
fileName = data.sliceOf(offset + 30, fileNameLength);
5051
extraField = data.sliceOf(offset + 30 + fileNameLength, extraFieldLength);
51-
fileData = data.sliceOf(offset + 30 + fileNameLength + extraFieldLength, compressedSize);
52+
int fileDataLength;
53+
if (compressionMethod == STORED) {
54+
fileDataLength = uncompressedSize;
55+
} else {
56+
fileDataLength = compressedSize;
57+
}
58+
fileData = data.sliceOf(offset + 30 + fileNameLength + extraFieldLength, fileDataLength);
5259
}
5360

5461
@Override

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

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
import software.coley.llzip.part.LocalFileHeader;
55
import software.coley.llzip.util.BufferData;
66
import software.coley.llzip.util.ByteData;
7-
import software.coley.llzip.util.ByteDataInputStream;
87

98
import java.io.ByteArrayOutputStream;
109
import java.io.IOException;
10+
import java.util.zip.DataFormatException;
1111
import java.util.zip.Inflater;
12-
import java.util.zip.InflaterInputStream;
12+
import java.util.zip.ZipException;
1313

1414
/**
1515
* Decompressor implementation for {@link ZipCompressions#DEFLATED}.
@@ -24,12 +24,34 @@ public ByteData decompress(LocalFileHeader header, ByteData bytes) throws IOExce
2424
Inflater inflater = new Inflater(true);
2525
ByteArrayOutputStream out = new ByteArrayOutputStream();
2626
try {
27-
InflaterInputStream in = new InflaterInputStream(new ByteDataInputStream(bytes), inflater);
28-
// We can't trust the uncompressed size, so we will keep going until the inflater stream says we're done.
27+
byte[] output = new byte[1024];
2928
byte[] buffer = new byte[1024];
30-
int len;
31-
while ((len = in.read(buffer)) != -1)
32-
out.write(buffer, 0, len);
29+
long position = 0L;
30+
long length = bytes.length();
31+
boolean eof;
32+
do {
33+
eof = false;
34+
if (inflater.needsInput()) {
35+
int remaining = (int) Math.min(buffer.length, length);
36+
if (remaining == 0) {
37+
buffer[0] = 0;
38+
inflater.setInput(buffer, 0, 1);
39+
eof = true;
40+
} else {
41+
bytes.get(position, buffer, 0, remaining);
42+
length -= remaining;
43+
position += remaining;
44+
inflater.setInput(buffer, 0, remaining);
45+
}
46+
}
47+
int count = inflater.inflate(output);
48+
if (count != 0) {
49+
out.write(output, 0, count);
50+
}
51+
} while (!eof && !inflater.finished() && !inflater.needsDictionary());
52+
}catch (DataFormatException e) {
53+
String s = e.getMessage();
54+
throw (ZipException) new ZipException(s != null ? null : "Invalid ZLIB data format").initCause(e);
3355
} finally {
3456
inflater.end();
3557
}

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

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.io.IOException;
1616
import java.util.HashSet;
1717
import java.util.Set;
18+
import java.util.TreeSet;
1819

1920
/**
2021
* The JVM has some edge cases in how it parses zip/jar files.
@@ -41,8 +42,8 @@ public void read(ZipArchive zip, ByteData data) throws IOException {
4142
long precedingEndOfCentralDirectory = ByteDataUtil.lastIndexOf(data, endOfCentralDirectoryOffset - 1, ZipPatterns.END_OF_CENTRAL_DIRECTORY);
4243
if (precedingEndOfCentralDirectory == endOfCentralDirectoryOffset) {
4344
// The prior end part match is target end part, so we can't use it as a base offset.
44-
jvmBaseOffset = 0;
45-
} else if (precedingEndOfCentralDirectory == -1) {
45+
jvmBaseOffset = 0L;
46+
} else if (precedingEndOfCentralDirectory == -1L) {
4647
// There was no match for a prior end part. We will seek forwards until finding a *VALID* PK starting header.
4748
jvmBaseOffset = ByteDataUtil.indexOf(data, ZipPatterns.PK);
4849
while (jvmBaseOffset >= 0L) {
@@ -56,13 +57,13 @@ else if (ByteDataUtil.startsWith(data, jvmBaseOffset, ZipPatterns.CENTRAL_DIRECT
5657
break;
5758
} catch (Exception ex) {
5859
// Invalid, seek forward
59-
jvmBaseOffset = ByteDataUtil.indexOf(data, jvmBaseOffset+1, ZipPatterns.PK);
60+
jvmBaseOffset = ByteDataUtil.indexOf(data, jvmBaseOffset+1L, ZipPatterns.PK);
6061
}
6162
}
6263
} else {
6364
// There was a prior end part, so we will seek past it's length and use that as the base offset.
6465
// 22 is the minimum possible size of an end part. It can be longer with comments applied, but there are almost never comments.
65-
jvmBaseOffset = precedingEndOfCentralDirectory + 22;
66+
jvmBaseOffset = precedingEndOfCentralDirectory + 22L;
6667
}
6768
// Read end header
6869
EndOfCentralDirectory end = new EndOfCentralDirectory();
@@ -71,9 +72,9 @@ else if (ByteDataUtil.startsWith(data, jvmBaseOffset, ZipPatterns.CENTRAL_DIRECT
7172
// Read central directories
7273
long len = data.length();
7374
long centralDirectoryOffset = len - ZipPatterns.CENTRAL_DIRECTORY_FILE_HEADER.length;
74-
while (centralDirectoryOffset > 0) {
75-
centralDirectoryOffset = ByteDataUtil.lastIndexOf(data, centralDirectoryOffset - 1, ZipPatterns.CENTRAL_DIRECTORY_FILE_HEADER);
76-
if (centralDirectoryOffset >= 0) {
75+
while (centralDirectoryOffset > 0L) {
76+
centralDirectoryOffset = ByteDataUtil.lastIndexOf(data, centralDirectoryOffset - 1L, ZipPatterns.CENTRAL_DIRECTORY_FILE_HEADER);
77+
if (centralDirectoryOffset >= 0L) {
7778
CentralDirectoryFileHeader directory = new CentralDirectoryFileHeader();
7879
directory.read(data, centralDirectoryOffset);
7980
zip.getParts().add(directory);
@@ -82,12 +83,15 @@ else if (ByteDataUtil.startsWith(data, jvmBaseOffset, ZipPatterns.CENTRAL_DIRECT
8283
// Read local files
8384
// - Set to prevent duplicate file header entries for the same offset
8485
Set<Long> offsets = new HashSet<>();
86+
TreeSet<Long> lfhOffsets = new TreeSet<>();
87+
for (CentralDirectoryFileHeader directory : zip.getCentralDirectories()) {
88+
lfhOffsets.add(jvmBaseOffset + directory.getRelativeOffsetOfLocalHeader());
89+
}
8590
for (CentralDirectoryFileHeader directory : zip.getCentralDirectories()) {
8691
long offset = jvmBaseOffset + directory.getRelativeOffsetOfLocalHeader();
8792
if (!offsets.contains(offset) && ByteDataUtil.startsWith(data, offset, ZipPatterns.LOCAL_FILE_HEADER)) {
88-
// JVM local file header needs to be aware of where the NEXT entry is.
8993
try {
90-
JvmLocalFileHeader file = new JvmLocalFileHeader();
94+
JvmLocalFileHeader file = new JvmLocalFileHeader(lfhOffsets);
9195
file.read(data, offset);
9296
zip.getParts().add(file);
9397
directory.link(file);

src/main/java/software/coley/llzip/util/BufferData.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package software.coley.llzip.util;
22

3+
import java.io.IOException;
4+
import java.io.OutputStream;
35
import java.nio.ByteBuffer;
46
import java.nio.ByteOrder;
57

@@ -39,6 +41,25 @@ public void get(long position, byte[] b, int off, int len) {
3941
.get(b, off, len);
4042
}
4143

44+
@Override
45+
public void transferTo(OutputStream out, byte[] buf) throws IOException {
46+
ByteBuffer buffer = this.buffer;
47+
int remaining = buffer.remaining();
48+
if (buffer.hasArray()) {
49+
out.write(buffer.array(), buffer.arrayOffset(), remaining);
50+
} else {
51+
buffer.mark();
52+
int copyThreshold = buf.length;
53+
while (remaining != 0) {
54+
int length = Math.min(copyThreshold, remaining);
55+
buffer.get(buf, 0, length);
56+
out.write(buf, 0, length);
57+
remaining -= length;
58+
}
59+
buffer.reset();
60+
}
61+
}
62+
4263
@Override
4364
public ByteData slice(long startIndex, long endIndex) {
4465
return new BufferData(ByteDataUtil.sliceExact(buffer, validate(startIndex), validate(endIndex)));
@@ -69,6 +90,16 @@ private static int validate(long v) {
6990
return (int) v;
7091
}
7192

93+
/**
94+
* @param buffer
95+
* Byte buffer to wrap.
96+
*
97+
* @return Buffer data.
98+
*/
99+
public static BufferData wrap(ByteBuffer buffer) {
100+
return new BufferData(buffer);
101+
}
102+
72103
/**
73104
* @param array
74105
* Byte array to wrap.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,94 @@
11
package software.coley.llzip.util;
22

3+
import java.io.IOException;
4+
import java.io.OutputStream;
5+
36
/**
47
* Byte array data.
58
*
69
* @author xDark
710
*/
811
public interface ByteData {
912

13+
/**
14+
* Gets int at specific position.
15+
*
16+
* @param position
17+
* Position to read int from.
18+
*
19+
* @return Read int.
20+
*/
1021
int getInt(long position);
1122

23+
/**
24+
* Gets short at specific position.
25+
*
26+
* @param position
27+
* Position to short int from.
28+
*
29+
* @return Read short.
30+
*/
1231
short getShort(long position);
1332

33+
/**
34+
* Gets byte at specific position.
35+
*
36+
* @param position
37+
* Position to read byte from.
38+
*
39+
* @return Read byte.
40+
*/
1441
byte get(long position);
1542

43+
/**
44+
* Copied array of bytes from position.
45+
*
46+
* @param position
47+
* Position to copy from.
48+
*/
1649
void get(long position, byte[] b, int off, int len);
1750

51+
/**
52+
* Transfers data to target output stream.
53+
*
54+
* @param out
55+
* Stream to transfer data to.
56+
* @param buf
57+
* Buffer to use for transferring.
58+
*
59+
* @throws IOException
60+
* If any I/O error occurs.
61+
*/
62+
void transferTo(OutputStream out, byte[] buf) throws IOException;
63+
64+
/**
65+
* Makes a slice of data.
66+
*
67+
* @param startIndex
68+
* Starting index.
69+
* @param endIndex
70+
* End index.
71+
*
72+
* @return Slice of this data.
73+
*/
1874
ByteData slice(long startIndex, long endIndex);
1975

76+
/**
77+
* Same as above, but with relative length.
78+
*
79+
* @param startIndex
80+
* Starting index.
81+
* @param length
82+
* Slice length.
83+
*
84+
* @return Slice of this data.
85+
*/
2086
default ByteData sliceOf(long startIndex, long length) {
2187
return slice(startIndex, startIndex + length);
2288
}
2389

90+
/**
91+
* @return Length of the data.
92+
*/
2493
long length();
2594
}

0 commit comments

Comments
 (0)