1/*
2 * Copyright (C) 2014 Square, Inc.
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 */
16package okio;
17
18import java.io.IOException;
19import java.util.zip.CRC32;
20import java.util.zip.Deflater;
21
22import static java.util.zip.Deflater.DEFAULT_COMPRESSION;
23
24/**
25 * A sink that uses <a href="http://www.ietf.org/rfc/rfc1952.txt">GZIP</a> to
26 * compress written data to another sink.
27 *
28 * <h3>Sync flush</h3>
29 * Aggressive flushing of this stream may result in reduced compression. Each
30 * call to {@link #flush} immediately compresses all currently-buffered data;
31 * this early compression may be less effective than compression performed
32 * without flushing.
33 *
34 * <p>This is equivalent to using {@link Deflater} with the sync flush option.
35 * This class does not offer any partial flush mechanism. For best performance,
36 * only call {@link #flush} when application behavior requires it.
37 */
38public final class GzipSink implements Sink {
39  /** Sink into which the GZIP format is written. */
40  private final BufferedSink sink;
41
42  /** The deflater used to compress the body. */
43  private final Deflater deflater;
44
45  /**
46   * The deflater sink takes care of moving data between decompressed source and
47   * compressed sink buffers.
48   */
49  private final DeflaterSink deflaterSink;
50
51  private boolean closed;
52
53  /** Checksum calculated for the compressed body. */
54  private final CRC32 crc = new CRC32();
55
56  public GzipSink(Sink sink) {
57    if (sink == null) throw new IllegalArgumentException("sink == null");
58    this.deflater = new Deflater(DEFAULT_COMPRESSION, true /* No wrap */);
59    this.sink = Okio.buffer(sink);
60    this.deflaterSink = new DeflaterSink(this.sink, deflater);
61
62    writeHeader();
63  }
64
65  @Override public void write(Buffer source, long byteCount) throws IOException {
66    if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
67    if (byteCount == 0) return;
68
69    updateCrc(source, byteCount);
70    deflaterSink.write(source, byteCount);
71  }
72
73  @Override public void flush() throws IOException {
74    deflaterSink.flush();
75  }
76
77  @Override public Timeout timeout() {
78    return sink.timeout();
79  }
80
81  @Override public void close() throws IOException {
82    if (closed) return;
83
84    // This method delegates to the DeflaterSink for finishing the deflate process
85    // but keeps responsibility for releasing the deflater's resources. This is
86    // necessary because writeFooter needs to query the processed byte count which
87    // only works when the deflater is still open.
88
89    Throwable thrown = null;
90    try {
91      deflaterSink.finishDeflate();
92      writeFooter();
93    } catch (Throwable e) {
94      thrown = e;
95    }
96
97    try {
98      deflater.end();
99    } catch (Throwable e) {
100      if (thrown == null) thrown = e;
101    }
102
103    try {
104      sink.close();
105    } catch (Throwable e) {
106      if (thrown == null) thrown = e;
107    }
108    closed = true;
109
110    if (thrown != null) Util.sneakyRethrow(thrown);
111  }
112
113  private void writeHeader() {
114    // Write the Gzip header directly into the buffer for the sink to avoid handling IOException.
115    Buffer buffer = this.sink.buffer();
116    buffer.writeShort(0x1f8b); // Two-byte Gzip ID.
117    buffer.writeByte(0x08); // 8 == Deflate compression method.
118    buffer.writeByte(0x00); // No flags.
119    buffer.writeInt(0x00); // No modification time.
120    buffer.writeByte(0x00); // No extra flags.
121    buffer.writeByte(0x00); // No OS.
122  }
123
124  private void writeFooter() throws IOException {
125    sink.writeIntLe((int) crc.getValue()); // CRC of original data.
126    sink.writeIntLe(deflater.getTotalIn()); // Length of original data.
127  }
128
129  /** Updates the CRC with the given bytes. */
130  private void updateCrc(Buffer buffer, long byteCount) {
131    for (Segment head = buffer.head; byteCount > 0; head = head.next) {
132      int segmentLength = (int) Math.min(byteCount, head.limit - head.pos);
133      crc.update(head.data, head.pos, segmentLength);
134      byteCount -= segmentLength;
135    }
136  }
137}
138