@4sure-tech/vc-bitstring-status-lists
Version:
TypeScript library for W3C Bitstring Status List v1.0 specification - privacy-preserving credential status management
212 lines (180 loc) • 7.05 kB
text/typescript
import {assertIsNonNegativeInteger, assertIsPositiveInteger} from "../utils/assertions"
import {StatusRangeError} from "../status-list/errors";
const LIST_BLOCK_SIZE = 16384 // 16KB
/**
* BitManager - Low-level bitstring manipulation for W3C Bitstring Status Lists
*
* Manages a packed bitstring where credentials are mapped to bit positions.
* Each credential gets a fixed-width status entry at position: credentialIndex * statusSize.
*
* Features:
* - Direct bit access without entry tracking
* - Automatic buffer expansion in 16KB blocks
* - MSB-first bit ordering within bytes
* - Zero-initialized status values
*
* @example
* ```typescript
* const manager = new BitManager({ statusSize: 2 })
* manager.setStatus(0, 3) // Sets 2 bits at position 0
* console.log(manager.getStatus(0)) // Returns 3
* console.log(manager.getStatus(1)) // Returns 0 (unset)
* ```
*/
export class BitManager {
private bits: Uint8Array
private readonly statusSize: number
/**
* Creates a new BitManager instance
*
* @param options.statusSize - Bits per credential status (default: 1, max: 31)
* @param options.buffer - Existing buffer for decoding
* @param options.initialSize - Initial buffer size in bytes (default: 16KB)
*/
constructor(options: {
statusSize?: number
buffer?: Uint8Array
initialSize?: number
} = {}) {
const {statusSize = 1, buffer, initialSize = LIST_BLOCK_SIZE} = options
assertIsPositiveInteger(statusSize, 'statusSize')
// Guard against JavaScript bitwise operation overflow (which is insane for this use case, but it's the real limit.)
if (statusSize > 30) {
throw new StatusRangeError(`statusSize ${statusSize} exceeds maximum supported value of 30 bits`)
}
this.statusSize = statusSize
this.bits = buffer ? new Uint8Array(buffer) : new Uint8Array(initialSize)
}
/**
* Gets the status value for a credential
*
* @param credentialIndex - Non-negative credential identifier
* @returns Status value (0 to 2^statusSize - 1)
*/
getStatus(credentialIndex: number): number {
assertIsNonNegativeInteger(credentialIndex, 'credentialIndex')
// Check if index exceeds reasonable bounds
const maxIndex = Math.floor(this.bits.length * 8 / this.statusSize)
if (credentialIndex >= maxIndex) {
throw new StatusRangeError(`credentialIndex ${credentialIndex} exceeds buffer bounds`)
}
const startBit = credentialIndex * this.statusSize
return this.readStatusBits(startBit)
}
/**
* Sets the status value for a credential
*
* @param credentialIndex - Non-negative credential identifier
* @param status - Status value (0 to 2^statusSize - 1)
* @throws {StatusRangeError} If status exceeds maximum value for statusSize
*/
setStatus(credentialIndex: number, status: number): void {
assertIsNonNegativeInteger(credentialIndex, 'credentialIndex')
assertIsNonNegativeInteger(status, 'status')
const maxValue = (1 << this.statusSize) - 1
if (status > maxValue) {
throw new StatusRangeError(`Status ${status} exceeds maximum value ${maxValue} for ${this.statusSize} bits`)
}
const startBit = credentialIndex * this.statusSize
this.writeStatusBits(startBit, status)
}
/**
* Returns current buffer trimmed to actual data size
*
* @returns Copy of buffer containing only written data
*/
toBuffer(): Uint8Array {
// search backwards from the end for non-zero bytes
let lastNonZeroByte = -1
for (let i = this.bits.length - 1; i >= 0; i--) {
if (this.bits[i] !== 0) {
lastNonZeroByte = i
break
}
}
if (lastNonZeroByte === -1) {
return new Uint8Array([])
}
return this.bits.slice(0, lastNonZeroByte + 1)
}
/**
* Gets the uniform status size for all entries
*
* @returns Number of bits per status entry
*/
getStatusSize(): number {
return this.statusSize
}
/**
* Gets the total buffer size in bytes
*
* @returns Buffer size in bytes
*/
getBufferLength(): number {
return this.bits.length
}
/**
* Reads status bits from buffer starting at bit position
*
* @param startBit - Starting bit position
* @returns Decoded status value
*/
private readStatusBits(startBit: number): number {
let status = 0
for (let i = 0; i < this.statusSize; i++) {
const bitIndex = startBit + i
const byteIndex = Math.floor(bitIndex / 8)
const bitOffset = bitIndex % 8
if (byteIndex >= this.bits.length) {
continue
}
const bit = (this.bits[byteIndex] >> (7 - bitOffset)) & 1
// MSB of status value should be leftmost bit
status |= bit << (this.statusSize - i - 1)
}
return status
}
/**
* Writes status bits to buffer starting at bit position
*
* @param startBit - Starting bit position
* @param status - Status value to write
*/
private writeStatusBits(startBit: number, status: number): void {
for (let i = 0; i < this.statusSize; i++) {
const bitIndex = startBit + i
const byteIndex = Math.floor(bitIndex / 8)
const bitOffset = bitIndex % 8
this.ensureBufferCanHold(byteIndex + 1)
// MSB of status value should be leftmost bit
const bit = (status >> (this.statusSize - i - 1)) & 1
if (bit === 1) {
this.bits[byteIndex] |= (1 << (7 - bitOffset))
} else {
this.bits[byteIndex] &= ~(1 << (7 - bitOffset))
}
}
}
/**
* Ensures buffer can hold the specified number of bytes
* Grows in bitstring-sized chunks for better memory efficiency
*
* @param requiredBytes - Minimum bytes needed
*/
private ensureBufferCanHold(requiredBytes: number): void {
if (requiredBytes > this.bits.length) {
// Calculate growth in bitstring-sized chunks rather than fixed 16KB blocks
const bitsNeeded = requiredBytes * 8
const entriesNeeded = Math.ceil(bitsNeeded / this.statusSize)
const optimalBits = entriesNeeded * this.statusSize
const optimalBytes = Math.ceil(optimalBits / 8)
// Still ensure minimum block size for reasonable growth
const minGrowthBytes = Math.max(optimalBytes, LIST_BLOCK_SIZE)
const blocksNeeded = Math.ceil(minGrowthBytes / LIST_BLOCK_SIZE)
const newSize = blocksNeeded * LIST_BLOCK_SIZE
const newBuffer = new Uint8Array(newSize)
newBuffer.set(this.bits)
this.bits = newBuffer
}
}
}