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