sbf-tools 0.1.0

Septentrio Binary Format (SBF) parser library
Documentation
use std::io::{self, Cursor, Read};

use sbf_tools::{block_ids, calculate_block_crc, SbfBlock, SbfError, SbfReadExt};

const UNKNOWN_BLOCK_ID: u16 = 8190;
const TEST_WNC: u16 = 2200;

struct ChunkedReader {
    inner: Cursor<Vec<u8>>,
    chunk_size: usize,
}

impl ChunkedReader {
    fn new(data: Vec<u8>, chunk_size: usize) -> Self {
        Self {
            inner: Cursor::new(data),
            chunk_size,
        }
    }
}

impl Read for ChunkedReader {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let max = buf.len().min(self.chunk_size);
        self.inner.read(&mut buf[..max])
    }
}

fn build_block(block_id: u16, block_rev: u8, tow_ms: u32, wnc: u16, body: &[u8]) -> Vec<u8> {
    let mut total_len = 8 + 6 + body.len();
    while (total_len & 0x03) != 0 {
        total_len += 1;
    }

    let mut block = vec![0u8; total_len];
    let id_rev = block_id | ((u16::from(block_rev) & 0x07) << 13);

    block[0] = 0x24;
    block[1] = 0x40;
    block[4..6].copy_from_slice(&id_rev.to_le_bytes());
    block[6..8].copy_from_slice(&(total_len as u16).to_le_bytes());
    block[8..12].copy_from_slice(&tow_ms.to_le_bytes());
    block[12..14].copy_from_slice(&wnc.to_le_bytes());
    block[14..14 + body.len()].copy_from_slice(body);

    let crc = calculate_block_crc(&block[4..total_len]);
    block[2..4].copy_from_slice(&crc.to_le_bytes());
    block
}

fn with_bad_crc(mut block: Vec<u8>) -> Vec<u8> {
    block[2] ^= 0xff;
    block
}

#[test]
fn reader_resynchronizes_after_crc_error_and_preserves_unknown_block() {
    let receiver_time = build_block(
        block_ids::RECEIVER_TIME,
        0,
        1_000,
        TEST_WNC,
        &[24, 3, 31, 23, 59, 58, 18u8, 4],
    );
    let corrupted_end_of_meas =
        with_bad_crc(build_block(block_ids::END_OF_MEAS, 0, 1_001, TEST_WNC, &[]));
    let unknown = build_block(UNKNOWN_BLOCK_ID, 1, 1_002, TEST_WNC, &[0xde, 0xad, 0xbe]);
    let end_of_meas = build_block(block_ids::END_OF_MEAS, 0, 1_003, TEST_WNC, &[]);

    let mut stream = vec![0xaa, 0xbb, 0xcc];
    stream.extend_from_slice(&receiver_time);
    stream.extend_from_slice(&corrupted_end_of_meas);
    stream.extend_from_slice(&unknown);
    stream.extend_from_slice(&end_of_meas);

    let mut reader = ChunkedReader::new(stream.clone(), 5).sbf_blocks();

    let first = reader.read_block().unwrap().unwrap();
    let SbfBlock::ReceiverTime(time) = first else {
        panic!("expected ReceiverTime");
    };
    assert_eq!(time.tow_ms(), 1_000);
    assert_eq!(time.wnc(), TEST_WNC);
    assert_eq!(time.utc_string(), "2024-03-31T23:59:58Z");
    assert!(time.is_synchronized());

    let second = reader.read_block().unwrap().unwrap();
    let SbfBlock::Unknown { id, rev, data } = second else {
        panic!("expected Unknown block");
    };
    assert_eq!(id, UNKNOWN_BLOCK_ID);
    assert_eq!(rev, 1);
    assert_eq!(data, unknown[8..].to_vec());

    let third = reader.read_block().unwrap().unwrap();
    let SbfBlock::EndOfMeas(block) = third else {
        panic!("expected EndOfMeas");
    };
    assert_eq!(block.tow_ms(), 1_003);
    assert_eq!(block.wnc(), TEST_WNC);

    assert!(reader.read_block().unwrap().is_none());

    let stats = reader.stats();
    assert_eq!(stats.bytes_read, stream.len() as u64);
    assert_eq!(stats.blocks_parsed, 3);
    assert_eq!(stats.crc_errors, 1);
    assert_eq!(stats.parse_errors, 0);
    assert!(stats.bytes_skipped >= 4);
}

#[test]
fn reader_reports_incomplete_final_block() {
    let receiver_time = build_block(
        block_ids::RECEIVER_TIME,
        0,
        2_000,
        TEST_WNC,
        &[24, 4, 1, 0, 0, 0, 18u8, 3],
    );
    let mut truncated_end_of_meas = build_block(block_ids::END_OF_MEAS, 0, 2_001, TEST_WNC, &[]);
    truncated_end_of_meas.truncate(truncated_end_of_meas.len() - 3);

    let mut stream = receiver_time;
    stream.extend_from_slice(&truncated_end_of_meas);

    let mut reader = ChunkedReader::new(stream, 4).sbf_blocks();

    let first = reader.read_block().unwrap().unwrap();
    assert!(matches!(first, SbfBlock::ReceiverTime(_)));

    let err = reader.read_block().unwrap_err();
    let SbfError::IncompleteBlock { needed, have } = err else {
        panic!("expected IncompleteBlock");
    };
    assert_eq!(needed, 8);
    assert!(have >= 2);
}