1 /* 2 * Copyright (C) 2007 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.dx.cf.direct; 18 19 import com.android.dex.util.FileUtils; 20 import java.io.ByteArrayOutputStream; 21 import java.io.File; 22 import java.io.IOException; 23 import java.io.InputStream; 24 import java.util.ArrayList; 25 import java.util.Arrays; 26 import java.util.Collections; 27 import java.util.Comparator; 28 import java.util.zip.ZipEntry; 29 import java.util.zip.ZipFile; 30 31 /** 32 * Opens all the class files found in a class path element. Path elements 33 * can point to class files, {jar,zip,apk} files, or directories containing 34 * class files. 35 */ 36 public class ClassPathOpener { 37 38 /** {@code non-null;} pathname to start with */ 39 private final String pathname; 40 /** {@code non-null;} callback interface */ 41 private final Consumer consumer; 42 /** 43 * If true, sort such that classes appear before their inner 44 * classes and "package-info" occurs before all other classes in that 45 * package. 46 */ 47 private final boolean sort; 48 private FileNameFilter filter; 49 50 /** 51 * Callback interface for {@code ClassOpener}. 52 */ 53 public interface Consumer { 54 55 /** 56 * Provides the file name and byte array for a class path element. 57 * 58 * @param name {@code non-null;} filename of element. May not be a valid 59 * filesystem path. 60 * 61 * @param lastModified milliseconds since 1970-Jan-1 00:00:00 GMT 62 * @param bytes {@code non-null;} file data 63 * @return true on success. Result is or'd with all other results 64 * from {@code processFileBytes} and returned to the caller 65 * of {@code process()}. 66 */ processFileBytes(String name, long lastModified, byte[] bytes)67 boolean processFileBytes(String name, long lastModified, byte[] bytes); 68 69 /** 70 * Informs consumer that an exception occurred while processing 71 * this path element. Processing will continue if possible. 72 * 73 * @param ex {@code non-null;} exception 74 */ onException(Exception ex)75 void onException(Exception ex); 76 77 /** 78 * Informs consumer that processing of an archive file has begun. 79 * 80 * @param file {@code non-null;} archive file being processed 81 */ onProcessArchiveStart(File file)82 void onProcessArchiveStart(File file); 83 } 84 85 /** 86 * Filter interface for {@code ClassOpener}. 87 */ 88 public interface FileNameFilter { 89 accept(String path)90 boolean accept(String path); 91 } 92 93 /** 94 * An accept all filter. 95 */ 96 public static final FileNameFilter acceptAll = new FileNameFilter() { 97 98 @Override 99 public boolean accept(String path) { 100 return true; 101 } 102 }; 103 104 /** 105 * Constructs an instance. 106 * 107 * @param pathname {@code non-null;} path element to process 108 * @param sort if true, sort such that classes appear before their inner 109 * classes and "package-info" occurs before all other classes in that 110 * package. 111 * @param consumer {@code non-null;} callback interface 112 */ ClassPathOpener(String pathname, boolean sort, Consumer consumer)113 public ClassPathOpener(String pathname, boolean sort, Consumer consumer) { 114 this(pathname, sort, acceptAll, consumer); 115 } 116 117 /** 118 * Constructs an instance. 119 * 120 * @param pathname {@code non-null;} path element to process 121 * @param sort if true, sort such that classes appear before their inner 122 * classes and "package-info" occurs before all other classes in that 123 * package. 124 * @param consumer {@code non-null;} callback interface 125 */ ClassPathOpener(String pathname, boolean sort, FileNameFilter filter, Consumer consumer)126 public ClassPathOpener(String pathname, boolean sort, FileNameFilter filter, 127 Consumer consumer) { 128 this.pathname = pathname; 129 this.sort = sort; 130 this.consumer = consumer; 131 this.filter = filter; 132 } 133 134 /** 135 * Processes a path element. 136 * 137 * @return the OR of all return values 138 * from {@code Consumer.processFileBytes()}. 139 */ process()140 public boolean process() { 141 File file = new File(pathname); 142 143 return processOne(file, true); 144 } 145 146 /** 147 * Processes one file. 148 * 149 * @param file {@code non-null;} the file to process 150 * @param topLevel whether this is a top-level file (that is, 151 * specified directly on the commandline) 152 * @return whether any processing actually happened 153 */ processOne(File file, boolean topLevel)154 private boolean processOne(File file, boolean topLevel) { 155 try { 156 if (file.isDirectory()) { 157 return processDirectory(file, topLevel); 158 } 159 160 String path = file.getPath(); 161 162 if (path.endsWith(".zip") || 163 path.endsWith(".jar") || 164 path.endsWith(".apk")) { 165 return processArchive(file); 166 } 167 if (filter.accept(path)) { 168 byte[] bytes = FileUtils.readFile(file); 169 return consumer.processFileBytes(path, file.lastModified(), bytes); 170 } else { 171 return false; 172 } 173 } catch (Exception ex) { 174 consumer.onException(ex); 175 return false; 176 } 177 } 178 179 /** 180 * Sorts java class names such that outer classes preceed their inner 181 * classes and "package-info" preceeds all other classes in its package. 182 * 183 * @param a {@code non-null;} first class name 184 * @param b {@code non-null;} second class name 185 * @return {@code compareTo()}-style result 186 */ compareClassNames(String a, String b)187 private static int compareClassNames(String a, String b) { 188 // Ensure inner classes sort second 189 a = a.replace('$','0'); 190 b = b.replace('$','0'); 191 192 /* 193 * Assuming "package-info" only occurs at the end, ensures package-info 194 * sorts first. 195 */ 196 a = a.replace("package-info", ""); 197 b = b.replace("package-info", ""); 198 199 return a.compareTo(b); 200 } 201 202 /** 203 * Processes a directory recursively. 204 * 205 * @param dir {@code non-null;} file representing the directory 206 * @param topLevel whether this is a top-level directory (that is, 207 * specified directly on the commandline) 208 * @return whether any processing actually happened 209 */ processDirectory(File dir, boolean topLevel)210 private boolean processDirectory(File dir, boolean topLevel) { 211 if (topLevel) { 212 dir = new File(dir, "."); 213 } 214 215 File[] files = dir.listFiles(); 216 int len = files.length; 217 boolean any = false; 218 219 if (sort) { 220 Arrays.sort(files, new Comparator<File>() { 221 @Override 222 public int compare(File a, File b) { 223 return compareClassNames(a.getName(), b.getName()); 224 } 225 }); 226 } 227 228 for (int i = 0; i < len; i++) { 229 any |= processOne(files[i], false); 230 } 231 232 return any; 233 } 234 235 /** 236 * Processes the contents of an archive ({@code .zip}, 237 * {@code .jar}, or {@code .apk}). 238 * 239 * @param file {@code non-null;} archive file to process 240 * @return whether any processing actually happened 241 * @throws IOException on i/o problem 242 */ processArchive(File file)243 private boolean processArchive(File file) throws IOException { 244 ZipFile zip = new ZipFile(file); 245 246 ArrayList<? extends java.util.zip.ZipEntry> entriesList 247 = Collections.list(zip.entries()); 248 249 if (sort) { 250 Collections.sort(entriesList, new Comparator<ZipEntry>() { 251 @Override 252 public int compare (ZipEntry a, ZipEntry b) { 253 return compareClassNames(a.getName(), b.getName()); 254 } 255 }); 256 } 257 258 consumer.onProcessArchiveStart(file); 259 260 ByteArrayOutputStream baos = new ByteArrayOutputStream(40000); 261 byte[] buf = new byte[20000]; 262 boolean any = false; 263 264 for (ZipEntry one : entriesList) { 265 final boolean isDirectory = one.isDirectory(); 266 267 String path = one.getName(); 268 if (filter.accept(path)) { 269 final byte[] bytes; 270 if (!isDirectory) { 271 InputStream in = zip.getInputStream(one); 272 273 baos.reset(); 274 int read; 275 while ((read = in.read(buf)) != -1) { 276 baos.write(buf, 0, read); 277 } 278 279 in.close(); 280 bytes = baos.toByteArray(); 281 } else { 282 bytes = new byte[0]; 283 } 284 285 any |= consumer.processFileBytes(path, one.getTime(), bytes); 286 } 287 } 288 289 zip.close(); 290 return any; 291 } 292 } 293