diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/OpcOutputStream.java b/src/ooxml/java/org/apache/poi/xssf/streaming/OpcOutputStream.java new file mode 100644 index 0000000000..5cbf536fb0 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/OpcOutputStream.java @@ -0,0 +1,141 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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 org.apache.poi.xssf.streaming; + +import org.apache.poi.xssf.streaming.Zip64Impl.Entry; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.*; + +/** + * ZIP64 OutputStream implementation compatible with MS Excel. + * Drop in replacement for `java.util.ZipOutputStream`. + * + * For more information see https://github.com/rzymek/opczip + * + * @author Krzysztof Rzymkowski + */ +class OpcOutputStream extends DeflaterOutputStream { + + private final Zip64Impl spec; + private final List entries = new ArrayList<>(); + private final CRC32 crc = new CRC32(); + private Entry current; + private int written = 0; + private boolean finished = false; + + /** + * Creates ZIP64 output stream + * + * @param out target stream to write compressed data to + */ + public OpcOutputStream(OutputStream out) { + super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true)); + this.spec = new Zip64Impl(out); + } + + /** + * @see Deflater#setLevel(int) + */ + public void setLevel(int level) { + super.def.setLevel(level); + } + + /** + * @see ZipOutputStream#putNextEntry(ZipEntry) + */ + public void putNextEntry(String name) throws IOException { + if (current != null) { + closeEntry(); + } + current = new Entry(name); + current.offset = written; + written += spec.writeLFH(current); + entries.add(current); + } + + /** + * @see ZipOutputStream#closeEntry() + */ + public void closeEntry() throws IOException { + if (current == null) { + throw new IllegalStateException("not current zip current"); + } + def.finish(); + while (!def.finished()) { + deflate(); + } + + current.size = def.getBytesRead(); + current.compressedSize = (int) def.getBytesWritten(); + current.crc = crc.getValue(); + + written += current.compressedSize; + written += spec.writeDAT(current); + current = null; + def.reset(); + crc.reset(); + } + + + /** + * @see ZipOutputStream#finish() + */ + @Override + public void finish() throws IOException { + if(finished){ + return; + } + if(current != null) { + closeEntry(); + } + int offset = written; + for (Entry entry : entries) { + written += spec.writeCEN(entry); + } + written += spec.writeEND(entries.size(), offset, written - offset); + finished = true; + } + + /** + * @see ZipOutputStream#write(byte[], int, int) + */ + @Override + public synchronized void write(byte[] b, int off, int len) throws IOException { + if (off < 0 || len < 0 || off > b.length - len) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + super.write(b, off, len); + crc.update(b, off, len); + } + + /** + * @see ZipOutputStream#close() + */ + @Override + public void close() throws IOException { + finish(); + out.close(); + } +} + diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/OpcZipArchiveOutputStream.java b/src/ooxml/java/org/apache/poi/xssf/streaming/OpcZipArchiveOutputStream.java new file mode 100644 index 0000000000..573d580e37 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/OpcZipArchiveOutputStream.java @@ -0,0 +1,81 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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 org.apache.poi.xssf.streaming; + +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; + +import java.io.IOException; +import java.io.OutputStream; + +class OpcZipArchiveOutputStream extends ZipArchiveOutputStream { + private final OpcOutputStream out; + + OpcZipArchiveOutputStream(OutputStream out) { + super(out); + this.out = new OpcOutputStream(out); + } + + @Override + public void setLevel(int level) { + out.setLevel(level); + } + + + @Override + public void putArchiveEntry(ArchiveEntry archiveEntry) throws IOException { + out.putNextEntry(archiveEntry.getName()); + } + + @Override + public void closeArchiveEntry() throws IOException { + out.closeEntry(); + } + + + @Override + public void finish() throws IOException { + out.finish(); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + } + + @Override + public void close() throws IOException { + out.close(); + } + + @Override + public void write(int b) throws IOException { + out.write(b); + } + + @Override + public void flush() throws IOException { + out.flush(); + } + + @Override + public void write(byte[] b) throws IOException { + out.write(b); + } +} + diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFWorkbook.java b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFWorkbook.java index 57d1f4f590..5cf22eead5 100644 --- a/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFWorkbook.java +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/SXSSFWorkbook.java @@ -32,6 +32,7 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import org.apache.commons.compress.archivers.ArchiveOutputStream; import org.apache.commons.compress.archivers.zip.Zip64Mode; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; @@ -385,8 +386,7 @@ public class SXSSFWorkbook implements Workbook { } protected void injectData(ZipEntrySource zipEntrySource, OutputStream out) throws IOException { - ZipArchiveOutputStream zos = new ZipArchiveOutputStream(out); - zos.setUseZip64(zip64Mode); + ArchiveOutputStream zos = createArchiveOutputStream(out); try { Enumeration en = zipEntrySource.getEntries(); while (en.hasMoreElements()) { @@ -421,6 +421,16 @@ public class SXSSFWorkbook implements Workbook { } } + protected ZipArchiveOutputStream createArchiveOutputStream(OutputStream out) { + if (Zip64Mode.Always.equals(zip64Mode)) { + return new OpcZipArchiveOutputStream(out); + } else { + ZipArchiveOutputStream zos = new ZipArchiveOutputStream(out); + zos.setUseZip64(zip64Mode); + return zos; + } + } + private static void copyStreamAndInjectWorksheet(InputStream in, OutputStream out, InputStream worksheetData) throws IOException { InputStreamReader inReader = new InputStreamReader(in, StandardCharsets.UTF_8); OutputStreamWriter outWriter = new OutputStreamWriter(out, StandardCharsets.UTF_8); diff --git a/src/ooxml/java/org/apache/poi/xssf/streaming/Zip64Impl.java b/src/ooxml/java/org/apache/poi/xssf/streaming/Zip64Impl.java new file mode 100644 index 0000000000..5dfc49008a --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/streaming/Zip64Impl.java @@ -0,0 +1,185 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You 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 org.apache.poi.xssf.streaming; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.ZipEntry; + +import static java.nio.charset.StandardCharsets.US_ASCII; + +/** + * Excel compatible Zip64 implementation. + * For more information see https://github.com/rzymek/opczip + * + * @author Krzysztof Rzymkowski + */ +class Zip64Impl { + private static final long PK0102 = 0x02014b50L; + private static final long PK0304 = 0x04034b50L; + private static final long PK0506 = 0x06054b50L; + private static final long PK0708 = 0x08074b50L; + + private static final int VERSION_20 = 20; + private static final int VERSION_45 = 45; + private static final int DATA_DESCRIPTOR_USED = 0x08; + private static final int ZIP64_FIELD = 0x0001; + private static final long MAX32 = 0xffffffffL; + + private final OutputStream out; + private int written = 0; + + static class Entry { + final String filename; + long crc; + long size; + int compressedSize; + int offset; + + Entry(String filename) { + this.filename = filename; + } + } + + Zip64Impl(OutputStream out) { + this.out = out; + } + + /** + * Write Local File Header + */ + int writeLFH(Entry entry) throws IOException { + written = 0; + writeInt(PK0304); // "PK\003\004" + writeShort(VERSION_45); // version required: 4.5 + writeShort(DATA_DESCRIPTOR_USED); // flags: 8 = data descriptor used + writeShort(ZipEntry.DEFLATED); // compression method: 8 = deflate + writeInt(0); // file modification time & date + writeInt(entry.crc); // CRC-32 + writeInt(0); // compressed file size + writeInt(0); // uncompressed file size + writeShort(entry.filename.length()); // filename length + writeShort(0); // extra flags size + byte[] filenameBytes = entry.filename.getBytes(US_ASCII); + out.write(filenameBytes); // filename characters + return written + filenameBytes.length; + } + + /** + * Write Data Descriptor + */ + int writeDAT(Entry entry) throws IOException { + written = 0; + writeInt(PK0708); // data descriptor signature "PK\007\008" + writeInt(entry.crc); // crc-32 + writeLong(entry.compressedSize); // compressed size (zip64) + writeLong(entry.size); // uncompressed size (zip64) + return written; + } + + /** + * Write Central directory file header + */ + int writeCEN(Entry entry) throws IOException { + written = 0; + boolean useZip64 = entry.size > MAX32; + writeInt(PK0102); // "PK\001\002" + writeShort(VERSION_45); // version made by: 4.5 + writeShort(useZip64 ? VERSION_45 : VERSION_20);// version required: 4.5 + writeShort(DATA_DESCRIPTOR_USED); // flags: 8 = data descriptor used + writeShort(ZipEntry.DEFLATED); // compression method: 8 = deflate + writeInt(0); // file modification time & date + writeInt(entry.crc); // CRC-32 + writeInt(entry.compressedSize); // compressed size + writeInt(useZip64 ? MAX32 : entry.size); // uncompressed size + writeShort(entry.filename.length()); // filename length + writeShort(useZip64 + ? (2 + 2 + 8) /* short + short + long*/ + : 0); // extra field len + writeShort(0); // comment length + writeShort(0); // disk number where file starts + writeShort(0); // internal file attributes (unused) + writeInt(0); // external file attributes (unused) + writeInt(entry.offset); // LFH offset + byte[] filenameBytes = entry.filename.getBytes(US_ASCII); + out.write(filenameBytes); // filename characters + if (useZip64) { + // Extra field: + writeShort(ZIP64_FIELD); // ZIP64 field signature + writeShort(8); // size of extra field (below) + writeLong(entry.size); // uncompressed size + } + return written + filenameBytes.length; + } + + /** + * Write End of central directory record (EOCD) + */ + int writeEND(int entriesCount, int offset, int length) throws IOException { + written = 0; + writeInt(PK0506); // "PK\005\006" + writeShort(0); // number of this disk + writeShort(0); // central directory start disk + writeShort(entriesCount); // number of directory entries on disk + writeShort(entriesCount); // total number of directory entries + writeInt(length); // length of central directory + writeInt(offset); // offset of central directory + writeShort(0); // comment length + return written; + } + + /** + * Writes a 16-bit short to the output stream in little-endian byte order. + */ + private void writeShort(int v) throws IOException { + OutputStream out = this.out; + out.write((v >>> 0) & 0xff); + out.write((v >>> 8) & 0xff); + written += 2; + } + + /** + * Writes a 32-bit int to the output stream in little-endian byte order. + */ + private void writeInt(long v) throws IOException { + OutputStream out = this.out; + out.write((int) ((v >>> 0) & 0xff)); + out.write((int) ((v >>> 8) & 0xff)); + out.write((int) ((v >>> 16) & 0xff)); + out.write((int) ((v >>> 24) & 0xff)); + written += 4; + } + + /** + * Writes a 64-bit int to the output stream in little-endian byte order. + */ + private void writeLong(long v) throws IOException { + OutputStream out = this.out; + out.write((int) ((v >>> 0) & 0xff)); + out.write((int) ((v >>> 8) & 0xff)); + out.write((int) ((v >>> 16) & 0xff)); + out.write((int) ((v >>> 24) & 0xff)); + out.write((int) ((v >>> 32) & 0xff)); + out.write((int) ((v >>> 40) & 0xff)); + out.write((int) ((v >>> 48) & 0xff)); + out.write((int) ((v >>> 56) & 0xff)); + written += 8; + } + +} +