• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.apksig.internal.zip;
18 
19 import static com.android.apksig.internal.zip.ZipUtils.UINT32_MAX_VALUE;
20 import static com.android.apksig.internal.zip.ZipUtils.ZIP64_COMPRESSED_SIZE_FIELD_NAME;
21 import static com.android.apksig.internal.zip.ZipUtils.ZIP64_UNCOMPRESSED_SIZE_FIELD_NAME;
22 
23 import com.android.apksig.internal.util.ByteBufferSink;
24 import com.android.apksig.internal.zip.ZipUtils.Zip64Fields;
25 import com.android.apksig.util.DataSink;
26 import com.android.apksig.util.DataSource;
27 import com.android.apksig.zip.ZipFormatException;
28 
29 import java.io.Closeable;
30 import java.io.IOException;
31 import java.nio.ByteBuffer;
32 import java.nio.ByteOrder;
33 import java.nio.charset.StandardCharsets;
34 import java.util.zip.DataFormatException;
35 import java.util.zip.Inflater;
36 
37 /**
38  * ZIP Local File record.
39  *
40  * <p>The record consists of the Local File Header, file data, and (if present) Data Descriptor.
41  */
42 public class LocalFileRecord {
43     private static final int RECORD_SIGNATURE = 0x04034b50;
44     private static final int HEADER_SIZE_BYTES = 30;
45 
46     private static final int GP_FLAGS_OFFSET = 6;
47     private static final int CRC32_OFFSET = 14;
48     private static final int COMPRESSED_SIZE_OFFSET = 18;
49     private static final int UNCOMPRESSED_SIZE_OFFSET = 22;
50     private static final int NAME_LENGTH_OFFSET = 26;
51     private static final int EXTRA_LENGTH_OFFSET = 28;
52     private static final int NAME_OFFSET = HEADER_SIZE_BYTES;
53 
54     private static final int DATA_DESCRIPTOR_SIZE_BYTES_WITHOUT_SIGNATURE = 12;
55     private static final int DATA_DESCRIPTOR_SIGNATURE = 0x08074b50;
56 
57     private final String mName;
58     private final int mNameSizeBytes;
59     private final ByteBuffer mExtra;
60 
61     private final long mStartOffsetInArchive;
62     private final long mSize;
63 
64     private final int mDataStartOffset;
65     private final long mDataSize;
66     private final boolean mDataCompressed;
67     private final long mUncompressedDataSize;
68 
LocalFileRecord( String name, int nameSizeBytes, ByteBuffer extra, long startOffsetInArchive, long size, int dataStartOffset, long dataSize, boolean dataCompressed, long uncompressedDataSize)69     private LocalFileRecord(
70             String name,
71             int nameSizeBytes,
72             ByteBuffer extra,
73             long startOffsetInArchive,
74             long size,
75             int dataStartOffset,
76             long dataSize,
77             boolean dataCompressed,
78             long uncompressedDataSize) {
79         mName = name;
80         mNameSizeBytes = nameSizeBytes;
81         mExtra = extra;
82         mStartOffsetInArchive = startOffsetInArchive;
83         mSize = size;
84         mDataStartOffset = dataStartOffset;
85         mDataSize = dataSize;
86         mDataCompressed = dataCompressed;
87         mUncompressedDataSize = uncompressedDataSize;
88     }
89 
getName()90     public String getName() {
91         return mName;
92     }
93 
getExtra()94     public ByteBuffer getExtra() {
95         ByteBuffer result = (mExtra.capacity() > 0) ? mExtra.slice() : mExtra;
96         result.order(ByteOrder.LITTLE_ENDIAN);
97         return result;
98     }
99 
getExtraFieldStartOffsetInsideRecord()100     public int getExtraFieldStartOffsetInsideRecord() {
101         return HEADER_SIZE_BYTES + mNameSizeBytes;
102     }
103 
getStartOffsetInArchive()104     public long getStartOffsetInArchive() {
105         return mStartOffsetInArchive;
106     }
107 
getDataStartOffsetInRecord()108     public int getDataStartOffsetInRecord() {
109         return mDataStartOffset;
110     }
111 
112     /**
113      * Returns the size (in bytes) of this record.
114      */
getSize()115     public long getSize() {
116         return mSize;
117     }
118 
119     /**
120      * Returns {@code true} if this record's file data is stored in compressed form.
121      */
isDataCompressed()122     public boolean isDataCompressed() {
123         return mDataCompressed;
124     }
125 
126     /**
127      * Returns the Local File record starting at the current position of the provided buffer
128      * and advances the buffer's position immediately past the end of the record. The record
129      * consists of the Local File Header, data, and (if present) Data Descriptor.
130      */
getRecord( DataSource apk, CentralDirectoryRecord cdRecord, long cdStartOffset)131     public static LocalFileRecord getRecord(
132             DataSource apk,
133             CentralDirectoryRecord cdRecord,
134             long cdStartOffset) throws ZipFormatException, IOException {
135         return getRecord(
136                 apk,
137                 cdRecord,
138                 cdStartOffset,
139                 true, // obtain extra field contents
140                 true // include Data Descriptor (if present)
141                 );
142     }
143 
144     /**
145      * Returns the Local File record starting at the current position of the provided buffer
146      * and advances the buffer's position immediately past the end of the record. The record
147      * consists of the Local File Header, data, and (if present) Data Descriptor.
148      */
getRecord( DataSource apk, CentralDirectoryRecord cdRecord, long cdStartOffset, boolean extraFieldContentsNeeded, boolean dataDescriptorIncluded)149     private static LocalFileRecord getRecord(
150             DataSource apk,
151             CentralDirectoryRecord cdRecord,
152             long cdStartOffset,
153             boolean extraFieldContentsNeeded,
154             boolean dataDescriptorIncluded) throws ZipFormatException, IOException {
155         // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform
156         // exhibited when reading an APK for the purposes of verifying its signatures.
157 
158         String entryName = cdRecord.getName();
159         int cdRecordEntryNameSizeBytes = cdRecord.getNameSizeBytes();
160         int headerSizeWithName = HEADER_SIZE_BYTES + cdRecordEntryNameSizeBytes;
161         long headerStartOffset = cdRecord.getLocalFileHeaderOffset();
162         long headerEndOffset = headerStartOffset + headerSizeWithName;
163         if (headerEndOffset > cdStartOffset) {
164             throw new ZipFormatException(
165                     "Local File Header of " + entryName + " extends beyond start of Central"
166                             + " Directory. LFH end: " + headerEndOffset
167                             + ", CD start: " + cdStartOffset);
168         }
169         ByteBuffer header;
170         try {
171             header = apk.getByteBuffer(headerStartOffset, headerSizeWithName);
172         } catch (IOException e) {
173             throw new IOException("Failed to read Local File Header of " + entryName, e);
174         }
175         header.order(ByteOrder.LITTLE_ENDIAN);
176 
177         int recordSignature = header.getInt();
178         if (recordSignature != RECORD_SIGNATURE) {
179             throw new ZipFormatException(
180                     "Not a Local File Header record for entry " + entryName + ". Signature: 0x"
181                             + Long.toHexString(recordSignature & 0xffffffffL));
182         }
183         short gpFlags = header.getShort(GP_FLAGS_OFFSET);
184         boolean dataDescriptorUsed = (gpFlags & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0;
185         boolean cdDataDescriptorUsed =
186                 (cdRecord.getGpFlags() & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0;
187         if (dataDescriptorUsed != cdDataDescriptorUsed) {
188             throw new ZipFormatException(
189                     "Data Descriptor presence mismatch between Local File Header and Central"
190                             + " Directory for entry " + entryName
191                             + ". LFH: " + dataDescriptorUsed + ", CD: " + cdDataDescriptorUsed);
192         }
193         long uncompressedDataCrc32FromCdRecord = cdRecord.getCrc32();
194         long compressedDataSizeFromCdRecord = cdRecord.getCompressedSize();
195         long uncompressedDataSizeFromCdRecord = cdRecord.getUncompressedSize();
196         int nameLength = ZipUtils.getUnsignedInt16(header, NAME_LENGTH_OFFSET);
197         if (nameLength > cdRecordEntryNameSizeBytes) {
198             throw new ZipFormatException(
199                     "Name mismatch between Local File Header and Central Directory for entry"
200                             + entryName + ". LFH: " + nameLength
201                             + " bytes, CD: " + cdRecordEntryNameSizeBytes + " bytes");
202         }
203         String name = CentralDirectoryRecord.getName(header, NAME_OFFSET, nameLength);
204         if (!entryName.equals(name)) {
205             throw new ZipFormatException(
206                     "Name mismatch between Local File Header and Central Directory. LFH: \""
207                             + name + "\", CD: \"" + entryName + "\"");
208         }
209         int extraLength = ZipUtils.getUnsignedInt16(header, EXTRA_LENGTH_OFFSET);
210         long dataStartOffset = headerStartOffset + HEADER_SIZE_BYTES + nameLength + extraLength;
211         long dataSize;
212         boolean compressed =
213                 (cdRecord.getCompressionMethod() != ZipUtils.COMPRESSION_METHOD_STORED);
214         if (compressed) {
215             dataSize = compressedDataSizeFromCdRecord;
216         } else {
217             dataSize = uncompressedDataSizeFromCdRecord;
218         }
219         long dataEndOffset = dataStartOffset + dataSize;
220         if (dataEndOffset > cdStartOffset) {
221             throw new ZipFormatException(
222                     "Local File Header data of " + entryName + " overlaps with Central Directory"
223                             + ". LFH data start: " + dataStartOffset
224                             + ", LFH data end: " + dataEndOffset + ", CD start: " + cdStartOffset);
225         }
226 
227         ByteBuffer extra = EMPTY_BYTE_BUFFER;
228         if ((extraFieldContentsNeeded) && (extraLength > 0)) {
229             extra = apk.getByteBuffer(
230                     headerStartOffset + HEADER_SIZE_BYTES + nameLength, extraLength);
231             extra.order(ByteOrder.LITTLE_ENDIAN);
232         }
233 
234         if (!dataDescriptorUsed) {
235             long crc32 = ZipUtils.getUnsignedInt32(header, CRC32_OFFSET);
236             if (crc32 != uncompressedDataCrc32FromCdRecord) {
237                 throw new ZipFormatException(
238                         "CRC-32 mismatch between Local File Header and Central Directory for entry "
239                                 + entryName
240                                 + ". LFH: "
241                                 + crc32
242                                 + ", CD: "
243                                 + uncompressedDataCrc32FromCdRecord);
244             }
245             long compressedSize = ZipUtils.getUnsignedInt32(header, COMPRESSED_SIZE_OFFSET);
246             long uncompressedSize = ZipUtils.getUnsignedInt32(header, UNCOMPRESSED_SIZE_OFFSET);
247 
248             // If the record contains an extra field and any of the other fields subject to the
249             // 32-bit limitation indicate the presence of a ZIP64 block, then check the extra field
250             // for this block to obtain the actual values of the affected fields.
251             if (extraLength > 0
252                     && (compressedSize == UINT32_MAX_VALUE
253                             || uncompressedSize == UINT32_MAX_VALUE)) {
254                 // If the extra buffer was not previously obtained due to the flag not being set,
255                 // get the extra buffer now.
256                 if (!extraFieldContentsNeeded) {
257                     extra =
258                             apk.getByteBuffer(
259                                     headerStartOffset + HEADER_SIZE_BYTES + nameLength,
260                                     extraLength);
261                     extra.order(ByteOrder.LITTLE_ENDIAN);
262                 }
263                 Zip64Fields zip64Fields = new Zip64Fields(uncompressedSize, compressedSize);
264                 ZipUtils.parseExtraField(extra, zip64Fields);
265                 extra.position(0);
266                 uncompressedSize =
267                         ZipUtils.checkAndReturnZip64Value(
268                                 uncompressedSize,
269                                 zip64Fields.uncompressedSize,
270                                 entryName,
271                                 ZIP64_UNCOMPRESSED_SIZE_FIELD_NAME);
272                 compressedSize =
273                         ZipUtils.checkAndReturnZip64Value(
274                                 compressedSize,
275                                 zip64Fields.compressedSize,
276                                 entryName,
277                                 ZIP64_COMPRESSED_SIZE_FIELD_NAME);
278             }
279             if (compressedSize != compressedDataSizeFromCdRecord) {
280                 throw new ZipFormatException(
281                         "Compressed size mismatch between Local File Header and Central Directory"
282                                 + " for entry "
283                                 + entryName
284                                 + ". LFH: "
285                                 + compressedSize
286                                 + ", CD: "
287                                 + compressedDataSizeFromCdRecord);
288             }
289 
290             if (uncompressedSize != uncompressedDataSizeFromCdRecord) {
291                 throw new ZipFormatException(
292                         "Uncompressed size mismatch between Local File Header and Central Directory"
293                                 + " for entry "
294                                 + entryName
295                                 + ". LFH: "
296                                 + uncompressedSize
297                                 + ", CD: "
298                                 + uncompressedDataSizeFromCdRecord);
299             }
300         }
301 
302         long recordEndOffset = dataEndOffset;
303         // Include the Data Descriptor (if requested and present) into the record.
304         if ((dataDescriptorIncluded) && ((gpFlags & ZipUtils.GP_FLAG_DATA_DESCRIPTOR_USED) != 0)) {
305             // The record's data is supposed to be followed by the Data Descriptor. Unfortunately,
306             // the descriptor's size is not known in advance because the spec lets the signature
307             // field (the first four bytes) be omitted. Thus, there's no 100% reliable way to tell
308             // how long the Data Descriptor record is. Most parsers (including Android) check
309             // whether the first four bytes look like Data Descriptor record signature and, if so,
310             // assume that it is indeed the record's signature. However, this is the wrong
311             // conclusion if the record's CRC-32 (next field after the signature) has the same value
312             // as the signature. In any case, we're doing what Android is doing.
313             long dataDescriptorEndOffset =
314                     dataEndOffset + DATA_DESCRIPTOR_SIZE_BYTES_WITHOUT_SIGNATURE;
315             if (dataDescriptorEndOffset > cdStartOffset) {
316                 throw new ZipFormatException(
317                         "Data Descriptor of " + entryName + " overlaps with Central Directory"
318                                 + ". Data Descriptor end: " + dataEndOffset
319                                 + ", CD start: " + cdStartOffset);
320             }
321             ByteBuffer dataDescriptorPotentialSig = apk.getByteBuffer(dataEndOffset, 4);
322             dataDescriptorPotentialSig.order(ByteOrder.LITTLE_ENDIAN);
323             if (dataDescriptorPotentialSig.getInt() == DATA_DESCRIPTOR_SIGNATURE) {
324                 dataDescriptorEndOffset += 4;
325                 if (dataDescriptorEndOffset > cdStartOffset) {
326                     throw new ZipFormatException(
327                             "Data Descriptor of " + entryName + " overlaps with Central Directory"
328                                     + ". Data Descriptor end: " + dataEndOffset
329                                     + ", CD start: " + cdStartOffset);
330                 }
331             }
332             recordEndOffset = dataDescriptorEndOffset;
333         }
334 
335         long recordSize = recordEndOffset - headerStartOffset;
336         int dataStartOffsetInRecord = HEADER_SIZE_BYTES + nameLength + extraLength;
337 
338         return new LocalFileRecord(
339                 entryName,
340                 cdRecordEntryNameSizeBytes,
341                 extra,
342                 headerStartOffset,
343                 recordSize,
344                 dataStartOffsetInRecord,
345                 dataSize,
346                 compressed,
347                 uncompressedDataSizeFromCdRecord);
348     }
349 
350     /**
351      * Outputs this record and returns returns the number of bytes output.
352      */
outputRecord(DataSource sourceApk, DataSink output)353     public long outputRecord(DataSource sourceApk, DataSink output) throws IOException {
354         long size = getSize();
355         sourceApk.feed(getStartOffsetInArchive(), size, output);
356         return size;
357     }
358 
359     /**
360      * Outputs this record, replacing its extra field with the provided one, and returns returns the
361      * number of bytes output.
362      */
outputRecordWithModifiedExtra( DataSource sourceApk, ByteBuffer extra, DataSink output)363     public long outputRecordWithModifiedExtra(
364             DataSource sourceApk,
365             ByteBuffer extra,
366             DataSink output) throws IOException {
367         long recordStartOffsetInSource = getStartOffsetInArchive();
368         int extraStartOffsetInRecord = getExtraFieldStartOffsetInsideRecord();
369         int extraSizeBytes = extra.remaining();
370         int headerSize = extraStartOffsetInRecord + extraSizeBytes;
371         ByteBuffer header = ByteBuffer.allocate(headerSize);
372         header.order(ByteOrder.LITTLE_ENDIAN);
373         sourceApk.copyTo(recordStartOffsetInSource, extraStartOffsetInRecord, header);
374         header.put(extra.slice());
375         header.flip();
376         ZipUtils.setUnsignedInt16(header, EXTRA_LENGTH_OFFSET, extraSizeBytes);
377 
378         long outputByteCount = header.remaining();
379         output.consume(header);
380         long remainingRecordSize = getSize() - mDataStartOffset;
381         sourceApk.feed(recordStartOffsetInSource + mDataStartOffset, remainingRecordSize, output);
382         outputByteCount += remainingRecordSize;
383         return outputByteCount;
384     }
385 
386     /**
387      * Outputs the specified Local File Header record with its data and returns the number of bytes
388      * output.
389      */
outputRecordWithDeflateCompressedData( String name, int lastModifiedTime, int lastModifiedDate, byte[] compressedData, long crc32, long uncompressedSize, DataSink output)390     public static long outputRecordWithDeflateCompressedData(
391             String name,
392             int lastModifiedTime,
393             int lastModifiedDate,
394             byte[] compressedData,
395             long crc32,
396             long uncompressedSize,
397             DataSink output) throws IOException {
398         byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8);
399         int recordSize = HEADER_SIZE_BYTES + nameBytes.length;
400         ByteBuffer result = ByteBuffer.allocate(recordSize);
401         result.order(ByteOrder.LITTLE_ENDIAN);
402         result.putInt(RECORD_SIGNATURE);
403         ZipUtils.putUnsignedInt16(result,  0x14); // Minimum version needed to extract
404         result.putShort(ZipUtils.GP_FLAG_EFS); // General purpose flag: UTF-8 encoded name
405         result.putShort(ZipUtils.COMPRESSION_METHOD_DEFLATED);
406         ZipUtils.putUnsignedInt16(result, lastModifiedTime);
407         ZipUtils.putUnsignedInt16(result, lastModifiedDate);
408         ZipUtils.putUnsignedInt32(result, crc32);
409         ZipUtils.putUnsignedInt32(result, compressedData.length);
410         ZipUtils.putUnsignedInt32(result, uncompressedSize);
411         ZipUtils.putUnsignedInt16(result, nameBytes.length);
412         ZipUtils.putUnsignedInt16(result, 0); // Extra field length
413         result.put(nameBytes);
414         if (result.hasRemaining()) {
415             throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit());
416         }
417         result.flip();
418 
419         long outputByteCount = result.remaining();
420         output.consume(result);
421         outputByteCount += compressedData.length;
422         output.consume(compressedData, 0, compressedData.length);
423         return outputByteCount;
424     }
425 
426     private static final ByteBuffer EMPTY_BYTE_BUFFER = ByteBuffer.allocate(0);
427 
428     /**
429      * Sends uncompressed data of this record into the the provided data sink.
430      */
outputUncompressedData( DataSource lfhSection, DataSink sink)431     public void outputUncompressedData(
432             DataSource lfhSection,
433             DataSink sink) throws IOException, ZipFormatException {
434         long dataStartOffsetInArchive = mStartOffsetInArchive + mDataStartOffset;
435         try {
436             if (mDataCompressed) {
437                 try (InflateSinkAdapter inflateAdapter = new InflateSinkAdapter(sink)) {
438                     lfhSection.feed(dataStartOffsetInArchive, mDataSize, inflateAdapter);
439                     long actualUncompressedSize = inflateAdapter.getOutputByteCount();
440                     if (actualUncompressedSize != mUncompressedDataSize) {
441                         throw new ZipFormatException(
442                                 "Unexpected size of uncompressed data of " + mName
443                                         + ". Expected: " + mUncompressedDataSize + " bytes"
444                                         + ", actual: " + actualUncompressedSize + " bytes");
445                     }
446                 } catch (IOException e) {
447                     if (e.getCause() instanceof DataFormatException) {
448                         throw new ZipFormatException("Data of entry " + mName + " malformed", e);
449                     }
450                     throw e;
451                 }
452             } else {
453                 lfhSection.feed(dataStartOffsetInArchive, mDataSize, sink);
454                 // No need to check whether output size is as expected because DataSource.feed is
455                 // guaranteed to output exactly the number of bytes requested.
456             }
457         } catch (IOException e) {
458             throw new IOException(
459                     "Failed to read data of " + ((mDataCompressed) ? "compressed" : "uncompressed")
460                         + " entry " + mName,
461                     e);
462         }
463         // Interestingly, Android doesn't check that uncompressed data's CRC-32 is as expected. We
464         // thus don't check either.
465     }
466 
467     /**
468      * Sends uncompressed data pointed to by the provided ZIP Central Directory (CD) record into the
469      * provided data sink.
470      */
outputUncompressedData( DataSource source, CentralDirectoryRecord cdRecord, long cdStartOffsetInArchive, DataSink sink)471     public static void outputUncompressedData(
472             DataSource source,
473             CentralDirectoryRecord cdRecord,
474             long cdStartOffsetInArchive,
475             DataSink sink) throws ZipFormatException, IOException {
476         // IMPLEMENTATION NOTE: This method attempts to mimic the behavior of Android platform
477         // exhibited when reading an APK for the purposes of verifying its signatures.
478         // When verifying an APK, Android doesn't care reading the extra field or the Data
479         // Descriptor.
480         LocalFileRecord lfhRecord =
481                 getRecord(
482                         source,
483                         cdRecord,
484                         cdStartOffsetInArchive,
485                         false, // don't care about the extra field
486                         false // don't read the Data Descriptor
487                         );
488         lfhRecord.outputUncompressedData(source, sink);
489     }
490 
491     /**
492      * Returns the uncompressed data pointed to by the provided ZIP Central Directory (CD) record.
493      */
getUncompressedData( DataSource source, CentralDirectoryRecord cdRecord, long cdStartOffsetInArchive)494     public static byte[] getUncompressedData(
495             DataSource source,
496             CentralDirectoryRecord cdRecord,
497             long cdStartOffsetInArchive) throws ZipFormatException, IOException {
498         if (cdRecord.getUncompressedSize() > Integer.MAX_VALUE) {
499             throw new IOException(
500                     cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize());
501         }
502         byte[] result = null;
503         try {
504             result = new byte[(int) cdRecord.getUncompressedSize()];
505         } catch (OutOfMemoryError e) {
506             throw new IOException(
507                     cdRecord.getName() + " too large: " + cdRecord.getUncompressedSize(), e);
508         }
509         ByteBuffer resultBuf = ByteBuffer.wrap(result);
510         ByteBufferSink resultSink = new ByteBufferSink(resultBuf);
511         outputUncompressedData(
512                 source,
513                 cdRecord,
514                 cdStartOffsetInArchive,
515                 resultSink);
516         return result;
517     }
518 
519     /**
520      * {@link DataSink} which inflates received data and outputs the deflated data into the provided
521      * delegate sink.
522      */
523     private static class InflateSinkAdapter implements DataSink, Closeable {
524         private final DataSink mDelegate;
525 
526         private Inflater mInflater = new Inflater(true);
527         private byte[] mOutputBuffer;
528         private byte[] mInputBuffer;
529         private long mOutputByteCount;
530         private boolean mClosed;
531 
InflateSinkAdapter(DataSink delegate)532         private InflateSinkAdapter(DataSink delegate) {
533             mDelegate = delegate;
534         }
535 
536         @Override
consume(byte[] buf, int offset, int length)537         public void consume(byte[] buf, int offset, int length) throws IOException {
538             checkNotClosed();
539             mInflater.setInput(buf, offset, length);
540             if (mOutputBuffer == null) {
541                 mOutputBuffer = new byte[65536];
542             }
543             while (!mInflater.finished()) {
544                 int outputChunkSize;
545                 try {
546                     outputChunkSize = mInflater.inflate(mOutputBuffer);
547                 } catch (DataFormatException e) {
548                     throw new IOException("Failed to inflate data", e);
549                 }
550                 if (outputChunkSize == 0) {
551                     return;
552                 }
553                 mDelegate.consume(mOutputBuffer, 0, outputChunkSize);
554                 mOutputByteCount += outputChunkSize;
555             }
556         }
557 
558         @Override
consume(ByteBuffer buf)559         public void consume(ByteBuffer buf) throws IOException {
560             checkNotClosed();
561             if (buf.hasArray()) {
562                 consume(buf.array(), buf.arrayOffset() + buf.position(), buf.remaining());
563                 buf.position(buf.limit());
564             } else {
565                 if (mInputBuffer == null) {
566                     mInputBuffer = new byte[65536];
567                 }
568                 while (buf.hasRemaining()) {
569                     int chunkSize = Math.min(buf.remaining(), mInputBuffer.length);
570                     buf.get(mInputBuffer, 0, chunkSize);
571                     consume(mInputBuffer, 0, chunkSize);
572                 }
573             }
574         }
575 
getOutputByteCount()576         public long getOutputByteCount() {
577             return mOutputByteCount;
578         }
579 
580         @Override
close()581         public void close() throws IOException {
582             mClosed = true;
583             mInputBuffer = null;
584             mOutputBuffer = null;
585             if (mInflater != null) {
586                 mInflater.end();
587                 mInflater = null;
588             }
589         }
590 
checkNotClosed()591         private void checkNotClosed() {
592             if (mClosed) {
593                 throw new IllegalStateException("Closed");
594             }
595         }
596     }
597 }
598