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