1 /* 2 * Copyright (C) 2013 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 package com.android.launcher3; 17 18 import com.android.launcher3.backup.BackupProtos.CheckedMessage; 19 import com.android.launcher3.backup.BackupProtos.Favorite; 20 import com.android.launcher3.backup.BackupProtos.Key; 21 import com.android.launcher3.backup.BackupProtos.Journal; 22 import com.android.launcher3.backup.BackupProtos.Resource; 23 import com.android.launcher3.backup.BackupProtos.Screen; 24 import com.android.launcher3.backup.BackupProtos.Widget; 25 26 import com.google.protobuf.nano.InvalidProtocolBufferNanoException; 27 import com.google.protobuf.nano.MessageNano; 28 29 import java.io.BufferedInputStream; 30 import java.io.ByteArrayOutputStream; 31 import java.io.File; 32 import java.io.FileInputStream; 33 import java.io.FileNotFoundException; 34 import java.io.FileOutputStream; 35 import java.io.IOException; 36 import java.lang.System; 37 import java.util.LinkedList; 38 import java.util.List; 39 import java.util.zip.CRC32; 40 41 import javax.xml.bind.DatatypeConverter; 42 43 44 /** 45 * Commandline utility for decoding Launcher3 backup protocol buffers. 46 * 47 * <P>When using com.android.internal.backup.LocalTransport, the file names are base64-encoded Key 48 * protocol buffers with a prefix, that have been base64-encoded again by the transport: 49 * <pre> 50 * echo "TDpDQUlnL0pxVTVnOD0=" | launcher_protoutil -k 51 * </pre> 52 * 53 * <P>This tool understands these file names and will use the embedded Key to detect the type and 54 * extract the payload automatically: 55 * <pre> 56 * launcher_protoutil /tmp/TDpDQUlnL0pxVTVnOD0= 57 * </pre> 58 * 59 * <P>With payload debugging enabled, base64-encoded protocol buffers will be written to the logs. 60 * Copy the encoded snippet from the log, and specify the type explicitly, with the Logs flags: 61 * <pre> 62 * echo "CAEYLiCJ9JKsDw==" | launcher_protoutil -L -k 63 * </pre> 64 * For backup payloads it is more convenient to copy the log snippet to a file: 65 * <pre> 66 * launcher_protoutil -L -f favorite.log 67 * </pre> 68 */ 69 class DecoderRing { 70 71 public static final String STANDARD_IN = "**stdin**"; 72 73 private static Class[] TYPES = { 74 Key.class, 75 Favorite.class, 76 Screen.class, 77 Resource.class, 78 Widget.class 79 }; 80 static final int ICON_TYPE_BITMAP = 1; 81 main(String[ ] args)82 public static void main(String[ ] args) 83 throws Exception { 84 Class defaultType = null; 85 boolean extractImages = false; 86 boolean fromLogs = false; 87 int skip = 0; 88 List<File> files = new LinkedList<File>(); 89 boolean verbose = false; 90 91 for (int i = 0; i < args.length; i++) { 92 if ("-k".equals(args[i])) { 93 defaultType = Key.class; 94 } else if ("-f".equals(args[i])) { 95 defaultType = Favorite.class; 96 } else if ("-j".equals(args[i])) { 97 defaultType = Journal.class; 98 } else if ("-i".equals(args[i])) { 99 defaultType = Resource.class; 100 } else if ("-s".equals(args[i])) { 101 defaultType = Screen.class; 102 } else if ("-w".equals(args[i])) { 103 defaultType = Widget.class; 104 } else if ("-S".equals(args[i])) { 105 if ((i + 1) < args.length) { 106 skip = Integer.valueOf(args[++i]); 107 } else { 108 usage(args); 109 } 110 } else if ("-x".equals(args[i])) { 111 extractImages = true; 112 } else if ("-v".equals(args[i])) { 113 verbose = true; 114 } else if ("-L".equals(args[i])) { 115 fromLogs = true; 116 } else if (args[i] != null && !args[i].startsWith("-")) { 117 files.add(new File(args[i])); 118 } else { 119 System.err.println("Unsupported flag: " + args[i]); 120 usage(args); 121 } 122 } 123 124 if (defaultType == null && files.isEmpty()) { 125 // can't infer file type without the key 126 usage(args); 127 } 128 129 if (files.size() > 1 && defaultType != null) { 130 System.err.println("Explicit type ignored for multiple files."); 131 defaultType = null; 132 } 133 134 if (files.isEmpty()) { 135 files.add(new File(STANDARD_IN)); 136 } 137 138 for (File source : files) { 139 Class type = null; 140 if (defaultType == null) { 141 Key key = decodeKey(source.getName().getBytes(), fromLogs); 142 if (key != null) { 143 type = TYPES[key.type]; 144 if (verbose) { 145 System.err.println(source.getName() + " is a " + type.getSimpleName()); 146 System.out.println(key.toString()); 147 } 148 } 149 } else { 150 type = defaultType; 151 } 152 153 // read in the bytes 154 ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); 155 BufferedInputStream input = null; 156 if (source.getName() == STANDARD_IN) { 157 input = new BufferedInputStream(System.in); 158 } else { 159 try { 160 input = new BufferedInputStream(new FileInputStream(source)); 161 } catch (FileNotFoundException e) { 162 System.err.println("failed to open file: " + source + ", " + e); 163 System.exit(1); 164 } 165 } 166 byte[] buffer = new byte[1024]; 167 try { 168 while (input.available() > 0) { 169 int n = input.read(buffer); 170 int offset = 0; 171 if (skip > 0) { 172 offset = Math.min(skip, n); 173 n -= offset; 174 skip -= offset; 175 } 176 if (n > 0) { 177 byteStream.write(buffer, offset, n); 178 } 179 } 180 } catch (IOException e) { 181 System.err.println("failed to read input: " + e); 182 System.exit(1); 183 } 184 185 MessageNano proto = null; 186 byte[] payload = byteStream.toByteArray(); 187 if (type == Key.class) { 188 proto = decodeKey(payload, fromLogs); 189 } else if (type != null) { 190 proto = decodeBackupData(payload, type, fromLogs); 191 } 192 193 // Generic string output 194 if (proto != null) { 195 System.out.println(proto.toString()); 196 } 197 198 if (extractImages) { 199 String prefix = "stdin"; 200 if (source != null) { 201 prefix = source.getName(); 202 } 203 // save off the icon bits in a file for inspection 204 if (proto instanceof Resource) { 205 Resource icon = (Resource) proto; 206 writeImageData(icon.data, prefix + ".png"); 207 } 208 209 // save off the icon bits in a file for inspection 210 if (proto instanceof Favorite) { 211 Favorite favorite = (Favorite) proto; 212 if (favorite.iconType == ICON_TYPE_BITMAP) { 213 writeImageData(favorite.icon, prefix + ".png"); 214 } 215 } 216 217 // save off the widget icon and preview bits in files for inspection 218 if (proto instanceof Widget) { 219 Widget widget = (Widget) proto; 220 if (widget.icon != null) { 221 writeImageData(widget.icon.data, prefix + "_icon.png"); 222 } 223 if (widget.preview != null) { 224 writeImageData(widget.preview.data, prefix + "_preview.png"); 225 } 226 } 227 } 228 } 229 System.exit(0); 230 } 231 232 // In logcat, backup data is base64 encoded, but in localtransport files it is raw decodeBackupData(byte[] payload, Class type, boolean fromLogs)233 private static MessageNano decodeBackupData(byte[] payload, Class type, boolean fromLogs) 234 throws InstantiationException, IllegalAccessException { 235 MessageNano proto;// other types are wrapped in a checksum message 236 CheckedMessage wrapper = new CheckedMessage(); 237 try { 238 if (fromLogs) { 239 payload = DatatypeConverter.parseBase64Binary(new String(payload)); 240 } 241 MessageNano.mergeFrom(wrapper, payload); 242 } catch (InvalidProtocolBufferNanoException e) { 243 System.err.println("failed to parse wrapper: " + e); 244 System.exit(1); 245 } 246 247 CRC32 checksum = new CRC32(); 248 checksum.update(wrapper.payload); 249 if (wrapper.checksum != checksum.getValue()) { 250 System.err.println("wrapper checksum failed"); 251 System.exit(1); 252 } 253 254 // decode the actual message 255 proto = (MessageNano) type.newInstance(); 256 try { 257 MessageNano.mergeFrom(proto, wrapper.payload); 258 } catch (InvalidProtocolBufferNanoException e) { 259 System.err.println("failed to parse proto: " + e); 260 System.exit(1); 261 } 262 return proto; 263 } 264 265 // In logcat, keys are base64 encoded with no prefix. 266 // The localtransport adds a prefix and the base64 encodes the whole thing again. decodeKey(byte[] payload, boolean fromLogs)267 private static Key decodeKey(byte[] payload, boolean fromLogs) { 268 Key key = new Key(); 269 try { 270 String encodedKey = new String(payload); 271 if (!fromLogs) { 272 byte[] rawKey = DatatypeConverter.parseBase64Binary(encodedKey); 273 if (rawKey[0] != 'L' || rawKey[1] != ':') { 274 System.err.println(encodedKey + " is not a launcher backup key."); 275 return null; 276 } 277 encodedKey = new String(rawKey, 2, rawKey.length - 2); 278 } 279 byte[] keyProtoData = DatatypeConverter.parseBase64Binary(encodedKey); 280 key = Key.parseFrom(keyProtoData); 281 } catch (InvalidProtocolBufferNanoException protoException) { 282 System.err.println("failed to extract key from filename: " + protoException); 283 return null; 284 } catch (IllegalArgumentException base64Exception) { 285 System.err.println("failed to extract key from filename: " + base64Exception); 286 return null; 287 } 288 289 // keys are self-checked 290 if (key.checksum != checkKey(key)) { 291 System.err.println("key ckecksum failed"); 292 return null; 293 } 294 return key; 295 } 296 writeImageData(byte[] data, String path)297 private static void writeImageData(byte[] data, String path) { 298 FileOutputStream iconFile = null; 299 try { 300 iconFile = new FileOutputStream(path); 301 iconFile.write(data); 302 System.err.println("wrote " + path); 303 } catch (IOException e) { 304 System.err.println("failed to write image file: " + e); 305 } finally { 306 if (iconFile != null) { 307 try { 308 iconFile.close(); 309 } catch (IOException e) { 310 System.err.println("failed to close the image file: " + e); 311 } 312 } 313 } 314 } 315 checkKey(Key key)316 private static long checkKey(Key key) { 317 CRC32 checksum = new CRC32(); 318 checksum.update(key.type); 319 checksum.update((int) (key.id & 0xffff)); 320 checksum.update((int) ((key.id >> 32) & 0xffff)); 321 if (key.name != null && key.name.length() > 0) { 322 checksum.update(key.name.getBytes()); 323 } 324 return checksum.getValue(); 325 } 326 usage(String[] args)327 private static void usage(String[] args) { 328 System.err.println("launcher_protoutil [-x] [-S b] [-k|-f|-i|-s|-w] [filename]"); 329 System.err.println("\t-k\tdecode a key"); 330 System.err.println("\t-f\tdecode a favorite"); 331 System.err.println("\t-i\tdecode a icon"); 332 System.err.println("\t-s\tdecode a screen"); 333 System.err.println("\t-w\tdecode a widget"); 334 System.err.println("\t-S b\tskip b bytes"); 335 System.err.println("\t-x\textract image data to files"); 336 System.err.println("\t-v\tprint key type data, as well as payload"); 337 System.err.println("\t-l\texpect data from logcat, instead of the local transport"); 338 System.err.println("\tfilename\tread from filename, not stdin"); 339 System.exit(1); 340 } 341 }