from_os_str/
lib.rs

1#![forbid(unsafe_code)]
2#![warn(clippy::all)]
3#![forbid(missing_docs)]
4#![deny(warnings)]
5//! A macro for trying to convert an &OsStr to another more usefull type
6//! There are lots of ways to do that and this will pick the best via
7//! [autoref based specialization](https://lukaskalbertodt.github.io/2019/12/05/generalized-autoref-based-specialization.html)
8//! e.g. a `PathBuf` will be created via `From<OsString>` not `From<String>` so non UTF8 paths
9//! will work.
10//! ```
11//! # #[macro_use] extern crate from_os_str;
12//! # fn main() {
13//! use from_os_str::*;
14//! use std::ffi::OsStr;
15//! use std::path::Path;
16//! let os_str = OsStr::new("123");
17//! let path = try_from_os_str!(os_str as &Path);
18//! assert_eq!(path, Ok(Path::new("123")));
19//! let int = try_from_os_str!(os_str as u8);
20//! assert_eq!(int, Ok(123));
21//! # }
22//! ```
23
24use std::{
25    convert::Infallible,
26    error::Error as StdError,
27    ffi::{OsStr, OsString},
28    fmt::Display,
29    marker::PhantomData,
30    path::Path,
31    str::FromStr,
32};
33
34/// An error that can occure when converting an OsString to another type
35/// It can either be a problem converting the bytes passed into the OsStr
36/// as a valid UTF8 string or an error parsing the string
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum Error<T> {
39    /// The OsStr contains bytes that are not valid UTF8
40    Utf8,
41    /// Parsing the string failed
42    ParseErr(T),
43}
44
45impl<T: Display> Display for Error<T> {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            Error::Utf8 => write!(f, "invalid utf-8 sequence"),
49            Error::ParseErr(err) => err.fmt(f),
50        }
51    }
52}
53
54impl<T: StdError + 'static> StdError for Error<T> {
55    fn source(&self) -> Option<&(dyn StdError + 'static)> {
56        match self {
57            Error::Utf8 => None,
58            Error::ParseErr(err) => Some(err),
59        }
60    }
61}
62
63#[doc(hidden)]
64pub struct Wrap<'a, T>(&'a OsStr, PhantomData<T>);
65
66impl<'a, T> Wrap<'a, T> {
67    pub fn new(s: &'a OsStr) -> Self {
68        Wrap(s, PhantomData::<T>)
69    }
70}
71
72// Generate a trait for one layer of autoref based specialization
73// https://lukaskalbertodt.github.io/2019/12/05/generalized-autoref-based-specialization.html
74macro_rules! specialize {
75    (impl ($($and:tt)+) $name:ident for $from_ty:path {
76        fn from_str($s:ident: &str) -> Result<T, $err:ty> {$($body:tt)*}
77    }) => {
78        specialize! {
79            impl ($($and)+) $name for $from_ty {
80                fn specialized(&self) -> Result<T, Error<$err>> {
81                    match self.0.to_str() {
82                        None => Err(Error::Utf8),
83                        Some($s) => {$($body)*}.map_err(Error::ParseErr),
84                    }
85                }
86            }
87        }
88    };
89    (impl ($($and:tt)+) $name:ident for $from_ty:path {
90        fn from_os_str($s:ident: &OsStr) -> Result<T, $err:ty> {$($body:tt)*}
91    }) => {
92        specialize! {
93            impl ($($and)+) $name for $from_ty {
94                fn specialized(&self) -> Result<T, $err> {
95                    let $s = self.0;
96                    $($body)*
97                }
98            }
99        }
100    };
101    (impl ($($and:tt)+) $name:ident for $from_ty:path {
102        fn specialized(&$self:ident) -> Result<T, $err:ty> {$($body:tt)*}
103    }) => {
104        #[doc(hidden)]
105        pub trait $name {
106            type Return;
107            fn specialized(&self) -> Self::Return;
108        }
109
110        impl<'a, T: $from_ty> $name for $($and)+Wrap<'a, T> {
111            type Return = Result<T, $err>;
112            fn specialized(&$self) -> Self::Return {$($body)*}
113        }
114    };
115}
116
117// Conversions from lowest priority to heighest
118specialize! {
119    impl (&) Specialize8 for FromStr {
120        fn from_str(s: &str) -> Result<T, T::Err> {
121            T::from_str(s)
122        }
123    }
124}
125
126specialize! {
127    impl (&&) Specialize7 for TryFrom<&'a str> {
128        fn from_str(s: &str) -> Result<T, T::Error> {
129            T::try_from(s)
130        }
131    }
132}
133
134specialize! {
135    impl (&&) Specialize6 for TryFrom<&'a OsStr> {
136        fn from_os_str(s: &OsStr) -> Result<T, T::Error> {
137            T::try_from(s)
138        }
139    }
140}
141
142specialize! {
143    impl (&&&&) Specialize5 for From<String> {
144        fn from_str(s: &str) -> Result<T, Infallible> {
145            Ok(T::from(s.to_string()))
146        }
147    }
148}
149
150specialize! {
151    impl (&&&&&) Specialize4 for From<&'a str> {
152        fn from_str(s: &str) -> Result<T, Infallible> {
153            Ok(T::from(s))
154        }
155    }
156}
157
158specialize! {
159    impl (&&&&&&) Specialize3 for From<OsString> {
160        fn from_os_str(s: &OsStr) -> Result<T, Infallible> {
161            Ok(T::from(s.to_os_string()))
162        }
163    }
164}
165
166specialize! {
167    impl (&&&&&&&) Specialize2 for From<&'a Path> {
168        fn from_os_str(s: &OsStr) -> Result<T, Infallible> {
169            Ok(T::from(Path::new(s)))
170        }
171    }
172}
173
174specialize! {
175    impl (&&&&&&&&) Specialize1 for From<&'a OsStr> {
176        fn from_os_str(s: &OsStr) -> Result<T, Infallible> {
177            Ok(T::from(s))
178        }
179    }
180}
181
182/// Convert an `&OsStr` to another more usefull type
183/// There are lots of ways to do that and this will pick the best via
184/// [autoref based specialization](https://lukaskalbertodt.github.io/2019/12/05/generalized-autoref-based-specialization.html)
185/// e.g. a `PathBuf` will becreated via `From<OsString>` not `From<String>` so non UTF8 paths
186/// will work.
187/// ```
188/// # #[macro_use] extern crate from_os_str;
189/// # fn main() -> Result<(), Box<dyn std::error::Error>>{
190/// use from_os_str::*;
191/// use std::ffi::OsStr;
192/// use std::path::Path;
193/// let os_str = OsStr::new("123");
194/// let path = try_from_os_str!(os_str as &Path)?;
195/// assert_eq!(path, Path::new("123"));
196/// let str = try_from_os_str!(os_str as &str)?;
197/// assert_eq!(str, "123");
198/// let string = try_from_os_str!(os_str as String)?;
199/// assert_eq!(string, "123".to_string());
200/// let int = try_from_os_str!(os_str as u8)?;
201/// assert_eq!(int, 123);
202/// # Ok(())}
203/// ```
204#[macro_export]
205macro_rules! try_from_os_str {
206    ($name:ident as $typ:ty) => {
207        (&&&&&&&&Wrap::<$typ>::new($name)).specialized()
208    };
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use std::path::Path;
215    use std::path::PathBuf;
216
217    #[derive(Debug, Clone, PartialEq, Eq)]
218    struct Foo(String);
219
220    impl From<&OsStr> for Foo {
221        fn from(s: &OsStr) -> Self {
222            Foo("OS: ".to_string() + &s.to_string_lossy())
223        }
224    }
225
226    impl FromStr for Foo {
227        type Err = Infallible;
228
229        fn from_str(s: &str) -> Result<Self, Self::Err> {
230            Ok(Foo("STR: ".to_string() + s))
231        }
232    }
233
234    #[test]
235    fn it_works() {
236        let os_str = OsStr::new("123");
237        let os_str_2 = try_from_os_str!(os_str as &OsStr).unwrap();
238        assert_eq!(os_str_2, os_str);
239
240        let path = try_from_os_str!(os_str as &Path).unwrap();
241        assert_eq!(path, Path::new("123"));
242        let path = try_from_os_str!(os_str as PathBuf).unwrap();
243        assert_eq!(&path, Path::new("123"));
244
245        let str = try_from_os_str!(os_str as &str).unwrap();
246        assert_eq!(str, "123");
247        let string = try_from_os_str!(os_str as String).unwrap();
248        assert_eq!(string, "123".to_string());
249        let int = try_from_os_str!(os_str as u8).unwrap();
250        assert_eq!(int, 123);
251
252        // test priority works
253        let foo = try_from_os_str!(os_str as Foo);
254        assert_eq!(foo, Ok(Foo("OS: 123".to_owned())));
255    }
256
257    #[test]
258    #[cfg(unix)]
259    fn it_works_with_non_utf8() {
260        use std::os::unix::ffi::OsStrExt;
261        let os_str = OsStr::from_bytes(&[0xff, 0xff]);
262        let os_str_2 = try_from_os_str!(os_str as &OsStr).unwrap();
263        assert_eq!(os_str_2, os_str);
264
265        let path = try_from_os_str!(os_str as &Path).unwrap();
266        assert_eq!(path, Path::new(os_str));
267        let path = try_from_os_str!(os_str as PathBuf).unwrap();
268        assert_eq!(path, Path::new(os_str));
269        let str = try_from_os_str!(os_str as &str);
270        assert_eq!(str, Err(Error::Utf8));
271        let string = try_from_os_str!(os_str as String);
272        assert_eq!(string, Err(Error::Utf8));
273        let int = try_from_os_str!(os_str as u8);
274        assert_eq!(int, Err(Error::Utf8));
275
276        // test priority works
277        let foo = try_from_os_str!(os_str as Foo);
278        assert_eq!(foo, Ok(Foo("OS: ��".to_owned())));
279    }
280}