1 package org.robolectric.res.android; 2 3 import static java.nio.charset.StandardCharsets.ISO_8859_1; 4 import static java.nio.charset.StandardCharsets.UTF_8; 5 import static org.robolectric.res.android.Asset.toIntExact; 6 import static org.robolectric.res.android.Util.ALOGV; 7 8 import com.google.common.collect.ImmutableMap; 9 import com.google.common.primitives.Ints; 10 import com.google.common.primitives.Longs; 11 import com.google.common.primitives.Shorts; 12 import java.io.File; 13 import java.io.FileInputStream; 14 import java.io.IOException; 15 import java.io.InputStream; 16 import java.io.RandomAccessFile; 17 import java.nio.charset.Charset; 18 import java.util.zip.ZipEntry; 19 import java.util.zip.ZipException; 20 import java.util.zip.ZipFile; 21 22 public class FileMap { 23 24 /** ZIP archive central directory end header signature. */ 25 private static final int ENDSIG = 0x6054b50; 26 27 private static final int EOCD_SIZE = 22; 28 29 private static final int ZIP64_EOCD_SIZE = 56; 30 31 private static final int ZIP64_EOCD_LOCATOR_SIZE = 20; 32 33 /** ZIP64 archive central directory end header signature. */ 34 private static final int ENDSIG64 = 0x6064b50; 35 36 private static final int MAX_COMMENT_SIZE = 64 * 1024; // 64k 37 38 /** the maximum size of the end of central directory sections in bytes */ 39 private static final int MAXIMUM_ZIP_EOCD_SIZE = 40 MAX_COMMENT_SIZE + EOCD_SIZE + ZIP64_EOCD_SIZE + ZIP64_EOCD_LOCATOR_SIZE; 41 42 private ZipFile zipFile; 43 private ZipEntry zipEntry; 44 45 @SuppressWarnings("unused") 46 private boolean readOnly; 47 48 private int fd; 49 private boolean isFromZip; 50 51 // Create a new mapping on an open file. 52 // 53 // Closing the file descriptor does not unmap the pages, so we don't 54 // claim ownership of the fd. 55 // 56 // Returns "false" on failure. create(String origFileName, int fd, long offset, int length, boolean readOnly)57 boolean create(String origFileName, int fd, long offset, int length, boolean readOnly) { 58 this.mFileName = origFileName; 59 this.fd = fd; 60 this.mDataOffset = offset; 61 this.readOnly = readOnly; 62 return true; 63 } 64 65 // #if defined(__MINGW32__) 66 // int adjust; 67 // off64_t adjOffset; 68 // size_t adjLength; 69 // 70 // if (mPageSize == -1) { 71 // SYSTEM_INFO si; 72 // 73 // GetSystemInfo( &si ); 74 // mPageSize = si.dwAllocationGranularity; 75 // } 76 // 77 // DWORD protect = readOnly ? PAGE_READONLY : PAGE_READWRITE; 78 // 79 // mFileHandle = (HANDLE) _get_osfhandle(fd); 80 // mFileMapping = CreateFileMapping( mFileHandle, NULL, protect, 0, 0, NULL); 81 // if (mFileMapping == NULL) { 82 // ALOGE("CreateFileMapping(%s, %" PRIx32 ") failed with error %" PRId32 "\n", 83 // mFileHandle, protect, GetLastError() ); 84 // return false; 85 // } 86 // 87 // adjust = offset % mPageSize; 88 // adjOffset = offset - adjust; 89 // adjLength = length + adjust; 90 // 91 // mBasePtr = MapViewOfFile( mFileMapping, 92 // readOnly ? FILE_MAP_READ : FILE_MAP_ALL_ACCESS, 93 // 0, 94 // (DWORD)(adjOffset), 95 // adjLength ); 96 // if (mBasePtr == NULL) { 97 // ALOGE("MapViewOfFile(%" PRId64 ", 0x%x) failed with error %" PRId32 "\n", 98 // adjOffset, adjLength, GetLastError() ); 99 // CloseHandle(mFileMapping); 100 // mFileMapping = INVALID_HANDLE_VALUE; 101 // return false; 102 // } 103 // #else // !defined(__MINGW32__) 104 // int prot, flags, adjust; 105 // off64_t adjOffset; 106 // size_t adjLength; 107 // 108 // void* ptr; 109 // 110 // assert(fd >= 0); 111 // assert(offset >= 0); 112 // assert(length > 0); 113 // 114 // // init on first use 115 // if (mPageSize == -1) { 116 // mPageSize = sysconf(_SC_PAGESIZE); 117 // if (mPageSize == -1) { 118 // ALOGE("could not get _SC_PAGESIZE\n"); 119 // return false; 120 // } 121 // } 122 // 123 // adjust = offset % mPageSize; 124 // adjOffset = offset - adjust; 125 // adjLength = length + adjust; 126 // 127 // flags = MAP_SHARED; 128 // prot = PROT_READ; 129 // if (!readOnly) 130 // prot |= PROT_WRITE; 131 // 132 // ptr = mmap(NULL, adjLength, prot, flags, fd, adjOffset); 133 // if (ptr == MAP_FAILED) { 134 // ALOGE("mmap(%lld,0x%x) failed: %s\n", 135 // (long long)adjOffset, adjLength, strerror(errno)); 136 // return false; 137 // } 138 // mBasePtr = ptr; 139 // #endif // !defined(__MINGW32__) 140 // 141 // mFileName = origFileName != NULL ? strdup(origFileName) : NULL; 142 // mBaseLength = adjLength; 143 // mDataOffset = offset; 144 // mDataPtr = (char*) mBasePtr + adjust; 145 // mDataLength = length; 146 // 147 // assert(mBasePtr != NULL); 148 // 149 // ALOGV("MAP: base %s/0x%x data %s/0x%x\n", 150 // mBasePtr, mBaseLength, mDataPtr, mDataLength); 151 // 152 // return true; 153 // } 154 createFromZip( String origFileName, ZipFile zipFile, ZipEntry entry, long offset, int length, boolean readOnly)155 boolean createFromZip( 156 String origFileName, 157 ZipFile zipFile, 158 ZipEntry entry, 159 long offset, 160 int length, 161 boolean readOnly) { 162 isFromZip = true; 163 this.zipFile = zipFile; 164 this.zipEntry = entry; 165 166 assert (fd >= 0); 167 assert (offset >= 0); 168 // assert(length > 0); 169 170 // init on first use 171 // if (mPageSize == -1) { 172 // mPageSize = sysconf(_SC_PAGESIZE); 173 // if (mPageSize == -1) { 174 // ALOGE("could not get _SC_PAGESIZE\n"); 175 // return false; 176 // } 177 // } 178 179 // adjust = Math.toIntExact(offset % mPageSize); 180 // adjOffset = offset - adjust; 181 // adjLength = length + adjust; 182 183 // flags = MAP_SHARED; 184 // prot = PROT_READ; 185 // if (!readOnly) 186 // prot |= PROT_WRITE; 187 188 // ptr = mmap(null, adjLength, prot, flags, fd, adjOffset); 189 // if (ptr == MAP_FAILED) { 190 // ALOGE("mmap(%lld,0x%x) failed: %s\n", 191 // (long long)adjOffset, adjLength, strerror(errno)); 192 // return false; 193 // } 194 // mBasePtr = ptr; 195 196 mFileName = origFileName != null ? origFileName : null; 197 // mBaseLength = adjLength; 198 mDataOffset = offset; 199 // mDataPtr = mBasePtr + adjust; 200 mDataLength = toIntExact(entry.getSize()); 201 202 // assert(mBasePtr != 0); 203 204 ALOGV("MAP: base %s/0x%x data %s/0x%x\n", mBasePtr, mBaseLength, mDataPtr, mDataLength); 205 206 return true; 207 } 208 guessDataOffsets(File zipFile, int length)209 static ImmutableMap<String, Long> guessDataOffsets(File zipFile, int length) { 210 ImmutableMap.Builder<String, Long> result = ImmutableMap.builder(); 211 212 // Parse the zip file entry offsets from the central directory section. 213 // See https://en.wikipedia.org/wiki/Zip_(file_format) 214 215 try (RandomAccessFile randomAccessFile = new RandomAccessFile(zipFile, "r")) { 216 217 // First read the 'end of central directory record' in order to find the start of the central 218 // directory 219 int endOfCdSize = Math.min(MAXIMUM_ZIP_EOCD_SIZE, length); 220 int endofCdOffset = length - endOfCdSize; 221 randomAccessFile.seek(endofCdOffset); 222 byte[] buffer = new byte[endOfCdSize]; 223 randomAccessFile.readFully(buffer); 224 225 int centralDirOffset = findCentralDir(buffer); 226 if (centralDirOffset == -1) { 227 // If the zip file contains > 2^16 entries, a Zip64 EOCD is written, and the central 228 // dir offset in the regular EOCD may be -1. 229 centralDirOffset = findCentralDir64(buffer); 230 } 231 int offset = centralDirOffset - endofCdOffset; 232 if (offset < 0) { 233 // read the entire central directory record into memory 234 // for the framework jars this max of 5MB for Q 235 // TODO: consider using a smaller buffer size and re-reading as necessary 236 offset = 0; 237 randomAccessFile.seek(centralDirOffset); 238 final int cdSize = length - centralDirOffset; 239 buffer = new byte[cdSize]; 240 randomAccessFile.readFully(buffer); 241 } else { 242 // the central directory is already in the buffer, no need to reread 243 } 244 245 // now read the entries 246 while (true) { 247 // Instead of trusting numRecords, read until we find the 248 // end-of-central-directory signature. numRecords may wrap 249 // around with >64K entries. 250 int sig = readInt(buffer, offset); 251 if (sig == ENDSIG || sig == ENDSIG64) { 252 break; 253 } 254 255 int bitFlag = readShort(buffer, offset + 8); 256 int fileNameLength = readShort(buffer, offset + 28); 257 int extraLength = readShort(buffer, offset + 30); 258 int fieldCommentLength = readShort(buffer, offset + 32); 259 int relativeOffsetOfLocalFileHeader = readInt(buffer, offset + 42); 260 261 byte[] nameBytes = copyBytes(buffer, offset + 46, fileNameLength); 262 Charset encoding = getEncoding(bitFlag); 263 String fileName = new String(nameBytes, encoding); 264 byte[] localHeaderBuffer = new byte[30]; 265 randomAccessFile.seek(relativeOffsetOfLocalFileHeader); 266 randomAccessFile.readFully(localHeaderBuffer); 267 // There are two extra field lengths stored in the zip - one in the central directory, 268 // one in the local header. And we should use one in local header to calculate the 269 // correct file content offset, because they are different some times. 270 int localHeaderExtraLength = readShort(localHeaderBuffer, 28); 271 int fileOffset = 272 relativeOffsetOfLocalFileHeader + 30 + fileNameLength + localHeaderExtraLength; 273 result.put(fileName, (long) fileOffset); 274 offset += 46 + fileNameLength + extraLength + fieldCommentLength; 275 } 276 277 return result.build(); 278 } catch (IOException e) { 279 throw new RuntimeException(e); 280 } 281 } 282 copyBytes(byte[] buffer, int offset, int length)283 private static byte[] copyBytes(byte[] buffer, int offset, int length) { 284 byte[] result = new byte[length]; 285 System.arraycopy(buffer, offset, result, 0, length); 286 return result; 287 } 288 getEncoding(int bitFlags)289 private static Charset getEncoding(int bitFlags) { 290 // UTF-8 now supported in name and comments: check general bit flag, bit 291 // 11, to determine if UTF-8 is being used or ISO-8859-1 is being used. 292 return (0 != ((bitFlags >>> 11) & 1)) ? UTF_8 : ISO_8859_1; 293 } 294 findCentralDir(byte[] buffer)295 private static int findCentralDir(byte[] buffer) throws IOException { 296 // find start of central directory by scanning backwards 297 int scanOffset = buffer.length - EOCD_SIZE; 298 299 while (true) { 300 int val = readInt(buffer, scanOffset); 301 if (val == ENDSIG) { 302 break; 303 } 304 305 // Ok, keep backing up looking for the ZIP end central directory 306 // signature. 307 --scanOffset; 308 if (scanOffset < 0) { 309 throw new ZipException("ZIP directory not found, not a ZIP archive."); 310 } 311 } 312 // scanOffset is now start of end of central directory record 313 // the 'offset to central dir' data is at position 16 in the record 314 int offsetToCentralDir = readInt(buffer, scanOffset + 16); 315 return offsetToCentralDir; 316 } 317 findCentralDir64(byte[] buffer)318 private static int findCentralDir64(byte[] buffer) throws IOException { 319 // find start of central directory by scanning backwards 320 int scanOffset = buffer.length - EOCD_SIZE - ZIP64_EOCD_LOCATOR_SIZE - ZIP64_EOCD_SIZE; 321 322 while (true) { 323 int val = readInt(buffer, scanOffset); 324 if (val == ENDSIG64) { 325 break; 326 } 327 328 // Ok, keep backing up looking for the ZIP end central directory 329 // signature. 330 --scanOffset; 331 if (scanOffset < 0) { 332 throw new ZipException("ZIP directory not found, not a ZIP archive."); 333 } 334 } 335 // scanOffset is now start of end of central directory record 336 // the 'offset to central dir' data is at position 16 in the record 337 long offsetToCentralDir = readLong(buffer, scanOffset + 48); 338 return (int) offsetToCentralDir; 339 } 340 341 /** Read a 32-bit integer from a bytebuffer in little-endian order. */ readInt(byte[] buffer, int offset)342 private static int readInt(byte[] buffer, int offset) { 343 return Ints.fromBytes( 344 buffer[offset + 3], buffer[offset + 2], buffer[offset + 1], buffer[offset]); 345 } 346 347 /** Read a 64-bit integer from a bytebuffer in little-endian order. */ readLong(byte[] buffer, int offset)348 private static long readLong(byte[] buffer, int offset) { 349 return Longs.fromBytes( 350 buffer[offset + 7], 351 buffer[offset + 6], 352 buffer[offset + 5], 353 buffer[offset + 4], 354 buffer[offset + 3], 355 buffer[offset + 2], 356 buffer[offset + 1], 357 buffer[offset]); 358 } 359 360 /** Read a 16-bit short from a bytebuffer in little-endian order. */ readShort(byte[] buffer, int offset)361 private static short readShort(byte[] buffer, int offset) { 362 return Shorts.fromBytes(buffer[offset + 1], buffer[offset]); 363 } 364 365 /* 366 * This represents a memory-mapped file. It might be the entire file or 367 * only part of it. This requires a little bookkeeping because the mapping 368 * needs to be aligned on page boundaries, and in some cases we'd like to 369 * have multiple references to the mapped area without creating additional 370 * maps. 371 * 372 * This always uses MAP_SHARED. 373 * 374 * TODO: we should be able to create a new FileMap that is a subset of 375 * an existing FileMap and shares the underlying mapped pages. Requires 376 * completing the refcounting stuff and possibly introducing the notion 377 * of a FileMap hierarchy. 378 */ 379 // class FileMap { 380 // public: 381 // FileMap(void); 382 // 383 // FileMap(FileMap&& f); 384 // FileMap& operator=(FileMap&& f); 385 386 /* 387 * Create a new mapping on an open file. 388 * 389 * Closing the file descriptor does not unmap the pages, so we don't 390 * claim ownership of the fd. 391 * 392 * Returns "false" on failure. 393 */ 394 // boolean create(String origFileName, int fd, 395 // long offset, int length, boolean readOnly) { 396 // } 397 398 // ~FileMap(void); 399 400 /* 401 * Return the name of the file this map came from, if known. 402 */ getFileName()403 String getFileName() { 404 return mFileName; 405 } 406 407 /* 408 * Get a pointer to the piece of the file we requested. 409 */ getDataPtr()410 synchronized byte[] getDataPtr() { 411 if (mDataPtr == null) { 412 mDataPtr = new byte[mDataLength]; 413 414 InputStream is; 415 try { 416 if (isFromZip) { 417 is = zipFile.getInputStream(zipEntry); 418 } else { 419 is = new FileInputStream(getFileName()); 420 } 421 try { 422 readFully(is, mDataPtr); 423 } finally { 424 is.close(); 425 } 426 } catch (IOException e) { 427 throw new RuntimeException(e); 428 } 429 } 430 return mDataPtr; 431 } 432 readFully(InputStream is, byte[] bytes)433 public static void readFully(InputStream is, byte[] bytes) throws IOException { 434 int size = bytes.length; 435 int remaining = size; 436 while (remaining > 0) { 437 int location = size - remaining; 438 int bytesRead = is.read(bytes, location, remaining); 439 if (bytesRead == -1) { 440 break; 441 } 442 remaining -= bytesRead; 443 } 444 445 if (remaining > 0) { 446 throw new RuntimeException("failed to read " + size + " (" + remaining + " bytes unread)"); 447 } 448 } 449 450 /* 451 * Get the length we requested. 452 */ getDataLength()453 int getDataLength() { 454 return mDataLength; 455 } 456 457 /* 458 * Get the data offset used to create this map. 459 */ getDataOffset()460 long getDataOffset() { 461 return mDataOffset; 462 } 463 getZipEntry()464 public ZipEntry getZipEntry() { 465 return zipEntry; 466 } 467 468 // /* 469 // * This maps directly to madvise() values, but allows us to avoid 470 // * including <sys/mman.h> everywhere. 471 // */ 472 // enum MapAdvice { 473 // NORMAL, RANDOM, SEQUENTIAL, WILLNEED, DONTNEED 474 // }; 475 // 476 // /* 477 // * Apply an madvise() call to the entire file. 478 // * 479 // * Returns 0 on success, -1 on failure. 480 // */ 481 // int advise(MapAdvice advice); 482 // 483 // protected: 484 // 485 // private: 486 // // these are not implemented 487 // FileMap(const FileMap& src); 488 // const FileMap& operator=(const FileMap& src); 489 // 490 String mFileName; // original file name, if known 491 int mBasePtr; // base of mmap area; page aligned 492 int mBaseLength; // length, measured from "mBasePtr" 493 long mDataOffset; // offset used when map was created 494 byte[] mDataPtr; // start of requested data, offset from base 495 int mDataLength; // length, measured from "mDataPtr" 496 static long mPageSize; 497 498 @Override toString()499 public String toString() { 500 if (isFromZip) { 501 return "FileMap{" + "zipFile=" + zipFile.getName() + ", zipEntry=" + zipEntry + '}'; 502 } else { 503 return "FileMap{" + "mFileName='" + mFileName + '\'' + '}'; 504 } 505 } 506 } 507