• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.fastdeploy;
18 
19 import java.io.File;
20 import java.io.FileInputStream;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.OutputStream;
24 import java.lang.StringBuilder;
25 import java.util.Collections;
26 import java.util.Comparator;
27 import java.util.List;
28 import java.util.ArrayList;
29 
30 import java.nio.charset.StandardCharsets;
31 import static java.nio.charset.StandardCharsets.UTF_8;
32 
33 import java.util.Comparator;
34 import java.util.Iterator;
35 import java.util.List;
36 import java.util.AbstractMap.SimpleEntry;
37 
38 import com.android.fastdeploy.APKMetaData;
39 import com.android.fastdeploy.APKEntry;
40 
41 public final class DeployPatchGenerator {
42     private static final int BUFFER_SIZE = 128 * 1024;
43 
main(String[] args)44     public static void main(String[] args) {
45         try {
46             if (args.length < 2) {
47                 showUsage(0);
48             }
49 
50             boolean verbose = false;
51             if (args.length > 2) {
52                 String verboseFlag = args[2];
53                 if (verboseFlag.compareTo("--verbose") == 0) {
54                     verbose = true;
55                 }
56             }
57 
58             StringBuilder sb = null;
59             String apkPath = args[0];
60             String deviceMetadataPath = args[1];
61             File hostFile = new File(apkPath);
62 
63             List<APKEntry> deviceZipEntries = getMetadataFromFile(deviceMetadataPath);
64             System.err.println("Device Entries (" + deviceZipEntries.size() + ")");
65             if (verbose) {
66                 sb = new StringBuilder();
67                 for (APKEntry entry : deviceZipEntries) {
68                     APKEntryToString(entry, sb);
69                 }
70                 System.err.println(sb.toString());
71             }
72 
73             List<APKEntry> hostFileEntries = PatchUtils.getAPKMetaData(hostFile).getEntriesList();
74             System.err.println("Host Entries (" + hostFileEntries.size() + ")");
75             if (verbose) {
76                 sb = new StringBuilder();
77                 for (APKEntry entry : hostFileEntries) {
78                     APKEntryToString(entry, sb);
79                 }
80                 System.err.println(sb.toString());
81             }
82 
83             List<SimpleEntry<APKEntry, APKEntry>> identicalContentsEntrySet =
84                 getIdenticalContents(deviceZipEntries, hostFileEntries);
85             reportIdenticalContents(identicalContentsEntrySet, hostFile);
86 
87             if (verbose) {
88                 sb = new StringBuilder();
89                 for (SimpleEntry<APKEntry, APKEntry> identicalEntry : identicalContentsEntrySet) {
90                     APKEntry entry = identicalEntry.getValue();
91                     APKEntryToString(entry, sb);
92                 }
93                 System.err.println("Identical Entries (" + identicalContentsEntrySet.size() + ")");
94                 System.err.println(sb.toString());
95             }
96 
97             createPatch(identicalContentsEntrySet, hostFile, System.out);
98         } catch (Exception e) {
99             System.err.println("Error: " + e);
100             e.printStackTrace();
101             System.exit(2);
102         }
103         System.exit(0);
104     }
105 
showUsage(int exitCode)106     private static void showUsage(int exitCode) {
107         System.err.println("usage: deploypatchgenerator <apkpath> <deviceapkmetadata> [--verbose]");
108         System.err.println("");
109         System.exit(exitCode);
110     }
111 
APKEntryToString(APKEntry entry, StringBuilder outputString)112     private static void APKEntryToString(APKEntry entry, StringBuilder outputString) {
113         outputString.append(String.format("Filename: %s\n", entry.getFileName()));
114         outputString.append(String.format("CRC32: 0x%08X\n", entry.getCrc32()));
115         outputString.append(String.format("Data Offset: %d\n", entry.getDataOffset()));
116         outputString.append(String.format("Compressed Size: %d\n", entry.getCompressedSize()));
117         outputString.append(String.format("Uncompressed Size: %d\n", entry.getUncompressedSize()));
118     }
119 
getMetadataFromFile(String deviceMetadataPath)120     private static List<APKEntry> getMetadataFromFile(String deviceMetadataPath) throws IOException {
121         InputStream is = new FileInputStream(new File(deviceMetadataPath));
122         APKMetaData apkMetaData = APKMetaData.parseDelimitedFrom(is);
123         return apkMetaData.getEntriesList();
124     }
125 
getIdenticalContents( List<APKEntry> deviceZipEntries, List<APKEntry> hostZipEntries)126     private static List<SimpleEntry<APKEntry, APKEntry>> getIdenticalContents(
127         List<APKEntry> deviceZipEntries, List<APKEntry> hostZipEntries) throws IOException {
128         List<SimpleEntry<APKEntry, APKEntry>> identicalContents =
129             new ArrayList<SimpleEntry<APKEntry, APKEntry>>();
130 
131         for (APKEntry deviceZipEntry : deviceZipEntries) {
132             for (APKEntry hostZipEntry : hostZipEntries) {
133                 if (deviceZipEntry.getCrc32() == hostZipEntry.getCrc32() &&
134                     deviceZipEntry.getFileName().equals(hostZipEntry.getFileName())) {
135                     identicalContents.add(new SimpleEntry(deviceZipEntry, hostZipEntry));
136                 }
137             }
138         }
139 
140         Collections.sort(identicalContents, new Comparator<SimpleEntry<APKEntry, APKEntry>>() {
141             @Override
142             public int compare(
143                 SimpleEntry<APKEntry, APKEntry> p1, SimpleEntry<APKEntry, APKEntry> p2) {
144                 return Long.compare(p1.getValue().getDataOffset(), p2.getValue().getDataOffset());
145             }
146         });
147 
148         return identicalContents;
149     }
150 
reportIdenticalContents( List<SimpleEntry<APKEntry, APKEntry>> identicalContentsEntrySet, File hostFile)151     private static void reportIdenticalContents(
152         List<SimpleEntry<APKEntry, APKEntry>> identicalContentsEntrySet, File hostFile)
153         throws IOException {
154         long totalEqualBytes = 0;
155         int totalEqualFiles = 0;
156         for (SimpleEntry<APKEntry, APKEntry> entries : identicalContentsEntrySet) {
157             APKEntry hostAPKEntry = entries.getValue();
158             totalEqualBytes += hostAPKEntry.getCompressedSize();
159             totalEqualFiles++;
160         }
161 
162         float savingPercent = (float) (totalEqualBytes * 100) / hostFile.length();
163 
164         System.err.println("Detected " + totalEqualFiles + " equal APK entries");
165         System.err.println(totalEqualBytes + " bytes are equal out of " + hostFile.length() + " ("
166             + savingPercent + "%)");
167     }
168 
createPatch(List<SimpleEntry<APKEntry, APKEntry>> zipEntrySimpleEntrys, File hostFile, OutputStream patchStream)169     static void createPatch(List<SimpleEntry<APKEntry, APKEntry>> zipEntrySimpleEntrys,
170         File hostFile, OutputStream patchStream) throws IOException, PatchFormatException {
171         FileInputStream hostFileInputStream = new FileInputStream(hostFile);
172 
173         patchStream.write(PatchUtils.SIGNATURE.getBytes(StandardCharsets.US_ASCII));
174         PatchUtils.writeFormattedLong(hostFile.length(), patchStream);
175 
176         byte[] buffer = new byte[BUFFER_SIZE];
177         long totalBytesWritten = 0;
178         Iterator<SimpleEntry<APKEntry, APKEntry>> entrySimpleEntryIterator =
179             zipEntrySimpleEntrys.iterator();
180         while (entrySimpleEntryIterator.hasNext()) {
181             SimpleEntry<APKEntry, APKEntry> entrySimpleEntry = entrySimpleEntryIterator.next();
182             APKEntry deviceAPKEntry = entrySimpleEntry.getKey();
183             APKEntry hostAPKEntry = entrySimpleEntry.getValue();
184 
185             long newDataLen = hostAPKEntry.getDataOffset() - totalBytesWritten;
186             long oldDataOffset = deviceAPKEntry.getDataOffset();
187             long oldDataLen = deviceAPKEntry.getCompressedSize();
188 
189             PatchUtils.writeFormattedLong(newDataLen, patchStream);
190             PatchUtils.pipe(hostFileInputStream, patchStream, buffer, newDataLen);
191             PatchUtils.writeFormattedLong(oldDataOffset, patchStream);
192             PatchUtils.writeFormattedLong(oldDataLen, patchStream);
193 
194             long skip = hostFileInputStream.skip(oldDataLen);
195             if (skip != oldDataLen) {
196                 throw new PatchFormatException("skip error: attempted to skip " + oldDataLen
197                     + " bytes but return code was " + skip);
198             }
199             totalBytesWritten += oldDataLen + newDataLen;
200         }
201         long remainderLen = hostFile.length() - totalBytesWritten;
202         PatchUtils.writeFormattedLong(remainderLen, patchStream);
203         PatchUtils.pipe(hostFileInputStream, patchStream, buffer, remainderLen);
204         PatchUtils.writeFormattedLong(0, patchStream);
205         PatchUtils.writeFormattedLong(0, patchStream);
206         patchStream.flush();
207     }
208 }
209