• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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