Skip to main content

strop_core/
path_serde.rs

1//! Versioned native path representation. No lossy round-trip can select a file.
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4
5#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
6pub enum PathError {
7    #[error("legacy path contains an ambiguous replacement character")]
8    AmbiguousLegacy,
9    #[error("unsupported path encoding version or platform")]
10    Encoding,
11    #[error("path is empty or contains a native NUL")]
12    Invalid,
13}
14
15#[derive(Serialize, Deserialize)]
16#[serde(tag = "encoding", deny_unknown_fields)]
17enum Native {
18    #[serde(rename = "unix-bytes")]
19    Unix { version: u32, bytes: Vec<u8> },
20    #[serde(rename = "windows-utf16")]
21    Windows { version: u32, units: Vec<u16> },
22}
23#[derive(Deserialize)]
24#[serde(untagged)]
25enum Wire {
26    Legacy(String),
27    Native(Native),
28}
29
30pub fn serialize<S: serde::Serializer>(path: &Path, serializer: S) -> Result<S::Ok, S::Error> {
31    #[cfg(unix)]
32    {
33        use std::os::unix::ffi::OsStrExt;
34        Native::Unix {
35            version: 1,
36            bytes: path.as_os_str().as_bytes().to_vec(),
37        }
38        .serialize(serializer)
39    }
40    #[cfg(windows)]
41    {
42        use std::os::windows::ffi::OsStrExt;
43        Native::Windows {
44            version: 1,
45            units: path.as_os_str().encode_wide().collect(),
46        }
47        .serialize(serializer)
48    }
49    #[cfg(not(any(unix, windows)))]
50    {
51        let _ = path;
52        Err(serde::ser::Error::custom(PathError::Encoding))
53    }
54}
55
56pub fn deserialize<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<PathBuf, D::Error> {
57    decode(Wire::deserialize(deserializer)?).map_err(serde::de::Error::custom)
58}
59
60fn decode(wire: Wire) -> Result<PathBuf, PathError> {
61    let path = match wire {
62        Wire::Legacy(value) => {
63            if value.contains('\u{fffd}') {
64                return Err(PathError::AmbiguousLegacy);
65            }
66            PathBuf::from(value)
67        }
68        Wire::Native(native) => match native {
69            #[cfg(unix)]
70            Native::Unix { version: 1, bytes } => {
71                use std::os::unix::ffi::OsStringExt;
72                PathBuf::from(std::ffi::OsString::from_vec(bytes))
73            }
74            #[cfg(windows)]
75            Native::Windows { version: 1, units } => {
76                use std::os::windows::ffi::OsStringExt;
77                PathBuf::from(std::ffi::OsString::from_wide(&units))
78            }
79            _ => return Err(PathError::Encoding),
80        },
81    };
82    validate(&path)?;
83    Ok(path)
84}
85
86pub fn validate(path: &Path) -> Result<(), PathError> {
87    #[cfg(unix)]
88    let nul = {
89        use std::os::unix::ffi::OsStrExt;
90        path.as_os_str().as_bytes().contains(&0)
91    };
92    #[cfg(windows)]
93    let nul = {
94        use std::os::windows::ffi::OsStrExt;
95        path.as_os_str().encode_wide().any(|unit| unit == 0)
96    };
97    #[cfg(not(any(unix, windows)))]
98    let nul = true;
99    if path.as_os_str().is_empty() || nul {
100        return Err(PathError::Invalid);
101    }
102    Ok(())
103}
104
105pub mod option {
106    use super::*;
107    struct PathRef<'a>(&'a Path);
108    impl Serialize for PathRef<'_> {
109        fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
110            super::serialize(self.0, serializer)
111        }
112    }
113    pub fn serialize<S: serde::Serializer>(
114        path: &Option<PathBuf>,
115        serializer: S,
116    ) -> Result<S::Ok, S::Error> {
117        path.as_deref().map(PathRef).serialize(serializer)
118    }
119    pub fn deserialize<'de, D: serde::Deserializer<'de>>(
120        deserializer: D,
121    ) -> Result<Option<PathBuf>, D::Error> {
122        Option::<Wire>::deserialize(deserializer)?
123            .map(decode)
124            .transpose()
125            .map_err(serde::de::Error::custom)
126    }
127}