Skip to main content

fmf_contract/
pod.rs

1//! `#[repr(C)]` POD types shared by the FFI (by layout) and the pipe wire
2//! (by explicit little-endian serialization in fmf-proto).
3//!
4//! The `const`
5//! blocks pin every size and offset at `cargo check` time — the same pins
6//! fmf-ffi's `contract_tests` re-assert at run time as an independent
7//! tripwire, and gen-contract radiates to C# `[FieldOffset]` values.
8
9use core::mem::{align_of, offset_of, size_of};
10
11use crate::volume;
12
13/// 48-byte result row, no internal padding. Offsets index into the page's
14/// trailing string blob (WTF-8). Mirrored by C# `LayoutKind.Explicit`.
15#[repr(C)]
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct FmfRow {
18    /// Stable engine-internal entry handle for this result.
19    pub entry_ref: u64,
20    /// NTFS File Reference Number of the entry.
21    pub frn: u64,
22    /// File size in bytes.
23    pub size: u64,
24    /// Last-modified time (Windows FILETIME ticks: 100 ns since 1601).
25    pub mtime: i64,
26    /// Byte offset of the file name into the page's trailing string blob.
27    pub name_off: u32,
28    /// Byte offset of the parent path into the page's trailing string blob.
29    pub parent_path_off: u32,
30    /// Packed entry attribute flags (hidden/system/directory bits).
31    pub flags: u32,
32    /// File name length in bytes within the string blob (WTF-8).
33    pub name_len: u16,
34    /// Parent path length in bytes within the string blob (WTF-8).
35    pub parent_path_len: u16,
36}
37
38impl FmfRow {
39    /// Size of one row in bytes (48).
40    pub const LEN: usize = size_of::<Self>();
41}
42
43const _: () = {
44    assert!(size_of::<FmfRow>() == 48);
45    assert!(align_of::<FmfRow>() == 8);
46    assert!(offset_of!(FmfRow, entry_ref) == 0);
47    assert!(offset_of!(FmfRow, frn) == 8);
48    assert!(offset_of!(FmfRow, size) == 16);
49    assert!(offset_of!(FmfRow, mtime) == 24);
50    assert!(offset_of!(FmfRow, name_off) == 32);
51    assert!(offset_of!(FmfRow, parent_path_off) == 36);
52    assert!(offset_of!(FmfRow, flags) == 40);
53    assert!(offset_of!(FmfRow, name_len) == 44);
54    assert!(offset_of!(FmfRow, parent_path_len) == 46);
55};
56
57/// FFI page: one contiguous engine-allocated block (row array + string
58/// blob). Pointers, so FFI-only — the pipe sends rows and blob inline.
59#[repr(C)]
60pub struct FmfPage {
61    /// Number of rows in the `rows` array.
62    pub row_count: u32,
63    /// C ABI padding (reserved; always 0).
64    #[expect(clippy::pub_underscore_fields, reason = "C ABI padding/reserved field")]
65    pub _pad: u32,
66    /// Pointer to the `row_count`-element array of [`FmfRow`].
67    pub rows: *const FmfRow,
68    /// Pointer to the trailing string blob the row offsets index into.
69    pub blob: *const u8,
70    /// String blob length in bytes.
71    pub blob_len: u32,
72    /// C ABI padding (reserved; always 0).
73    #[expect(clippy::pub_underscore_fields, reason = "C ABI padding/reserved field")]
74    pub _pad2: u32,
75}
76
77const _: () = {
78    assert!(size_of::<FmfPage>() == 32);
79    assert!(align_of::<FmfPage>() == 8);
80    assert!(offset_of!(FmfPage, row_count) == 0);
81    assert!(offset_of!(FmfPage, rows) == 8);
82    assert!(offset_of!(FmfPage, blob) == 16);
83    assert!(offset_of!(FmfPage, blob_len) == 24);
84};
85
86/// Engine-allocated UTF-8 JSON payload (stats, query traces); release with
87/// `fmf_blob_free`.
88#[repr(C)]
89pub struct FmfBlob {
90    /// Pointer to the UTF-8 JSON payload bytes.
91    pub data: *const u8,
92    /// Payload length in bytes.
93    pub len: u32,
94    /// C ABI padding (reserved; always 0).
95    #[expect(clippy::pub_underscore_fields, reason = "C ABI padding/reserved field")]
96    pub _pad: u32,
97}
98
99const _: () = {
100    assert!(size_of::<FmfBlob>() == 16);
101    assert!(align_of::<FmfBlob>() == 8);
102    assert!(offset_of!(FmfBlob, data) == 0);
103    assert!(offset_of!(FmfBlob, len) == 8);
104    assert!(offset_of!(FmfBlob, _pad) == 12);
105};
106
107/// POD event payload — FFI callback argument and pipe event-push body
108/// (32 bytes). `volume` is the zero-padded UTF-8 drive label ("C:").
109#[repr(C)]
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub struct FmfEvent {
112    /// [`crate::events::EventKind`] as u32.
113    pub kind: u32,
114    /// C ABI padding (reserved; always 0).
115    #[expect(clippy::pub_underscore_fields, reason = "C ABI padding/reserved field")]
116    pub _pad: u32,
117    /// Entry count carried by the event (e.g. indexed entries so far).
118    pub entries: u64,
119    /// Zero-padded UTF-8 drive label this event concerns (e.g. "C:").
120    pub volume: [u8; 16],
121}
122
123impl FmfEvent {
124    /// Size of the event in bytes (32).
125    pub const LEN: usize = size_of::<Self>();
126
127    /// Builds an event, encoding `volume` into the fixed 16-byte label.
128    #[must_use]
129    pub fn new(kind: u32, entries: u64, volume: &str) -> Self {
130        Self {
131            kind,
132            _pad: 0,
133            entries,
134            volume: volume::encode_label(volume),
135        }
136    }
137
138    /// Decodes the fixed 16-byte label back to a trimmed drive string.
139    #[must_use]
140    pub fn volume_str(&self) -> &str {
141        volume::decode_label(&self.volume)
142    }
143}
144
145const _: () = {
146    assert!(size_of::<FmfEvent>() == 32);
147    assert!(align_of::<FmfEvent>() == 8);
148    assert!(offset_of!(FmfEvent, kind) == 0);
149    assert!(offset_of!(FmfEvent, _pad) == 4);
150    assert!(offset_of!(FmfEvent, entries) == 8);
151    assert!(offset_of!(FmfEvent, volume) == 16);
152};
153
154/// Query options — 20 bytes, no padding, LE on the wire. Field values are
155/// the [`crate::options`] enums as u32.
156#[repr(C)]
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
158pub struct FmfQueryOptions {
159    /// Result column to order by ([`crate::options::SortKey`] as u32).
160    pub sort: u32,
161    /// Nonzero sorts descending; 0 ascending.
162    pub desc: u32,
163    /// Case-matching policy ([`crate::options::CaseMode`] as u32).
164    pub case_mode: u32,
165    /// Nonzero shows hidden/system entries (default-excluded otherwise).
166    pub include_hidden_system: u32,
167    /// Packed regex mode (ADR-0023): bit0 = treat the whole query as one
168    /// regex, bit1 = scope (0 = file name, 1 = full path). 0 = off (the
169    /// query parses normally; `regex:` per-term syntax still works). Higher
170    /// bits are reserved 0, so a future flag keeps `LEN` at 20.
171    pub regex_mode: u32,
172}
173
174impl FmfQueryOptions {
175    /// Size of the options struct in bytes (20).
176    pub const LEN: usize = size_of::<Self>();
177}
178
179const _: () = {
180    assert!(size_of::<FmfQueryOptions>() == 20);
181    assert!(align_of::<FmfQueryOptions>() == 4);
182    assert!(offset_of!(FmfQueryOptions, sort) == 0);
183    assert!(offset_of!(FmfQueryOptions, desc) == 4);
184    assert!(offset_of!(FmfQueryOptions, case_mode) == 8);
185    assert!(offset_of!(FmfQueryOptions, include_hidden_system) == 12);
186    assert!(offset_of!(FmfQueryOptions, regex_mode) == 16);
187};
188
189/// FFI volume status. `state` is [`crate::options::VolumeState`] as u32.
190#[repr(C)]
191#[derive(Debug, Clone, Copy)]
192pub struct FmfVolumeStatus {
193    /// Zero-padded UTF-8 drive label this status concerns (e.g. "C:").
194    pub label: [u8; 16],
195    /// Volume lifecycle state ([`crate::options::VolumeState`] as u32).
196    pub state: u32,
197    /// C ABI padding (reserved; always 0).
198    #[expect(clippy::pub_underscore_fields, reason = "C ABI padding/reserved field")]
199    pub _pad: u32,
200    /// Number of indexed entries on this volume.
201    pub entries: u64,
202}
203
204const _: () = {
205    assert!(size_of::<FmfVolumeStatus>() == 32);
206    assert!(align_of::<FmfVolumeStatus>() == 8);
207    assert!(offset_of!(FmfVolumeStatus, label) == 0);
208    assert!(offset_of!(FmfVolumeStatus, state) == 16);
209    assert!(offset_of!(FmfVolumeStatus, entries) == 24);
210};
211
212/// 16-byte little-endian pipe frame header. `to_bytes`/`from_bytes` are
213/// pure byte conversions — the `MAX_PAYLOAD_LEN` *policy* lives in
214/// fmf-proto's `decode_header`/`read_frame`.
215#[repr(C)]
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub struct FrameHeader {
218    /// Payload length in bytes (the header itself excluded).
219    pub len: u32,
220    /// Frame operation code (request/response/event kind).
221    pub opcode: u16,
222    /// Per-opcode bit flags.
223    pub flags: u16,
224    /// Request/response correlation; 0 on event pushes.
225    pub request_id: u32,
226    /// Status code ([`crate::codes`]); meaningful on responses only.
227    pub status: i32,
228}
229
230impl FrameHeader {
231    /// Size of the frame header in bytes (16).
232    pub const LEN: usize = size_of::<Self>();
233
234    /// Serializes the header to its 16-byte little-endian wire form.
235    #[must_use]
236    pub fn to_bytes(self) -> [u8; Self::LEN] {
237        let mut b = [0u8; Self::LEN];
238        b[0..4].copy_from_slice(&self.len.to_le_bytes());
239        b[4..6].copy_from_slice(&self.opcode.to_le_bytes());
240        b[6..8].copy_from_slice(&self.flags.to_le_bytes());
241        b[8..12].copy_from_slice(&self.request_id.to_le_bytes());
242        b[12..16].copy_from_slice(&self.status.to_le_bytes());
243        b
244    }
245
246    /// Parses a header from its 16-byte little-endian wire form.
247    #[must_use]
248    pub const fn from_bytes(b: &[u8; Self::LEN]) -> Self {
249        Self {
250            len: u32::from_le_bytes([b[0], b[1], b[2], b[3]]),
251            opcode: u16::from_le_bytes([b[4], b[5]]),
252            flags: u16::from_le_bytes([b[6], b[7]]),
253            request_id: u32::from_le_bytes([b[8], b[9], b[10], b[11]]),
254            status: i32::from_le_bytes([b[12], b[13], b[14], b[15]]),
255        }
256    }
257}
258
259const _: () = {
260    assert!(size_of::<FrameHeader>() == 16);
261    assert!(offset_of!(FrameHeader, len) == 0);
262    assert!(offset_of!(FrameHeader, opcode) == 4);
263    assert!(offset_of!(FrameHeader, flags) == 6);
264    assert!(offset_of!(FrameHeader, request_id) == 8);
265    assert!(offset_of!(FrameHeader, status) == 12);
266};