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(ffi::apple_log_os_log_message_component_copy_placeholder(
168                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(
190                            self.ptr.as_ptr(),
191                            &mut length,
192                        ),
193                        length.max(0) as usize,
194                    )
195                };
196                OSLogMessageArgument::Data(bytes)
197            }
198            OSLogMessageArgumentCategory::Double => OSLogMessageArgument::Double(unsafe {
199                ffi::apple_log_os_log_message_component_get_double(self.ptr.as_ptr())
200            }),
201            OSLogMessageArgumentCategory::Int64 => OSLogMessageArgument::Signed(unsafe {
202                ffi::apple_log_os_log_message_component_get_int64(self.ptr.as_ptr())
203            }),
204            OSLogMessageArgumentCategory::String => {
205                let value = unsafe {
206                    take_optional_c_string(ffi::apple_log_os_log_message_component_copy_string(
207                        self.ptr.as_ptr(),
208                    ))
209                }
210                .unwrap_or_default();
211                OSLogMessageArgument::String(value)
212            }
213            OSLogMessageArgumentCategory::UInt64 => OSLogMessageArgument::Unsigned(unsafe {
214                ffi::apple_log_os_log_message_component_get_uint64(self.ptr.as_ptr())
215            }),
216        }
217    }
218}
219
220impl Drop for OSLogMessageComponent {
221    fn drop(&mut self) {
222        unsafe { ffi::apple_log_os_log_message_component_release(self.ptr.as_ptr()) };
223    }
224}
225
226/// Common fields exposed by every `OSLogEntry` snapshot.
227pub trait OSLogEntryCommon {
228    fn raw_entry_ptr(&self) -> *mut c_void;
229
230    #[must_use]
231    fn composed_message(&self) -> String {
232        unsafe {
233            take_optional_c_string(ffi::apple_log_os_log_entry_copy_composed_message(
234                self.raw_entry_ptr(),
235            ))
236        }
237        .unwrap_or_default()
238    }
239
240    #[must_use]
241    fn date(&self) -> SystemTime {
242        secs_to_system_time(unsafe {
243            ffi::apple_log_os_log_entry_get_date_seconds(self.raw_entry_ptr())
244        })
245    }
246
247    #[must_use]
248    fn store_category(&self) -> OSLogStoreCategory {
249        OSLogStoreCategory::from_raw(unsafe {
250            ffi::apple_log_os_log_entry_get_store_category(self.raw_entry_ptr())
251        })
252    }
253}
254
255/// Fields shared by entries conforming to `OSLogEntryFromProcess`.
256pub trait OSLogEntryFromProcess: OSLogEntryCommon {
257    #[must_use]
258    fn activity_identifier(&self) -> u64 {
259        unsafe { ffi::apple_log_os_log_entry_get_activity_identifier(self.raw_entry_ptr()) }
260    }
261
262    #[must_use]
263    fn process(&self) -> String {
264        unsafe {
265            take_optional_c_string(ffi::apple_log_os_log_entry_copy_process(
266                self.raw_entry_ptr(),
267            ))
268        }
269        .unwrap_or_default()
270    }
271
272    #[must_use]
273    fn process_identifier(&self) -> i32 {
274        unsafe { ffi::apple_log_os_log_entry_get_process_identifier(self.raw_entry_ptr()) }
275    }
276
277    #[must_use]
278    fn sender(&self) -> String {
279        unsafe {
280            take_optional_c_string(ffi::apple_log_os_log_entry_copy_sender(
281                self.raw_entry_ptr(),
282            ))
283        }
284        .unwrap_or_default()
285    }
286
287    #[must_use]
288    fn thread_identifier(&self) -> u64 {
289        unsafe { ffi::apple_log_os_log_entry_get_thread_identifier(self.raw_entry_ptr()) }
290    }
291}
292
293/// Fields shared by entries conforming to `OSLogEntryWithPayload`.
294pub trait OSLogEntryWithPayload: OSLogEntryFromProcess {
295    #[must_use]
296    fn category(&self) -> String {
297        unsafe {
298            take_optional_c_string(ffi::apple_log_os_log_entry_copy_category(
299                self.raw_entry_ptr(),
300            ))
301        }
302        .unwrap_or_default()
303    }
304
305    #[must_use]
306    fn format_string(&self) -> String {
307        unsafe {
308            take_optional_c_string(ffi::apple_log_os_log_entry_copy_format_string(
309                self.raw_entry_ptr(),
310            ))
311        }
312        .unwrap_or_default()
313    }
314
315    #[must_use]
316    fn subsystem(&self) -> String {
317        unsafe {
318            take_optional_c_string(ffi::apple_log_os_log_entry_copy_subsystem(
319                self.raw_entry_ptr(),
320            ))
321        }
322        .unwrap_or_default()
323    }
324
325    #[must_use]
326    fn components(&self) -> Vec<OSLogMessageComponent> {
327        let count = unsafe { ffi::apple_log_os_log_entry_component_count(self.raw_entry_ptr()) };
328        (0..count.max(0) as usize)
329            .filter_map(|index| {
330                NonNull::new(unsafe {
331                    ffi::apple_log_os_log_entry_component_get(self.raw_entry_ptr(), index as isize)
332                })
333                .map(|ptr| OSLogMessageComponent { ptr })
334            })
335            .collect()
336    }
337}
338
339/// `OSLogPosition` wrapper.
340pub struct OSLogPosition {
341    ptr: NonNull<c_void>,
342}
343
344impl OSLogPosition {
345    const fn from_raw(ptr: NonNull<c_void>) -> Self {
346        Self { ptr }
347    }
348
349    pub(crate) const fn as_ptr(&self) -> *mut c_void {
350        self.ptr.as_ptr()
351    }
352}
353
354impl Drop for OSLogPosition {
355    fn drop(&mut self) {
356        unsafe { ffi::apple_log_os_log_position_release(self.ptr.as_ptr()) };
357    }
358}
359
360/// Typed entry returned from `OSLogStore` enumeration.
361pub enum OSLogStoreEntry {
362    Log(OSLogEntryLog),
363    Signpost(OSLogEntrySignpost),
364    Boundary(OSLogEntryBoundary),
365    Activity(OSLogEntryActivity),
366}
367
368/// Safe wrapper around `OSLogStore`.
369pub struct OSLogStore {
370    ptr: NonNull<c_void>,
371}
372
373impl OSLogStore {
374    /// Opens the local log store.
375    ///
376    /// # Errors
377    ///
378    /// Returns an error if the store cannot be opened.
379    pub fn local() -> Result<Self, LogError> {
380        let ptr = bridge_ptr_result("OSLogStore::local", |error_out| unsafe {
381            ffi::apple_log_os_log_store_local(error_out)
382        })?;
383        Ok(Self { ptr })
384    }
385
386    /// Opens a scoped log store.
387    ///
388    /// # Errors
389    ///
390    /// Returns an error if the store cannot be opened.
391    pub fn new(scope: OSLogStoreScope) -> Result<Self, LogError> {
392        let ptr = bridge_ptr_result("OSLogStore::new", |error_out| unsafe {
393            ffi::apple_log_os_log_store_create(scope.raw(), error_out)
394        })?;
395        Ok(Self { ptr })
396    }
397
398    /// Opens a logarchive URL.
399    ///
400    /// # Errors
401    ///
402    /// Returns an error if the path contains a NUL byte or the store cannot be opened.
403    pub fn from_url(path: impl AsRef<Path>) -> Result<Self, LogError> {
404        let path = path_c_string(path.as_ref())?;
405        let ptr = bridge_ptr_result("OSLogStore::from_url", |error_out| unsafe {
406            ffi::apple_log_os_log_store_from_url(path.as_ptr(), error_out)
407        })?;
408        Ok(Self { ptr })
409    }
410
411    #[must_use]
412    pub fn position_at(&self, time: SystemTime) -> OSLogPosition {
413        let ptr = NonNull::new(unsafe {
414            ffi::apple_log_os_log_store_position_date(self.ptr.as_ptr(), system_time_to_secs(time))
415        })
416        .expect("Swift bridge never returns NULL for OSLogStore::position_at");
417        OSLogPosition::from_raw(ptr)
418    }
419
420    #[must_use]
421    pub fn position_time_interval_since_end(&self, duration: Duration) -> OSLogPosition {
422        let ptr = NonNull::new(unsafe {
423            ffi::apple_log_os_log_store_position_since_end(
424                self.ptr.as_ptr(),
425                duration.as_secs_f64(),
426            )
427        })
428        .expect("Swift bridge never returns NULL for OSLogStore::position_time_interval_since_end");
429        OSLogPosition::from_raw(ptr)
430    }
431
432    #[must_use]
433    pub fn position_time_interval_since_latest_boot(&self, duration: Duration) -> OSLogPosition {
434        let ptr = NonNull::new(unsafe {
435            ffi::apple_log_os_log_store_position_since_latest_boot(
436                self.ptr.as_ptr(),
437                duration.as_secs_f64(),
438            )
439        })
440        .expect(
441            "Swift bridge never returns NULL for OSLogStore::position_time_interval_since_latest_boot",
442        );
443        OSLogPosition::from_raw(ptr)
444    }
445
446    /// Enumerates entries from the store.
447    ///
448    /// # Errors
449    ///
450    /// Returns an error if the predicate contains a NUL byte or the store enumeration fails.
451    pub fn get_entries(
452        &self,
453        options: OSLogEnumeratorOptions,
454        position: Option<&OSLogPosition>,
455        predicate: Option<&str>,
456    ) -> Result<Vec<OSLogStoreEntry>, LogError> {
457        let predicate = predicate
458            .map(|value| c_string_arg("predicate", value))
459            .transpose()?;
460        let list = OSLogEntryList {
461            ptr: bridge_ptr_result("OSLogStore::get_entries", |error_out| unsafe {
462                ffi::apple_log_os_log_store_get_entries(
463                    self.ptr.as_ptr(),
464                    options.bits(),
465                    position.map_or(std::ptr::null_mut(), OSLogPosition::as_ptr),
466                    predicate
467                        .as_ref()
468                        .map_or(std::ptr::null(), |value| value.as_ptr()),
469                    error_out,
470                )
471            })?,
472        };
473        let count =
474            unsafe { ffi::apple_log_os_log_entry_list_count(list.ptr.as_ptr()) }.max(0) as usize;
475        let mut entries = Vec::with_capacity(count);
476        for index in 0..count {
477            let Some(entry_ptr) = NonNull::new(unsafe {
478                ffi::apple_log_os_log_entry_list_get(list.ptr.as_ptr(), index as isize)
479            }) else {
480                continue;
481            };
482            let entry = match unsafe { ffi::apple_log_os_log_entry_kind(entry_ptr.as_ptr()) } {
483                1 => OSLogStoreEntry::Log(OSLogEntryLog::from_raw(entry_ptr)),
484                2 => OSLogStoreEntry::Signpost(OSLogEntrySignpost::from_raw(entry_ptr)),
485                3 => OSLogStoreEntry::Boundary(OSLogEntryBoundary::from_raw(entry_ptr)),
486                4 => OSLogStoreEntry::Activity(OSLogEntryActivity::from_raw(entry_ptr)),
487                _ => {
488                    unsafe { ffi::apple_log_os_log_entry_release(entry_ptr.as_ptr()) };
489                    continue;
490                }
491            };
492            entries.push(entry);
493        }
494        Ok(entries)
495    }
496
497    /// Alias for `get_entries`.
498    ///
499    /// # Errors
500    ///
501    /// Returns the same errors as `get_entries`.
502    pub fn entries(
503        &self,
504        options: OSLogEnumeratorOptions,
505        position: Option<&OSLogPosition>,
506        predicate: Option<&str>,
507    ) -> Result<Vec<OSLogStoreEntry>, LogError> {
508        self.get_entries(options, position, predicate)
509    }
510}
511
512impl Drop for OSLogStore {
513    fn drop(&mut self) {
514        unsafe { ffi::apple_log_os_log_store_release(self.ptr.as_ptr()) };
515    }
516}