• 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, 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