figment_winreg/lib.rs
1//! figment provider which allows loading data from the Windows registry.
2//!
3//! Features:
4//! - Supports nesting (into subkeys).
5//! - Supports string expansions for `REG_EXPAND_SZ` values.
6//! - Can be configured to silently ignore errors while iterating over registry
7//! keys/values.
8
9use figment::value::{Empty, Value};
10use figment::{
11 value::{Dict, Map},
12 Error, Metadata, Profile, Provider
13};
14
15#[allow(clippy::wildcard_imports)]
16use winreg::{
17 enums::*,
18 types::FromRegValue,
19 {RegKey, HKEY}
20};
21
22pub use winreg;
23
24#[allow(clippy::wildcard_imports)]
25use windows::{
26 core::*, Win32::System::Environment::ExpandEnvironmentStringsA
27};
28
29#[cfg(not(windows))]
30compile_error!("This crate is for windows only");
31
32
33/// Different ways to handle non-fatal errors.
34#[derive(Copy, Clone)]
35pub enum FailStrategy {
36 /// If a non-fatal error occurs, terminate the registry iteration and return
37 /// an error to the the figment front-end.
38 Bail,
39
40 /// If a non-fatal error occurs, skip the operation and continue.
41 Skip
42}
43
44impl Default for FailStrategy {
45 fn default() -> Self {
46 Self::Bail
47 }
48}
49
50/// A provider that fetches its data from a Windows registry subkey.
51///
52/// # String Expansion
53/// This provider supports automatically expanding `REG_EXPAND_SZ` values
54/// (which is enabled by default), but it has a special caveat: The
55/// expansion is performed using `ExpandEnvironmentStringsA()`, which in
56/// legacy terms means that it operates on "ANSI" strings (which is
57/// nonsensical). Modern Windows versions treat the `*A()` functions as UTF-8
58/// strings, and Microsoft recommend doing this for new applications.
59/// However, `*A()` == UTF-8 is apparently only true if the process is running
60/// with the active codepage 65001, which can be accomplished by either
61/// globally configuring Windows to default codepage 65001 or setting it in
62/// the executable's manifest.
63pub struct RegistryProvider {
64 /// The profile to emit data to if nesting is disabled.
65 pub profile: Profile,
66
67 /// The root registry key.
68 hkey: HKEY,
69
70 /// The subkey path to load from.
71 regkey: String,
72
73 /// How to handle errors while iterating through the registry's subkeys and
74 /// values.
75 failstrategy: FailStrategy,
76
77 /// If `true` `REG_EXPAND_SZ` types will be expanded using
78 /// `ExpandEnvironmentStrings`. If `false` `REG_EXPAND_SZ` will be treated
79 /// like `REG_SZ`
80 expand: bool
81}
82
83impl RegistryProvider {
84 /// Create a Windows Registry figment [`Provider`].
85 ///
86 /// Defaults to bailing on error and expanding `REG_EXPAND_SZ` data.
87 #[allow(clippy::needless_pass_by_value)]
88 pub fn new(hkey: HKEY, regkey: impl ToString) -> Self {
89 Self {
90 profile: Profile::Default,
91 regkey: regkey.to_string(),
92 hkey,
93 failstrategy: FailStrategy::Bail,
94 expand: true
95 }
96 }
97
98 /// Configure how to handle errors occurring while processing registry
99 /// keys/values/data. By default these will cause error
100 /// (`FailStrategy::Bail`), but can be configured to `FailStrategy::Skip` to
101 /// silently ignore errors.
102 #[must_use]
103 pub const fn fail_strategy(mut self, strategy: FailStrategy) -> Self {
104 self.failstrategy = strategy;
105 self
106 }
107
108 /// Configure whether to expand `REG_EXPAND_SZ` or not.
109 ///
110 /// If set to `true` (the default) `REG_EXPAND_SZ` values will be their data
111 /// processed through `ExpandEnvironmentStrings()`.
112 ///
113 /// It set to `false` `REG_EXPAND_SZ` will be treated like `REG_SZ`.
114 #[must_use]
115 pub const fn expand(mut self, expand: bool) -> Self {
116 self.expand = expand;
117 self
118 }
119}
120
121
122impl Provider for RegistryProvider {
123 /// Returns metadata with kind Windows Registry.
124 ///
125 /// If the root key is known, then it will be prefix it accordingly:
126 /// `HKLM:Path\To\SubKey\Value`.
127 ///
128 /// If the root key is not known then it will simply ignore the root:
129 /// `Path\To\SubKey\Value`. Note that unknown root keys are unsupported.
130 ///
131 /// A profile will be inserted as the first part of the path:
132 /// `HKLM:<Profile>\Path\To\SubKey\Value`
133 fn metadata(&self) -> Metadata {
134 let reg = self.regkey.clone();
135 let root = rootname(self.hkey);
136
137 Metadata::named("Windows Registry")
138 .source(self.regkey.clone())
139 .interpolater(move |profile, keys| {
140 //println!("profile: {:?}\nkeys: {:?}", profile, keys);
141 if profile.is_custom() {
142 root.map_or_else(
143 || format!(r"{profile}\{reg}\{}", keys.join(r"\")),
144 |root| format!(r"{root}:{profile}\{reg}\{}", keys.join(r"\"))
145 )
146 } else {
147 root.map_or_else(
148 || format!(r"{reg}\{}", keys.join(r"\")),
149 |root| format!(r"{root}:{reg}\{}", keys.join(r"\"))
150 )
151 }
152 })
153 }
154
155 /// Load the requested data from the Windows registry.
156 fn data(&self) -> std::result::Result<Map<Profile, Dict>, Error> {
157 let hkey = match RegKey::predef(self.hkey).open_subkey(&self.regkey) {
158 Ok(result) => result,
159 Err(e) => match self.failstrategy {
160 FailStrategy::Bail => {
161 return Err(Error::from(format!("Unable to open subkey; {e}")));
162 }
163 FailStrategy::Skip => {
164 return Ok(self.profile.collect(Dict::new()));
165 }
166 }
167 };
168
169 let map = self.traverse_tree(hkey, &self.regkey)?;
170
171 Ok(self.profile.collect(map))
172 }
173}
174
175
176impl RegistryProvider {
177 #[allow(clippy::too_many_lines)]
178 fn traverse_tree(
179 &self,
180 regkey: RegKey,
181 subkey: &str
182 ) -> std::result::Result<Dict, Error> {
183 // we use a stack so that we can go down the registry hierarchy.
184 // once we find a unexplored subkey we add the current key to the stack
185 // and start exploring the subkey.
186 let mut stack: Vec<(RegKey, String, Dict)> = Vec::new();
187
188 // push the first entry onto the stack so we have a starting point
189 stack.push((regkey, subkey.to_string(), Dict::new()));
190
191 let result = 'outer: loop {
192 // Note: unwrap() should be safe here since we can only en up here if
193 // data has just been added. Make sure this invariant is upheld if
194 // making changes.
195 let (reg, key, mut dic) = stack.pop().unwrap();
196
197 // if we havent done this already
198 // add all the subkeys to the dict with Empty values
199 if dic.is_empty() {
200 for ekey in reg.enum_keys() {
201 let ekey = match ekey {
202 Ok(ekey) => ekey,
203 Err(e) => match self.failstrategy {
204 FailStrategy::Bail => {
205 return Err(Error::from(format!("Unable to read key {e}")));
206 }
207 FailStrategy::Skip => {
208 continue;
209 }
210 }
211 };
212 dic.insert(ekey.clone(), Value::from(Empty::None));
213 }
214 }
215
216 // look through all subkeys
217 // if one is Empty add it too the top of the stack and restart the loop
218 for (nkey, nval) in dic.clone() {
219 if nval == Value::from(Empty::None) {
220 let nreg = match reg.open_subkey(nkey.clone()) {
221 Ok(v) => v,
222 Err(e) => {
223 return Err(Error::from(format!("Unable to open subkey. {e}")));
224 }
225 };
226 let ndic = Dict::new();
227 stack.push((reg, key, dic));
228 stack.push((nreg, nkey.clone(), ndic));
229 continue 'outer;
230 }
231 }
232
233 // all subkeys are explored
234 // grab any values in this key before we leave
235 for eval in reg.enum_values() {
236 let (vkey, vvalue) = match eval {
237 Ok((vkey, vvalue)) => (vkey, vvalue),
238 Err(e) => match self.failstrategy {
239 FailStrategy::Bail => {
240 return Err(Error::from(format!(
241 "Unable to read key value {e}"
242 )));
243 }
244 FailStrategy::Skip => {
245 continue;
246 }
247 }
248 };
249 match vvalue.vtype {
250 REG_SZ => {
251 dic.insert(
252 vkey,
253 Value::from(match String::from_reg_value(&vvalue) {
254 Ok(v) => v,
255 Err(e) => match self.failstrategy {
256 FailStrategy::Bail => {
257 return Err(Error::from(format!(
258 "Unable to convert registry entry to a String value. \
259 {e}"
260 )));
261 }
262 FailStrategy::Skip => {
263 continue;
264 }
265 }
266 })
267 );
268 }
269 // Strings with enviroment variables such as %USERPROFILE%
270 // we take them in and expand them meaning we replace the variable
271 // with its current value
272 REG_EXPAND_SZ => {
273 if !self.expand {
274 // self.expand is false, so treat this as if it were a REG_SZ
275 dic.insert(
276 vkey,
277 Value::from(match String::from_reg_value(&vvalue) {
278 Ok(v) => v,
279 Err(e) => match self.failstrategy {
280 FailStrategy::Bail => {
281 return Err(Error::from(format!(
282 "Unable to convert registry entry to a String value. \
283 {e}"
284 )));
285 }
286 FailStrategy::Skip => {
287 continue;
288 }
289 }
290 })
291 );
292
293 // process next entry
294 continue;
295 }
296
297
298 match w32_expand_string(
299 vkey.as_ref(),
300 &vvalue.to_string(),
301 self.failstrategy
302 )? {
303 Some(s) => {
304 dic.insert(vkey, Value::from(s));
305 }
306 None => {
307 // Skip on error is enabled, and an error occurred.
308 continue;
309 }
310 }
311 }
312 REG_MULTI_SZ => {
313 dic.insert(
314 vkey,
315 Value::from(match Vec::<String>::from_reg_value(&vvalue) {
316 Ok(v) => v,
317 Err(e) => match self.failstrategy {
318 FailStrategy::Bail => {
319 return Err(Error::from(format!(
320 "Unable to convert registry entry to a Vec<String> \
321 value. {e}"
322 )))
323 }
324 FailStrategy::Skip => {
325 continue;
326 }
327 }
328 })
329 );
330 }
331 REG_DWORD => {
332 dic.insert(
333 vkey,
334 Value::from(match u32::from_reg_value(&vvalue) {
335 Ok(v) => v,
336 Err(e) => match self.failstrategy {
337 FailStrategy::Bail => {
338 return Err(Error::from(format!(
339 "Unable to convert registry entry to a u32 value. {e}"
340 )))
341 }
342 FailStrategy::Skip => {
343 continue;
344 }
345 }
346 })
347 );
348 }
349 REG_QWORD => {
350 dic.insert(
351 vkey,
352 Value::from(match u64::from_reg_value(&vvalue) {
353 Ok(v) => v,
354 Err(e) => match self.failstrategy {
355 FailStrategy::Bail => {
356 return Err(Error::from(format!(
357 "Unable to convert registry entry to a u64 value. {e}"
358 )))
359 }
360 FailStrategy::Skip => {
361 continue;
362 }
363 }
364 })
365 );
366 }
367 REG_DWORD_BIG_ENDIAN => {
368 let v = match u32::from_reg_value(&vvalue) {
369 Ok(v) => v,
370 Err(e) => match self.failstrategy {
371 FailStrategy::Bail => {
372 return Err(Error::from(format!(
373 "Unable to convert registry entry to a u32 value. {e}"
374 )))
375 }
376 FailStrategy::Skip => {
377 continue;
378 }
379 }
380 }
381 .to_be();
382
383 dic.insert(vkey, Value::from(v));
384 }
385
386 REG_NONE
387 | REG_BINARY
388 | REG_LINK
389 | REG_RESOURCE_LIST
390 | REG_FULL_RESOURCE_DESCRIPTOR
391 | REG_RESOURCE_REQUIREMENTS_LIST => {
392 // Ignored
393 continue;
394 }
395 }
396 }
397
398 // Collapse this entry into the underlying stack's dictionary and go
399 // back one level unless were at the end then return the current
400 // dictionary
401 if stack.is_empty() {
402 // The stack is empty at this point which means the current node being
403 // held is the root node and we've exhaused all the keys and values in
404 // the registry. Break out of loop and return the root node.
405 break dic;
406 }
407 // safe to unwrap since we just checked and found stack to be
408 // non-empty.
409 let (lreg, lkey, mut ldic) = stack.pop().unwrap();
410 ldic.insert(key, Value::from(dic));
411 stack.push((lreg, lkey, ldic));
412 };
413
414 Ok(result)
415 }
416}
417
418
419fn w32_expand_string(
420 key: &str,
421 src: &str,
422 failstrategy: FailStrategy
423) -> std::result::Result<Option<String>, Error> {
424 // Use ExpandEnvironmentStrings() to expand the value data.
425 //
426 // Passing an empty slice as the destination as will cause this function to
427 // calculate the size needed to store the expanded buffer.
428 //
429 // The return value of this function can tell us two things.
430 // If the function succeeds in expanding the string it returns the length of
431 // the expanded string.
432 // If the buffer we send in is too small it returns the length the buffer
433 // needs to be for it to succeed.
434 //
435 // So we start of by making it fail so we know how big of a buffer we need
436 // to store the expanded string. So we send in an 0 length destination
437 // buffer.
438 //
439 // SAFETY: The windows crate's impl IntoParam on the source string should
440 // ensure that the input string is null terminated as appropriate.
441 // We're providing a 0-length destination slice, which the windows
442 // crate wrapper will translate to a null pointer, so
443 // ExpandEnvironmentStrings() shouldn't actually try to write any
444 // data to the target buffer.
445 let alloc_needed =
446 unsafe { ExpandEnvironmentStringsA(PCSTR(src.as_ptr()), None) };
447
448 if alloc_needed == 0 {
449 match failstrategy {
450 FailStrategy::Bail => {
451 return Err(Error::from(format!(
452 "Unable to get size for expand environment string buffer {key}"
453 )));
454 }
455 FailStrategy::Skip => {
456 // Just move on to next entry.
457 return Ok(None);
458 }
459 }
460 }
461
462 // Allocate a buffer for storing the expanded string buffer.
463 // alloc_needed allegedly includes the null terminator.
464 let mut exp_buf = Vec::<u8>::with_capacity(alloc_needed as usize);
465
466 // Send in the string we want to expand together with the new target buffer
467 // and its size.
468 //
469 // This should expanded the string into the buffer and return how many
470 // characters it stored (including terminating null character).
471 //
472 // SAFETY: We have allocated all the memory that gets modified and we only
473 // work within that buffer by passing it as a slice.
474 let n = unsafe {
475 exp_buf.set_len(alloc_needed as usize);
476
477 ExpandEnvironmentStringsA(
478 PCSTR(src.as_ptr()),
479 Some(exp_buf.as_mut_slice())
480 )
481 };
482
483 if n == 0 {
484 match failstrategy {
485 FailStrategy::Bail => {
486 return Err(Error::from(format!(
487 "Unable to expand environment string for value {key}"
488 )));
489 }
490 FailStrategy::Skip => {
491 // Just move on to next entry.
492 return Ok(None);
493 }
494 }
495 }
496
497 // Set the length of the vector to the length of the returned
498 // buffer, but remove one byte as it should be the terminating
499 // null.
500 unsafe { exp_buf.set_len((n - 1) as usize) };
501
502 // Attempt to create a regular String from the Vec<u8>.
503 let Ok(exp_str) = String::from_utf8(exp_buf) else {
504 match failstrategy {
505 FailStrategy::Bail => {
506 return Err(Error::from(format!(
507 "Unable to expand environment string for value {key}"
508 )));
509 }
510 FailStrategy::Skip => {
511 // Just move on to next entry.
512 return Ok(None);
513 }
514 }
515 };
516
517
518 Ok(Some(exp_str))
519}
520
521
522const fn rootname(hkey: HKEY) -> Option<&'static str> {
523 match hkey {
524 HKEY_CLASSES_ROOT => Some("HKCR"),
525 HKEY_CURRENT_USER => Some("HKCU"),
526 HKEY_LOCAL_MACHINE => Some("HKLM"),
527 HKEY_USERS => Some("HKEY_USERS"),
528 HKEY_CURRENT_CONFIG => Some("HKCC"),
529 _ => None
530 }
531}
532
533// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :