launchd/
lib.rs

1//! A Rust library for creating and parsing Launchd files.
2//!
3//! It's still in early development and all help is welcome.
4//!
5//! ## Example
6//!
7//! ``` rust
8//! use launchd::{CalendarInterval, Error, Launchd};
9//!
10//! fn main() -> Result<(), Error> {
11//!     let ci = CalendarInterval::default()
12//!         .with_hour(12)?
13//!         .with_minute(10)?
14//!         .with_weekday(7)?;
15//!
16//!     let launchd = Launchd::new("LABEL", "./foo/bar.txt")?
17//!             .with_user_name("Henk")
18//!             .with_program_arguments(vec!["Hello".to_string(), "World!".to_string()])
19//!             .with_start_calendar_intervals(vec![ci])
20//!             .disabled();
21//!
22//!     #[cfg(feature="io")] // Default
23//!     return launchd.to_writer_xml(std::io::stdout());
24//!
25//!     #[cfg(not(feature="io"))] // If you don't want to build any optional dependencies
26//!     return Ok(());
27//! }
28//! ```
29//!
30//! Results in:
31//!
32//! ``` xml
33//! <?xml version="1.0" encoding="UTF-8"?>
34//! <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
35//! <plist version="1.0">
36//! <dict>
37//!         <key>Label</key>
38//!         <string>LABEL</string>
39//!         <key>Disabled</key>
40//!         <true />
41//!         <key>UserName</key>
42//!         <string>Henk</string>
43//!         <key>Program</key>
44//!         <string>./foo/bar.txt</string>
45//!         <key>ProgramArguments</key>
46//!         <array>
47//!                 <string>Hello</string>
48//!                 <string>World!</string>
49//!         </array>
50//!         <key>StartCalendarIntervals</key>
51//!         <array>
52//!                 <dict>
53//!                         <key>Minute</key>
54//!                         <integer>10</integer>
55//!                         <key>Hour</key>
56//!                         <integer>12</integer>
57//!                         <key>Weekday</key>
58//!                         <integer>7</integer>
59//!                 </dict>
60//!         </array>
61//! </dict>
62//! </plist>
63//! ```
64
65mod error;
66pub mod keep_alive;
67pub mod mach_services;
68pub mod process_type;
69pub mod resource_limits;
70pub mod sockets;
71
72pub use self::error::Error;
73pub use self::keep_alive::{KeepAliveOptions, KeepAliveType};
74pub use self::mach_services::{MachServiceEntry, MachServiceOptions};
75pub use self::process_type::ProcessType;
76pub use self::resource_limits::ResourceLimits;
77pub use self::sockets::{BonjourType, Socket, SocketOptions, Sockets};
78
79#[cfg(feature = "cron")]
80use cron::{Schedule, TimeUnitSpec};
81#[cfg(feature = "plist")]
82use plist::Value;
83#[cfg(feature = "io")]
84use plist::{from_bytes, from_file, from_reader, from_reader_xml};
85#[cfg(feature = "io")]
86use plist::{to_file_binary, to_file_xml, to_writer_binary, to_writer_xml};
87#[cfg(feature = "serde")]
88use serde::{Deserialize, Serialize};
89use std::collections::HashMap;
90#[cfg(feature = "cron")]
91use std::convert::TryInto;
92#[cfg(feature = "io")]
93use std::io::{Read, Seek, Write};
94use std::path::Path;
95
96/// Representation of a launchd.plist file.
97/// The definition of which can be found [here](https://www.manpagez.com/man/5/launchd.plist/).
98///
99/// Usage:
100/// ```
101/// use launchd::{Launchd, Error, CalendarInterval};
102/// use std::path::Path;
103///
104/// fn example() -> Result<Launchd, Error> {
105///     Ok(Launchd::new("LABEL", Path::new("./foo/bar.txt"))?
106///         .with_user_name("Henk")
107///         .with_program_arguments(vec!["Hello".to_string(), "World!".to_string()])
108///         .with_start_calendar_intervals(vec![CalendarInterval::default().with_hour(12)?])
109///         .disabled()
110///         // etc...
111///     )
112/// }
113///
114/// let launchd = example();
115///
116/// ```
117/// This will create a launchd representation with the label "LABEL", running "./foo/bar.txt"
118/// with the args "Hello" and "World!", for the user "Henk", each day at 12.
119///
120/// NB: The usage is still subject to change.
121// TODO: Fill with all options in https://www.manpagez.com/man/5/launchd.plist/
122// TODO: remove owned Strings (?)
123#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
124#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
125#[cfg_attr(feature = "io", serde(rename_all = "PascalCase"))]
126#[derive(Debug, Default, PartialEq)]
127pub struct Launchd {
128    label: String,
129    disabled: Option<bool>,
130    user_name: Option<String>,
131    group_name: Option<String>,
132    #[cfg_attr(feature = "serde", serde(rename = "inetdCompatibility"))]
133    inetd_compatibility: Option<HashMap<InetdCompatibility, bool>>,
134    limit_load_to_hosts: Option<Vec<String>>,
135    limit_load_from_hosts: Option<Vec<String>>,
136    limit_load_to_session_type: Option<LoadSessionType>,
137    limit_load_to_hardware: Option<HashMap<String, Vec<String>>>,
138    limit_load_from_hardware: Option<HashMap<String, Vec<String>>>,
139    program: Option<String>, // TODO: Ensure this: "NOTE: The Program key must be an absolute path."
140    bundle_program: Option<String>,
141    program_arguments: Option<Vec<String>>,
142    enable_globbing: Option<bool>,
143    enable_transactions: Option<bool>,
144    enable_pressured_exit: Option<bool>,
145    on_demand: Option<bool>, // NB: deprecated (see KeepAlive), but still needed for reading old plists.
146    #[cfg_attr(feature = "serde", serde(rename = "ServiceIPC"))]
147    service_ipc: Option<bool>, // NB: "Please remove this key from your launchd.plist."
148    keep_alive: Option<KeepAliveType>,
149    run_at_load: Option<bool>,
150    root_directory: Option<String>,
151    working_directory: Option<String>,
152    environment_variables: Option<HashMap<String, String>>,
153    umask: Option<u16>, // NB: This is a Unix permission mask. Defined as: typedef __uint16_t __darwin_mode_t;
154    time_out: Option<u32>,
155    exit_time_out: Option<u32>,
156    throttle_interval: Option<u32>,
157    init_groups: Option<bool>,
158    watch_paths: Option<Vec<String>>,
159    queue_directories: Option<Vec<String>>,
160    start_on_mount: Option<bool>,
161    start_interval: Option<u32>,
162    start_calendar_intervals: Option<Vec<CalendarInterval>>,
163    standard_in_path: Option<String>,
164    standard_out_path: Option<String>,
165    standard_error_path: Option<String>,
166    debug: Option<bool>,
167    wait_for_debugger: Option<bool>,
168    soft_resource_limits: Option<ResourceLimits>,
169    hard_resource_limits: Option<ResourceLimits>,
170    nice: Option<i32>,
171    process_type: Option<ProcessType>,
172    abandon_process_group: Option<bool>,
173    #[cfg_attr(feature = "serde", serde(rename = "LowPriorityIO"))]
174    low_priority_io: Option<bool>,
175    #[cfg_attr(feature = "serde", serde(rename = "LowPriorityBackgroundIO"))]
176    low_priority_background_io: Option<bool>,
177    materialize_dataless_files: Option<bool>,
178    launch_only_once: Option<bool>,
179    mach_services: Option<HashMap<String, MachServiceEntry>>,
180    sockets: Option<Sockets>,
181    launch_events: Option<LaunchEvents>,
182    hopefully_exits_last: Option<bool>, // NB: Deprecated, keep for reading old plists.
183    hopefully_exits_first: Option<bool>, // NB: Deprecated, keep for reading old plists.
184    session_create: Option<bool>,
185    legacy_timers: Option<bool>, // NB: Deprecated, keep for reading old plists.
186                                 // associated_bundle_identifiers: Option<<string or array of strings>>
187}
188
189// Defined as a "<dictionary of dictionaries of dictionaries>" in launchd.plist(5)
190// Use plist::Value as the value can be String, Integer, Boolean, etc.
191// Doing this precludes the use of #[derive(Eq)] on the Launchd struct, but in practice "PartialEq" is fine.
192#[cfg(feature = "plist")]
193type LaunchEvents = HashMap<String, HashMap<String, HashMap<String, Value>>>;
194
195// TODO: the current implementation is dependent on plist. Is this necessary?
196#[cfg(not(feature = "plist"))]
197type LaunchEvents = ();
198
199/// Representation of a CalendarInterval
200///
201/// Usage:
202/// ```
203/// use launchd::{CalendarInterval, Error};
204/// fn example() -> Result<(), Error> {
205///     let calendarinterval = CalendarInterval::default()
206///             .with_hour(12)?
207///             .with_minute(0)?
208///             .with_weekday(7);
209///     Ok(())
210/// }
211/// ```
212#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
213#[cfg_attr(feature = "io", serde(rename_all = "PascalCase"))]
214#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
215pub struct CalendarInterval {
216    minute: Option<u8>,
217    hour: Option<u8>,
218    day: Option<u8>,
219    weekday: Option<u8>,
220    month: Option<u8>,
221}
222
223#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
224#[derive(Debug, Clone, PartialEq, Eq, Hash)]
225pub enum InetdCompatibility {
226    Wait, // Exclude a "NoWait" as that is not a valid key.
227}
228
229// Move LoadSessionType to it's own module?
230#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
231#[cfg_attr(feature = "serde", serde(untagged))]
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub enum LoadSessionType {
234    BareString(String),
235    Array(Vec<String>),
236}
237
238impl From<String> for LoadSessionType {
239    fn from(value: String) -> Self {
240        LoadSessionType::BareString(value)
241    }
242}
243
244impl From<&str> for LoadSessionType {
245    fn from(value: &str) -> Self {
246        LoadSessionType::BareString(value.to_owned())
247    }
248}
249
250impl From<Vec<String>> for LoadSessionType {
251    fn from(value: Vec<String>) -> Self {
252        LoadSessionType::Array(value)
253    }
254}
255
256impl From<Vec<&str>> for LoadSessionType {
257    fn from(value: Vec<&str>) -> Self {
258        LoadSessionType::Array(value.into_iter().map(|s| s.to_owned()).collect())
259    }
260}
261
262// TODO: This can be generated by a macro (maybe derive_builder?)
263impl Launchd {
264    pub fn new<S: AsRef<str>, P: AsRef<Path>>(label: S, program: P) -> Result<Self, Error> {
265        let pathstr = program
266            .as_ref()
267            .to_str()
268            .ok_or(Error::PathConversion)?
269            .to_owned();
270        Ok(Launchd {
271            label: String::from(label.as_ref()),
272            program: Some(pathstr),
273            ..Default::default()
274        })
275    }
276
277    pub fn with_label<S: AsRef<str>>(mut self, label: S) -> Self {
278        self.label = String::from(label.as_ref());
279        self
280    }
281
282    pub fn with_disabled(mut self, disabled: bool) -> Self {
283        self.disabled = Some(disabled);
284        self
285    }
286
287    pub fn disabled(self) -> Self {
288        self.with_disabled(true)
289    }
290
291    pub fn with_user_name<S: AsRef<str>>(mut self, user_name: S) -> Self {
292        self.user_name = Some(String::from(user_name.as_ref()));
293        self
294    }
295
296    pub fn with_group_name<S: AsRef<str>>(mut self, group_name: S) -> Self {
297        self.group_name = Some(String::from(group_name.as_ref()));
298        self
299    }
300
301    pub fn with_program<P: AsRef<Path>>(mut self, program: P) -> Result<Self, Error> {
302        let pathstr = program
303            .as_ref()
304            .to_str()
305            .ok_or(Error::PathConversion)?
306            .to_owned();
307        self.program = Some(pathstr);
308        Ok(self)
309    }
310
311    pub fn with_bundle_program<S: AsRef<str>>(mut self, bundle: S) -> Self {
312        self.bundle_program = Some(String::from(bundle.as_ref()));
313        self
314    }
315
316    pub fn with_program_arguments(mut self, program_arguments: Vec<String>) -> Self {
317        self.program_arguments = Some(program_arguments);
318        self
319    }
320
321    pub fn with_run_at_load(mut self, run_at_load: bool) -> Self {
322        self.run_at_load = Some(run_at_load);
323        self
324    }
325
326    pub fn run_at_load(self) -> Self {
327        self.with_run_at_load(true)
328    }
329
330    pub fn with_queue_directories(mut self, queue_directories: Vec<String>) -> Self {
331        self.queue_directories = Some(queue_directories);
332        self
333    }
334
335    pub fn with_watch_paths(mut self, watch_paths: Vec<String>) -> Self {
336        self.watch_paths = Some(watch_paths);
337        self
338    }
339
340    pub fn with_start_on_mount(mut self, start_on_mount: bool) -> Self {
341        self.start_on_mount = Some(start_on_mount);
342        self
343    }
344
345    pub fn start_on_mount(self) -> Self {
346        self.with_start_on_mount(true)
347    }
348
349    pub fn with_start_interval(mut self, start_interval: u32) -> Self {
350        self.start_interval = Some(start_interval);
351        self
352    }
353
354    pub fn with_start_calendar_intervals(
355        mut self,
356        start_calendar_intervals: Vec<CalendarInterval>,
357    ) -> Self {
358        self.start_calendar_intervals = Some(start_calendar_intervals);
359        self
360    }
361
362    pub fn with_abandon_process_group(mut self, value: bool) -> Self {
363        self.abandon_process_group = Some(value);
364        self
365    }
366
367    pub fn abandon_process_group(self) -> Self {
368        self.with_abandon_process_group(true)
369    }
370
371    pub fn with_debug(mut self, value: bool) -> Self {
372        self.debug = Some(value);
373        self
374    }
375
376    pub fn debug(self) -> Self {
377        self.with_debug(true)
378    }
379
380    pub fn with_enable_globbing(mut self, value: bool) -> Self {
381        self.enable_globbing = Some(value);
382        self
383    }
384
385    pub fn enable_globbing(self) -> Self {
386        self.with_enable_globbing(true)
387    }
388
389    pub fn with_enable_transactions(mut self, value: bool) -> Self {
390        self.enable_transactions = Some(value);
391        self
392    }
393
394    pub fn enable_transactions(self) -> Self {
395        self.with_enable_transactions(true)
396    }
397
398    pub fn with_enable_pressured_exit(mut self, value: bool) -> Self {
399        self.enable_pressured_exit = Some(value);
400        self
401    }
402
403    pub fn enable_pressured_exit(self) -> Self {
404        self.with_enable_pressured_exit(true)
405    }
406
407    pub fn with_environment_variables(mut self, env: HashMap<String, String>) -> Self {
408        self.environment_variables = Some(env);
409        self
410    }
411
412    pub fn with_exit_timeout(mut self, timeout: u32) -> Self {
413        self.exit_time_out = Some(timeout);
414        self
415    }
416
417    pub fn with_init_groups(mut self, value: bool) -> Self {
418        self.init_groups = Some(value);
419        self
420    }
421
422    pub fn init_groups(self) -> Self {
423        self.with_init_groups(true)
424    }
425
426    pub fn with_launch_only_once(mut self, value: bool) -> Self {
427        self.launch_only_once = Some(value);
428        self
429    }
430
431    pub fn launch_only_once(self) -> Self {
432        self.with_launch_only_once(true)
433    }
434
435    pub fn with_limit_load_from_hosts(mut self, value: Vec<String>) -> Self {
436        self.limit_load_from_hosts = Some(value);
437        self
438    }
439
440    pub fn with_limit_to_from_hosts(mut self, value: Vec<String>) -> Self {
441        self.limit_load_to_hosts = Some(value);
442        self
443    }
444
445    pub fn with_limit_load_to_session_type(mut self, value: LoadSessionType) -> Self {
446        self.limit_load_to_session_type = Some(value);
447        self
448    }
449
450    pub fn with_limit_load_to_hardware(mut self, value: HashMap<String, Vec<String>>) -> Self {
451        self.limit_load_to_hardware = Some(value);
452        self
453    }
454
455    pub fn with_limit_load_from_hardware(mut self, value: HashMap<String, Vec<String>>) -> Self {
456        self.limit_load_from_hardware = Some(value);
457        self
458    }
459
460    pub fn with_low_priority_io(mut self, value: bool) -> Self {
461        self.low_priority_io = Some(value);
462        self
463    }
464
465    pub fn low_priority_io(self) -> Self {
466        self.with_low_priority_io(true)
467    }
468
469    pub fn with_low_priority_background_io(mut self, value: bool) -> Self {
470        self.low_priority_background_io = Some(value);
471        self
472    }
473
474    pub fn low_priority_background_io(self) -> Self {
475        self.with_low_priority_background_io(true)
476    }
477
478    pub fn with_mach_services(mut self, services: HashMap<String, MachServiceEntry>) -> Self {
479        self.mach_services = Some(services);
480        self
481    }
482
483    pub fn with_nice(mut self, nice: i32) -> Self {
484        self.nice = Some(nice);
485        self
486    }
487
488    pub fn with_root_directory<P: AsRef<Path>>(mut self, path: P) -> Result<Self, Error> {
489        let pathstr = path
490            .as_ref()
491            .to_str()
492            .ok_or(Error::PathConversion)?
493            .to_owned();
494        self.root_directory = Some(pathstr);
495        Ok(self)
496    }
497
498    pub fn with_standard_error_path<P: AsRef<Path>>(mut self, path: P) -> Result<Self, Error> {
499        let pathstr = path
500            .as_ref()
501            .to_str()
502            .ok_or(Error::PathConversion)?
503            .to_owned();
504        self.standard_error_path = Some(pathstr);
505        Ok(self)
506    }
507
508    pub fn with_standard_in_path<P: AsRef<Path>>(mut self, path: P) -> Result<Self, Error> {
509        let pathstr = path
510            .as_ref()
511            .to_str()
512            .ok_or(Error::PathConversion)?
513            .to_owned();
514        self.standard_in_path = Some(pathstr);
515        Ok(self)
516    }
517
518    pub fn with_standard_out_path<P: AsRef<Path>>(mut self, path: P) -> Result<Self, Error> {
519        let pathstr = path
520            .as_ref()
521            .to_str()
522            .ok_or(Error::PathConversion)?
523            .to_owned();
524        self.standard_out_path = Some(pathstr);
525        Ok(self)
526    }
527
528    pub fn with_throttle_interval(mut self, value: u32) -> Self {
529        self.throttle_interval = Some(value);
530        self
531    }
532
533    pub fn with_timeout(mut self, timeout: u32) -> Self {
534        self.time_out = Some(timeout);
535        self
536    }
537
538    pub fn with_umask(mut self, umask: u16) -> Self {
539        self.umask = Some(umask);
540        self
541    }
542
543    pub fn with_wait_for_debugger(mut self, value: bool) -> Self {
544        self.wait_for_debugger = Some(value);
545        self
546    }
547
548    pub fn wait_for_debugger(self) -> Self {
549        self.with_wait_for_debugger(true)
550    }
551
552    pub fn with_materialize_dataless_files(mut self, value: bool) -> Self {
553        self.materialize_dataless_files = Some(value);
554        self
555    }
556
557    pub fn materialize_dataless_files(self) -> Self {
558        self.with_materialize_dataless_files(true)
559    }
560
561    pub fn with_working_directory<P: AsRef<Path>>(mut self, path: P) -> Result<Self, Error> {
562        let pathstr = path
563            .as_ref()
564            .to_str()
565            .ok_or(Error::PathConversion)?
566            .to_owned();
567        self.working_directory = Some(pathstr);
568        Ok(self)
569    }
570
571    pub fn with_inetd_compatibility(mut self, wait: bool) -> Self {
572        self.inetd_compatibility = Some(HashMap::from([(InetdCompatibility::Wait, wait)]));
573        self
574    }
575
576    pub fn with_keep_alive(mut self, keep_alive: KeepAliveType) -> Self {
577        self.keep_alive = Some(keep_alive);
578        self
579    }
580
581    pub fn with_process_type(mut self, process_type: ProcessType) -> Self {
582        self.process_type = Some(process_type);
583        self
584    }
585
586    pub fn with_hard_resource_limits(mut self, limits: ResourceLimits) -> Self {
587        self.hard_resource_limits = Some(limits);
588        self
589    }
590
591    pub fn with_soft_resource_limits(mut self, limits: ResourceLimits) -> Self {
592        self.soft_resource_limits = Some(limits);
593        self
594    }
595
596    pub fn with_socket(mut self, socket: Sockets) -> Self {
597        if let Some(sockets) = self.sockets.take() {
598            match (sockets, socket) {
599                (Sockets::Array(mut arr), Sockets::Array(mut new_arr)) => {
600                    arr.append(&mut new_arr);
601                    self.sockets = Some(Sockets::Array(arr));
602                }
603                (Sockets::Array(mut arr), Sockets::Dictionary(new_dict)) => {
604                    arr.push(new_dict);
605                    self.sockets = Some(Sockets::Array(arr));
606                }
607                (Sockets::Dictionary(dict), Sockets::Dictionary(new_dict)) => {
608                    self.sockets = Some(Sockets::Array(vec![dict, new_dict]))
609                }
610                (Sockets::Dictionary(dict), Sockets::Array(mut new_arr)) => {
611                    new_arr.insert(0, dict);
612                    self.sockets = Some(Sockets::Array(new_arr));
613                }
614            }
615        } else {
616            self.sockets = Some(socket);
617        }
618        self
619    }
620
621    pub fn with_launch_events(mut self, value: LaunchEvents) -> Self {
622        self.launch_events = Some(value);
623        self
624    }
625
626    pub fn with_session_create(mut self, value: bool) -> Self {
627        self.session_create = Some(value);
628        self
629    }
630
631    pub fn session_create(self) -> Self {
632        self.with_session_create(true)
633    }
634}
635
636#[cfg(feature = "io")]
637impl Launchd {
638    // Write --
639    pub fn to_writer_xml<W: Write>(&self, writer: W) -> Result<(), Error> {
640        to_writer_xml(writer, self).map_err(Error::Write)
641    }
642
643    pub fn to_file_xml<P: AsRef<Path>>(&self, file: P) -> Result<(), Error> {
644        to_file_xml(file, self).map_err(Error::Write)
645    }
646
647    pub fn to_writer_binary<W: Write>(&self, writer: W) -> Result<(), Error> {
648        to_writer_binary(writer, self).map_err(Error::Write)
649    }
650
651    pub fn to_file_binary<P: AsRef<Path>>(&self, file: P) -> Result<(), Error> {
652        to_file_binary(file, self).map_err(Error::Write)
653    }
654
655    // Read --
656    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
657        from_bytes(bytes).map_err(Error::Read)
658    }
659
660    pub fn from_file<P: AsRef<Path>>(file: P) -> Result<Self, Error> {
661        from_file(file).map_err(Error::Read)
662    }
663
664    pub fn from_reader<R: Read + Seek>(reader: R) -> Result<Self, Error> {
665        from_reader(reader).map_err(Error::Read)
666    }
667
668    pub fn from_reader_xml<R: Read + Seek>(reader: R) -> Result<Self, Error> {
669        from_reader_xml(reader).map_err(Error::Read)
670    }
671}
672
673impl CalendarInterval {
674    #[cfg(feature = "cron")] // This has some use for launchd::with_start_calendar_intervals as well
675    fn is_initialized(&self) -> bool {
676        self.minute.is_some()
677            || self.hour.is_some()
678            || self.day.is_some()
679            || self.weekday.is_some()
680            || self.month.is_some()
681    }
682
683    pub fn with_minute(self, minute: u8) -> Result<Self, Error> {
684        if minute > 59 {
685            Err(Error::CalendarFieldOutOfBounds(0..=59, minute))
686        } else {
687            let mut result = self;
688            result.minute = Some(minute);
689            Ok(result)
690        }
691    }
692
693    pub fn with_hour(self, hour: u8) -> Result<Self, Error> {
694        if hour > 23 {
695            Err(Error::CalendarFieldOutOfBounds(0..=23, hour))
696        } else {
697            let mut result = self;
698            result.hour = Some(hour);
699            Ok(result)
700        }
701    }
702
703    pub fn with_day(self, day: u8) -> Result<Self, Error> {
704        if day == 0 || day > 31 {
705            Err(Error::CalendarFieldOutOfBounds(1..=31, day))
706        } else {
707            let mut result = self;
708            result.day = Some(day);
709            Ok(result)
710        }
711    }
712
713    pub fn with_weekday(self, weekday: u8) -> Result<Self, Error> {
714        if weekday > 7 {
715            Err(Error::CalendarFieldOutOfBounds(0..=7, weekday))
716        } else {
717            let mut result = self;
718            result.weekday = Some(weekday);
719            Ok(result)
720        }
721    }
722
723    pub fn with_month(self, month: u8) -> Result<Self, Error> {
724        if month == 0 || month > 12 {
725            Err(Error::CalendarFieldOutOfBounds(1..=12, month))
726        } else {
727            let mut result = self;
728            result.month = Some(month);
729            Ok(result)
730        }
731    }
732
733    #[cfg(feature = "cron")]
734    pub fn from_cron_schedule(schedule: Schedule) -> Result<Vec<Self>, Error> {
735        let mut result_vec = Vec::new();
736        for month in schedule.months().iter() {
737            for weekday in schedule.days_of_week().iter() {
738                for day in schedule.days_of_month().iter() {
739                    for hour in schedule.hours().iter() {
740                        for minute in schedule.minutes().iter() {
741                            let result = Self::default();
742
743                            // TODO: clean this mess up (thiserror + anyhow ?)
744                            if !schedule.months().is_all() {
745                                result.with_month(
746                                    month
747                                        .try_into()
748                                        .map_err(|_| Error::InvalidCronField(month))?,
749                                )?;
750                            }
751                            if !schedule.days_of_week().is_all() {
752                                result.with_weekday(
753                                    weekday
754                                        .try_into()
755                                        .map_err(|_| Error::InvalidCronField(weekday))?,
756                                )?;
757                            }
758                            if !schedule.days_of_month().is_all() {
759                                result.with_day(
760                                    day.try_into().map_err(|_| Error::InvalidCronField(day))?,
761                                )?;
762                            }
763                            if !schedule.hours().is_all() {
764                                result.with_hour(
765                                    hour.try_into().map_err(|_| Error::InvalidCronField(hour))?,
766                                )?;
767                            }
768                            if !schedule.minutes().is_all() {
769                                result.with_minute(
770                                    minute
771                                        .try_into()
772                                        .map_err(|_| Error::InvalidCronField(minute))?,
773                                )?;
774                            }
775
776                            if result.is_initialized() {
777                                result_vec.push(result);
778                            }
779
780                            if schedule.minutes().is_all() {
781                                break;
782                            }
783                        }
784                        if schedule.hours().is_all() {
785                            break;
786                        }
787                    }
788                    if schedule.days_of_month().is_all() {
789                        break;
790                    }
791                }
792                if schedule.days_of_week().is_all() {
793                    break;
794                }
795            }
796            if schedule.months().is_all() {
797                break;
798            }
799        }
800        Ok(result_vec)
801    }
802}
803
804#[cfg(test)]
805mod tests {
806
807    #[cfg(feature = "io")]
808    macro_rules! test_case {
809        ($fname:expr) => {
810            concat!(env!("CARGO_MANIFEST_DIR"), "/tests/resources/", $fname)
811        };
812    }
813
814    use super::*;
815
816    #[test]
817    fn create_valid_launchd() {
818        let check = Launchd {
819            abandon_process_group: None,
820            debug: None,
821            disabled: None,
822            enable_globbing: None,
823            enable_transactions: None,
824            enable_pressured_exit: None,
825            on_demand: None,
826            service_ipc: None,
827            environment_variables: None,
828            exit_time_out: None,
829            group_name: None,
830            inetd_compatibility: None,
831            init_groups: None,
832            hard_resource_limits: None,
833            keep_alive: None,
834            label: "Label".to_string(),
835            launch_only_once: None,
836            launch_events: None,
837            legacy_timers: None,
838            limit_load_from_hosts: None,
839            limit_load_to_hosts: None,
840            limit_load_to_session_type: None,
841            limit_load_to_hardware: None,
842            limit_load_from_hardware: None,
843            low_priority_io: None,
844            low_priority_background_io: None,
845            hopefully_exits_first: None,
846            hopefully_exits_last: None,
847            mach_services: None,
848            materialize_dataless_files: None,
849            session_create: None,
850            nice: None,
851            process_type: None,
852            program_arguments: None,
853            program: Some("./henk.sh".to_string()),
854            bundle_program: None,
855            queue_directories: None,
856            root_directory: None,
857            run_at_load: None,
858            sockets: None,
859            soft_resource_limits: None,
860            standard_error_path: None,
861            standard_in_path: None,
862            standard_out_path: None,
863            start_calendar_intervals: None,
864            start_interval: None,
865            start_on_mount: None,
866            throttle_interval: None,
867            time_out: None,
868            umask: None,
869            user_name: None,
870            wait_for_debugger: None,
871            watch_paths: None,
872            working_directory: None,
873        };
874        let test = Launchd::new("Label", "./henk.sh");
875        assert!(test.is_ok());
876        assert_eq!(test.unwrap(), check);
877    }
878
879    #[test]
880    fn create_valid_calendar_interval() {
881        let check = CalendarInterval {
882            minute: Some(5),
883            hour: Some(5),
884            day: Some(5),
885            weekday: Some(5),
886            month: Some(5),
887        };
888
889        let test = CalendarInterval::default()
890            .with_day(5)
891            .and_then(|ci| ci.with_minute(5))
892            .and_then(|ci| ci.with_hour(5))
893            .and_then(|ci| ci.with_weekday(5))
894            .and_then(|ci| ci.with_month(5));
895
896        assert!(test.is_ok());
897        assert_eq!(test.unwrap(), check);
898    }
899
900    #[test]
901    fn create_invalid_calendar_interval() {
902        let test = CalendarInterval::default()
903            .with_day(32)
904            .and_then(|ci| ci.with_minute(5))
905            .and_then(|ci| ci.with_hour(5))
906            .and_then(|ci| ci.with_weekday(5))
907            .and_then(|ci| ci.with_month(5));
908        assert!(test.is_err());
909        eprintln!("{}", test.unwrap_err());
910    }
911
912    #[test]
913    #[cfg(feature = "io")]
914    fn load_complex_launch_events_1_plist() {
915        let test = Launchd::from_file(test_case!("launchevents-1.plist")).unwrap();
916
917        match test.launch_events {
918            Some(events) => assert!(events.contains_key("com.apple.distnoted.matching")),
919            _ => panic!("No launch events found"),
920        };
921    }
922
923    #[test]
924    #[cfg(feature = "io")]
925    fn load_complex_launch_events_2_plist() {
926        let check: LaunchEvents = vec![(
927            "com.apple.iokit.matching".to_string(),
928            vec![(
929                "com.apple.device-attach".to_string(),
930                vec![
931                    ("IOMatchLaunchStream".to_string(), Value::from(true)),
932                    ("idProduct".to_string(), Value::from("*")),
933                    ("idVendor".to_string(), Value::from(4176)),
934                    ("IOProviderClass".to_string(), Value::from("IOUSBDevice")),
935                ]
936                .into_iter()
937                .collect(),
938            )]
939            .into_iter()
940            .collect(),
941        )]
942        .into_iter()
943        .collect();
944
945        let test = Launchd::from_file(test_case!("launchevents-2.plist")).unwrap();
946
947        match test.launch_events {
948            Some(events) => assert_eq!(events, check),
949            _ => panic!("No launch events found"),
950        };
951    }
952
953    #[test]
954    #[cfg(feature = "io")]
955    fn load_complex_machservices_1_plist() {
956        let check = vec![
957            (
958                "com.apple.private.alloy.accessibility.switchcontrol-idswake".to_string(),
959                MachServiceEntry::from(true),
960            ),
961            (
962                "com.apple.AssistiveControl.startup".to_string(),
963                MachServiceEntry::from(MachServiceOptions::new().reset_at_close()),
964            ),
965            (
966                "com.apple.AssistiveControl.running".to_string(),
967                MachServiceEntry::from(
968                    MachServiceOptions::new()
969                        .hide_until_check_in()
970                        .reset_at_close(),
971                ),
972            ),
973        ]
974        .into_iter()
975        .collect();
976
977        let test = Launchd::from_file(test_case!("machservices-1.plist")).unwrap();
978
979        match test.mach_services {
980            Some(events) => assert_eq!(events, check),
981            _ => panic!("No launch events found"),
982        };
983    }
984}