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