loggersoft.kotlin.streams.BitStream.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of binary-streams Show documentation
Show all versions of binary-streams Show documentation
Implementation of I/O binary streams for Kotlin.
/*
* Copyright (C) 2018 Alexander Kornilov ([email protected])
*
* 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 loggersoft.kotlin.streams
import java.io.Closeable
import java.io.EOFException
import java.io.Flushable
import java.math.BigInteger
import kotlin.experimental.and
import kotlin.experimental.inv
import kotlin.experimental.or
import kotlin.math.min
/**
* Bit access over [Stream].
*
* @author Alexander Kornilov ([email protected]).
*/
@Suppress("NOTHING_TO_INLINE")
class BitStream(private val stream: Stream) : Closeable, AutoCloseable, Flushable {
/**
* Constructs stream from [StreamInput]
*/
constructor(input: StreamInput): this(StreamAdapterInput(input))
/**
* Constructs stream from [StreamOutput]
*/
constructor(output: StreamOutput): this(StreamAdapterOutput(output))
/**
* Indicates that [position] of the stream can be changed.
*/
val isSeekable: Boolean = stream.isSeekable
/**
* Indicates that stream has fixed [size].
*/
val isFixedSize: Boolean = stream.isFixedSize
/**
* Indicates that stream is readable.
*/
val isReadable: Boolean = stream.isReadable
/**
* Indicates that stream is writable.
*/
val isWritable: Boolean = stream.isWritable
/**
* Current stream size in bytes or negative value if size isn't available at moment.
*/
val size: Long = stream.size
/**
* Current absolute byte position in the stream or -1 if not supported or not available at moment.
*/
var position: Long
get() = calculateActualPosition().first
set(value) {
if (!seekInCache(value, 0)) {
resetCache(false)
stream.position = value
}
bitOffset = 0
}
/**
* Bit offset in current byte (0..7).
*/
var offset: Int
get() = calculateActualPosition().second
set(value) {
val newOffset = when {
value < 0 -> 0
value > 7 -> 7
else -> value
}
if (!seekInCache(-1, newOffset)) {
resetCache()
bitOffset = newOffset
}
}
/**
* Current absolute bit position in the stream.
*/
var bitPosition: Long
get() {
val (actualPosition, actualOffset) = calculateActualPosition()
return if (actualPosition >= 0) actualPosition * 8 + actualOffset else -1
}
set(value) {
val newBitPosition = if (value >= 0) value else 0
val newPosition = newBitPosition / 8
val newOffset = (newBitPosition % 8).toInt()
if (!seekInCache(newPosition, newOffset)) {
resetCache(false)
position = newPosition
bitOffset = newOffset
}
}
/**
* Skips [bits] in the stream.
*/
fun skip(bits: Int): Boolean {
if (bits < 0) return false
if (bits == 0) return true
return if (seekInCache(bits)) true else {
val hasReadCache = cacheMode == CacheMode.Read && cacheBits >= 8
val availableBytes = if (cacheMode == CacheMode.Read) cacheAvailable / 8 else 0
resetCache(false)
val bitDelta = bits - if (hasReadCache) (8 - bitOffset) else 0
val skipBytes = bitDelta / 8 - availableBytes
bitOffset = bitDelta % 8
skipBytes(skipBytes)
}
}
/**
* Reads one bit from the stream.
* @throws EOFException
*/
fun readBit(): Boolean
= if (loadCache() >= 1) cache.and(1L.shl(cacheOffset++)) != 0L else throw EOFException()
/**
* Reads specified [bits] from the stream.
* The [bits] should be in range 1..64.
* If [signed] is true the last bit is interpreted as indicator of sign.
* Bits order is sequential that means BigEndian byte order for the
* integers which contain more then one byte.
* @throws EOFException
*/
fun readBits(bits: Int, signed: Boolean = false): Long {
require(bits in 1..64)
return readNumber(bits, signed, 0L, ::processBitsLong) { value, b, s ->
if (s && bits < 64 && value.shr(b - 1) != 0L) value.or(-1L shl(b)) else value
}
}
/**
* Reads byte from the stream.
* @throws EOFException
*/
fun readByte(): Byte = readBits(8, true).toByte()
/**
* Reads byte as unsigned integer from the stream.
* @throws EOFException
*/
fun readByteUnsigned(): Int = readBits(8, false).toInt()
/**
* Reads arbitrary number of [bits] from the stream.
* If [signed] is true the last bit is interpreted as indicator of sign.
* Bits order is sequential that means BigEndian byte order for the
* integers which contain more then one byte.
* @throws EOFException
*/
fun readBigInteger(bits: Int, signed: Boolean = false): BigInteger {
require(bits > 0)
return readNumber(bits, signed, BigInteger.ZERO, ::processBitsBigInteger) { value, b, s ->
if (s && value.shr(b - 1) != BigInteger.ZERO) {
var mask = BigInteger.ZERO
for (i in 0 until b) mask = mask.or(BigInteger.ONE.shl(i))
value.not().and(mask).negate() - BigInteger.ONE
} else value
}
}
/**
* Writes one bit to the stream.
*/
fun write(value: Boolean) {
prepareWrite()
cache = if (value) cache.or(1L.shl(cacheOffset))
else cache.and(1L.shl(cacheOffset).inv())
bitsWritten(1)
}
/**
* Writes one byte to the stream.
*/
fun write(value: Byte) = write(value.toLong(), 8)
/**
* Writes [bits] from the [Long]. The [bits] should be in 1..64.
* The bits are written in natural order that means BigEndian byte
* order for integers which have more than one byte.
*/
fun write(value: Long, bits: Int) {
require(bits in 1..64)
writeNumber(value, bits) { v, offset, bitmask, move ->
v.shr(offset).and(bitmask).shl(move)
}
}
/**
* Writes arbitrary number of [bits] from the [BigInteger].
* The bits are written in natural order that means BigEndian byte
* order for integers which have more than one byte.
*/
fun write(value: BigInteger, bits: Int) {
require(bits > 0)
writeNumber(value, bits) { v, offset, bitmask, move ->
v.shr(offset).toLong().and(bitmask).shl(move)
}
}
/**
* Flushes write cache into underlying stream.
*/
override fun flush() {
flushCache()
stream.flush()
}
/**
* Closes the stream (include underlying stream as well).
*/
override fun close() {
flushCache()
stream.close()
}
/**
* Writes bit as += operation.
* @see [write]
*/
operator fun plusAssign(value: Boolean) = write(value)
/**
* Writes byte as += operator.
* @see [write]
*/
operator fun plusAssign(value: Byte) = write(value)
private enum class CacheMode {
Empty,
Read,
Write
}
private var bitOffset = 0
private var cache = 0L
private var cacheOffset = 0
private var cacheBits = 0
private var cacheMode = CacheMode.Empty
private val readBuffer: ByteArray = ByteArray(8)
private inline val cacheAvailable
inline get() = cacheBits - cacheOffset
private inline val cacheCapacity
inline get() = 64 - cacheBits
private fun skipBytes(bytes: Int): Boolean {
if (bytes < 0) return false
if (bytes == 0) return true
val skipped = stream.skip(bytes.toLong()).toInt()
if (skipped == bytes) return true
if (skipped > bytes) return false
val rest = if (skipped <= 0) bytes else bytes - skipped
if (stream.isSeekable) {
val currentPosition = stream.position
if (currentPosition >= 0) {
stream.position = currentPosition + rest
return stream.position == currentPosition + rest
}
}
for(i in 0 until (readBuffer.size / rest)) if (stream.readBytes(readBuffer) != readBuffer.size) return false
val lastBlock = readBuffer.size % rest
return if (lastBlock > 0) stream.readBytes(readBuffer, lastBlock) == lastBlock else true
}
private inline fun processBitsLong(value: Long, offset: Int, cacheBits: Long): Long
= value.or(cacheBits.shl(offset))
private inline fun processBitsBigInteger(value: BigInteger, offset: Int, cacheBits: Long): BigInteger
= value.or(cacheBits.toBigIntegerUnsigned().shl(offset))
private inline fun readNumber(bits: Int, signed: Boolean, initValue: T, processBits: (value: T, offset: Int, cacheBits: Long) -> T, finalize: (value: T, bits: Int, signed: Boolean) -> T): T {
var count = 0
var result = initValue
while (count < bits && loadCache() > 0) {
val portion = min(cacheAvailable, bits - count)
result = processBits(result, count, getCacheBits(portion))
cacheOffset += portion
count += portion
}
return if (count < bits) throw EOFException() else finalize(result, bits, signed)
}
private inline fun getCacheBits(bits: Int): Long = cache.shr(cacheOffset).and(longBitmask(bits))
private inline fun canReadByte() = stream.isReadable && stream.isSeekable && stream.canRead(1)
private inline fun prepareWrite() {
cacheMode = when(cacheMode) {
CacheMode.Empty -> CacheMode.Write
CacheMode.Read -> { resetCache(); CacheMode.Write }
CacheMode.Write -> { if (cacheCapacity <= 0) flushCache(); return }
}
cacheOffset = bitOffset
cacheBits = if (bitOffset > 0 && canReadByte()) {
cache = stream.readByteUnsigned().toLong()
stream.position--
8
} else bitOffset
}
private inline fun bitsWritten(bits: Int) {
cacheOffset += bits
if (cacheOffset > cacheBits) cacheBits = cacheOffset
}
private inline fun writeNumber(value: T, bits: Int, getBits: (value: T, offset: Int, bitmask: Long, move: Int) -> Long) {
prepareWrite()
var count = 0
while (count < bits) {
if (cacheCapacity <= 0) flushCache()
val portion = min(cacheCapacity, bits - count)
val bitmask = longBitmask(portion)
cache = cache.and(bitmask.shl(cacheOffset).inv())
cache = cache.or(getBits(value, count, bitmask, cacheOffset))
bitsWritten(portion)
count += portion
}
}
private fun loadCache(): Int {
when(cacheMode) {
CacheMode.Read -> if (cacheAvailable > 0) return cacheAvailable
CacheMode.Write -> {
resetCache()
cacheMode = CacheMode.Read
}
CacheMode.Empty -> {
cacheOffset = bitOffset
cacheMode = CacheMode.Read
}
}
if (cacheCapacity < 8 || cacheCapacity % 8 != 0) clearCache()
val retrieved = stream.readBytes(readBuffer, cacheCapacity / 8)
if (retrieved <= 0) return cacheAvailable
for (index in 0 until retrieved) {
cache = cache.and(0xFFL.shl(cacheBits).inv())
cache = cache.or(readBuffer[index].toLong().and(0xFF).shl(cacheBits))
cacheBits += 8
}
return cacheAvailable
}
private fun resetCache(updatePosition: Boolean = true) {
val (actualPosition, actualOffset) = calculateActualPosition()
when (cacheMode) {
CacheMode.Read -> if (updatePosition) stream.position = actualPosition
CacheMode.Write -> flushCache()
else -> {}
}
cacheMode = CacheMode.Empty
bitOffset = actualOffset
clearCache()
}
private fun flushCache() {
if (cacheMode != CacheMode.Write || cacheBits <= 0) return
val bitRest = cacheBits % 8
if (bitRest == 0 || !canReadByte()) {
writeCache((cacheBits + 7) / 8)
clearCache()
return
}
val bytes = cacheOffset / 8
if (bytes > 0) writeCache(bytes)
val bitMask = byteBitmask(bitRest)
val lastBits = cache.shr(bytes * 8).toByte().and(bitMask)
val mergedLastByte = stream.readByte().and(bitMask.inv()).or(lastBits)
stream.position--
stream.writeByte(mergedLastByte)
stream.position--
clearCache()
}
private inline fun clearCache() {
cache = 0L
cacheOffset = 0
cacheBits = 0
}
private inline fun writeCache(bytes: Int) = stream.writeInt(cache, bytes, nativeByteOrder)
private fun calculateActualPosition(): Pair {
val streamPosition = stream.position
if (cacheBits <= 0 || cacheOffset < 0) return Pair(streamPosition, bitOffset)
val cacheBytes = cacheBits / 8
return when(cacheMode) {
CacheMode.Empty -> Pair(streamPosition, bitOffset)
CacheMode.Read -> if (streamPosition >= cacheBytes) Pair(streamPosition - cacheBytes + cacheOffset / 8, cacheOffset % 8)
else Pair(streamPosition, cacheOffset % 8)
CacheMode.Write -> Pair(streamPosition + cacheOffset / 8, cacheOffset % 8)
}
}
private fun seekInCache(position: Long, offset: Int = -1): Boolean =
seekInCache((if (position >= 0) (position - this.position).toInt() * 8 else 0) +
(if (offset >= 0) offset - this.offset else 0))
private fun seekInCache(bitDelta: Int): Boolean =
when (cacheMode) {
CacheMode.Empty -> false
CacheMode.Read -> if (cacheOffset + bitDelta in 0..cacheBits) { cacheOffset += bitDelta; true } else false
CacheMode.Write -> if (bitDelta < 0 && cacheOffset + bitDelta >= 0) { cacheOffset += bitDelta; true } else false
}
}