zvariant/
file_path.rs

1use serde::{Deserialize, Serialize};
2use std::{
3    borrow::Cow,
4    ffi::{CStr, CString, OsStr, OsString},
5    path::{Path, PathBuf},
6};
7
8use crate::Type;
9
10/// File name represented as a nul-terminated byte array.
11///
12/// While `zvariant::Type` and `serde::{Serialize, Deserialize}`, are implemented for [`Path`] and
13/// [`PathBuf`], unfortunately `serde` serializes them as UTF-8 strings and that limits the number
14/// of possible characters to use on a file path. This is not the desired behavior since file paths
15/// are not guaranteed to contain only UTF-8 characters.
16///
17/// To solve this problem, this type is provided which encodes the underlying file path as a
18/// null-terminated byte array.
19///
20/// # Examples:
21///
22/// ```
23/// use zvariant::FilePath;
24/// use std::path::{Path, PathBuf};
25///
26/// let path = Path::new("/hello/world\0");
27/// let path_buf = PathBuf::from(path);
28///
29/// let p1 = FilePath::from(path);
30/// let p2 = FilePath::from(path_buf);
31/// let p3 = FilePath::from("/hello/world");
32///
33/// assert_eq!(p1, p2);
34/// assert_eq!(p2, p3);
35/// ```
36#[derive(Type, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Clone, Ord, PartialOrd)]
37#[zvariant(signature = "ay")]
38pub struct FilePath<'f>(Cow<'f, CStr>);
39
40impl<'f> FilePath<'f> {
41    pub fn new(cow: Cow<'f, CStr>) -> Self {
42        Self(cow)
43    }
44}
45
46impl From<CString> for FilePath<'_> {
47    fn from(value: CString) -> Self {
48        FilePath(Cow::Owned(value))
49    }
50}
51
52impl<'f> From<&'f CString> for FilePath<'f> {
53    fn from(value: &'f CString) -> Self {
54        FilePath(Cow::Borrowed(value.as_c_str()))
55    }
56}
57
58impl<'f> From<&'f OsStr> for FilePath<'f> {
59    fn from(value: &'f OsStr) -> FilePath<'f> {
60        FilePath(bytes_with_null(value.as_encoded_bytes()))
61    }
62}
63
64impl<'f> From<&'f OsString> for FilePath<'f> {
65    fn from(value: &'f OsString) -> FilePath<'f> {
66        FilePath(bytes_with_null(value.as_encoded_bytes()))
67    }
68}
69
70impl From<OsString> for FilePath<'_> {
71    fn from(value: OsString) -> Self {
72        FilePath(vec_to_cstr(value.as_encoded_bytes().to_vec()))
73    }
74}
75
76impl<'f> From<&'f PathBuf> for FilePath<'f> {
77    fn from(value: &'f PathBuf) -> FilePath<'f> {
78        FilePath::from(value.as_os_str())
79    }
80}
81
82impl From<PathBuf> for FilePath<'_> {
83    fn from(value: PathBuf) -> FilePath<'static> {
84        FilePath::from(OsString::from(value))
85    }
86}
87
88impl<'f> From<&'f Path> for FilePath<'f> {
89    fn from(value: &'f Path) -> Self {
90        Self::from(value.as_os_str())
91    }
92}
93
94impl<'f> From<&'f CStr> for FilePath<'f> {
95    fn from(value: &'f CStr) -> Self {
96        Self(Cow::Borrowed(value))
97    }
98}
99
100impl<'f> From<&'f str> for FilePath<'f> {
101    fn from(value: &'f str) -> Self {
102        Self::from(OsStr::new(value))
103    }
104}
105
106impl<'f> AsRef<FilePath<'f>> for FilePath<'f> {
107    fn as_ref(&self) -> &FilePath<'f> {
108        self
109    }
110}
111
112impl From<FilePath<'_>> for OsString {
113    fn from(value: FilePath<'_>) -> Self {
114        // SAFETY: user is responsible of handling conversion from [FilePath] to [OsString]
115        // since FilePath is a set of null terminated bytes and it's interpretations mainly
116        // depends on the underlying platform.
117        // see [std::ffi::os_str::OsString::from_encoded_bytes_unchecked]
118        unsafe { OsString::from_encoded_bytes_unchecked(value.0.to_bytes().to_vec()) }
119    }
120}
121
122impl<'f> From<&'f FilePath<'f>> for &'f Path {
123    fn from(value: &'f FilePath<'f>) -> Self {
124        // This method should fail if FilePath does not represent UTF-8 valid chars
125        // since [Path] is akin to [str], hence the unwrap.
126        Path::new(value.0.as_ref().to_str().unwrap())
127    }
128}
129
130impl<'f> From<FilePath<'f>> for PathBuf {
131    fn from(value: FilePath<'f>) -> Self {
132        PathBuf::from(value.0.to_string_lossy().to_string())
133    }
134}
135
136/// Converts a `Vec<u8>` into a null-terminated `CStr`.
137///
138/// Truncates the vector at the first null byte, if present. If no null byte exists, appends one to
139/// ensure proper termination.
140///
141/// # Returns
142///
143/// A `Cow<'_, CStr>` containing a *guaranteed* null-terminated string.
144pub fn vec_to_cstr(mut bytes: Vec<u8>) -> Cow<'static, CStr> {
145    if let Some(pos) = bytes.iter().position(|&b| b == 0) {
146        bytes.truncate(pos + 1);
147    } else {
148        bytes.push(0);
149    }
150    // unwrap is fine here since we append the null byte.
151    Cow::Owned(CString::from_vec_with_nul(bytes).unwrap())
152}
153
154/// Converts a byte slice into a null-terminated [CStr].
155///
156/// Returns a borrowed [CStr] if the slice already contains a null byte; otherwise, returns an
157/// owned [CStr] with a null byte appended.
158///
159/// # Returns
160///
161/// A [Cow<'_, CStr>] containing a *guaranteed* null-terminated string.
162fn bytes_with_null(bytes: &[u8]) -> Cow<'_, CStr> {
163    if let Ok(cstr) = CStr::from_bytes_until_nul(bytes) {
164        return Cow::Borrowed(cstr);
165    }
166    // unwrap is fine, as we handled the null termination case above.
167    Cow::Owned(CString::new(bytes).unwrap())
168}
169
170#[cfg(test)]
171mod file_path_test {
172    use super::*;
173    use crate::zvariant::Signature;
174    use std::path::{Path, PathBuf};
175
176    #[test]
177    fn from_test() {
178        let path = Path::new("/hello/world");
179        let path_buf = PathBuf::from(path);
180        let osstr = OsStr::new("/hello/world");
181        let os_string = OsString::from("/hello/world");
182        let cstr = CStr::from_bytes_until_nul("/hello/world\0".as_bytes()).unwrap_or_default();
183        let cstring = CString::new("/hello/world").unwrap_or_default();
184
185        let p1 = FilePath::from(path);
186        let p2 = FilePath::from(path_buf);
187        let p3 = FilePath::from(osstr);
188        let p4 = FilePath::from(os_string);
189        let p5 = FilePath::from(cstr);
190        let p6 = FilePath::from(cstring);
191        let p7 = FilePath::from("/hello/world");
192
193        assert_eq!(p1, p2);
194        assert_eq!(p2, p3);
195        assert_eq!(p3, p4);
196        assert_eq!(p4, p5);
197        assert_eq!(p5, p6);
198        assert_eq!(p5, p7);
199    }
200
201    #[test]
202    fn filepath_signature() {
203        assert_eq!(
204            &Signature::static_array(&Signature::U8),
205            FilePath::SIGNATURE
206        );
207    }
208
209    #[test]
210    fn into_test() {
211        let first = PathBuf::from("/hello/world");
212        let third = OsString::from("/hello/world");
213        let fifth = Path::new("/hello/world");
214        let p = FilePath::from(first.clone());
215        let p2 = FilePath::from(third.clone());
216        let p3 = FilePath::from(fifth);
217        let second: PathBuf = p.into();
218        let forth: OsString = p2.into();
219        let sixth: &Path = (&p3).into();
220        assert_eq!(first, second);
221        assert_eq!(third, forth);
222        assert_eq!(fifth, sixth);
223    }
224
225    #[test]
226    fn vec_nul_termination() {
227        let v1 = vec![];
228        let v2 = vec![0x0];
229        let v3 = vec![0x1, 0x2, 0x0];
230        let v4 = vec![0x0, 0x0];
231        let v5 = vec![0x1, 0x0, 0x2, 0x0];
232
233        assert_eq!(
234            Cow::Borrowed(CStr::from_bytes_with_nul(&[0x0]).unwrap()),
235            vec_to_cstr(v1)
236        );
237        assert_eq!(
238            Cow::Borrowed(CStr::from_bytes_with_nul(&[0x0]).unwrap()),
239            vec_to_cstr(v2)
240        );
241        assert_eq!(
242            Cow::Borrowed(CStr::from_bytes_with_nul(&[0x1, 0x2, 0x0]).unwrap()),
243            vec_to_cstr(v3)
244        );
245        assert_eq!(
246            Cow::Borrowed(CStr::from_bytes_with_nul(&[0x0]).unwrap()),
247            vec_to_cstr(v4)
248        );
249        assert_eq!(
250            Cow::Borrowed(CStr::from_bytes_with_nul(&[0x1, 0x0]).unwrap()),
251            vec_to_cstr(v5)
252        );
253    }
254}