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 :