1 // Copyright 2016 Google Inc. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 package com.google.archivepatcher.generator;
16 
17 import com.google.archivepatcher.shared.RandomAccessFileInputStream;
18 
19 import java.io.File;
20 import java.io.IOException;
21 import java.util.ArrayList;
22 import java.util.Collections;
23 import java.util.Comparator;
24 import java.util.List;
25 import java.util.zip.ZipException;
26 
27 /**
28  * A simplified, structural representation of a zip or zip-like (jar, apk, etc) archive. The class
29  * provides the minimum structural information needed for patch generation and is not suitable as a
30  * general zip-processing library. In particular, there is little or no verification that any of the
31  * zip structure is correct or sane; it assumed that the input is sane.
32  */
33 public class MinimalZipArchive {
34 
35   /**
36    * Sorts {@link MinimalZipEntry} objects by {@link MinimalZipEntry#getFileOffsetOfLocalEntry()} in
37    * ascending order.
38    */
39   private static final Comparator<MinimalZipEntry> LOCAL_ENTRY_OFFSET_COMAPRATOR =
40       new Comparator<MinimalZipEntry>() {
41         @Override
42         public int compare(MinimalZipEntry o1, MinimalZipEntry o2) {
43           return Long.compare(o1.getFileOffsetOfLocalEntry(), o2.getFileOffsetOfLocalEntry());
44         }
45       };
46 
47   /**
48    * Generate a listing of all of the files in a zip archive in file order and return it. Each entry
49    * is a {@link MinimalZipEntry}, which has just enough information to generate a patch.
50    * @param file the zip file to read
51    * @return such a listing
52    * @throws IOException if anything goes wrong while reading
53    */
listEntries(File file)54   public static List<MinimalZipEntry> listEntries(File file) throws IOException {
55     try (RandomAccessFileInputStream in = new RandomAccessFileInputStream(file)) {
56       return listEntriesInternal(in);
57     }
58   }
59 
60   /**
61    * Internal implementation of {@link #listEntries(File)}.
62    * @param in the input stream to read from
63    * @return see {@link #listEntries(File)}
64    * @throws IOException if anything goes wrong while reading
65    */
listEntriesInternal(RandomAccessFileInputStream in)66   private static List<MinimalZipEntry> listEntriesInternal(RandomAccessFileInputStream in)
67       throws IOException {
68     // Step 1: Locate the end-of-central-directory record header.
69     long offsetOfEocd = MinimalZipParser.locateStartOfEocd(in, 32768);
70     if (offsetOfEocd == -1) {
71       // Archive is weird, abort.
72       throw new ZipException("EOCD record not found in last 32k of archive, giving up");
73     }
74 
75     // Step 2: Parse the end-of-central-directory data to locate the central directory itself
76     in.setRange(offsetOfEocd, in.length() - offsetOfEocd);
77     MinimalCentralDirectoryMetadata centralDirectoryMetadata = MinimalZipParser.parseEocd(in);
78 
79     // Step 3: Extract a list of all central directory entries (contiguous data stream)
80     in.setRange(
81         centralDirectoryMetadata.getOffsetOfCentralDirectory(),
82         centralDirectoryMetadata.getLengthOfCentralDirectory());
83     List<MinimalZipEntry> minimalZipEntries =
84         new ArrayList<MinimalZipEntry>(centralDirectoryMetadata.getNumEntriesInCentralDirectory());
85     for (int x = 0; x < centralDirectoryMetadata.getNumEntriesInCentralDirectory(); x++) {
86       minimalZipEntries.add(MinimalZipParser.parseCentralDirectoryEntry(in));
87     }
88 
89     // Step 4: Sort the entries in file order, not central directory order.
90     Collections.sort(minimalZipEntries, LOCAL_ENTRY_OFFSET_COMAPRATOR);
91 
92     // Step 5: Seek out each local entry and calculate the offset of the compressed data within
93     for (int x = 0; x < minimalZipEntries.size(); x++) {
94       MinimalZipEntry entry = minimalZipEntries.get(x);
95       long offsetOfNextEntry;
96       if (x < minimalZipEntries.size() - 1) {
97         // Don't allow reading past the start of the next entry, for sanity.
98         offsetOfNextEntry = minimalZipEntries.get(x + 1).getFileOffsetOfLocalEntry();
99       } else {
100         // Last entry. Don't allow reading into the central directory, for sanity.
101         offsetOfNextEntry = centralDirectoryMetadata.getOffsetOfCentralDirectory();
102       }
103       long rangeLength = offsetOfNextEntry - entry.getFileOffsetOfLocalEntry();
104       in.setRange(entry.getFileOffsetOfLocalEntry(), rangeLength);
105       long relativeDataOffset = MinimalZipParser.parseLocalEntryAndGetCompressedDataOffset(in);
106       entry.setFileOffsetOfCompressedData(entry.getFileOffsetOfLocalEntry() + relativeDataOffset);
107     }
108 
109     // Done!
110     return minimalZipEntries;
111   }
112 }
113