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    /// Returns a lossy UTF-8 representation of the file path.
46    ///
47    /// Invalid UTF-8 sequences are replaced with `U+FFFD REPLACEMENT CHARACTER`.
48    pub fn to_string_lossy(&self) -> Cow<'_, str> {
49        self.0.to_string_lossy()
50    }
51}
52
53impl From<CString> for FilePath<'_> {
54    fn from(value: CString) -> Self {
55        FilePath(Cow::Owned(value))
56    }
57}
58
59impl<'f> From<&'f CString> for FilePath<'f> {
60    fn from(value: &'f CString) -> Self {
61        FilePath(Cow::Borrowed(value.as_c_str()))
62    }
63}
64
65impl<'f> From<&'f OsStr> for FilePath<'f> {
66    fn from(value: &'f OsStr) -> FilePath<'f> {
67        FilePath(bytes_with_null(value.as_encoded_bytes()))
68    }
69}
70
71impl<'f> From<&'f OsString> for FilePath<'f> {
72    fn from(value: &'f OsString) -> FilePath<'f> {
73        FilePath(bytes_with_null(value.as_encoded_bytes()))
74    }
75}
76
77impl From<OsString> for FilePath<'_> {
78    fn from(value: OsString) -> Self {
79        #[allow(deprecated)]
80        FilePath(vec_to_cstr(value.as_encoded_bytes().to_vec()))
81    }
82}
83
84impl<'f> From<&'f PathBuf> for FilePath<'f> {
85    fn from(value: &'f PathBuf) -> FilePath<'f> {
86        FilePath::from(value.as_os_str())
87    }
88}
89
90impl From<PathBuf> for FilePath<'_> {
91    fn from(value: PathBuf) -> FilePath<'static> {
92        FilePath::from(OsString::from(value))
93    }
94}
95
96impl<'f> From<&'f Path> for FilePath<'f> {
97    fn from(value: &'f Path) -> Self {
98        Self::from(value.as_os_str())
99    }
100}
101
102impl<'f> From<&'f CStr> for FilePath<'f> {
103    fn from(value: &'f CStr) -> Self {
104        Self(Cow::Borrowed(value))
105    }
106}
107
108impl<'f> From<&'f str> for FilePath<'f> {
109    fn from(value: &'f str) -> Self {
110        Self::from(OsStr::new(value))
111    }
112}
113
114impl<'f> AsRef<FilePath<'f>> for FilePath<'f> {
115    fn as_ref(&self) -> &FilePath<'f> {
116        self
117    }
118}
119
120impl From<FilePath<'_>> for OsString {
121    fn from(value: FilePath<'_>) -> Self {
122        // SAFETY: user is responsible of handling conversion from [FilePath] to [OsString]
123        // since FilePath is a set of null terminated bytes and it's interpretations mainly
124        // depends on the underlying platform.
125        // see [std::ffi::os_str::OsString::from_encoded_bytes_unchecked]
126        unsafe { OsString::from_encoded_bytes_unchecked(value.0.to_bytes().to_vec()) }
127    }
128}
129
130impl<'f> From<&'f FilePath<'f>> for &'f Path {
131    fn from(value: &'f FilePath<'f>) -> Self {
132        // This method should fail if FilePath does not represent UTF-8 valid chars
133        // since [Path] is akin to [str], hence the unwrap.
134        Path::new(value.0.as_ref().to_str().unwrap())
135    }
136}
137
138impl<'f> From<FilePath<'f>> for PathBuf {
139    fn from(value: FilePath<'f>) -> Self {
140        PathBuf::from(value.0.to_string_lossy().to_string())
141    }
142}
143
144/// Converts a `Vec<u8>` into a null-terminated `CStr`.
145///
146/// Truncates the vector at the first null byte, if present. If no null byte exists, appends one to
147/// ensure proper termination.
148///
149/// # Returns
150///
151/// A `Cow<'_, CStr>` containing a *guaranteed* null-terminated string.
152#[doc(hidden)]
153#[deprecated(
154    since = "5.9.0",
155    note = "This function was never meant to be public and will be removed in a future release."
156)]
157pub fn vec_to_cstr(mut bytes: Vec<u8>) -> Cow<'static, CStr> {
158    if let Some(pos) = bytes.iter().position(|&b| b == 0) {
159        bytes.truncate(pos + 1);
160    } else {
161        bytes.push(0);
162    }
163    // unwrap is fine here since we append the null byte.
164    Cow::Owned(CString::from_vec_with_nul(bytes).unwrap())
165}
166
167/// Converts a byte slice into a null-terminated [CStr].
168///
169/// Returns a borrowed [CStr] if the slice already contains a null byte; otherwise, returns an
170/// owned [CStr] with a null byte appended.
171///
172/// # Returns
173///
174/// A [Cow<'_, CStr>] containing a *guaranteed* null-terminated string.
175fn bytes_with_null(bytes: &[u8]) -> Cow<'_, CStr> {
176    if let Ok(cstr) = CStr::from_bytes_until_nul(bytes) {
177        return Cow::Borrowed(cstr);
178    }
179    // unwrap is fine, as we handled the null termination case above.
180    Cow::Owned(CString::new(bytes).unwrap())
181}
182
183#[cfg(test)]
184mod file_path_test {
185    use super::*;
186    use crate::zvariant::Signature;
187    use std::path::{Path, PathBuf};
188
189    #[test]
190    fn from_test() {
191        let path = Path::new("/hello/world");
192        let path_buf = PathBuf::from(path);
193        let osstr = OsStr::new("/hello/world");
194        let os_string = OsString::from("/hello/world");
195        let cstr = CStr::from_bytes_until_nul("/hello/world\0".as_bytes()).unwrap_or_default();
196        let cstring = CString::new("/hello/world").unwrap_or_default();
197
198        let p1 = FilePath::from(path);
199        let p2 = FilePath::from(path_buf);
200        let p3 = FilePath::from(osstr);
201        let p4 = FilePath::from(os_string);
202        let p5 = FilePath::from(cstr);
203        let p6 = FilePath::from(cstring);
204        let p7 = FilePath::from("/hello/world");
205
206        assert_eq!(p1, p2);
207        assert_eq!(p2, p3);
208        assert_eq!(p3, p4);
209        assert_eq!(p4, p5);
210        assert_eq!(p5, p6);
211        assert_eq!(p5, p7);
212    }
213
214    #[test]
215    fn filepath_signature() {
216        assert_eq!(
217            &Signature::static_array(&Signature::U8),
218            FilePath::SIGNATURE
219        );
220    }
221
222    #[test]
223    fn into_test() {
224        let first = PathBuf::from("/hello/world");
225        let third = OsString::from("/hello/world");
226        let fifth = Path::new("/hello/world");
227        let p = FilePath::from(first.clone());
228        let p2 = FilePath::from(third.clone());
229        let p3 = FilePath::from(fifth);
230        let second: PathBuf = p.into();
231        let forth: OsString = p2.into();
232        let sixth: &Path = (&p3).into();
233        assert_eq!(first, second);
234        assert_eq!(third, forth);
235        assert_eq!(fifth, sixth);
236    }
237
238    #[test]
239    fn vec_nul_termination() {
240        #[allow(deprecated)]
241        fn call_vec_to_cstr(v: Vec<u8>) -> Cow<'static, CStr> {
242            vec_to_cstr(v)
243        }
244        let v1 = vec![];
245        let v2 = vec![0x0];
246        let v3 = vec![0x1, 0x2, 0x0];
247        let v4 = vec![0x0, 0x0];
248        let v5 = vec![0x1, 0x0, 0x2, 0x0];
249
250        assert_eq!(
251            Cow::Borrowed(CStr::from_bytes_with_nul(&[0x0]).unwrap()),
252            call_vec_to_cstr(v1)
253        );
254        assert_eq!(
255            Cow::Borrowed(CStr::from_bytes_with_nul(&[0x0]).unwrap()),
256            call_vec_to_cstr(v2)
257        );
258        assert_eq!(
259            Cow::Borrowed(CStr::from_bytes_with_nul(&[0x1, 0x2, 0x0]).unwrap()),
260            call_vec_to_cstr(v3)
261        );
262        assert_eq!(
263            Cow::Borrowed(CStr::from_bytes_with_nul(&[0x0]).unwrap()),
264            call_vec_to_cstr(v4)
265        );
266        assert_eq!(
267            Cow::Borrowed(CStr::from_bytes_with_nul(&[0x1, 0x0]).unwrap()),
268            call_vec_to_cstr(v5)
269        );
270    }
271}