1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
/*
* Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/piot/blob-stream-rs
* Licensed under the MIT License. See LICENSE in the project root for license information.
*/
use core::fmt;
use std::error::Error;
use bit_array_rs::BitArray;
type ChunkIndex = usize;
#[derive(Debug)]
pub enum BlobError {
InvalidChunkIndex(usize, usize),
UnexpectedChunkSize(usize, usize, usize),
OutOfBounds,
RedundantSameContents(ChunkIndex),
RedundantContentDiffers(ChunkIndex),
}
impl fmt::Display for BlobError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidChunkIndex(id, max) => {
write!(f, "illegal chunk index: {id} (max: {max})")
}
Self::UnexpectedChunkSize(expected, found, id) => write!(
f,
"unexpected chunk size. expected {expected} but encountered {found} for chunk {id}"
),
Self::OutOfBounds => write!(f, "calculated slice range is out of bounds"),
Self::RedundantSameContents(chunk_index) => write!(f, "chunk {chunk_index} has already been received"),
Self::RedundantContentDiffers(chunk_index) => write!(f, "chunk {chunk_index} has already been received, but now received different content for that chunk. this is serious"),
}
}
}
impl Error for BlobError {} // it implements Debug and Display
/// A struct representing a stream of binary data divided into fixed-size chunks.
#[allow(unused)]
pub struct BlobStreamIn {
bit_array: BitArray,
fixed_chunk_size: usize,
octet_count: usize,
blob: Vec<u8>,
}
impl BlobStreamIn {
/// Creates a new `BlobStreamIn` instance with the specified number of octets and chunk size.
///
/// # Parameters
/// - `octet_count`: The total number of octets (bytes) in the stream.
/// - `fixed_chunk_size`: The size of each chunk in the stream.
///
/// # Panics
/// Panics if `fixed_chunk_size` is zero.
///
/// # Returns
/// A new `BlobStreamIn` instance.
#[allow(unused)]
#[must_use]
pub fn new(octet_count: usize, fixed_chunk_size: usize) -> Self {
assert!(
fixed_chunk_size > 0,
"fixed_chunk_size must be greater than zero"
);
let chunk_count = octet_count.div_ceil(fixed_chunk_size);
Self {
bit_array: BitArray::new(chunk_count),
fixed_chunk_size,
octet_count,
blob: vec![0u8; octet_count],
}
}
/// Returns the total number of expected chunks.
///
/// This function provides the total count of chunks that are expected
/// based on the size of the data and the chunk size.
///
/// # Returns
///
/// The total number of chunks (`usize`) that are expected for the data.
#[must_use]
pub const fn chunk_count(&self) -> usize {
self.bit_array.bit_count()
}
/// Checks if all chunks have been received.
///
/// # Returns
/// `true` if all chunks have been received; `false` otherwise.
#[must_use]
pub const fn is_complete(&self) -> bool {
self.bit_array.all_set()
}
/// Returns a reference to the complete blob if all chunks have been received.
///
/// # Returns
/// An `Option` containing a reference to the blob if complete; otherwise, `None`.
#[must_use]
pub fn blob(&self) -> Option<&[u8]> {
self.is_complete().then(|| &self.blob[..])
}
/// Sets a chunk of data at the specified `chunk_index` with the provided `payload`.
///
/// # Parameters
/// - `chunk_index`: The index of the chunk to set.
/// - `payload`: A slice of octets representing the chunk's data.
///
/// # Errors
/// Returns a `BlobError` if:
/// - The `chunk_index` is invalid.
/// - The `payload` size does not match the expected size for the chunk.
/// - The chunk has already been set, with either the same or different contents.
///
/// # Returns
/// `Ok(())` if the chunk was set successfully; otherwise, a `BlobError`.
pub fn set_chunk(&mut self, chunk_index: ChunkIndex, payload: &[u8]) -> Result<(), BlobError> {
let chunk_count = self.bit_array.bit_count();
if chunk_index >= chunk_count {
return Err(BlobError::InvalidChunkIndex(chunk_index, chunk_count));
}
let expected_size = if chunk_index == chunk_count - 1 {
// It was the last chunk
self.octet_count % self.fixed_chunk_size
} else {
self.fixed_chunk_size
};
if payload.len() != expected_size {
return Err(BlobError::UnexpectedChunkSize(
expected_size,
payload.len(),
chunk_index,
));
}
let octet_offset = chunk_index * self.fixed_chunk_size;
if octet_offset + expected_size > self.blob.len() {
return Err(BlobError::OutOfBounds);
}
if self.bit_array.get(chunk_index) {
// It has been set previously
let is_same_contents =
&self.blob[octet_offset..octet_offset + expected_size] == payload;
let err = if is_same_contents {
BlobError::RedundantSameContents(chunk_index)
} else {
BlobError::RedundantContentDiffers(chunk_index)
};
return Err(err);
}
self.blob[octet_offset..octet_offset + expected_size].copy_from_slice(payload);
self.bit_array.set(chunk_index);
Ok(())
}
}