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