    001/*
    002 *  Licensed to the Apache Software Foundation (ASF) under one or more
    003 *  contributor license agreements.  See the NOTICE file distributed with
    004 *  this work for additional information regarding copyright ownership.
    005 *  The ASF licenses this file to You under the Apache License, Version 2.0
    006 *  (the "License"); you may not use this file except in compliance with
    007 *  the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 *  Unless required by applicable law or agreed to in writing, software
    012 *  distributed under the License is distributed on an "AS IS" BASIS,
    013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 *  See the License for the specific language governing permissions and
    015 *  limitations under the License.
    016 */
    017
    018package org.apache.commons.compress.archivers.zip;
    019
    020import java.io.File;
    021import java.io.IOException;
    022import java.io.Serializable;
    023import java.nio.ByteBuffer;
    024import java.nio.channels.SeekableByteChannel;
    025import java.nio.file.Files;
    026import java.nio.file.OpenOption;
    027import java.nio.file.Path;
    028import java.nio.file.StandardOpenOption;
    029import java.util.ArrayList;
    030import java.util.Arrays;
    031import java.util.Comparator;
    032import java.util.List;
    033import java.util.Objects;
    034import java.util.regex.Pattern;
    035import java.util.stream.Collectors;
    036import java.util.stream.Stream;
    037
    038import org.apache.commons.compress.archivers.ArchiveStreamFactory;
    039import org.apache.commons.compress.utils.FileNameUtils;
    040import org.apache.commons.compress.utils.MultiReadOnlySeekableByteChannel;
    041
    042/**
    043 * {@link MultiReadOnlySeekableByteChannel} that knows what a split ZIP archive should look like.
    044 * <p>
    045 * If you want to read a split archive using {@link ZipFile} then create an instance of this class from the parts of the archive.
    046 * </p>
    047 *
    048 * @since 1.20
    049 */
    050public class ZipSplitReadOnlySeekableByteChannel extends MultiReadOnlySeekableByteChannel {
    051
    052    private static final class ZipSplitSegmentComparator implements Comparator<Path>, Serializable {
    053        private static final long serialVersionUID = 20200123L;
    054
    055        @Override
    056        public int compare(final Path file1, final Path file2) {
    057            final String extension1 = FileNameUtils.getExtension(file1);
    058            final String extension2 = FileNameUtils.getExtension(file2);
    059
    060            if (!extension1.startsWith("z")) {
    061                return -1;
    062            }
    063
    064            if (!extension2.startsWith("z")) {
    065                return 1;
    066            }
    067
    068            final Integer splitSegmentNumber1 = Integer.parseInt(extension1.substring(1));
    069            final Integer splitSegmentNumber2 = Integer.parseInt(extension2.substring(1));
    070
    071            return splitSegmentNumber1.compareTo(splitSegmentNumber2);
    072        }
    073    }
    074
    075    private static final Path[] EMPTY_PATH_ARRAY = {};
    076    private static final int ZIP_SPLIT_SIGNATURE_LENGTH = 4;
    077
    078    /**
    079     * Concatenates ZIP split files from the last segment(the extension SHOULD be .zip)
    080     *
    081     * @param lastSegmentFile the last segment of ZIP split files, note that the extension SHOULD be .zip
    082     * @return SeekableByteChannel that concatenates all ZIP split files
    083     * @throws IllegalArgumentException if the lastSegmentFile's extension is NOT .zip
    084     * @throws IOException              if the first channel doesn't seem to hold the beginning of a split archive
    085     */
    086    public static SeekableByteChannel buildFromLastSplitSegment(final File lastSegmentFile) throws IOException {
    087        return buildFromLastSplitSegment(lastSegmentFile.toPath());
    088    }
    089
    090    /**
    091     * Concatenates ZIP split files from the last segment (the extension MUST be .zip)
    092     *
    093     * @param lastSegmentPath the last segment of ZIP split files, note that the extension MUST be .zip
    094     * @return SeekableByteChannel that concatenates all ZIP split files
    095     * @throws IllegalArgumentException if the lastSegmentPath's extension is NOT .zip
    096     * @throws IOException              if the first channel doesn't seem to hold the beginning of a split archive
    097     * @since 1.22
    098     */
    099    public static SeekableByteChannel buildFromLastSplitSegment(final Path lastSegmentPath) throws IOException {
    100        final String extension = FileNameUtils.getExtension(lastSegmentPath);
    101        if (!extension.equalsIgnoreCase(ArchiveStreamFactory.ZIP)) {
    102            throw new IllegalArgumentException("The extension of last ZIP split segment should be .zip");
    103        }
    104
    105        final Path parent = Objects.nonNull(lastSegmentPath.getParent()) ? lastSegmentPath.getParent() : lastSegmentPath.getFileSystem().getPath(".");
    106        final String fileBaseName = FileNameUtils.getBaseName(lastSegmentPath);
    107        final ArrayList<Path> splitZipSegments;
    108
    109        // ZIP split segments should be like z01,z02....z(n-1) based on the ZIP specification
    110        final Pattern pattern = Pattern.compile(Pattern.quote(fileBaseName) + ".[zZ][0-9]+");
    111        try (Stream<Path> walk = Files.walk(parent, 1)) {
    112            splitZipSegments = walk.filter(Files::isRegularFile).filter(path -> pattern.matcher(path.getFileName().toString()).matches())
    113                    .sorted(new ZipSplitSegmentComparator()).collect(Collectors.toCollection(ArrayList::new));
    114        }
    115
    116        return forPaths(lastSegmentPath, splitZipSegments);
    117    }
    118
    119    /**
    120     * Concatenates the given files.
    121     *
    122     * @param files the files to concatenate, note that the LAST FILE of files should be the LAST SEGMENT(.zip) and these files should be added in correct order
    123     *              (e.g. .z01, .z02... .z99, .zip)
    124     * @return SeekableByteChannel that concatenates all provided files
    125     * @throws NullPointerException if files is null
    126     * @throws IOException          if opening a channel for one of the files fails
    127     * @throws IOException          if the first channel doesn't seem to hold the beginning of a split archive
    128     */
    129    public static SeekableByteChannel forFiles(final File... files) throws IOException {
    130        final List<Path> paths = new ArrayList<>();
    131        for (final File f : Objects.requireNonNull(files, "files")) {
    132            paths.add(f.toPath());
    133        }
    134
    135        return forPaths(paths.toArray(EMPTY_PATH_ARRAY));
    136    }
    137
    138    /**
    139     * Concatenates the given files.
    140     *
    141     * @param lastSegmentFile the last segment of split ZIP segments, its extension should be .zip
    142     * @param files           the files to concatenate except for the last segment, note these files should be added in correct order (e.g. .z01, .z02... .z99)
    143     * @return SeekableByteChannel that concatenates all provided files
    144     * @throws IOException          if the first channel doesn't seem to hold the beginning of a split archive
    145     * @throws NullPointerException if files or lastSegmentFile is null
    146     */
    147    public static SeekableByteChannel forFiles(final File lastSegmentFile, final Iterable<File> files) throws IOException {
    148        Objects.requireNonNull(files, "files");
    149        Objects.requireNonNull(lastSegmentFile, "lastSegmentFile");
    150
    151        final List<Path> filesList = new ArrayList<>();
    152        files.forEach(f -> filesList.add(f.toPath()));
    153
    154        return forPaths(lastSegmentFile.toPath(), filesList);
    155    }
    156
    157    /**
    158     * Concatenates the given channels.
    159     *
    160     * @param channels the channels to concatenate, note that the LAST CHANNEL of channels should be the LAST SEGMENT(.zip) and these channels should be added
    161     *                 in correct order (e.g. .z01, .z02... .z99, .zip)
    162     * @return SeekableByteChannel that concatenates all provided channels
    163     * @throws NullPointerException if channels is null
    164     * @throws IOException          if reading channels fails
    165     */
    166    public static SeekableByteChannel forOrderedSeekableByteChannels(final SeekableByteChannel... channels) throws IOException {
    167        if (Objects.requireNonNull(channels, "channels").length == 1) {
    168            return channels[0];
    169        }
    170        return new ZipSplitReadOnlySeekableByteChannel(Arrays.asList(channels));
    171    }
    172
    173    /**
    174     * Concatenates the given channels.
    175     *
    176     * @param lastSegmentChannel channel of the last segment of split ZIP segments, its extension should be .zip
    177     * @param channels           the channels to concatenate except for the last segment, note these channels should be added in correct order (e.g. .z01,
    178     *                           .z02... .z99)
    179     * @return SeekableByteChannel that concatenates all provided channels
    180     * @throws NullPointerException if lastSegmentChannel or channels is null
    181     * @throws IOException          if the first channel doesn't seem to hold the beginning of a split archive
    182     */
    183    public static SeekableByteChannel forOrderedSeekableByteChannels(final SeekableByteChannel lastSegmentChannel, final Iterable<SeekableByteChannel> channels)
    184            throws IOException {
    185        Objects.requireNonNull(channels, "channels");
    186        Objects.requireNonNull(lastSegmentChannel, "lastSegmentChannel");
    187
    188        final List<SeekableByteChannel> channelsList = new ArrayList<>();
    189        channels.forEach(channelsList::add);
    190        channelsList.add(lastSegmentChannel);
    191
    192        return forOrderedSeekableByteChannels(channelsList.toArray(new SeekableByteChannel[0]));
    193    }
    194
    195    /**
    196     * Concatenates the given file paths.
    197     *
    198     * @param paths the file paths to concatenate, note that the LAST FILE of files should be the LAST SEGMENT(.zip) and these files should be added in correct
    199     *              order (e.g.: .z01, .z02... .z99, .zip)
    200     * @param openOptions the options to open paths (shared by all paths).
    201     * @return SeekableByteChannel that concatenates all provided files
    202     * @throws NullPointerException if files is null
    203     * @throws IOException          if opening a channel for one of the files fails
    204     * @throws IOException          if the first channel doesn't seem to hold the beginning of a split archive
    205     * @since 1.22
    206     */
    207    public static SeekableByteChannel forPaths(final List<Path> paths, final OpenOption[] openOptions) throws IOException {
    208        final List<SeekableByteChannel> channels = new ArrayList<>();
    209        for (final Path path : Objects.requireNonNull(paths, "paths")) {
    210            channels.add(Files.newByteChannel(path, openOptions));
    211        }
    212        if (channels.size() == 1) {
    213            return channels.get(0);
    214        }
    215        return new ZipSplitReadOnlySeekableByteChannel(channels);
    216    }
    217
    218    /**
    219     * Concatenates the given file paths.
    220     *
    221     * @param paths the file paths to concatenate, note that the LAST FILE of files should be the LAST SEGMENT(.zip) and these files should be added in correct
    222     *              order (e.g.: .z01, .z02... .z99, .zip)
    223     * @return SeekableByteChannel that concatenates all provided files
    224     * @throws NullPointerException if files is null
    225     * @throws IOException          if opening a channel for one of the files fails
    226     * @throws IOException          if the first channel doesn't seem to hold the beginning of a split archive
    227     * @since 1.22
    228     */
    229    public static SeekableByteChannel forPaths(final Path... paths) throws IOException {
    230        return forPaths(Arrays.asList(paths), new OpenOption[] { StandardOpenOption.READ });
    231    }
    232
    233    /**
    234     * Concatenates the given file paths.
    235     *
    236     * @param lastSegmentPath the last segment path of split ZIP segments, its extension must be .zip
    237     * @param paths           the file paths to concatenate except for the last segment, note these files should be added in correct order (e.g.: .z01, .z02...
    238     *                        .z99)
    239     * @return SeekableByteChannel that concatenates all provided files
    240     * @throws IOException          if the first channel doesn't seem to hold the beginning of a split archive
    241     * @throws NullPointerException if files or lastSegmentPath is null
    242     * @since 1.22
    243     */
    244    public static SeekableByteChannel forPaths(final Path lastSegmentPath, final Iterable<Path> paths) throws IOException {
    245        Objects.requireNonNull(paths, "paths");
    246        Objects.requireNonNull(lastSegmentPath, "lastSegmentPath");
    247
    248        final List<Path> filesList = new ArrayList<>();
    249        paths.forEach(filesList::add);
    250        filesList.add(lastSegmentPath);
    251
    252        return forPaths(filesList.toArray(EMPTY_PATH_ARRAY));
    253    }
    254
    255    private final ByteBuffer zipSplitSignatureByteBuffer = ByteBuffer.allocate(ZIP_SPLIT_SIGNATURE_LENGTH);
    256
    257    /**
    258     * Concatenates the given channels.
    259     *
    260     * <p>
    261     * The channels should be add in ascending order, e.g. z01, z02, ... z99, ZIP please note that the .zip file is the last segment and should be added as the
    262     * last one in the channels
    263     * </p>
    264     *
    265     * @param channels the channels to concatenate
    266     * @throws NullPointerException if channels is null
    267     * @throws IOException          if the first channel doesn't seem to hold the beginning of a split archive
    268     */
    269    public ZipSplitReadOnlySeekableByteChannel(final List<SeekableByteChannel> channels) throws IOException {
    270        super(channels);
    271
    272        // the first split ZIP segment should begin with ZIP split signature
    273        assertSplitSignature(channels);
    274    }
    275
    276    /**
    277     * Based on the ZIP specification:
    278     *
    279     * <p>
    280     * 8.5.3 Spanned/Split archives created using PKZIP for Windows (V2.50 or greater), PKZIP Command Line (V2.50 or greater), or PKZIP Explorer will include a
    281     * special spanning signature as the first 4 bytes of the first segment of the archive. This signature (0x08074b50) will be followed immediately by the
    282     * local header signature for the first file in the archive.
    283     * </p>
    284     * <p>
    285     * The first 4 bytes of the first ZIP split segment should be the ZIP split signature(0x08074B50)
    286     * </p>
    287     *
    288     * @param channels channels to be validated
    289     * @throws IOException
    290     */
    291    private void assertSplitSignature(final List<SeekableByteChannel> channels) throws IOException {
    292        final SeekableByteChannel channel = channels.get(0);
    293        // the ZIP split file signature is at the beginning of the first split segment
    294        channel.position(0L);
    295
    296        zipSplitSignatureByteBuffer.rewind();
    297        channel.read(zipSplitSignatureByteBuffer);
    298        final ZipLong signature = new ZipLong(zipSplitSignatureByteBuffer.array());
    299        if (!signature.equals(ZipLong.DD_SIG)) {
    300            channel.position(0L);
    301            throw new IOException("The first ZIP split segment does not begin with split ZIP file signature");
    302        }
    303
    304        channel.position(0L);
    305    }
    306}