Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

windows: use ENV & SHGetFolderPathW for APPDATA/LOCALAPPDATA #11

Merged
merged 1 commit into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,9 @@ repository = "https://github.com/lunacookies/etcetera"
version = "0.7.1"

[dependencies]
cfg-if = "1.0.0"
home = "0.5.4"
cfg-if = "1"
home = "0.5"

# We should keep this in sync with the `home` crate.
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.48", features = ["Win32_Foundation", "Win32_UI_Shell"] }
70 changes: 70 additions & 0 deletions src/app_strategy/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@ use std::path::{Path, PathBuf};

/// This strategy follows Windows’ conventions. It seems that all Windows GUI apps, and some command-line ones follow this pattern. The specification is available [here](https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid).
///
/// This initial example removes all the relevant environment variables to show the strategy’s use of the:
/// - (on Windows) SHGetFolderPathW API.
/// - (on non-Windows) Windows default directories.
///
/// ```
/// use etcetera::app_strategy::AppStrategy;
/// use etcetera::app_strategy::AppStrategyArgs;
/// use etcetera::app_strategy::Windows;
/// use std::path::Path;
///
/// // Remove the environment variables that the strategy reads from.
/// std::env::remove_var("USERPROFILE");
/// std::env::remove_var("APPDATA");
/// std::env::remove_var("LOCALAPPDATA");
///
/// let app_strategy = Windows::new(AppStrategyArgs {
/// top_level_domain: "org".to_string(),
/// author: "Acme Corp".to_string(),
Expand Down Expand Up @@ -43,6 +52,67 @@ use std::path::{Path, PathBuf};
/// None
/// );
/// ```
///
/// This next example gives the environment variables values:
///
/// ```
/// use etcetera::app_strategy::AppStrategy;
/// use etcetera::app_strategy::AppStrategyArgs;
/// use etcetera::app_strategy::Windows;
/// use std::path::Path;
///
/// let home_path = if cfg!(windows) {
/// "C:\\my_home_location\\".to_string()
/// } else {
/// etcetera::home_dir().unwrap().to_string_lossy().to_string()
/// };
/// let data_path = if cfg!(windows) {
/// "C:\\my_data_location\\"
/// } else {
/// "/my_data_location/"
/// };
/// let cache_path = if cfg!(windows) {
/// "C:\\my_cache_location\\"
/// } else {
/// "/my_cache_location/"
/// };
///
/// std::env::set_var("USERPROFILE", &home_path);
/// std::env::set_var("APPDATA", data_path);
/// std::env::set_var("LOCALAPPDATA", cache_path);
///
/// let app_strategy = Windows::new(AppStrategyArgs {
/// top_level_domain: "org".to_string(),
/// author: "Acme Corp".to_string(),
/// app_name: "Frobnicator Plus".to_string(),
/// }).unwrap();
///
/// assert_eq!(
/// app_strategy.home_dir(),
/// Path::new(&home_path)
/// );
/// assert_eq!(
/// app_strategy.config_dir(),
/// Path::new(&format!("{}/Acme Corp/Frobnicator Plus/config", data_path))
/// );
/// assert_eq!(
/// app_strategy.data_dir(),
/// Path::new(&format!("{}/Acme Corp/Frobnicator Plus/data", data_path))
/// );
/// assert_eq!(
/// app_strategy.cache_dir(),
/// Path::new(&format!("{}/Acme Corp/Frobnicator Plus/cache", cache_path))
/// );
/// assert_eq!(
/// app_strategy.state_dir(),
/// None
/// );
/// assert_eq!(
/// app_strategy.runtime_dir(),
/// None
/// );
/// ```

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Windows {
base_strategy: base_strategy::Windows,
Expand Down
120 changes: 117 additions & 3 deletions src/base_strategy/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@ use std::path::{Path, PathBuf};

/// This strategy follows Windows’ conventions. It seems that all Windows GUI apps, and some command-line ones follow this pattern. The specification is available [here](https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid).
///
/// This initial example removes all the relevant environment variables to show the strategy’s use of the:
/// - (on Windows) SHGetFolderPathW API.
/// - (on non-Windows) Windows default directories.
///
/// ```
/// use etcetera::base_strategy::BaseStrategy;
/// use etcetera::base_strategy::Windows;
/// use std::path::Path;
///
/// // Remove the environment variables that the strategy reads from.
/// std::env::remove_var("USERPROFILE");
/// std::env::remove_var("APPDATA");
/// std::env::remove_var("LOCALAPPDATA");
///
/// let base_strategy = Windows::new().unwrap();
///
/// let home_dir = etcetera::home_dir().unwrap();
Expand Down Expand Up @@ -36,11 +45,115 @@ use std::path::{Path, PathBuf};
/// None
/// );
/// ```
///
/// This next example gives the environment variables values:
///
/// ```
/// use etcetera::base_strategy::BaseStrategy;
/// use etcetera::base_strategy::Windows;
/// use std::path::Path;
///
/// let home_path = if cfg!(windows) {
/// "C:\\foo\\".to_string()
/// } else {
/// etcetera::home_dir().unwrap().to_string_lossy().to_string()
/// };
/// let data_path = if cfg!(windows) {
/// "C:\\bar\\"
/// } else {
/// "/bar/"
/// };
/// let cache_path = if cfg!(windows) {
/// "C:\\baz\\"
/// } else {
/// "/baz/"
/// };
///
/// std::env::set_var("USERPROFILE", &home_path);
/// std::env::set_var("APPDATA", data_path);
/// std::env::set_var("LOCALAPPDATA", cache_path);
///
/// let base_strategy = Windows::new().unwrap();
///
/// assert_eq!(
/// base_strategy.home_dir(),
/// Path::new(&home_path)
/// );
/// assert_eq!(
/// base_strategy.config_dir(),
/// Path::new(data_path)
/// );
/// assert_eq!(
/// base_strategy.data_dir(),
/// Path::new(data_path)
/// );
/// assert_eq!(
/// base_strategy.cache_dir(),
/// Path::new(cache_path)
/// );
/// assert_eq!(
/// base_strategy.state_dir(),
/// None
/// );
/// assert_eq!(
/// base_strategy.runtime_dir(),
/// None
/// );
/// ```

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Windows {
home_dir: PathBuf,
}

// Ref: https://github.com/rust-lang/cargo/blob/home-0.5.5/crates/home/src/windows.rs
// We should keep this code in sync with the above.
impl Windows {
fn dir_inner(env: &'static str) -> Option<PathBuf> {
std::env::var_os(env)
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.or_else(|| Self::dir_crt(env))
}

#[cfg(all(windows, target_vendor = "uwp"))]
fn dir_crt(env: &'static str) -> Option<PathBuf> {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;

use windows_sys::Win32::Foundation::{MAX_PATH, S_OK};
use windows_sys::Win32::UI::Shell::{SHGetFolderPathW, CSIDL_APPDATA, CSIDL_LOCAL_APPDATA};

let csidl = match env {
"APPDATA" => CSIDL_APPDATA,
"LOCALAPPDATA" => CSIDL_LOCAL_APPDATA,
_ => return None,
};

extern "C" {
fn wcslen(buf: *const u16) -> usize;
}

unsafe {
let mut path: Vec<u16> = Vec::with_capacity(MAX_PATH as usize);
match SHGetFolderPathW(0, csidl, 0, 0, path.as_mut_ptr()) {
S_OK => {
let len = wcslen(path.as_ptr());
path.set_len(len);
let s = OsString::from_wide(&path);
Some(PathBuf::from(s))
}
_ => None,
}
}
}

#[cfg(not(all(windows, target_vendor = "uwp")))]
fn dir_crt(_env: &'static str) -> Option<PathBuf> {
None
}
}

impl super::BaseStrategy for Windows {
type CreationError = crate::HomeDirError;

Expand All @@ -55,15 +168,16 @@ impl super::BaseStrategy for Windows {
}

fn config_dir(&self) -> PathBuf {
self.home_dir.join("AppData").join("Roaming")
self.data_dir()
}

fn data_dir(&self) -> PathBuf {
self.home_dir.join("AppData").join("Roaming")
Self::dir_inner("APPDATA").unwrap_or_else(|| self.home_dir.join("AppData").join("Roaming"))
}

fn cache_dir(&self) -> PathBuf {
self.home_dir.join("AppData").join("Local")
Self::dir_inner("LOCALAPPDATA")
.unwrap_or_else(|| self.home_dir.join("AppData").join("Local"))
}

fn state_dir(&self) -> Option<PathBuf> {
Expand Down
8 changes: 0 additions & 8 deletions src/base_strategy/xdg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,6 @@ use std::path::PathBuf;
/// let base_strategy = Xdg::new().unwrap();
///
/// assert_eq!(
/// base_strategy.home_dir(),
/// etcetera::home_dir().unwrap()
/// );
/// assert_eq!(
/// base_strategy.config_dir(),
/// Path::new(config_path)
/// );
Expand Down Expand Up @@ -135,10 +131,6 @@ use std::path::PathBuf;
///
/// // We still get the default values.
/// assert_eq!(
/// base_strategy.home_dir(),
/// &home_dir
/// );
/// assert_eq!(
/// base_strategy.config_dir().strip_prefix(&home_dir),
/// Ok(Path::new(".config/"))
/// );
Expand Down