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