Skip to main content

tauri_plugin_fs/
file_path.rs

1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use std::{
6    convert::Infallible,
7    path::{Path, PathBuf},
8    str::FromStr,
9};
10
11use serde::Serialize;
12use tauri::path::SafePathBuf;
13
14use crate::{Error, Result};
15
16/// Represents either a filesystem path or a URI pointing to a file
17/// such as `file://` URIs or Android `content://` URIs.
18#[derive(Debug, Serialize, Clone)]
19#[serde(untagged)]
20pub enum FilePath {
21    /// `file://` URIs or Android `content://` URIs.
22    Url(url::Url),
23    /// Regular [`PathBuf`]
24    Path(PathBuf),
25}
26
27/// Represents either a safe filesystem path or a URI pointing to a file
28/// such as `file://` URIs or Android `content://` URIs.
29#[derive(Debug, Clone, Serialize)]
30pub enum SafeFilePath {
31    /// `file://` URIs or Android `content://` URIs.
32    Url(url::Url),
33    /// Safe [`PathBuf`], see [`SafePathBuf``].
34    Path(SafePathBuf),
35}
36
37impl FilePath {
38    /// Get a reference to the contained [`Path`] if the variant is [`FilePath::Path`].
39    ///
40    /// Use [`FilePath::into_path`] to try to convert the [`FilePath::Url`] variant as well.
41    #[inline]
42    pub fn as_path(&self) -> Option<&Path> {
43        match self {
44            Self::Url(_) => None,
45            Self::Path(p) => Some(p),
46        }
47    }
48
49    /// Try to convert into [`PathBuf`] if possible.
50    ///
51    /// This calls [`Url::to_file_path`](url::Url::to_file_path) if the variant is [`FilePath::Url`],
52    /// otherwise returns the contained [PathBuf] as is.
53    #[inline]
54    pub fn into_path(self) -> Result<PathBuf> {
55        match self {
56            Self::Url(url) => url.to_file_path().map_err(|_| Error::InvalidPathUrl),
57            Self::Path(p) => Ok(p),
58        }
59    }
60
61    /// Takes the contained [`PathBuf`] if the variant is [`FilePath::Path`],
62    /// and when possible, converts Windows UNC paths to regular paths.
63    #[inline]
64    pub fn simplified(self) -> Self {
65        match self {
66            Self::Url(url) => Self::Url(url),
67            Self::Path(p) => Self::Path(dunce::simplified(&p).to_path_buf()),
68        }
69    }
70}
71
72impl SafeFilePath {
73    /// Get a reference to the contained [`Path`] if the variant is [`SafeFilePath::Path`].
74    ///
75    /// Use [`SafeFilePath::into_path`] to try to convert the [`SafeFilePath::Url`] variant as well.
76    #[inline]
77    pub fn as_path(&self) -> Option<&Path> {
78        match self {
79            Self::Url(_) => None,
80            Self::Path(p) => Some(p.as_ref()),
81        }
82    }
83
84    /// Try to convert into [`PathBuf`] if possible.
85    ///
86    /// This calls [`Url::to_file_path`](url::Url::to_file_path) if the variant is [`SafeFilePath::Url`],
87    /// otherwise returns the contained [PathBuf] as is.
88    #[inline]
89    pub fn into_path(self) -> Result<PathBuf> {
90        match self {
91            Self::Url(url) => url.to_file_path().map_err(|_| Error::InvalidPathUrl),
92            Self::Path(p) => Ok(p.as_ref().to_owned()),
93        }
94    }
95
96    /// Takes the contained [`PathBuf`] if the variant is [`SafeFilePath::Path`],
97    /// and when possible, converts Windows UNC paths to regular paths.
98    #[inline]
99    pub fn simplified(self) -> Self {
100        match self {
101            Self::Url(url) => Self::Url(url),
102            Self::Path(p) => {
103                // Safe to unwrap since it was a safe file path already
104                Self::Path(SafePathBuf::new(dunce::simplified(p.as_ref()).to_path_buf()).unwrap())
105            }
106        }
107    }
108}
109
110impl std::fmt::Display for FilePath {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        match self {
113            Self::Url(u) => u.fmt(f),
114            Self::Path(p) => p.display().fmt(f),
115        }
116    }
117}
118
119impl std::fmt::Display for SafeFilePath {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        match self {
122            Self::Url(u) => u.fmt(f),
123            Self::Path(p) => p.display().fmt(f),
124        }
125    }
126}
127
128impl<'de> serde::Deserialize<'de> for FilePath {
129    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
130    where
131        D: serde::Deserializer<'de>,
132    {
133        struct FilePathVisitor;
134
135        impl serde::de::Visitor<'_> for FilePathVisitor {
136            type Value = FilePath;
137
138            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
139                formatter.write_str("a string representing an file URL or a path")
140            }
141
142            fn visit_str<E>(self, s: &str) -> std::result::Result<Self::Value, E>
143            where
144                E: serde::de::Error,
145            {
146                FilePath::from_str(s).map_err(|e| {
147                    serde::de::Error::invalid_value(
148                        serde::de::Unexpected::Str(s),
149                        &e.to_string().as_str(),
150                    )
151                })
152            }
153        }
154
155        deserializer.deserialize_str(FilePathVisitor)
156    }
157}
158
159impl<'de> serde::Deserialize<'de> for SafeFilePath {
160    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
161    where
162        D: serde::Deserializer<'de>,
163    {
164        struct SafeFilePathVisitor;
165
166        impl serde::de::Visitor<'_> for SafeFilePathVisitor {
167            type Value = SafeFilePath;
168
169            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
170                formatter.write_str("a string representing an file URL or a path")
171            }
172
173            fn visit_str<E>(self, s: &str) -> std::result::Result<Self::Value, E>
174            where
175                E: serde::de::Error,
176            {
177                SafeFilePath::from_str(s).map_err(|e| {
178                    serde::de::Error::invalid_value(
179                        serde::de::Unexpected::Str(s),
180                        &e.to_string().as_str(),
181                    )
182                })
183            }
184        }
185
186        deserializer.deserialize_str(SafeFilePathVisitor)
187    }
188}
189
190impl FromStr for FilePath {
191    type Err = Infallible;
192    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
193        if let Ok(url) = url::Url::from_str(s)
194            && url.scheme().len() != 1
195        {
196            return Ok(Self::Url(url));
197        }
198        Ok(Self::Path(PathBuf::from(s)))
199    }
200}
201
202impl FromStr for SafeFilePath {
203    type Err = Error;
204    fn from_str(s: &str) -> Result<Self> {
205        if let Ok(url) = url::Url::from_str(s)
206            // single letter schemes are Windows drive letters, and URLs that cannot be a base
207            // (no `//` nor `/` after the scheme) are relative paths such as `notes:2024.txt`
208            && url.scheme().len() != 1 && !url.cannot_be_a_base()
209        {
210            return Ok(Self::Url(url));
211        }
212
213        SafePathBuf::new(s.into())
214            .map(SafeFilePath::Path)
215            .map_err(Error::UnsafePathBuf)
216    }
217}
218
219impl From<PathBuf> for FilePath {
220    fn from(value: PathBuf) -> Self {
221        Self::Path(value)
222    }
223}
224
225impl TryFrom<PathBuf> for SafeFilePath {
226    type Error = Error;
227    fn try_from(value: PathBuf) -> Result<Self> {
228        SafePathBuf::new(value)
229            .map(SafeFilePath::Path)
230            .map_err(Error::UnsafePathBuf)
231    }
232}
233
234impl From<&Path> for FilePath {
235    fn from(value: &Path) -> Self {
236        Self::Path(value.to_owned())
237    }
238}
239
240impl TryFrom<&Path> for SafeFilePath {
241    type Error = Error;
242    fn try_from(value: &Path) -> Result<Self> {
243        SafePathBuf::new(value.to_path_buf())
244            .map(SafeFilePath::Path)
245            .map_err(Error::UnsafePathBuf)
246    }
247}
248
249impl From<&PathBuf> for FilePath {
250    fn from(value: &PathBuf) -> Self {
251        Self::Path(value.to_owned())
252    }
253}
254
255impl TryFrom<&PathBuf> for SafeFilePath {
256    type Error = Error;
257    fn try_from(value: &PathBuf) -> Result<Self> {
258        SafePathBuf::new(value.to_owned())
259            .map(SafeFilePath::Path)
260            .map_err(Error::UnsafePathBuf)
261    }
262}
263
264impl From<url::Url> for FilePath {
265    fn from(value: url::Url) -> Self {
266        Self::Url(value)
267    }
268}
269
270impl From<url::Url> for SafeFilePath {
271    fn from(value: url::Url) -> Self {
272        Self::Url(value)
273    }
274}
275
276impl TryFrom<FilePath> for PathBuf {
277    type Error = Error;
278    fn try_from(value: FilePath) -> Result<Self> {
279        value.into_path()
280    }
281}
282
283impl TryFrom<SafeFilePath> for PathBuf {
284    type Error = Error;
285    fn try_from(value: SafeFilePath) -> Result<Self> {
286        value.into_path()
287    }
288}
289
290impl From<SafeFilePath> for FilePath {
291    fn from(value: SafeFilePath) -> Self {
292        match value {
293            SafeFilePath::Url(url) => FilePath::Url(url),
294            SafeFilePath::Path(p) => FilePath::Path(p.as_ref().to_owned()),
295        }
296    }
297}
298
299impl TryFrom<FilePath> for SafeFilePath {
300    type Error = Error;
301
302    fn try_from(value: FilePath) -> Result<Self> {
303        match value {
304            FilePath::Url(url) => Ok(SafeFilePath::Url(url)),
305            FilePath::Path(p) => SafePathBuf::new(p)
306                .map(SafeFilePath::Path)
307                .map_err(Error::UnsafePathBuf),
308        }
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use std::str::FromStr;
315
316    use super::SafeFilePath;
317
318    #[test]
319    fn safe_file_path_from_str() {
320        for url in [
321            "file:///C:/Users",
322            "file:///home/user/file.txt",
323            "content://com.android.providers/document/1",
324            "asset://localhost/file.txt",
325        ] {
326            assert!(
327                matches!(SafeFilePath::from_str(url), Ok(SafeFilePath::Url(_))),
328                "{url}"
329            );
330        }
331
332        for path in [
333            "C:/Users",
334            "C:\\Users",
335            "notes:2024.txt",
336            "ab:c",
337            "dir/file.txt",
338            "/home/user/file.txt",
339        ] {
340            assert!(
341                matches!(SafeFilePath::from_str(path), Ok(SafeFilePath::Path(_))),
342                "{path}"
343            );
344        }
345    }
346}