My suggestion to not use ByteArrayOutputStream is because it really adds little value here. The result cannot be longer than bytes.length. We copy the result twice right now, just because you are don't want to keep an output index counter.

If the loop was using a new-style for loop with an iterator, maybe it would make sense to try and use a result buffer type that hides the result buffer pointer. But its not, its heavily relying on the index and direct array access because this is more efficient than doing a new substring call on each character position as you walk down the string. At that point it can be easier to follow the code if both the source and destination buffers are using the same approach, a direct pointer into the array.

Whatever, we can keep the ByteArrayOutputStream. I just think its silly to allocate that, and a temporary byte array, and copy the temporary byte array over to a real byte array, just so you can call RawParseUtils.decode(), which accepts an array, a starting position, and an ending position, and thus does not need the temporary array.