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)) {

Reply via email to