rafx_base/
b3f.rs

1// Basic Binary Block Format (B3F)
2//
3// File Format
4// [4] magic number encoded as u32 (0xBB33FF00)
5// [4] file tag (arbitrary 4 bytes for user)
6// [4] version (arbitrary meaning for user, encoded as u32)
7// [4] block count (encoded as u32)
8// [8] bytes indicating 0 (0x00)
9// [8*n] ending offset of block
10// [x] pad to 16 byte offset
11// [n*len(n)] data (format/encoding/semantics would be implied by file tag). Each block begins at
12// [x] pad to 16 byte offset
13//
14// Endianness is undefined. Use the magic number to detect if endianness is different between
15// writer/reader
16//
17// This format can be encoded into a block, making this structure hierarchical. In this
18// case, omit the magic number, and use the file tag to optionally indicate the contents
19// of the block. (So it becomes a "block tag")
20//
21// if you c-cast the range memory from byte 16 to block count * 4, you have an array of u32 of n+1
22// length where n is number of blocks. Offset for block n is given by array[n]. End of block n is
23// given by array[n+1]. Size of block n in bytes is given by array[n+1] - array[n]
24//
25// Alignment of blocks to 16 bytes promotes reinterpreting bytes i.e. u8 to u64 or __m128 without
26// tripping over undefined behavior
27
28use std::convert::TryInto;
29
30const HEADER_SIZE_IN_BYTES: usize = 16;
31const BLOCK_LENGTH_SIZE_IN_BYTES: usize = 8;
32const BLOCK_ALIGNMENT_IN_BYTES: usize = 16;
33
34pub struct B3FReader<'a> {
35    data: &'a [u8],
36}
37
38impl<'a> B3FReader<'a> {
39    pub fn new(data: &'a [u8]) -> Option<B3FReader<'a>> {
40        if data.len() < 16 {
41            return None;
42        }
43
44        let magic_number = u32::from_ne_bytes(data[0..4].try_into().ok()?);
45        if magic_number != 0xBB33FF00 {
46            return None;
47        }
48
49        let reader = B3FReader { data };
50
51        Some(reader)
52    }
53
54    pub fn file_tag_as_u32(&self) -> u32 {
55        u32::from_ne_bytes(self.data[4..8].try_into().unwrap())
56    }
57
58    pub fn file_tag_as_u8(&self) -> &[u8] {
59        &self.data[4..8]
60    }
61
62    pub fn version(&self) -> u32 {
63        u32::from_ne_bytes(self.data[8..12].try_into().unwrap())
64    }
65
66    pub fn block_count(&self) -> usize {
67        u32::from_ne_bytes(self.data[12..16].try_into().unwrap()) as usize
68    }
69
70    pub fn get_block(
71        &self,
72        index: usize,
73    ) -> &'a [u8] {
74        // assumed by some implementation details here
75        debug_assert_eq!(BLOCK_LENGTH_SIZE_IN_BYTES, 8);
76        let begin_size_offset = HEADER_SIZE_IN_BYTES + (index * BLOCK_LENGTH_SIZE_IN_BYTES);
77        let size_data = &self.data[begin_size_offset..];
78        let mut begin = u64::from_ne_bytes(size_data[0..8].try_into().unwrap()) as usize;
79        let end = u64::from_ne_bytes(size_data[8..16].try_into().unwrap()) as usize;
80
81        // Begin position needs to be rounded up to 16-byte offset
82        begin = ((begin + BLOCK_ALIGNMENT_IN_BYTES - 1) / BLOCK_ALIGNMENT_IN_BYTES)
83            * BLOCK_ALIGNMENT_IN_BYTES;
84
85        let mut data_offset =
86            HEADER_SIZE_IN_BYTES + ((self.block_count() + 1) * BLOCK_LENGTH_SIZE_IN_BYTES);
87        data_offset = ((data_offset + BLOCK_ALIGNMENT_IN_BYTES - 1) / BLOCK_ALIGNMENT_IN_BYTES)
88            * BLOCK_ALIGNMENT_IN_BYTES;
89        &self.data[data_offset..][begin..end]
90    }
91}