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