qualia_core_db/q42/volume/
index.rs1use std::io;
4
5use super::super::{
6 BIDX_MAGIC, FIELD_RANGE_INDEX_ENTRY_BYTES, FIELD_RANGE_INDEX_HEADER_BYTES,
7 FIELD_RANGE_INDEX_MAGIC,
8};
9
10const BIDX_HEADER_BYTES: usize = 16;
11const BIDX_ENTRY_BYTES: usize = 16;
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub struct BidxBlockRange {
20 pub start: usize,
21 pub end: usize,
22}
23
24impl BidxBlockRange {
25 #[inline]
26 pub fn len(self) -> usize {
27 self.end - self.start
28 }
29
30 #[inline]
31 pub fn is_empty(self) -> bool {
32 self.start == self.end
33 }
34}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub struct BidxMatchPage {
39 pub range: BidxBlockRange,
41 pub returned: usize,
43 pub next_cursor: Option<usize>,
45}
46
47fn invalid(message: impl Into<String>) -> io::Error {
48 io::Error::new(io::ErrorKind::InvalidData, message.into())
49}
50
51fn layout(bidx: &[u8]) -> io::Result<usize> {
52 if bidx.len() < BIDX_HEADER_BYTES {
53 return Err(invalid("BIDX section is shorter than its header"));
54 }
55 if bidx[0..4] != BIDX_MAGIC {
56 return Err(invalid("invalid BIDX magic"));
57 }
58 let version = u32::from_le_bytes(bidx[4..8].try_into().unwrap());
59 if version != 1 {
60 return Err(invalid(format!("unsupported BIDX version {version}")));
61 }
62 let block_count = u32::from_le_bytes(bidx[8..12].try_into().unwrap()) as usize;
63 let expected = BIDX_HEADER_BYTES
64 .checked_add(
65 block_count
66 .checked_mul(BIDX_ENTRY_BYTES)
67 .ok_or_else(|| invalid("BIDX entry count overflows usize"))?,
68 )
69 .ok_or_else(|| invalid("BIDX length overflows usize"))?;
70 if bidx.len() != expected {
71 return Err(invalid(format!(
72 "BIDX length {} does not match {block_count} entries ({expected} bytes)",
73 bidx.len()
74 )));
75 }
76 Ok(block_count)
77}
78
79fn range_at(bidx: &[u8], index: usize) -> (u64, u64) {
80 let offset = BIDX_HEADER_BYTES + index * BIDX_ENTRY_BYTES;
81 let min = u64::from_le_bytes(bidx[offset..offset + 8].try_into().unwrap());
82 let max = u64::from_le_bytes(bidx[offset + 8..offset + 16].try_into().unwrap());
83 (min, max)
84}
85
86pub(crate) fn validate_bidx(bidx: &[u8], expected_blocks: usize) -> io::Result<()> {
90 let block_count = layout(bidx)?;
91 if block_count != expected_blocks {
92 return Err(invalid(format!(
93 "BIDX block count {block_count} does not match directory count {expected_blocks}"
94 )));
95 }
96
97 let mut previous_min = 0u64;
98 let mut previous_max = 0u64;
99 for index in 0..block_count {
100 let (min, max) = range_at(bidx, index);
101 if min > max {
102 return Err(invalid(format!("BIDX entry {index} has min > max")));
103 }
104 if index != 0 && (min < previous_min || max < previous_max) {
105 return Err(invalid(format!(
106 "BIDX entry {index} is not monotonic by min/max object hash"
107 )));
108 }
109 previous_min = min;
110 previous_max = max;
111 }
112 Ok(())
113}
114
115pub(crate) fn validate_field_range_index(bytes: &[u8], expected_blocks: usize) -> io::Result<()> {
116 let expected = FIELD_RANGE_INDEX_HEADER_BYTES
117 .checked_add(
118 expected_blocks
119 .checked_mul(FIELD_RANGE_INDEX_ENTRY_BYTES)
120 .ok_or_else(|| invalid("field-range index length overflows"))?,
121 )
122 .ok_or_else(|| invalid("field-range index length overflows"))?;
123 if bytes.len() != expected {
124 return Err(invalid(format!(
125 "field-range index is {} bytes, expected {expected}",
126 bytes.len()
127 )));
128 }
129 if bytes[0..4] != FIELD_RANGE_INDEX_MAGIC {
130 return Err(invalid("field-range index has bad magic"));
131 }
132 if u32::from_le_bytes(bytes[4..8].try_into().unwrap()) != 1 {
133 return Err(invalid("unsupported field-range index version"));
134 }
135 if u32::from_le_bytes(bytes[8..12].try_into().unwrap()) as usize != expected_blocks {
136 return Err(invalid("field-range index block count mismatch"));
137 }
138 for index in 0..expected_blocks {
139 let offset = FIELD_RANGE_INDEX_HEADER_BYTES + index * FIELD_RANGE_INDEX_ENTRY_BYTES;
140 for field in 0..3 {
141 let min_offset = offset + field * 16;
142 let min = u64::from_le_bytes(bytes[min_offset..min_offset + 8].try_into().unwrap());
143 let max =
144 u64::from_le_bytes(bytes[min_offset + 8..min_offset + 16].try_into().unwrap());
145 if min > max {
146 return Err(invalid("field-range index has an inverted range"));
147 }
148 }
149 }
150 Ok(())
151}
152
153pub(crate) fn bidx_block_range_for_hash(
159 bidx: &[u8],
160 object_hash: u64,
161) -> io::Result<Option<BidxBlockRange>> {
162 let block_count = layout(bidx)?;
163 validate_bidx(bidx, block_count)?;
164 if block_count == 0 {
165 return Ok(None);
166 }
167
168 let mut lo = 0usize;
170 let mut hi = block_count;
171 while lo < hi {
172 let mid = lo + (hi - lo) / 2;
173 if range_at(bidx, mid).1 < object_hash {
174 lo = mid + 1;
175 } else {
176 hi = mid;
177 }
178 }
179 let start = lo;
180 if start == block_count || range_at(bidx, start).0 > object_hash {
181 return Ok(None);
182 }
183
184 lo = start;
186 hi = block_count;
187 while lo < hi {
188 let mid = lo + (hi - lo) / 2;
189 if range_at(bidx, mid).0 <= object_hash {
190 lo = mid + 1;
191 } else {
192 hi = mid;
193 }
194 }
195
196 Ok(Some(BidxBlockRange { start, end: lo }))
197}
198
199pub(crate) fn bidx_blocks_for_hash_into(
201 bidx: &[u8],
202 object_hash: u64,
203 cursor: usize,
204 out: &mut [usize],
205) -> io::Result<Option<BidxMatchPage>> {
206 let Some(range) = bidx_block_range_for_hash(bidx, object_hash)? else {
207 return Ok(None);
208 };
209 if out.is_empty() && cursor < range.len() {
210 return Err(io::Error::new(
211 io::ErrorKind::InvalidInput,
212 "BIDX page buffer must contain at least one block index",
213 ));
214 }
215 if cursor > range.len() {
216 return Err(io::Error::new(
217 io::ErrorKind::InvalidInput,
218 "BIDX cursor is beyond the matching interval",
219 ));
220 }
221 let remaining = range.len() - cursor;
222 let returned = remaining.min(out.len());
223 for (offset, slot) in out.iter_mut().take(returned).enumerate() {
224 *slot = range.start + cursor + offset;
225 }
226 let next = cursor + returned;
227 Ok(Some(BidxMatchPage {
228 range,
229 returned,
230 next_cursor: (next < range.len()).then_some(next),
231 }))
232}