hurray_ffi/file/kv.rs
1//! The file-level KV metadata section, in both directions.
2//!
3//! One tagged value type serves reads and writes. That is deliberate: a KV section read
4//! from one file can be handed straight to another file's writer, which is the operation
5//! a tool that rewrites a container actually performs. A read-only accessor set and a
6//! separate write-only struct would have made that a transcription loop.
7//!
8//! On a read, the pointers inside a value borrow from the reader handle; on a write, they
9//! borrow from the caller. Nothing is copied in either direction.
10
11use hurray_io::file::KvValue;
12
13use crate::status::{HurrayStatus, HURRAY_ERR_INVALID_FILE};
14
15// ── Value kinds ───────────────────────────────────────────────────────────────
16//
17// The wire tags from `docs/spec/metadata.md` § KV Metadata Section, not a second
18// numbering: one value, one tag, wherever it is observed.
19
20/// UTF-8 string: `data` and `data_len`. Not NUL-terminated.
21pub const HURRAY_KV_STRING: u8 = 0x01;
22/// Signed 64-bit integer: `i64_value`.
23pub const HURRAY_KV_INT64: u8 = 0x02;
24/// Unsigned 64-bit integer: `u64_value`.
25pub const HURRAY_KV_UINT64: u8 = 0x03;
26/// IEEE 754 binary64: `f64_value`.
27pub const HURRAY_KV_FLOAT64: u8 = 0x04;
28/// Boolean: `bool_value`.
29pub const HURRAY_KV_BOOL: u8 = 0x05;
30/// Opaque bytes: `data` and `data_len`.
31pub const HURRAY_KV_BYTES: u8 = 0x06;
32/// Homogeneous array of scalars: `items` and `item_count`.
33pub const HURRAY_KV_ARRAY: u8 = 0x07;
34
35// ── Values ────────────────────────────────────────────────────────────────────
36
37/// One typed KV value: a `kind` tag and the fields that tag selects.
38///
39/// A flat tagged struct rather than a union, because every binding generator downstream
40/// of this header reads a struct correctly and few read a union well. Fields the tag does
41/// not select are zero on a read and ignored on a write.
42///
43/// Arrays nest one level and no further — the format forbids an array of arrays — so an
44/// `items` element is always a scalar.
45#[repr(C)]
46#[derive(Clone, Copy)]
47pub struct HurrayKvValue {
48 /// Which `HURRAY_KV_*` kind this value is.
49 pub kind: u8,
50 /// Set when `kind` is [`HURRAY_KV_BOOL`].
51 pub bool_value: bool,
52 /// Set when `kind` is [`HURRAY_KV_INT64`].
53 pub i64_value: i64,
54 /// Set when `kind` is [`HURRAY_KV_UINT64`].
55 pub u64_value: u64,
56 /// Set when `kind` is [`HURRAY_KV_FLOAT64`].
57 pub f64_value: f64,
58 /// First byte of a [`HURRAY_KV_STRING`] or [`HURRAY_KV_BYTES`] payload, else null.
59 pub data: *const u8,
60 /// Byte length of `data`. A string is UTF-8 and carries no NUL terminator.
61 pub data_len: usize,
62 /// First element of a [`HURRAY_KV_ARRAY`], else null.
63 pub items: *const HurrayKvValue,
64 /// Number of elements at `items`.
65 pub item_count: usize,
66}
67
68impl Default for HurrayKvValue {
69 /// A zeroed value with a null `kind`, which is what a C caller gets from `{0}`.
70 fn default() -> Self {
71 Self {
72 kind: 0,
73 bool_value: false,
74 i64_value: 0,
75 u64_value: 0,
76 f64_value: 0.0,
77 data: std::ptr::null(),
78 data_len: 0,
79 items: std::ptr::null(),
80 item_count: 0,
81 }
82 }
83}
84
85/// One KV pair: a borrowed key and its value.
86///
87/// The key is UTF-8 and MUST NOT be empty; it carries no NUL terminator, so `key_len` is
88/// not optional.
89#[repr(C)]
90#[derive(Clone, Copy)]
91pub struct HurrayKvEntry {
92 /// First byte of the key.
93 pub key: *const u8,
94 /// Key length in bytes.
95 pub key_len: usize,
96 /// The value this key maps to.
97 pub value: HurrayKvValue,
98}
99
100impl Default for HurrayKvEntry {
101 fn default() -> Self {
102 Self {
103 key: std::ptr::null(),
104 key_len: 0,
105 value: HurrayKvValue::default(),
106 }
107 }
108}
109
110// ── Rust → C: a reader handle's own snapshot ──────────────────────────────────
111
112/// A reader handle's KV section, owned so that the pointers it hands out stay valid for
113/// the life of the handle.
114///
115/// The core keeps its own copy, which it needs; this one exists because a C caller holds
116/// pointers rather than values, and a pointer into the core would be invalidated the
117/// moment the next read borrows it mutably.
118pub(crate) struct KvTable {
119 /// Owned keys and values. Every `data` pointer below points into these.
120 _owned: Vec<(String, KvValue)>,
121 /// Every array's elements, flattened. Fully populated before any pointer into it is
122 /// taken, and never grown afterwards.
123 _items: Vec<HurrayKvValue>,
124 /// One entry per pair, in file order.
125 entries: Vec<HurrayKvEntry>,
126}
127
128impl KvTable {
129 /// Snapshots a decoded KV section into the C representation.
130 pub(crate) fn new(kv: &[(String, KvValue)]) -> Self {
131 let owned: Vec<(String, KvValue)> = kv.to_vec();
132
133 // Exact capacity, because `items` must not reallocate once an entry points into
134 // it. Arrays do not nest, so one pass over the top level counts every element.
135 let total_items: usize = owned
136 .iter()
137 .map(|(_, v)| match v {
138 KvValue::Array(elements) => elements.len(),
139 _ => 0,
140 })
141 .sum();
142 let mut items: Vec<HurrayKvValue> = Vec::with_capacity(total_items);
143
144 // Two passes over the arrays: fill `items` first, then build the entries that
145 // point into it, so no pointer is taken before the last push.
146 let mut spans: Vec<(usize, usize)> = Vec::with_capacity(owned.len());
147 for (_, value) in &owned {
148 let start = items.len();
149 if let KvValue::Array(elements) = value {
150 for element in elements {
151 items.push(scalar_to_c(element));
152 }
153 }
154 spans.push((start, items.len() - start));
155 }
156
157 let base = items.as_ptr();
158 let entries = owned
159 .iter()
160 .zip(&spans)
161 .map(|((key, value), &(start, len))| HurrayKvEntry {
162 key: key.as_ptr(),
163 key_len: key.len(),
164 value: match value {
165 KvValue::Array(_) => HurrayKvValue {
166 kind: HURRAY_KV_ARRAY,
167 // SAFETY: `start + len` is within `items`, which has its final
168 // length here and never grows again.
169 items: unsafe { base.add(start) },
170 item_count: len,
171 ..HurrayKvValue::default()
172 },
173 scalar => scalar_to_c(scalar),
174 },
175 })
176 .collect();
177
178 Self {
179 _owned: owned,
180 _items: items,
181 entries,
182 }
183 }
184
185 /// The entries, in file order.
186 pub(crate) fn entries(&self) -> &[HurrayKvEntry] {
187 &self.entries
188 }
189}
190
191/// Converts one non-array value. An `Array` reaches this only from a malformed section,
192/// and is reported as the unknown kind `0` rather than silently flattened.
193fn scalar_to_c(value: &KvValue) -> HurrayKvValue {
194 match value {
195 KvValue::String(s) => HurrayKvValue {
196 kind: HURRAY_KV_STRING,
197 data: s.as_ptr(),
198 data_len: s.len(),
199 ..HurrayKvValue::default()
200 },
201 KvValue::Int64(v) => HurrayKvValue {
202 kind: HURRAY_KV_INT64,
203 i64_value: *v,
204 ..HurrayKvValue::default()
205 },
206 KvValue::Uint64(v) => HurrayKvValue {
207 kind: HURRAY_KV_UINT64,
208 u64_value: *v,
209 ..HurrayKvValue::default()
210 },
211 KvValue::Float64(v) => HurrayKvValue {
212 kind: HURRAY_KV_FLOAT64,
213 f64_value: *v,
214 ..HurrayKvValue::default()
215 },
216 KvValue::Bool(b) => HurrayKvValue {
217 kind: HURRAY_KV_BOOL,
218 bool_value: *b,
219 ..HurrayKvValue::default()
220 },
221 KvValue::Bytes(b) => HurrayKvValue {
222 kind: HURRAY_KV_BYTES,
223 data: b.as_ptr(),
224 data_len: b.len(),
225 ..HurrayKvValue::default()
226 },
227 // `KvValue` is `#[non_exhaustive]`, and an array cannot be an array's element.
228 _ => HurrayKvValue::default(),
229 }
230}
231
232// ── C → Rust: what a writer is handed ─────────────────────────────────────────
233
234/// Converts `count` caller-supplied entries into the pairs the writer core takes.
235///
236/// # Safety
237///
238/// `entries` MUST point to `count` readable [`HurrayKvEntry`] values, each with a key
239/// readable for `key_len` bytes and a value whose own pointers are readable for their
240/// declared lengths, all unmodified for the duration of the call.
241pub(crate) unsafe fn kv_from_c(
242 entries: *const HurrayKvEntry,
243 count: usize,
244) -> Result<Vec<(String, KvValue)>, HurrayStatus> {
245 let mut out = Vec::with_capacity(count);
246 // SAFETY: caller guarantees `count` readable entries at `entries`.
247 for entry in std::slice::from_raw_parts(entries, count) {
248 // SAFETY: caller guarantees `key_len` readable bytes at `key`.
249 let key = str_from_c(entry.key, entry.key_len)?;
250 out.push((key.to_owned(), value_from_c(&entry.value)?));
251 }
252 Ok(out)
253}
254
255/// Converts one caller-supplied value.
256///
257/// # Safety
258///
259/// `value`'s pointers MUST be readable for their declared lengths.
260unsafe fn value_from_c(value: &HurrayKvValue) -> Result<KvValue, HurrayStatus> {
261 match value.kind {
262 HURRAY_KV_STRING => Ok(KvValue::String(
263 str_from_c(value.data, value.data_len)?.to_owned(),
264 )),
265 HURRAY_KV_INT64 => Ok(KvValue::Int64(value.i64_value)),
266 HURRAY_KV_UINT64 => Ok(KvValue::Uint64(value.u64_value)),
267 HURRAY_KV_FLOAT64 => Ok(KvValue::Float64(value.f64_value)),
268 HURRAY_KV_BOOL => Ok(KvValue::Bool(value.bool_value)),
269 HURRAY_KV_BYTES => Ok(KvValue::Bytes(
270 bytes_from_c(value.data, value.data_len)?.to_vec(),
271 )),
272 HURRAY_KV_ARRAY => {
273 if value.items.is_null() && value.item_count > 0 {
274 return Err(HURRAY_ERR_INVALID_FILE);
275 }
276 let mut elements = Vec::with_capacity(value.item_count);
277 // SAFETY: caller guarantees `item_count` readable values at `items`.
278 for element in std::slice::from_raw_parts(value.items, value.item_count) {
279 if element.kind == HURRAY_KV_ARRAY {
280 // An array of arrays is not representable on the wire. Refused here
281 // rather than at encode time so the message names the caller's input.
282 return Err(HURRAY_ERR_INVALID_FILE);
283 }
284 elements.push(value_from_c(element)?);
285 }
286 Ok(KvValue::Array(elements))
287 }
288 _ => Err(HURRAY_ERR_INVALID_FILE),
289 }
290}
291
292/// Borrows `len` bytes at `ptr`. A zero length needs no pointer.
293///
294/// # Safety
295///
296/// `ptr` MUST be readable for `len` bytes, unmodified for the duration of the call.
297pub(crate) unsafe fn bytes_from_c(
298 ptr: *const u8,
299 len: usize,
300) -> Result<&'static [u8], HurrayStatus> {
301 if len == 0 {
302 return Ok(&[]);
303 }
304 if ptr.is_null() {
305 return Err(crate::status::HURRAY_ERR_NULL_POINTER);
306 }
307 // SAFETY: caller guarantees `len` readable bytes at `ptr`. The returned lifetime is
308 // not really 'static; every caller drops the borrow before returning to C.
309 Ok(std::slice::from_raw_parts(ptr, len))
310}
311
312/// Borrows `len` bytes at `ptr` as UTF-8.
313///
314/// # Safety
315///
316/// As [`bytes_from_c`].
317pub(crate) unsafe fn str_from_c(ptr: *const u8, len: usize) -> Result<&'static str, HurrayStatus> {
318 // SAFETY: delegated to the caller's guarantee on `ptr` and `len`.
319 let bytes = bytes_from_c(ptr, len)?;
320 // Names and keys are UTF-8 on the wire, so a non-UTF-8 one would make a file no
321 // reader could parse: refused at the boundary, not written.
322 std::str::from_utf8(bytes).map_err(|_| HURRAY_ERR_INVALID_FILE)
323}