View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.commons.compress.archivers.zip;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.nio.ByteBuffer;
24  import java.nio.channels.FileChannel;
25  import java.nio.file.Files;
26  import java.nio.file.Path;
27  import java.nio.file.StandardCopyOption;
28  import java.nio.file.StandardOpenOption;
29  import java.util.ArrayList;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Objects;
33  import java.util.TreeMap;
34  
35  import org.apache.commons.io.file.PathUtils;
36  
37  /**
38   * Used internally by {@link ZipArchiveOutputStream} when creating a split archive.
39   *
40   * @since 1.20
41   */
42  final class ZipSplitOutputStream extends RandomAccessOutputStream {
43  
44      /**
45       * 8.5.1 Capacities for split archives are as follows:
46       * <p>
47       * Maximum number of segments = 4,294,967,295 - 1 Maximum .ZIP segment size = 4,294,967,295 bytes (refer to section 8.5.6) Minimum segment size = 64K
48       * </p>
49       * <p>
50       * Maximum PKSFX segment size = 2,147,483,647 bytes
51       * </p>
52       */
53      private static final long ZIP_SEGMENT_MIN_SIZE = 64 * 1024L;
54      private static final long ZIP_SEGMENT_MAX_SIZE = 4294967295L;
55  
56      private FileChannel currentChannel;
57      private FileRandomAccessOutputStream outputStream;
58      private Path zipFile;
59      private final long splitSize;
60      private long totalPosition;
61      private int currentSplitSegmentIndex;
62      private long currentSplitSegmentBytesWritten;
63      private boolean finished;
64      private final byte[] singleByte = new byte[1];
65      private final List<Long> diskToPosition = new ArrayList<>();
66      private final TreeMap<Long, Path> positionToFiles = new TreeMap<>();
67  
68      /**
69       * Creates a split ZIP. If the ZIP file is smaller than the split size, then there will only be one split ZIP, and its suffix is .zip, otherwise the split
70       * segments should be like .z01, .z02, ... .z(N-1), .zip
71       *
72       * @param zipFile   the ZIP file to write to
73       * @param splitSize the split size
74       * @throws IllegalArgumentException if arguments are illegal: Zip split segment size should between 64K and 4,294,967,295.
75       * @throws IOException              if an I/O error occurs
76       */
77      ZipSplitOutputStream(final File zipFile, final long splitSize) throws IllegalArgumentException, IOException {
78          this(zipFile.toPath(), splitSize);
79      }
80  
81      /**
82       * Creates a split ZIP. If the ZIP file is smaller than the split size, then there will only be one split ZIP, and its suffix is .zip, otherwise the split
83       * segments should be like .z01, .z02, ... .z(N-1), .zip
84       *
85       * @param zipFile   the path to ZIP file to write to.
86       * @param splitSize the split size.
87       * @throws IllegalArgumentException if arguments are illegal: Zip split segment size should between 64K and 4,294,967,295.
88       * @throws IOException              if an I/O error occurs.
89       * @since 1.22
90       */
91      ZipSplitOutputStream(final Path zipFile, final long splitSize) throws IllegalArgumentException, IOException {
92          if (splitSize < ZIP_SEGMENT_MIN_SIZE || splitSize > ZIP_SEGMENT_MAX_SIZE) {
93              throw new IllegalArgumentException("Zip split segment size should between 64K and 4,294,967,295");
94          }
95          this.zipFile = zipFile;
96          this.splitSize = splitSize;
97          this.outputStream = new FileRandomAccessOutputStream(zipFile);
98          this.currentChannel = this.outputStream.channel();
99          this.positionToFiles.put(0L, this.zipFile);
100         this.diskToPosition.add(0L);
101         // write the ZIP split signature 0x08074B50 to the ZIP file
102         writeZipSplitSignature();
103     }
104 
105     public long calculateDiskPosition(final long disk, final long localOffset) throws IOException {
106         if (disk >= Integer.MAX_VALUE) {
107             throw new IOException("Disk number exceeded internal limits: limit=" + Integer.MAX_VALUE + " requested=" + disk);
108         }
109         return diskToPosition.get((int) disk) + localOffset;
110     }
111 
112     @Override
113     public void close() throws IOException {
114         if (!finished) {
115             finish();
116         }
117     }
118 
119     /**
120      * Creates the new ZIP split segment, the last ZIP segment should be .zip, and the ZIP split segments' suffix should be like .z01, .z02, .z03, ... .z99,
121      * .z100, ..., .z(N-1), .zip
122      * <p>
123      * 8.3.3 Split ZIP files are typically written to the same location and are subject to name collisions if the spanned name format is used since each segment
124      * will reside on the same drive. To avoid name collisions, split archives are named as follows.
125      * </p>
126      * <p>
127      * Segment 1 = filename.z01 Segment n-1 = filename.z(n-1) Segment n = filename.zip
128      * </p>
129      * <p>
130      * NOTE: The ZIP split segment begin from 1,2,3,... , and we're creating a new segment, so the new segment suffix should be (currentSplitSegmentIndex + 2)
131      * </p>
132      *
133      * @param zipSplitSegmentSuffixIndex
134      * @return
135      * @throws IOException if an I/O error occurs.
136      */
137     private Path createNewSplitSegmentFile(final Integer zipSplitSegmentSuffixIndex) throws IOException {
138         final Path newFile = getSplitSegmentFileName(zipSplitSegmentSuffixIndex);
139 
140         if (Files.exists(newFile)) {
141             throw new IOException("split ZIP segment " + newFile + " already exists");
142         }
143         return newFile;
144     }
145 
146     /**
147      * The last ZIP split segment's suffix should be .zip
148      *
149      * @throws IOException if an I/O error occurs.
150      */
151     private void finish() throws IOException {
152         if (finished) {
153             throw new IOException("This archive has already been finished");
154         }
155         final Path path = zipFile;
156 
157         final String zipFileBaseName = PathUtils.getBaseName(path);
158         outputStream.close();
159         Files.move(zipFile, zipFile.resolveSibling(zipFileBaseName + ".zip"), StandardCopyOption.ATOMIC_MOVE);
160         finished = true;
161     }
162 
163     public long getCurrentSplitSegmentBytesWritten() {
164         return currentSplitSegmentBytesWritten;
165     }
166 
167     public int getCurrentSplitSegmentIndex() {
168         return currentSplitSegmentIndex;
169     }
170 
171     private Path getSplitSegmentFileName(final Integer zipSplitSegmentSuffixIndex) {
172         final int newZipSplitSegmentSuffixIndex = zipSplitSegmentSuffixIndex == null ? currentSplitSegmentIndex + 2 : zipSplitSegmentSuffixIndex;
173         final Path path = zipFile;
174         final String baseName = PathUtils.getBaseName(path);
175         final StringBuilder extension = new StringBuilder(".z");
176         if (newZipSplitSegmentSuffixIndex <= 9) {
177             extension.append("0").append(newZipSplitSegmentSuffixIndex);
178         } else {
179             extension.append(newZipSplitSegmentSuffixIndex);
180         }
181 
182         final Path parent = zipFile.getParent();
183         final String dir = Objects.nonNull(parent) ? parent.toAbsolutePath().toString() : ".";
184         return zipFile.getFileSystem().getPath(dir, baseName + extension.toString());
185     }
186 
187     /**
188      * Creates a new ZIP split segment and prepare to write to the new segment
189      *
190      * @throws IOException if an I/O error occurs.
191      */
192     private void openNewSplitSegment() throws IOException {
193         Path newFile;
194         if (currentSplitSegmentIndex == 0) {
195             outputStream.close();
196             newFile = createNewSplitSegmentFile(1);
197             Files.move(zipFile, newFile, StandardCopyOption.ATOMIC_MOVE);
198             this.positionToFiles.put(0L, newFile);
199         }
200 
201         newFile = createNewSplitSegmentFile(null);
202 
203         outputStream.close();
204         outputStream = new FileRandomAccessOutputStream(newFile);
205         currentChannel = outputStream.channel();
206         currentSplitSegmentBytesWritten = 0;
207         zipFile = newFile;
208         currentSplitSegmentIndex++;
209         this.diskToPosition.add(this.totalPosition);
210         this.positionToFiles.put(this.totalPosition, newFile);
211     }
212 
213     @Override
214     public long position() {
215         return totalPosition;
216     }
217 
218     /**
219      * Prepares to write unsplittable content.
220      * <p>
221      * Some data cannot be written to different split segments, for example:
222      * </p>
223      * <p>
224      * 4.4.1.5 The end of central directory record and the Zip64 end of central directory locator record MUST reside on the same disk when splitting or spanning
225      * an archive.
226      * </p>
227      *
228      * @param unsplittableContentSize the split size request must be less than or equal to the the split size.
229      * @throws IllegalArgumentException if unsplittable content size is bigger than the split segment size.
230      * @throws IOException if an I/O error occurs.
231      */
232     public void prepareToWriteUnsplittableContent(final long unsplittableContentSize) throws IllegalArgumentException, IOException {
233         if (unsplittableContentSize > this.splitSize) {
234             throw new IllegalArgumentException("The unsplittable content size is bigger than the split segment size");
235         }
236 
237         final long bytesRemainingInThisSegment = this.splitSize - this.currentSplitSegmentBytesWritten;
238         if (bytesRemainingInThisSegment < unsplittableContentSize) {
239             openNewSplitSegment();
240         }
241     }
242 
243     @Override
244     public void write(final byte[] b) throws IOException {
245         write(b, 0, b.length);
246     }
247 
248     /**
249      * Writes the data to ZIP split segments, if the remaining space of current split segment is not enough, then a new split segment should be created
250      *
251      * @param b   data to write
252      * @param off offset of the start of data in param b
253      * @param len the length of data to write
254      * @throws IOException if an I/O error occurs.
255      */
256     @Override
257     public void write(final byte[] b, final int off, final int len) throws IOException {
258         if (len <= 0) {
259             return;
260         }
261 
262         if (currentSplitSegmentBytesWritten >= splitSize) {
263             openNewSplitSegment();
264             write(b, off, len);
265         } else if (currentSplitSegmentBytesWritten + len > splitSize) {
266             final int bytesToWriteForThisSegment = (int) splitSize - (int) currentSplitSegmentBytesWritten;
267             write(b, off, bytesToWriteForThisSegment);
268             openNewSplitSegment();
269             write(b, off + bytesToWriteForThisSegment, len - bytesToWriteForThisSegment);
270         } else {
271             outputStream.write(b, off, len);
272             currentSplitSegmentBytesWritten += len;
273             totalPosition += len;
274         }
275     }
276 
277     @Override
278     public void write(final int i) throws IOException {
279         singleByte[0] = (byte) (i & 0xff);
280         write(singleByte);
281     }
282 
283     @Override
284     public void writeAll(final byte[] b, final int off, final int len, final long atPosition) throws IOException {
285         long remainingPosition = atPosition;
286         for (int remainingOff = off, remainingLen = len; remainingLen > 0; ) {
287             final Map.Entry<Long, Path> segment = positionToFiles.floorEntry(remainingPosition);
288             final Long segmentEnd = positionToFiles.higherKey(remainingPosition);
289             if (segmentEnd == null) {
290                 ZipIoUtil.writeAll(this.currentChannel, ByteBuffer.wrap(b, remainingOff, remainingLen), remainingPosition - segment.getKey());
291                 remainingPosition += remainingLen;
292                 remainingOff += remainingLen;
293                 remainingLen = 0;
294             } else if (remainingPosition + remainingLen <= segmentEnd) {
295                 writeToSegment(segment.getValue(), remainingPosition - segment.getKey(), b, remainingOff, remainingLen);
296                 remainingPosition += remainingLen;
297                 remainingOff += remainingLen;
298                 remainingLen = 0;
299             } else {
300                 final int toWrite = Math.toIntExact(segmentEnd - remainingPosition);
301                 writeToSegment(segment.getValue(), remainingPosition - segment.getKey(), b, remainingOff, toWrite);
302                 remainingPosition += toWrite;
303                 remainingOff += toWrite;
304                 remainingLen -= toWrite;
305             }
306         }
307     }
308 
309     private void writeToSegment(final Path segment, final long position, final byte[] b, final int off, final int len) throws IOException {
310         try (FileChannel channel = FileChannel.open(segment, StandardOpenOption.WRITE)) {
311             ZipIoUtil.writeAll(channel, ByteBuffer.wrap(b, off, len), position);
312         }
313     }
314 
315     /**
316      * Writes the ZIP split signature (0x08074B50) to the head of the first ZIP split segment
317      *
318      * @throws IOException if an I/O error occurs.
319      */
320     private void writeZipSplitSignature() throws IOException {
321         outputStream.write(ZipArchiveOutputStream.DD_SIG);
322         currentSplitSegmentBytesWritten += ZipArchiveOutputStream.DD_SIG.length;
323         totalPosition += ZipArchiveOutputStream.DD_SIG.length;
324     }
325 }