1
0

split into individual modules

This commit is contained in:
2025-10-16 15:13:38 +08:00
parent 4b1f85c2f3
commit dab91f1581
4 changed files with 535 additions and 210 deletions

25
example/ppic.toml Normal file
View File

@ -0,0 +1,25 @@
identifier = "PineapplePicture"
path = 'C:\path\to\ppic.exe'
clsid = "{B5291320-FE7C-4069-BF87-A0AC327FCD20}"
[manners]
common = '"C:\path\to\ppic.exe" "%1"'
[exts]
".jpg" = "common"
".jfif" = "common"
".gif" = "common"
".bmp" = "common"
".png" = "common"
".ico" = "common"
".jpeg" = "common"
".tif" = "common"
".tiff" = "common"
".webp" = "common"
".svg" = "common"
".kra" = "common"
".xcf" = "common"
".avif" = "common"
".qoi" = "common"
".apng" = "common"
".exr" = "common"

283
wfassoc/src/assoc.rs Normal file
View File

@ -0,0 +1,283 @@
//! The module including all struct representing Windows file association concept,
//! like file extension, ProgId, CLSID and etc.
use regex::Regex;
use std::fmt::Display;
use std::str::FromStr;
use std::sync::LazyLock;
use thiserror::Error as TeError;
use uuid::Uuid;
// region: File Extension
/// The struct representing an file extension which must start with dot (`.`)
/// and followed by at least one arbitrary characters.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Ext {
/// The body of file extension (excluding dot).
body: String,
}
impl Ext {
/// Create an new file extension.
pub fn new(raw: &str) -> Result<Self, ParseExtError> {
Self::from_str(raw)
}
/// Get the body part of file extension (excluding dot)
pub fn inner(&self) -> &str {
&self.body
}
}
/// The error occurs when try parsing string into FileExt.
#[derive(Debug, TeError)]
#[error("given file extension name \"{inner}\" is invalid")]
pub struct ParseExtError {
inner: String,
}
impl ParseExtError {
fn new(inner: &str) -> Self {
Self {
inner: inner.to_string(),
}
}
}
impl Display for Ext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, ".{}", self.body)
}
}
impl FromStr for Ext {
type Err = ParseExtError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\.([^\.]+)$").unwrap());
match RE.captures(s) {
Some(v) => Ok(Self {
body: v[1].to_string(),
}),
None => Err(ParseExtError::new(s)),
}
}
}
// endregion
// region: Programmatic Identifiers (ProgId)
/// The struct representing Programmatic Identifiers (ProgId).
///
/// Because there is optional part in standard ProgId, and not all software developers
/// are willing to following Microsoft suggestions, there is no strict constaint for ProgId.
/// So this struct is actually an enum which holding any possible ProgId format.
///
/// Reference:
/// - https://learn.microsoft.com/en-us/windows/win32/shell/fa-progids
/// - https://learn.microsoft.com/en-us/windows/win32/com/-progid--key
pub enum ProgId {
Plain(String),
Loose(LosseProgId),
Strict(StrictProgId),
}
impl From<&str> for ProgId {
fn from(s: &str) -> Self {
// match it for strict ProgId first
if let Ok(v) = StrictProgId::from_str(s) {
return Self::Strict(v);
}
// then match for loose ProgId
if let Ok(v) = LosseProgId::from_str(s) {
return Self::Loose(v);
}
// fallback with plain
Self::Plain(s.to_string())
}
}
impl Display for ProgId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProgId::Plain(v) => v.fmt(f),
ProgId::Loose(v) => v.fmt(f),
ProgId::Strict(v) => v.fmt(f),
}
}
}
/// The error occurs when parsing ProgId.
#[derive(Debug, TeError)]
#[error("given ProgId \"{inner}\" is invalid")]
pub struct ParseProgIdError {
inner: String,
}
impl ParseProgIdError {
fn new(s: &str) -> Self {
Self {
inner: s.to_string(),
}
}
}
/// The ProgId similar with strict ProgId, but no version part.
pub struct LosseProgId {
vendor: String,
component: String,
}
impl LosseProgId {
pub fn new(vendor: &str, component: &str) -> Self {
Self {
vendor: vendor.to_string(),
component: component.to_string(),
}
}
pub fn get_vendor(&self) -> &str {
&self.vendor
}
pub fn get_component(&self) -> &str {
&self.component
}
}
impl FromStr for LosseProgId {
type Err = ParseProgIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^([a-zA-Z0-9]+)\.([a-zA-Z0-9]+)$").unwrap());
let caps = RE.captures(s);
if let Some(caps) = caps {
let vendor = &caps[1];
let component = &caps[2];
Ok(Self::new(vendor, component))
} else {
Err(ParseProgIdError::new(s))
}
}
}
impl Display for LosseProgId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}", self.vendor, self.component)
}
}
/// The ProgId exactly follows `[Vendor or Application].[Component].[Version]` format.
pub struct StrictProgId {
vendor: String,
component: String,
version: u32,
}
impl StrictProgId {
pub fn new(vendor: &str, component: &str, version: u32) -> Self {
Self {
vendor: vendor.to_string(),
component: component.to_string(),
version,
}
}
pub fn get_vendor(&self) -> &str {
&self.vendor
}
pub fn get_component(&self) -> &str {
&self.component
}
pub fn get_version(&self) -> u32 {
self.version
}
}
impl FromStr for StrictProgId {
type Err = ParseProgIdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^([a-zA-Z0-9]+)\.([a-zA-Z0-9]+)\.([0-9]+)$").unwrap());
let caps = RE.captures(s);
if let Some(caps) = caps {
let vendor = &caps[1];
let component = &caps[2];
let version = caps[3]
.parse::<u32>()
.map_err(|_| ParseProgIdError::new(s))?;
Ok(Self::new(vendor, component, version))
} else {
Err(ParseProgIdError::new(s))
}
}
}
impl Display for StrictProgId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.vendor, self.component, self.version)
}
}
// endregion
// region: CLSID
pub struct Clsid {
inner: Uuid,
}
impl Clsid {
pub fn new(uuid: &str) -> Result<Self, ParseClsidError> {
Self::from_str(uuid)
}
// TODO: May add CLSID generator in there.
}
/// The error occurs when parsing CLSID
#[derive(Debug, TeError)]
#[error("given string \"{inner}\" is invalid for uuid")]
pub struct ParseClsidError {
inner: String,
}
impl ParseClsidError {
fn new(s: &str) -> Self {
Self {
inner: s.to_string(),
}
}
}
impl FromStr for Clsid {
type Err = ParseClsidError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self {
inner: Uuid::parse_str(s).map_err(|_| ParseClsidError::new(s))?,
})
}
}
impl Display for Clsid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.inner.braced().to_string())
}
}
// endregion
// region: Icon Resource
// endregion
// region: String Resource
// endregion

View File

@ -4,14 +4,14 @@
#[cfg(not(target_os = "windows"))]
compile_error!("Crate wfassoc is only supported on Windows.");
use regex::Regex;
pub(crate) mod assoc;
pub(crate) mod utilities;
use indexmap::{IndexMap, IndexSet};
use std::ffi::OsStr;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::LazyLock;
use thiserror::Error as TeError;
use indexmap::{IndexMap, IndexSet};
use winreg::RegKey;
use winreg::enums::{
HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_READ, KEY_WRITE,
@ -21,121 +21,26 @@ use winreg::enums::{
/// All possible error occurs in this crate.
#[derive(Debug, TeError)]
pub enum WfError {
#[error("no administrative privilege")]
NoPrivilege,
pub enum Error {
#[error("error occurs when manipulating with Registry: {0}")]
BadRegOper(#[from] std::io::Error),
#[error("{0}")]
CastOsStr(#[from] utilities::CastOsStrError),
#[error("no administrative privilege")]
NoPrivilege,
#[error("given full path to application is invalid")]
BadFullAppPath,
#[error("failed when casting OS string into string")]
BadOsStrCast,
#[error("file extension {0} is already registered")]
#[error("manner \"{0}\" is already registered")]
DupManner(String),
#[error("file extension \"{0}\" is already registered")]
DupExt(String),
#[error("the token of associated manner for file extension is invalid")]
InvalidAssocManner,
}
/// The result type used in this crate.
pub type WfResult<T> = Result<T, WfError>;
// endregion
// region: Utilities
/// The println macro only works on Debug mode
/// for tracing the execution of some important functions.
macro_rules! debug_println {
// For no argument.
() => {
if cfg!(debug_assertions) {
println!();
}
};
// For one or more arguments like println!.
($($arg:tt)*) => {
if cfg!(debug_assertions) {
println!($($arg)*);
}
};
}
/// Check whether current process has administrative privilege.
///
/// It usually means that checking whether current process is running as Administrator.
/// Return true if it is, otherwise false.
///
/// Reference: https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
fn has_privilege() -> bool {
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Security::{
AllocateAndInitializeSid, CheckTokenMembership, FreeSid, PSID, SECURITY_NT_AUTHORITY,
};
use windows_sys::Win32::System::SystemServices::{
DOMAIN_ALIAS_RID_ADMINS, SECURITY_BUILTIN_DOMAIN_RID,
};
use windows_sys::core::BOOL;
let nt_authority = SECURITY_NT_AUTHORITY.clone();
let mut administrators_group: PSID = PSID::default();
let success: BOOL = unsafe {
AllocateAndInitializeSid(
&nt_authority,
2,
SECURITY_BUILTIN_DOMAIN_RID as u32,
DOMAIN_ALIAS_RID_ADMINS as u32,
0,
0,
0,
0,
0,
0,
&mut administrators_group,
)
};
if success == 0 {
panic!("Win32 AllocateAndInitializeSid() failed");
}
let mut is_member: BOOL = BOOL::default();
let success: BOOL =
unsafe { CheckTokenMembership(HANDLE::default(), administrators_group, &mut is_member) };
unsafe {
FreeSid(administrators_group);
}
if success == 0 {
panic!("Win32 CheckTokenMembership() failed");
}
is_member != 0
}
/// Notify Windows that some file associations are changed, and should refresh them.
/// This function must be called once you change any file associations.
fn notify_assoc_changed() -> () {
use windows_sys::Win32::UI::Shell::{SHCNE_ASSOCCHANGED, SHCNF_IDLIST, SHChangeNotify};
unsafe {
SHChangeNotify(
SHCNE_ASSOCCHANGED as i32,
SHCNF_IDLIST,
Default::default(),
Default::default(),
)
}
}
/// Try casting given &Path into &str.
fn path_to_str(path: &Path) -> WfResult<&str> {
path.to_str().ok_or(WfError::BadOsStrCast)
}
/// Try casting given &OsStr into &str.
fn osstr_to_str(osstr: &OsStr) -> WfResult<&str> {
osstr.to_str().ok_or(WfError::BadOsStrCast)
}
pub type Result<T> = std::result::Result<T, Error>;
// endregion
@ -168,7 +73,7 @@ impl TryFromViewError {
impl TryFrom<View> for Scope {
type Error = TryFromViewError;
fn try_from(value: View) -> Result<Self, Self::Error> {
fn try_from(value: View) -> std::result::Result<Self, Self::Error> {
match value {
View::User => Ok(Self::User),
View::System => Ok(Self::System),
@ -184,7 +89,7 @@ impl Scope {
// If we operate on System, and we do not has privilege,
// we think we do not have privilege, otherwise,
// there is no privilege required.
!matches!(self, Self::System if !has_privilege())
!matches!(self, Self::System if !utilities::has_privilege())
}
}
@ -211,113 +116,111 @@ impl From<Scope> for View {
// endregion
// region: File Extension
// region: Manner
/// The struct representing an file extension which must start with dot (`.`)
/// and followed by at least one arbitrary characters.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Ext {
/// The body of file extension (excluding dot).
body: String,
/// The struct representing a program manner.
/// Manner usually mean the way to open files,
/// or more preciously, the consititution of command arguments passed to program.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Manner {
argv: String,
}
impl Ext {
/// Create an new file extension.
pub fn new(raw: &str) -> Result<Self, ParseExtError> {
Self::from_str(raw)
}
/// Get the body part of file extension (excluding dot)
pub fn inner(&self) -> &str {
&self.body
}
}
/// The error occurs when try parsing string into FileExt.
#[derive(Debug, TeError)]
#[error("given file extension name \"{inner}\" is invalid")]
pub struct ParseExtError {
inner: String
}
impl ParseExtError {
fn new(inner: &str) -> Self {
Self { inner: inner.to_string() }
}
}
impl Display for Ext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, ".{}", self.body)
}
}
impl FromStr for Ext {
type Err = ParseExtError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\.([^\.]+)$").unwrap());
match RE.captures(s) {
Some(v) => Ok(Self {
body: v[1].to_string(),
}),
None => Err(ParseExtError::new(s)),
impl Manner {
pub fn new(argv: &str) -> Self {
Self {
argv: argv.to_string(),
}
}
}
impl Display for Manner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.argv)
}
}
// endregion
// region: Program
/// The struct representing a complete program for registration and unregistration.
pub struct Program {
/// The identifier of this program.
identifier: String,
/// The fully qualified path to the application.
full_path: PathBuf,
/// Optional default icon resource for overriding.
///
/// TODO: Use specialized IconRc struct instead.
default_icon: Option<String>,
/// Optional friendly app name for overriding.
///
/// TODO: Use specialized StringRc for overriding.
friendly_app_name: Option<String>,
/// The collection holding all manners of this program.
manners: IndexSet<Manner>,
/// The collection holding all file extensions supported by this program.
exts: IndexSet<Ext>,
/// The key is file estension and value is its associated manner for opening it.
exts: IndexMap<assoc::Ext, Token>,
}
impl Program {
/// Create a new registrar for following operations.
///
/// `identifier` is the unique name of this program.
/// If should only contain digits and alphabet chars,
/// and should not start with any digits.
/// For example, "MyApp" is okey but following names are not okey:
///
/// - `My App`
/// - `3DViewer`
/// - `我的Qt最时尚`
///
/// More preciously, `identifier` will be used as the vendor part of ProgId.
///
/// `full_path` is the fully qualified path to the application.
///
/// `default_icon` is an optional icon resource replacing the default one
/// fetched from the first icon resource of your executable application.
///
/// `friendly_app_name` also is an optional string or string resource replacing
/// the info fetched from executable application's version information.
pub fn new(
full_path: &Path,
default_icon: Option<&str>,
friendly_app_name: Option<&str>,
) -> Self {
pub fn new(identifier: &str, full_path: &Path) -> Self {
// TODO: Add checker for identifier
Self {
identifier: identifier.to_string(),
full_path: full_path.to_path_buf(),
default_icon: default_icon.map(|s| s.to_string()),
friendly_app_name: friendly_app_name.map(|s| s.to_string()),
exts: IndexSet::new(),
manners: IndexSet::new(),
exts: IndexMap::new(),
}
}
/// Add file extension supported by this program.
pub fn add_ext(&mut self, ext: &Ext) -> WfResult<()> {
if self.exts.insert(ext.clone()) {
Ok(())
/// Add manner provided by this program.
pub fn add_manner(&mut self, manner: Manner) -> Result<Token> {
// Backup a stringfied manner for error output.
let manner_str = manner.to_string();
// Insert manner.
let idx = self.manners.len();
if self.manners.insert(manner) {
Ok(idx)
} else {
Err(WfError::DupExt(ext.to_string()))
Err(Error::DupExt(manner_str))
}
}
/// Get the reference to manner with given token.
pub fn get_manner(&self, token: Token) -> Option<&Manner> {
self.manners.get_index(token)
}
/// Add file extension supported by this program and its associated manner.
pub fn add_ext(&mut self, ext: assoc::Ext, token: Token) -> Result<Token> {
// Check manner token
if let None = self.get_manner(token) {
return Err(Error::InvalidAssocManner);
}
// Backup a stringfied extension for error output.
let ext_str = ext.to_string();
// Insert file extension
let idx = self.exts.len();
if let None = self.exts.insert(ext, token) {
Ok(idx)
} else {
Err(Error::DupExt(ext_str))
}
}
/// Get the reference to file extension with given token.
pub fn get_ext(&self, token: Token) -> Option<&assoc::Ext> {
self.exts.get_index(token).map(|p| p.0)
}
}
impl Program {
@ -325,10 +228,10 @@ impl Program {
const APPLICATIONS: &str = "Software\\Classes\\Applications";
/// Register this application.
pub fn register(&self, scope: Scope) -> WfResult<()> {
pub fn register(&self, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(WfError::NoPrivilege);
return Err(Error::NoPrivilege);
}
// Fetch root key.
@ -345,37 +248,32 @@ impl Program {
let subkey_parent = hk.open_subkey_with_flags(Self::APP_PATHS, KEY_READ)?;
let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?;
// Write App Paths values
subkey.set_value("", &path_to_str(&self.full_path)?)?;
subkey.set_value("Path", &osstr_to_str(&start_in)?)?;
subkey.set_value("", &utilities::path_to_str(&self.full_path)?)?;
subkey.set_value("Path", &utilities::osstr_to_str(&start_in)?)?;
// Create Applications subkey
debug_println!("Adding Applications subkey...");
let subkey_parent = hk.open_subkey_with_flags(Self::APPLICATIONS, KEY_READ)?;
let (subkey, _) = subkey_parent.create_subkey_with_flags(file_name, KEY_WRITE)?;
// Write Applications values
if let Some(default_icon) = &self.default_icon {
subkey.set_value("DefaultIcon", default_icon)?;
}
if let Some(friendly_app_name) = &self.friendly_app_name {
subkey.set_value("FriendlyAppName", friendly_app_name)?;
}
if !self.exts.is_empty() {
let (supported_types, _) = subkey.create_subkey_with_flags("SupportedTypes", KEY_WRITE)?;
for ext in &self.exts {
let (supported_types, _) =
subkey.create_subkey_with_flags("SupportedTypes", KEY_WRITE)?;
for ext in self.exts.keys() {
supported_types.set_value(ext.to_string(), &"")?;
}
}
// Okey
notify_assoc_changed();
utilities::notify_assoc_changed();
Ok(())
}
/// Unregister this application.
pub fn unregister(&self, scope: Scope) -> WfResult<()> {
pub fn unregister(&self, scope: Scope) -> Result<()> {
// Check privilege
if !scope.has_privilege() {
return Err(WfError::NoPrivilege);
return Err(Error::NoPrivilege);
}
// Fetch root key and file name.
@ -396,14 +294,14 @@ impl Program {
subkey_parent.delete_subkey_all(file_name)?;
// Okey
notify_assoc_changed();
utilities::notify_assoc_changed();
Ok(())
}
/// Check whether this application has been registered.
///
/// Please note that this is a rough check and do not validate any data.
pub fn is_registered(&self, scope: Scope) -> WfResult<bool> {
pub fn is_registered(&self, scope: Scope) -> Result<bool> {
// Fetch root key and file name.
let hk = RegKey::predef(match scope {
Scope::User => HKEY_CURRENT_USER,
@ -433,25 +331,25 @@ impl Program {
impl Program {
/// Extract the file name part from full path to application,
/// which was used in Registry path component.
fn extract_file_name(&self) -> WfResult<&OsStr> {
fn extract_file_name(&self) -> Result<&OsStr> {
// Get the file name part and make sure it is not empty.
// Empty checker is CRUCIAL!
self.full_path
.file_name()
.and_then(|p| if p.is_empty() { None } else { Some(p) })
.ok_or(WfError::BadFullAppPath)
.ok_or(Error::BadFullAppPath)
}
/// Extract the start in path from full path to application,
/// which basically is the stem of full path.
fn extract_start_in(&self) -> WfResult<&OsStr> {
fn extract_start_in(&self) -> Result<&OsStr> {
// Get parent part and make sure it is not empty
// Empty checker is CRUCIAL!
self.full_path
.parent()
.map(|p| p.as_os_str())
.and_then(|p| if p.is_empty() { None } else { Some(p) })
.ok_or(WfError::BadFullAppPath)
.ok_or(Error::BadFullAppPath)
}
}

119
wfassoc/src/utilities.rs Normal file
View File

@ -0,0 +1,119 @@
//! The module containing useful stuff used in this crate.
use std::ffi::OsStr;
use std::path::Path;
use thiserror::Error as TeError;
/// The println macro only works on Debug mode
/// for tracing the execution of some important functions.
#[macro_export]
macro_rules! debug_println {
// For no argument.
() => {
if cfg!(debug_assertions) {
println!();
}
};
// For one or more arguments like println!.
($($arg:tt)*) => {
if cfg!(debug_assertions) {
println!($($arg)*);
}
};
}
// region: Windows Related
/// Check whether current process has administrative privilege.
///
/// It usually means that checking whether current process is running as Administrator.
/// Return true if it is, otherwise false.
///
/// Reference: https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership
pub fn has_privilege() -> bool {
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Security::{
AllocateAndInitializeSid, CheckTokenMembership, FreeSid, PSID, SECURITY_NT_AUTHORITY,
};
use windows_sys::Win32::System::SystemServices::{
DOMAIN_ALIAS_RID_ADMINS, SECURITY_BUILTIN_DOMAIN_RID,
};
use windows_sys::core::BOOL;
let nt_authority = SECURITY_NT_AUTHORITY.clone();
let mut administrators_group: PSID = PSID::default();
let success: BOOL = unsafe {
AllocateAndInitializeSid(
&nt_authority,
2,
SECURITY_BUILTIN_DOMAIN_RID as u32,
DOMAIN_ALIAS_RID_ADMINS as u32,
0,
0,
0,
0,
0,
0,
&mut administrators_group,
)
};
if success == 0 {
panic!("Win32 AllocateAndInitializeSid() failed");
}
let mut is_member: BOOL = BOOL::default();
let success: BOOL =
unsafe { CheckTokenMembership(HANDLE::default(), administrators_group, &mut is_member) };
unsafe {
FreeSid(administrators_group);
}
if success == 0 {
panic!("Win32 CheckTokenMembership() failed");
}
is_member != 0
}
/// Notify Windows that some file associations are changed, and should refresh them.
/// This function must be called once you change any file associations.
pub fn notify_assoc_changed() -> () {
use windows_sys::Win32::UI::Shell::{SHCNE_ASSOCCHANGED, SHCNF_IDLIST, SHChangeNotify};
unsafe {
SHChangeNotify(
SHCNE_ASSOCCHANGED as i32,
SHCNF_IDLIST,
Default::default(),
Default::default(),
)
}
}
// endregion
// region OS String Related
/// The error occurs when casting `OsStr` into `str`.
#[derive(Debug, TeError)]
#[error("failed when casting OS string into string")]
pub struct CastOsStrError {}
impl CastOsStrError {
fn new() -> Self {
Self {}
}
}
/// Try casting given &Path into &str.
pub fn path_to_str(path: &Path) -> Result<&str, CastOsStrError> {
path.to_str().ok_or(CastOsStrError::new())
}
/// Try casting given &OsStr into &str.
pub fn osstr_to_str(osstr: &OsStr) -> Result<&str, CastOsStrError> {
osstr.to_str().ok_or(CastOsStrError::new())
}
// endregion