objc2_foundation/url.rs
1#![cfg(feature = "std")]
2#![cfg(unix)] // TODO: Use as_encoded_bytes/from_encoded_bytes_unchecked once in MSRV.
3#![cfg(not(feature = "gnustep-1-7"))] // Doesn't seem to be available on GNUStep?
4use core::ptr::NonNull;
5use std::ffi::{CStr, CString, OsStr};
6use std::os::unix::ffi::OsStrExt;
7use std::path::{Path, PathBuf};
8
9use objc2::rc::Retained;
10use objc2::AnyThread;
11
12use crate::NSURL;
13
14const PATH_MAX: usize = 1024;
15
16/// [`Path`] conversion.
17impl NSURL {
18 // FIXME(breaking): Make this private.
19 pub fn from_path(
20 path: &Path,
21 is_directory: bool,
22 // TODO: Expose this?
23 base_url: Option<&NSURL>,
24 ) -> Option<Retained<Self>> {
25 // See comments in `CFURL::from_path`.
26 let bytes = path.as_os_str().as_bytes();
27
28 if bytes.is_empty() {
29 // `initFileURLWithFileSystemRepresentation:isDirectory:relativeToURL:`,
30 // checks this, but that's marked as non-null, so we'd get a panic
31 // if we didn't implement the check manually ourselves.
32 return None;
33 }
34
35 // TODO: Should we strip trailing \0 to fully match CoreFoundation?
36 let cstr = CString::new(bytes).ok()?;
37 let ptr = NonNull::new(cstr.as_ptr().cast_mut()).unwrap();
38
39 // SAFETY: The pointer is a C string, and valid for the duration of
40 // the call.
41 Some(unsafe {
42 Self::initFileURLWithFileSystemRepresentation_isDirectory_relativeToURL(
43 Self::alloc(),
44 ptr,
45 is_directory,
46 base_url,
47 )
48 })
49 }
50
51 /// Create a file url from a [`Path`].
52 ///
53 /// If the path is relative, it will be considered relative to the current
54 /// directory.
55 ///
56 /// Returns `None` when given an invalid path (such as a path containing
57 /// interior NUL bytes). The exact checks are not guaranteed.
58 ///
59 ///
60 /// # Non-unicode and HFS+ support
61 ///
62 /// Modern Apple disk drives use APFS nowadays, which forces all paths to
63 /// be valid unicode. The URL standard also uses unicode, and non-unicode
64 /// parts of the URL will be percent-encoded, and the url will be given
65 /// the scheme `file://`. All of this is as it should be.
66 ///
67 /// Unfortunately, a lot of Foundation APIs (including the `NSFileManager`
68 /// and `NSData` APIs) currently assume that they can always get unicode
69 /// paths _back_ by calling [`NSURL::path`] internally, which is not true.
70 ///
71 /// If you need to support non-unicode paths in HFS+ with these APIs, you
72 /// can work around this issue by percent-encoding any non-unicode parts
73 /// of the path yourself beforehand, similar to [what's done in the
74 /// `trash-rs` crate](https://github.com/Byron/trash-rs/pull/127).
75 /// (this function cannot do that for you, since it relies on a quirk of
76 /// HFS+ that b"\xf8" and b"%F8" refer to the same file).
77 ///
78 ///
79 /// # Examples
80 ///
81 /// ```
82 /// use std::path::Path;
83 /// use objc2_foundation::NSURL;
84 ///
85 /// // Absolute paths work as you'd expect.
86 /// let url = NSURL::from_file_path("/tmp/file.txt").unwrap();
87 /// assert_eq!(url.to_file_path().unwrap(), Path::new("/tmp/file.txt"));
88 ///
89 /// // Relative paths are relative to the current directory.
90 /// let url = NSURL::from_file_path("foo.txt").unwrap();
91 /// assert_eq!(url.to_file_path().unwrap(), std::env::current_dir().unwrap().join("foo.txt"));
92 ///
93 /// // Some invalid paths return `None`.
94 /// assert!(NSURL::from_file_path("").is_none());
95 /// // Another example of an invalid path containing interior NUL bytes.
96 /// assert!(NSURL::from_file_path("/a/\0a").is_none());
97 /// ```
98 #[inline]
99 #[doc(alias = "fileURLWithFileSystemRepresentation:isDirectory:relativeToURL:")]
100 #[doc(alias = "initFileURLWithFileSystemRepresentation:isDirectory:relativeToURL:")]
101 pub fn from_file_path<P: AsRef<Path>>(path: P) -> Option<Retained<Self>> {
102 Self::from_path(path.as_ref(), false, None)
103 }
104
105 /// Create a directory url from a [`Path`].
106 ///
107 /// This differs from [`from_file_path`][Self::from_file_path] in that the
108 /// path is treated as a directory, which means that other normalization
109 /// rules are applied to it (to make it end with a `/`).
110 ///
111 ///
112 /// # Examples
113 ///
114 /// ```
115 /// use std::path::Path;
116 /// use objc2_foundation::NSURL;
117 ///
118 /// // Directory paths get trailing slashes appended
119 /// let url = NSURL::from_directory_path("/Library").unwrap();
120 /// assert_eq!(url.to_file_path().unwrap(), Path::new("/Library/"));
121 ///
122 /// // Unless they already have them.
123 /// let url = NSURL::from_directory_path("/Library/").unwrap();
124 /// assert_eq!(url.to_file_path().unwrap(), Path::new("/Library/"));
125 ///
126 /// // Similarly for relative paths.
127 /// let url = NSURL::from_directory_path("foo").unwrap();
128 /// assert_eq!(url.to_file_path().unwrap(), std::env::current_dir().unwrap().join("foo/"));
129 ///
130 /// // Various dots may be stripped.
131 /// let url = NSURL::from_directory_path("/Library/././.").unwrap();
132 /// assert_eq!(url.to_file_path().unwrap(), Path::new("/Library/"));
133 ///
134 /// // Though of course not if they have semantic meaning.
135 /// let url = NSURL::from_directory_path("/Library/..").unwrap();
136 /// assert_eq!(url.to_file_path().unwrap(), Path::new("/Library/.."));
137 /// ```
138 #[inline]
139 #[doc(alias = "fileURLWithFileSystemRepresentation:isDirectory:relativeToURL:")]
140 #[doc(alias = "initFileURLWithFileSystemRepresentation:isDirectory:relativeToURL:")]
141 pub fn from_directory_path<P: AsRef<Path>>(path: P) -> Option<Retained<Self>> {
142 Self::from_path(path.as_ref(), true, None)
143 }
144
145 /// Extract the path part of the URL as a `PathBuf`.
146 ///
147 /// This will return a path regardless of [`isFileURL`][Self::isFileURL].
148 /// It is the responsibility of the caller to ensure that the URL is valid
149 /// to use as a file URL.
150 ///
151 ///
152 /// # Compatibility note
153 ///
154 /// This currently does not work for non-unicode paths (which are fairly
155 /// rare on macOS since HFS+ was been superseded by APFS).
156 ///
157 /// This also currently always returns absolute paths (it converts
158 /// relative URL paths to absolute), but that may change in the future.
159 ///
160 ///
161 /// # Examples
162 ///
163 /// ```
164 /// use std::path::Path;
165 /// use objc2_foundation::{NSURL, NSString};
166 ///
167 /// let url = unsafe { NSURL::URLWithString(&NSString::from_str("file:///tmp/foo.txt")).unwrap() };
168 /// assert_eq!(url.to_file_path().unwrap(), Path::new("/tmp/foo.txt"));
169 /// ```
170 ///
171 /// See also the examples in [`from_file_path`][Self::from_file_path].
172 #[doc(alias = "getFileSystemRepresentation:maxLength:")]
173 #[doc(alias = "fileSystemRepresentation")]
174 pub fn to_file_path(&self) -> Option<PathBuf> {
175 let mut buf = [0u8; PATH_MAX];
176 let ptr = NonNull::new(buf.as_mut_ptr()).unwrap().cast();
177 // SAFETY: The provided buffer is valid.
178 // We prefer getFileSystemRepresentation:maxLength: over
179 // `fileSystemRepresentation`, since the former is guaranteed to
180 // handle internal NUL bytes (even if there probably won't be any,
181 // NSURL seems to avoid that by construction).
182 let result = unsafe { self.getFileSystemRepresentation_maxLength(ptr, buf.len()) };
183 if !result {
184 return None;
185 }
186
187 // SAFETY: Foundation is guaranteed to null-terminate the buffer if
188 // the function succeeded.
189 let cstr = unsafe { CStr::from_bytes_until_nul(&buf).unwrap_unchecked() };
190
191 let path = OsStr::from_bytes(cstr.to_bytes());
192 Some(PathBuf::from(path))
193 }
194}
195
196// See also CFURL's tests, they're a bit more exhaustive.
197#[cfg(test)]
198#[cfg(unix)]
199mod tests {
200 use std::os::unix::ffi::OsStrExt;
201
202 use super::*;
203
204 #[test]
205 fn invalid_path() {
206 assert_eq!(NSURL::from_file_path(""), None);
207 assert_eq!(NSURL::from_file_path("/\0/a"), None);
208 }
209
210 #[test]
211 fn roundtrip() {
212 let path = Path::new(OsStr::from_bytes(b"/abc/def"));
213 let url = NSURL::from_file_path(path).unwrap();
214 assert_eq!(url.to_file_path().unwrap(), path);
215
216 let path = Path::new(OsStr::from_bytes(b"/\x08"));
217 let url = NSURL::from_file_path(path).unwrap();
218 assert_eq!(url.to_file_path().unwrap(), path);
219
220 // Non-unicode
221 let path = Path::new(OsStr::from_bytes(b"/\x08"));
222 let url = NSURL::from_file_path(path).unwrap();
223 assert_eq!(url.to_file_path().unwrap(), path);
224 }
225
226 #[test]
227 #[cfg(all(feature = "NSData", feature = "NSFileManager", feature = "NSError"))]
228 #[ignore = "needs HFS+ file system"]
229 fn special_paths() {
230 use crate::{NSData, NSFileManager};
231
232 let manager = NSFileManager::defaultManager();
233
234 let path = Path::new(OsStr::from_bytes(b"\xf8"));
235 // Foundation is broken, needs a different encoding to work.
236 let url = NSURL::from_file_path("%F8").unwrap();
237
238 // Create, read and remove file, using different APIs.
239 std::fs::write(path, "").unwrap();
240 assert_eq!(NSData::dataWithContentsOfURL(&url), Some(NSData::new()));
241 manager.removeItemAtURL_error(&url).unwrap();
242 }
243
244 // Useful when testing HFS+ and non-UTF-8:
245 // echo > $(echo "0000000: f8" | xxd -r)
246}