    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 */
    018
    019package org.apache.commons.compress.archivers.zip;
    020
    021import java.io.File;
    022import java.io.IOException;
    023import java.io.Serializable;
    024import java.nio.ByteBuffer;
    025import java.nio.channels.SeekableByteChannel;
    026import java.nio.file.Files;
    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 *
    045 * <p>If you want to read a split archive using {@link ZipFile} then create an instance of this class from the parts of
    046 * the archive.</p>
    047 *
    048 * @since 1.20
    049 */
    050public class ZipSplitReadOnlySeekableByteChannel extends MultiReadOnlySeekableByteChannel {
    051
    052    private static 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    private static final Path[] EMPTY_PATH_ARRAY = {};
    075    private static final int ZIP_SPLIT_SIGNATURE_LENGTH = 4;
    076
    077    /**
    078     * Concatenates ZIP split files from the last segment(the extension SHOULD be .zip)
    079     *
    080     * @param lastSegmentFile the last segment of ZIP split files, note that the extension SHOULD be .zip
    081     * @return SeekableByteChannel that concatenates all ZIP split files
    082     * @throws IllegalArgumentException if the lastSegmentFile's extension is NOT .zip
    083     * @throws IOException if the first channel doesn't seem to hold
    084     * 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     * @param lastSegmentPath the last segment of ZIP split files, note that the extension MUST be .zip
    093     * @return SeekableByteChannel that concatenates all ZIP split files
    094     * @throws IllegalArgumentException if the lastSegmentPath's extension is NOT .zip
    095     * @throws IOException if the first channel doesn't seem to hold
    096     * 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()
    106                : lastSegmentPath.getFileSystem().getPath(".");
    107        final String fileBaseName = FileNameUtils.getBaseName(lastSegmentPath);
    108        final ArrayList<Path> splitZipSegments;
    109
    110        // ZIP split segments should be like z01,z02....z(n-1) based on the ZIP specification
    111        final Pattern pattern = Pattern.compile(Pattern.quote(fileBaseName) + ".[zZ][0-9]+");
    112        try (Stream<Path> walk = Files.walk(parent, 1)) {
    113            splitZipSegments = walk
    114                    .filter(Files::isRegularFile)
    115                    .filter(path -> pattern.matcher(path.getFileName().toString()).matches())
    116                    .sorted(new ZipSplitSegmentComparator())
    117                    .collect(Collectors.toCollection(ArrayList::new));
    118        }
    119
    120        return forPaths(lastSegmentPath, splitZipSegments);
    121    }
    122
    123    /**
    124     * Concatenates the given files.
    125     *
    126     * @param files the files to concatenate, note that the LAST FILE of files should be the LAST SEGMENT(.zip)
    127     *              and theses files should be added in correct order (e.g. .z01, .z02... .z99, .zip)
    128     * @return SeekableByteChannel that concatenates all provided files
    129     * @throws NullPointerException if files is null
    130     * @throws IOException          if opening a channel for one of the files fails
    131     * @throws IOException if the first channel doesn't seem to hold
    132     * the beginning of a split archive
    133     */
    134    public static SeekableByteChannel forFiles(final File... files) throws IOException {
    135        final List<Path> paths = new ArrayList<>();
    136        for (final File f : Objects.requireNonNull(files, "files must not be null")) {
    137            paths.add(f.toPath());
    138        }
    139
    140        return forPaths(paths.toArray(EMPTY_PATH_ARRAY));
    141    }
    142
    143    /**
    144     * Concatenates the given files.
    145     *
    146     * @param lastSegmentFile the last segment of split ZIP segments, its extension should be .zip
    147     * @param files           the files to concatenate except for the last segment,
    148     *                        note theses files should be added in correct order (e.g. .z01, .z02... .z99)
    149     * @return SeekableByteChannel that concatenates all provided files
    150     * @throws IOException if the first channel doesn't seem to hold
    151     * the beginning of a split archive
    152     * @throws NullPointerException if files or lastSegmentFile is null
    153     */
    154    public static SeekableByteChannel forFiles(final File lastSegmentFile, final Iterable<File> files) throws IOException {
    155        Objects.requireNonNull(files, "files");
    156        Objects.requireNonNull(lastSegmentFile, "lastSegmentFile");
    157
    158        final List<Path> filesList = new ArrayList<>();
    159        files.forEach(f -> filesList.add(f.toPath()));
    160
    161        return forPaths(lastSegmentFile.toPath(), filesList);
    162    }
    163
    164    /**
    165     * Concatenates the given channels.
    166     *
    167     * @param channels the channels to concatenate, note that the LAST CHANNEL of channels should be the LAST SEGMENT(.zip)
    168     *                 and theses channels should be added in correct order (e.g. .z01, .z02... .z99, .zip)
    169     * @return SeekableByteChannel that concatenates all provided channels
    170     * @throws NullPointerException if channels is null
    171     * @throws IOException if reading channels fails
    172     */
    173    public static SeekableByteChannel forOrderedSeekableByteChannels(final SeekableByteChannel... channels) throws IOException {
    174        if (Objects.requireNonNull(channels, "channels must not be null").length == 1) {
    175            return channels[0];
    176        }
    177        return new ZipSplitReadOnlySeekableByteChannel(Arrays.asList(channels));
    178    }
    179
    180    /**
    181     * Concatenates the given channels.
    182     *
    183     * @param lastSegmentChannel channel of the last segment of split ZIP segments, its extension should be .zip
    184     * @param channels           the channels to concatenate except for the last segment,
    185     *                           note theses channels should be added in correct order (e.g. .z01, .z02... .z99)
    186     * @return SeekableByteChannel that concatenates all provided channels
    187     * @throws NullPointerException if lastSegmentChannel or channels is null
    188     * @throws IOException if the first channel doesn't seem to hold
    189     * the beginning of a split archive
    190     */
    191    public static SeekableByteChannel forOrderedSeekableByteChannels(final SeekableByteChannel lastSegmentChannel,
    192        final Iterable<SeekableByteChannel> channels) throws IOException {
    193        Objects.requireNonNull(channels, "channels");
    194        Objects.requireNonNull(lastSegmentChannel, "lastSegmentChannel");
    195
    196        final List<SeekableByteChannel> channelsList = new ArrayList<>();
    197        channels.forEach(channelsList::add);
    198        channelsList.add(lastSegmentChannel);
    199
    200        return forOrderedSeekableByteChannels(channelsList.toArray(new SeekableByteChannel[0]));
    201    }
    202
    203    /**
    204     * Concatenates the given file paths.
    205     * @param paths the file paths to concatenate, note that the LAST FILE of files should be the LAST SEGMENT(.zip)
    206     * and these files should be added in correct order (e.g.: .z01, .z02... .z99, .zip)
    207     * @return SeekableByteChannel that concatenates all provided files
    208     * @throws NullPointerException if files is null
    209     * @throws IOException if opening a channel for one of the files fails
    210     * @throws IOException if the first channel doesn't seem to hold
    211     * the beginning of a split archive
    212     * @since 1.22
    213     */
    214    public static SeekableByteChannel forPaths(final Path... paths) throws IOException {
    215        final List<SeekableByteChannel> channels = new ArrayList<>();
    216        for (final Path path : Objects.requireNonNull(paths, "paths must not be null")) {
    217            channels.add(Files.newByteChannel(path, StandardOpenOption.READ));
    218        }
    219        if (channels.size() == 1) {
    220            return channels.get(0);
    221        }
    222        return new ZipSplitReadOnlySeekableByteChannel(channels);
    223    }
    224
    225    /**
    226     * Concatenates the given file paths.
    227     * @param lastSegmentPath the last segment path of split ZIP segments, its extension must be .zip
    228     * @param paths the file paths to concatenate except for the last segment,
    229     * note these files should be added in correct order (e.g.: .z01, .z02... .z99)
    230     * @return SeekableByteChannel that concatenates all provided files
    231     * @throws IOException if the first channel doesn't seem to hold
    232     * the beginning of a split archive
    233     * @throws NullPointerException if files or lastSegmentPath is null
    234     * @since 1.22
    235     */
    236    public static SeekableByteChannel forPaths(final Path lastSegmentPath, final Iterable<Path> paths) throws IOException {
    237        Objects.requireNonNull(paths, "paths");
    238        Objects.requireNonNull(lastSegmentPath, "lastSegmentPath");
    239
    240        final List<Path> filesList = new ArrayList<>();
    241        paths.forEach(filesList::add);
    242        filesList.add(lastSegmentPath);
    243
    244        return forPaths(filesList.toArray(EMPTY_PATH_ARRAY));
    245    }
    246
    247    private final ByteBuffer zipSplitSignatureByteBuffer =
    248        ByteBuffer.allocate(ZIP_SPLIT_SIGNATURE_LENGTH);
    249
    250    /**
    251     * Concatenates the given channels.
    252     *
    253     * <p>The channels should be add in ascending order, e.g. z01,
    254     * z02, ... z99, ZIP please note that the .zip file is the last
    255     * segment and should be added as the last one in the channels</p>
    256     *
    257     * @param channels the channels to concatenate
    258     * @throws NullPointerException if channels is null
    259     * @throws IOException if the first channel doesn't seem to hold
    260     * the beginning of a split archive
    261     */
    262    public ZipSplitReadOnlySeekableByteChannel(final List<SeekableByteChannel> channels)
    263        throws IOException {
    264        super(channels);
    265
    266        // the first split ZIP segment should begin with ZIP split signature
    267        assertSplitSignature(channels);
    268    }
    269
    270    /**
    271     * Based on the ZIP specification:
    272     *
    273     * <p>
    274     * 8.5.3 Spanned/Split archives created using PKZIP for Windows
    275     * (V2.50 or greater), PKZIP Command Line (V2.50 or greater),
    276     * or PKZIP Explorer will include a special spanning
    277     * signature as the first 4 bytes of the first segment of
    278     * the archive.  This signature (0x08074b50) will be
    279     * followed immediately by the local header signature for
    280     * the first file in the archive.
    281     *
    282     * <p>
    283     * the first 4 bytes of the first ZIP split segment should be the ZIP split signature(0x08074B50)
    284     *
    285     * @param channels channels to be validated
    286     * @throws IOException
    287     */
    288    private void assertSplitSignature(final List<SeekableByteChannel> channels)
    289        throws IOException {
    290        final SeekableByteChannel channel = channels.get(0);
    291        // the ZIP split file signature is at the beginning of the first split segment
    292        channel.position(0L);
    293
    294        zipSplitSignatureByteBuffer.rewind();
    295        channel.read(zipSplitSignatureByteBuffer);
    296        final ZipLong signature = new ZipLong(zipSplitSignatureByteBuffer.array());
    297        if (!signature.equals(ZipLong.DD_SIG)) {
    298            channel.position(0L);
    299            throw new IOException("The first ZIP split segment does not begin with split ZIP file signature");
    300        }
    301
    302        channel.position(0L);
    303    }
    304}