COMPRESS-391: Allow alignment on zip content
Project: http://git-wip-us.apache.org/repos/asf/commons-compress/repo Commit: http://git-wip-us.apache.org/repos/asf/commons-compress/commit/bd64dc7a Tree: http://git-wip-us.apache.org/repos/asf/commons-compress/tree/bd64dc7a Diff: http://git-wip-us.apache.org/repos/asf/commons-compress/diff/bd64dc7a Branch: refs/heads/master Commit: bd64dc7abee36984e830c740588990739b71cc93 Parents: ca7ea93 Author: Zbynek Vyskovsky <kvr...@gmail.com> Authored: Thu May 4 20:44:44 2017 -0700 Committer: Stefan Bodewig <bode...@apache.org> Committed: Thu May 11 20:04:34 2017 +0200 ---------------------------------------------------------------------- .../compress/archivers/zip/ZipArchiveEntry.java | 24 +++++++ .../archivers/zip/ZipArchiveOutputStream.java | 39 +++++++++-- .../compress/archivers/zip/ZipFileTest.java | 71 ++++++++++++++++++++ 3 files changed, 130 insertions(+), 4 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/commons-compress/blob/bd64dc7a/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveEntry.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveEntry.java b/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveEntry.java index f769cf5..b626ea6 100644 --- a/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveEntry.java +++ b/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveEntry.java @@ -85,6 +85,7 @@ public class ZipArchiveEntry extends java.util.zip.ZipEntry private int platform = PLATFORM_FAT; private int rawFlag; private long externalAttributes = 0; + private int alignment = 0; private ZipExtraField[] extraFields; private UnparseableExtraFieldData unparseableExtra = null; private String name = null; @@ -323,6 +324,29 @@ public class ZipArchiveEntry extends java.util.zip.ZipEntry } /** + * Gets currently configured alignment. + * + * @return + * alignment for this entry. + */ + protected int getAlignment() { + return this.alignment; + } + + /** + * Sets alignment for this entry. + * + * @param alignment + * requested alignment, 0 for default. + */ + public void setAlignment(int alignment) { + if ((alignment & (alignment - 1)) != 0) { + throw new IllegalArgumentException("Invalid value for alignment, must be power of two: " + alignment); + } + this.alignment = alignment; + } + + /** * Replaces all currently attached extra fields with the new array. * @param fields an array of extra fields */ http://git-wip-us.apache.org/repos/asf/commons-compress/blob/bd64dc7a/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveOutputStream.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveOutputStream.java b/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveOutputStream.java index e3066b0..72e3d5b 100644 --- a/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveOutputStream.java +++ b/src/main/java/org/apache/commons/compress/archivers/zip/ZipArchiveOutputStream.java @@ -142,6 +142,17 @@ public class ZipArchiveOutputStream extends ArchiveOutputStream { @Deprecated public static final int EFS_FLAG = GeneralPurposeBit.UFT8_NAMES_FLAG; + /** + * Size of the extra field header (id + length). + */ + public static final int EXTRAFIELD_HEADER_SIZE = 4; + + /** + * Extra field id used for padding (there is no special value documented, + * therefore USHORT_MAX seems to be good choice). + */ + public static final int EXTRAFIELD_PADDING_ID = 0xffff; + private static final byte[] EMPTY = new byte[0]; /** @@ -1026,8 +1037,8 @@ public class ZipArchiveOutputStream extends ArchiveOutputStream { addUnicodeExtraFields(ze, encodable, name); } - final byte[] localHeader = createLocalFileHeader(ze, name, encodable, phased); final long localHeaderStart = streamCompressor.getTotalBytesWritten(); + final byte[] localHeader = createLocalFileHeader(ze, name, encodable, phased, localHeaderStart); offsets.put(ze, localHeaderStart); entry.localDataStart = localHeaderStart + LFH_CRC_OFFSET; // At crc offset writeCounted(localHeader); @@ -1036,10 +1047,16 @@ public class ZipArchiveOutputStream extends ArchiveOutputStream { private byte[] createLocalFileHeader(final ZipArchiveEntry ze, final ByteBuffer name, final boolean encodable, - final boolean phased) { + final boolean phased, long archiveOffset) throws IOException { final byte[] extra = ze.getLocalFileDataExtra(); final int nameLen = name.limit() - name.position(); - final int len= LFH_FILENAME_OFFSET + nameLen + extra.length; + int len= LFH_FILENAME_OFFSET + nameLen + extra.length; + int padding = 0; + int alignment = ze.getAlignment(); + if (alignment > 1 && ((archiveOffset + len) & (alignment - 1)) != 0) { + padding = (int) ((-archiveOffset - len - EXTRAFIELD_HEADER_SIZE) & (alignment - 1)); + len += EXTRAFIELD_HEADER_SIZE+padding; + } final byte[] buf = new byte[len]; System.arraycopy(LFH_SIG, 0, buf, LFH_SIG_OFFSET, WORD); @@ -1091,13 +1108,27 @@ public class ZipArchiveOutputStream extends ArchiveOutputStream { // file name length putShort(nameLen, buf, LFH_FILENAME_LENGTH_OFFSET); + int totalExtra = extra.length + (padding > 0 ? padding + EXTRAFIELD_HEADER_SIZE : 0); + if (totalExtra > 0xffff) { + throw new IOException("Too much data for extra fields and padding"+ + ", extra="+extra.length+ + ", padding="+padding); + } // extra field length - putShort(extra.length, buf, LFH_EXTRA_LENGTH_OFFSET); + putShort(totalExtra, buf, LFH_EXTRA_LENGTH_OFFSET); // file name System.arraycopy( name.array(), name.arrayOffset(), buf, LFH_FILENAME_OFFSET, nameLen); + // extra fields System.arraycopy(extra, 0, buf, LFH_FILENAME_OFFSET + nameLen, extra.length); + + // padding + if (padding > 0) { + putShort(EXTRAFIELD_PADDING_ID, buf, LFH_FILENAME_OFFSET + nameLen + extra.length); + putShort(padding, buf, LFH_FILENAME_OFFSET + nameLen + extra.length + 2); + } + return buf; } http://git-wip-us.apache.org/repos/asf/commons-compress/blob/bd64dc7a/src/test/java/org/apache/commons/compress/archivers/zip/ZipFileTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/commons/compress/archivers/zip/ZipFileTest.java b/src/test/java/org/apache/commons/compress/archivers/zip/ZipFileTest.java index 8455bd7..5b7b900 100644 --- a/src/test/java/org/apache/commons/compress/archivers/zip/ZipFileTest.java +++ b/src/test/java/org/apache/commons/compress/archivers/zip/ZipFileTest.java @@ -28,6 +28,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -460,6 +461,76 @@ public class ZipFileTest { } } + /** + * Test entries alignment. + */ + @Test + public void testEntryAlignment() throws Exception { + SeekableInMemoryByteChannel zipContent = new SeekableInMemoryByteChannel(); + try (ZipArchiveOutputStream zipOutput = new ZipArchiveOutputStream(zipContent)) { + ZipArchiveEntry inflatedEntry = new ZipArchiveEntry("inflated.txt"); + inflatedEntry.setMethod(ZipEntry.DEFLATED); + inflatedEntry.setAlignment(1024); + zipOutput.putArchiveEntry(inflatedEntry); + zipOutput.write("Hello Deflated\n".getBytes(Charset.forName("UTF-8"))); + zipOutput.closeArchiveEntry(); + + ZipArchiveEntry storedEntry = new ZipArchiveEntry("stored.txt"); + storedEntry.setMethod(ZipEntry.STORED); + storedEntry.setAlignment(1024); + zipOutput.putArchiveEntry(storedEntry); + zipOutput.write("Hello Stored\n".getBytes(Charset.forName("UTF-8"))); + zipOutput.closeArchiveEntry(); + + } + + try (ZipFile zf = new ZipFile(new SeekableInMemoryByteChannel( + Arrays.copyOfRange(zipContent.array(), 0, (int)zipContent.size()) + ))) { + ZipArchiveEntry inflatedEntry = zf.getEntry("inflated.txt"); + assertNotEquals(-1L, inflatedEntry.getCompressedSize()); + assertNotEquals(-1L, inflatedEntry.getSize()); + assertEquals(0L, inflatedEntry.getDataOffset()%1024); + try (InputStream stream = zf.getInputStream(inflatedEntry)) { + Assert.assertEquals("Hello Deflated\n", + new String(IOUtils.toByteArray(stream), Charset.forName("UTF-8"))); + } + ZipArchiveEntry storedEntry = zf.getEntry("stored.txt"); + assertNotEquals(-1L, storedEntry.getCompressedSize()); + assertNotEquals(-1L, storedEntry.getSize()); + assertEquals(0L, inflatedEntry.getDataOffset()%1024); + try (InputStream stream = zf.getInputStream(storedEntry)) { + Assert.assertEquals("Hello Stored\n", + new String(IOUtils.toByteArray(stream), Charset.forName("UTF-8"))); + } + } + } + + /** + * Test too big alignment, resulting into exceeding extra field limit. + */ + @Test(expected = IOException.class) + public void testEntryAlignmentExceed() throws Exception { + SeekableInMemoryByteChannel zipContent = new SeekableInMemoryByteChannel(); + try (ZipArchiveOutputStream zipOutput = new ZipArchiveOutputStream(zipContent)) { + ZipArchiveEntry inflatedEntry = new ZipArchiveEntry("inflated.txt"); + inflatedEntry.setMethod(ZipEntry.STORED); + inflatedEntry.setAlignment(0x20000); + zipOutput.putArchiveEntry(inflatedEntry); + zipOutput.write("Hello Stored\n".getBytes(Charset.forName("UTF-8"))); + zipOutput.closeArchiveEntry(); + } + } + + /** + * Test non power of 2 alignment. + */ + @Test(expected = IllegalArgumentException.class) + public void testInvalidAlignment() throws Exception { + ZipArchiveEntry entry = new ZipArchiveEntry("dummy"); + entry.setAlignment(3); + } + private void assertAllReadMethods(byte[] expected, ZipFile zipFile, ZipArchiveEntry entry) { // simple IOUtil read try (InputStream stream = zf.getInputStream(entry)) {