Skip to main content

apple_log/
os_log_store.rs

1#![allow(
2    clippy::cast_possible_wrap,
3    clippy::cast_sign_loss,
4    clippy::missing_panics_doc
5)]
6
7use core::ffi::c_void;
8use core::ops::{BitOr, BitOrAssign};
9use std::path::Path;
10use std::ptr::NonNull;
11use std::time::{Duration, SystemTime};
12
13use crate::bridge_support::{
14    bridge_ptr_result, c_string_arg, path_c_string, secs_to_system_time, system_time_to_secs,
15    take_optional_c_string, take_owned_bytes,
16};
17use crate::error::LogError;
18use crate::ffi;
19use crate::os_log_entry_activity::OSLogEntryActivity;
20use crate::os_log_entry_boundary::OSLogEntryBoundary;
21use crate::os_log_entry_log::OSLogEntryLog;
22use crate::os_log_entry_signpost::OSLogEntrySignpost;
23
24struct OSLogEntryList {
25    ptr: NonNull<c_void>,
26}
27
28impl Drop for OSLogEntryList {
29    fn drop(&mut self) {
30        unsafe { ffi::apple_log_os_log_entry_list_release(self.ptr.as_ptr()) };
31    }
32}
33
34/// `OSLogStore.Scope`.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub enum OSLogStoreScope {
37    System,
38    CurrentProcessIdentifier,
39}
40
41impl OSLogStoreScope {
42    const fn raw(self) -> i32 {
43        match self {
44            Self::System => 0,
45            Self::CurrentProcessIdentifier => 1,
46        }
47    }
48}
49
50/// `OSLogEnumerator.Options`.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
52pub struct OSLogEnumeratorOptions(usize);
53
54impl OSLogEnumeratorOptions {
55    pub const NONE: Self = Self(0);
56    pub const REVERSE: Self = Self(1);
57
58    #[must_use]
59    pub const fn bits(self) -> usize {
60        self.0
61    }
62
63    #[must_use]
64    pub const fn contains(self, other: Self) -> bool {
65        (self.0 & other.0) == other.0
66    }
67}
68
69impl BitOr for OSLogEnumeratorOptions {
70    type Output = Self;
71
72    fn bitor(self, rhs: Self) -> Self::Output {
73        Self(self.0 | rhs.0)
74    }
75}
76
77impl BitOrAssign for OSLogEnumeratorOptions {
78    fn bitor_assign(&mut self, rhs: Self) {
79        self.0 |= rhs.0;
80    }
81}
82
83/// `OSLogEntry.StoreCategory`.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85pub enum OSLogStoreCategory {
86    Undefined,
87    Metadata,
88    ShortTerm,
89    LongTermAuto,
90    LongTerm1,
91    LongTerm3,
92    LongTerm7,
93    LongTerm14,
94    LongTerm30,
95}
96
97impl OSLogStoreCategory {
98    pub(crate) const fn from_raw(raw: i32) -> Self {
99        match raw {
100            1 => Self::Metadata,
101            2 => Self::ShortTerm,
102            3 => Self::LongTermAuto,
103            4 => Self::LongTerm1,
104            5 => Self::LongTerm3,
105            6 => Self::LongTerm7,
106            7 => Self::LongTerm14,
107            8 => Self::LongTerm30,
108            _ => Self::Undefined,
109        }
110    }
111}
112
113/// `OSLogMessageComponent.ArgumentCategory`.
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
115pub enum OSLogMessageArgumentCategory {
116    Undefined,
117    Data,
118    Double,
119    Int64,
120    String,
121    UInt64,
122}
123
124impl OSLogMessageArgumentCategory {
125    const fn from_raw(raw: i32) -> Self {
126        match raw {
127            1 => Self::Data,
128            2 => Self::Double,
129            3 => Self::Int64,
130            4 => Self::String,
131            5 => Self::UInt64,
132            _ => Self::Undefined,
133        }
134    }
135}
136
137/// Decoded `OSLogMessageComponent.Argument` value.
138#[derive(Debug, Clone, PartialEq)]
139pub enum OSLogMessageArgument {
140    Undefined,
141    Data(Vec<u8>),
142    Double(f64),
143    Signed(i64),
144    String(String),
145    Unsigned(u64),
146}
147
148/// Snapshot of an `OSLogMessageComponent`.
149pub struct OSLogMessageComponent {
150    ptr: NonNull<c_void>,
151}
152
153impl OSLogMessageComponent {
154    #[must_use]
155    pub fn format_substring(&self) -> String {
156        unsafe {
157            take_optional_c_string(
158                ffi::apple_log_os_log_message_component_copy_format_substring(self.ptr.as_ptr()),
159            )
160        }
161        .unwrap_or_default()
162    }
163
164    #[must_use]
165    pub fn placeholder(&self) -> String {
166        unsafe {
167            take_optional_c_string(
168                ffi::apple_log_os_log_message_component_copy_placeholder(self.ptr.as_ptr()),
169            )
170        }
171        .unwrap_or_default()
172    }
173
174    #[must_use]
175    pub fn argument_category(&self) -> OSLogMessageArgumentCategory {
176        OSLogMessageArgumentCategory::from_raw(unsafe {
177            ffi::apple_log_os_log_message_component_get_argument_category(self.ptr.as_ptr())
178        })
179    }
180
181    #[must_use]
182    pub fn argument(&self) -> OSLogMessageArgument {
183        match self.argument_category() {
184            OSLogMessageArgumentCategory::Undefined => OSLogMessageArgument::Undefined,
185            OSLogMessageArgumentCategory::Data => {
186                let mut length = 0_isize;
187                let bytes = unsafe {
188                    take_owned_bytes(
189                        ffi::apple_log_os_log_message_component_copy_data(self.ptr.as_ptr(), &mut length),
190                        length.max(0) as usize,
191                    )
192                };
193                OSLogMessageArgument::Data(bytes)
194            }
195            OSLogMessageArgumentCategory::Double => OSLogMessageArgument::Double(unsafe {
196                ffi::apple_log_os_log_message_component_get_double(self.ptr.as_ptr())
197            }),
198            OSLogMessageArgumentCategory::Int64 => OSLogMessageArgument::Signed(unsafe {
199                ffi::apple_log_os_log_message_component_get_int64(self.ptr.as_ptr())
200            }),
201            OSLogMessageArgumentCategory::String => {
202                let value = unsafe {
203                    take_optional_c_string(
204                        ffi::apple_log_os_log_message_component_copy_string(self.ptr.as_ptr()),
205                    )
206                }
207                .unwrap_or_default();
208                OSLogMessageArgument::String(value)
209            }
210            OSLogMessageArgumentCategory::UInt64 => OSLogMessageArgument::Unsigned(unsafe {
211                ffi::apple_log_os_log_message_component_get_uint64(self.ptr.as_ptr())
212            }),
213        }
214    }
215}
216
217impl Drop for OSLogMessageComponent {
218    fn drop(&mut self) {
219        unsafe { ffi::apple_log_os_log_message_component_release(self.ptr.as_ptr()) };
220    }
221}
222
223/// Common fields exposed by every `OSLogEntry` snapshot.
224pub trait OSLogEntryCommon {
225    fn raw_entry_ptr(&self) -> *mut c_void;
226
227    #[must_use]
228    fn composed_message(&self) -> String {
229        unsafe { take_optional_c_string(ffi::apple_log_os_log_entry_copy_composed_message(self.raw_entry_ptr())) }
230            .unwrap_or_default()
231    }
232
233    #[must_use]
234    fn date(&self) -> SystemTime {
235        secs_to_system_time(unsafe { ffi::apple_log_os_log_entry_get_date_seconds(self.raw_entry_ptr()) })
236    }
237
238    #[must_use]
239    fn store_category(&self) -> OSLogStoreCategory {
240        OSLogStoreCategory::from_raw(unsafe {
241            ffi::apple_log_os_log_entry_get_store_category(self.raw_entry_ptr())
242        })
243    }
244}
245
246/// Fields shared by entries conforming to `OSLogEntryFromProcess`.
247pub trait OSLogEntryFromProcess: OSLogEntryCommon {
248    #[must_use]
249    fn activity_identifier(&self) -> u64 {
250        unsafe { ffi::apple_log_os_log_entry_get_activity_identifier(self.raw_entry_ptr()) }
251    }
252
253    #[must_use]
254    fn process(&self) -> String {
255        unsafe { take_optional_c_string(ffi::apple_log_os_log_entry_copy_process(self.raw_entry_ptr())) }
256            .unwrap_or_default()
257    }
258
259    #[must_use]
260    fn process_identifier(&self) -> i32 {
261        unsafe { ffi::apple_log_os_log_entry_get_process_identifier(self.raw_entry_ptr()) }
262    }
263
264    #[must_use]
265    fn sender(&self) -> String {
266        unsafe { take_optional_c_string(ffi::apple_log_os_log_entry_copy_sender(self.raw_entry_ptr())) }
267            .unwrap_or_default()
268    }
269
270    #[must_use]
271    fn thread_identifier(&self) -> u64 {
272        unsafe { ffi::apple_log_os_log_entry_get_thread_identifier(self.raw_entry_ptr()) }
273    }
274}
275
276/// Fields shared by entries conforming to `OSLogEntryWithPayload`.
277pub trait OSLogEntryWithPayload: OSLogEntryFromProcess {
278    #[must_use]
279    fn category(&self) -> String {
280        unsafe { take_optional_c_string(ffi::apple_log_os_log_entry_copy_category(self.raw_entry_ptr())) }
281            .unwrap_or_default()
282    }
283
284    #[must_use]
285    fn format_string(&self) -> String {
286        unsafe {
287            take_optional_c_string(ffi::apple_log_os_log_entry_copy_format_string(self.raw_entry_ptr()))
288        }
289        .unwrap_or_default()
290    }
291
292    #[must_use]
293    fn subsystem(&self) -> String {
294        unsafe { take_optional_c_string(ffi::apple_log_os_log_entry_copy_subsystem(self.raw_entry_ptr())) }
295            .unwrap_or_default()
296    }
297
298    #[must_use]
299    fn components(&self) -> Vec<OSLogMessageComponent> {
300        let count = unsafe { ffi::apple_log_os_log_entry_component_count(self.raw_entry_ptr()) };
301        (0..count.max(0) as usize)
302            .filter_map(|index| {
303                NonNull::new(unsafe {
304                    ffi::apple_log_os_log_entry_component_get(self.raw_entry_ptr(), index as isize)
305                })
306                .map(|ptr| OSLogMessageComponent { ptr })
307            })
308            .collect()
309    }
310}
311
312/// `OSLogPosition` wrapper.
313pub struct OSLogPosition {
314    ptr: NonNull<c_void>,
315}
316
317impl OSLogPosition {
318    const fn from_raw(ptr: NonNull<c_void>) -> Self {
319        Self { ptr }
320    }
321
322    pub(crate) const fn as_ptr(&self) -> *mut c_void {
323        self.ptr.as_ptr()
324    }
325}
326
327impl Drop for OSLogPosition {
328    fn drop(&mut self) {
329        unsafe { ffi::apple_log_os_log_position_release(self.ptr.as_ptr()) };
330    }
331}
332
333/// Typed entry returned from `OSLogStore` enumeration.
334pub enum OSLogStoreEntry {
335    Log(OSLogEntryLog),
336    Signpost(OSLogEntrySignpost),
337    Boundary(OSLogEntryBoundary),
338    Activity(OSLogEntryActivity),
339}
340
341/// Safe wrapper around `OSLogStore`.
342pub struct OSLogStore {
343    ptr: NonNull<c_void>,
344}
345
346impl OSLogStore {
347    /// Opens the local log store.
348    ///
349    /// # Errors
350    ///
351    /// Returns an error if the store cannot be opened.
352    pub fn local() -> Result<Self, LogError> {
353        let ptr = bridge_ptr_result("OSLogStore::local", |error_out| unsafe {
354            ffi::apple_log_os_log_store_local(error_out)
355        })?;
356        Ok(Self { ptr })
357    }
358
359    /// Opens a scoped log store.
360    ///
361    /// # Errors
362    ///
363    /// Returns an error if the store cannot be opened.
364    pub fn new(scope: OSLogStoreScope) -> Result<Self, LogError> {
365        let ptr = bridge_ptr_result("OSLogStore::new", |error_out| unsafe {
366            ffi::apple_log_os_log_store_create(scope.raw(), error_out)
367        })?;
368        Ok(Self { ptr })
369    }
370
371    /// Opens a logarchive URL.
372    ///
373    /// # Errors
374    ///
375    /// Returns an error if the path contains a NUL byte or the store cannot be opened.
376    pub fn from_url(path: impl AsRef<Path>) -> Result<Self, LogError> {
377        let path = path_c_string(path.as_ref())?;
378        let ptr = bridge_ptr_result("OSLogStore::from_url", |error_out| unsafe {
379            ffi::apple_log_os_log_store_from_url(path.as_ptr(), error_out)
380        })?;
381        Ok(Self { ptr })
382    }
383
384    #[must_use]
385    pub fn position_at(&self, time: SystemTime) -> OSLogPosition {
386        let ptr = NonNull::new(unsafe {
387            ffi::apple_log_os_log_store_position_date(self.ptr.as_ptr(), system_time_to_secs(time))
388        })
389        .expect("Swift bridge never returns NULL for OSLogStore::position_at");
390        OSLogPosition::from_raw(ptr)
391    }
392
393    #[must_use]
394    pub fn position_time_interval_since_end(&self, duration: Duration) -> OSLogPosition {
395        let ptr = NonNull::new(unsafe {
396            ffi::apple_log_os_log_store_position_since_end(self.ptr.as_ptr(), duration.as_secs_f64())
397        })
398        .expect("Swift bridge never returns NULL for OSLogStore::position_time_interval_since_end");
399        OSLogPosition::from_raw(ptr)
400    }
401
402    #[must_use]
403    pub fn position_time_interval_since_latest_boot(&self, duration: Duration) -> OSLogPosition {
404        let ptr = NonNull::new(unsafe {
405            ffi::apple_log_os_log_store_position_since_latest_boot(
406                self.ptr.as_ptr(),
407                duration.as_secs_f64(),
408            )
409        })
410        .expect(
411            "Swift bridge never returns NULL for OSLogStore::position_time_interval_since_latest_boot",
412        );
413        OSLogPosition::from_raw(ptr)
414    }
415
416    /// Enumerates entries from the store.
417    ///
418    /// # Errors
419    ///
420    /// Returns an error if the predicate contains a NUL byte or the store enumeration fails.
421    pub fn get_entries(
422        &self,
423        options: OSLogEnumeratorOptions,
424        position: Option<&OSLogPosition>,
425        predicate: Option<&str>,
426    ) -> Result<Vec<OSLogStoreEntry>, LogError> {
427        let predicate = predicate.map(|value| c_string_arg("predicate", value)).transpose()?;
428        let list = OSLogEntryList {
429            ptr: bridge_ptr_result("OSLogStore::get_entries", |error_out| unsafe {
430                ffi::apple_log_os_log_store_get_entries(
431                    self.ptr.as_ptr(),
432                    options.bits(),
433                    position.map_or(std::ptr::null_mut(), OSLogPosition::as_ptr),
434                    predicate.as_ref().map_or(std::ptr::null(), |value| value.as_ptr()),
435                    error_out,
436                )
437            })?,
438        };
439        let count = unsafe { ffi::apple_log_os_log_entry_list_count(list.ptr.as_ptr()) }.max(0) as usize;
440        let mut entries = Vec::with_capacity(count);
441        for index in 0..count {
442            let Some(entry_ptr) = NonNull::new(unsafe {
443                ffi::apple_log_os_log_entry_list_get(list.ptr.as_ptr(), index as isize)
444            }) else {
445                continue;
446            };
447            let entry = match unsafe { ffi::apple_log_os_log_entry_kind(entry_ptr.as_ptr()) } {
448                1 => OSLogStoreEntry::Log(OSLogEntryLog::from_raw(entry_ptr)),
449                2 => OSLogStoreEntry::Signpost(OSLogEntrySignpost::from_raw(entry_ptr)),
450                3 => OSLogStoreEntry::Boundary(OSLogEntryBoundary::from_raw(entry_ptr)),
451                4 => OSLogStoreEntry::Activity(OSLogEntryActivity::from_raw(entry_ptr)),
452                _ => {
453                    unsafe { ffi::apple_log_os_log_entry_release(entry_ptr.as_ptr()) };
454                    continue;
455                }
456            };
457            entries.push(entry);
458        }
459        Ok(entries)
460    }
461
462    /// Alias for `get_entries`.
463    ///
464    /// # Errors
465    ///
466    /// Returns the same errors as `get_entries`.
467    pub fn entries(
468        &self,
469        options: OSLogEnumeratorOptions,
470        position: Option<&OSLogPosition>,
471        predicate: Option<&str>,
472    ) -> Result<Vec<OSLogStoreEntry>, LogError> {
473        self.get_entries(options, position, predicate)
474    }
475}
476
477impl Drop for OSLogStore {
478    fn drop(&mut self) {
479        unsafe { ffi::apple_log_os_log_store_release(self.ptr.as_ptr()) };
480    }
481}