1
0

feat(windows): add blank path guard for registry operations

- Add BlankStringError to handle empty registry paths
- Implement blank_path_guard function to prevent dangerous registry operations
- Apply blank path guard to extension and ProgId key operations
- Update error handling to include BlankPath error variant
This commit is contained in:
2025-10-29 10:53:29 +08:00
parent a3456e9fdd
commit 3694a6a82a
2 changed files with 58 additions and 23 deletions

View File

@ -24,6 +24,8 @@ pub enum Error {
ParseExt(#[from] crate::extra::windows::ParseExtError), ParseExt(#[from] crate::extra::windows::ParseExtError),
#[error("{0}")] #[error("{0}")]
ParseProgId(#[from] crate::extra::windows::ParseProgIdError), ParseProgId(#[from] crate::extra::windows::ParseProgIdError),
#[error("{0}")]
BlankPath(#[from] crate::extra::winreg::BlankStringError),
} }
/// The result type used in this crate. /// The result type used in this crate.
@ -232,11 +234,14 @@ pub struct ExtKey {
impl ExtKey { impl ExtKey {
/// Set the default "Open With" of this file extension to given ProgId. /// Set the default "Open With" of this file extension to given ProgId.
pub fn link(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> { pub fn link(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> {
use winreg_extra::blank_path_guard;
// Open Classes key // Open Classes key
let classes = ClassesVisitor::open_with_scope(scope)?; let classes = ClassesVisitor::open_with_scope(scope)?;
// Open or create this extension key // Open or create this extension key
let (subkey, _) = classes.create_subkey_with_flags(self.ext.to_string(), KEY_WRITE)?; let (subkey, _) =
classes.create_subkey_with_flags(blank_path_guard(self.ext.to_string())?, KEY_WRITE)?;
// Set the default way to open this file extension // Set the default way to open this file extension
subkey.set_value("", &prog_id.to_string())?; subkey.set_value("", &prog_id.to_string())?;
@ -249,15 +254,18 @@ impl ExtKey {
/// If the default "Open With" of this file extension is not given ProgId, /// If the default "Open With" of this file extension is not given ProgId,
/// or there is no such file extension, this function do nothing. /// or there is no such file extension, this function do nothing.
pub fn unlink(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> { pub fn unlink(&self, prog_id: &ProgIdKey, scope: Scope) -> Result<()> {
use winreg_extra::{try_get_value, try_open_subkey_with_flags}; use winreg_extra::{blank_path_guard, try_get_value, try_open_subkey_with_flags};
// Open Classes key // Open Classes key
let classes = ClassesVisitor::open_with_scope(scope)?; let classes = ClassesVisitor::open_with_scope(scope)?;
// Open key for this extension. // Open key for this extension.
// If there is no such key, return directly. // If there is no such key, return directly.
if let Some(subkey) = try_open_subkey_with_flags(&classes, self.ext.to_string(), KEY_WRITE)? if let Some(subkey) = try_open_subkey_with_flags(
{ &classes,
blank_path_guard(self.ext.to_string())?,
KEY_WRITE,
)? {
// Only delete the default key if it is equal to our ProgId // Only delete the default key if it is equal to our ProgId
if let Some(value) = try_get_value::<String, _>(&subkey, "")? { if let Some(value) = try_get_value::<String, _>(&subkey, "")? {
if value == prog_id.to_string() { if value == prog_id.to_string() {
@ -275,13 +283,17 @@ impl ExtKey {
/// ///
/// This function will return its associated ProgId if "Open With" was set. /// This function will return its associated ProgId if "Open With" was set.
pub fn query(&self, view: View) -> Result<Option<ProgIdKey>> { pub fn query(&self, view: View) -> Result<Option<ProgIdKey>> {
use winreg_extra::{try_get_value, try_open_subkey_with_flags}; use winreg_extra::{blank_path_guard, try_get_value, try_open_subkey_with_flags};
// Open Classes key // Open Classes key
let classes = ClassesVisitor::open_with_view(view)?; let classes = ClassesVisitor::open_with_view(view)?;
// Open key for this extension if possible // Open key for this extension if possible
let rv = match try_open_subkey_with_flags(&classes, self.ext.to_string(), KEY_READ)? { let rv = match try_open_subkey_with_flags(
&classes,
blank_path_guard(self.ext.to_string())?,
KEY_READ,
)? {
Some(subkey) => { Some(subkey) => {
// Try get associated ProgId if possible // Try get associated ProgId if possible
match try_get_value::<String, _>(&subkey, "")? { match try_get_value::<String, _>(&subkey, "")? {
@ -327,8 +339,11 @@ pub struct ProgIdKey {
impl ProgIdKey { impl ProgIdKey {
/// Create ProgId into Registry in given scope with given parameters /// Create ProgId into Registry in given scope with given parameters
pub fn create(&self, scope: Scope, command: &str) -> Result<()> { pub fn create(&self, scope: Scope, command: &str) -> Result<()> {
use winreg_extra::blank_path_guard;
let classes = ClassesVisitor::open_with_scope(scope)?; let classes = ClassesVisitor::open_with_scope(scope)?;
let (subkey, _) = classes.create_subkey_with_flags(self.prog_id.to_string(), KEY_WRITE)?; let (subkey, _) = classes
.create_subkey_with_flags(blank_path_guard(self.prog_id.to_string())?, KEY_WRITE)?;
// Create verb // Create verb
let (subkey_verb, _) = subkey.create_subkey_with_flags("open", KEY_READ)?; let (subkey_verb, _) = subkey.create_subkey_with_flags("open", KEY_READ)?;
@ -340,10 +355,10 @@ impl ProgIdKey {
/// Delete this ProgId from registry in given scope. /// Delete this ProgId from registry in given scope.
pub fn delete(&self, scope: Scope) -> Result<()> { pub fn delete(&self, scope: Scope) -> Result<()> {
use winreg_extra::safe_delete_subkey_all; use winreg_extra::blank_path_guard;
let classes = ClassesVisitor::open_with_scope(scope)?; let classes = ClassesVisitor::open_with_scope(scope)?;
safe_delete_subkey_all(&classes, self.prog_id.to_string())?; classes.delete_subkey_all(blank_path_guard(self.prog_id.to_string())?)?;
Ok(()) Ok(())
} }

View File

@ -3,6 +3,7 @@
use std::ffi::OsStr; use std::ffi::OsStr;
use std::ops::Deref; use std::ops::Deref;
use std::ops::DerefMut; use std::ops::DerefMut;
use thiserror::Error as TeError;
use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND; use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND;
use windows_sys::Win32::System::Registry::REG_SAM_FLAGS; use windows_sys::Win32::System::Registry::REG_SAM_FLAGS;
use winreg::RegKey; use winreg::RegKey;
@ -11,11 +12,11 @@ use winreg::types::FromRegValue;
// region: Extra Operations // region: Extra Operations
/// Get the subkey with given name. /// Get the subkey with given name.
/// ///
/// If error occurs when fetching given subkey, it return `Err(...)`, /// If error occurs when fetching given subkey, it return `Err(...)`,
/// otherwise, it will return `Ok(Some(...))` if aubkey is existing, /// otherwise, it will return `Ok(Some(...))` if aubkey is existing,
/// or `Ok(None)` if there is no suchsub key. /// or `Ok(None)` if there is no suchsub key.
/// ///
/// Comparing with the function provided by winreg, /// Comparing with the function provided by winreg,
/// it differ "no such subkey" error and other access error. /// it differ "no such subkey" error and other access error.
pub fn try_open_subkey_with_flags<P: AsRef<OsStr>>( pub fn try_open_subkey_with_flags<P: AsRef<OsStr>>(
@ -28,19 +29,19 @@ pub fn try_open_subkey_with_flags<P: AsRef<OsStr>>(
Err(e) => match e.raw_os_error() { Err(e) => match e.raw_os_error() {
Some(errno) => match errno as u32 { Some(errno) => match errno as u32 {
ERROR_FILE_NOT_FOUND => Ok(None), ERROR_FILE_NOT_FOUND => Ok(None),
_ => Err(e) _ => Err(e),
} },
_ => Err(e), _ => Err(e),
}, },
} }
} }
/// Get the value by given key. /// Get the value by given key.
/// ///
/// If error occurs when fetching given key, it return `Err(...)`, /// If error occurs when fetching given key, it return `Err(...)`,
/// otherwise, it will return `Ok(Some(...))` if key is existing, /// otherwise, it will return `Ok(Some(...))` if key is existing,
/// or `Ok(None)` if there is no such key. /// or `Ok(None)` if there is no such key.
/// ///
/// Comparing with the function provided by winreg, /// Comparing with the function provided by winreg,
/// it differ "no such key" error and other access error. /// it differ "no such key" error and other access error.
pub fn try_get_value<T: FromRegValue, N: AsRef<OsStr>>( pub fn try_get_value<T: FromRegValue, N: AsRef<OsStr>>(
@ -52,21 +53,40 @@ pub fn try_get_value<T: FromRegValue, N: AsRef<OsStr>>(
Err(e) => match e.raw_os_error() { Err(e) => match e.raw_os_error() {
Some(errno) => match errno as u32 { Some(errno) => match errno as u32 {
ERROR_FILE_NOT_FOUND => Ok(None), ERROR_FILE_NOT_FOUND => Ok(None),
_ => Err(e) _ => Err(e),
} },
_ => Err(e), _ => Err(e),
}, },
} }
} }
/// Passing empty string to `delete_subkey_all` may cause // endregion
/// that the whole parent tree are deleted, not the subkey.
/// So we create this "safe" function to prevent this horrible scenarios. // region: Blank String Guard
pub fn safe_delete_subkey_all<P: AsRef<OsStr>>(regkey: &RegKey, path: P) -> std::io::Result<()> {
/// The error occurs when given string is empty.
#[derive(Debug, TeError)]
#[error("unexpected empty string")]
pub struct BlankStringError {}
impl BlankStringError {
fn new() -> Self {
Self {}
}
}
/// Check whether given registry path is empty.
/// If it is, return error, otherwise the given path.
///
/// Passing empty path for some registry functions is dangerous.
/// Because it will cause unexpected behavior that returning key self, rather than subkey.
/// This is VERY dangerous especially for those registry delete functions.
/// So I create this function to prevent any harmful blank path was passed into registry function.
pub fn blank_path_guard<P: AsRef<OsStr>>(path: P) -> std::result::Result<P, BlankStringError> {
if path.as_ref().is_empty() { if path.as_ref().is_empty() {
Err(std::io::Error::other("dangerous Registry delete_subkey_all")) Err(BlankStringError::new())
} else { } else {
regkey.delete_subkey_all(path) Ok(path)
} }
} }