Skip to main content

reovim_kernel/api/module/
probe.rs

1//! FFI-safe module probe for metadata discovery.
2
3use {super::ModuleId, crate::api::version::Version};
4
5/// FFI-safe module probe for metadata discovery.
6///
7/// Linux equivalent: `struct modinfo` + `vermagic` string
8///
9/// This struct uses fixed-size arrays instead of pointers to avoid
10/// lifetime issues when the module is unloaded. All strings are
11/// null-terminated within their fixed buffers.
12///
13/// # FFI Safety
14///
15/// - `#[repr(C)]` ensures predictable memory layout across dynamic library boundaries
16/// - Fixed-size arrays avoid pointer invalidation when module is unloaded
17/// - All fields are Copy, no heap allocation required
18/// - Can be returned by value across FFI boundary safely
19///
20/// # Buffer Sizes
21///
22/// - `id`: 64 bytes (63 chars + nul) - module identifier
23/// - `name`: 128 bytes (127 chars + nul) - display name
24///
25/// Strings exceeding these limits are truncated (not an error).
26///
27/// # Example
28///
29/// ```
30/// use reovim_kernel::api::v1::{ModuleProbe, Version};
31///
32/// let probe = ModuleProbe::new(
33///     "lang-rust",
34///     "Rust Language Support",
35///     Version::new(1, 0, 0),
36///     Version::new(1, 0, 0),
37/// );
38///
39/// assert_eq!(probe.id_str(), "lang-rust");
40/// assert_eq!(probe.name_str(), "Rust Language Support");
41/// ```
42#[repr(C)]
43#[derive(Debug, Clone, Copy)]
44pub struct ModuleProbe {
45    /// Module ID (null-terminated, max 63 chars + nul)
46    pub id: [u8; 64],
47    /// Module name (null-terminated, max 127 chars + nul)
48    pub name: [u8; 128],
49    /// Module version
50    pub version: Version,
51    /// Required kernel API version
52    pub api_version: Version,
53    /// Rustc version used to compile the module (for ABI compatibility checks)
54    pub rustc_version: [u8; 64],
55    /// Number of required dependencies (max 8)
56    pub required_deps_count: u8,
57    /// Required dependency IDs (null-terminated strings)
58    pub required_deps: [[u8; 64]; 8],
59    /// Number of optional dependencies (max 8)
60    pub optional_deps_count: u8,
61    /// Optional dependency IDs (null-terminated strings)
62    pub optional_deps: [[u8; 64]; 8],
63}
64
65impl ModuleProbe {
66    /// Create a new probe with the given metadata.
67    ///
68    /// Strings are truncated if they exceed buffer size.
69    /// This is a const fn for use in static initialization.
70    ///
71    /// # Example
72    ///
73    /// ```
74    /// use reovim_kernel::api::v1::{ModuleProbe, Version};
75    ///
76    /// // Can be used in const context
77    /// const PROBE: ModuleProbe = ModuleProbe::new(
78    ///     "my-module",
79    ///     "My Module",
80    ///     Version::new(1, 0, 0),
81    ///     Version::new(1, 0, 0),
82    /// );
83    /// ```
84    #[must_use]
85    pub const fn new(id: &str, name: &str, version: Version, api_version: Version) -> Self {
86        let mut probe = Self {
87            id: [0; 64],
88            name: [0; 128],
89            version,
90            api_version,
91            rustc_version: [0; 64],
92            required_deps_count: 0,
93            required_deps: [[0; 64]; 8],
94            optional_deps_count: 0,
95            optional_deps: [[0; 64]; 8],
96        };
97
98        // Copy id (const fn compatible - no iterator)
99        let id_bytes = id.as_bytes();
100        let id_len = if id_bytes.len() < 63 {
101            id_bytes.len()
102        } else {
103            63
104        };
105        let mut i = 0;
106        while i < id_len {
107            probe.id[i] = id_bytes[i];
108            i += 1;
109        }
110
111        // Copy name
112        let name_bytes = name.as_bytes();
113        let name_len = if name_bytes.len() < 127 {
114            name_bytes.len()
115        } else {
116            127
117        };
118        i = 0;
119        while i < name_len {
120            probe.name[i] = name_bytes[i];
121            i += 1;
122        }
123
124        probe
125    }
126
127    /// Get module ID as string slice.
128    ///
129    /// Returns the null-terminated string content from the fixed buffer.
130    #[must_use]
131    pub fn id_str(&self) -> &str {
132        let len = self
133            .id
134            .iter()
135            .position(|&b| b == 0)
136            .unwrap_or(self.id.len());
137        // Safety: we only write valid UTF-8 in new()
138        std::str::from_utf8(&self.id[..len]).unwrap_or("")
139    }
140
141    /// Get module name as string slice.
142    ///
143    /// Returns the null-terminated string content from the fixed buffer.
144    #[must_use]
145    pub fn name_str(&self) -> &str {
146        let len = self
147            .name
148            .iter()
149            .position(|&b| b == 0)
150            .unwrap_or(self.name.len());
151        std::str::from_utf8(&self.name[..len]).unwrap_or("")
152    }
153
154    /// Get rustc version as string slice.
155    ///
156    /// Returns empty string if not set.
157    #[must_use]
158    pub fn rustc_version_str(&self) -> &str {
159        let len = self
160            .rustc_version
161            .iter()
162            .position(|&b| b == 0)
163            .unwrap_or(self.rustc_version.len());
164        std::str::from_utf8(&self.rustc_version[..len]).unwrap_or("")
165    }
166
167    /// Get required dependencies as `ModuleId` list.
168    ///
169    /// Returns up to 8 dependencies stored in the probe.
170    #[must_use]
171    #[cfg_attr(coverage_nightly, coverage(off))]
172    pub fn required_deps(&self) -> Vec<ModuleId> {
173        let count = (self.required_deps_count as usize).min(8);
174        (0..count)
175            .filter_map(|i| {
176                let len = self.required_deps[i]
177                    .iter()
178                    .position(|&b| b == 0)
179                    .unwrap_or(64);
180                if len == 0 {
181                    None
182                } else {
183                    std::str::from_utf8(&self.required_deps[i][..len])
184                        .ok()
185                        .map(|s| ModuleId::from_string(s.to_string()))
186                }
187            })
188            .collect()
189    }
190
191    /// Get optional dependencies as `ModuleId` list.
192    ///
193    /// Returns up to 8 dependencies stored in the probe.
194    #[must_use]
195    #[cfg_attr(coverage_nightly, coverage(off))]
196    pub fn optional_deps(&self) -> Vec<ModuleId> {
197        let count = (self.optional_deps_count as usize).min(8);
198        (0..count)
199            .filter_map(|i| {
200                let len = self.optional_deps[i]
201                    .iter()
202                    .position(|&b| b == 0)
203                    .unwrap_or(64);
204                if len == 0 {
205                    None
206                } else {
207                    std::str::from_utf8(&self.optional_deps[i][..len])
208                        .ok()
209                        .map(|s| ModuleId::from_string(s.to_string()))
210                }
211            })
212            .collect()
213    }
214
215    /// Set rustc version (builder pattern for const contexts).
216    ///
217    /// # Example
218    ///
219    /// ```
220    /// use reovim_kernel::api::v1::{ModuleProbe, Version};
221    ///
222    /// let probe = ModuleProbe::new("test", "Test", Version::new(1, 0, 0), Version::new(0, 2, 0))
223    ///     .with_rustc_version("1.92.0");
224    /// assert_eq!(probe.rustc_version_str(), "1.92.0");
225    /// ```
226    #[must_use]
227    pub const fn with_rustc_version(mut self, version: &str) -> Self {
228        let bytes = version.as_bytes();
229        let len = if bytes.len() < 63 { bytes.len() } else { 63 };
230        let mut i = 0;
231        while i < len {
232            self.rustc_version[i] = bytes[i];
233            i += 1;
234        }
235        self
236    }
237
238    /// Add a required dependency at the specified index (builder pattern).
239    ///
240    /// Index must be 0-7. Silently ignored if index >= 8.
241    #[must_use]
242    #[allow(clippy::cast_possible_truncation)] // Safe: index < 8 is checked
243    #[cfg_attr(coverage_nightly, coverage(off))]
244    pub const fn with_required_dep(mut self, index: usize, dep: &str) -> Self {
245        if index >= 8 {
246            return self;
247        }
248        let bytes = dep.as_bytes();
249        let len = if bytes.len() < 63 { bytes.len() } else { 63 };
250        let mut i = 0;
251        while i < len {
252            self.required_deps[index][i] = bytes[i];
253            i += 1;
254        }
255        // Update count if this extends it (safe cast: index < 8)
256        if index as u8 >= self.required_deps_count {
257            self.required_deps_count = (index + 1) as u8;
258        }
259        self
260    }
261
262    /// Add an optional dependency at the specified index (builder pattern).
263    ///
264    /// Index must be 0-7. Silently ignored if index >= 8.
265    #[must_use]
266    #[allow(clippy::cast_possible_truncation)] // Safe: index < 8 is checked
267    #[cfg_attr(coverage_nightly, coverage(off))]
268    pub const fn with_optional_dep(mut self, index: usize, dep: &str) -> Self {
269        if index >= 8 {
270            return self;
271        }
272        let bytes = dep.as_bytes();
273        let len = if bytes.len() < 63 { bytes.len() } else { 63 };
274        let mut i = 0;
275        while i < len {
276            self.optional_deps[index][i] = bytes[i];
277            i += 1;
278        }
279        // Safe cast: index < 8
280        if index as u8 >= self.optional_deps_count {
281            self.optional_deps_count = (index + 1) as u8;
282        }
283        self
284    }
285}