pax_global_header 0000666 0000000 0000000 00000000064 14306644577 0014532 g ustar 00root root 0000000 0000000 52 comment=352d7a22e8b09f6284af2ace2dc57eb424f1bc09
zipflinger-7.2.2/ 0000775 0000000 0000000 00000000000 14306644577 0013713 5 ustar 00root root 0000000 0000000 zipflinger-7.2.2/BUILD 0000664 0000000 0000000 00000013333 14306644577 0014500 0 ustar 00root root 0000000 0000000 load("//tools/base/bazel:bazel.bzl", "iml_module")
load("//tools/base/bazel:maven.bzl", "maven_library")
load("//tools/base/common:version.bzl", "BUILD_VERSION")
maven_library(
name = "zipflinger",
srcs = glob([
"src/com/android/zipflinger/**/*.java",
]),
coordinates = "com.android:zipflinger:" + BUILD_VERSION,
description = "Library used to build and incrementally modify zip files",
visibility = [
"//tools/base:__pkg__",
"//tools/base/bazel:__subpackages__",
"//tools/base/build-system:__subpackages__",
"//tools/base/deploy/deployer:__subpackages__",
"//tools/base/lint:__subpackages__",
"//tools/base/signflinger:__subpackages__",
"//tools/base/zipflinger/tools:__subpackages__",
],
deps = [
"//tools/base/annotations",
],
)
java_library(
name = "test_utils",
srcs = [
"test/src/java/com/android/zipflinger/AbstractZipflingerTest.java",
"test/src/java/com/android/zipflinger/MockInputStream.java",
],
deps = [
":zipflinger",
"//tools/base/testutils:tools.testutils",
"@maven//:junit.junit",
],
)
java_test(
name = "testParsing",
size = "small",
srcs = [
"test/src/java/com/android/zipflinger/ParsingTest.java",
],
data = [
"test/resource/stripped.ap_",
"test/resource/zip_no_fd.zip",
"test/resource/zip_with_fd.zip",
],
jvm_flags = ["-Dtest.suite.jar=testParsing.jar"],
test_class = "com.android.testutils.JarTestSuite",
deps = [
":test_utils",
":zipflinger",
"//tools/base/testutils:tools.testutils",
"@maven//:junit.junit",
],
)
java_test(
name = "testsFreeStore",
size = "small",
srcs = [
"test/src/java/com/android/zipflinger/FreeStoreTest.java",
],
data = [
"test/resource/zip_no_fd.zip",
"test/resource/zip_with_fd.zip",
],
jvm_flags = ["-Dtest.suite.jar=testsFreeStore.jar"],
test_class = "com.android.testutils.JarTestSuite",
deps = [
":test_utils",
":zipflinger",
"//tools/base/testutils:tools.testutils",
"@maven//:junit.junit",
],
)
java_test(
name = "testsZipFlinger",
size = "medium",
srcs = [
"test/src/java/com/android/zipflinger/ZipFlingerTest.java",
],
data = [
"test/resource/1-2-3files.zip",
"test/resource/4-5files.zip",
"test/resource/file1.txt",
"test/resource/file2.txt",
"test/resource/file3.txt",
"test/resource/file4.txt",
"test/resource/text.txt",
"test/resource/two_files.zip",
"test/resource/zip_with_directories.zip",
],
jvm_flags = ["-Dtest.suite.jar=testsZipFlinger.jar"],
tags = ["no_test_mac"],
test_class = "com.android.testutils.JarTestSuite",
deps = [
":test_utils",
":zipflinger",
"//tools/base/testutils:tools.testutils",
"@maven//:junit.junit",
],
)
java_test(
name = "testZip64",
size = "medium",
srcs = [
"test/src/java/com/android/zipflinger/Zip64Test.java",
],
data = [
"test/resource/5GiBFile.zip",
],
jvm_flags = ["-Dtest.suite.jar=testZip64.jar"],
test_class = "com.android.testutils.JarTestSuite",
deps = [
":test_utils",
":zipflinger",
"//tools/base/testutils:tools.testutils",
"@maven//:junit.junit",
],
)
java_test(
name = "testsCompressor",
size = "small",
srcs = [
"test/src/java/com/android/zipflinger/CompressorTest.java",
],
data = [
"test/resource/file4.txt",
],
jvm_flags = ["-Dtest.suite.jar=testsCompressor.jar"],
test_class = "com.android.testutils.JarTestSuite",
deps = [
":test_utils",
":zipflinger",
"//tools/base/testutils:tools.testutils",
"@maven//:junit.junit",
],
)
java_test(
name = "testsMerge",
size = "small",
srcs = [
"test/src/java/com/android/zipflinger/ZipMergeTest.java",
],
data = [
"test/resource/1-2-3files.zip",
"test/resource/4-5files.zip",
],
jvm_flags = ["-Dtest.suite.jar=testsMerge.jar"],
test_class = "com.android.testutils.JarTestSuite",
deps = [
":test_utils",
":zipflinger",
"//tools/base/testutils:tools.testutils",
"@maven//:junit.junit",
],
)
java_test(
name = "testInts",
size = "small",
srcs = [
"test/src/java/com/android/zipflinger/IntsTest.java",
],
jvm_flags = ["-Dtest.suite.jar=testInts.jar"],
test_class = "com.android.testutils.JarTestSuite",
deps = [
":zipflinger",
"//tools/base/testutils:tools.testutils",
"@maven//:junit.junit",
],
)
java_test(
name = "testRepo",
size = "small",
srcs = [
"test/src/java/com/android/zipflinger/RepoTest.java",
],
jvm_flags = ["-Dtest.suite.jar=testRepo.jar"],
test_class = "com.android.testutils.JarTestSuite",
deps = [
":test_utils",
":zipflinger",
"//tools/base/testutils:tools.testutils",
"@maven//:junit.junit",
],
)
# managed by go/iml_to_build
iml_module(
name = "studio.android.sdktools.zipflinger",
srcs = ["src"],
iml_files = ["android.sdktools.zipflinger.iml"],
lint_baseline = "lint_baseline.xml",
tags = ["no_test_mac"],
test_data = glob(["test/resource/**"]),
test_srcs = ["test/src/java"],
visibility = ["//visibility:public"],
# do not sort: must match IML order
deps = [
"//prebuilts/studio/intellij-sdk:studio-sdk",
"//tools/base/annotations:studio.android.sdktools.android-annotations[module]",
"//tools/base/testutils:studio.android.sdktools.testutils[module, test]",
],
)
zipflinger-7.2.2/README.md 0000664 0000000 0000000 00000022613 14306644577 0015176 0 ustar 00root root 0000000 0000000 # Zipflinger
Zipflinger is a library dedicated to ZIP files manipulation. It can create an archive from scratch
but also add/remove entries without decompressing/compressing the whole archive.
The goal of the library is to work as fast as possible (its original purpose is to enable fast
Android APK deployment). The two main features allowing high-speed are Zipflinger's ability to
edit the CD of an archive and its usage of zero-copy transfer when moving entries across archives.
The library is made of four components named ZipArchive, Freestore, Mapper (Input), and Writer
(Output).
```
+------------------------------------+
| ZipArchive |
+------------+-----------+-----------+
| Freestore | Mapper | Writer |
+------------+-----------+-----------+
^ +
| |
+ v
+------------------------------------+
| MYFILE.ZIP |
+------------------------------------+
```
Design choice discussion:
Order of operations:
====================
In order to avoid creating holes when editing an archive, zipflinger recommends (but does not enforce)
submitting all delete operations first and then submit add operations. A "deferred add" mechanism was
initially used where delete operations were carried immediately but additions were deferred until the
archive was closed. This approach was ultimately abandoned since it increased the memory footprint
significantly when BytesSource were involved.
Prevent silent overwrite:
=========================
It is by design that Zipflinger throws an exception when attempting to overwrite an entry in an archive.
By asking developer to aknowledge an overwrite by first deleting an entry, this mecanism has allowed to
surface many bugs.
## ZipArchive
ZipArchive is the interface to create/read/write an archive. Typically an user will provide the path
to an archive and request operations such as add/delete.
In the code sample below, an Android APK is "incrementally" updated. The AAPT2 output (recognizable
to its file extension .apk_) is opened. Since the archive exists, it will be modified. Had it not
existed, the archive would have been create. Two operations are requested:
1. An old entry is deleted.
2. A new entry is added.
```
ZipArchive archive = new ZipArchive("app.ap_");
// Delete (to reduce holes to a minimum, it is mandatory to do all delete
// operation first).
archive.delete("classes18.dex");
// Add sources
File myFile = new File("/path/to/file");
BytesSource source = new BytesSource(file, "entryName", Deflater.NO_COMPRESSION);
archive.add(source);
// Don't forget to close in order to release the archive fd/handle.
archive.close();
```
Such an operation can be performed by Zipflinger in under 100 ms with a mid-range 2019 SSD laptop.
If an entry has been deleted in the middle of the archive, Zipflinger will not leave a "hole" there.
This is done in order to be compatible with top-down parsers such as jarsigner or the JDK zip classes. To this effect,
Zipflinger fills empty space with virtual entries (a.k.a a Local File Header with no name, up to
64KiB extra and no Central Directory entry). Alignment is also done via "extra field".
Entry name heuristic:
- Deleting a non-existing entry will fail silently.
- Adding an existing entry will not silently overwrite but will throw an exception instead.
## ZipMap
The mapper only plays a part when opening an existing archive. The goal of the mapper is to locate
all entries via the Central Directories and build a map of the LFHs (Local File Header) , CDRs
(Central Directory Record) and compile these information into a list of Entry. This data is fed to
the FreeStore to build a map of what is currently used in the file and where their is available
space. It is also an efficient way to list entries in a zip archive if it is the only operation
you need to perform.
Note that if a zip contains several entries with the same name, the last entry in CD order
(not top-down) order is kept.
## ZipRepo
If all operations needed are to list entries and read entries content, ZipRepo is the object to use.
It is lightweight compared to a ZipArchive and allows to read entries via an InputStream to exceed
the 2GiB limitation and reduce heap stress.
## Freestore
The freestore behaves like a memory allocator except that is deals with file address space instead
of memory address space. The list of file locations is kept in a double linked list. Two consecutive
free areas are never contiguous. If space is freed, adjacent free blocks are merged together. As a
result, used space is implicitly described by the "gap" between two free blocks.
All write/delete operations in an archive must first go through the freestore.
- When a zip entry is deleted, the entry Location is returned to the FreeStore.
- When a zip entry is added, a Location must be requested to the Freestore.
Allocations alignment is supported. This is to accommodate Android Package Manager optimizations
where a zip entry is directly mmaped. Upon requesting an aligned allocation, an offset must also
be provided because what needs to be aligned is not the ZIP entry but the zip entry payload.
## ZipWriter
All zip write operations are tracked by the Writer. This is done so an accurate map of written
Locations can be generated when the file is closed and enable incremental V2 signing.
## Sources
To add an entry to a zip, Zipflinger is fed sources. Typically two sources ares supported:
- Source (usually BytesSource)
- ZipSource (made of several ZipSourceEntry)
Source are well-suited for payload already located in memory or in a File. The typical usecase
is when an APK needs to be updated with a new file and also V1 signed. The new file will have been
loaded from storage to generate a hash values.
Note that a BytesSource can be built from an InputStream, in which case the the stream is drained
entirely in the BytesSource constructor.
ZipSource allows to transfer entries from one zip to an other. Zero-copy is used to speed up transfer
. Compression type/format is not changed during the transfer. Upon selecting an entry for transfer,
ZipSourceEntry is returned. The handle is only used if alignment needs to be requested.
All sources can be requested to be aligned via the Source.align() method. All sources except for the
ZipSourceEntry can be requested to be uncompressed/re-compressed.
## File properties and symbolic links
Zipflinger will preserve UNIX permissions as found in the Central Directory "external
attribute" entries when transferring entries between zip archives.
By default, zipflinger creates zip entries with "read" and "write" permissions for user, group, and
others. Symbolic links are also followed. If you want to preserve the executable permission or if
you want to not follow symbolic links, you must use the FullFileSource object.
Keep in mind that FullFileSource is a little bit slower to process files since it needs to perform
extra I/O in order to retrieve each properties.
## Memory (heap) stress
If you find that ByteSource stresses the heap too much or if you run out of memory on large entries,
use a LargeFileSource. These use storage to temporarily store the payload and never load it all
in memory. Because this is also done in the Constructor, compression can still be parallelized and
there is little speed impact.
## Performance considerations when using ZipSource
Zipflinger excels at moving zip entries between zip archives thanks to zero-copy transfer. However
using zero-copy is not always possible.
Best case: If no compression change is requested or if both the source and the destination are inflated,
then zero copy transfer will be used and max speed is achieved.
Ok case: If the src is inflated and the dst is deflated, zipflinger cannot zero-copy since the payload
must be deflated.
Worse case: If both the src and the dst are deflated, there is no way for Zipflinger to know what level
of compression was used to generate the src (this is not part of Deflate specs or Zip container format).
In order to guarantee the deflate level, Zipflinger has not choice but to inflate the
payload and then deflate it at the requested level, even if the compression level are identical.
## Zip64 Support
Zipflinger has full support for zip64 archives. It is able to handle zip64EOCD (more than 65536
entries) with zip64Locator and zip64 extra fields containing 64-bit compressed, uncompressed, and
offset values (archives larger than 4GiB). There is no facility to handle files larger than 2GiB.
## Profiling
To peek inside Zipflinger and understand where walltime is spent, you can run the "profiler" target.
```
tools/base/bazel/bazel run //tools/base/zipflinger:profiler
Profiling with an APK :
- Total size (MiB) : 118
- Num res : 5000
- Size res (KiB) : 16
- Num dex : 10
- Size dex (MiB) : 4
```
Once the target has run, retrieve the report from the workstation tmp folder. e.g On Linux:
```
cp /tmp/report.json ~/
```
You can examine the report in Chrome via about://tracing.
Edit time (ms) on a 3Ghz machine with a PM981 NVMe drive.
```
APK Size NumRes SizeRes NumDex SizeDex Time (ms)
120 MiB 5000 16 KiB 10 4 MiB 27
60 MiB 2500 16 KiB 10 4 MiB 18
49 MiB 2500 4 KiB 10 4 MiB 18
```
The edit time is dominated by the parsing time (itself dominated by the number of entries).
zipflinger-7.2.2/android.sdktools.zipflinger.iml 0000664 0000000 0000000 00000001352 14306644577 0022050 0 ustar 00root root 0000000 0000000
zipflinger-7.2.2/cookbook.md 0000664 0000000 0000000 00000006766 14306644577 0016062 0 ustar 00root root 0000000 0000000 # Zipflinger cheatsheet
Zipflinger is a library dedicated to ZIP files manipulation. It is capable of adding and removing
entries without decompressing/compressing the whole archive. It supports:
- Listing content of a zip archive.
- Deleting entry in an archive.
- Adding entries in an archive with source from filesystem, memory, and other zip archives.
Deleting a non-existing entry will fail silently.
Adding an existing entry will not silently overwrite but will throw an exception instead.
## How to list the content of an archive
```
Map map = ZipArchive.listEntries(new File("/path/to/zip"));
for(Entry entry : map.getEntries().values()) {
entry.getName();
entry.getCrc();
...
}
```
## How to replace an entry in an archive
```
ZipArchive zip = new ZipArchive("app.apk");
zip.delete("classes18.dex"); // All deletes must be submitted first.
zip.add(new BytesSource(new File("classes18.dex"), "classes18.dex", Deflater.BEST_SPEED));
zip.add(new BytesSource(new File("img.png"), "image.png", Deflater.NO_COMPRESSION));
zip.close();
```
## How to merge two zips into one
```
ZipArchive zip = new ZipArchive("app.apk");
ZipSource zipSource1 = ZipSource.selectAll(new File("/path/to/zip1.zip"));
zip.add(zipSource1);
ZipSource zipSource2 = ZipSource.selectAll(new File("/path/to/zip2.zip"));
zip.add(zipSource2);
zip.close();
```
## How to copy a zip entry from an other zip into an existing apk
```
ZipArchive zip = new ZipArchive("app.apk");
ZipSource zipSource = new ZipSource(new File("/path/to/zip1.zip"));
zipSource.select("classes18.dex", "classes18NewName.dex"); // non-aligned (default)
ZipSourceEntry alignedEntry = zipSource.select("lib.so", "lib.so"); // aligned
alignedEntry.align(4);
zip.addZipSource(zipSource);
zip.close();
```
## How to iterate over a zip source entries and select only a few
```
ZipArchive zip = new ZipArchive("app.apk");
ZipSource zipSource = new ZipSource(new File("/path/to/zip1.zip"));
for(String name : zipSource.entries().keys()) {
if (youwantIt) {
zipSource.select(name, "newName");
}
}
zip.add(zipSource);
zip.close();
```
## Generate multiple zips from one zip source
Creating a ZipSource is not an I/O free operation since the CD of the source archive has to be parsed.
In the case where one source zip is to be used to generate multiple destination zips, parsing
can be done only once by providing the same ZipMap to each ZipSource.
```
// The source zip is parsed only once.
ZipMap map = ZipMap.from(new File("source.zip"));
ZipSource zipSource1 = new ZipSource(map);
zipSource1.select("a", "a");
try(ZipArchive archive = new ZipArchive("dest1.zip")) {
archive.add(zipSource1);
}
ZipSource zipSource2 = new ZipSource(map);
zipSource2.select("b", "b");
try(ZipArchive archive = new ZipArchive("dest2.zip")) {
archive.add(zipSource2);
}
```
# Add files to a zip and preserve executable permission
```
try(ZipArchive zip = new ZipArchive("archive.zip")) {
String p = "/path/x";
int c = Deflater.NO_COMPRESSION;
zip.add(new FullFileSource(p, "x", c));
}
```
# Add symbolic links to a zip
```
try(ZipArchive zip = new ZipArchive("archive.zip")) {
String p = "/path/x";
int c = Deflater.NO_COMPRESSION;
FullFileSource.Symlink perm = FullFileSource.Symlink.DO_NOT_FOLLOW;
zip.add(new FullFileSource(p , "x", c, perm));
}
```
# How to extract content from an archive
```
try(ZipRepo repo = new ZipRepo("source.zip")) {
try(InputStream inputStream = repo.getContent("entryName")) {
...
}
}
```
zipflinger-7.2.2/lint_baseline.xml 0000664 0000000 0000000 00000002723 14306644577 0017251 0 ustar 00root root 0000000 0000000
zipflinger-7.2.2/src/ 0000775 0000000 0000000 00000000000 14306644577 0014502 5 ustar 00root root 0000000 0000000 zipflinger-7.2.2/src/com/ 0000775 0000000 0000000 00000000000 14306644577 0015260 5 ustar 00root root 0000000 0000000 zipflinger-7.2.2/src/com/android/ 0000775 0000000 0000000 00000000000 14306644577 0016700 5 ustar 00root root 0000000 0000000 zipflinger-7.2.2/src/com/android/zipflinger/ 0000775 0000000 0000000 00000000000 14306644577 0021051 5 ustar 00root root 0000000 0000000 zipflinger-7.2.2/src/com/android/zipflinger/Archive.java 0000664 0000000 0000000 00000003673 14306644577 0023306 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.io.Closeable;
import java.io.IOException;
public interface Archive extends Closeable {
/**
* Add a source to the archive.
*
* @param source The source to add to this zip archive.
* @throws IllegalStateException if the entry name already exists in the archive.
* @throws IOException if writing to the zip archive fails.
*/
void add(@NonNull Source source) throws IOException;
/**
* Add a set of selected entries from an other zip archive.
*
* @param sources A zip archive with selected entries to add to this zip archive.
* @throws IllegalStateException if the entry name already exists in the archive.
* @throws IOException if writing to the zip archive fails.
*/
void add(@NonNull ZipSource sources) throws IOException;
/**
* Delete an entry from this archive. If the entry did not exist, this method does nothing. To
* avoid creating "holes" in the archive, it is mendatory to delete all entries first and add
* sources second.
*
* @param name The name of the entry to delete.
* @throws IllegalStateException if entries have been added.
*/
void delete(@NonNull String name) throws IOException;
@Override
void close() throws IOException;
}
zipflinger-7.2.2/src/com/android/zipflinger/BytesSource.java 0000664 0000000 0000000 00000006753 14306644577 0024176 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.Deflater;
public class BytesSource extends Source {
// Bytes to be written in the zip, after the Local File Header.
private ByteBuffer zipEntryPayload;
protected BytesSource(String name) {
super(name);
}
/**
* @param bytes
* @param name
* @param compressionLevel One of java.util.zip.Deflater compression level.
*/
public BytesSource(@NonNull byte[] bytes, @NonNull String name, int compressionLevel)
throws IOException {
super(name);
build(bytes, bytes.length, compressionLevel);
}
public BytesSource(@NonNull Path file, @NonNull String name, int compressionLevel)
throws IOException {
super(name);
byte[] bytes = Files.readAllBytes(file);
build(bytes, bytes.length, compressionLevel);
}
/** @deprecated Use {@link #BytesSource(Path, String, int)} instead. */
@Deprecated
public BytesSource(@NonNull File file, @NonNull String name, int compressionLevel)
throws IOException {
this(file.toPath(), name, compressionLevel);
}
/**
* @param stream BytesSource takes ownership of the InputStream and will close it after draining
* it.
* @param name
* @param compressionLevel
* @throws IOException
*/
public BytesSource(@NonNull InputStream stream, @NonNull String name, int compressionLevel)
throws IOException {
super(name);
try (NoCopyByteArrayOutputStream ncbos = new NoCopyByteArrayOutputStream(16000)) {
byte[] tmpBuffer = new byte[16000];
int bytesRead;
while ((bytesRead = stream.read(tmpBuffer)) != -1) {
ncbos.write(tmpBuffer, 0, bytesRead);
}
stream.close();
build(ncbos.buf(), ncbos.getCount(), compressionLevel);
}
}
protected void build(byte[] bytes, int size, int compressionLevel) throws IOException {
crc = Crc32.crc32(bytes, 0, size);
uncompressedSize = size;
if (compressionLevel == Deflater.NO_COMPRESSION) {
zipEntryPayload = ByteBuffer.wrap(bytes, 0, size);
compressedSize = uncompressedSize;
compressionFlag = LocalFileHeader.COMPRESSION_NONE;
} else {
zipEntryPayload = Compressor.deflate(bytes, 0, size, compressionLevel);
compressedSize = zipEntryPayload.limit();
compressionFlag = LocalFileHeader.COMPRESSION_DEFLATE;
}
}
@Override
public void prepare() {}
@Override
public long writeTo(@NonNull ZipWriter writer) throws IOException {
return writer.write(zipEntryPayload);
}
}
zipflinger-7.2.2/src/com/android/zipflinger/CentralDirectory.java 0000664 0000000 0000000 00000012224 14306644577 0025172 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
class CentralDirectory {
// The Central Directory as it was read when an archive already existed.
private final ByteBuffer buf;
private final List deletedLocations = new ArrayList<>();
private final Map entries;
private final Map addedEntries = new LinkedHashMap<>();
CentralDirectory(@NonNull ByteBuffer buf, @NonNull Map entries) {
this.buf = buf;
this.entries = entries;
}
@NonNull
Location delete(@NonNull String name) {
if (entries.containsKey(name)) {
Entry entry = entries.get(name);
deletedLocations.add(entry.getCdLocation());
entries.remove(name);
return entry.getLocation();
}
if (addedEntries.containsKey(name)) {
CentralDirectoryRecord record = addedEntries.remove(name);
return record.getLocation();
}
return Location.INVALID;
}
long getNumEntries() {
return (long) entries.size() + addedEntries.size();
}
void write(@NonNull ZipWriter writer) throws IOException {
// Four steps operations (first write old entries then new entries):
// 1/ Sort deleted entries by location.
// 2/ Create a list of "clean" (not deleted) locations.
// 3/ Write all old (non-deleted) locations.
// 4/ Write all new entries.
// Step 1
Collections.sort(deletedLocations);
// Step 2 (Build list of non-deleted locations).
List cleanCDLocations = new ArrayList<>();
long remainingStart = 0;
long remainingSize = buf.capacity();
for (Location deletedLocation : deletedLocations) {
Location cleanLoc =
new Location(remainingStart, deletedLocation.first - remainingStart);
// If cleanLoc is the left end of the remaining CD, cleanLoc size is 0.
if (cleanLoc.size() > 0) {
cleanCDLocations.add(cleanLoc);
}
remainingStart = deletedLocation.last + 1;
remainingSize -= (deletedLocation.size() + cleanLoc.size());
}
// Add the remaining of the CD as a clear location
if (remainingSize > 0) {
cleanCDLocations.add(new Location(remainingStart, remainingSize));
}
// Step 3: write clean CD chunks
for (Location toWrite : cleanCDLocations) {
buf.limit(Math.toIntExact(toWrite.first + toWrite.size()));
buf.position(Math.toIntExact(toWrite.first));
ByteBuffer view = buf.slice();
writer.write(view);
}
// Step 4: write new entries
// Assess how much data the CD requires
long totalSize = 0;
for (CentralDirectoryRecord record : addedEntries.values()) {
totalSize += record.getSize();
}
// Generate the CD portion of new entries
ByteBuffer cdBuffer =
ByteBuffer.allocate(Math.toIntExact(totalSize)).order(ByteOrder.LITTLE_ENDIAN);
for (CentralDirectoryRecord record : addedEntries.values()) {
record.write(cdBuffer);
}
// Write new entries
cdBuffer.rewind();
writer.write(cdBuffer);
}
void add(@NonNull String name, @NonNull CentralDirectoryRecord record) {
addedEntries.put(name, record);
}
boolean contains(@NonNull String name) {
return entries.containsKey(name) || addedEntries.containsKey(name);
}
@NonNull
List listEntries() {
List list = new ArrayList<>();
list.addAll(entries.keySet());
list.addAll(addedEntries.keySet());
return list;
}
@Nullable
public ExtractionInfo getExtractionInfo(@NonNull String name) {
Entry entry = entries.get(name);
if (entry != null) {
return new ExtractionInfo(entry.getPayloadLocation(), entry.isCompressed());
}
CentralDirectoryRecord cd = addedEntries.get(name);
if (cd != null) {
boolean isCompressed = cd.getCompressionFlag() != LocalFileHeader.COMPRESSION_NONE;
return new ExtractionInfo(cd.getPayloadLocation(), isCompressed);
}
return null;
}
}
zipflinger-7.2.2/src/com/android/zipflinger/CentralDirectoryRecord.java 0000664 0000000 0000000 00000011622 14306644577 0026332 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
class CentralDirectoryRecord {
public static final int SIGNATURE = 0x02014b50;
public static final int SIZE = 46;
public static final int DATA_DESCRIPTOR_FLAG = 0x0008;
public static final int DATA_DESCRIPTOR_SIGNATURE = 0x08074b50;
// JDK 9 consider time&data field with value 0 as invalid. Use 1 instead.
// These are in MS-DOS 16-bit format. For actual specs, see:
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms724247(v=vs.85).aspx
public static final short DEFAULT_TIME = 1 | 1 << 5 | 1 << 11;
public static final short DEFAULT_DATE = 1 | 1 << 5 | 1 << 9;
// Zip64 extra format:
// uint16_t id (0x0001)
// uint16_t size payload (0x18)
// Payload:
// - uint64_t Uncompressed size.
// - uint64_t Compressed size.
// - uint64_t offset to LFH in archive.
private static final int ZIP64_PAYLOAD_SIZE = Long.BYTES * 3;
private static final int ZIP64_EXTRA_SIZE = Short.BYTES * 2 + ZIP64_PAYLOAD_SIZE;
private final byte[] nameBytes;
private final int crc;
private final long compressedSize;
private final long uncompressedSize;
// Location of the Local file header to end of payload in file space.
private final Location location;
private final short compressionFlag;
private final short versionMadeBy;
private final int externalAttribute;
private final Location payloadLocation;
private final boolean isZip64;
CentralDirectoryRecord(@NonNull Source source,
Location location,
Location payloadLocation) {
this.nameBytes = source.getNameBytes();
this.crc = source.getCrc();
this.compressedSize = source.getCompressedSize();
this.uncompressedSize = source.getUncompressedSize();
this.location = location;
this.compressionFlag = source.getCompressionFlag();
this.payloadLocation = payloadLocation;
this.isZip64 =
compressedSize > Zip64.LONG_MAGIC
|| uncompressedSize > Zip64.LONG_MAGIC
|| location.first > Zip64.LONG_MAGIC;
this.versionMadeBy = source.getVersionMadeBy();
this.externalAttribute = source.getExternalAttributes();
}
void write(@NonNull ByteBuffer buf) {
short versionNeeded = isZip64 ? Zip64.VERSION_NEEDED : 0;
int size = isZip64 ? Zip64.INT_MAGIC : Ints.longToUint(uncompressedSize);
int csize = isZip64 ? Zip64.INT_MAGIC : Ints.longToUint(compressedSize);
int offset = isZip64 ? Zip64.INT_MAGIC : Ints.longToUint(location.first);
ByteBuffer extra = buildExtraField();
buf.putInt(SIGNATURE);
buf.putShort(versionMadeBy);
buf.putShort(versionNeeded);
buf.putShort((short) 0); // flag
buf.putShort(compressionFlag);
buf.putShort(DEFAULT_TIME);
buf.putShort(DEFAULT_DATE);
buf.putInt(crc);
buf.putInt(csize); // compressed size
buf.putInt(size); // size
buf.putShort(Ints.intToUshort(nameBytes.length));
buf.putShort(Ints.intToUshort(extra.capacity()));
buf.putShort((short) 0); // comment size
buf.putShort((short) 0); // disk # start
buf.putShort((short) 0); // internal att
buf.putInt(externalAttribute);
buf.putInt(offset);
buf.put(nameBytes);
buf.put(extra);
}
short getCompressionFlag() {
return compressionFlag;
}
long getSize() {
long extraSize = isZip64 ? ZIP64_EXTRA_SIZE : 0;
return SIZE + nameBytes.length + extraSize;
}
@NonNull
Location getPayloadLocation() {
return payloadLocation;
}
@NonNull
Location getLocation() {
return location;
}
@NonNull
private ByteBuffer buildExtraField() {
if (!isZip64) {
return ByteBuffer.allocate(0);
}
ByteBuffer buf = ByteBuffer.allocate(ZIP64_EXTRA_SIZE).order(ByteOrder.LITTLE_ENDIAN);
buf.putShort(Zip64.EXTRA_ID);
buf.putShort(Ints.intToUshort(ZIP64_PAYLOAD_SIZE));
buf.putLong(uncompressedSize);
buf.putLong(compressedSize);
buf.putLong(location.first);
buf.rewind();
return buf;
}
}
zipflinger-7.2.2/src/com/android/zipflinger/Compressor.java 0000664 0000000 0000000 00000006664 14306644577 0024064 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import java.util.zip.InflaterOutputStream;
public class Compressor {
@NonNull
public static ByteBuffer deflate(
@NonNull byte[] bytes, int offset, int size, int compressionLevel) throws IOException {
NoCopyByteArrayOutputStream out = new NoCopyByteArrayOutputStream(size);
Deflater deflater = new Deflater(compressionLevel, true);
try (DeflaterOutputStream dout = new DeflaterOutputStream(out, deflater)) {
dout.write(bytes, offset, size);
dout.flush();
}
return out.getByteBuffer();
}
@NonNull
public static ByteBuffer deflate(@NonNull byte[] bytes, int compressionLevel)
throws IOException {
return deflate(bytes, 0, bytes.length, compressionLevel);
}
@NonNull
public static ByteBuffer inflate(@NonNull byte[] bytes) throws IOException {
NoCopyByteArrayOutputStream out = new NoCopyByteArrayOutputStream(bytes.length);
Inflater inflater = new Inflater(true);
try (InflaterOutputStream dout = new InflaterOutputStream(out, inflater)) {
dout.write(bytes);
dout.flush();
}
return out.getByteBuffer();
}
// Exhaust input content into output, inflate / deflate data as needed.
// Closes both streams once piping is done.
public static void pipe(
@NonNull InputStream in,
@NonNull OutputStream out,
boolean inDeflated,
int outputCompression)
throws IOException {
Inflater inflater = new Inflater(true);
Deflater deflater = new Deflater(outputCompression, true);
boolean outDeflated = outputCompression != Deflater.NO_COMPRESSION;
try (InputStream ins = inDeflated ? new InflaterInputStream(in, inflater) : in;
OutputStream outs = outDeflated ? new DeflaterOutputStream(out, deflater) : out) {
byte[] buffer = new byte[8192];
int read;
while ((read = ins.read(buffer)) != -1) {
outs.write(buffer, 0, read);
}
}
}
// Is it the caller's responsibility to close() the returned InputStream.
@NonNull
static InputStream wrapToInflate(@NonNull InputStream inputStream) {
Inflater inflater = new Inflater(true);
return new InflaterInputStream(inputStream, inflater);
}
@NonNull
static Deflater getDeflater(int compressionLevel) {
return new Deflater(compressionLevel, true);
}
private Compressor() {}
}
zipflinger-7.2.2/src/com/android/zipflinger/Crc32.java 0000664 0000000 0000000 00000002061 14306644577 0022567 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.util.zip.CRC32;
class Crc32 {
public static int crc32(@NonNull byte[] bytes) {
return crc32(bytes, 0, bytes.length);
}
public static int crc32(@NonNull byte[] bytes, int offset, int size) {
CRC32 crc = new CRC32();
crc.update(bytes, offset, size);
return Ints.longToUint(crc.getValue());
}
private Crc32() {}
}
zipflinger-7.2.2/src/com/android/zipflinger/EndOfCentralDirectory.java 0000664 0000000 0000000 00000010356 14306644577 0026112 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
class EndOfCentralDirectory {
private static final int SIGNATURE = 0x06054b50;
static final int SIZE = 22;
private static final long MAX_SIZE = Ints.USHRT_MAX + SIZE;
static final short DISK_NUMBER = 0;
private int numEntries;
private Location location;
private Location cdLocation;
private EndOfCentralDirectory() {
this.numEntries = 0;
this.location = Location.INVALID;
this.cdLocation = Location.INVALID;
}
private void parse(@NonNull ByteBuffer buffer) {
// skip diskNumber (2) + cdDiskNumber (2) + #entries (2)
buffer.position(buffer.position() + 6);
numEntries = Ints.ushortToInt(buffer.getShort());
long cdSize = Ints.uintToLong(buffer.getInt());
long cdOffset = Ints.uintToLong(buffer.getInt());
cdLocation = new Location(cdOffset, cdSize);
buffer.position(buffer.position() + 2); // Skip comment length
}
@NonNull
public Location getLocation() {
return location;
}
@NonNull
public Location getCdLocation() {
return cdLocation;
}
public int numEntries() {
return numEntries;
}
public void setLocation(@NonNull Location location) {
this.location = location;
}
// Search the EOCD. If not found the returned object location will be set to Location.INVALID.
@NonNull
public static EndOfCentralDirectory find(@NonNull FileChannel channel) throws IOException {
long fileSize = channel.size();
EndOfCentralDirectory eocd = new EndOfCentralDirectory();
if (fileSize < SIZE) {
return eocd;
}
int sizeToRead = Math.toIntExact(Math.min(fileSize, MAX_SIZE));
long offset = fileSize - sizeToRead;
ByteBuffer buffer = ByteBuffer.allocate(sizeToRead).order(ByteOrder.LITTLE_ENDIAN);
channel.read(buffer, offset);
buffer.position(buffer.capacity() - SIZE);
while (true) {
int signature = buffer.getInt(); // Read 4 bytes.
if (signature == EndOfCentralDirectory.SIGNATURE) {
eocd.parse(buffer);
eocd.setLocation(new Location(offset + buffer.position() - SIZE, SIZE));
break;
}
if (buffer.position() <= 4) {
break;
}
buffer.position(buffer.position() - Integer.BYTES - 1); // Backtrack 5 bytes.
}
return eocd;
}
@NonNull
public static Location write(
@NonNull ZipWriter writer, @NonNull Location cdLocation, long entriesCount)
throws IOException {
boolean isZip64 = Zip64.needZip64Footer(entriesCount, cdLocation);
short numEntries = isZip64 ? Zip64.SHORT_MAGIC : Ints.longToUshort(entriesCount);
int eocdSize = isZip64 ? Zip64.INT_MAGIC : Ints.longToUint(cdLocation.size());
int eocdOffset = isZip64 ? Zip64.INT_MAGIC : Ints.longToUint(cdLocation.first);
ByteBuffer eocd = ByteBuffer.allocate(SIZE).order(ByteOrder.LITTLE_ENDIAN);
eocd.putInt(SIGNATURE);
eocd.putShort(DISK_NUMBER);
eocd.putShort((short) 0); // cd disk number
eocd.putShort(numEntries);
eocd.putShort(numEntries);
eocd.putInt(eocdSize);
eocd.putInt(eocdOffset);
eocd.putShort((short) 0); // comment size
eocd.rewind();
long position = writer.position();
writer.write(eocd);
return new Location(position, SIZE);
}
}
zipflinger-7.2.2/src/com/android/zipflinger/Entry.java 0000664 0000000 0000000 00000006513 14306644577 0023022 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.nio.charset.StandardCharsets;
public class Entry {
// The location (in file space) of the zip entry which includes the LFH, payload and
// Data descriptor.
private Location location = Location.INVALID;
// The location (in CD space) of this entry in the CD.
private Location cdLocation = Location.INVALID;
// The location (in file space) of the zip entry payload (the actual file data).
private Location payloadLocation = Location.INVALID;
private String name = "";
private int crc;
private long compressedSize;
private long uncompressedSize;
private short compressionFlag;
private short versionMadeBy;
private int externalAttributes;
Entry() {}
public short getCompressionFlag() {
return compressionFlag;
}
public long getCompressedSize() {
return compressedSize;
}
public long getUncompressedSize() {
return uncompressedSize;
}
public String getName() {
return name;
}
public int getCrc() {
return crc;
}
public boolean isDirectory() {
return name.charAt(name.length() - 1) == '/';
}
public boolean isCompressed() {
return compressionFlag != LocalFileHeader.COMPRESSION_NONE;
}
@NonNull
Location getCdLocation() {
return cdLocation;
}
@NonNull
Location getLocation() {
return location;
}
@NonNull
public Location getPayloadLocation() {
return payloadLocation;
}
void setCdLocation(@NonNull Location cdLocation) {
this.cdLocation = cdLocation;
}
void setNameBytes(@NonNull byte[] nameBytes) {
this.name = new String(nameBytes, StandardCharsets.UTF_8);
}
void setCrc(int crc) {
this.crc = crc;
}
void setPayloadLocation(@NonNull Location payloadLocation) {
this.payloadLocation = payloadLocation;
}
void setCompressionFlag(short compressionFlag) {
this.compressionFlag = compressionFlag;
}
void setCompressedSize(long compressedSize) {
this.compressedSize = compressedSize;
}
void setUncompressedSize(long ucompressedSize) {
this.uncompressedSize = ucompressedSize;
}
void setLocation(@NonNull Location location) {
this.location = location;
}
void setVersionMadeBy(short versionMadeByFlag) {
this.versionMadeBy = versionMadeByFlag;
}
void setExternalAttributes(int externalAttributes) {
this.externalAttributes = externalAttributes;
}
short getVersionMadeBy() {
return versionMadeBy;
}
int getExternalAttributes() {
return externalAttributes;
}
}
zipflinger-7.2.2/src/com/android/zipflinger/ExtractionInfo.java 0000664 0000000 0000000 00000002136 14306644577 0024652 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
public class ExtractionInfo {
private final Location location;
private final boolean isCompressed;
public ExtractionInfo(@NonNull Location location, boolean isCompressed) {
this.location = location;
this.isCompressed = isCompressed;
}
@NonNull
public Location getLocation() {
return location;
}
public boolean isCompressed() {
return isCompressed;
}
}
zipflinger-7.2.2/src/com/android/zipflinger/FreeStore.java 0000664 0000000 0000000 00000017377 14306644577 0023631 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
// This works like a memory allocator except it deals with file address space instead of
// memory address space.
class FreeStore {
static final long DEFAULT_ALIGNMENT = 4;
static final long PAGE_ALIGNMENT = 4096;
private Zone head;
// A zone tracks the free file address space. Two consecutive zones are never contiguous which
// mean that upon modification, if two zone "touch" each others, they are merged together into
// a bigger free zone.
//
// Used space is not tracked but inferred from each gap between free zones.
protected static class Zone {
public Zone next;
public Zone prev;
public Location loc;
public Zone() {
this.next = null;
this.prev = null;
}
public void shrinkBy(long amount) {
assert loc.size() > amount;
loc = new Location(loc.first + amount, loc.size() - amount);
// If the zone is empty, remove it from the list.
if (loc.size() == 0) {
prev.next = next;
if (next != null) {
next.prev = prev;
}
}
}
}
FreeStore(@NonNull Map zipEntries) {
// Create an immutable marker of unusable space which make "insert on head" ugly code go away.
head = new Zone();
head.loc = new Location(-1, 1);
// Use zip entries location (used space) to build the free zones list.
List usedLocations = new ArrayList<>();
for (Entry entry : zipEntries.values()) {
usedLocations.add(entry.getLocation());
}
Collections.sort(usedLocations);
Zone prevFreeZone = head;
Location prevUsedLoc = prevFreeZone.loc;
for (Location usedLoc : usedLocations) {
// If there is a gap, mark is as FREE space.
long gap = usedLoc.first - prevUsedLoc.last - 1;
if (gap > 0) {
Zone free = new Zone();
prevFreeZone.next = free;
free.prev = prevFreeZone;
free.loc = new Location(prevUsedLoc.last + 1, gap);
prevFreeZone = free;
}
prevUsedLoc = usedLoc;
}
// Mark everything remaining as a free zone.
Zone remainingZone = new Zone();
remainingZone.prev = prevFreeZone;
remainingZone.next = null;
prevFreeZone.next = remainingZone;
remainingZone.loc =
new Location(prevUsedLoc.last + 1, Long.MAX_VALUE - 1 - prevUsedLoc.last);
}
// Performs unaligned allocation.
@NonNull
Location ualloc(long requestedSize) {
Zone cursor;
for (cursor = head.next; cursor != null; cursor = cursor.next) {
// We are searching for a block big enough to contain:
// - The requested size
// - Post-padding space for potentially needed virtual entry to fill holes.
if (cursor.loc.size() >= requestedSize + LocalFileHeader.VIRTUAL_HEADER_SIZE) {
break;
}
}
if (cursor == null) {
throw new IllegalStateException("Out of file address space.");
}
Location allocated = new Location(cursor.loc.first, requestedSize);
cursor.shrinkBy(requestedSize);
return allocated;
}
// Performs aligned allocation. The offset is necessary because what needs to be aligned is not
// the first byte in the allocation but the first byte in the zip entry payload.
// This method may return more than requested. If it does the extra space is padding that must
// be consumed by an "extra" field.
@NonNull
Location alloc(long requestedSize, long payloadOffset, long alignment) {
Zone cursor;
for (cursor = head.next; cursor != null; cursor = cursor.next) {
long padding = padFor(cursor.loc.first, payloadOffset, alignment);
// We are searching for a block big enough to contain:
// - The requested size
// - Pre-padding space for extra field ALIGNMENT
// - Post-padding space for potentially needed virtual entry to fill holes.
if (cursor.loc.size()
>= requestedSize + padding + LocalFileHeader.VIRTUAL_HEADER_SIZE) {
requestedSize += padding;
break;
}
}
if (cursor == null) {
throw new IllegalStateException("Out of file address space.");
}
Location allocated = new Location(cursor.loc.first, requestedSize);
cursor.shrinkBy(requestedSize);
return allocated;
}
// Mark an area of the file available for allocation. This will merge up to two zones into one
// if they touch each others.
void free(@NonNull Location loc) {
Zone cursor = head.next;
while (cursor != null) {
if (loc.first > cursor.prev.loc.last && loc.last < cursor.loc.first) {
break;
}
cursor = cursor.next;
}
if (cursor == null) {
throw new IllegalStateException("Double free");
}
// Insert a free zone
Zone newFreeZone = new Zone();
newFreeZone.loc = loc;
newFreeZone.prev = cursor.prev;
newFreeZone.next = cursor;
cursor.prev.next = newFreeZone;
cursor.prev = newFreeZone;
cursor = newFreeZone;
// If previous zone is contiguous, merge this zone into previous.
if (cursor.prev.loc.last + 1 == cursor.loc.first && cursor.prev != head) {
Zone prev = cursor.prev;
prev.next = cursor.next;
cursor.next.prev = prev;
prev.loc = new Location(prev.loc.first, prev.loc.size() + cursor.loc.size());
cursor = prev;
}
// If next zone is contiguous, merge this zone into next.
if (cursor.next != null && cursor.next.loc.first - 1 == cursor.loc.last) {
Zone next = cursor.next;
next.prev = cursor.prev;
cursor.prev.next = next;
next.loc = new Location(cursor.loc.first, cursor.loc.size() + next.loc.size());
}
}
@NonNull
Location getLastFreeLocation() {
Zone zone = head.next;
while (zone.next != null) {
zone = zone.next;
}
return zone.loc;
}
@NonNull
List getFreeLocations() {
List locs = new ArrayList<>();
Zone cursor = head.next;
while (cursor != null) {
locs.add(cursor.loc);
cursor = cursor.next;
}
return locs;
}
// How much padding is needed if this address+offset is not aligned (a.k.a: An extra field will
// have to be created in order to fill this space).
static long padFor(long address, long offset, long alignment) {
long pointer = address + offset;
if ((pointer % alignment) == 0) {
return 0;
} else {
return alignment - (pointer % alignment);
}
}
}
zipflinger-7.2.2/src/com/android/zipflinger/FullFileSource.java 0000664 0000000 0000000 00000003670 14306644577 0024605 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.Deflater;
public class FullFileSource extends BytesSource {
public enum Symlink {
FOLLOW,
DO_NOT_FOLLOW
};
public FullFileSource(@NonNull Path file, @NonNull String entryName, int compressionLevel)
throws IOException {
this(file, entryName, compressionLevel, Symlink.FOLLOW);
}
public FullFileSource(
@NonNull Path file,
@NonNull String entryName,
int compressionLevel,
Symlink symlinkPolicy)
throws IOException {
super(entryName);
if (Files.isExecutable(file)) {
externalAttributes |= PERMISSION_EXEC;
}
byte[] bytes;
if (!Files.isSymbolicLink(file) || symlinkPolicy == Symlink.FOLLOW) {
bytes = Files.readAllBytes(file);
} else {
externalAttributes |= PERMISSION_LINK;
compressionLevel = Deflater.NO_COMPRESSION;
Path target = Files.readSymbolicLink(file);
bytes = target.toString().getBytes(StandardCharsets.US_ASCII);
}
build(bytes, bytes.length, compressionLevel);
}
}
zipflinger-7.2.2/src/com/android/zipflinger/Ints.java 0000664 0000000 0000000 00000003340 14306644577 0022631 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
class Ints {
public static final long USHRT_MAX = 65_535L;
public static final long UINT_MAX = 0xFF_FF_FF_FFL;
static long uintToLong(int i) {
return i & 0xFF_FF_FF_FFL;
}
static int ushortToInt(short i) {
return i & 0xFF_FF;
}
static int longToUint(long i) {
if ((i & 0xFF_FF_FF_FF_00_00_00_00L) != 0) {
throw new IllegalStateException("Long cannot fit in uint");
}
return (int) i;
}
static short intToUshort(int i) {
if ((i & 0xFF_FF_00_00) != 0) {
throw new IllegalStateException("Int cannot fit in ushort");
}
return (short) i;
}
static short longToUshort(long i) {
if ((i & 0xFF_FF_FF_FF_FF_FF_00_00L) != 0) {
throw new IllegalStateException("long cannot fit in ushort");
}
return (short) i;
}
public static long ulongToLong(long i) {
if ((i & 0x80_00_00_00_00_00_00_00L) != 0) {
throw new IllegalStateException("ulong cannot fit in long");
}
return i;
}
private Ints() {}
}
zipflinger-7.2.2/src/com/android/zipflinger/LargeFileSource.java 0000664 0000000 0000000 00000013012 14306644577 0024724 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Locale;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
public class LargeFileSource extends Source {
private final Path transferSrc;
private final int compressionLevel;
private static final String TMP_DIR = System.getProperty("java.io.tmpdir");
// Does not load the whole file in memory. If the entry is not compressed, only read it to
// compute the CRC32 and zero-copy src when needed. If compression is requested, uses a tmp
// storage to store the deflated payload then zero-copy it when needed.
public LargeFileSource(
@NonNull Path src,
@Nullable Path tmpStorage,
@NonNull String name,
int compressionLevel)
throws IOException {
super(name);
this.compressionLevel = compressionLevel;
if (tmpStorage == null && compressionLevel != Deflater.NO_COMPRESSION) {
String msg = "Compression without a provided tmp Path is not supported";
throw new IllegalStateException(msg);
}
try (CheckedInputStream in =
new CheckedInputStream(Files.newInputStream(src), new CRC32())) {
if (compressionLevel == Deflater.NO_COMPRESSION) {
buildStored(in);
transferSrc = src;
} else {
buildCompressed(in, compressionLevel, tmpStorage);
transferSrc = tmpStorage;
}
// At this point the input file has been completely read. We can request the crc32.
crc = Ints.longToUint(in.getChecksum().getValue());
}
}
public LargeFileSource(@NonNull Path src, @NonNull String name, int compressionLevel)
throws IOException {
this(src, getTmpStoragePath(src.getFileName().toString()), name, compressionLevel);
}
@NonNull
public static Path getTmpStoragePath(@NonNull String entryName) {
StringBuilder filename = new StringBuilder();
filename.append(Integer.toHexString(entryName.hashCode()));
filename.append("-");
filename.append(Thread.currentThread().getId());
filename.append("-");
filename.append(System.nanoTime());
filename.append(".tmp");
Path tmp = Paths.get(TMP_DIR, filename.toString());
if (Files.exists(tmp)) {
String msg = String.format(Locale.US, "Cannot use path '%s' (exists)", tmp);
throw new IllegalStateException(msg);
}
return tmp;
}
private void buildStored(@NonNull InputStream in) throws IOException {
byte[] buffer = new byte[4096];
long inputSize = 0;
int read;
while ((read = in.read(buffer)) != -1) {
inputSize += read;
}
compressedSize = inputSize;
uncompressedSize = compressedSize;
compressionFlag = LocalFileHeader.COMPRESSION_NONE;
}
private void buildCompressed(@NonNull InputStream in, int compressionLevel, @NonNull Path tmp)
throws IOException {
// Make sure we are not going to overwrite another tmp file.
if (Files.exists(tmp)) {
String msg = String.format("Tmp storage '%s' already exists", tmp.toAbsolutePath());
throw new IllegalStateException(msg);
}
// Pipe the src into the tmp compressed file.
Deflater deflater = Compressor.getDeflater(compressionLevel);
try (DeflaterOutputStream out =
new DeflaterOutputStream(
Files.newOutputStream(tmp, StandardOpenOption.CREATE_NEW), deflater)) {
// Just in case we crash before writeTo is called, attempt to clean up on VM exit.
tmp.toFile().deleteOnExit();
int read;
byte[] buffer = new byte[4096];
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
}
compressedSize = deflater.getBytesWritten();
uncompressedSize = deflater.getBytesRead();
compressionFlag = LocalFileHeader.COMPRESSION_DEFLATE;
}
@Override
public void prepare() throws IOException {}
@Override
public long writeTo(@NonNull ZipWriter writer) throws IOException {
try (FileChannel src = FileChannel.open(transferSrc, StandardOpenOption.READ)) {
writer.transferFrom(src, 0, this.compressedSize);
return this.compressedSize;
} finally {
if (compressionLevel != Deflater.NO_COMPRESSION) {
Files.delete(transferSrc);
}
}
}
}
zipflinger-7.2.2/src/com/android/zipflinger/LocalFileHeader.java 0000664 0000000 0000000 00000013213 14306644577 0024657 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
class LocalFileHeader {
private static final int SIGNATURE = 0x04034b50;
public static final int LOCAL_FILE_HEADER_SIZE = 30;
// Minimum number of bytes needed to create a virtual zip entry (an entry not present in
// the Central Directory with name length = 0 and an extra field containing padding data).
public static final long VIRTUAL_HEADER_SIZE = LOCAL_FILE_HEADER_SIZE;
public static final short COMPRESSION_NONE = 0;
public static final short COMPRESSION_DEFLATE = 8;
static final long VIRTUAL_ENTRY_MAX_SIZE = LOCAL_FILE_HEADER_SIZE + Ints.USHRT_MAX;
static final long OFFSET_TO_NAME = 26;
// Zip64 extra payload must only include uncompressed size and compressed size. It differs
// from the Central Directory Record which also features an uint64_t offset to the LFH.
private static final int ZIP64_PAYLOAD_SIZE = Long.BYTES * 2;
private static final int ZIP64_EXTRA_SIZE = Short.BYTES * 2 + ZIP64_PAYLOAD_SIZE;
private final byte[] nameBytes;
private final short compressionFlag;
private final int crc;
private final long compressedSize;
private final long uncompressedSize;
private final boolean isZip64;
private int padding;
LocalFileHeader(Source source) {
this.nameBytes = source.getNameBytes();
this.compressionFlag = source.getCompressionFlag();
this.crc = source.getCrc();
this.compressedSize = source.getCompressedSize();
this.uncompressedSize = source.getUncompressedSize();
this.isZip64 = compressedSize > Zip64.LONG_MAGIC || uncompressedSize > Zip64.LONG_MAGIC;
this.padding = 0;
}
public static void fillVirtualEntry(@NonNull ByteBuffer virtualEntry) {
int sizeToFill = virtualEntry.capacity();
if (sizeToFill < VIRTUAL_HEADER_SIZE) {
String message = String.format("Not enough space for virtual entry (%d)", sizeToFill);
throw new IllegalStateException(message);
}
virtualEntry.order(ByteOrder.LITTLE_ENDIAN);
virtualEntry.putInt(SIGNATURE);
virtualEntry.putShort((short) 0); // Version needed
virtualEntry.putShort((short) 0); // general purpose flag
virtualEntry.putShort(COMPRESSION_NONE);
virtualEntry.putShort(CentralDirectoryRecord.DEFAULT_TIME);
virtualEntry.putShort(CentralDirectoryRecord.DEFAULT_DATE);
virtualEntry.putInt(0); // CRC-32
virtualEntry.putInt(0); // compressed size
virtualEntry.putInt(0); // uncompressed size
virtualEntry.putShort((short) 0); // file name length
// -2 for the extra length ushort we have to write
virtualEntry.putShort(Ints.intToUshort(virtualEntry.remaining() - 2)); // extra length
virtualEntry.rewind();
}
public void setPadding(int padding) {
if (padding > Ints.USHRT_MAX) {
String err = String.format("Padding cannot be more than %s bytes", Ints.USHRT_MAX);
throw new IllegalStateException(err);
}
this.padding = padding;
}
public void write(@NonNull ZipWriter writer) throws IOException {
ByteBuffer extraField = buildExtraField();
int bytesNeeded = LOCAL_FILE_HEADER_SIZE + nameBytes.length + extraField.capacity();
short versionNeeded = isZip64 ? Zip64.VERSION_NEEDED : 0;
int size = isZip64 ? Zip64.INT_MAGIC : Ints.longToUint(uncompressedSize);
int csize = isZip64 ? Zip64.INT_MAGIC : Ints.longToUint(compressedSize);
ByteBuffer buffer = ByteBuffer.allocate(bytesNeeded).order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt(SIGNATURE);
buffer.putShort(versionNeeded);
buffer.putShort((short) 0); // general purpose flag
buffer.putShort(compressionFlag);
buffer.putShort(CentralDirectoryRecord.DEFAULT_TIME);
buffer.putShort(CentralDirectoryRecord.DEFAULT_DATE);
buffer.putInt(crc);
buffer.putInt(csize); // compressed size
buffer.putInt(size); // size
buffer.putShort(Ints.intToUshort(nameBytes.length));
buffer.putShort(Ints.intToUshort(extraField.capacity())); // Extra size
buffer.put(nameBytes);
buffer.put(extraField);
buffer.rewind();
writer.write(buffer);
}
public long getSize() {
long extraSize = isZip64 ? ZIP64_EXTRA_SIZE : 0;
return LOCAL_FILE_HEADER_SIZE + nameBytes.length + extraSize;
}
@NonNull
private ByteBuffer buildExtraField() {
if (!isZip64) {
return ByteBuffer.allocate(padding);
}
ByteBuffer zip64extra = ByteBuffer.allocate(ZIP64_EXTRA_SIZE + padding);
zip64extra.order(ByteOrder.LITTLE_ENDIAN);
zip64extra.putShort(Zip64.EXTRA_ID);
zip64extra.putShort(Ints.intToUshort(ZIP64_PAYLOAD_SIZE));
zip64extra.putLong(uncompressedSize);
zip64extra.putLong(compressedSize);
zip64extra.rewind();
return zip64extra;
}
}
zipflinger-7.2.2/src/com/android/zipflinger/Location.java 0000664 0000000 0000000 00000003652 14306644577 0023472 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.text.NumberFormat;
public class Location implements Comparable {
public static final Location INVALID = new Location(Long.MAX_VALUE, Long.MAX_VALUE);
public final long first;
public final long last;
public Location(long first, long size) {
this.first = first;
this.last = first + size - 1;
}
public long size() {
return last - first + 1;
}
public boolean isValid() {
return !this.equals(INVALID);
}
@NonNull
@Override
public String toString() {
return "(offset="
+ NumberFormat.getInstance().format(first)
+ ", size="
+ NumberFormat.getInstance().format(size())
+ ")";
}
@Override
public boolean equals(@NonNull Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Location)) {
return false;
}
Location other = (Location) obj;
return first == other.first && last == other.last;
}
@Override
public int hashCode() {
return Long.hashCode(first);
}
@Override
public int compareTo(Location o) {
return Math.toIntExact(this.first - o.first);
}
}
zipflinger-7.2.2/src/com/android/zipflinger/NoCopyByteArrayOutputStream.java 0000664 0000000 0000000 00000002427 14306644577 0027350 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
// A class which contrary to ByteArrayOutputStream allows to peek into the buffer
// without performing a full copy of the content. This stream does not need to be closed.
public class NoCopyByteArrayOutputStream extends ByteArrayOutputStream {
public NoCopyByteArrayOutputStream(int size) {
super(size);
}
@NonNull
public byte[] buf() {
return buf;
}
public int getCount() {
return count;
}
public ByteBuffer getByteBuffer() {
return ByteBuffer.wrap(buf, 0, count);
}
}
zipflinger-7.2.2/src/com/android/zipflinger/PayloadInputStream.java 0000664 0000000 0000000 00000004524 14306644577 0025506 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/*
* A wrapper around a FileChannel providing a bounded view of
* it according to the provided Location.
*
* Does not need to be closed. And does not close the wrapped
* channel either.
*/
public class PayloadInputStream extends InputStream {
private FileChannel channel;
private Location boundaries;
private long position;
public PayloadInputStream(@NonNull FileChannel channel, @NonNull Location location)
throws IOException {
this.channel = channel;
this.boundaries = location;
this.position = location.first;
if (location.first < 0 || location.last >= channel.size()) {
throw new IllegalStateException("Location not within channel boundaries");
}
}
@Override
public int read() throws IOException {
if (position > boundaries.last) {
return -1;
}
byte[] buffer = new byte[1];
read(buffer);
// Convert from [-128, 127] to [0-255] according to InputStream requirements.
return buffer[0] & 0xFF;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (position > boundaries.last) {
return -1;
}
long available = boundaries.last - position + 1;
available = Math.min(available, Integer.MAX_VALUE);
int toRead = Math.min(Math.toIntExact(available), len);
ByteBuffer buffer = ByteBuffer.wrap(b, off, toRead);
int read = channel.read(buffer, position);
position += read;
return read;
}
}
zipflinger-7.2.2/src/com/android/zipflinger/Source.java 0000664 0000000 0000000 00000010262 14306644577 0023155 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public abstract class Source {
private final String name;
private final byte[] nameBytes;
public static final long NO_ALIGNMENT = 0;
private long alignment = NO_ALIGNMENT;
protected long compressedSize;
protected long uncompressedSize;
protected int crc;
protected short compressionFlag;
protected short versionMadeBy;
public static final short MADE_BY_UNIX = 3 << 8;
// For more of these magical values, see zipinfo.c in unzip source code.
// All these values are shifted left 16 bits because this is where they
// are expected in the zip external attribute field.
private static final int TYPE_FREG = 0100000; // Regular File
private static final int TYPE_FLNK = 0120000; // Symbolic link
private static final int UNX_IRUSR = 00400; /* Unix read : owner */
private static final int UNX_IWUSR = 00200; /* Unix write : owner */
private static final int UNX_IXUSR = 00100; /* Unix execute : owner */
private static final int UNX_IRGRP = 00040; /* Unix read : group */
private static final int UNX_IWGRP = 00020; /* Unix write : group */
private static final int UNX_IXGRP = 00010; /* Unix execute : group */
private static final int UNX_IROTH = 00004; /* Unix read : other */
private static final int UNX_IWOTH = 00002; /* Unix write : other */
private static final int UNX_IXOTH = 00001; /* Unix execute : other */
private static final int UNX_IRALL = UNX_IRUSR | UNX_IRGRP | UNX_IROTH;
private static final int UNX_IWALL = UNX_IWUSR | UNX_IWGRP | UNX_IWOTH;
public static final int PERMISSION_USR_RW = (UNX_IRUSR | UNX_IWUSR) << 16;
public static final int PERMISSION_RW = (UNX_IRALL | UNX_IWALL) << 16;
public static final int PERMISSION_EXEC = (UNX_IXUSR | UNX_IXGRP | UNX_IXOTH) << 16;
public static final int PERMISSION_LINK = TYPE_FLNK << 16;
public static final int PERMISSION_DEFAULT = (TYPE_FREG << 16) | PERMISSION_RW;
protected int externalAttributes;
protected Source(@NonNull String name) {
this.name = name;
nameBytes = name.getBytes(StandardCharsets.UTF_8);
versionMadeBy = MADE_BY_UNIX;
externalAttributes = PERMISSION_DEFAULT;
}
@NonNull
public String getName() {
return name;
}
@NonNull
byte[] getNameBytes() {
return nameBytes;
}
boolean isAligned() {
return alignment != NO_ALIGNMENT;
}
public void align(long alignment) {
this.alignment = alignment;
}
long getAlignment() {
return alignment;
}
int getCrc() {
return crc;
}
public long getCompressedSize() {
return compressedSize;
}
public long getUncompressedSize() {
return uncompressedSize;
}
short getCompressionFlag() {
return compressionFlag;
}
public short getVersionMadeBy() {
return versionMadeBy;
}
public int getExternalAttributes() {
return externalAttributes;
}
public void setExternalAttributes(int externalAttributes) {
this.externalAttributes = externalAttributes;
}
// Guaranteed to be called before writeTo. After this method has been called, every fields
// in an entry must be known (csize, size, crc32, and compressionFlag).
public abstract void prepare() throws IOException;
// Return the number of bytes written.
public abstract long writeTo(@NonNull ZipWriter writer) throws IOException;
}
zipflinger-7.2.2/src/com/android/zipflinger/Sources.java 0000664 0000000 0000000 00000003555 14306644577 0023347 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
public class Sources {
// The general maximum amount of memory (in bytes) a Source should hold onto.
public static final int LARGE_LIMIT = 100_000_000;
public static Source from(File file, @NonNull String name, int compressionLevel)
throws IOException {
return from(file.toPath(), name, compressionLevel);
}
public static Source from(Path path, @NonNull String name, int compressionLevel)
throws IOException {
if (Files.size(path) > LARGE_LIMIT) {
return new LargeFileSource(path, name, compressionLevel);
} else {
return new BytesSource(path, name, compressionLevel);
}
}
public static Source from(InputStream in, String name, int compressionLevel)
throws IOException {
return from(in, name, compressionLevel, LARGE_LIMIT);
}
public static Source from(InputStream in, String name, int compressionLevel, int largeLimit)
throws IOException {
return new StreamSource(in, name, compressionLevel, largeLimit);
}
}
zipflinger-7.2.2/src/com/android/zipflinger/StableArchive.java 0000664 0000000 0000000 00000004337 14306644577 0024437 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
public class StableArchive implements Archive {
private final Archive archive;
private final ArrayList sources;
private final ArrayList zipSources;
private final ArrayList deletedEntries;
public StableArchive(Archive archive) {
this.archive = archive;
sources = new ArrayList<>();
zipSources = new ArrayList<>();
deletedEntries = new ArrayList<>();
}
@Override
public void add(@NonNull Source source) {
sources.add(source);
}
@Override
public void add(@NonNull ZipSource sources) {
zipSources.add(sources);
}
@Override
public void delete(@NonNull String name) {
deletedEntries.add(name);
}
@Override
public void close() throws IOException {
sources.sort(Comparator.comparing(Source::getName));
zipSources.sort(Comparator.comparing(ZipSource::getName));
for (ZipSource zipSource : zipSources) {
zipSource.getSelectedEntries().sort(Comparator.comparing(Source::getName));
}
deletedEntries.sort(Comparator.naturalOrder());
try (Archive arch = archive) {
for (String toDelete : deletedEntries) {
arch.delete(toDelete);
}
for (Source source : sources) {
arch.add(source);
}
for (ZipSource zipSource : zipSources) {
arch.add(zipSource);
}
}
}
}
zipflinger-7.2.2/src/com/android/zipflinger/StreamSource.java 0000664 0000000 0000000 00000010245 14306644577 0024332 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
class StreamSource extends Source {
private static final int TMP_BUFFER_SIZE = 4096;
@NonNull
private final NoCopyByteArrayOutputStream buffer;
@Nullable
private Path tmpStore = null;
private long tmpStoreSize = 0;
// Drain the InputStream until it is empty. Keep up to largeLimit bytes
// in memory and use a backing storage if the InputStream is bigger.
public StreamSource(@NonNull InputStream src, @NonNull String name, int compressionLevel, int largeLimit)
throws IOException {
super(name);
buffer = new NoCopyByteArrayOutputStream(TMP_BUFFER_SIZE);
long bytesRead = 0;
try (CheckedInputStream in = new CheckedInputStream(src, new CRC32());
OutputStream out = getOutput(compressionLevel)) {
int read;
byte[] bytes = new byte[TMP_BUFFER_SIZE];
while ((read = in.read(bytes)) != -1) {
out.write(bytes, 0, read);
bytesRead += read;
if (buffer.getCount() > largeLimit) {
flushBuffer();
}
}
crc = Ints.longToUint(in.getChecksum().getValue());
} finally {
src.close();
}
compressedSize = buffer.getCount() + tmpStoreSize;
uncompressedSize = bytesRead;
if (compressionLevel == Deflater.NO_COMPRESSION) {
compressionFlag = LocalFileHeader.COMPRESSION_NONE;
} else {
compressionFlag = LocalFileHeader.COMPRESSION_DEFLATE;
}
}
private void flushBuffer() throws IOException {
FileChannel fc;
if (tmpStore == null) {
tmpStore = LargeFileSource.getTmpStoragePath(getName());
fc = FileChannel.open(tmpStore, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
// Just in case we crash before writeTo is called, attempt to clean up on VM exit.
tmpStore.toFile().deleteOnExit();
} else {
fc = FileChannel.open(tmpStore, StandardOpenOption.APPEND);
}
try(FileChannel channel = fc) {
ByteBuffer b = buffer.getByteBuffer();
tmpStoreSize += b.remaining();
channel.write(b);
buffer.reset();
}
}
private OutputStream getOutput(int compressionLevel) {
if (compressionLevel == Deflater.NO_COMPRESSION) {
return buffer;
} else {
Deflater deflater = Compressor.getDeflater(compressionLevel);
return new DeflaterOutputStream(buffer, deflater);
}
}
@Override
public void prepare() throws IOException {}
@Override
public long writeTo(@NonNull ZipWriter writer) throws IOException {
if (tmpStore != null) {
try (FileChannel src = FileChannel.open(tmpStore, StandardOpenOption.READ)) {
writer.transferFrom(src, 0, tmpStoreSize);
} finally {
Files.delete(tmpStore);
}
}
writer.write(buffer.getByteBuffer());
return buffer.getCount() + tmpStoreSize;
}
}
zipflinger-7.2.2/src/com/android/zipflinger/SynchronizedArchive.java 0000664 0000000 0000000 00000002777 14306644577 0025712 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.io.IOException;
public class SynchronizedArchive implements Archive {
private final Archive archive;
public SynchronizedArchive(Archive archive) {
this.archive = archive;
}
@Override
public void add(@NonNull Source source) throws IOException {
synchronized (archive) {
archive.add(source);
}
}
@Override
public void add(@NonNull ZipSource sources) throws IOException {
synchronized (archive) {
archive.add(sources);
}
}
@Override
public void delete(@NonNull String name) throws IOException {
synchronized (archive) {
archive.delete(name);
}
}
@Override
public void close() throws IOException {
synchronized (archive) {
archive.close();
}
}
}
zipflinger-7.2.2/src/com/android/zipflinger/Zip64.java 0000664 0000000 0000000 00000002341 14306644577 0022630 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
public class Zip64 {
static final short EXTRA_ID = 0x0001;
static final long LONG_MAGIC = 0xFF_FF_FF_FFL;
static final int INT_MAGIC = (int) LONG_MAGIC;
static final int SHORT_MAGIC = (short) LONG_MAGIC;
static final short VERSION_NEEDED = 0x2D;
public static boolean needZip64Footer(long numEntries, Location cdLocation) {
return numEntries > Ints.USHRT_MAX
|| cdLocation.first > Ints.UINT_MAX
|| cdLocation.size() > Ints.UINT_MAX;
}
public enum Policy {
ALLOW,
FORBID
};
private Zip64() {}
}
zipflinger-7.2.2/src/com/android/zipflinger/Zip64Eocd.java 0000664 0000000 0000000 00000006667 14306644577 0023442 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
public class Zip64Eocd {
private static final int SIGNATURE = 0x06064b50;
static final int SIZE = 56;
private long numEntries;
private Location cdLocation;
public Zip64Eocd(long numEntries, @NonNull Location cdLocation) {
this.numEntries = numEntries;
this.cdLocation = cdLocation;
}
private Zip64Eocd() {
this(0, Location.INVALID);
}
@NonNull
public Location write(@NonNull ZipWriter writer) throws IOException {
ByteBuffer eocd = ByteBuffer.allocate(SIZE).order(ByteOrder.LITTLE_ENDIAN);
eocd.putInt(SIGNATURE);
eocd.putLong(SIZE - 12); // Peculiar specs mandate not to include 12 bytes already written.
eocd.putShort((short) 0); // Version made by
eocd.putShort(Zip64.VERSION_NEEDED); // Version needed to extract
eocd.putInt(0); // disk #
eocd.putInt(0); // total # of disks
eocd.putLong(numEntries); // # entries in cd on this disk
eocd.putLong(numEntries); // total # entries in cd
eocd.putLong(cdLocation.size()); // CD offset.
eocd.putLong(cdLocation.first); // size of CD.
eocd.rewind();
long position = writer.position();
writer.write(eocd);
return new Location(position, SIZE);
}
@NonNull
Location getCdLocation() {
return cdLocation;
}
@NonNull
static Zip64Eocd parse(@NonNull FileChannel channel, long eocdOffset) throws IOException {
Zip64Eocd zip64Eocd = new Zip64Eocd();
long fileSize = channel.size();
if (eocdOffset < 0 || eocdOffset + SIZE > fileSize) {
return zip64Eocd;
}
ByteBuffer buffer = ByteBuffer.allocate(SIZE).order(ByteOrder.LITTLE_ENDIAN);
channel.read(buffer, eocdOffset);
buffer.rewind();
int signature = buffer.getInt(); // signature
if (signature != SIGNATURE) {
return zip64Eocd;
}
// Skip uninteresting fields
buffer.position(buffer.position() + 28);
// eocd.getLong(); 8 // size of zip64EOCD
// eocd.getShort(); 2 // Version made by
// eocd.getShort(); 2 // Version needed to extract
// eocd.getInt(); 4 // disk #
// eocd.getInt(); 4 // total # of disks
// eocd.getLong(); 8 // # entries in cd on this disk
long numEntries = buffer.getLong(); // total # entries in cd
long size = Ints.ulongToLong(buffer.getLong()); // size of CD.
long offset = Ints.ulongToLong(buffer.getLong()); // CD offset.
zip64Eocd.numEntries = numEntries;
zip64Eocd.cdLocation = new Location(offset, size);
return zip64Eocd;
}
}
zipflinger-7.2.2/src/com/android/zipflinger/Zip64Locator.java 0000664 0000000 0000000 00000005775 14306644577 0024172 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
public class Zip64Locator {
private static final int SIGNATURE = 0x07064b50;
public static final int SIZE = 20;
static final int TOTAL_NUMBER_DISK = EndOfCentralDirectory.DISK_NUMBER + 1;
private Location location;
private long offsetToEOCD64;
private Zip64Locator() {
location = Location.INVALID;
offsetToEOCD64 = 0;
}
@NonNull
public Location getLocation() {
return location;
}
public long getOffsetToEOCD64() {
return offsetToEOCD64;
}
public static Location write(@NonNull ZipWriter writer, @NonNull Location eocdLocation)
throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(SIZE).order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt(SIGNATURE);
buffer.putInt(0); // CD disk number
buffer.putLong(eocdLocation.first); // offset
buffer.putInt(TOTAL_NUMBER_DISK);
buffer.rewind();
long position = writer.position();
writer.write(buffer);
return new Location(position, SIZE);
}
@NonNull
static Zip64Locator find(@NonNull FileChannel channel, @NonNull EndOfCentralDirectory eocd)
throws IOException {
Zip64Locator locator = new Zip64Locator();
Location locatorLocation = new Location(eocd.getLocation().first - SIZE, SIZE);
long fileSize = channel.size();
if (locatorLocation.last >= fileSize) {
return locator;
}
if (locatorLocation.first < 0) {
return locator;
}
ByteBuffer locatorBuffer = ByteBuffer.allocate(SIZE).order(ByteOrder.LITTLE_ENDIAN);
channel.read(locatorBuffer, locatorLocation.first);
locatorBuffer.rewind();
if (locator.parse(locatorBuffer)) {
locator.location = locatorLocation;
}
return locator;
}
private boolean parse(@NonNull ByteBuffer buffer) {
int signature = buffer.getInt();
if (signature != SIGNATURE) {
return false;
}
buffer.position(buffer.position() + 4); // skip CD disk number
offsetToEOCD64 = Ints.ulongToLong(buffer.getLong());
// Don't read the rest, this is not needed
return true;
}
}
zipflinger-7.2.2/src/com/android/zipflinger/ZipArchive.java 0000664 0000000 0000000 00000031163 14306644577 0023764 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class ZipArchive implements Archive {
private final FreeStore freestore;
private boolean closed;
private final Path file;
private final CentralDirectory cd;
private final ZipWriter writer;
private final ZipReader reader;
private final Zip64.Policy policy;
private ZipInfo zipInfo;
private boolean modified;
public ZipArchive(@NonNull Path file) throws IOException {
this(file, Zip64.Policy.ALLOW);
}
/**
* The object used to manipulate a zip archive.
*
* @param file the file object
*/
public ZipArchive(@NonNull Path file, Zip64.Policy policy) throws IOException {
this.file = file;
this.policy = policy;
if (Files.exists(file)) {
ZipMap map = ZipMap.from(file, true, policy);
zipInfo = new ZipInfo(map.getPayloadLocation(), map.getCdLoc(), map.getEocdLoc());
cd = map.getCentralDirectory();
freestore = new FreeStore(map.getEntries());
} else {
zipInfo = new ZipInfo();
Map entries = new LinkedHashMap<>();
cd = new CentralDirectory(ByteBuffer.allocate(0), entries);
freestore = new FreeStore(entries);
}
writer = new ZipWriter(file);
reader = new ZipReader(file);
closed = false;
modified = false;
}
/** @deprecated Use ZipArchive(Path) instead. */
@Deprecated
public ZipArchive(@NonNull File file) throws IOException {
this(file.toPath());
}
/** @deprecated Use {@link #ZipArchive(Path, Zip64.Policy)} instead. */
@Deprecated
public ZipArchive(@NonNull File file, Zip64.Policy policy) throws IOException {
this(file.toPath(), policy);
}
/**
* Returns the list of zip entries found in the archive. Note that these are the entries found
* in the central directory via bottom-up parsing, not all entries present in the payload as a
* top-down parser may return.
*
* @param file the zip archive to list.
* @return the list of entries in the archive, parsed bottom-up (via the Central Directory).
*/
@NonNull
public static Map listEntries(@NonNull Path file) throws IOException {
return ZipMap.from(file, false).getEntries();
}
/** @deprecated Use {@link #listEntries(Path)} instead. */
@Deprecated
public static Map listEntries(@NonNull File file) throws IOException {
return listEntries(file.toPath());
}
@NonNull
public List listEntries() {
return cd.listEntries();
}
@Nullable
public ByteBuffer getContent(@NonNull String name) throws IOException {
ExtractionInfo extractInfo = cd.getExtractionInfo(name);
if (extractInfo == null) {
return null;
}
Location loc = extractInfo.getLocation();
ByteBuffer payloadByteBuffer = ByteBuffer.allocate(Math.toIntExact(loc.size()));
reader.read(payloadByteBuffer, loc.first);
if (extractInfo.isCompressed()) {
return Compressor.inflate(payloadByteBuffer.array());
} else {
return payloadByteBuffer;
}
}
@Nullable
public InputStream getInputStream(@NonNull String name) throws IOException {
ExtractionInfo extractInfo = cd.getExtractionInfo(name);
if (extractInfo == null) {
return null;
}
InputStream in = new PayloadInputStream(reader.getChannel(), extractInfo.getLocation());
if (extractInfo.isCompressed()) {
return Compressor.wrapToInflate(in);
}
return in;
}
/** See Archive.add documentation */
@Override
public void add(@NonNull Source source) throws IOException {
if (closed) {
throw new IllegalStateException(
String.format("Cannot add source to closed archive %s", file));
}
writeSource(source);
}
/** See Archive.add documentation */
@Override
public void add(@NonNull ZipSource sources) throws IOException {
if (closed) {
throw new IllegalStateException(
String.format("Cannot add zip source to closed archive %s", file));
}
try {
sources.open();
for (Source source : sources.getSelectedEntries()) {
writeSource(source);
}
} finally {
sources.close();
}
}
/** See Archive.delete documentation */
@Override
public void delete(@NonNull String name) {
if (closed) {
throw new IllegalStateException(
String.format("Cannot delete '%s' from closed archive %s", name, file));
}
Location loc = cd.delete(name);
if (loc.isValid()) {
freestore.free(loc);
modified = true;
}
}
/**
* Carry all write operations to the storage system to reflect the delete/add operations
* requested via add/delete methods.
*/
// TODO: Zip64 -> Add boolean allowZip64
@Override
public void close() throws IOException {
closeWithInfo();
}
@NonNull
public ZipInfo closeWithInfo() throws IOException {
if (closed) {
throw new IllegalStateException("Attempt to close a closed archive");
}
closed = true;
try (ZipWriter w = writer;
ZipReader r = reader) {
writeArchive(w);
}
return zipInfo;
}
@NonNull
public Path getPath() {
return file;
}
public boolean isClosed() {
return closed;
}
private void writeArchive(@NonNull ZipWriter writer) throws IOException {
// There is no need to fill space and write footers if an already existing archive
// has not been modified.
if (zipInfo.eocd.isValid() && !modified) {
return;
}
// Fill all empty space with virtual entry (not the last one since it represent all of
// the unused file space.
List freeLocations = freestore.getFreeLocations();
for (int i = 0; i < freeLocations.size() - 1; i++) {
fillFreeLocation(freeLocations.get(i), writer);
}
// Write the Central Directory
Location lastFreeLocation = freestore.getLastFreeLocation();
long cdStart = lastFreeLocation.first;
writer.position(cdStart);
cd.write(writer);
Location cdLocation = new Location(cdStart, writer.position() - cdStart);
long numEntries = cd.getNumEntries();
// Write zip64 EOCD and Locator (only if needed)
writeZip64Footers(writer, cdLocation, numEntries);
// Write EOCD
Location eocdLocation = EndOfCentralDirectory.write(writer, cdLocation, numEntries);
writer.truncate(writer.position());
// Build and return location map
Location payLoadLocation = new Location(0, cdStart);
zipInfo = new ZipInfo(payLoadLocation, cdLocation, eocdLocation);
}
private void writeZip64Footers(
@NonNull ZipWriter writer, @NonNull Location cdLocation, long numEntries)
throws IOException {
if (!Zip64.needZip64Footer(numEntries, cdLocation)) {
return;
}
if (policy == Zip64.Policy.FORBID) {
String message =
String.format(
"Zip64 required but forbidden (#entries=%d, cd=%s)",
numEntries, cdLocation);
throw new IllegalStateException(message);
}
Zip64Eocd eocd = new Zip64Eocd(numEntries, cdLocation);
Location eocdLocation = eocd.write(writer);
Zip64Locator.write(writer, eocdLocation);
}
// Fill archive holes with virtual entries. Use extra field to fill as much as possible.
private static void fillFreeLocation(@NonNull Location location, @NonNull ZipWriter writer)
throws IOException {
long spaceToFill = location.size();
if (spaceToFill < LocalFileHeader.VIRTUAL_HEADER_SIZE) {
// There is not enough space to create a virtual entry here. The FreeStore
// never creates such gaps so it was already in the zip. Leave it as it is.
return;
}
while (spaceToFill > 0) {
long entrySize;
if (spaceToFill <= LocalFileHeader.VIRTUAL_ENTRY_MAX_SIZE) {
// Consume all the remaining space.
entrySize = spaceToFill;
} else {
// Consume as much as possible while leaving enough for the next LFH entry.
entrySize = Ints.USHRT_MAX;
}
int size = Math.toIntExact(entrySize);
ByteBuffer virtualEntry = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
LocalFileHeader.fillVirtualEntry(virtualEntry);
writer.write(virtualEntry, location.first + location.size() - spaceToFill);
spaceToFill -= virtualEntry.capacity();
}
}
private void writeSource(@NonNull Source source) throws IOException {
modified = true;
validateName(source);
source.prepare();
// Calculate the size we need (header + payload)
LocalFileHeader lfh = new LocalFileHeader(source);
long headerSize = lfh.getSize();
long bytesNeeded = headerSize + source.getCompressedSize();
// Allocate file space
Location loc;
if (source.isAligned()) {
loc = freestore.alloc(bytesNeeded, headerSize, source.getAlignment());
lfh.setPadding(Math.toIntExact(loc.size() - bytesNeeded));
} else {
loc = freestore.ualloc(bytesNeeded);
}
writer.position(loc.first);
lfh.write(writer);
// Write payload
long payloadStart = writer.position();
long payloadSize = source.writeTo(writer);
Location payloadLocation = new Location(payloadStart, payloadSize);
// Update Central Directory record
CentralDirectoryRecord cdRecord = new CentralDirectoryRecord(source, loc, payloadLocation);
cd.add(source.getName(), cdRecord);
checkPolicy(source, loc, payloadLocation);
}
private void checkPolicy(
@NonNull Source source, @NonNull Location cdloc, @NonNull Location payloadLoc) {
if (policy == Zip64.Policy.ALLOW) {
return;
}
if (source.getUncompressedSize() >= Zip64.LONG_MAGIC
|| source.getCompressedSize() >= Zip64.LONG_MAGIC
|| cdloc.first >= Zip64.LONG_MAGIC
|| payloadLoc.first >= Zip64.LONG_MAGIC) {
String message =
String.format(
"Zip64 forbidden but required in entry %s size=%d, csize=%d, cdloc=%s, loc=%s",
source.getName(),
source.getUncompressedSize(),
source.getCompressedSize(),
cdloc,
payloadLoc);
throw new IllegalStateException(message);
}
}
private void validateName(@NonNull Source source) {
byte[] nameBytes = source.getNameBytes();
String name = source.getName();
if (nameBytes.length > Ints.USHRT_MAX) {
throw new IllegalStateException(
String.format("Name '%s' is more than %d bytes", name, Ints.USHRT_MAX));
}
if (cd.contains(name)) {
String template = "Zip file '%s' already contains entry '%s', cannot overwrite";
String msg = String.format(template, file.toAbsolutePath().toString(), name);
throw new IllegalStateException(msg);
}
}
}
zipflinger-7.2.2/src/com/android/zipflinger/ZipInfo.java 0000664 0000000 0000000 00000002004 14306644577 0023266 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
public class ZipInfo {
public final Location payload;
public final Location cd;
public final Location eocd;
public ZipInfo() {
this(Location.INVALID, Location.INVALID, Location.INVALID);
}
public ZipInfo(Location payload, Location cd, Location eocd) {
this.payload = payload;
this.cd = cd;
this.eocd = eocd;
}
}
zipflinger-7.2.2/src/com/android/zipflinger/ZipMap.java 0000664 0000000 0000000 00000036653 14306644577 0023131 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.LinkedHashMap;
import java.util.Map;
public class ZipMap {
private final Map entries = new LinkedHashMap<>();
private CentralDirectory cd = null;
// To build an accurate location of entries in the zip payload, data descriptors must be read.
// This is not useful if an user only wants a list of entries in the zip but it is mandatory
// if zip entries are deleted/added.
private final boolean accountDataDescriptors;
private final Path zipFile;
private long fileSize;
private Location payloadLocation;
private Location cdLocation;
private Location eocdLocation;
static final String LFH_LENGTH_ERROR =
"The provided zip (%s) is invalid. Entry '%s' name field is %d bytes"
+ " in the Central Directory but %d in the Local File Header";
private ZipMap(@NonNull Path zipFile, boolean accountDataDescriptors) {
this.zipFile = zipFile;
this.accountDataDescriptors = accountDataDescriptors;
}
@NonNull
public static ZipMap from(@NonNull Path zipFile) throws IOException {
return from(zipFile, false);
}
/** @deprecated Use {@link #from(Path)} instead. */
@Deprecated
@NonNull
public static ZipMap from(@NonNull File zipFile) throws IOException {
return from(zipFile.toPath(), false);
}
@NonNull
public static ZipMap from(@NonNull Path zipFile, boolean accountDataDescriptors)
throws IOException {
return from(zipFile, accountDataDescriptors, Zip64.Policy.ALLOW);
}
@NonNull
public static ZipMap from(
@NonNull Path zipFile, boolean accountDataDescriptors, Zip64.Policy policy)
throws IOException {
ZipMap map = new ZipMap(zipFile, accountDataDescriptors);
map.parse(policy);
return map;
}
@NonNull
public Location getPayloadLocation() {
return payloadLocation;
}
@NonNull
public Location getCdLoc() {
return cdLocation;
}
@NonNull
public Location getEocdLoc() {
return eocdLocation;
}
private void parse(Zip64.Policy policy) throws IOException {
try (FileChannel channel = FileChannel.open(zipFile, StandardOpenOption.READ)) {
fileSize = channel.size();
EndOfCentralDirectory eocd = EndOfCentralDirectory.find(channel);
if (!eocd.getLocation().isValid()) {
throw new IllegalStateException(
String.format("Could not find EOCD in '%s'", zipFile));
}
eocdLocation = eocd.getLocation();
cdLocation = eocd.getCdLocation();
// Check if this is a zip64 archive
Zip64Locator locator = Zip64Locator.find(channel, eocd);
if (locator.getLocation().isValid()) {
if (policy == Zip64.Policy.FORBID) {
String message =
String.format("Cannot parse forbidden zip64 archive %s", zipFile);
throw new IllegalStateException(message);
}
Zip64Eocd zip64EOCD = Zip64Eocd.parse(channel, locator.getOffsetToEOCD64());
cdLocation = zip64EOCD.getCdLocation();
if (!cdLocation.isValid()) {
String message = String.format("Zip64Locator led to bad EOCD64 in %s", zipFile);
throw new IllegalStateException(message);
}
}
if (!cdLocation.isValid()) {
throw new IllegalStateException(
String.format("Could not find CD in '%s'", zipFile));
}
parseCentralDirectory(channel, cdLocation, policy);
payloadLocation = new Location(0, cdLocation.first);
}
}
private void parseCentralDirectory(
@NonNull FileChannel channel, @NonNull Location location, Zip64.Policy policy)
throws IOException {
if (location.size() > Integer.MAX_VALUE) {
throw new IllegalStateException("CD larger than 2GiB not supported");
}
int size = Math.toIntExact(location.size());
ByteBuffer buf = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
channel.read(buf, location.first);
buf.rewind();
while (buf.remaining() >= 4 && buf.getInt() == CentralDirectoryRecord.SIGNATURE) {
Entry entry = new Entry();
parseCentralDirectoryRecord(buf, channel, entry);
if (!entry.getName().isEmpty()) {
entries.put(entry.getName(), entry);
}
checkPolicy(entry, policy);
}
cd = new CentralDirectory(buf, entries);
sanityCheck(location);
}
private static void checkPolicy(@NonNull Entry entry, Zip64.Policy policy) {
if (policy == Zip64.Policy.ALLOW) {
return;
}
if (entry.getUncompressedSize() > Zip64.LONG_MAGIC
|| entry.getCompressedSize() > Zip64.LONG_MAGIC
|| entry.getLocation().first > Zip64.LONG_MAGIC) {
String message =
String.format(
"Entry %s infringes forbidden zip64 policy (size=%d, csize=%d, loc=%s)",
entry.getName(),
entry.getUncompressedSize(),
entry.getCompressedSize(),
entry.getLocation());
throw new IllegalStateException(message);
}
}
private void sanityCheck(Location cdLocation) {
//Sanity check that:
// - All payload locations are within the file (and not in the CD).
for (Entry e : entries.values()) {
Location loc = e.getLocation();
if (loc.first < 0) {
throw new IllegalStateException("Invalid first loc '" + e.getName() + "' " + loc);
}
if (loc.last >= fileSize) {
throw new IllegalStateException(
fileSize + "Invalid last loc '" + e.getName() + "' " + loc);
}
Location cdLoc = e.getCdLocation();
if (cdLoc.first < 0) {
throw new IllegalStateException(
"Invalid first cdloc '" + e.getName() + "' " + cdLoc);
}
long cdSize = cdLocation.size();
if (cdLoc.last >= cdSize) {
throw new IllegalStateException(
cdSize + "Invalid last loc '" + e.getName() + "' " + cdLoc);
}
}
}
@NonNull
public Map getEntries() {
return entries;
}
@NonNull
CentralDirectory getCentralDirectory() {
return cd;
}
public void parseCentralDirectoryRecord(
@NonNull ByteBuffer buf, @NonNull FileChannel channel, @NonNull Entry entry)
throws IOException {
long cdEntryStart = (long) buf.position() - 4;
entry.setVersionMadeBy(buf.getShort());
buf.getShort(); // versionNeededToExtract
short flags = buf.getShort();
short compressionFlag = buf.getShort();
entry.setCompressionFlag(compressionFlag);
buf.position(buf.position() + 4);
//short modTime = buf.getShort();
//short modDate = buf.getShort();
int crc = buf.getInt();
entry.setCrc(crc);
entry.setCompressedSize(Ints.uintToLong(buf.getInt()));
entry.setUncompressedSize(Ints.uintToLong(buf.getInt()));
int pathLength = Ints.ushortToInt(buf.getShort());
int extraLength = Ints.ushortToInt(buf.getShort());
int commentLength = Ints.ushortToInt(buf.getShort());
buf.position(buf.position() + 4);
// short diskNumber = buf.getShort();
// short intAttributes = buf.getShort();
entry.setExternalAttributes(buf.getInt());
entry.setLocation(new Location(Ints.uintToLong(buf.getInt()), 0));
parseName(buf, pathLength, entry);
// Process extra field. If the entry is zip64, this may change size, csize, and offset.
if (extraLength > 0) {
int position = buf.position();
int limit = buf.limit();
buf.limit(position + extraLength);
parseExtra(buf.slice(), entry);
buf.limit(limit);
buf.position(position + extraLength);
}
// Skip comment field
buf.position(buf.position() + commentLength);
// Retrieve the local header extra size since there are no guarantee it is the same as the
// central directory size.
// Semi-paranoid mode: Also check that the local name size is the same as the cd name size.
ByteBuffer localFieldBuffer =
readLocalFields(
entry.getLocation().first + LocalFileHeader.OFFSET_TO_NAME, entry, channel);
int localPathLength = Ints.ushortToInt(localFieldBuffer.getShort());
int localExtraLength = Ints.ushortToInt(localFieldBuffer.getShort());
if (pathLength != localPathLength) {
String path = this.zipFile.toAbsolutePath().toString();
String entryName = entry.getName();
String msg = LFH_LENGTH_ERROR;
String message = String.format(msg, path, entryName, localPathLength, pathLength);
throw new IllegalStateException(message);
}
// At this point we have everything we need to calculate payload location.
boolean isCompressed = compressionFlag != 0;
long payloadSize = isCompressed ? entry.getCompressedSize() : entry.getUncompressedSize();
long start = entry.getLocation().first;
long end =
start
+ LocalFileHeader.LOCAL_FILE_HEADER_SIZE
+ pathLength
+ localExtraLength
+ payloadSize;
entry.setLocation(new Location(start, end - start));
Location payloadLocation =
new Location(
start
+ LocalFileHeader.LOCAL_FILE_HEADER_SIZE
+ pathLength
+ localExtraLength,
payloadSize);
entry.setPayloadLocation(payloadLocation);
// At this point we have everything we need to calculate CD location.
long cdEntrySize =
(long) CentralDirectoryRecord.SIZE + pathLength + extraLength + commentLength;
entry.setCdLocation(new Location(cdEntryStart, cdEntrySize));
// Parse data descriptor to adjust crc, compressed size, and uncompressed size.
boolean hasDataDescriptor =
((flags & CentralDirectoryRecord.DATA_DESCRIPTOR_FLAG)
== CentralDirectoryRecord.DATA_DESCRIPTOR_FLAG);
if (hasDataDescriptor) {
if (accountDataDescriptors) {
// This is expensive. Fortunately ZIP archive rarely use DD nowadays.
channel.position(end);
parseDataDescriptor(channel, entry);
} else {
entry.setLocation(Location.INVALID);
}
}
}
private static void parseExtra(@NonNull ByteBuffer buf, @NonNull Entry entry) {
buf.order(ByteOrder.LITTLE_ENDIAN);
while (buf.remaining() >= 4) {
short id = buf.getShort();
int size = Ints.ushortToInt(buf.getShort());
if (id == Zip64.EXTRA_ID) {
parseZip64Extra(buf, entry);
}
if (buf.remaining() >= size) {
buf.position(buf.position() + size);
}
}
}
private static void parseZip64Extra(@NonNull ByteBuffer buf, @NonNull Entry entry) {
if (entry.getUncompressedSize() == Zip64.LONG_MAGIC) {
if (buf.remaining() < 8) {
throw new IllegalStateException("Bad zip64 extra for entry " + entry.getName());
}
entry.setUncompressedSize(Ints.ulongToLong(buf.getLong()));
}
if (entry.getCompressedSize() == Zip64.LONG_MAGIC) {
if (buf.remaining() < 8) {
throw new IllegalStateException("Bad zip64 extra for entry " + entry.getName());
}
entry.setCompressedSize(Ints.ulongToLong(buf.getLong()));
}
if (entry.getLocation().first == Zip64.LONG_MAGIC) {
if (buf.remaining() < 8) {
throw new IllegalStateException("Bad zip64 extra for entry " + entry.getName());
}
long offset = Ints.ulongToLong(buf.getLong());
entry.setLocation(new Location(offset, 0));
}
}
private ByteBuffer readLocalFields(long offset, Entry entry, FileChannel channel)
throws IOException {
// The extra field is not guaranteed to be the same in the LFH and in the CDH. In practice there is
// often padding space that is not in the CD. We need to read the LFH.
ByteBuffer localFieldsBuffer = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
if (offset < 0 || (offset + 4) > fileSize) {
throw new IllegalStateException(
"Entry :" + entry.getName() + " invalid offset (" + offset + ")");
}
channel.read(localFieldsBuffer, offset);
localFieldsBuffer.rewind();
return localFieldsBuffer;
}
private static void parseName(@NonNull ByteBuffer buf, int length, @NonNull Entry entry) {
byte[] pathBytes = new byte[length];
buf.get(pathBytes);
entry.setNameBytes(pathBytes);
}
private static void parseDataDescriptor(@NonNull FileChannel channel, @NonNull Entry entry)
throws IOException {
// If zip entries have data descriptor, we need to go an fetch every single entry to look if
// the "optional" marker is there. Adjust zip entry area accordingly.
ByteBuffer dataDescriptorBuffer = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN);
channel.read(dataDescriptorBuffer);
dataDescriptorBuffer.rewind();
int dataDescriptorLength = 12;
if (dataDescriptorBuffer.getInt() == CentralDirectoryRecord.DATA_DESCRIPTOR_SIGNATURE) {
dataDescriptorLength += 4;
}
Location adjustedLocation =
new Location(
entry.getLocation().first,
entry.getLocation().size() + dataDescriptorLength);
entry.setLocation(adjustedLocation);
}
@NonNull
public Path getPath() {
return zipFile;
}
/** @deprecated Use {@link #getPath()} instead. */
@Deprecated
@NonNull
public File getFile() {
return zipFile.toFile();
}
}
zipflinger-7.2.2/src/com/android/zipflinger/ZipReader.java 0000664 0000000 0000000 00000003435 14306644577 0023606 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import java.io.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class ZipReader implements Closeable {
private final Path file;
private FileChannel channel;
private boolean isOpen;
ZipReader(Path file) {
this.file = file;
isOpen = false;
}
@Override
public void close() throws IOException {
if (!isOpen) {
return;
}
channel.close();
}
void read(ByteBuffer byteBuffer, long offset) throws IOException {
ensureOpen();
channel.read(byteBuffer, offset);
byteBuffer.rewind();
}
void ensureOpen() throws IOException {
if (isOpen) {
return;
}
this.channel = FileChannel.open(file, StandardOpenOption.READ);
if (!channel.isOpen()) {
throw new IllegalStateException("Unable to open Channel to " + file.toAbsolutePath());
}
isOpen = true;
}
FileChannel getChannel() throws IOException {
ensureOpen();
return channel;
}
}
zipflinger-7.2.2/src/com/android/zipflinger/ZipRepo.java 0000664 0000000 0000000 00000006325 14306644577 0023312 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Map;
public class ZipRepo implements Closeable {
private final ZipMap zipMap;
private final FileChannel channel;
private final Path file;
public ZipRepo(@NonNull String filePath) throws IOException {
this(ZipMap.from(Paths.get(filePath), false, Zip64.Policy.ALLOW));
}
public ZipRepo(@NonNull Path path) throws IOException {
this(ZipMap.from(path, false, Zip64.Policy.ALLOW));
}
public ZipRepo(@NonNull ZipMap zipMap) throws IOException {
this.zipMap = zipMap;
this.channel = FileChannel.open(zipMap.getPath(), StandardOpenOption.READ);
this.file = zipMap.getPath();
}
@NonNull
public Map getEntries() {
return zipMap.getEntries();
}
@NonNull
ZipMap getZipMap() {
return zipMap;
}
@Override
public void close() throws IOException {
channel.close();
}
@NonNull
private Entry getEntry(@NonNull String entryName) {
Entry entry = zipMap.getEntries().get(entryName);
if (entry == null) {
String msg = String.format("No entry '%s' in file '%s'", entryName, file);
throw new IllegalArgumentException(msg);
}
return entry;
}
// Is it the caller's responsibility to close() the returned InputStream.
@NonNull
public InputStream getInputStream(@NonNull String entryName) throws IOException {
Entry entry = getEntry(entryName);
Location payloadLocation = entry.getPayloadLocation();
InputStream inputStream = new PayloadInputStream(channel, payloadLocation);
if (!entry.isCompressed()) {
return inputStream;
}
return Compressor.wrapToInflate(inputStream);
}
@NonNull
public ByteBuffer getContent(@NonNull String entryName) throws IOException {
Entry entry = getEntry(entryName);
Location payloadLocation = entry.getPayloadLocation();
ByteBuffer payloadByteBuffer = ByteBuffer.allocate(Math.toIntExact(payloadLocation.size()));
channel.read(payloadByteBuffer, payloadLocation.first);
payloadByteBuffer.rewind();
if (entry.isCompressed()) {
return Compressor.inflate(payloadByteBuffer.array());
} else {
return payloadByteBuffer;
}
}
}
zipflinger-7.2.2/src/com/android/zipflinger/ZipSource.java 0000664 0000000 0000000 00000010743 14306644577 0023644 0 ustar 00root root 0000000 0000000 /*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.zipflinger;
import com.android.annotations.NonNull;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.zip.Deflater;
public class ZipSource {
public static final int COMPRESSION_NO_CHANGE = -2;
private FileChannel channel;
private ZipMap map;
private final List selectedEntries = new ArrayList<>();
public ZipSource(ZipMap map) {
this.map = map;
}
public ZipSource(@NonNull Path file) throws IOException {
this(ZipMap.from(file, false));
}
@NonNull
public void select(@NonNull String entryName, @NonNull String newName) {
select(entryName, newName, COMPRESSION_NO_CHANGE, Source.NO_ALIGNMENT);
}
/**
* Select an entry to be copied to the archive managed by zipflinger.
*
*
An entry will remain unchanged and zero-copy will happen when: - compression level is
* COMPRESSION_NO_CHANGE. - compression level is 1-9 and the entry is already compressed. -
* compression level is Deflater.NO_COMPRESSION and the entry is already uncompressed.
*
*