[go: up one dir, main page]

objc2-core-foundation 0.3.1

Bindings to the CoreFoundation framework
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
#[allow(unused_imports)]
use crate::{CFIndex, CFRetained, CFURL};

/// [`Path`][std::path::Path] conversion.
#[cfg(feature = "std")]
#[cfg(unix)] // TODO: Use as_encoded_bytes/from_encoded_bytes_unchecked once in MSRV.
impl CFURL {
    #[inline]
    fn from_path(
        path: &std::path::Path,
        is_directory: bool,
        base: Option<&CFURL>,
    ) -> Option<CFRetained<CFURL>> {
        use std::os::unix::ffi::OsStrExt;

        // CFURL expects to get a string with the system encoding, and will
        // internally handle the different encodings, depending on if compiled
        // for Apple platforms or Windows (which is very rare, but could
        // technically happen).
        let bytes = path.as_os_str().as_bytes();

        // Never gonna happen, allocations can't be this large in Rust.
        debug_assert!(bytes.len() < CFIndex::MAX as usize);
        let len = bytes.len() as CFIndex;

        if let Some(base) = base {
            // The base URL must be a directory URL (have a trailing "/").
            // If the path is absolute, this URL is ignored.
            //
            // TODO: Expose this publicly?
            unsafe {
                Self::from_file_system_representation_relative_to_base(
                    None,
                    bytes.as_ptr(),
                    len,
                    is_directory,
                    Some(base),
                )
            }
        } else {
            unsafe {
                Self::from_file_system_representation(None, bytes.as_ptr(), len, is_directory)
            }
        }
    }

    /// Create a file url from a [`Path`][std::path::Path].
    ///
    /// This is useful because a lot of CoreFoundation APIs use `CFURL` to
    /// represent file-system paths as well.
    ///
    /// Non-unicode parts of the URL will be percent-encoded, and the url will
    /// have the scheme `file://`.
    ///
    /// If the path is relative, it will be considered relative to the current
    /// directory.
    ///
    /// Returns `None` when given an invalid path (such as a path containing
    /// interior NUL bytes). The exact checks are not guaranteed.
    ///
    ///
    /// # Examples
    ///
    /// ```
    /// use std::path::Path;
    /// use objc2_core_foundation::CFURL;
    ///
    /// // Absolute paths work as you'd expect.
    /// let url = CFURL::from_file_path("/tmp/file.txt").unwrap();
    /// assert_eq!(url.to_file_path().unwrap(), Path::new("/tmp/file.txt"));
    ///
    /// // Relative paths are relative to the current directory.
    /// let url = CFURL::from_file_path("foo.txt").unwrap();
    /// assert_eq!(url.to_file_path().unwrap(), std::env::current_dir().unwrap().join("foo.txt"));
    ///
    /// // Some invalid paths return `None`.
    /// assert!(CFURL::from_file_path("").is_none());
    /// // Another example of an invalid path containing interior NUL bytes.
    /// assert!(CFURL::from_file_path("/a/\0a").is_none());
    ///
    /// // Trailing NUL bytes are stripped.
    /// // NOTE: This only seems to work on some versions of CoreFoundation.
    /// let url = CFURL::from_file_path("/a\0\0").unwrap();
    /// assert_eq!(url.to_file_path().unwrap(), Path::new("/a"));
    /// ```
    #[inline]
    #[doc(alias = "CFURLCreateFromFileSystemRepresentation")]
    pub fn from_file_path<P: AsRef<std::path::Path>>(path: P) -> Option<CFRetained<CFURL>> {
        Self::from_path(path.as_ref(), false, None)
    }

    /// Create a directory url from a [`Path`][std::path::Path].
    ///
    /// This differs from [`from_file_path`][Self::from_file_path] in that the
    /// path is treated as a directory, which means that other normalization
    /// rules are applied to it (to make it end with a `/`).
    ///
    ///
    /// # Examples
    ///
    /// ```
    /// use std::path::Path;
    /// use objc2_core_foundation::CFURL;
    ///
    /// // Directory paths get trailing slashes appended
    /// let url = CFURL::from_directory_path("/Library").unwrap();
    /// assert_eq!(url.to_file_path().unwrap(), Path::new("/Library/"));
    ///
    /// // Unless they already have them.
    /// let url = CFURL::from_directory_path("/Library/").unwrap();
    /// assert_eq!(url.to_file_path().unwrap(), Path::new("/Library/"));
    ///
    /// // Similarly for relative paths.
    /// let url = CFURL::from_directory_path("foo").unwrap();
    /// assert_eq!(url.to_file_path().unwrap(), std::env::current_dir().unwrap().join("foo/"));
    ///
    /// // Various dots may be stripped.
    /// let url = CFURL::from_directory_path("/Library/././.").unwrap();
    /// assert_eq!(url.to_file_path().unwrap(), Path::new("/Library/"));
    ///
    /// // Though of course not if they have semantic meaning.
    /// let url = CFURL::from_directory_path("/Library/..").unwrap();
    /// assert_eq!(url.to_file_path().unwrap(), Path::new("/Library/.."));
    /// ```
    #[inline]
    #[doc(alias = "CFURLCreateFromFileSystemRepresentation")]
    pub fn from_directory_path<P: AsRef<std::path::Path>>(path: P) -> Option<CFRetained<CFURL>> {
        Self::from_path(path.as_ref(), true, None)
    }

    /// Extract the path part of the URL as a [`PathBuf`][std::path::PathBuf].
    ///
    /// This will return a path regardless of whether the scheme is `file://`.
    /// It is the responsibility of the caller to ensure that the URL is valid
    /// to use as a file URL.
    ///
    ///
    /// # Compatibility note
    ///
    /// This currently does not work for non-unicode paths (which are fairly
    /// rare on macOS since HFS+ was been superseded by APFS).
    ///
    /// This also currently always returns absolute paths (it converts
    /// relative URL paths to absolute), but that may change in the future.
    ///
    ///
    /// # Examples
    ///
    /// ```
    /// use std::path::Path;
    /// use objc2_core_foundation::{CFURL, CFString};
    ///
    /// let url = CFURL::from_string(None, &CFString::from_str("file:///tmp/foo.txt"), None).unwrap();
    /// assert_eq!(url.to_file_path().unwrap(), Path::new("/tmp/foo.txt"));
    /// ```
    ///
    /// See also the examples in [`from_file_path`][Self::from_file_path].
    #[doc(alias = "CFURLGetFileSystemRepresentation")]
    pub fn to_file_path(&self) -> Option<std::path::PathBuf> {
        use std::os::unix::ffi::OsStrExt;

        const PATH_MAX: usize = 1024;

        // TODO: if a path is relative with no base, how do we get that
        // relative path out again (without adding current dir?).
        //
        // TODO: Should we do something to handle paths larger than PATH_MAX?
        // What can we even do? (since it's impossible for us to tell why the
        // conversion failed, so we can't know if we need to allocate, or if
        // the URL just cannot be converted).
        let mut buf = [0u8; PATH_MAX];
        let result = unsafe {
            self.file_system_representation(true, buf.as_mut_ptr(), buf.len() as CFIndex)
        };
        if !result {
            return None;
        }

        // SAFETY: CF is guaranteed to null-terminate the buffer if
        // the function succeeded.
        let cstr = unsafe { core::ffi::CStr::from_bytes_until_nul(&buf).unwrap_unchecked() };

        let path = std::ffi::OsStr::from_bytes(cstr.to_bytes());
        Some(path.into())
    }
}

/// String conversion.
impl CFURL {
    /// Create an URL from a `CFString`.
    ///
    /// Returns `None` if the URL is considered invalid by CoreFoundation. The
    /// exact details of which strings are invalid URLs are considered an
    /// implementation detail.
    ///
    /// Note in particular that not all strings that the URL spec considers
    /// invalid are considered invalid by CoreFoundation too. If you need
    /// spec-compliant parsing, consider the [`url`] crate instead.
    ///
    /// [`url`]: https://docs.rs/url/
    ///
    /// # Examples
    ///
    /// Construct and inspect a `CFURL`.
    ///
    /// ```
    /// use objc2_core_foundation::{
    ///     CFString, CFURL, CFURLCopyHostName, CFURLCopyScheme, CFURLCopyPath,
    /// };
    ///
    /// let url = CFURL::from_string(None, &CFString::from_str("http://example.com/foo"), None).unwrap();
    /// assert_eq!(url.string().to_string(), "http://example.com/foo");
    /// assert_eq!(CFURLCopyScheme(&url).unwrap().to_string(), "http");
    /// assert_eq!(CFURLCopyHostName(&url).unwrap().to_string(), "example.com");
    /// assert_eq!(CFURLCopyPath(&url).unwrap().to_string(), "/foo");
    /// ```
    ///
    /// Fail parsing certain strings.
    ///
    /// ```
    /// use objc2_core_foundation::{CFString, CFURL};
    ///
    /// // Percent-encoding needs two characters.
    /// assert_eq!(CFURL::from_string(None, &CFString::from_str("http://example.com/%A"), None), None);
    ///
    /// // Two hash-characters is disallowed.
    /// assert_eq!(CFURL::from_string(None, &CFString::from_str("http://example.com/abc#a#b"), None), None);
    /// ```
    #[inline]
    #[doc(alias = "CFURLCreateWithString")]
    pub fn from_string(
        allocator: Option<&crate::CFAllocator>,
        url_string: &crate::CFString,
        base_url: Option<&CFURL>,
    ) -> Option<CFRetained<Self>> {
        Self::__from_string(allocator, Some(url_string), base_url)
    }

    /// Create an URL from a string without checking it for validity.
    ///
    /// Returns `None` on some OS versions when the string contains interior
    /// NUL bytes.
    ///
    /// # Safety
    ///
    /// The URL must be valid.
    ///
    /// Note that it is unclear whether this is actually a safety requirement,
    /// or simply a correctness requirement. So we conservatively mark this
    /// function as `unsafe`.
    #[inline]
    #[cfg(feature = "CFString")]
    #[doc(alias = "CFURLCreateWithBytes")]
    pub unsafe fn from_str_unchecked(s: &str) -> Option<CFRetained<Self>> {
        let ptr = s.as_ptr();

        // Never gonna happen, allocations can't be this large in Rust.
        debug_assert!(s.len() < CFIndex::MAX as usize);
        let len = s.len() as CFIndex;

        let encoding = crate::CFStringBuiltInEncodings::EncodingUTF8;
        // SAFETY: The pointer and length are valid, and the encoding is a
        // superset of ASCII.
        //
        // Unlike `CFURLCreateWithString`, this does _not_ verify the URL at
        // all, and thus we propagate the validity checks to the user. See
        // also the source code for the checks:
        // https://github.com/apple-oss-distributions/CF/blob/CF-1153.18/CFURL.c#L1882-L1970
        unsafe { Self::with_bytes(None, ptr, len, encoding.0, None) }
    }

    /// Get the string-representation of the URL.
    ///
    /// The string may be overly sanitized (percent-encoded), do not rely on
    /// this returning exactly the same string as was passed in
    /// [`from_string`][Self::from_string].
    #[doc(alias = "CFURLGetString")]
    pub fn string(&self) -> CFRetained<crate::CFString> {
        // URLs contain valid UTF-8, so this should only fail on allocation
        // error.
        self.__string().expect("failed getting string from CFURL")
    }
}

#[cfg(unix)]
#[cfg(test)]
#[cfg(feature = "CFString")]
#[cfg(feature = "std")]
mod tests {
    use std::ffi::OsStr;
    use std::os::unix::ffi::OsStrExt;
    use std::path::Path;
    use std::{env::current_dir, string::ToString};

    use crate::{CFString, CFURLPathStyle};

    use super::*;

    #[test]
    fn from_string() {
        let url =
            CFURL::from_string(None, &CFString::from_str("https://example.com/xyz"), None).unwrap();
        assert_eq!(url.to_file_path().unwrap(), Path::new("/xyz"));
        assert_eq!(url.string().to_string(), "https://example.com/xyz");

        // Invalid.
        let url = CFURL::from_string(None, &CFString::from_str("\0"), None);
        assert_eq!(url, None);

        // Also invalid.
        let url = CFURL::from_string(None, &CFString::from_str("http://example.com/%a"), None);
        assert_eq!(url, None);

        // Though using `from_str_unchecked` succeeds.
        let url = unsafe { CFURL::from_str_unchecked("http://example.com/%a") }.unwrap();
        assert_eq!(url.string().to_string(), "http://example.com/%25a");
        assert_eq!(url.to_file_path().unwrap(), Path::new("/%a"));

        let url = unsafe { CFURL::from_str_unchecked("/\0a\0") }.unwrap();
        assert_eq!(url.string().to_string(), "/%00a%00");
        assert_eq!(url.to_file_path(), None);
    }

    #[test]
    fn to_string_may_extra_percent_encode() {
        let url = CFURL::from_string(None, &CFString::from_str("["), None).unwrap();
        assert_eq!(url.string().to_string(), "%5B");
    }

    #[test]
    #[cfg(feature = "objc2")]
    fn invalid_with_nul_bytes() {
        // This is a bug in newer CF versions:
        // https://github.com/swiftlang/swift-corelibs-foundation/issues/5200
        let url = unsafe { CFURL::from_str_unchecked("a\0aaaaaa") };
        if objc2::available!(macos = 12.0, ios = 15.0, watchos = 8.0, tvos = 15.0, ..) {
            assert_eq!(url, None);
        } else {
            assert_eq!(url.unwrap().string().to_string(), "a%00aaaaaa");
        }
    }

    #[test]
    fn to_from_path() {
        let url = CFURL::from_file_path("/").unwrap();
        assert_eq!(url.to_file_path().unwrap(), Path::new("/"));

        let url = CFURL::from_file_path("/abc/def").unwrap();
        assert_eq!(url.to_file_path().unwrap(), Path::new("/abc/def"));

        let url = CFURL::from_directory_path("/abc/def").unwrap();
        assert_eq!(url.to_file_path().unwrap(), Path::new("/abc/def/"));

        let url = CFURL::from_file_path("relative.txt").unwrap();
        assert_eq!(
            url.to_file_path(),
            Some(current_dir().unwrap().join("relative.txt"))
        );
        assert_eq!(
            url.absolute_url().unwrap().to_file_path(),
            Some(current_dir().unwrap().join("relative.txt"))
        );

        let str = "/with space and wéird UTF-8 chars: 😀";
        let url = CFURL::from_file_path(str).unwrap();
        assert_eq!(url.to_file_path().unwrap(), Path::new(str));
    }

    #[test]
    fn invalid_path() {
        assert_eq!(CFURL::from_file_path(""), None);
        assert_eq!(CFURL::from_file_path("/\0/a"), None);
    }

    #[test]
    fn from_dir_strips_dot() {
        let url = CFURL::from_directory_path("/Library/.").unwrap();
        assert_eq!(url.to_file_path().unwrap(), Path::new("/Library/"));
    }

    /// Ensure that trailing NULs are completely stripped.
    #[test]
    #[cfg_attr(
        not(target_os = "macos"),
        ignore = "seems to work differently in the simulator"
    )]
    fn path_with_trailing_nul() {
        let url = CFURL::from_file_path("/abc/def\0\0\0").unwrap();
        assert_eq!(url.to_file_path().unwrap(), Path::new("/abc/def"));

        let path = url
            .file_system_path(CFURLPathStyle::CFURLPOSIXPathStyle)
            .unwrap();
        assert_eq!(path.to_string(), "/abc/def");
        #[allow(deprecated)]
        let path = url
            .file_system_path(CFURLPathStyle::CFURLHFSPathStyle)
            .unwrap();
        assert!(path.to_string().ends_with(":abc:def")); // $DISK_NAME:abc:def
        let path = url
            .file_system_path(CFURLPathStyle::CFURLWindowsPathStyle)
            .unwrap();
        assert_eq!(path.to_string(), "\\abc\\def");
    }

    #[test]
    fn path_with_base() {
        let base = CFURL::from_directory_path("/abc/").unwrap();
        let url = CFURL::from_path(Path::new("def"), false, Some(&base)).unwrap();
        assert_eq!(url.to_file_path().unwrap(), Path::new("/abc/def"));
        let url = CFURL::from_path(Path::new("def/"), false, Some(&base)).unwrap();
        assert_eq!(url.to_file_path().unwrap(), Path::new("/abc/def/"));
        let url = CFURL::from_path(Path::new("/def"), false, Some(&base)).unwrap();
        assert_eq!(url.to_file_path().unwrap(), Path::new("/def"));
    }

    #[test]
    fn path_invalid_utf8() {
        // Non-root path.
        let url = CFURL::from_file_path(OsStr::from_bytes(b"abc\xd4def/xyz")).unwrap();
        assert_eq!(url.to_file_path().unwrap(), current_dir().unwrap()); // Huh?
        assert!(url
            .file_system_path(CFURLPathStyle::CFURLPOSIXPathStyle)
            .is_none());
        assert_eq!(url.string().to_string(), "abc%D4def/xyz");
        assert_eq!(url.path().unwrap().to_string(), "abc%D4def/xyz");

        // Root path.
        // lone continuation byte (128) (invalid utf8)
        let url = CFURL::from_file_path(OsStr::from_bytes(b"/\xf8a/b/c")).unwrap();
        assert_eq!(
            url.to_file_path().unwrap(),
            OsStr::from_bytes(b"/\xf8a/b/c")
        );
        assert_eq!(url.string().to_string(), "file:///%F8a/b/c");
        assert_eq!(url.path().unwrap().to_string(), "/%F8a/b/c");

        // Joined paths
        let url = CFURL::from_path(
            Path::new(OsStr::from_bytes(b"sub\xd4/%D4")),
            false,
            Some(&url),
        )
        .unwrap();
        assert_eq!(url.to_file_path(), None);
        assert_eq!(url.string().to_string(), "sub%D4/%25D4");
        assert_eq!(url.path().unwrap().to_string(), "sub%D4/%25D4");
        let abs = url.absolute_url().unwrap();
        assert_eq!(abs.to_file_path(), None);
        assert_eq!(abs.string().to_string(), "file:///%F8a/b/sub%D4/%25D4");
        assert_eq!(abs.path().unwrap().to_string(), "/%F8a/b/sub%D4/%25D4");
    }

    #[test]
    fn path_percent_encoded() {
        let url = CFURL::from_file_path("/%D4").unwrap();
        assert_eq!(url.path().unwrap().to_string(), "/%25D4");
        assert_eq!(url.to_file_path().unwrap(), Path::new("/%D4"));

        let url = CFURL::from_file_path("/%invalid").unwrap();
        assert_eq!(url.path().unwrap().to_string(), "/%25invalid");
        assert_eq!(url.to_file_path().unwrap(), Path::new("/%invalid"));
    }

    #[test]
    fn path_percent_encoded_eq() {
        let normal = CFURL::from_file_path(OsStr::from_bytes(b"\xf8")).unwrap();
        let percent = CFURL::from_file_path("%F8").unwrap();
        // Not equal, even though the filesystem may consider these paths equal.
        assert_ne!(normal, percent);
    }

    #[test]
    #[allow(deprecated)]
    #[ignore = "TODO: Crashes - is this unsound?"]
    fn hfs_invalid_utf8() {
        let url = CFURL::from_file_path(OsStr::from_bytes(b"\xd4")).unwrap();
        assert!(url
            .file_system_path(CFURLPathStyle::CFURLHFSPathStyle)
            .is_none());
    }
}