61 | * Specifically, APK verity tree is built from the APK, but as if the APK Signing Block (which
62 | * must be page aligned) and the "Central Directory offset" field in End of Central Directory
63 | * are skipped.
64 | */
65 | byte[] generateVerityTreeRootHash(DataSource beforeApkSigningBlock, DataSource centralDir,
66 | DataSource eocd) throws IOException {
67 | if (beforeApkSigningBlock.size() % CHUNK_SIZE != 0) {
68 | throw new IllegalStateException("APK Signing Block size not a multiple of " + CHUNK_SIZE
69 | + ": " + beforeApkSigningBlock.size());
70 | }
71 |
72 | return generateVerityTreeRootHash(DataSources.link(beforeApkSigningBlock, centralDir, eocd));
73 | }
74 |
75 | /**
76 | * Returns the root hash of the verity tree built from the data source.
77 | *
78 | * The tree is built bottom up. The bottom level has 256-bit digest for each 4 KB block in the
79 | * input file. If the total size is larger than 4 KB, take this level as input and repeat the
80 | * same procedure, until the level is within 4 KB. If salt is given, it will apply to each
81 | * digestion before the actual data.
82 | *
83 | * The returned root hash is calculated from the last level of 4 KB chunk, similarly with salt.
84 | *
85 | * The tree is currently stored only in memory and is never written out. Nevertheless, it is
86 | * the actual verity tree format on disk, and is supposed to be re-generated on device.
87 | *
88 | * This is package-private for testing purpose.
89 | */
90 | private byte[] generateVerityTreeRootHash(DataSource fileSource) throws IOException {
91 | int digestSize = mMd.getDigestLength();
92 |
93 | // Calculate the summed area table of level size. In other word, this is the offset
94 | // table of each level, plus the next non-existing level.
95 | int[] levelOffset = calculateLevelOffset(fileSource.size(), digestSize);
96 |
97 |
98 | byte[] verityBuffer = new byte[levelOffset[levelOffset.length - 1]];
99 |
100 | // Generate the hash tree bottom-up.
101 | for (int i = levelOffset.length - 2; i >= 0; i--) {
102 | DataSink middleBufferSink = DataSinks.fromData(verityBuffer, levelOffset[i], levelOffset[i + 1]);
103 | DataSource src;
104 | if (i == levelOffset.length - 2) {
105 | src = fileSource;
106 | } else {
107 | int start = levelOffset[i + 1];
108 | int end = levelOffset[i + 2];
109 | src = DataSources.fromData(verityBuffer, start, end - start);
110 | }
111 | digestDataByChunks(src, middleBufferSink);
112 |
113 | // If the output is not full chunk, pad with 0s.
114 | long totalOutput = divideRoundup(src.size()) * digestSize;
115 | int incomplete = (int) (totalOutput % CHUNK_SIZE);
116 | if (incomplete > 0) {
117 | byte[] padding = new byte[CHUNK_SIZE - incomplete];
118 | middleBufferSink.consume(padding, 0, padding.length);
119 | }
120 | }
121 |
122 | // Finally, calculate the root hash from the top level (only page).
123 | return saltedDigest(verityBuffer);
124 | }
125 |
126 | /**
127 | * Returns an array of summed area table of level size in the verity tree. In other words, the
128 | * returned array is offset of each level in the verity tree file format, plus an additional
129 | * offset of the next non-existing level (i.e. end of the last level + 1). Thus the array size
130 | * is level + 1.
131 | */
132 | private static int[] calculateLevelOffset(long dataSize, int digestSize) {
133 | // Compute total size of each level, bottom to top.
134 | ArrayList levelSize = new ArrayList<>();
135 | while (true) {
136 | long chunkCount = divideRoundup(dataSize);
137 | long size = CHUNK_SIZE * divideRoundup(chunkCount * digestSize);
138 | levelSize.add(size);
139 | if (chunkCount * digestSize <= CHUNK_SIZE) {
140 | break;
141 | }
142 | dataSize = chunkCount * digestSize;
143 | }
144 |
145 | // Reverse and convert to summed area table.
146 | int[] levelOffset = new int[levelSize.size() + 1];
147 | levelOffset[0] = 0;
148 | for (int i = 0; i < levelSize.size(); i++) {
149 | // We don't support verity tree if it is larger then Integer.MAX_VALUE.
150 | levelOffset[i + 1] = levelOffset[i] + toIntExact(
151 | levelSize.get(levelSize.size() - i - 1));
152 | }
153 | return levelOffset;
154 | }
155 |
156 | /**
157 | * Digest data source by chunks then feeds them to the sink one by one. If the last unit is
158 | * less than the chunk size and padding is desired, feed with extra padding 0 to fill up the
159 | * chunk before digesting.
160 | */
161 | private void digestDataByChunks(DataSource dataSource, DataSink dataSink) throws IOException {
162 | dataSource = dataSource.align(CHUNK_SIZE);
163 | long size = dataSource.size();
164 | long offset = 0;
165 | for (; offset + CHUNK_SIZE <= size; offset += CHUNK_SIZE) {
166 | byte[] hash = saltedDigest(dataSource);
167 | dataSink.consume(hash, 0, hash.length);
168 | }
169 |
170 | // Send the last incomplete chunk with 0 padding to the sink at once.
171 | int remaining = (int) (size % CHUNK_SIZE);
172 | if (remaining > 0) {
173 | throw new IllegalStateException("Remaining: " + remaining);
174 | }
175 | }
176 |
177 | private byte[] saltedDigest(DataSource source) throws IOException {
178 | mMd.reset();
179 | if (mSalt != null) {
180 | mMd.update(mSalt);
181 | }
182 | source.copyTo(mMd, VerityTreeBuilder.CHUNK_SIZE);
183 | return mMd.digest();
184 | }
185 |
186 | private byte[] saltedDigest(byte[] data) {
187 | mMd.reset();
188 | if (mSalt != null) {
189 | mMd.update(mSalt);
190 | }
191 | mMd.update(data, 0, VerityTreeBuilder.CHUNK_SIZE);
192 | return mMd.digest();
193 | }
194 |
195 | /**
196 | * Divides a number and round up to the closest integer.
197 | */
198 | private static long divideRoundup(long dividend) {
199 | return (dividend + (long) VerityTreeBuilder.CHUNK_SIZE - 1) / (long) VerityTreeBuilder.CHUNK_SIZE;
200 | }
201 |
202 | private static int toIntExact(long value) {
203 | if ((int) value != value) {
204 | throw new ArithmeticException("integer overflow");
205 | }
206 | return (int) value;
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/src/bin/mt/apksign/ZipBuffer.java:
--------------------------------------------------------------------------------
1 | package bin.mt.apksign;
2 |
3 | import bin.io.RandomAccessFile;
4 |
5 | import java.io.EOFException;
6 | import java.io.IOException;
7 |
8 | class ZipBuffer {
9 | static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
10 | static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
11 | static final int EOCD_SIG = 0X06054B50;
12 | static final int MIN_EOCD_SIZE = 22;
13 | static final int MAX_EOCD_SIZE = MIN_EOCD_SIZE + 0xFFFF;
14 | static final int APK_SIG_BLOCK_MIN_SIZE = 32;
15 |
16 | private final long entriesDataSizeBytes;
17 | private final long centralDirectoryOffset;
18 | private final long centralDirectorySizeBytes;
19 | private final long eocdOffset;
20 | private final boolean hasApkSigBlock;
21 | private final RandomAccessFile file;
22 |
23 | ZipBuffer(RandomAccessFile file) throws IOException {
24 | this.file = file;
25 | boolean found = false;
26 | long length = length();
27 | long off = length - MIN_EOCD_SIZE;
28 | final long stopSearching =
29 | Math.max(0L, length - MAX_EOCD_SIZE);
30 | while (off >= stopSearching) {
31 | seek(off);
32 | if (readInt() == EOCD_SIG) {
33 | found = true;
34 | break;
35 | }
36 | off--;
37 | }
38 | if (!found) {
39 | throw new IOException("Archive is not a ZIP archive");
40 | }
41 |
42 | eocdOffset = off;
43 | // 没做zip64支持
44 | seek(off + 12);
45 | centralDirectorySizeBytes = readUInt();
46 | centralDirectoryOffset = readUInt();
47 |
48 | long entriesDataEnd = centralDirectoryOffset;
49 | boolean matchV2SigBlock = false;
50 | try {
51 | if (centralDirectoryOffset >= APK_SIG_BLOCK_MIN_SIZE) {
52 | seek(centralDirectoryOffset - 16);
53 | if (readLong() == APK_SIG_BLOCK_MAGIC_LO && readLong() == APK_SIG_BLOCK_MAGIC_HI) {
54 | seek(centralDirectoryOffset - 24);
55 | long size = readLong();
56 | long sigStart = centralDirectoryOffset - size - 8;
57 | seek(sigStart);
58 | if (readLong() == size) {
59 | matchV2SigBlock = true;
60 | entriesDataEnd = sigStart;
61 | }
62 | }
63 | }
64 | } catch (Exception e) {
65 | e.printStackTrace();
66 | }
67 | entriesDataSizeBytes = entriesDataEnd;
68 | hasApkSigBlock = matchV2SigBlock;
69 | }
70 |
71 | public long length() throws IOException {
72 | return file.length();
73 | }
74 |
75 | public void seek(long position) throws IOException {
76 | file.seek(position);
77 | }
78 |
79 | public long position() throws IOException {
80 | return file.getFilePointer();
81 | }
82 |
83 | public void skip(int length) throws IOException {
84 | if (length < 0)
85 | throw new IOException("Skip " + length);
86 | long pos = file.getFilePointer() + length;
87 | long len = file.length();
88 | if (pos > len)
89 | throw new EOFException();
90 | file.seek(pos);
91 | }
92 |
93 | public byte[] readBytes(int len) throws IOException {
94 | byte[] bytes = new byte[len];
95 | file.readFully(bytes);
96 | return bytes;
97 | }
98 |
99 | public int readInt() throws IOException {
100 | int ch1 = file.read();
101 | int ch2 = file.read();
102 | int ch3 = file.read();
103 | int ch4 = file.read();
104 | if ((ch1 | ch2 | ch3 | ch4) < 0)
105 | throw new EOFException();
106 | return (ch1) | (ch2 << 8) | (ch3 << 16) | (ch4 << 24);
107 | }
108 |
109 | public long readLong() throws IOException {
110 | long ch1 = file.read();
111 | long ch2 = file.read();
112 | long ch3 = file.read();
113 | long ch4 = file.read();
114 | long ch5 = file.read();
115 | long ch6 = file.read();
116 | long ch7 = file.read();
117 | long ch8 = file.read();
118 | if ((ch1 | ch2 | ch3 | ch4) < 0)
119 | throw new EOFException();
120 | return (ch1) | (ch2 << 8) | (ch3 << 16) | (ch4 << 24) | (ch5 << 32) | (ch6 << 40) | (ch7 << 48) | (ch8 << 56);
121 |
122 | }
123 |
124 | public long readUInt() throws IOException {
125 | return readInt() & 0xFFFFFFFFL;
126 | }
127 |
128 | public long getEntriesDataSizeBytes() {
129 | return entriesDataSizeBytes;
130 | }
131 |
132 | public long getCentralDirectoryOffset() {
133 | return centralDirectoryOffset;
134 | }
135 |
136 | public long getCentralDirectorySizeBytes() {
137 | return centralDirectorySizeBytes;
138 | }
139 |
140 | public long getEocdOffset() {
141 | return eocdOffset;
142 | }
143 |
144 | public boolean hasApkSigBlock() {
145 | return hasApkSigBlock;
146 | }
147 |
148 | public RandomAccessFile getFile() {
149 | return file;
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/src/bin/mt/apksign/data/ByteArrayDataSink.java:
--------------------------------------------------------------------------------
1 | package bin.mt.apksign.data;
2 |
3 | import java.io.EOFException;
4 | import java.io.IOException;
5 |
6 | public class ByteArrayDataSink implements DataSink {
7 | private byte[] data;
8 | private int pos;
9 | private int limit;
10 |
11 | ByteArrayDataSink(byte[] data, int pos, int limit) {
12 | this.data = data;
13 | this.pos = pos;
14 | this.limit = limit;
15 | }
16 |
17 | @Override
18 | public void consume(byte[] buf, int offset, int length) throws IOException {
19 | if (pos + length > limit) {
20 | throw new EOFException();
21 | }
22 | System.arraycopy(buf, offset, data, pos, length);
23 | pos += length;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/bin/mt/apksign/data/ByteArrayDataSource.java:
--------------------------------------------------------------------------------
1 | package bin.mt.apksign.data;
2 |
3 | import java.io.EOFException;
4 | import java.io.IOException;
5 | import java.io.OutputStream;
6 |
7 | public class ByteArrayDataSource implements DataSource {
8 | private byte[] data;
9 | private int start;
10 | private int size;
11 | private int pos;
12 |
13 | ByteArrayDataSource(byte[] data, int start, int size) {
14 | if (start + size > data.length)
15 | throw new IllegalArgumentException();
16 | this.data = data;
17 | this.start = start;
18 | this.size = size;
19 | this.pos = 0;
20 | }
21 |
22 | @Override
23 | public long size() {
24 | return size;
25 | }
26 |
27 | @Override
28 | public long pos() {
29 | return pos;
30 | }
31 |
32 | @Override
33 | public void reset() {
34 | pos = 0;
35 | }
36 |
37 | @Override
38 | public void copyTo(OutputStream os, long length) throws IOException {
39 | if (length > remaining())
40 | throw new EOFException();
41 | os.write(data, start + pos, (int) length);
42 | pos += length;
43 | }
44 |
45 | public byte[] getBuffer() {
46 | return data;
47 | }
48 |
49 | public int getStart() {
50 | return start;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/bin/mt/apksign/data/ChainedDataSource.java:
--------------------------------------------------------------------------------
1 | package bin.mt.apksign.data;
2 |
3 | import java.io.EOFException;
4 | import java.io.IOException;
5 | import java.io.OutputStream;
6 |
7 | public class ChainedDataSource implements DataSource {
8 | private DataSource[] sources;
9 | private DataSource currentSource;
10 | private int currentIndex;
11 | private long size;
12 | private long pos;
13 |
14 | ChainedDataSource(DataSource... sources) {
15 | if (sources.length == 0)
16 | throw new IllegalArgumentException();
17 | this.sources = sources;
18 | this.currentIndex = 0;
19 | this.currentSource = sources[currentIndex];
20 | this.pos = 0;
21 | this.size = 0;
22 | for (DataSource source : sources) {
23 | size += source.size();
24 | }
25 | }
26 |
27 | @Override
28 | public long size() {
29 | return size;
30 | }
31 |
32 | @Override
33 | public long pos() {
34 | return pos;
35 | }
36 |
37 | @Override
38 | public void reset() throws IOException {
39 | this.currentIndex = 0;
40 | this.currentSource = sources[currentIndex];
41 | this.pos = 0;
42 | for (DataSource source : sources) {
43 | source.reset();
44 | }
45 | }
46 |
47 | @Override
48 | public void copyTo(OutputStream os, long length) throws IOException {
49 | if (length > remaining())
50 | throw new EOFException();
51 | while (length > 0) {
52 | long len = Math.min(length, currentSource.remaining());
53 | currentSource.copyTo(os, len);
54 | length -= len;
55 | pos += len;
56 | if (currentSource.remaining() == 0 && currentIndex < sources.length - 1) {
57 | currentSource = sources[++currentIndex];
58 | }
59 | }
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/src/bin/mt/apksign/data/DataSink.java:
--------------------------------------------------------------------------------
1 | package bin.mt.apksign.data;
2 |
3 | import java.io.IOException;
4 |
5 | public interface DataSink {
6 |
7 | void consume(byte[] buf, int offset, int length) throws IOException;
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/src/bin/mt/apksign/data/DataSinks.java:
--------------------------------------------------------------------------------
1 | package bin.mt.apksign.data;
2 |
3 | public class DataSinks {
4 |
5 | public static DataSink fromData(byte[] data) {
6 | return fromData(data, 0, data.length);
7 | }
8 |
9 | public static DataSink fromData(byte[] data, int position, int limit) {
10 | return new ByteArrayDataSink(data, position, limit);
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/bin/mt/apksign/data/DataSource.java:
--------------------------------------------------------------------------------
1 | package bin.mt.apksign.data;
2 |
3 | import bin.io.RandomAccessFile;
4 |
5 | import java.io.ByteArrayOutputStream;
6 | import java.io.IOException;
7 | import java.io.OutputStream;
8 | import java.security.MessageDigest;
9 |
10 | public interface DataSource {
11 |
12 | long size();
13 |
14 | long pos();
15 |
16 | default long remaining() {
17 | return size() - pos();
18 | }
19 |
20 | void reset() throws IOException;
21 |
22 | void copyTo(OutputStream os, long length) throws IOException;
23 |
24 | default void copyTo(MessageDigest digest, long length) throws IOException {
25 | OutputStream os = new OutputStream() {
26 | @Override
27 | public void write(int b) {
28 | digest.update((byte) b);
29 | }
30 |
31 | @Override
32 | public void write(byte[] b, int off, int len) {
33 | digest.update(b, off, len);
34 | }
35 | };
36 | copyTo(os, length);
37 | }
38 |
39 | default void copyTo(RandomAccessFile accessFile, long length) throws IOException {
40 | OutputStream os = new OutputStream() {
41 | @Override
42 | public void write(int b) throws IOException {
43 | accessFile.write(b);
44 | }
45 |
46 | @Override
47 | public void write(byte[] b, int off, int len) throws IOException {
48 | accessFile.write(b, off, len);
49 | }
50 | };
51 | copyTo(os, length);
52 | }
53 |
54 | default DataSource align(int align) {
55 | return DataSources.align(this, align);
56 | }
57 |
58 | default ByteArrayDataSource toMemory() throws IOException {
59 | long remaining = remaining();
60 | if (remaining > Integer.MAX_VALUE) {
61 | throw new IOException("Data too large");
62 | }
63 | ByteArrayOutputStream baos = new ByteArrayOutputStream((int) remaining);
64 | copyTo(baos, remaining);
65 | return (ByteArrayDataSource) DataSources.fromData(baos.toByteArray());
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/src/bin/mt/apksign/data/DataSources.java:
--------------------------------------------------------------------------------
1 | package bin.mt.apksign.data;
2 |
3 | import bin.io.RandomAccessFile;
4 |
5 | import java.io.IOException;
6 |
7 | public class DataSources {
8 |
9 | public static DataSource fromFile(RandomAccessFile randomAccessFile, long start, long size) {
10 | return new FileDataSource(randomAccessFile, start, size);
11 | }
12 |
13 | public static DataSource fromData(byte[] data) {
14 | return fromData(data, 0, data.length);
15 | }
16 |
17 | public static DataSource fromData(byte[] data, int start, int size) {
18 | return new ByteArrayDataSource(data, start, size);
19 | }
20 |
21 | public static DataSource align(DataSource source, int align) {
22 | long size = source.size();
23 | int overCount = (int) (size % align);
24 | if (overCount == 0)
25 | return source;
26 | int fillCount = align - overCount;
27 | return link(source, fromData(new byte[fillCount]));
28 | }
29 |
30 | public static DataSource link(DataSource... sources) {
31 | return new ChainedDataSource(sources);
32 | }
33 |
34 | public static void reset(DataSource... sources) throws IOException {
35 | for (DataSource source : sources) {
36 | source.reset();
37 | }
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/src/bin/mt/apksign/data/FileDataSource.java:
--------------------------------------------------------------------------------
1 | package bin.mt.apksign.data;
2 |
3 | import bin.io.RandomAccessFile;
4 |
5 | import java.io.EOFException;
6 | import java.io.IOException;
7 | import java.io.OutputStream;
8 |
9 | public class FileDataSource implements DataSource {
10 | private RandomAccessFile randomAccessFile;
11 | private long start;
12 | private long size;
13 | private long pos;
14 |
15 | FileDataSource(RandomAccessFile randomAccessFile, long start, long size) {
16 | this.randomAccessFile = randomAccessFile;
17 | this.start = start;
18 | this.size = size;
19 | }
20 |
21 |
22 | @Override
23 | public long size() {
24 | return size;
25 | }
26 |
27 | @Override
28 | public long pos() {
29 | return pos;
30 | }
31 |
32 | @Override
33 | public void reset() {
34 | pos = 0;
35 | }
36 |
37 | @Override
38 | public void copyTo(OutputStream os, long length) throws IOException {
39 | if (length > remaining())
40 | throw new EOFException();
41 | byte[] buf = new byte[4096];
42 | int readLen;
43 | randomAccessFile.seek(start + pos);
44 | while (length > 0 && (readLen = randomAccessFile.read(buf, 0, (int) Math.min(length, buf.length))) != -1) {
45 | os.write(buf, 0, readLen);
46 | length -= readLen;
47 | pos += readLen;
48 | }
49 | if (length != 0)
50 | throw new IllegalStateException("Remaining length: " + length);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/bin/mt/apksign/key/JksSignatureKey.java:
--------------------------------------------------------------------------------
1 | package bin.mt.apksign.key;
2 |
3 | import java.io.File;
4 | import java.io.FileInputStream;
5 | import java.security.KeyStore;
6 | import java.security.PrivateKey;
7 | import java.security.cert.X509Certificate;
8 |
9 | public class JksSignatureKey implements SignatureKey {
10 | private X509Certificate certificate;
11 | private PrivateKey privateKey;
12 |
13 | public JksSignatureKey(String path, String storePassword, String alias, String aliasPassword) throws Exception {
14 | this(new File(path), storePassword, alias, aliasPassword);
15 | }
16 |
17 | public JksSignatureKey(File file, String storePassword, String alias, String aliasPassword) throws Exception {
18 | KeyStore keyStore = KeyStore.getInstance("jks");
19 | keyStore.load(new FileInputStream(file), storePassword.toCharArray());
20 | certificate = (X509Certificate) keyStore.getCertificate(alias);
21 | privateKey = (PrivateKey) keyStore.getKey(alias, aliasPassword.toCharArray());
22 | }
23 |
24 | @Override
25 | public X509Certificate getCertificate() {
26 | return certificate;
27 | }
28 |
29 | @Override
30 | public PrivateKey getPrivateKey() {
31 | return privateKey;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/bin/mt/apksign/key/SignatureKey.java:
--------------------------------------------------------------------------------
1 | package bin.mt.apksign.key;
2 |
3 | import java.security.PrivateKey;
4 | import java.security.cert.X509Certificate;
5 |
6 | public interface SignatureKey {
7 |
8 | X509Certificate getCertificate() throws Exception;
9 |
10 | PrivateKey getPrivateKey() throws Exception;
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/src/bin/zip/BridgeInputStream.java:
--------------------------------------------------------------------------------
1 | package bin.zip;
2 |
3 | import bin.io.RandomAccessFile;
4 |
5 | import java.io.IOException;
6 | import java.io.InputStream;
7 |
8 | /**
9 | * @author Bin
10 | */
11 | public class BridgeInputStream extends InputStream {
12 | private final RandomAccessFile archive;
13 | private long remaining;
14 | private long loc;
15 |
16 | public BridgeInputStream(RandomAccessFile archive, long start, long remaining) {
17 | this.archive = archive;
18 | this.remaining = remaining;
19 | loc = start;
20 | }
21 |
22 | public int read() throws IOException {
23 | if (remaining-- <= 0) {
24 | return -1;
25 | }
26 | synchronized (archive) {
27 | archive.seek(loc++);
28 | return archive.read();
29 | }
30 | }
31 |
32 | @Override
33 | public int available() {
34 | return (int) (remaining & Integer.MAX_VALUE);
35 | }
36 |
37 | public int read(byte[] b, int off, int len) throws IOException {
38 | if (remaining <= 0) {
39 | return -1;
40 | }
41 |
42 | if (len <= 0) {
43 | return 0;
44 | }
45 |
46 | if (len > remaining) {
47 | len = (int) remaining;
48 | }
49 | int ret;
50 | synchronized (archive) {
51 | archive.seek(loc);
52 | ret = archive.read(b, off, len);
53 | }
54 | if (ret > 0) {
55 | loc += ret;
56 | remaining -= ret;
57 | }
58 | return ret;
59 | }
60 |
61 | }
--------------------------------------------------------------------------------
/src/bin/zip/BridgeOutputStream.java:
--------------------------------------------------------------------------------
1 | package bin.zip;
2 |
3 | import bin.io.RandomAccessFile;
4 |
5 | import java.io.IOException;
6 | import java.io.OutputStream;
7 |
8 | /**
9 | * @author Bin
10 | */
11 | public class BridgeOutputStream extends OutputStream {
12 | private final RandomAccessFile archive;
13 | private long count = 0;
14 |
15 | public BridgeOutputStream(RandomAccessFile archive) {
16 | this.archive = archive;
17 | }
18 |
19 | @Override
20 | public void write(byte[] b, int off, int len) throws IOException {
21 | if (len > 0) {
22 | archive.write(b, off, len);
23 | count += len;
24 | }
25 | }
26 |
27 | @Override
28 | public void write(int b) throws IOException {
29 | archive.write(b);
30 | count++;
31 | }
32 |
33 | public long getCount() {
34 | return count;
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/src/bin/zip/CenterFileHeader.java:
--------------------------------------------------------------------------------
1 | package bin.zip;
2 |
3 | /**
4 | * @author Bin
5 | */
6 | class CenterFileHeader implements Comparable {
7 | int generalPurposeFlag;
8 | int method;
9 | int time;
10 | int crc;
11 | long compressedSize;
12 | long size;
13 | String nameStr;
14 | byte[] name;
15 | byte[] extra;
16 | byte[] comment;
17 | int diskNumberStart;
18 | int internalAttributes;
19 | int externalAttributes;
20 | long headerOffset;
21 | long dataOffset;
22 | boolean isDirectory;
23 | boolean isUtf8;
24 | boolean isHost;
25 | boolean sizeNeedZip64;
26 | boolean offsetNeedZip64;
27 |
28 | CenterFileHeader(String name) {
29 | this.nameStr = name;
30 | this.name = name.getBytes(ZipConstant.UTF_8);
31 | isUtf8 = true;
32 | extra = new byte[0];
33 | comment = new byte[0];
34 | time = (int) ZipUtil.javaToDosTime(System.currentTimeMillis());
35 | isDirectory = name.endsWith("/") || name.endsWith("\\");
36 | compressedSize = ZipEntry.UNKNOWN_SIZE;
37 | size = ZipEntry.UNKNOWN_SIZE;
38 | }
39 |
40 | CenterFileHeader(ZipEntry entry) {
41 | nameStr = entry.getName();
42 | name = entry.getName().getBytes(ZipConstant.UTF_8);
43 | isUtf8 = true;
44 | time = (int) ZipUtil.javaToDosTime(entry.getTime());
45 | method = entry.getMethod();
46 | crc = entry.getCrc();
47 | compressedSize = entry.getCompressedSize();
48 | size = entry.getSize();
49 | extra = entry.getExtra() == null ? new byte[0] : entry.getExtra();
50 | comment = entry.getCommentData() == null ? new byte[0] : entry.getCommentData();
51 | internalAttributes = entry.getInternalAttributes();
52 | externalAttributes = entry.getExternalAttributes();
53 | isDirectory = entry.isDirectory();
54 | }
55 |
56 | boolean needZip64() {
57 | return sizeNeedZip64 || offsetNeedZip64;
58 | }
59 |
60 | boolean isEncrypted() {
61 | return (generalPurposeFlag & 1) != 0;
62 | }
63 |
64 | int version() {
65 | if (needZip64()) {
66 | return 45;
67 | } else if (method == ZipConstant.METHOD_STORED && !isEncrypted()) {
68 | return 10;
69 | } else {
70 | return 20;
71 | }
72 | }
73 |
74 | @Override
75 | public int compareTo(CenterFileHeader o) {
76 | return nameStr.compareTo(o.nameStr);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/bin/zip/CrcOutputStream.java:
--------------------------------------------------------------------------------
1 | package bin.zip;
2 |
3 | import java.io.IOException;
4 | import java.io.OutputStream;
5 | import java.util.zip.CRC32;
6 |
7 | /**
8 | * @author Bin
9 | */
10 | public class CrcOutputStream extends OutputStream {
11 | private OutputStream os;
12 | private CRC32 crc32 = new CRC32();
13 | private long count = 0;
14 |
15 | public CrcOutputStream(OutputStream os) {
16 | this.os = os;
17 | }
18 |
19 | @Override
20 | public void write(byte[] b, int off, int len) throws IOException {
21 | os.write(b, off, len);
22 | crc32.update(b, off, len);
23 | count += len;
24 | }
25 |
26 | @Override
27 | public void write(int b) throws IOException {
28 | os.write(b);
29 | crc32.update(b);
30 | count++;
31 | }
32 |
33 | public long getCount() {
34 | return count;
35 | }
36 |
37 | public int getCrc() {
38 | return (int) crc32.getValue();
39 | }
40 |
41 | @Override
42 | public void close() throws IOException {
43 | os.close();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/bin/zip/DataMultiplexing.java:
--------------------------------------------------------------------------------
1 | package bin.zip;
2 |
3 | import bin.mt.apksign.V2V3SchemeSigner;
4 | import bin.mt.apksign.key.JksSignatureKey;
5 |
6 | import java.io.BufferedInputStream;
7 | import java.io.File;
8 | import java.io.IOException;
9 | import java.io.InputStream;
10 | import java.text.DecimalFormat;
11 | import java.util.*;
12 |
13 | public class DataMultiplexing {
14 |
15 | public static void main(String[] args) throws Exception {
16 | File input = new File("test.apk");
17 | File output = new File("output.apk");
18 | optimize(input, output, "assets/base.apk", true);
19 | V2V3SchemeSigner.sign(output, new JksSignatureKey("test.jks", "123456", "123456", "123456"), true, true);
20 | System.out.println("Check " + isZipFileContentEquals(input, output));
21 | }
22 |
23 | /**
24 | * @param input 输入文件
25 | * @param output 输出文件
26 | * @param hostEntryName 原包路径,如 assets/base.apk
27 | * @param printDetails 是否打印优化详情
28 | */
29 | public static void optimize(String input, String output, String hostEntryName, boolean printDetails) throws IOException {
30 | optimize(new File(input), new File(output), hostEntryName, printDetails);
31 | }
32 |
33 | /**
34 | * @param input 输入文件
35 | * @param output 输出文件
36 | * @param hostEntryName 原包路径,如 assets/base.apk
37 | * @param printDetails 是否打印优化详情
38 | */
39 | public static void optimize(File input, File output, String hostEntryName, boolean printDetails) throws IOException {
40 | try (ZipFile zipFile = new ZipFile(input)) {
41 | ZipEntry hostEntry = zipFile.getEntryNonNull(hostEntryName);
42 | Set children = new TreeSet<>();
43 | // 返回的innerZipFile已经close了,但内部的entries还在
44 | ZipFile innerZipFile = collectChildren(zipFile, hostEntry, children);
45 | if (innerZipFile == null) {
46 | throw new IOException("No multiplexable data found");
47 | }
48 | List otherZipEntry = new ArrayList<>();
49 | for (ZipEntry entry : zipFile.getEntries()) {
50 | if (entry != hostEntry && !children.contains(entry.getName())) {
51 | otherZipEntry.add(entry);
52 | }
53 | }
54 | try (ZipMaker zipMaker = new ZipMaker(output)) {
55 | ZipMaker.HostEntryHolder holder = zipMaker.putNextHostEntry(hostEntry.getName(), innerZipFile);
56 | String format = "%0" + Math.min(Long.toHexString(hostEntry.getSize()).length(), 9) + "x";
57 | if (printDetails) {
58 | System.out.println(hostEntry.getName() + " >> offset=0x" + Long.toHexString(holder.getHostEntryHeaderOffset()));
59 | }
60 | for (String name : children) {
61 | long offset = holder.putNextVirtualEntry(name);
62 | if (printDetails) {
63 | System.out.println(" +0x" + String.format(format, offset) + " " + name);
64 | }
65 | }
66 | for (ZipEntry entry : otherZipEntry) {
67 | zipMaker.copyZipEntry(entry, zipFile);
68 | }
69 | }
70 | }
71 | long inputLen = input.length();
72 | long outputLen = output.length();
73 | System.out.printf("Data multiplexing optimize: %s (%s) -> %s (%s) [%.2f%%]\n", input.getName(), formatFileSize(inputLen), output.getName(), formatFileSize(outputLen), (outputLen - inputLen) * 100f / inputLen);
74 | }
75 |
76 | /**
77 | * 判断两个ZIP文件内容是否完全相同
78 | */
79 | public static boolean isZipFileContentEquals(File file1, File file2) throws IOException {
80 | try (ZipFile zipFile1 = new ZipFile(file1); ZipFile zipFile2 = new ZipFile(file2)) {
81 | if (zipFile1.getEntrySize() != zipFile2.getEntrySize()) {
82 | return false;
83 | }
84 | for (ZipEntry entry1 : zipFile1.getEntries()) {
85 | ZipEntry entry2 = zipFile2.getEntry(entry1.getName());
86 | if (entry2 == null) {
87 | return false;
88 | }
89 | if (entry1.isDirectory() && entry2.isDirectory()) {
90 | continue;
91 | }
92 | if (entry1.getMethod() != entry2.getMethod()) {
93 | return false;
94 | }
95 | if (entry1.getCrc() != entry2.getCrc()) {
96 | return false;
97 | }
98 | if (entry1.getSize() != entry2.getSize()) {
99 | return false;
100 | }
101 | if (!Arrays.equals(entry1.getCommentData(), entry2.getCommentData())) {
102 | return false;
103 | }
104 | if (!isInputStreamContentEquals(zipFile1.getInputStream(entry1), zipFile2.getInputStream(entry2))) {
105 | return false;
106 | }
107 | }
108 | return true;
109 | }
110 | }
111 |
112 | private static ZipFile collectChildren(ZipFile outer, ZipEntry hostEntry, Set children) throws IOException {
113 | try (ZipFile inner = openEntryAsZipFile(outer, hostEntry)) {
114 | for (ZipEntry outerEntry : outer.getEntries()) {
115 | if (outerEntry == hostEntry || outerEntry.isDirectory()) {
116 | continue;
117 | }
118 | ZipEntry innerEntry = inner.getEntry(outerEntry.getName());
119 | if (innerEntry == null) {
120 | continue;
121 | }
122 | if (outerEntry.getMethod() != innerEntry.getMethod()) {
123 | continue;
124 | }
125 | if (outerEntry.getCrc() != innerEntry.getCrc()) {
126 | continue;
127 | }
128 | if (outerEntry.getSize() != innerEntry.getSize()) {
129 | continue;
130 | }
131 | if (!Arrays.equals(outerEntry.getCommentData(), innerEntry.getCommentData())) {
132 | continue;
133 | }
134 | // 必须4k对齐
135 | if (innerEntry.getMethod() == ZipMaker.METHOD_STORED) {
136 | String name = innerEntry.getName();
137 | if (name.equals("resources.arsc") && innerEntry.getDataOffset() % 4 != 0) {
138 | continue;
139 | }
140 | if (name.endsWith(".so") && innerEntry.getDataOffset() % 4096 != 0) {
141 | continue;
142 | }
143 | }
144 | boolean equals = outerEntry.getCompressedSize() == innerEntry.getCompressedSize() &&
145 | isInputStreamContentEquals(inner.getRawInputStream(innerEntry), outer.getRawInputStream(outerEntry)) ||
146 | isInputStreamContentEquals(inner.getInputStream(innerEntry), outer.getInputStream(outerEntry));
147 | if (equals) {
148 | children.add(innerEntry.getName());
149 | }
150 | }
151 | return children.isEmpty() ? null : inner;
152 | }
153 | }
154 |
155 | private static ZipFile openEntryAsZipFile(ZipFile zipFile, ZipEntry hostEntry) throws IOException {
156 | if (hostEntry.getMethod() == ZipMaker.METHOD_STORED) {
157 | return zipFile.openEntryAsZipFile(hostEntry);
158 | } else {
159 | throw new IOException("Entry must be packaged with the stored method: " + hostEntry.getName());
160 | }
161 | }
162 |
163 | private static boolean isInputStreamContentEquals(InputStream input1, InputStream input2) throws IOException {
164 | if (input1 == input2) {
165 | return true;
166 | }
167 | if (!(input1 instanceof BufferedInputStream)) {
168 | input1 = new BufferedInputStream(input1);
169 | }
170 | if (!(input2 instanceof BufferedInputStream)) {
171 | input2 = new BufferedInputStream(input2);
172 | }
173 |
174 | int ch = input1.read();
175 | while (-1 != ch) {
176 | final int ch2 = input2.read();
177 | if (ch != ch2) {
178 | return false;
179 | }
180 | ch = input1.read();
181 | }
182 |
183 | final int ch2 = input2.read();
184 | return ch2 == -1;
185 | }
186 |
187 | private static final DecimalFormat df = new DecimalFormat("#.00");
188 |
189 | private static String formatFileSize(long fileSize) {
190 | if (fileSize < 1024)
191 | return fileSize + "B";
192 | else if (fileSize < 1024 * 1024)
193 | return df.format((double) fileSize / 1024) + "KB";
194 | else if (fileSize < 1024 * 1024 * 1024)
195 | return df.format((double) fileSize / 1048576) + "MB";
196 | else
197 | return df.format((double) fileSize / 1073741824) + "GB";
198 | }
199 |
200 | }
201 |
--------------------------------------------------------------------------------
/src/bin/zip/ExtraDataRecord.java:
--------------------------------------------------------------------------------
1 | package bin.zip;
2 |
3 | import java.io.IOException;
4 | import java.util.Arrays;
5 | import java.util.HashSet;
6 | import java.util.Set;
7 |
8 | /**
9 | * @author Bin
10 | */
11 | class ExtraDataRecord {
12 | private static final Set KNOWN_HEADER = new HashSet<>();
13 |
14 | static {
15 | // https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
16 | // 4.5.2 The current Header ID mappings defined by PKWARE are:
17 | KNOWN_HEADER.add(0x0001);
18 | KNOWN_HEADER.add(0x0007);
19 | KNOWN_HEADER.add(0x0008);
20 | KNOWN_HEADER.add(0x0009);
21 | KNOWN_HEADER.add(0x000a);
22 | KNOWN_HEADER.add(0x000c);
23 | KNOWN_HEADER.add(0x000d);
24 | KNOWN_HEADER.add(0x000e);
25 | KNOWN_HEADER.add(0x000f);
26 | KNOWN_HEADER.add(0x0014);
27 | KNOWN_HEADER.add(0x0015);
28 | KNOWN_HEADER.add(0x0016);
29 | KNOWN_HEADER.add(0x0017);
30 | KNOWN_HEADER.add(0x0018);
31 | KNOWN_HEADER.add(0x0019);
32 | KNOWN_HEADER.add(0x0020);
33 | KNOWN_HEADER.add(0x0021);
34 | KNOWN_HEADER.add(0x0022);
35 | KNOWN_HEADER.add(0x0023);
36 | KNOWN_HEADER.add(0x0065);
37 | KNOWN_HEADER.add(0x0066);
38 | KNOWN_HEADER.add(0x4690);
39 | KNOWN_HEADER.add(0x07c8);
40 | KNOWN_HEADER.add(0x2605);
41 | KNOWN_HEADER.add(0x2705);
42 | KNOWN_HEADER.add(0x2805);
43 | KNOWN_HEADER.add(0x334d);
44 | KNOWN_HEADER.add(0x4341);
45 | KNOWN_HEADER.add(0x4453);
46 | KNOWN_HEADER.add(0x4704);
47 | KNOWN_HEADER.add(0x470f);
48 | KNOWN_HEADER.add(0x4b46);
49 | KNOWN_HEADER.add(0x4c41);
50 | KNOWN_HEADER.add(0x4d49);
51 | KNOWN_HEADER.add(0x4f4c);
52 | KNOWN_HEADER.add(0x5356);
53 | KNOWN_HEADER.add(0x5455);
54 | KNOWN_HEADER.add(0x554e);
55 | KNOWN_HEADER.add(0x5855);
56 | KNOWN_HEADER.add(0x6375);
57 | KNOWN_HEADER.add(0x6542);
58 | KNOWN_HEADER.add(0x7075);
59 | KNOWN_HEADER.add(0x756e);
60 | KNOWN_HEADER.add(0x7855);
61 | KNOWN_HEADER.add(0xa11e);
62 | KNOWN_HEADER.add(0xa220);
63 | KNOWN_HEADER.add(0xfd4a);
64 | KNOWN_HEADER.add(0x9901);
65 | KNOWN_HEADER.add(0x9902);
66 | }
67 |
68 | /**
69 | * 去除无效数据
70 | */
71 | public static byte[] trim(byte[] extra) throws IOException {
72 | int offset = 0;
73 | while (extra.length - offset >= 4) {
74 | int header = ZipUtil.readUShort(extra, offset);
75 | int size = ZipUtil.readUShort(extra, offset + 2);
76 | if (!KNOWN_HEADER.contains(header) || offset + 4 + size > extra.length) {
77 | break;
78 | }
79 | offset += 4 + size;
80 | }
81 | return Arrays.copyOf(extra, offset);
82 | }
83 |
84 | public static byte[] set(byte[] extra, int header, byte[] data) throws IOException {
85 | extra = remove(extra, header);
86 | byte[] newExtra = new byte[4 + data.length + extra.length];
87 | ZipUtil.writeShort(newExtra, 0, header);
88 | ZipUtil.writeShort(newExtra, 2, data.length);
89 | System.arraycopy(data, 0, newExtra, 4, data.length);
90 | System.arraycopy(extra, 0, newExtra, 4 + data.length, extra.length);
91 | return newExtra;
92 | }
93 |
94 | public static byte[] remove(byte[] extra, int header) throws IOException {
95 | int offset = 0;
96 | while (extra.length - offset >= 4) {
97 | int h = ZipUtil.readUShort(extra, offset);
98 | int size = ZipUtil.readUShort(extra, offset + 2);
99 | offset += 4;
100 | if (size > extra.length - offset)
101 | return extra;
102 | if (h != header) {
103 | offset += size;
104 | } else {
105 | offset -= 4;
106 | size += 4;
107 | byte[] bytes = new byte[extra.length - size];
108 | System.arraycopy(extra, 0, bytes, 0, offset);
109 | System.arraycopy(extra, offset + size, bytes, offset, extra.length - size - offset);
110 | return bytes;
111 | }
112 | }
113 | return extra;
114 | }
115 |
116 | public static ExtraDataRecord find(byte[] extra, int header) throws IOException {
117 | int offset = 0;
118 | while (extra.length - offset >= 4) {
119 | int h = ZipUtil.readUShort(extra, offset);
120 | int size = ZipUtil.readUShort(extra, offset + 2);
121 | offset += 4;
122 | if (size > extra.length - offset)
123 | return null;
124 | if (h != header) {
125 | offset += size;
126 | } else {
127 | byte[] bytes = new byte[size];
128 | System.arraycopy(extra, offset, bytes, 0, size);
129 | ExtraDataRecord record = new ExtraDataRecord();
130 | record.setHeader(header);
131 | record.setSizeOfData(size);
132 | record.setData(bytes);
133 | return record;
134 | }
135 | }
136 | return null;
137 | }
138 |
139 | public static byte[] generateAESExtra(int aesKeyStrength, int method) throws IOException {
140 | int versionNumber = 2; // 2
141 | String vendorID = "AE"; // 2
142 | // aesKeyStrength // 1
143 | // method // 2
144 | byte[] data = new byte[7];
145 | ZipUtil.writeShort(data, 0, versionNumber);
146 | ZipUtil.writeBytes(data, 2, vendorID.getBytes());
147 | ZipUtil.writeByte(data, 4, aesKeyStrength);
148 | ZipUtil.writeShort(data, 5, method);
149 | return data;
150 | }
151 |
152 | private int header;
153 |
154 | private int sizeOfData;
155 |
156 | private byte[] data;
157 |
158 | public int getHeader() {
159 | return header;
160 | }
161 |
162 | public void setHeader(int header) {
163 | this.header = header;
164 | }
165 |
166 | public int getSizeOfData() {
167 | return sizeOfData;
168 | }
169 |
170 | public void setSizeOfData(int sizeOfData) {
171 | this.sizeOfData = sizeOfData;
172 | }
173 |
174 | public byte[] getData() {
175 | return data;
176 | }
177 |
178 | public void setData(byte[] data) {
179 | this.data = data;
180 | }
181 |
182 | public int readUByte(int off) throws IOException {
183 | return ZipUtil.readUByte(data, off);
184 | }
185 |
186 | public int readUShort(int off) throws IOException {
187 | return ZipUtil.readUShort(data, off);
188 | }
189 |
190 | public int readInt(int off) throws IOException {
191 | return ZipUtil.readInt(data, off);
192 | }
193 |
194 | public long readLong(int off) throws IOException {
195 | return ZipUtil.readLong(data, off);
196 | }
197 |
198 | }
199 |
--------------------------------------------------------------------------------
/src/bin/zip/NoWrapDeflaterOutputStream.java:
--------------------------------------------------------------------------------
1 | package bin.zip;
2 |
3 | import java.io.IOException;
4 | import java.io.OutputStream;
5 | import java.util.zip.Deflater;
6 | import java.util.zip.DeflaterOutputStream;
7 |
8 | /**
9 | * @author Bin
10 | */
11 | public class NoWrapDeflaterOutputStream extends DeflaterOutputStream {
12 |
13 | public NoWrapDeflaterOutputStream(OutputStream os, int level) {
14 | super(os, new Deflater(level, true));
15 | }
16 |
17 | @Override
18 | public void close() throws IOException {
19 | super.close();
20 | def.end();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/bin/zip/NoWrapInflaterInputStream.java:
--------------------------------------------------------------------------------
1 | package bin.zip;
2 |
3 | import java.io.EOFException;
4 | import java.io.IOException;
5 | import java.io.InputStream;
6 | import java.util.zip.Inflater;
7 | import java.util.zip.InflaterInputStream;
8 | import java.util.zip.ZipException;
9 |
10 | /**
11 | * @author Bin
12 | */
13 | public class NoWrapInflaterInputStream extends InflaterInputStream {
14 | private final ZipEntry entry;
15 |
16 | public NoWrapInflaterInputStream(ZipEntry entry, InputStream in) {
17 | super(in, new Inflater(true));
18 | this.entry = entry;
19 | }
20 |
21 | @Override
22 | public int read(byte[] b, int off, int len) throws IOException {
23 | try {
24 | return super.read(b, off, len);
25 | } catch (ZipException e) {
26 | e.printStackTrace();
27 | throw new ZipException("Error: " + e.getMessage() + " (" + entry.getName() + ")");
28 | } catch (EOFException e) {
29 | return -1;
30 | }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/src/bin/zip/ZipConstant.java:
--------------------------------------------------------------------------------
1 | package bin.zip;
2 |
3 | import java.nio.charset.Charset;
4 |
5 | /**
6 | * @author Bin
7 | */
8 | interface ZipConstant {
9 | @SuppressWarnings("CharsetObjectCanBeUsed")
10 | Charset UTF_8 = Charset.forName("UTF-8");
11 | int METHOD_STORED = 0;
12 | int METHOD_DEFLATED = 8;
13 |
14 | int PLATFORM_FAT = 0;
15 |
16 | int UFT8_NAMES_FLAG = 1 << 11;
17 |
18 | int EXTRA_HEADER_UNICODE_NAME = 0x7075;
19 | int EXTRA_HEADER_UNICODE_COMMENT = 0x6375;
20 |
21 | /**
22 | * local file header signature
23 | */
24 | int LFH_SIG = 0x04034B50;
25 |
26 | /**
27 | * local file data descriptor signature
28 | */
29 | int EXT_SIG = 0x08074b50;
30 |
31 | /**
32 | * End of central dir signature
33 | */
34 | int EOCD_SIG = 0x06054B50;
35 |
36 | /**
37 | * Central file header signature
38 | */
39 | int CFH_SIG = 0x02014B50;
40 |
41 | int BUFF_SIZE = 1024 * 4;
42 |
43 | int SHORT = 2;
44 |
45 | int WORD = 4;
46 |
47 | int MIN_EOCD_SIZE =
48 | /* end of central dir signature */ WORD
49 | /* number of this disk */ + SHORT
50 | /* number of the disk with the */
51 | /* start of the central directory */ + SHORT
52 | /* total number of entries in */
53 | /* the central dir on this disk */ + SHORT
54 | /* total number of entries in */
55 | /* the central dir */ + SHORT
56 | /* size of the central directory */ + WORD
57 | /* offset of start of central */
58 | /* directory with respect to */
59 | /* the starting disk number */ + WORD
60 | /* zipfile comment length */ + SHORT;
61 |
62 | int MAX_EOCD_SIZE = MIN_EOCD_SIZE
63 | /* maximum length of zipfile comment */ + 0xFFFF;
64 |
65 | int CFD_LOCATOR_OFFSET =
66 | /* end of central dir signature */ WORD
67 | /* number of this disk */ + SHORT
68 | /* number of the disk with the */
69 | /* start of the central directory */ + SHORT
70 | /* total number of entries in */
71 | /* the central dir on this disk */ + SHORT
72 | /* total number of entries in */
73 | /* the central dir */ + SHORT
74 | /* size of the central directory */ + WORD;
75 |
76 | int LFH_OFFSET_FOR_FILENAME_LENGTH =
77 | /* local file header signature */ WORD
78 | /* version needed to extract */ + SHORT
79 | /* general purpose bit flag */ + SHORT
80 | /* compression method */ + SHORT
81 | /* last mod file time */ + SHORT
82 | /* last mod file date */ + SHORT
83 | /* crc-32 */ + WORD
84 | /* compressed size */ + WORD
85 | /* uncompressed size */ + WORD;
86 |
87 | /**
88 | * The maximum supported entry / archive size for standard (non zip64) entries and archives.
89 | */
90 | long MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE = 0x00000000ffffffffL;
91 |
92 | /*
93 | * Size (in bytes) of the zip64 end of central directory locator. This will be located
94 | * immediately before the end of central directory record if a given zipfile is in the
95 | * zip64 format.
96 | */
97 | int ZIP64_LOCATOR_SIZE = 20;
98 |
99 | /**
100 | * The zip64 end of central directory locator signature (4 bytes wide).
101 | */
102 | int ZIP64_LOCATOR_SIGNATURE = 0x07064b50;
103 |
104 | /**
105 | * The zip64 end of central directory record singature (4 bytes wide).
106 | */
107 | int ZIP64_EOCD_RECORD_SIGNATURE = 0x06064b50;
108 |
109 | /**
110 | * The header ID of the zip64 extended info header. This value is used to identify
111 | * zip64 data in the "extra" field in the file headers.
112 | */
113 | short ZIP64_EXTENDED_INFO_HEADER_ID = 0x0001;
114 |
115 | /**
116 | * The "effective" size of the zip64 eocd record. This excludes the fields that
117 | * are proprietary, signature, or fields we aren't interested in. We include the
118 | * following (contiguous) fields in this calculation :
119 | * - disk number (4 bytes)
120 | * - disk with start of central directory (4 bytes)
121 | * - number of central directory entries on this disk (8 bytes)
122 | * - total number of central directory entries (8 bytes)
123 | * - size of the central directory (8 bytes)
124 | * - offset of the start of the central directory (8 bytes)
125 | */
126 | int ZIP64_EOCD_RECORD_EFFECTIVE_SIZE = 40;
127 |
128 | }
129 |
--------------------------------------------------------------------------------
/src/bin/zip/ZipEntry.java:
--------------------------------------------------------------------------------
1 | package bin.zip;
2 |
3 | import java.io.IOException;
4 |
5 | import static bin.zip.ZipConstant.MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE;
6 | import static bin.zip.ZipConstant.ZIP64_EXTENDED_INFO_HEADER_ID;
7 |
8 | /**
9 | * @author Bin
10 | */
11 | public class ZipEntry {
12 | public static final long UNKNOWN_SIZE = -1;
13 | private int platform = ZipConstant.PLATFORM_FAT;
14 | private int generalPurposeFlag;
15 | private int method;
16 | private String name;
17 | private long time;
18 | private int crc;
19 | private long compressedSize = UNKNOWN_SIZE;
20 | private long size = UNKNOWN_SIZE;
21 | private int internalAttributes = 0;
22 | private int externalAttributes = 0;
23 | private long headerOffset;
24 | private long dataOffset;
25 | private byte[] extra;
26 | private byte[] commentData;
27 |
28 | ZipEntry() {
29 | }
30 |
31 | public ZipEntry(String name) {
32 | setName(name);
33 | }
34 |
35 | public int getPlatform() {
36 | return platform;
37 | }
38 |
39 | void setPlatform(int platform) {
40 | this.platform = platform;
41 | }
42 |
43 | public int getGeneralPurposeFlag() {
44 | return generalPurposeFlag;
45 | }
46 |
47 | void setGeneralPurposeFlag(int generalPurposeFlag) {
48 | this.generalPurposeFlag = generalPurposeFlag;
49 | }
50 |
51 | public int getMethod() {
52 | return method;
53 | }
54 |
55 | public void setMethod(int method) {
56 | this.method = method;
57 | }
58 |
59 | public long getTime() {
60 | return time;
61 | }
62 |
63 | public void setTime(long time) {
64 | this.time = time;
65 | }
66 |
67 | public int getCrc() {
68 | return crc;
69 | }
70 |
71 | void setCrc(int crc) {
72 | this.crc = crc;
73 | }
74 |
75 | public long getCompressedSize() {
76 | return compressedSize;
77 | }
78 |
79 | public void setCompressedSize(long compressedSize) {
80 | this.compressedSize = compressedSize;
81 | }
82 |
83 | public long getSize() {
84 | return size;
85 | }
86 |
87 | public void setSize(long size) {
88 | this.size = size;
89 | }
90 |
91 | public boolean isDirectory() {
92 | return getName().endsWith("/");
93 | }
94 |
95 | public String getName() {
96 | return name;
97 | }
98 |
99 | public void setName(String name) {
100 | if (name == null)
101 | name = "";
102 | if (getPlatform() == ZipConstant.PLATFORM_FAT && !name.contains("/")) {
103 | name = name.replace('\\', '/');
104 | }
105 | this.name = name;
106 | }
107 |
108 | public byte[] getExtra() {
109 | return extra;
110 | }
111 |
112 | void setExtra(byte[] extra) {
113 | this.extra = extra;
114 | }
115 |
116 | public int getInternalAttributes() {
117 | return internalAttributes;
118 | }
119 |
120 | void setInternalAttributes(int internalAttributes) {
121 | this.internalAttributes = internalAttributes;
122 | }
123 |
124 | public int getExternalAttributes() {
125 | return externalAttributes;
126 | }
127 |
128 | void setExternalAttributes(int externalAttributes) {
129 | this.externalAttributes = externalAttributes;
130 | }
131 |
132 | public long getHeaderOffset() {
133 | return headerOffset;
134 | }
135 |
136 | void setHeaderOffset(long headerOffset) {
137 | this.headerOffset = headerOffset;
138 | }
139 |
140 | public long getDataOffset() {
141 | return dataOffset;
142 | }
143 |
144 | void setDataOffset(long dataOffset) {
145 | this.dataOffset = dataOffset;
146 | }
147 |
148 | boolean setupZip64WithCenterDirectoryExtra(byte[] extra) throws IOException {
149 | ExtraDataRecord record = ExtraDataRecord.find(extra, ZIP64_EXTENDED_INFO_HEADER_ID);
150 | if (record == null) {
151 | if (compressedSize == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE ||
152 | size == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE ||
153 | headerOffset == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) {
154 | throw new IOException("File contains no zip64 extended information: "
155 | + "name=" + name + ", compressedSize=" + compressedSize + ", size="
156 | + size + ", headerOffset=" + headerOffset);
157 | }
158 | return false;
159 | }
160 | int offset = 0;
161 | if (size == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) {
162 | size = record.readLong(offset);
163 | offset += 8;
164 | }
165 | if (compressedSize == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) {
166 | compressedSize = record.readLong(offset);
167 | offset += 8;
168 | }
169 | if (headerOffset == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) {
170 | headerOffset = record.readLong(offset);
171 | }
172 | return true;
173 | }
174 |
175 | void setNameData(byte[] nameData) {
176 | this.name = new String(nameData, ZipConstant.UTF_8);
177 | }
178 |
179 | void setCommentData(byte[] commentData) {
180 | this.commentData = commentData;
181 | }
182 |
183 | public byte[] getCommentData() {
184 | return commentData;
185 | }
186 |
187 | }
188 |
--------------------------------------------------------------------------------
/src/bin/zip/ZipFile.java:
--------------------------------------------------------------------------------
1 | package bin.zip;
2 |
3 | import bin.io.RandomAccessFactory;
4 | import bin.io.RandomAccessFile;
5 |
6 | import java.io.*;
7 | import java.util.*;
8 |
9 | import static bin.zip.ZipConstant.*;
10 |
11 | /**
12 | * @author Bin
13 | */
14 | public class ZipFile implements Closeable {
15 | private final RandomAccessFile archive;
16 | private final Map entries = new LinkedHashMap<>();
17 |
18 | public ZipFile(File file) throws IOException {
19 | this(RandomAccessFactory.from(file, "r"));
20 | }
21 |
22 | public ZipFile(RandomAccessFile archive) throws IOException {
23 | this.archive = archive;
24 | readEntries();
25 | }
26 |
27 | public ZipEntry getEntry(String name) {
28 | return entries.get(name);
29 | }
30 |
31 | public ZipEntry getEntryNonNull(String name) throws IOException {
32 | ZipEntry entry = entries.get(name);
33 | if (entry == null) {
34 | throw new IOException("Entry not found: " + name);
35 | }
36 | return entry;
37 | }
38 |
39 | public ArrayList getEntries() {
40 | return new ArrayList<>(entries.values());
41 | }
42 |
43 | public int getEntrySize() {
44 | return entries.size();
45 | }
46 |
47 | private void readEntries() throws IOException {
48 | EocdRecord eocdRecord = readEocdRecord();
49 | if (eocdRecord == null) {
50 | throw new IOException("EOCD not found");
51 | }
52 | List list = new ArrayList<>();
53 | boolean zip64 = eocdRecord.zip64;
54 | _seek(eocdRecord.centralDirOffset);
55 | while (_readInt() == CFH_SIG) {
56 | ZipEntry ze = new ZipEntry();
57 | int versionMadeBy = _readUShort();
58 | ze.setPlatform((versionMadeBy >> 8) & 0xF);
59 |
60 | _readUShort(); // skip version info
61 |
62 | ze.setGeneralPurposeFlag(_readUShort());
63 | ze.setMethod(_readUShort());
64 | ze.setTime(ZipUtil.dosToJavaTime(_readUInt()));
65 | ze.setCrc(_readInt());
66 |
67 | ze.setCompressedSize(_readUInt());
68 | ze.setSize(_readUInt());
69 |
70 | int fileNameLen = _readUShort();
71 | int extraLen = _readUShort();
72 | int commentLen = _readUShort();
73 |
74 | _readUShort(); // disk number
75 |
76 | ze.setInternalAttributes(_readUShort());
77 | ze.setExternalAttributes(_readInt());
78 |
79 | ze.setHeaderOffset(_readUInt());
80 |
81 | ze.setNameData(_readBytes(fileNameLen));
82 |
83 | if (extraLen > 0) {
84 | if (zip64)
85 | ze.setupZip64WithCenterDirectoryExtra(_readBytes(extraLen));
86 | else
87 | _skip(extraLen);
88 | }
89 |
90 | if (commentLen > 0) {
91 | try {
92 | byte[] comment = _readBytes(commentLen);
93 | ze.setCommentData(comment);
94 | } catch (IOException ignored) {
95 | }
96 | }
97 |
98 | list.add(ze);
99 | }
100 |
101 | //noinspection Java8ListSort,ComparatorCombinators
102 | Collections.sort(list, (e1, e2) -> Long.compare(e1.getHeaderOffset(), e2.getHeaderOffset()));
103 | Set ok = new HashSet<>(list.size());
104 |
105 | for (ZipEntry entry : list) {
106 | try {
107 | long offset = entry.getHeaderOffset();
108 | _seek(offset + LFH_OFFSET_FOR_FILENAME_LENGTH);
109 | int fileNameLen = _readUShort();
110 | int extraLen = _readUShort();
111 | _skip(fileNameLen);
112 | byte[] extra = _readBytes(extraLen);
113 | // 去除zip64Extra
114 | extra = ExtraDataRecord.remove(extra, ZIP64_EXTENDED_INFO_HEADER_ID);
115 | entry.setExtra(extra);
116 | entry.setDataOffset(offset + LFH_OFFSET_FOR_FILENAME_LENGTH
117 | + SHORT + SHORT + fileNameLen + extraLen);
118 | ok.add(entry.getName());
119 | } catch (EOFException e) {
120 | e.printStackTrace();
121 | }
122 | }
123 | entries.clear();
124 | for (ZipEntry entry : list) {
125 | String key = entry.getName();
126 | if (ok.contains(key)) {
127 | entries.put(key, entry);
128 | }
129 | }
130 | }
131 |
132 | private EocdRecord readEocdRecord() throws IOException {
133 | boolean found = false;
134 | long length = _length();
135 | long off = length - MIN_EOCD_SIZE;
136 | final long stopSearching = Math.max(0L, length - MAX_EOCD_SIZE);
137 | while (off >= stopSearching) {
138 | _seek(off);
139 | if (_readInt() == EOCD_SIG) {
140 | found = true;
141 | break;
142 | }
143 | off--;
144 | }
145 | if (!found) {
146 | return null;
147 | }
148 |
149 | try {
150 | final long zip64EocdRecordOffset = parseZip64EocdRecordLocator(off);
151 |
152 | EocdRecord record = parseEocdRecord(off + 4, (zip64EocdRecordOffset != -1) /* isZip64 */);
153 | if (record.commentLength > 0) {
154 | try {
155 | _readBytes(record.commentLength);
156 | } catch (IOException ignored) {
157 | record = new EocdRecord(record.numEntries, record.centralDirOffset, 0, record.zip64);
158 | }
159 | }
160 |
161 | if (zip64EocdRecordOffset != -1) {
162 | record = parseZip64EocdRecord(zip64EocdRecordOffset, record.commentLength);
163 | }
164 |
165 | return record;
166 | } catch (IOException e) {
167 | e.printStackTrace();
168 | return null;
169 | }
170 | }
171 |
172 | private long parseZip64EocdRecordLocator(long eocdOffset)
173 | throws IOException {
174 | // The spec stays curiously silent about whether a zip file with an EOCD record,
175 | // a zip64 locator and a zip64 eocd record is considered "empty". In our implementation,
176 | // we parse all records and read the counts from them instead of drawing any size or
177 | // layout based information.
178 | if (eocdOffset > ZIP64_LOCATOR_SIZE) {
179 | _seek(eocdOffset - ZIP64_LOCATOR_SIZE);
180 | if (_readInt() == ZIP64_LOCATOR_SIGNATURE) {
181 | final int diskWithCentralDir = _readInt();
182 | final long zip64EocdRecordOffset = _readLong();
183 | final int numDisks = _readInt();
184 | if (numDisks != 1 || diskWithCentralDir != 0) {
185 | throw new IOException("Spanned archives not supported");
186 | }
187 | return zip64EocdRecordOffset;
188 | }
189 | }
190 | return -1;
191 | }
192 |
193 | private EocdRecord parseEocdRecord(long offset, boolean isZip64) throws IOException {
194 | _seek(offset);
195 | final long numEntries;
196 | final long centralDirOffset;
197 | if (isZip64) {
198 | numEntries = -1;
199 | centralDirOffset = -1;
200 | _skip(16);
201 | } else {
202 | _skip(4);
203 | numEntries = _readUShort();
204 | _skip(6);
205 | centralDirOffset = _readUInt();
206 | }
207 | final int commentLength = _readUShort();
208 | return new EocdRecord(numEntries, centralDirOffset, commentLength, false);
209 | }
210 |
211 | private EocdRecord parseZip64EocdRecord(long eocdRecordOffset, int commentLength) throws IOException {
212 | _seek(eocdRecordOffset);
213 | final int signature = _readInt();
214 | if (signature != ZIP64_EOCD_RECORD_SIGNATURE) {
215 | throw new IOException("Invalid zip64 eocd record offset, sig="
216 | + Integer.toHexString(signature) + " offset=" + eocdRecordOffset);
217 | }
218 | _skip(12);
219 | int diskNumber = _readInt();
220 | int diskWithCentralDirStart = _readInt();
221 | long numEntries = _readLong();
222 | long totalNumEntries = _readLong();
223 | _readLong();
224 | long centralDirOffset = _readLong();
225 | if (numEntries != totalNumEntries || diskNumber != 0 || diskWithCentralDirStart != 0) {
226 | throw new IOException("Spanned archives not supported :" +
227 | " numEntries=" + numEntries + ", totalNumEntries=" + totalNumEntries +
228 | ", diskNumber=" + diskNumber + ", diskWithCentralDirStart=" +
229 | diskWithCentralDirStart);
230 | }
231 | return new EocdRecord(numEntries, centralDirOffset, commentLength, true);
232 | }
233 |
234 | public InputStream getRawInputStream(ZipEntry ze) {
235 | return new BridgeInputStream(archive, ze.getDataOffset(), ze.getCompressedSize());
236 | }
237 |
238 | public InputStream getInputStream(ZipEntry ze) throws IOException {
239 | long start = ze.getDataOffset();
240 | int method = ze.getMethod();
241 | InputStream is = new BridgeInputStream(archive, start, method == METHOD_STORED ? ze.getSize() : ze.getCompressedSize());
242 | switch (method) {
243 | case METHOD_DEFLATED:
244 | is = new NoWrapInflaterInputStream(ze, is);
245 | break;
246 | case METHOD_STORED:
247 | break;
248 | default:
249 | throw new IOException("Unsupported compression method " + ze.getMethod() + " (" + ze.getName() + ")");
250 | }
251 | if (method != METHOD_STORED) {
252 | is = new BufferedInputStream(is, 64 * 1024);
253 | }
254 | return is;
255 | }
256 |
257 | public ZipFile openEntryAsZipFile(ZipEntry entry) throws IOException {
258 | if (entry.getMethod() != METHOD_STORED) {
259 | throw new IOException("Entry is not stored: " + entry.getName());
260 | }
261 | return new ZipFile(archive.newFragment(entry.getDataOffset(), entry.getCompressedSize()));
262 | }
263 |
264 | public RandomAccessFile getArchive() {
265 | return archive;
266 | }
267 |
268 | private long _length() throws IOException {
269 | return archive.length();
270 | }
271 |
272 | private void _seek(long position) throws IOException {
273 | archive.seek(position);
274 | }
275 |
276 | private void _skip(long length) throws IOException {
277 | if (length < 0)
278 | throw new IOException("Skip " + length);
279 | long pos = archive.getFilePointer() + length;
280 | long len = archive.length();
281 | if (pos > len)
282 | throw new EOFException();
283 | archive.seek(pos);
284 | }
285 |
286 | private byte[] _readBytes(int len) throws IOException {
287 | byte[] bytes = new byte[len];
288 | archive.readFully(bytes);
289 | return bytes;
290 | }
291 |
292 | private int _readInt() throws IOException {
293 | int ch1 = archive.read();
294 | int ch2 = archive.read();
295 | int ch3 = archive.read();
296 | int ch4 = archive.read();
297 | if ((ch1 | ch2 | ch3 | ch4) < 0)
298 | throw new EOFException();
299 | return (ch1) | (ch2 << 8) | (ch3 << 16) | (ch4 << 24);
300 | }
301 |
302 | private int _readUShort() throws IOException {
303 | int ch1 = archive.read();
304 | int ch2 = archive.read();
305 | if ((ch1 | ch2) < 0)
306 | throw new EOFException();
307 | return ch1 | (ch2 << 8);
308 | }
309 |
310 | private long _readUInt() throws IOException {
311 | int value = _readInt();
312 | return value & 0xFFFFFFFFL;
313 | }
314 |
315 | private long _readLong() throws IOException {
316 | return _readUInt() | (_readUInt() << 32);
317 | }
318 |
319 | private boolean closed = false;
320 |
321 | @Override
322 | public void close() throws IOException {
323 | if (closed)
324 | return;
325 | archive.close();
326 | closed = true;
327 | }
328 |
329 | private static class EocdRecord {
330 | final long numEntries;
331 | final long centralDirOffset;
332 | final int commentLength;
333 | final boolean zip64;
334 |
335 | EocdRecord(long numEntries, long centralDirOffset, int commentLength, boolean zip64) {
336 | this.numEntries = numEntries;
337 | this.centralDirOffset = centralDirOffset;
338 | this.commentLength = commentLength;
339 | this.zip64 = zip64;
340 | }
341 | }
342 |
343 | }
344 |
--------------------------------------------------------------------------------
/src/bin/zip/ZipMaker.java:
--------------------------------------------------------------------------------
1 | package bin.zip;
2 |
3 | import bin.io.RandomAccessFactory;
4 | import bin.io.RandomAccessFile;
5 |
6 | import java.io.*;
7 | import java.nio.charset.Charset;
8 | import java.util.ArrayList;
9 | import java.util.Arrays;
10 | import java.util.Collections;
11 | import java.util.Objects;
12 | import java.util.zip.Deflater;
13 |
14 | import static bin.zip.ZipConstant.*;
15 |
16 | /**
17 | * @author Bin
18 | */
19 | public class ZipMaker implements Closeable {
20 | public static final int LEVEL_FASTEST = Deflater.BEST_SPEED;
21 | public static final int LEVEL_FASTER = 3;
22 | public static final int LEVEL_DEFAULT = Deflater.DEFAULT_COMPRESSION;
23 | public static final int LEVEL_BETTER = 7;
24 | public static final int LEVEL_BEST = Deflater.BEST_COMPRESSION;
25 |
26 | public static final int METHOD_DEFLATED = ZipConstant.METHOD_DEFLATED;
27 | public static final int METHOD_STORED = ZipConstant.METHOD_STORED;
28 |
29 | private final RandomAccessFile archive;
30 |
31 | private final ArrayList headers = new ArrayList<>();
32 |
33 | private CenterFileHeader currentHeader;
34 |
35 | private int method = METHOD_DEFLATED;
36 |
37 | private int level = LEVEL_DEFAULT;
38 |
39 | private Charset encoding = ZipConstant.UTF_8;
40 |
41 | private String comment;
42 |
43 | private boolean needsZip64EocdRecord;
44 |
45 | private boolean forceZip64;
46 |
47 | private CrcOutputStream topOutput;
48 |
49 | private BridgeOutputStream bottomOutput;
50 |
51 | public ZipMaker(String path) throws IOException {
52 | this(new File(path));
53 | }
54 |
55 | public ZipMaker(File file) throws IOException {
56 | if (file.exists())
57 | //noinspection ResultOfMethodCallIgnored
58 | file.delete();
59 | this.archive = RandomAccessFactory.from(file, "rw");
60 | }
61 |
62 | public void setForceZip64(boolean forceZip64) {
63 | this.forceZip64 = forceZip64;
64 | }
65 |
66 | public void setEncoding(Charset encoding) {
67 | this.encoding = encoding;
68 | }
69 |
70 | public void setEncoding(String encoding) {
71 | this.encoding = Charset.forName(encoding);
72 | }
73 |
74 | public void setMethod(int method) {
75 | this.method = method;
76 | }
77 |
78 | public int getMethod() {
79 | return method;
80 | }
81 |
82 | public void setLevel(int level) {
83 | this.level = level;
84 | }
85 |
86 | public int getLevel() {
87 | return level;
88 | }
89 |
90 | public void setComment(String comment) {
91 | this.comment = comment;
92 | }
93 |
94 | public String getComment() {
95 | return comment;
96 | }
97 |
98 | private final byte[] copyEntryBuffer = new byte[8 * 1024];
99 |
100 | public void putNextEntry(String name) throws IOException {
101 | putNextEntry(new CenterFileHeader(name));
102 | }
103 |
104 | public void putNextEntry(ZipEntry ze) throws IOException {
105 | putNextEntry(new CenterFileHeader(ze));
106 | }
107 |
108 | private void putNextEntry(CenterFileHeader header) throws IOException {
109 | if (currentHeader != null) {
110 | closeEntry();
111 | }
112 | header.headerOffset = _getFilePointer();
113 | headers.add(header);
114 |
115 | if (!header.isDirectory) {
116 | currentHeader = header;
117 |
118 | int generalPurposeFlag = 0;
119 | int method = this.method;
120 |
121 | bottomOutput = new BridgeOutputStream(archive);
122 | OutputStream os = bottomOutput;
123 |
124 | if (header.isUtf8)
125 | generalPurposeFlag |= UFT8_NAMES_FLAG;
126 |
127 | switch (this.method) {
128 | case METHOD_DEFLATED:
129 | os = new NoWrapDeflaterOutputStream(os, level);
130 | break;
131 | case METHOD_STORED:
132 | break;
133 | default:
134 | throw new IOException("Unsupported compression method " + method);
135 | }
136 |
137 | topOutput = new CrcOutputStream(os);
138 |
139 | header.generalPurposeFlag = generalPurposeFlag;
140 | header.method = method;
141 | } else {
142 | header.method = METHOD_STORED;
143 | if (header.isUtf8) {
144 | header.generalPurposeFlag = UFT8_NAMES_FLAG;
145 | }
146 | }
147 |
148 | writeHeader(header);
149 | header.dataOffset = _getFilePointer();
150 | }
151 |
152 | public void putNextRawEntry(ZipEntry ze) throws IOException {
153 | if (currentHeader != null)
154 | closeEntry();
155 | CenterFileHeader header = new CenterFileHeader(ze);
156 | if (header.isUtf8)
157 | header.generalPurposeFlag |= UFT8_NAMES_FLAG;
158 | header.headerOffset = _getFilePointer();
159 | headers.add(header);
160 | writeHeader(header);
161 | header.dataOffset = _getFilePointer();
162 | }
163 |
164 | public void writeRaw(byte[] data) throws IOException {
165 | writeRaw(data, 0, data.length);
166 | }
167 |
168 | public void writeRaw(byte[] data, int off, int len) throws IOException {
169 | archive.write(data, off, len);
170 | }
171 |
172 | public void copyZipEntry(ZipEntry ze, ZipFile zipFile) throws IOException {
173 | putNextRawEntry(ze);
174 | if (!ze.isDirectory()) {
175 | InputStream is = zipFile.getRawInputStream(ze);
176 | byte[] buffer = copyEntryBuffer;
177 | int len;
178 | while ((len = is.read(buffer)) != -1) {
179 | writeRaw(buffer, 0, len);
180 | }
181 | }
182 | }
183 |
184 | public HostEntryHolder putNextHostEntry(String name, ZipFile zipFile) throws IOException {
185 | if (name.endsWith("/") || name.endsWith("\\")) {
186 | throw new IOException("Invalid host entry name: " + name);
187 | }
188 | int savedMethod = method;
189 | method = METHOD_STORED;
190 | CenterFileHeader centerFileHeader = new CenterFileHeader(name);
191 | centerFileHeader.isHost = true;
192 | putNextEntry(centerFileHeader);
193 | method = savedMethod;
194 | return new HostEntryHolder(zipFile);
195 | }
196 |
197 | public class HostEntryHolder {
198 | private final CenterFileHeader hostHeader;
199 | private final ZipFile zipFile;
200 |
201 | private HostEntryHolder(ZipFile zipFile) throws IOException {
202 | this.hostHeader = Objects.requireNonNull(currentHeader);
203 | this.zipFile = zipFile;
204 | try (RandomAccessFile archive = zipFile.getArchive().newSameInstance()) {
205 | writeFully(new BridgeInputStream(archive, 0, archive.length()));
206 | closeEntry();
207 | }
208 | }
209 |
210 | public long getHostEntryHeaderOffset() {
211 | return hostHeader.headerOffset;
212 | }
213 |
214 | /**
215 | * @return virtualEntry.headerOffset - hostHeader.headerOffset
216 | */
217 | public long putNextVirtualEntry(String name) throws IOException {
218 | ZipEntry innerEntry = zipFile.getEntryNonNull(name);
219 | CenterFileHeader header = new CenterFileHeader(innerEntry);
220 | setupNeedZip64(header);
221 | header.headerOffset = innerEntry.getHeaderOffset() + hostHeader.dataOffset;
222 | header.dataOffset = innerEntry.getDataOffset() + hostHeader.dataOffset;
223 | headers.add(header);
224 | return header.headerOffset - hostHeader.headerOffset;
225 | }
226 | }
227 |
228 | private void writeHeader(CenterFileHeader header) throws IOException {
229 | setupNeedZip64(header);
230 |
231 | _writeInt(LFH_SIG);
232 | _writeShort(header.version());
233 | _writeShort(header.generalPurposeFlag);
234 | _writeShort(header.method);
235 | _writeInt(header.time);
236 | _writeInt(header.crc);
237 | if (header.sizeNeedZip64) {
238 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE);
239 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE);
240 | } else {
241 | _writeUInt(header.compressedSize);
242 | _writeUInt(header.size);
243 | }
244 | _writeShort(header.name.length);
245 |
246 | byte[] extra;
247 | if (header.sizeNeedZip64) {
248 | byte[] data = new byte[2 * 8];
249 | ZipUtil.writeLong(data, 0, header.size);
250 | ZipUtil.writeLong(data, 8, header.compressedSize);
251 | extra = ExtraDataRecord.set(header.extra, ZIP64_EXTENDED_INFO_HEADER_ID, data);
252 | } else {
253 | extra = ExtraDataRecord.remove(header.extra, ZIP64_EXTENDED_INFO_HEADER_ID);
254 | }
255 | // zipAlign
256 | if (header.method == METHOD_STORED) {
257 | int alignment;
258 | if (header.isHost || new String(header.name, ZipConstant.UTF_8).endsWith(".so")) {
259 | // -p: memory page alignment for stored shared object files
260 | alignment = 4096;
261 | } else {
262 | alignment = 4;
263 | }
264 | long extraDataOffset = _getFilePointer() + 2 + header.name.length;
265 | extra = align(alignment, extra, extraDataOffset);
266 | }
267 | _writeShort(extra.length);
268 | _writeBytes(header.name);
269 |
270 | _writeBytes(extra);
271 | }
272 |
273 | private void setupNeedZip64(CenterFileHeader header) {
274 | if (forceZip64) {
275 | header.sizeNeedZip64 = true;
276 | header.offsetNeedZip64 = true;
277 | } else {
278 | // 只知道原体积但不知道压缩后体积时,若原体积大于0xf0000000L则采用zip64
279 | if (header.size >= 0xf0000000L && header.compressedSize == ZipEntry.UNKNOWN_SIZE) {
280 | header.sizeNeedZip64 = true;
281 | } else if (header.size >= MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE ||
282 | header.compressedSize >= MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) {
283 | header.sizeNeedZip64 = true;
284 | }
285 | if (header.headerOffset >= MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE)
286 | header.offsetNeedZip64 = true;
287 | }
288 | if (header.needZip64()) {
289 | needsZip64EocdRecord = true;
290 | }
291 | if (header.size == ZipEntry.UNKNOWN_SIZE) {
292 | header.size = 0;
293 | }
294 | if (header.compressedSize == ZipEntry.UNKNOWN_SIZE) {
295 | header.compressedSize = 0;
296 | }
297 | }
298 |
299 | public void write(int b) throws IOException {
300 | topOutput.write(b);
301 | }
302 |
303 | public void write(byte[] data) throws IOException {
304 | topOutput.write(data);
305 | }
306 |
307 | public void write(byte[] data, int off, int len) throws IOException {
308 | topOutput.write(data, off, len);
309 | }
310 |
311 | public void writeFully(InputStream is) throws IOException {
312 | int len;
313 | byte[] b = new byte[1024 * 4];
314 | while ((len = is.read(b)) > 0)
315 | write(b, 0, len);
316 | }
317 |
318 | public void closeEntry() throws IOException {
319 | if (currentHeader == null) {
320 | return;
321 | }
322 | topOutput.close();
323 |
324 | currentHeader.crc = topOutput.getCrc();
325 | currentHeader.compressedSize = bottomOutput.getCount();
326 | currentHeader.size = topOutput.getCount();
327 |
328 | long saved = _getFilePointer();
329 | _seek(currentHeader.headerOffset + WORD + SHORT + SHORT + SHORT + WORD);
330 | _writeInt(currentHeader.crc);
331 | if (currentHeader.sizeNeedZip64) {
332 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE);
333 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE);
334 |
335 | // skip nameLength + extraLength + nameData
336 | _skip(SHORT + SHORT + currentHeader.name.length);
337 |
338 | // skip zip64Extra(header + size)
339 | _skip(4);
340 |
341 | // update local extra
342 | _writeLong(currentHeader.size);
343 | _writeLong(currentHeader.compressedSize);
344 | } else {
345 | if (currentHeader.compressedSize >= MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE ||
346 | currentHeader.size >= MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) {
347 | throw new IOException("Zip entry size needs zip64: name=" + new String(currentHeader.name)
348 | + ", compressedSize=" + currentHeader.compressedSize
349 | + ", size=" + currentHeader.size
350 | );
351 | }
352 | _writeUInt(currentHeader.compressedSize);
353 | _writeUInt(currentHeader.size);
354 | }
355 |
356 | archive.seek(saved);
357 |
358 | topOutput = null;
359 | bottomOutput = null;
360 | currentHeader = null;
361 | }
362 |
363 | @Override
364 | public void close() throws IOException {
365 | if (archive.isClosed())
366 | return;
367 | if (currentHeader != null)
368 | closeEntry();
369 | long cdOffset = _getFilePointer();
370 | try {
371 | Collections.sort(headers);
372 | } catch (RuntimeException e) {
373 | throw new IOException(e);
374 | }
375 | for (CenterFileHeader header : headers) {
376 | writeCentralFileHeader(header);
377 | }
378 | long cdSize = _getFilePointer() - cdOffset;
379 | writeCentralDirectoryEnd(cdSize, cdOffset);
380 | archive.close();
381 | }
382 |
383 | private byte[] align(int alignment, byte[] extra, long extraDataOffset) throws IOException {
384 | if (isAligned(extraDataOffset + extra.length, alignment)) {
385 | return extra;
386 | }
387 | extra = ExtraDataRecord.trim(extra);
388 | int padding = getAlignedPadding(extraDataOffset + extra.length, alignment);
389 | return Arrays.copyOf(extra, extra.length + padding);
390 | }
391 |
392 | private static boolean isAligned(long pos, int alignTo) {
393 | return (pos % alignTo) == 0;
394 | }
395 |
396 | private static int getAlignedPadding(long pos, int alignTo) {
397 | return (int) (alignTo - (pos % alignTo)) % alignTo;
398 | }
399 |
400 | private void writeCentralFileHeader(CenterFileHeader header) throws IOException {
401 | boolean needZip64 = header.needZip64();
402 | byte[] extra;
403 | if (needZip64) {
404 | byte[] data = new byte[3 * 8];
405 | ZipUtil.writeLong(data, 0, header.size);
406 | ZipUtil.writeLong(data, 8, header.compressedSize);
407 | ZipUtil.writeLong(data, 16, header.headerOffset);
408 | extra = ExtraDataRecord.set(header.extra, ZIP64_EXTENDED_INFO_HEADER_ID, data);
409 | } else {
410 | extra = ExtraDataRecord.remove(header.extra, ZIP64_EXTENDED_INFO_HEADER_ID);
411 | }
412 |
413 | _writeInt(CFH_SIG);
414 | _writeShort(Math.max(20, header.version()));
415 | _writeShort(header.version());
416 | _writeShort(header.generalPurposeFlag);
417 | _writeShort(header.method);
418 | _writeInt(header.time);
419 | _writeInt(header.crc);
420 | if (needZip64) {
421 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE);
422 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE);
423 | } else {
424 | _writeUInt(header.compressedSize);
425 | _writeUInt(header.size);
426 | }
427 | _writeShort(header.name.length);
428 | _writeShort(extra.length);
429 | _writeShort(header.comment.length);
430 | _writeShort(header.diskNumberStart);
431 | _writeShort(header.internalAttributes);
432 | _writeInt(header.externalAttributes);
433 | if (needZip64) {
434 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE);
435 | } else {
436 | _writeUInt(header.headerOffset);
437 | }
438 | _writeBytes(header.name);
439 | _writeBytes(extra);
440 | _writeBytes(header.comment);
441 | }
442 |
443 | private void writeCentralDirectoryEnd(long cdSize, long cdOffset) throws IOException {
444 | if (headers.size() >= 0xffff) {
445 | needsZip64EocdRecord = true;
446 | }
447 | if (needsZip64EocdRecord) {
448 | // Zip64 end of central directory record
449 | _writeInt(ZIP64_EOCD_RECORD_SIGNATURE);
450 | _writeLong(ZIP64_EOCD_RECORD_EFFECTIVE_SIZE + 4);
451 | _writeShort(20);
452 | _writeShort(20);
453 | _writeInt(0); // number of disk
454 | _writeInt(0); // number of disk with start of central dir.
455 | _writeLong(headers.size());
456 | _writeLong(headers.size());
457 | _writeLong(cdSize);
458 | _writeLong(cdOffset);
459 |
460 | // Zip64 end of central directory locator
461 | _writeInt(ZIP64_LOCATOR_SIGNATURE);
462 | _writeInt(0);
463 | _writeLong(cdSize + cdOffset);
464 | _writeInt(1);
465 | }
466 | byte[] comment = this.comment == null ? new byte[0] : this.comment.getBytes(encoding);
467 | _writeInt(EOCD_SIG);
468 | _writeShort(0);
469 | _writeShort(0);
470 | if (needsZip64EocdRecord) {
471 | _writeShort(0xFFFF);
472 | _writeShort(0xFFFF);
473 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE);
474 | _writeUInt(MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE);
475 | } else {
476 | _writeShort(headers.size());
477 | _writeShort(headers.size());
478 | _writeUInt(cdSize);
479 | _writeUInt(cdOffset);
480 | }
481 | _writeShort(comment.length);
482 | _writeBytes(comment);
483 | }
484 |
485 | private void _seek(long position) throws IOException {
486 | archive.seek(position);
487 | }
488 |
489 | private long _getFilePointer() throws IOException {
490 | return archive.getFilePointer();
491 | }
492 |
493 | private void _skip(int n) throws IOException {
494 | archive.skipBytes(n);
495 | }
496 |
497 | private void _writeBytes(byte[] data) throws IOException {
498 | if (data.length > 0)
499 | archive.write(data);
500 | }
501 |
502 | private void _writeShort(int v) throws IOException {
503 | archive.write(v & 0xFF);
504 | archive.write((v >>> 8) & 0xFF);
505 | }
506 |
507 | private void _writeInt(int v) throws IOException {
508 | archive.write(v & 0xFF);
509 | archive.write((v >>> 8) & 0xFF);
510 | archive.write((v >>> 16) & 0xFF);
511 | archive.write((v >>> 24) & 0xFF);
512 | }
513 |
514 | private void _writeLong(long v) throws IOException {
515 | archive.write((int) (v & 0xFF));
516 | archive.write((int) ((v >>> 8) & 0xFF));
517 | archive.write((int) ((v >>> 16) & 0xFF));
518 | archive.write((int) ((v >>> 24) & 0xFF));
519 | archive.write((int) ((v >>> 32) & 0xFF));
520 | archive.write((int) ((v >>> 40) & 0xFF));
521 | archive.write((int) ((v >>> 48) & 0xFF));
522 | archive.write((int) ((v >>> 56) & 0xFF));
523 | }
524 |
525 | private void _writeUInt(long v) throws IOException {
526 | if (v < 0 || v > 0xffffffffL) {
527 | throw new IOException("Value out of unsigned int.");
528 | }
529 | archive.write((int) (v & 0xFF));
530 | archive.write((int) ((v >>> 8) & 0xFF));
531 | archive.write((int) ((v >>> 16) & 0xFF));
532 | archive.write((int) ((v >>> 24) & 0xFF));
533 | }
534 | }
535 |
--------------------------------------------------------------------------------
/src/bin/zip/ZipUtil.java:
--------------------------------------------------------------------------------
1 | package bin.zip;
2 |
3 | import java.io.EOFException;
4 | import java.io.IOException;
5 | import java.util.Calendar;
6 |
7 | /**
8 | * @author Bin
9 | */
10 | public class ZipUtil {
11 | private static final Calendar CALENDAR = Calendar.getInstance();
12 |
13 | public static long dosToJavaTime(long dosTime) {
14 | synchronized (CALENDAR) {
15 | CALENDAR.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980);
16 | CALENDAR.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1);
17 | CALENDAR.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f);
18 | CALENDAR.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f);
19 | CALENDAR.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f);
20 | CALENDAR.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e);
21 | return CALENDAR.getTime().getTime();
22 | }
23 | }
24 |
25 | public static long javaToDosTime(long time) {
26 | Calendar cal = Calendar.getInstance();
27 | cal.setTimeInMillis(time);
28 | int year = cal.get(Calendar.YEAR);
29 | if (year < 1980) {
30 | return (1 << 21) | (1 << 16);
31 | }
32 | return (year - 1980L) << 25 | (cal.get(Calendar.MONTH) + 1) << 21 |
33 | cal.get(Calendar.DATE) << 16 | cal.get(Calendar.HOUR_OF_DAY) << 11 | cal.get(Calendar.MINUTE) << 5 |
34 | cal.get(Calendar.SECOND) >> 1;
35 | }
36 |
37 | public static void writeByte(byte[] array, int pos, int value) throws IOException {
38 | if (pos + 1 > array.length) {
39 | throw new EOFException();
40 | }
41 | array[pos] = (byte) (value & 0xFF);
42 | }
43 |
44 | public static void writeShort(byte[] array, int pos, int value) throws IOException {
45 | if (pos + 2 > array.length) {
46 | throw new EOFException();
47 | }
48 | array[pos] = (byte) (value & 0xFF);
49 | array[pos + 1] = (byte) (value >>> 8 & 0xFF);
50 | }
51 |
52 | public static void writeLong(byte[] array, int pos, long value) throws IOException {
53 | if (pos + 8 > array.length) {
54 | throw new EOFException();
55 | }
56 | array[pos] = (byte) (value & 0xFF);
57 | array[pos + 1] = (byte) (value >>> 8 & 0xFF);
58 | array[pos + 2] = (byte) (value >>> 16 & 0xFF);
59 | array[pos + 3] = (byte) (value >>> 24 & 0xFF);
60 | array[pos + 4] = (byte) (value >>> 32 & 0xFF);
61 | array[pos + 5] = (byte) (value >>> 40 & 0xFF);
62 | array[pos + 6] = (byte) (value >>> 48 & 0xFF);
63 | array[pos + 7] = (byte) (value >>> 56 & 0xFF);
64 | }
65 |
66 | public static void writeBytes(byte[] array, int pos, byte[] value) throws IOException {
67 | if (pos + value.length > array.length) {
68 | throw new EOFException();
69 | }
70 | System.arraycopy(value, 0, array, pos, value.length);
71 | }
72 |
73 | public static int readUByte(byte[] b, int off) throws IOException {
74 | if (off + 1 > b.length) {
75 | throw new EOFException();
76 | }
77 | return (b[off] & 0xff);
78 | }
79 |
80 | public static int readUShort(byte[] b, int off) throws IOException {
81 | if (off + 2 > b.length) {
82 | throw new EOFException();
83 | }
84 | return (b[off + 1] & 0xFF) << 8 | b[off] & 0xFF;
85 | }
86 |
87 | public static int readInt(byte[] b, int off) throws IOException {
88 | if (off + 4 > b.length) {
89 | throw new EOFException();
90 | }
91 | return b[off + 3] << 24 | (b[off + 2] & 0xFF) << 16 | (b[off + 1] & 0xFF) << 8 | b[off] & 0xFF;
92 | }
93 |
94 | public static long readLong(byte[] b, int off) throws IOException {
95 | if (off + 8 > b.length) {
96 | throw new EOFException();
97 | }
98 | return (long) b[off + 7] << 56 | ((long) b[off + 6] & 0xFF) << 48
99 | | ((long) b[off + 5] & 0xFF) << 40 | ((long) b[off + 4] & 0xFF) << 32
100 | | ((long) b[off + 3] & 0xFF) << 24 | ((long) b[off + 2] & 0xFF) << 16
101 | | ((long) b[off + 1] & 0xFF) << 8 | (long) b[off] & 0xFF;
102 | }
103 |
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/test.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/L-JINBIN/ApkDataMultiplexing/0887b62c24df53f68ded273e04934163ef3dcd94/test.apk
--------------------------------------------------------------------------------
/test.jks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/L-JINBIN/ApkDataMultiplexing/0887b62c24df53f68ded273e04934163ef3dcd94/test.jks
--------------------------------------------------------------------------------