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}