Decoder.java
/*
* Copyright (c) 2025 Tanagra Space
* SPDX-License-Identifier: MIT
*/
package space.tanagra.ccsds124;
/**
* Decoding functions for CCSDS 124.0-B-1 decompression.
*
* <p>This class provides the COUNT, RLE, and bit insertion decoding functions that reverse the
* encoding operations defined in CCSDS 124.0-B-1.
*/
public final class Decoder {
private Decoder() {
// Utility class
}
/**
* Decodes a COUNT-encoded value from the bit stream.
*
* <p>Returns 0 for terminator '10', otherwise returns the decoded count value.
*
* @param reader the bit reader
* @return the decoded value (0 for terminator, >= 1 for count)
* @throws Ccsds124Exception if decoding fails
*/
public static int countDecode(BitReader reader) throws Ccsds124Exception {
int bit0 = reader.readBit();
if (bit0 == 0) {
// '0' means value = 1
return 1;
}
// First bit is 1
int bit1 = reader.readBit();
if (bit1 == 0) {
// '10' is terminator
return 0;
}
// '11...'
int bit2 = reader.readBit();
if (bit2 == 0) {
// '110' + 5 bits for values 2-33
int value = reader.readBits(5);
return value + 2;
}
// '111' + extended encoding for values >= 34
// Count zeros to determine field size (C uses do-while that includes terminating '1')
int size = 0;
int nextBit;
do {
nextBit = reader.readBit();
size++;
} while (nextBit == 0);
// Back up one bit since the '1' is part of the value (like C implementation)
// Since we can't back up, we account for it differently:
// value_bits = size + 5, but we already consumed the leading '1'
// So we read (size + 5 - 1) more bits and prepend the '1'
int numRemainingBits = size + 5 - 1;
int value = (1 << numRemainingBits) | reader.readBits(numRemainingBits);
return value + 2;
}
/**
* Decodes an RLE-encoded BitVector from the bit stream.
*
* <p>RLE decoding processes from the end of the vector (matching encoding which goes LSB to MSB).
* Each COUNT value represents a delta (number of zeros + 1 to skip), then sets a '1' bit.
*
* @param reader the bit reader
* @param length the expected length of the output vector
* @return the decoded BitVector
* @throws Ccsds124Exception if decoding fails
*/
public static BitVector rleDecode(BitReader reader, int length) throws Ccsds124Exception {
BitVector result = new BitVector(length);
rleDecodeInto(reader, result);
return result;
}
/**
* Decodes an RLE-encoded BitVector into a pre-allocated buffer.
*
* <p>The target buffer is zeroed before decoding. This avoids allocation in hot paths.
*
* @param reader the bit reader
* @param target pre-allocated BitVector to receive the result
* @throws Ccsds124Exception if decoding fails
*/
public static void rleDecodeInto(BitReader reader, BitVector target) throws Ccsds124Exception {
// Zero the target first
target.zero();
int length = target.length();
int bitPosition = length;
// Read COUNT values until terminator
int delta = countDecode(reader);
while (delta != 0) {
// A delta beyond the remaining bit position means the encoding is
// invalid for this vector length (GOTCHAS #20): reject it instead of
// silently skipping.
if (delta > bitPosition) {
throw new Ccsds124Exception("Invalid RLE delta: exceeds remaining bit position");
}
bitPosition -= delta;
target.setBit(bitPosition, 1);
delta = countDecode(reader);
}
}
/**
* Inserts bits from the stream into a BitVector where mask is 1.
*
* <p>Bits are inserted in reverse order (matching BE extraction).
*
* @param reader the bit reader
* @param data the BitVector to fill (modified in place)
* @param mask the mask indicating positions to fill
* @throws Ccsds124Exception if not enough bits available
*/
public static void bitInsert(BitReader reader, BitVector data, BitVector mask)
throws Ccsds124Exception {
bitInsert(reader, data, mask, null);
}
/**
* Inserts bits from the stream into a BitVector where mask is 1.
*
* <p>Bits are inserted in reverse order (matching BE extraction). Uses a pre-allocated scratch
* buffer to avoid allocation in hot paths.
*
* @param reader the bit reader
* @param data the BitVector to fill (modified in place)
* @param mask the mask indicating positions to fill
* @param scratch pre-allocated int array of at least mask.hammingWeight() size, or null to
* allocate
* @throws Ccsds124Exception if not enough bits available
*/
public static void bitInsert(BitReader reader, BitVector data, BitVector mask, int[] scratch)
throws Ccsds124Exception {
int length = Math.min(data.length(), mask.length());
// Count mask bits to know how many we need to insert
int posCount = mask.hammingWeight();
if (posCount == 0) {
return;
}
// Use scratch buffer or allocate
int[] bits = (scratch != null && scratch.length >= posCount) ? scratch : new int[posCount];
// Read all bits we need
for (int i = 0; i < posCount; i++) {
bits[i] = reader.readBit();
}
// Insert in reverse order: last read bit goes to first mask position
int bitIdx = posCount - 1;
for (int i = 0; i < length && bitIdx >= 0; i++) {
if (mask.getBit(i) != 0) {
data.setBit(i, bits[bitIdx--]);
}
}
}
}