1/* Copyright 2017 Google Inc. All Rights Reserved.
2
3   Distributed under MIT license.
4   See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
5*/
6
7package org.brotli.wrapper.enc;
8
9import java.io.IOException;
10import java.nio.ByteBuffer;
11import java.nio.channels.WritableByteChannel;
12import java.util.ArrayList;
13
14/**
15 * Base class for OutputStream / Channel implementations.
16 */
17public class Encoder {
18  private final WritableByteChannel destination;
19  private final EncoderJNI.Wrapper encoder;
20  final ByteBuffer inputBuffer;
21  ByteBuffer buffer;
22  boolean closed;
23
24  /**
25   * Brotli encoder settings.
26   */
27  public static final class Parameters {
28    private int quality = -1;
29    private int lgwin = -1;
30
31    public Parameters() { }
32
33    private Parameters(Parameters other) {
34      this.quality = other.quality;
35      this.lgwin = other.lgwin;
36    }
37
38    /**
39     * @param quality compression quality, or -1 for default
40     */
41    public Parameters setQuality(int quality) {
42      if (quality < -1 || quality > 11) {
43        throw new IllegalArgumentException("quality should be in range [0, 11], or -1");
44      }
45      this.quality = quality;
46      return this;
47    }
48
49    /**
50     * @param lgwin log2(LZ window size), or -1 for default
51     */
52    public Parameters setWindow(int lgwin) {
53      if ((lgwin != -1) && ((lgwin < 10) || (lgwin > 24))) {
54        throw new IllegalArgumentException("lgwin should be in range [10, 24], or -1");
55      }
56      this.lgwin = lgwin;
57      return this;
58    }
59  }
60
61  /**
62   * Creates a Encoder wrapper.
63   *
64   * @param destination underlying destination
65   * @param params encoding parameters
66   * @param inputBufferSize read buffer size
67   */
68  Encoder(WritableByteChannel destination, Parameters params, int inputBufferSize)
69      throws IOException {
70    if (inputBufferSize <= 0) {
71      throw new IllegalArgumentException("buffer size must be positive");
72    }
73    if (destination == null) {
74      throw new NullPointerException("destination can not be null");
75    }
76    this.destination = destination;
77    this.encoder = new EncoderJNI.Wrapper(inputBufferSize, params.quality, params.lgwin);
78    this.inputBuffer = this.encoder.getInputBuffer();
79  }
80
81  private void fail(String message) throws IOException {
82    try {
83      close();
84    } catch (IOException ex) {
85      /* Ignore */
86    }
87    throw new IOException(message);
88  }
89
90  /**
91   * @param force repeat pushing until all output is consumed
92   * @return true if all encoder output is consumed
93   */
94  boolean pushOutput(boolean force) throws IOException {
95    while (buffer != null) {
96      if (buffer.hasRemaining()) {
97        destination.write(buffer);
98      }
99      if (!buffer.hasRemaining()) {
100        buffer = null;
101      } else if (!force) {
102        return false;
103      }
104    }
105    return true;
106  }
107
108  /**
109   * @return true if there is space in inputBuffer.
110   */
111  boolean encode(EncoderJNI.Operation op) throws IOException {
112    boolean force = (op != EncoderJNI.Operation.PROCESS);
113    if (force) {
114      inputBuffer.limit(inputBuffer.position());
115    } else if (inputBuffer.hasRemaining()) {
116      return true;
117    }
118    boolean hasInput = true;
119    while (true) {
120      if (!encoder.isSuccess()) {
121        fail("encoding failed");
122      } else if (!pushOutput(force)) {
123        return false;
124      } else if (encoder.hasMoreOutput()) {
125        buffer = encoder.pull();
126      } else if (encoder.hasRemainingInput()) {
127        encoder.push(op, 0);
128      } else if (hasInput) {
129        encoder.push(op, inputBuffer.limit());
130        hasInput = false;
131      } else {
132        inputBuffer.clear();
133        return true;
134      }
135    }
136  }
137
138  void flush() throws IOException {
139    encode(EncoderJNI.Operation.FLUSH);
140  }
141
142  void close() throws IOException {
143    if (closed) {
144      return;
145    }
146    closed = true;
147    try {
148      encode(EncoderJNI.Operation.FINISH);
149    } finally {
150      encoder.destroy();
151      destination.close();
152    }
153  }
154
155  /**
156   * Encodes the given data buffer.
157   */
158  public static byte[] compress(byte[] data, Parameters params) throws IOException {
159    EncoderJNI.Wrapper encoder = new EncoderJNI.Wrapper(data.length, params.quality, params.lgwin);
160    ArrayList<byte[]> output = new ArrayList<byte[]>();
161    int totalOutputSize = 0;
162    try {
163      encoder.getInputBuffer().put(data);
164      encoder.push(EncoderJNI.Operation.FINISH, data.length);
165      while (true) {
166        if (!encoder.isSuccess()) {
167          throw new IOException("encoding failed");
168        } else if (encoder.hasMoreOutput()) {
169          ByteBuffer buffer = encoder.pull();
170          byte[] chunk = new byte[buffer.remaining()];
171          buffer.get(chunk);
172          output.add(chunk);
173          totalOutputSize += chunk.length;
174        } else if (!encoder.isFinished()) {
175          encoder.push(EncoderJNI.Operation.FINISH, 0);
176        } else {
177          break;
178        }
179      }
180    } finally {
181      encoder.destroy();
182    }
183    if (output.size() == 1) {
184      return output.get(0);
185    }
186    byte[] result = new byte[totalOutputSize];
187    int offset = 0;
188    for (byte[] chunk : output) {
189      System.arraycopy(chunk, 0, result, offset, chunk.length);
190      offset += chunk.length;
191    }
192    return result;
193  }
194
195  public static byte[] compress(byte[] data) throws IOException {
196    return compress(data, new Parameters());
197  }
198}
199