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
// Basic Binary Block Format (B3F)
//
// File Format
// [4] magic number encoded as u32 (0xBB33FF00)
// [4] file tag (arbitrary 4 bytes for user)
// [4] version (arbitrary meaning for user, encoded as u32)
// [4] block count (encoded as u32)
// [8] bytes indicating 0 (0x00)
// [8*n] ending offset of block
// [x] pad to 16 byte offset
// [n*len(n)] data (format/encoding/semantics would be implied by file tag). Each block begins at
// [x] pad to 16 byte offset
//
// Endianness is undefined. Use the magic number to detect if endianness is different between
// writer/reader
//
// This format can be encoded into a block, making this structure hierarchical. In this
// case, omit the magic number, and use the file tag to optionally indicate the contents
// of the block. (So it becomes a "block tag")
//
// if you c-cast the range memory from byte 16 to block count * 4, you have an array of u32 of n+1
// length where n is number of blocks. Offset for block n is given by array[n]. End of block n is
// given by array[n+1]. Size of block n in bytes is given by array[n+1] - array[n]
//
// Alignment of blocks to 16 bytes promotes reinterpreting bytes i.e. u8 to u64 or __m128 without
// tripping over undefined behavior

use std::convert::TryInto;

const HEADER_SIZE_IN_BYTES: usize = 16;
const BLOCK_LENGTH_SIZE_IN_BYTES: usize = 8;
const BLOCK_ALIGNMENT_IN_BYTES: usize = 16;

pub struct B3FReader<'a> {
    data: &'a [u8],
}

impl<'a> B3FReader<'a> {
    pub fn new(data: &'a [u8]) -> Option<B3FReader<'a>> {
        if data.len() < 16 {
            return None;
        }

        let magic_number = u32::from_ne_bytes(data[0..4].try_into().ok()?);
        if magic_number != 0xBB33FF00 {
            return None;
        }

        let reader = B3FReader { data };

        Some(reader)
    }

    pub fn file_tag_as_u32(&self) -> u32 {
        u32::from_ne_bytes(self.data[4..8].try_into().unwrap())
    }

    pub fn file_tag_as_u8(&self) -> &[u8] {
        &self.data[4..8]
    }

    pub fn version(&self) -> u32 {
        u32::from_ne_bytes(self.data[8..12].try_into().unwrap())
    }

    pub fn block_count(&self) -> usize {
        u32::from_ne_bytes(self.data[12..16].try_into().unwrap()) as usize
    }

    pub fn get_block(
        &self,
        index: usize,
    ) -> &'a [u8] {
        // assumed by some implementation details here
        debug_assert_eq!(BLOCK_LENGTH_SIZE_IN_BYTES, 8);
        let begin_size_offset = HEADER_SIZE_IN_BYTES + (index * BLOCK_LENGTH_SIZE_IN_BYTES);
        let size_data = &self.data[begin_size_offset..];
        let mut begin = u64::from_ne_bytes(size_data[0..8].try_into().unwrap()) as usize;
        let end = u64::from_ne_bytes(size_data[8..16].try_into().unwrap()) as usize;

        // Begin position needs to be rounded up to 16-byte offset
        begin = ((begin + BLOCK_ALIGNMENT_IN_BYTES - 1) / BLOCK_ALIGNMENT_IN_BYTES)
            * BLOCK_ALIGNMENT_IN_BYTES;

        let mut data_offset =
            HEADER_SIZE_IN_BYTES + ((self.block_count() + 1) * BLOCK_LENGTH_SIZE_IN_BYTES);
        data_offset = ((data_offset + BLOCK_ALIGNMENT_IN_BYTES - 1) / BLOCK_ALIGNMENT_IN_BYTES)
            * BLOCK_ALIGNMENT_IN_BYTES;
        &self.data[data_offset..][begin..end]
    }
}