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}