public class ZFile
extends java.lang.Object
implements java.io.Closeable
ZFile provides the main interface for interacting with zip files. A ZFile
can be created on a new file or in an existing file. Once created, files can be added or removed
from the zip file.
Changes in the zip file are always deferred. Any change requested is made in memory and
written to disk only when update() or close() is invoked.
Zip files are open initially in read-only mode and will switch to read-write when needed. This
is done automatically. Because modifications to the file are done in-memory, the zip file can
be manipulated when closed. When invoking update() or close() the zip file
will be reopen and changes will be written. However, the zip file cannot be modified outside
the control of ZFile. So, if a ZFile is closed, modified outside and then a file
is added or removed from the zip file, when reopening the zip file, ZFile will detect
the outside modification and will fail.
In memory manipulation means that files added to the zip file are kept in memory until written
to disk. This provides much faster operation and allows better zip file allocation (see below).
It may, however, increase the memory footprint of the application. When adding large files, if
memory consumption is a concern, a call to update() will actually write the file to
disk and discard the memory buffer. Information about allocation can be obtained from a
ByteTracker that can be given to the file on creation.
ZFile keeps track of allocation inside of the zip file. If a file is deleted, its
space is marked as freed and will be reused for an added file if it fits in the space.
Allocation of files to empty areas is done using a best fit algorithm. When adding a
file, if it doesn't fit in any free area, the zip file will be extended.
ZFile provides a fast way to merge data from another zip file
(see mergeFrom(ZFile, Predicate)) avoiding recompression and copying of equal files.
When merging, patterns of files may be provided that are ignored. This allows handling special
files in the merging process, such as files in META-INF.
When adding files to the zip file, unless files are explicitly required to be stored, files
will be deflated. However, deflating will not occur if the deflated file is larger then the
stored file, e.g. if compression would yield a bigger file. See Compressor for
details on how compression works.
Because ZFile was designed to be used in a build system and not as general-purpose
zip utility, it is very strict (and unforgiving) about the zip format and unsupported features.
ZFile supports alignment. Alignment means that file data (not entries -- the
local header must be discounted) must start at offsets that are multiple of a number -- the
alignment. Alignment is defined by an alignment rules (AlignmentRule in the
ZFileOptions object used to create the ZFile.
When a file is added to the zip, the alignment rules will be checked and alignment will be honored when positioning the file in the zip. This means that unused spaces in the zip may be generated as a result. However, alignment of existing entries will not be changed.
Entries can be realigned individually (see StoredEntry.realign() or the full zip file
may be realigned (see realign()). When realigning the full zip entries that are already
aligned will not be affected.
Because realignment may cause files to move in the zip, realignment is done in-memory meaning
that files that need to change location will moved to memory and will only be flushed when
either update() or close() are called.
Alignment only applies to filed that are forced to be uncompressed. This is because alignment is used to allow mapping files in the archive directly into memory and compressing defeats the purpose of alignment.
Manipulating zip files with ZFile may yield zip files with empty spaces between files.
This happens in two situations: (1) if alignment is required, files may be shifted to conform to
the request alignment leaving an empty space before the previous file, and (2) if a file is
removed or replaced with a file that does not fit the space it was in. By default, ZFile
does not do any special processing in these situations. Files are indexed by their offsets from
the central directory and empty spaces can exist in the zip file.
However, it is possible to tell ZFile to use the extra field in the local header
to do cover the empty spaces. This is done by setting
ZFileOptions.setCoverEmptySpaceUsingExtraField(boolean) to true. This has the
advantage of leaving no gaps between entries in the zip, as required by some tools like Oracle's
{code jar} tool. However, setting this option will destroy the contents of the file's extra
field.
Activating ZFileOptions.setCoverEmptySpaceUsingExtraField(boolean) may lead to
virtual files being added to the zip file. Since extra field is limited to 64k, it is not
possible to cover any space bigger than that using the extra field. In those cases, virtual
files are added to the file. A virtual file is a file that exists in the actual zip data,
but is not referenced from the central directory. A zip-compliant utility should ignore these
files. However, zip utilities that expect the zip to be a stream, such as Oracle's jar, will
find these files instead of considering the zip to be corrupt.
ZFile support sorting zip files. Sorting (done through the sortZipContents()
method) is a process by which all files are re-read into memory, if not already in memory,
removed from the zip and re-added in alphabetical order, respecting alignment rules. So, in
general, file b will come after file a unless file a is subject to
alignment that forces an empty space before that can be occupied by b. Sorting can be
used to minimize the changes between two zips.
Sorting in ZFile can be done manually or automatically. Manual sorting is done by
invoking sortZipContents(). Automatic sorting is done by setting the
ZFileOptions.getAutoSortFiles() option when creating the ZFile. Automatic
sorting invokes sortZipContents() immediately when doing an update() after
all extensions have processed the ZFileExtension.beforeUpdate(). This has the guarantee
that files added by extensions will be sorted, something that does not happen if the invocation
is sequential, i.e., sortZipContents() called before update(). The
drawback of automatic sorting is that sorting will happen every time update() is
called and the file is dirty having a possible penalty in performance.
To allow whole-apk signing, the ZFile allows the central directory location to be
offset by a fixed amount. This amount can be set using the setExtraDirectoryOffset(long)
method. Setting a non-zero value will add extra (unused) space in the zip file before the
central directory. This value can be changed at any time and it will force the central directory
rewritten when the file is updated or closed.
ZFile provides an extension mechanism to allow objects to register with the file
and be notified when changes to the file happen. This should be used
to add extra features to the zip file while providing strong decoupling. See
ZFileExtension, addZFileExtension(ZFileExtension) and
removeZFileExtension(ZFileExtension).
This class is not thread-safe. Neither are any of the classes associated with it in this package, except when otherwise noticed.
| Modifier and Type | Field and Description |
|---|---|
static char |
SEPARATOR
The file separator in paths in the zip file.
|
| Constructor and Description |
|---|
ZFile(java.io.File file)
Creates a new zip file.
|
ZFile(java.io.File file,
ZFileOptions options)
Creates a new zip file.
|
ZFile(java.io.File file,
ZFileOptions options,
boolean readOnly)
Creates a new zip file.
|
| Modifier and Type | Method and Description |
|---|---|
void |
add(java.lang.String name,
java.io.InputStream stream)
|
void |
add(java.lang.String name,
java.io.InputStream stream,
boolean mayCompress)
Adds a file to the archive.
|
void |
addAllRecursively(java.io.File file)
Adds all files and directories recursively.
|
void |
addAllRecursively(java.io.File file,
java.util.function.Function<? super java.io.File,java.lang.Boolean> mayCompress)
Adds all files and directories recursively.
|
void |
addZFileExtension(ZFileExtension extension)
Adds an extension to this zip file.
|
boolean |
areTimestampsIgnored()
Obtains whether this
ZFile is ignoring timestamps. |
void |
close()
Updates the file and closes it.
|
void |
directFullyRead(long offset,
byte[] data)
Reads exactly
data.length bytes of data, failing if it was not possible to read all
the requested data. |
void |
directFullyRead(long offset,
java.nio.ByteBuffer dest)
Reads exactly
dest.remaining() bytes of data, failing if it was not possible to read
all the requested data. |
java.io.InputStream |
directOpen(long start,
long end)
Opens a portion of the zip for reading.
|
int |
directRead(long offset,
byte[] data)
Same as
directRead(offset, data, 0, data.length). |
int |
directRead(long offset,
byte[] data,
int start,
int count)
Directly reads data from the zip file.
|
int |
directRead(long offset,
java.nio.ByteBuffer dest)
Directly reads data from the zip file.
|
long |
directSize()
Returns the current size (in bytes) of the underlying file.
|
void |
directWrite(long offset,
byte[] data)
Same as
directWrite(offset, data, 0, data.length). |
void |
directWrite(long offset,
byte[] data,
int start,
int count)
Directly writes data in the zip file.
|
java.util.Set<StoredEntry> |
entries()
Obtains all entries in the file.
|
void |
finishAllBackgroundTasks()
Wait for any background tasks to finish and report any errors.
|
StoredEntry |
get(java.lang.String path)
Obtains an entry at a given path in the zip.
|
byte[] |
getCentralDirectoryBytes()
Obtains the byte array representation of the central directory.
|
long |
getCentralDirectoryOffset()
Obtains the offset at which the central directory exists, or at which it will be written
if the zip file were to be flushed immediately.
|
long |
getCentralDirectorySize()
Obtains the size of the central directory, if the central directory is written in the zip
file.
|
byte[] |
getEocdBytes()
Obtains the byte array representation of the EOCD.
|
byte[] |
getEocdComment()
Obtains the comment in the EOCD.
|
long |
getEocdOffset()
Obtains the offset of the EOCD record, if the EOCD has been written to the file.
|
long |
getEocdSize()
Obtains the size of the EOCD record, if the EOCD has been written to the file.
|
long |
getExtraDirectoryOffset()
Obtains the extra offset for the central directory.
|
java.io.File |
getFile()
Obtains the filesystem path to the zip file.
|
boolean |
hasPendingChangesWithWait()
Are there in-memory changes that have not been written to the zip file?
|
void |
mergeFrom(ZFile src,
java.util.function.Predicate<java.lang.String> ignoreFilter)
Adds all files from another zip file, maintaining their compression.
|
void |
openReadOnly()
If the zip file is closed, opens it in read-only mode.
|
boolean |
realign()
Realigns all entries in the zip.
|
void |
removeZFileExtension(ZFileExtension extension)
Removes an extension from this zip file.
|
void |
setEocdComment(byte[] comment)
Sets the comment in the EOCD.
|
void |
setExtraDirectoryOffset(long offset)
Sets an extra offset for the central directory.
|
void |
sortZipContents()
Sorts all files in the zip.
|
void |
touch()
|
void |
update()
Updates the file writing new entries and removing deleted entries.
|
public static final char SEPARATOR
public ZFile(@Nonnull
java.io.File file)
throws java.io.IOException
ZFile will contain an empty structure. However, an (empty) zip file will
be created if either update() or close() are used. If a zip file exists,
it will be parsed and read.file - the zip filejava.io.IOException - some file exists but could not be readpublic ZFile(@Nonnull
java.io.File file,
@Nonnull
ZFileOptions options)
throws java.io.IOException
ZFile will contain an empty structure. However, an (empty) zip file will
be created if either update() or close() are used. If a zip file exists,
it will be parsed and read.file - the zip fileoptions - configuration optionsjava.io.IOException - some file exists but could not be readpublic ZFile(@Nonnull
java.io.File file,
@Nonnull
ZFileOptions options,
boolean readOnly)
throws java.io.IOException
ZFile will contain an empty structure. However, an (empty) zip file will
be created if either update() or close() are used. If a zip file exists,
it will be parsed and read.file - the zip fileoptions - configuration optionsreadOnly - should the file be open in read-only mode? If true then the file must
exist and no methods can be invoked that could potentially change the filejava.io.IOException - some file exists but could not be read@Nonnull public java.util.Set<StoredEntry> entries()
@Nullable public StoredEntry get(@Nonnull java.lang.String path)
path - the pathnull if none exists@Nonnull
public java.io.InputStream directOpen(long start,
long end)
throws java.io.IOException
start - the index within the zip file to start readingend - the index within the zip file to end reading (the actual byte pointed by
end will not be read)java.io.IOException - failed to open the zip filepublic void update()
throws java.io.IOException
java.io.IOException - failed to update the file; this exception may have been thrown by
the compressor but only reported herepublic void close()
throws java.io.IOException
close in interface java.io.Closeableclose in interface java.lang.AutoCloseablejava.io.IOException@Nonnull
public byte[] getCentralDirectoryBytes()
throws java.io.IOException
java.io.IOException - failed to compute the central directory byte representation@Nonnull
public byte[] getEocdBytes()
throws java.io.IOException
java.io.IOException - failed to obtain the byte representation of the EOCDpublic void openReadOnly()
throws java.io.IOException
directRead(long, byte[]), then this
method needs to be called.java.io.IOException - failed to open the filepublic void add(@Nonnull
java.lang.String name,
@Nonnull
java.io.InputStream stream)
throws java.io.IOException
name - the file name (i.e., path); paths should be defined using slashes
and the name should not end in slashstream - the source for the file's datajava.io.IOException - failed to read the source datajava.lang.IllegalStateException - if the file is in read-only modepublic void add(@Nonnull
java.lang.String name,
@Nonnull
java.io.InputStream stream,
boolean mayCompress)
throws java.io.IOException
Adding the file will not update the archive immediately. Updating will only happen
when the update() method is invoked.
Adding a file with the same name as an existing file will replace that file in the archive.
name - the file name (i.e., path); paths should be defined using slashes
and the name should not end in slashstream - the source for the file's datamayCompress - can the file be compressed? This flag will be ignored if the alignment
rules force the file to be aligned, in which case the file will not be compressed.java.io.IOException - failed to read the source datajava.lang.IllegalStateException - if the file is in read-only modepublic void mergeFrom(@Nonnull
ZFile src,
@Nonnull
java.util.function.Predicate<java.lang.String> ignoreFilter)
throws java.io.IOException
This method will not perform any changes in itself, it will only update in-memory data
structures. To actually write the zip file, invoke either update() or
close().
src - the source archiveignoreFilter - predicate that, if true, identifies files in src that
should be ignored by merging; merging will behave as if these files were not therejava.io.IOException - failed to read from src or write on the outputjava.lang.IllegalStateException - if the file is in read-only modepublic void touch()
update()
or close() are invoked.java.lang.IllegalStateException - if the file is in read-only modepublic void finishAllBackgroundTasks()
throws java.io.IOException
add(String, InputStream, boolean), update() and close().
However, if required for some purposes, e.g., ensuring all notifications have been
done to extensions, then this method may be called. It will wait for all background tasks
to complete.java.io.IOException - some background work failedpublic boolean realign()
throws java.io.IOException
StoredEntry.realign()
for all entries in the zip file.ZFile may refer to StoredEntrys that are no longer validjava.io.IOException - failed to realign the zip; some entries in the zip may have been lost
due to the I/O errorjava.lang.IllegalStateException - if the file is in read-only modepublic void addZFileExtension(@Nonnull
ZFileExtension extension)
extension - the listener to addjava.lang.IllegalStateException - if the file is in read-only modepublic void removeZFileExtension(@Nonnull
ZFileExtension extension)
extension - the listener to removejava.lang.IllegalStateException - if the file is in read-only modepublic void directWrite(long offset,
@Nonnull
byte[] data,
int start,
int count)
throws java.io.IOException
offset - the offset at which data should be writtendata - the data to write, may be an empty arraystart - start offset in data where data to write is locatedcount - number of bytes of data to writejava.io.IOException - failed to write the datajava.lang.IllegalStateException - if the file is in read-only modepublic void directWrite(long offset,
@Nonnull
byte[] data)
throws java.io.IOException
directWrite(offset, data, 0, data.length).offset - the offset at which data should be writtendata - the data to write, may be an empty arrayjava.io.IOException - failed to write the datajava.lang.IllegalStateException - if the file is in read-only modepublic long directSize()
throws java.io.IOException
java.io.IOException - if an I/O error occurspublic int directRead(long offset,
@Nonnull
byte[] data,
int start,
int count)
throws java.io.IOException
offset - the offset at which data should be writtendata - the array where read data should be storedstart - start position in the array where to write data tocount - how many bytes of data can be written-1 if there are no more bytes
to be readjava.io.IOException - failed to write the datapublic int directRead(long offset,
@Nonnull
java.nio.ByteBuffer dest)
throws java.io.IOException
offset - the offset from which data should be readdest - the output buffer to fill with data from the offset.-1 if there are no more bytes
to be readjava.io.IOException - failed to write the datapublic int directRead(long offset,
@Nonnull
byte[] data)
throws java.io.IOException
directRead(offset, data, 0, data.length).offset - the offset at which data should be readdata - receives the read data, may be an empty arrayjava.io.IOException - failed to read the datapublic void directFullyRead(long offset,
@Nonnull
byte[] data)
throws java.io.IOException
data.length bytes of data, failing if it was not possible to read all
the requested data.offset - the offset at which to start readingdata - the array that receives the data readjava.io.IOException - failed to read some data or there is not enough data to readpublic void directFullyRead(long offset,
@Nonnull
java.nio.ByteBuffer dest)
throws java.io.IOException
dest.remaining() bytes of data, failing if it was not possible to read
all the requested data.offset - the offset at which to start readingdest - the output buffer to fill with datajava.io.IOException - failed to read some data or there is not enough data to readpublic void addAllRecursively(@Nonnull
java.io.File file)
throws java.io.IOException
Equivalent to calling addAllRecursively(File, Function) using a function that
always returns true
file - a file or directory; if it is a directory, all files and directories will be
added recursivelyjava.io.IOException - failed to some (or all ) of the filesjava.lang.IllegalStateException - if the file is in read-only modepublic void addAllRecursively(@Nonnull
java.io.File file,
@Nonnull
java.util.function.Function<? super java.io.File,java.lang.Boolean> mayCompress)
throws java.io.IOException
file - a file or directory; if it is a directory, all files and directories will be
added recursivelymayCompress - a function that decides whether files may be compressedjava.io.IOException - failed to some (or all ) of the filesjava.lang.IllegalStateException - if the file is in read-only modepublic long getCentralDirectoryOffset()
public long getCentralDirectorySize()
-1 if the central directory has not
been computedpublic long getEocdOffset()
-1 if none exists yetpublic long getEocdSize()
-1 it none exists yet@Nonnull public byte[] getEocdComment()
public void setEocdComment(@Nonnull
byte[] comment)
comment - the new comment; no conversion is done, these exact bytes will be placed in
the EOCD commentjava.lang.IllegalStateException - if file is in read-only modepublic void setExtraDirectoryOffset(long offset)
offset - the offset or 0 to write the central directory at its current locationjava.lang.IllegalStateException - if file is in read-only modepublic long getExtraDirectoryOffset()
0 if no offset is setpublic boolean areTimestampsIgnored()
ZFile is ignoring timestamps.public void sortZipContents()
throws java.io.IOException
update() to force the entries to be
written to disk.java.io.IOException - failed to load or move a file in the zipjava.lang.IllegalStateException - if file is in read-only mode@Nonnull public java.io.File getFile()
public boolean hasPendingChangesWithWait()
throws java.io.IOException
Waits for all pending processing which may make changes.
java.io.IOException