Skip to main content

sysdir/
lib.rs

1// src/lib.rs
2//
3// Copyright (c) 2023 Ryan Lopopolo <rjl@hyperbo.la>
4//
5// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE> or
6// <http://www.apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT>
7// or <http://opensource.org/licenses/MIT>, at your option. All files in the
8// project carrying such notice may not be copied, modified, or distributed
9// except according to those terms.
10
11#![warn(clippy::all)]
12#![warn(clippy::pedantic)]
13#![warn(clippy::cargo)]
14#![allow(unknown_lints)]
15#![warn(missing_copy_implementations)]
16#![warn(missing_debug_implementations)]
17#![warn(missing_docs)]
18#![warn(rust_2018_idioms)]
19#![warn(trivial_casts, trivial_numeric_casts)]
20#![warn(unused_qualifications)]
21#![warn(variant_size_differences)]
22// Enable feature callouts in generated documentation:
23// https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html
24//
25// This approach is borrowed from tokio.
26#![cfg_attr(docsrs, feature(doc_cfg))]
27
28//! Enumeration of the filesystem paths for the various standard system
29//! directories where apps, resources, etc. get installed.
30//!
31//! This crate exposes Rust bindings to the `sysdir(3)` library functions
32//! provided by `libSystem.dylib` on macOS, iOS, tvOS, and watchOS.
33//!
34//! For more detailed documentation, refer to the [`sysdir(3)` man page](mod@man).
35//!
36//! # Platform Support
37//!
38//! The `sysdir` API first appeared in OS X 10.12, iOS 10, watchOS 3 and tvOS 10
39//! replacing the deprecated `NSSystemDirectories(3)` API.
40//!
41//! Note that this crate is completely empty on non-Apple platforms.
42//!
43//! ## Linkage
44//!
45//! `sysdir(3)` is provided by `libSystem`, which is linked into every binary on
46//! Apple platforms. This crate does not link to `CoreFoundation`, `Foundation`,
47//! or any other system libraries and frameworks.
48//!
49//! ## Path Semantics
50//!
51//! These bindings expose raw `sysdir(3)` search-path strings from Darwin.
52//! Returned values are not normalized filesystem paths:
53//!
54//! - user-domain results may contain a literal `~` instead of an expanded home
55//!   directory
56//! - if `NEXT_ROOT` is set and honored by the process, many local, network, and
57//!   system-domain results are prefixed by that directory
58//! - callers should not assume returned values are valid UTF-8 if `NEXT_ROOT`
59//!   contains non-UTF-8 bytes
60//!
61//! Callers that intend to use these values with filesystem APIs should expand
62//! `~`, account for `NEXT_ROOT`, and validate UTF-8 before opening or creating
63//! files.
64//!
65//! # Examples
66//!
67#![cfg_attr(
68    any(
69        target_os = "macos",
70        target_os = "ios",
71        target_os = "tvos",
72        target_os = "watchos"
73    ),
74    doc = "```"
75)]
76#![cfg_attr(
77    not(any(
78        target_os = "macos",
79        target_os = "ios",
80        target_os = "tvos",
81        target_os = "watchos"
82    )),
83    doc = "```compile_fail"
84)]
85//! use core::ffi::{c_char, CStr};
86//!
87//! use sysdir::*;
88//!
89//! let mut path = [0; PATH_MAX as usize];
90//!
91//! let dir = sysdir_search_path_directory_t::SYSDIR_DIRECTORY_USER;
92//! let domain_mask = SYSDIR_DOMAIN_MASK_LOCAL;
93//!
94//! unsafe {
95//!     let mut state = sysdir_start_search_path_enumeration(dir, domain_mask);
96//!     loop {
97//!         let path = path.as_mut_ptr().cast::<c_char>();
98//!         state = sysdir_get_next_search_path_enumeration(state, path);
99//!         if state == 0 {
100//!             break;
101//!         }
102//!         let path = CStr::from_ptr(path);
103//!         let bytes = path.to_bytes();
104//!         // `sysdir(3)` may prefix local-domain results with `NEXT_ROOT`,
105//!         // which can also introduce non-UTF-8 bytes.
106//!         assert!(bytes.ends_with(b"/Users"));
107//!     }
108//! }
109//! ```
110
111#![no_std]
112#![doc(html_root_url = "https://docs.rs/sysdir/1.3.3")]
113
114#[cfg(test)]
115extern crate std;
116
117// Ensure code blocks in `README.md` compile
118#[cfg(all(
119    doctest,
120    any(
121        target_os = "macos",
122        target_os = "ios",
123        target_os = "tvos",
124        target_os = "watchos"
125    )
126))]
127#[doc = include_str!("../README.md")]
128mod readme {}
129
130/// man page for `sysdir(3)`.
131///
132/// ```text
133#[doc = include_str!("../sysdir.3")]
134/// ```
135#[cfg(any(doc, doctest))]
136pub mod man {}
137
138/// Raw bindings to `sysdir(3)`, provided by `libSystem`.
139///
140/// The `sysdir` API first appeared in OS X 10.12, iOS 10, watchOS 3 and tvOS 10
141/// replacing the deprecated `NSSystemDirectories(3)` API.
142#[allow(missing_docs)]
143#[allow(non_camel_case_types)]
144#[allow(clippy::all)]
145#[allow(clippy::pedantic)]
146#[allow(clippy::restriction)]
147#[cfg(any(
148    target_os = "macos",
149    target_os = "ios",
150    target_os = "tvos",
151    target_os = "watchos"
152))]
153mod sys;
154
155#[cfg(any(
156    target_os = "macos",
157    target_os = "ios",
158    target_os = "tvos",
159    target_os = "watchos"
160))]
161pub use self::sys::*;
162
163#[cfg(all(
164    test,
165    any(
166        target_os = "macos",
167        target_os = "ios",
168        target_os = "tvos",
169        target_os = "watchos"
170    )
171))]
172mod tests {
173    use core::ffi::{CStr, c_char};
174    use std::os::unix::ffi::OsStrExt;
175    use std::{borrow::Cow, env};
176
177    use super::*;
178
179    fn expected_local_users_directory() -> Cow<'static, [u8]> {
180        match env::var_os("NEXT_ROOT") {
181            Some(next_root) => {
182                let next_root = next_root.as_os_str().as_bytes();
183                let next_root = next_root
184                    .iter()
185                    .rposition(|&byte| byte != b'/')
186                    .map_or(&[][..], |pos| &next_root[..=pos]);
187                if next_root.is_empty() {
188                    Cow::Borrowed(b"/Users")
189                } else {
190                    let mut path = std::vec::Vec::with_capacity(next_root.len() + b"/Users".len());
191                    path.extend_from_slice(next_root);
192                    path.extend_from_slice(b"/Users");
193                    Cow::Owned(path)
194                }
195            }
196            None => Cow::Borrowed(b"/Users"),
197        }
198    }
199
200    // EXAMPLES
201    //
202    // ```c
203    // #include <limits.h>
204    // #include <sysdir.h>
205    //
206    // char path[PATH_MAX];
207    // sysdir_search_path_enumeration_state state = sysdir_start_search_path_enumeration(dir, domainMask);
208    // while ( (state = sysdir_get_next_search_path_enumeration(state, path)) != 0 ) {
209    //     // Handle directory path
210    // }
211    // ```
212    #[test]
213    fn example_and_linkage() {
214        let mut count = 0_usize;
215        let mut path = [0; PATH_MAX as usize];
216        let expected = expected_local_users_directory();
217
218        let dir = sysdir_search_path_directory_t::SYSDIR_DIRECTORY_USER;
219        let domain_mask = SYSDIR_DOMAIN_MASK_LOCAL;
220
221        unsafe {
222            let mut state = sysdir_start_search_path_enumeration(dir, domain_mask);
223            loop {
224                let path = path.as_mut_ptr().cast::<c_char>();
225                state = sysdir_get_next_search_path_enumeration(state, path);
226                if state == 0 {
227                    break;
228                }
229                let path = CStr::from_ptr(path);
230                let bytes = path.to_bytes();
231                assert_eq!(bytes, expected.as_ref());
232                count += 1;
233            }
234        }
235
236        assert_eq!(count, 1, "Should iterate once and find `/Users`");
237    }
238
239    #[test]
240    fn example_and_linkage_with_opaque_state_helpers() {
241        let mut count = 0_usize;
242        let mut path = [0; PATH_MAX as usize];
243        let expected = expected_local_users_directory();
244
245        let dir = sysdir_search_path_directory_t::SYSDIR_DIRECTORY_USER;
246        let domain_mask = SYSDIR_DOMAIN_MASK_LOCAL;
247
248        unsafe {
249            let mut state = sysdir_start_search_path_enumeration(dir, domain_mask);
250            loop {
251                let path = path.as_mut_ptr().cast::<c_char>();
252                state = sysdir_get_next_search_path_enumeration(state, path);
253                if state.is_finished() {
254                    break;
255                }
256                let path = CStr::from_ptr(path);
257                let bytes = path.to_bytes();
258                assert_eq!(bytes, expected.as_ref());
259                count += 1;
260            }
261        }
262
263        assert_eq!(count, 1, "Should iterate once and find `/Users`");
264    }
265}