diff --git a/Cargo.toml b/Cargo.toml index f8167b6..79281d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,9 @@ maintenance = { status = "as-is" } [target.'cfg(unix)'.dependencies] libc = "0.2" +[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] +objc = "0.2" + [target.'cfg(target_os = "redox")'.dependencies] redox_users = "0.3.0" diff --git a/src/apple.rs b/src/apple.rs new file mode 100644 index 0000000..592a8e1 --- /dev/null +++ b/src/apple.rs @@ -0,0 +1,117 @@ +use objc::rc::autoreleasepool; +use objc::runtime::Object; +use std::ffi::CStr; +use std::os::raw::c_char; +use std::path::PathBuf; + +// We need to link to Foundation to access the NSFileManager class +#[link(name = "Foundation", kind = "framework")] +extern "C" {} + +/// Type of directory to lookup from macOS/iOS +#[repr(u64)] +pub enum SearchPathDirectory { + /// Applications directory, depending on domain. /Applications or ~/Applications typically + Application = 1, + AdminApplication = 4, + /// Library folder, can be /Library (system) or ~/Library (user) + Library = 5, + /// Location of usere's home directories, typically /Users + Users = 7, + /// Documentation, not sure if used... + Documentation = 8, + /// Documents folder, typically ~/Documents + Documents = 9, + AutosavedInformation = 11, + /// User's desktop folder, typically ~/Desktop + Desktop = 12, + /// Caches folder, Library/Caches + Caches = 13, + /// Applicatino support, Library/Application Support + /// + /// Typical home of non-userdefaults app data & settings + ApplicationSupport = 14, + /// Downloads folder, ~/Downloads + Downloads = 15, + /// Movies folder, ~/Movies + Movies = 17, + /// Music folder, ~/Music + Music = 18, + /// Pictures folder, ~/Pictures + Pictures = 19, + /// PPDs folder, Library/Printers/PPDs + PrinterDescription = 20, + /// Public folder, ~/Public + SharedPublic = 21, + /// Preference Panes, Library/PreferencePanes + PreferencePanes = 22, + /// User scripts folder for calling application, ~/Library/Application Scripts/code-signing-id + ApplicationScripts = 23, + /// Trash folder + Trash = 102, +} + +/// Domain for path to return, dirs currently mostly deals with user dirs so likely want UserDomain +#[repr(u64)] +pub enum SearchPathDomainMask { + /// Looks up directory in user's domain, so ~ + UserDomain = 1, + /// Local system domain, which is folders typically found in /Library + LocalDomain = 2, + /// Publically available locations on the local network + NetworkDomain = 4, + /// Read only system locations, /System (may be completely unavailable on newer systems?) + SystemDomain = 8, + /// Looks up directories in all of the current domains and future ones apple may add + AllDomains = 65535, +} + +/// Returns first path found on macOS/iOS systems given the requested type of path, within given domain +/// +/// Even if a path is returned, it may not exist yet and require creation +pub fn path_for_dir(dir: SearchPathDirectory, domain: SearchPathDomainMask) -> Option { + let mut result = None; + autoreleasepool(|| { + let cls = class!(NSFileManager); + unsafe { + let obj: *mut Object = msg_send![cls, defaultManager]; + let url: *mut Object = msg_send![obj, URLForDirectory:dir inDomain:domain as u64 appropriateForURL:0 create:false error:0]; + if !url.is_null() { + let path: *mut Object = msg_send![url, path]; + let s: *const c_char = msg_send![path, UTF8String]; + let c_str = CStr::from_ptr(s); + match c_str.to_str() { + Err(error) => { + println!("Error getting home dir string: {}", error); + } + Ok(string) => result = Some(PathBuf::from(string.to_owned())), + }; + } else { + println!("Failed to get dir"); + } + } + }); + result +} + +/// Returns user's home directory, or sandbox if called within sandboxed app +pub fn home_dir() -> Option { + unsafe { + let mut result = None; + autoreleasepool(|| { + let cls = class!(NSFileManager); + let obj: *mut Object = msg_send![cls, defaultManager]; + let url: *mut Object = msg_send![obj, homeDirectoryForCurrentUser]; + let path: *mut Object = msg_send![url, path]; + let s: *const c_char = msg_send![path, UTF8String]; + let c_str = CStr::from_ptr(s); + match c_str.to_str() { + Err(error) => { + println!("Error getting home dir string: {}", error); + } + Ok(string) => result = Some(PathBuf::from(string.to_owned())), + }; + }); + result + } +} diff --git a/src/lib.rs b/src/lib.rs index 9061def..4e3bc15 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,62 +15,76 @@ pub fn is_absolute_path(path: OsString) -> Option { #[cfg(all(unix, not(target_os = "redox")))] extern crate libc; -#[cfg(all(unix, not(target_os = "redox")))] +#[cfg(any(target_os = "macos", target_os = "ios"))] +#[macro_use] +extern crate objc; + +#[cfg(any(target_os = "macos", target_os = "ios"))] +mod apple; +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub use apple::{home_dir, path_for_dir, SearchPathDirectory, SearchPathDomainMask}; + +#[cfg(all( + unix, + not(any(target_os = "redox", target_os = "macos", target_os = "ios")) +))] mod target_unix_not_redox { -use std::env; -use std::ffi::{CStr, OsString}; -use std::mem; -use std::os::unix::ffi::OsStringExt; -use std::path::PathBuf; -use std::ptr; + use std::env; + use std::ffi::{CStr, OsString}; + use std::mem; + use std::os::unix::ffi::OsStringExt; + use std::path::PathBuf; + use std::ptr; -use super::libc; + use super::libc; -// https://github.com/rust-lang/rust/blob/master/src/libstd/sys/unix/os.rs#L498 -pub fn home_dir() -> Option { - return env::var_os("HOME") - .and_then(|h| if h.is_empty() { None } else { Some(h) }) - .or_else(|| unsafe { fallback() }) - .map(PathBuf::from); + // https://github.com/rust-lang/rust/blob/master/src/libstd/sys/unix/os.rs#L498 + pub fn home_dir() -> Option { + return env::var_os("HOME") + .and_then(|h| if h.is_empty() { None } else { Some(h) }) + .or_else(|| unsafe { fallback() }) + .map(PathBuf::from); - #[cfg(any(target_os = "android", target_os = "ios", target_os = "emscripten"))] - unsafe fn fallback() -> Option { - None - } - #[cfg(not(any(target_os = "android", target_os = "ios", target_os = "emscripten")))] - unsafe fn fallback() -> Option { - let amt = match libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) { - n if n < 0 => 512 as usize, - n => n as usize, - }; - let mut buf = Vec::with_capacity(amt); - let mut passwd: libc::passwd = mem::zeroed(); - let mut result = ptr::null_mut(); - match libc::getpwuid_r( - libc::getuid(), - &mut passwd, - buf.as_mut_ptr(), - buf.capacity(), - &mut result, - ) { - 0 if !result.is_null() => { - let ptr = passwd.pw_dir as *const _; - let bytes = CStr::from_ptr(ptr).to_bytes(); - if bytes.is_empty() { - None - } else { - Some(OsStringExt::from_vec(bytes.to_vec())) + #[cfg(any(target_os = "android", target_os = "ios", target_os = "emscripten"))] + unsafe fn fallback() -> Option { + None + } + #[cfg(not(any(target_os = "android", target_os = "ios", target_os = "emscripten")))] + unsafe fn fallback() -> Option { + let amt = match libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) { + n if n < 0 => 512 as usize, + n => n as usize, + }; + let mut buf = Vec::with_capacity(amt); + let mut passwd: libc::passwd = mem::zeroed(); + let mut result = ptr::null_mut(); + match libc::getpwuid_r( + libc::getuid(), + &mut passwd, + buf.as_mut_ptr(), + buf.capacity(), + &mut result, + ) { + 0 if !result.is_null() => { + let ptr = passwd.pw_dir as *const _; + let bytes = CStr::from_ptr(ptr).to_bytes(); + if bytes.is_empty() { + None + } else { + Some(OsStringExt::from_vec(bytes.to_vec())) + } } + _ => None, } - _ => None, } } } -} - -#[cfg(all(unix, not(target_os = "redox")))] +#[cfg(all( + unix, + not(any(target_os = "redox", target_os = "macos", target_os = "ios")) +))] pub use self::target_unix_not_redox::home_dir; #[cfg(target_os = "redox")] @@ -79,18 +93,17 @@ extern crate redox_users; #[cfg(target_os = "redox")] mod target_redox { -use std::path::PathBuf; + use std::path::PathBuf; -use super::redox_users::{All, AllUsers, Config}; + use super::redox_users::{All, AllUsers, Config}; -pub fn home_dir() -> Option { - let current_uid = redox_users::get_uid().ok()?; - let users = AllUsers::new(Config::default()).ok()?; - let user = users.get_by_id(current_uid)?; - - Some(PathBuf::from(user.home.clone())) -} + pub fn home_dir() -> Option { + let current_uid = redox_users::get_uid().ok()?; + let users = AllUsers::new(Config::default()).ok()?; + let user = users.get_by_id(current_uid)?; + Some(PathBuf::from(user.home.clone())) + } } #[cfg(target_os = "redox")] @@ -102,30 +115,33 @@ mod xdg_user_dirs; #[cfg(all(unix, not(any(target_os = "macos", target_os = "ios"))))] mod target_unix_not_mac { -use std::collections::HashMap; -use std::env; -use std::path::{Path, PathBuf}; + use std::collections::HashMap; + use std::env; + use std::path::{Path, PathBuf}; -use super::{home_dir, is_absolute_path}; -use super::xdg_user_dirs; + use super::xdg_user_dirs; + use super::{home_dir, is_absolute_path}; -fn user_dir_file(home_dir: &Path) -> PathBuf { - env::var_os("XDG_CONFIG_HOME").and_then(is_absolute_path).unwrap_or_else(|| home_dir.join(".config")).join("user-dirs.dirs") -} - -// this could be optimized further to not create a map and instead retrieve the requested path only -pub fn user_dir(user_dir_name: &str) -> Option { - if let Some(home_dir) = home_dir() { - xdg_user_dirs::single(&home_dir, &user_dir_file(&home_dir), user_dir_name).remove(user_dir_name) - } else { - None + fn user_dir_file(home_dir: &Path) -> PathBuf { + env::var_os("XDG_CONFIG_HOME") + .and_then(is_absolute_path) + .unwrap_or_else(|| home_dir.join(".config")) + .join("user-dirs.dirs") } -} -pub fn user_dirs(home_dir_path: &Path) -> HashMap { - xdg_user_dirs::all(home_dir_path, &user_dir_file(home_dir_path)) -} + // this could be optimized further to not create a map and instead retrieve the requested path only + pub fn user_dir(user_dir_name: &str) -> Option { + if let Some(home_dir) = home_dir() { + xdg_user_dirs::single(&home_dir, &user_dir_file(&home_dir), user_dir_name) + .remove(user_dir_name) + } else { + None + } + } + pub fn user_dirs(home_dir_path: &Path) -> HashMap { + xdg_user_dirs::all(home_dir_path, &user_dir_file(home_dir_path)) + } } #[cfg(all(unix, not(any(target_os = "macos", target_os = "ios"))))] @@ -137,79 +153,79 @@ extern crate winapi; #[cfg(target_os = "windows")] mod target_windows { -use std::ffi::OsString; -use std::os::windows::ffi::OsStringExt; -use std::path::PathBuf; -use std::ptr; -use std::slice; - -use super::winapi; -use super::winapi::shared::winerror; -use super::winapi::um::{combaseapi, knownfolders, shlobj, shtypes, winbase, winnt}; - -pub fn known_folder(folder_id: shtypes::REFKNOWNFOLDERID) -> Option { - unsafe { - let mut path_ptr: winnt::PWSTR = ptr::null_mut(); - let result = shlobj::SHGetKnownFolderPath(folder_id, 0, ptr::null_mut(), &mut path_ptr); - if result == winerror::S_OK { - let len = winbase::lstrlenW(path_ptr) as usize; - let path = slice::from_raw_parts(path_ptr, len); - let ostr: OsString = OsStringExt::from_wide(path); - combaseapi::CoTaskMemFree(path_ptr as *mut winapi::ctypes::c_void); - Some(PathBuf::from(ostr)) - } else { - None + use std::ffi::OsString; + use std::os::windows::ffi::OsStringExt; + use std::path::PathBuf; + use std::ptr; + use std::slice; + + use super::winapi; + use super::winapi::shared::winerror; + use super::winapi::um::{combaseapi, knownfolders, shlobj, shtypes, winbase, winnt}; + + pub fn known_folder(folder_id: shtypes::REFKNOWNFOLDERID) -> Option { + unsafe { + let mut path_ptr: winnt::PWSTR = ptr::null_mut(); + let result = shlobj::SHGetKnownFolderPath(folder_id, 0, ptr::null_mut(), &mut path_ptr); + if result == winerror::S_OK { + let len = winbase::lstrlenW(path_ptr) as usize; + let path = slice::from_raw_parts(path_ptr, len); + let ostr: OsString = OsStringExt::from_wide(path); + combaseapi::CoTaskMemFree(path_ptr as *mut winapi::ctypes::c_void); + Some(PathBuf::from(ostr)) + } else { + None + } } } -} -pub fn known_folder_profile() -> Option { - known_folder(&knownfolders::FOLDERID_Profile) -} - -pub fn known_folder_roaming_app_data() -> Option { - known_folder(&knownfolders::FOLDERID_RoamingAppData) -} + pub fn known_folder_profile() -> Option { + known_folder(&knownfolders::FOLDERID_Profile) + } -pub fn known_folder_local_app_data() -> Option { - known_folder(&knownfolders::FOLDERID_LocalAppData) -} + pub fn known_folder_roaming_app_data() -> Option { + known_folder(&knownfolders::FOLDERID_RoamingAppData) + } -pub fn known_folder_music() -> Option { - known_folder(&knownfolders::FOLDERID_Music) -} + pub fn known_folder_local_app_data() -> Option { + known_folder(&knownfolders::FOLDERID_LocalAppData) + } -pub fn known_folder_desktop() -> Option { - known_folder(&knownfolders::FOLDERID_Desktop) -} + pub fn known_folder_music() -> Option { + known_folder(&knownfolders::FOLDERID_Music) + } -pub fn known_folder_documents() -> Option { - known_folder(&knownfolders::FOLDERID_Documents) -} + pub fn known_folder_desktop() -> Option { + known_folder(&knownfolders::FOLDERID_Desktop) + } -pub fn known_folder_downloads() -> Option { - known_folder(&knownfolders::FOLDERID_Downloads) -} + pub fn known_folder_documents() -> Option { + known_folder(&knownfolders::FOLDERID_Documents) + } -pub fn known_folder_pictures() -> Option { - known_folder(&knownfolders::FOLDERID_Pictures) -} + pub fn known_folder_downloads() -> Option { + known_folder(&knownfolders::FOLDERID_Downloads) + } -pub fn known_folder_public() -> Option { - known_folder(&knownfolders::FOLDERID_Public) -} -pub fn known_folder_templates() -> Option { - known_folder(&knownfolders::FOLDERID_Templates) -} -pub fn known_folder_videos() -> Option { - known_folder(&knownfolders::FOLDERID_Videos) -} + pub fn known_folder_pictures() -> Option { + known_folder(&knownfolders::FOLDERID_Pictures) + } + pub fn known_folder_public() -> Option { + known_folder(&knownfolders::FOLDERID_Public) + } + pub fn known_folder_templates() -> Option { + known_folder(&knownfolders::FOLDERID_Templates) + } + pub fn known_folder_videos() -> Option { + known_folder(&knownfolders::FOLDERID_Videos) + } } #[cfg(target_os = "windows")] pub use self::target_windows::{ - known_folder, known_folder_profile, known_folder_roaming_app_data, known_folder_local_app_data, - known_folder_music, known_folder_desktop, known_folder_documents, known_folder_downloads, - known_folder_pictures, known_folder_public, known_folder_templates, known_folder_videos + known_folder, known_folder_desktop, known_folder_documents, known_folder_downloads, + known_folder_local_app_data, known_folder_music, known_folder_pictures, known_folder_profile, + known_folder_public, known_folder_roaming_app_data, known_folder_templates, + known_folder_videos, };