diff --git a/.changes/fs-windows-path.md b/.changes/fs-windows-path.md new file mode 100644 index 0000000000..561e561923 --- /dev/null +++ b/.changes/fs-windows-path.md @@ -0,0 +1,5 @@ +--- +"fs": patch +--- + +Fix can't use Windows paths like `C:/Users/UserName/file.txt` diff --git a/plugins/fs/src/commands.rs b/plugins/fs/src/commands.rs index 72289434c0..184d952622 100644 --- a/plugins/fs/src/commands.rs +++ b/plugins/fs/src/commands.rs @@ -23,13 +23,44 @@ use std::{ use crate::{scope::Entry, Error, FilePath, FsExt}; -#[derive(Debug, serde::Deserialize)] -#[serde(untagged)] +// TODO: Combine this with FilePath +#[derive(Debug)] pub enum SafeFilePath { Url(url::Url), Path(SafePathBuf), } +impl<'de> serde::Deserialize<'de> for SafeFilePath { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct SafeFilePathVisitor; + + impl<'de> serde::de::Visitor<'de> for SafeFilePathVisitor { + type Value = SafeFilePath; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string representing an file URL or a path") + } + + fn visit_str(self, s: &str) -> std::result::Result + where + E: serde::de::Error, + { + SafeFilePath::from_str(s).map_err(|e| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Str(s), + &e.to_string().as_str(), + ) + }) + } + } + + deserializer.deserialize_str(SafeFilePathVisitor) + } +} + impl From for FilePath { fn from(value: SafeFilePath) -> Self { match value { @@ -43,10 +74,11 @@ impl FromStr for SafeFilePath { type Err = CommandError; fn from_str(s: &str) -> Result { if let Ok(url) = url::Url::from_str(s) { - Ok(Self::Url(url)) - } else { - Ok(Self::Path(SafePathBuf::new(s.into())?)) + if url.scheme().len() != 1 { + return Ok(Self::Url(url)); + } } + Ok(Self::Path(SafePathBuf::new(s.into())?)) } } @@ -1168,3 +1200,19 @@ fn get_stat(metadata: std::fs::Metadata) -> FileInfo { blocks: usm!(blocks), } } + +mod test { + #[test] + fn safe_file_path_parse() { + use super::SafeFilePath; + + assert!(matches!( + serde_json::from_str::("\"C:/Users\""), + Ok(SafeFilePath::Path(_)) + )); + assert!(matches!( + serde_json::from_str::("\"file:///C:/Users\""), + Ok(SafeFilePath::Url(_)) + )); + } +} diff --git a/plugins/fs/src/lib.rs b/plugins/fs/src/lib.rs index 6b46ea366e..975c126254 100644 --- a/plugins/fs/src/lib.rs +++ b/plugins/fs/src/lib.rs @@ -50,23 +50,55 @@ pub use scope::{Event as ScopeEvent, Scope}; type Result = std::result::Result; +// TODO: Combine this with SafeFilePath /// Represents either a filesystem path or a URI pointing to a file /// such as `file://` URIs or Android `content://` URIs. -#[derive(Debug, serde::Deserialize)] -#[serde(untagged)] +#[derive(Debug)] pub enum FilePath { Url(url::Url), Path(PathBuf), } +impl<'de> serde::Deserialize<'de> for FilePath { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct FilePathVisitor; + + impl<'de> serde::de::Visitor<'de> for FilePathVisitor { + type Value = FilePath; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string representing an file URL or a path") + } + + fn visit_str(self, s: &str) -> std::result::Result + where + E: serde::de::Error, + { + FilePath::from_str(s).map_err(|e| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Str(s), + &e.to_string().as_str(), + ) + }) + } + } + + deserializer.deserialize_str(FilePathVisitor) + } +} + impl FromStr for FilePath { type Err = Infallible; fn from_str(s: &str) -> std::result::Result { if let Ok(url) = url::Url::from_str(s) { - Ok(Self::Url(url)) - } else { - Ok(Self::Path(PathBuf::from(s))) + if url.scheme().len() != 1 { + return Ok(Self::Url(url)); + } } + Ok(Self::Path(PathBuf::from(s))) } }