/*
* Copyright 2016-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.facebook.buck.charset;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import javax.annotation.concurrent.NotThreadSafe;
/**
* Wrapper for {@link CharsetDecoder} to provide decoding of NUL-terminated bytestrings to Unicode
* Strings.
*
* <p>Instances of this object are not thread-safe. If you want to re-use this object, make sure to
* synchronize access and invoke {@link #reset()} between uses.
*/
@NotThreadSafe
public class NulTerminatedCharsetDecoder {
public static final int DEFAULT_INITIAL_CHAR_BUFFER_CAPACITY = 512;
private final CharsetDecoder decoder;
private CharBuffer charBuffer;
public static class Result {
public final boolean nulTerminatorReached;
public final CoderResult coderResult;
public Result(boolean nulTerminatorReached, CoderResult coderResult) {
this.nulTerminatorReached = nulTerminatorReached;
this.coderResult = coderResult;
}
@Override
public boolean equals(Object other) {
if (!(other instanceof NulTerminatedCharsetDecoder.Result)) {
return false;
}
if (other == this) {
return true;
}
NulTerminatedCharsetDecoder.Result that = (NulTerminatedCharsetDecoder.Result) other;
return this.nulTerminatorReached == that.nulTerminatorReached
&& Objects.equals(this.coderResult, that.coderResult);
}
@Override
public int hashCode() {
return Objects.hash(nulTerminatorReached, coderResult);
}
@Override
public String toString() {
return String.format(
"%s nulTerminatorReached=%s coderResult=%s",
super.toString(), nulTerminatorReached, coderResult);
}
}
public NulTerminatedCharsetDecoder(CharsetDecoder decoder) {
this(decoder, DEFAULT_INITIAL_CHAR_BUFFER_CAPACITY);
}
public NulTerminatedCharsetDecoder(CharsetDecoder decoder, int initialCapacity) {
this.decoder = decoder;
this.charBuffer = CharBuffer.allocate((int) (initialCapacity * decoder.averageCharsPerByte()));
}
public static String decodeUTF8String(ByteBuffer in) throws CharacterCodingException {
return new NulTerminatedCharsetDecoder(StandardCharsets.UTF_8.newDecoder()).decodeString(in);
}
@SuppressWarnings("PMD.PrematureDeclaration")
public String decodeString(ByteBuffer in) throws CharacterCodingException {
reset();
int startPosition = in.position();
int nulOffset = findNulOffset(in);
if (nulOffset == in.limit()) {
throw new BufferUnderflowException();
}
int charBufferNeeded = (int) ((nulOffset - startPosition) * decoder.averageCharsPerByte());
if (charBuffer.capacity() < charBufferNeeded) {
charBuffer = CharBuffer.allocate(charBufferNeeded);
} else {
charBuffer.clear();
}
StringBuilder sb = new StringBuilder();
while (true) {
Result result = decodeChunk(in, nulOffset, charBuffer, true);
if (result.coderResult.isError()) {
result.coderResult.throwException();
}
charBuffer.flip();
sb.append(charBuffer);
charBuffer.compact();
if (result.nulTerminatorReached || !in.hasRemaining()) {
break;
}
}
return sb.toString();
}
public Result decode(ByteBuffer in, CharBuffer out, boolean endOfInput) {
return decodeChunk(in, findNulOffset(in), out, endOfInput);
}
private int findNulOffset(ByteBuffer in) {
int i;
for (i = in.position(); i < in.limit(); i++) {
if (in.get(i) == (byte) 0x00) {
break;
}
}
return i;
}
private Result decodeChunk(ByteBuffer in, int nulOffset, CharBuffer out, boolean endOfInput) {
Result result;
if (nulOffset == in.limit()) {
// We didn't find a NUL terminator. Decode what we can, but tell
// the caller we need to keep going.
CoderResult decoderResult = decoder.decode(in, out, endOfInput);
result = new Result(false, decoderResult);
} else {
// We found a NUL terminator, but we don't know if out has enough capacity
// to hold the values up to that point.
//
// Temporarily limit the buffer to exclude the NUL we found,
// decode as much as we can, and check if we made it to the NUL.
int oldLimit = in.limit();
in.limit(nulOffset);
CoderResult decoderResult = decoder.decode(in, out, true /* endOfInput */);
boolean nulTerminatorReached = !in.hasRemaining();
result = new Result(nulTerminatorReached, decoderResult);
in.limit(oldLimit);
if (nulTerminatorReached) {
// We consumed the entire buffer, so move past the NUL terminator.
in.position(nulOffset + 1);
}
}
return result;
}
public void reset() {
charBuffer.clear();
decoder.reset();
}
}