dikc_detector/
lib.rs

1//! Crate for finding bad Mac users.
2
3#![warn(missing_docs)]
4#![cfg(target_os = "macos")]
5
6use std::fmt::Display;
7
8use sysctl::{Ctl, Sysctl, SysctlError};
9
10/// Errors which will occur when checking Mac quality.
11#[derive(Debug)]
12#[non_exhaustive]
13pub enum Error {
14    /// The macOS version is not compliant with POSIX.
15    NotPosix,
16    /// The Mac model is bad.
17    BadMacModel,
18    /// Errors from [`sysctl`].
19    Sysctl(SysctlError),
20    /// Error when parsing macOS version.
21    ParseOsVersion,
22    /// Error variant that contains multiple errors.
23    Many(Vec<Self>),
24}
25
26impl Display for Error {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            Error::NotPosix => write!(f, "your macOS version is not compliant with POSIX, it is recommended to downgrade your macOS to a version prior to 14.4"),
30            Error::BadMacModel => write!(f, "you have a bad taste, sell your Mac immediately and get a MacBook Pro (13-inch, M1, 2020)"),
31            Error::Sysctl(err) => write!(f, "sysctl error: {}", err),
32            Error::ParseOsVersion => write!(f, "your macOS version looks weird and can't be parsed"),
33            Error::Many(errs) => {
34                write!(f, "multiple errors: ")?;
35                for err in errs {
36                    write!(f, "{}, ", err)?;
37                }
38                Ok(())
39            },
40        }
41    }
42}
43
44impl From<SysctlError> for Error {
45    #[inline]
46    fn from(value: SysctlError) -> Self {
47        Self::Sysctl(value)
48    }
49}
50
51impl std::error::Error for Error {
52    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
53        match self {
54            Error::Sysctl(err) => Some(err),
55            _ => None,
56        }
57    }
58}
59
60const HW_MODEL: &str = "hw.model";
61const KERN_OSPRODUCTVERSION: &str = "kern.osproductversion";
62
63/// Very bad machine.
64const PULP_MACHINE: &str = "MacBookPro16,1";
65
66/// Checks whether this Mac is bad.
67///
68/// # Errors
69///
70/// - Errors if macOS version is equal to or newer than __`14.4`__, which is not POSIX-compliant.
71/// - Errors if the Mac model is `MacBookPro16,1`.
72pub fn check() -> Result<(), Error> {
73    let mut errs: Vec<Error> = Vec::with_capacity(2);
74    if let Err(err) = check_posix() {
75        errs.push(err);
76    }
77    if let Err(err) = check_machine() {
78        errs.push(err);
79    }
80    if errs.is_empty() {
81        Ok(())
82    } else if let Some(err) = errs.pop().filter(|_| errs.is_empty()) {
83        Err(err)
84    } else {
85        Err(Error::Many(errs))
86    }
87}
88
89/// Checks whether macOS version is equal to or newer than __`14.4`__, which is not POSIX-compliant.
90fn check_posix() -> Result<(), Error> {
91    let ctl = Ctl::new(KERN_OSPRODUCTVERSION)?;
92    let ver_str = ctl.value_string()?;
93    let ver_split = ver_str.split('.');
94    let mut is_sonoma = false;
95    for num in ver_split {
96        if !is_sonoma {
97            match num.parse::<usize>().map_err(|_| Error::ParseOsVersion)? {
98                ..=13 => return Ok(()),
99                14 => is_sonoma = true,
100                _ => return Err(Error::NotPosix),
101            }
102        } else if let ..=3 = num.parse::<usize>().map_err(|_| Error::ParseOsVersion)? {
103            return Ok(());
104        } else {
105            return Err(Error::NotPosix);
106        }
107    }
108
109    // Can't split version string by `.` because the loop doesn't run.
110    Err(Error::ParseOsVersion)
111}
112
113fn check_machine() -> Result<(), Error> {
114    let ctl = Ctl::new(HW_MODEL)?;
115    if ctl.value_string()? == PULP_MACHINE {
116        Err(Error::BadMacModel)
117    } else {
118        Ok(())
119    }
120}
121
122#[cfg(test)]
123mod test {
124    #[test]
125    fn test_decency() {
126        assert!(crate::check().is_ok())
127    }
128}