mod constants; mod version; mod strsub; mod download; mod rules; mod assets; mod extract; mod settings; mod runner; mod jre; use std::borrow::Cow; use std::cmp::min; use std::env::consts::{ARCH, OS}; use std::error::Error; use std::ffi::{OsStr, OsString}; use std::fmt::{Display, Formatter}; use std::io::ErrorKind; use std::io::ErrorKind::AlreadyExists; use std::path::{Component, Path, PathBuf, Prefix}; use std::{env, process}; use std::env::JoinPathsError; use std::time::{Instant, SystemTime, UNIX_EPOCH}; use const_format::formatcp; use futures::{StreamExt, TryStreamExt}; use indexmap::IndexMap; use log::{debug, info, trace, warn}; use reqwest::Client; use sysinfo::System; use tokio::{fs, io}; use tokio_stream::wrappers::ReadDirStream; use download::{MultiDownloader, VerifiedDownload}; use rules::{CompatCheck, IncompatibleError}; use version::{VersionList, VersionResolveError, VersionResult}; use crate::version::{Logging, Library, OSRestriction, OperatingSystem, DownloadType, DownloadInfo, LibraryExtractRule, CompleteVersion, FeatureMatcher, ClientLogging}; use assets::{AssetError, AssetRepository}; use crate::util::{self, AsJavaPath}; pub use settings::*; pub use runner::run_the_game; pub use crate::util::{EnsureFileError, FileVerifyError, IntegrityError}; use crate::assets::AssetIndex; use runner::ArgumentType; use strsub::SubFunc; use crate::launcher::download::FileDownload; use crate::launcher::jre::{JavaRuntimeError, JavaRuntimeRepository}; use crate::version::manifest::VersionType; #[derive(Debug)] pub enum LogConfigError { UnknownType(String), InvalidId(Option), MissingURL, IO{ what: &'static str, error: io::Error }, Offline, Download{ url: String, error: reqwest::Error }, Integrity(IntegrityError) } impl Display for LogConfigError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { LogConfigError::UnknownType(log_type) => write!(f, "unknown log type {}", log_type), LogConfigError::InvalidId(oid) => match oid { Some(id) => write!(f, "invalid log config id: {}", id), None => f.write_str("missing log config id") }, LogConfigError::MissingURL => f.write_str("missing log config download URL"), LogConfigError::IO { what, error} => write!(f, "i/o error ({}): {}", what, error), LogConfigError::Offline => f.write_str("launcher in offline mode"), LogConfigError::Download { url, error } => write!(f, "failed to download log config ({}): {}", url, error), LogConfigError::Integrity(e) => write!(f, "log config verify error: {}", e) } } } impl Error for LogConfigError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { LogConfigError::IO { error, .. } => Some(error), LogConfigError::Download {error, ..} => Some(error), LogConfigError::Integrity(error) => Some(error), _ => None } } } struct SystemInfo { os: OperatingSystem, os_version: String, arch: String } struct LibraryRepository { home: PathBuf, natives: PathBuf } pub struct Launcher { online: bool, home: PathBuf, versions: VersionList, system_info: SystemInfo, libraries: LibraryRepository, assets: AssetRepository, java_runtimes: JavaRuntimeRepository } #[derive(Debug)] pub enum LaunchError { UnknownInstance(String), // version resolution errors UnknownVersion(String), LoadVersion(Box), ResolveVersion(VersionResolveError), IncompatibleVersion(IncompatibleError), MissingMainClass, // library errors LibraryDirError(PathBuf, io::Error), LibraryVerifyError(FileVerifyError), LibraryDownloadError, LibraryExtractError(extract::ZipExtractError), LibraryClasspathError(JoinPathsError), // ensure file errors EnsureFile(EnsureFileError), IO { what: &'static str, error: io::Error }, // log errors UnknownLogType(String), InvalidLogId(Option), // asset errors Assets(AssetError), // java runtime errors ResolveJavaRuntime { what: &'static str, error: io::Error }, MissingJavaRuntime, JavaRuntimeRepo(JavaRuntimeError) } impl Display for LaunchError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self { LaunchError::UnknownInstance(inst) => write!(f, "unknown instance: {inst}"), LaunchError::UnknownVersion(id) => write!(f, "unknown version id: {id}"), LaunchError::LoadVersion(e) => write!(f, "error loading remote version: {e}"), LaunchError::ResolveVersion(e) => write!(f, "error resolving remote version: {e}"), LaunchError::IncompatibleVersion(e) => e.fmt(f), LaunchError::MissingMainClass => f.write_str("main class not specified"), LaunchError::LibraryDirError(path, e) => write!(f, "failed to create library directory {}: {}", path.display(), e), LaunchError::LibraryVerifyError(e) => write!(f, "failed to verify library: {}", e), LaunchError::LibraryDownloadError => f.write_str("library download failed (see above logs for details)"), // TODO: booo this sucks LaunchError::LibraryExtractError(e) => write!(f, "library extract zip error: {e}"), LaunchError::LibraryClasspathError(e) => write!(f, "error building classpath: {e}"), LaunchError::IO { what, error } => write!(f, "i/o error ({}): {}", what, error), LaunchError::EnsureFile(e) => e.fmt(f), LaunchError::UnknownLogType(t) => write!(f, "unknown log type: {}", t), LaunchError::InvalidLogId(Some(id)) => write!(f, "invalid log id: {}", id), LaunchError::InvalidLogId(None) => write!(f, "missing log id"), LaunchError::Assets(e) => write!(f, "failed to fetch assets: {}", e), LaunchError::ResolveJavaRuntime { what, error } => write!(f, "failed to find java runtime ({}): {}", what, error), LaunchError::MissingJavaRuntime => f.write_str("suitable java executable not found"), LaunchError::JavaRuntimeRepo(e) => write!(f, "runtime repository error: {e}") } } } impl Error for LaunchError { fn cause(&self) -> Option<&dyn Error> { match &self { LaunchError::LoadVersion(e) => Some(e.as_ref()), LaunchError::ResolveVersion(e) => Some(e), LaunchError::IncompatibleVersion(e) => Some(e), LaunchError::LibraryDirError(_, e) => Some(e), LaunchError::LibraryVerifyError(e) => Some(e), LaunchError::LibraryExtractError(e) => Some(e), LaunchError::LibraryClasspathError(e) => Some(e), LaunchError::IO { error: e, .. } => Some(e), LaunchError::EnsureFile(e) => Some(e), LaunchError::Assets(e) => Some(e), LaunchError::ResolveJavaRuntime { error: e, .. } => Some(e), LaunchError::JavaRuntimeRepo(e) => Some(e), _ => None } } } struct LaunchInfo<'l, F: FeatureMatcher> { launcher: &'l Launcher, feature_matcher: &'l F, asset_index_name: Option, classpath: String, virtual_assets_path: Option, instance_home: PathBuf, natives_path: PathBuf, client_jar: Option, version_id: String, version_type: Option, asset_index: Option } #[derive(Debug)] pub struct Launch { jvm_args: Vec, game_args: Vec, main_class: String, instance_path: PathBuf, runtime_path: PathBuf, runtime_legacy_launch: bool } struct ProfileFeatureMatcher<'prof> { profile: &'prof Profile } impl FeatureMatcher for ProfileFeatureMatcher<'_> { fn matches(&self, feature: &str) -> bool { match feature { "has_custom_resolution" => self.profile.get_resolution().is_some(), _ => false } } } impl Launcher { // FIXME: more descriptive error type por favor pub async fn new(home: impl AsRef, online: bool) -> Result> { match tokio::fs::create_dir_all(home.as_ref()).await { Err(e) if e.kind() != AlreadyExists => { warn!("Failed to create launcher home directory: {}", e); return Err(e.into()); }, _ => () } let home = fs::canonicalize(home.as_ref()).await?; let versions_home = home.join("versions"); let versions; debug!("Version list online?: {online}"); if online { versions = VersionList::online(versions_home.as_ref()).await?; } else { versions = VersionList::offline(versions_home.as_ref()).await?; } let assets_path = home.join("assets"); let java_runtimes = JavaRuntimeRepository::new(home.join("jre"), online).await.map_err(LaunchError::JavaRuntimeRepo)?; Ok(Launcher { online, versions, system_info: SystemInfo::new(), libraries: LibraryRepository { home: home.join("libraries"), natives: home.join("natives") }, assets: AssetRepository::new(online, &assets_path).await?, java_runtimes, home }) } fn choose_lib_classifier<'lib>(&self, lib: &'lib Library) -> Option<&'lib str> { lib.natives.as_ref().map_or(None, |n| n.get(&self.system_info.os)).map(|s| s.as_str()) } async fn log_config_ensure(&self, config: &ClientLogging) -> Result { info!("Ensuring log configuration exists and is valid."); if config.log_type != "log4j2-xml" { return Err(LaunchError::UnknownLogType(config.log_type.clone())); } let dlinfo = &config.file; let Some(id) = dlinfo.id.as_ref() else { return Err(LaunchError::InvalidLogId(None)); }; let mut path = self.home.join("logging"); fs::create_dir_all(path.as_path()).await .map_err(|e| LaunchError::IO{ what: "creating log directory", error: e })?; let Some(Component::Normal(filename)) = Path::new(id).components().last() else { return Err(LaunchError::InvalidLogId(Some(id.clone()))); }; path.push(filename); debug!("Logger config {} is at {}", id, path.display()); util::ensure_file(&path, dlinfo.url.as_ref().map(|s| s.as_str()), dlinfo.size, dlinfo.sha1, self.online, false).await .map_err(|e| LaunchError::EnsureFile(e))?; struct PathSub<'a>(&'a Path); impl<'a> SubFunc<'a> for PathSub<'a> { fn substitute(&self, key: &str) -> Option> { match key { "path" => Some(self.0.to_string_lossy()), _ => None } } } Ok(strsub::replace_string(config.argument.as_str(), &PathSub(path.as_ref())).to_string()) } /* TODO: * - launch game using JNI * - auth */ pub async fn prepare_launch(&self, profile: &Profile, instance: &Instance) -> Result { let start = Instant::now(); let feature_matcher = ProfileFeatureMatcher { profile }; let version_id = profile.get_version(); let Some(version_id) = self.versions.get_profile_version_id(version_id) else { // idk how common this use case actually is warn!("Can't use latest release/snapshot profiles while offline!"); return Err(LaunchError::UnknownVersion("".into())); }; info!("Preparing launch for \"{}\"...", version_id); let inst_home = instance.get_path(&self.home).await.map_err(|e| LaunchError::IO { what: "resolving instance directory", error: e })?; fs::create_dir_all(inst_home.as_path()).await.map_err(|e| LaunchError::IO { what: "creating instance directory", error: e })?; info!("Launching the game in {}", inst_home.display()); let ver_res = self.versions.get_version_lazy(version_id.as_ref()); let ver = match ver_res { VersionResult::Remote(mv) => Cow::Owned(self.versions.load_remote_version(mv).await.map_err(|e| LaunchError::LoadVersion(e))?), VersionResult::Complete(cv) => Cow::Borrowed(cv), VersionResult::None => { return Err(LaunchError::UnknownVersion(version_id.into_owned()).into()) } }; let ver = self.versions.resolve_version(ver.as_ref()).await.map_err(|e| LaunchError::ResolveVersion(e))?; ver.rules_apply(&self.system_info, &feature_matcher).map_err(|e| LaunchError::IncompatibleVersion(e))?; info!("Resolved launch version {}!", ver.id); let mut extract_jobs = Vec::new(); let mut downloads = IndexMap::new(); for lib in ver.libraries.iter() { if lib.rules_apply(&self.system_info, &feature_matcher).is_err() { trace!("Skipping library {}, compatibility rules failed", lib.name); continue; } let classifier = self.choose_lib_classifier(lib); if let Some(dl) = self.libraries.create_download(lib, classifier) { let canon_name = lib.get_canonical_name(); if downloads.contains_key(&canon_name) { debug!("Skipping library {}, we already have another version of that library.", lib.name); continue; } trace!("Using library {} ({})", lib.name, classifier.unwrap_or("None")); dl.make_dirs().await.map_err(|e| LaunchError::LibraryDirError(dl.get_path().to_path_buf(), e))?; if lib.natives.is_some() { extract_jobs.push(LibraryExtractJob { source: dl.get_path().to_owned(), rule: lib.extract.clone() }); } downloads.insert(canon_name, dl); } else { trace!("Skipping library {} ({}), no download", lib.name, classifier.unwrap_or("None")); } } if self.online { info!("Downloading {} libraries...", downloads.len()); let client = Client::new(); MultiDownloader::new(downloads.values_mut()).perform(&client).await .inspect_err(|e| warn!("library download failed: {e}")) .try_fold((), |_, _| async {Ok(())}) .await .map_err(|_| LaunchError::LibraryDownloadError)?; } else { info!("Verifying {} libraries...", downloads.len()); download::verify_files(downloads.values_mut()).await.map_err(|e| { warn!("A library could not be verified: {}", e); warn!("Since the launcher is in offline mode, libraries cannot be downloaded. Please try again in online mode."); LaunchError::LibraryVerifyError(e) })?; } let log_arg; if let Some(logging) = ver.logging.as_ref().map_or(None, |l| l.client.as_ref()) { log_arg = Some(self.log_config_ensure(logging).await?); } else { log_arg = None; } // download assets let (asset_idx_name, asset_idx) = if let Some(idx_download) = ver.asset_index.as_ref() { let asset_idx_name = idx_download.id.as_ref().or(ver.assets.as_ref()).map(String::as_str); let asset_idx = self.assets.load_index(idx_download, asset_idx_name).await .map_err(|e| LaunchError::Assets(e))?; self.assets.ensure_assets(&asset_idx).await.map_err(|e| LaunchError::Assets(e))?; (asset_idx_name, Some(asset_idx)) } else { (None, None) }; // download client jar let client_jar_path; if let Some(client) = ver.downloads.get(&DownloadType::Client) { let mut client_path: PathBuf = [self.home.as_ref(), OsStr::new("versions"), OsStr::new(&ver.id)].iter().collect(); fs::create_dir_all(&client_path).await.map_err(|e| LaunchError::IO{ what: "creating client download directory", error: e })?; client_path.push(format!("{}.jar", ver.id)); info!("Downloading client jar {}", client_path.display()); util::ensure_file(client_path.as_path(), client.url.as_ref().map(|s| s.as_str()), client.size, client.sha1, self.online, false).await .map_err(|e| LaunchError::EnsureFile(e))?; client_jar_path = Some(client_path); } else { client_jar_path = None; } // clean up old natives let nnatives = self.libraries.clean_old_natives().await?; info!("Cleaned up {} old natives directories.", nnatives); // extract natives (execute this function unconditionally because we still need the natives dir to exist) info!("Extracting natives from libraries"); let natives_dir = self.libraries.extract_natives(extract_jobs).await?; let game_assets = if let Some(asset_idx) = asset_idx.as_ref() { info!("Reconstructing assets"); self.assets.reconstruct_assets(asset_idx, inst_home.as_path(), asset_idx_name).await .map_err(|e| LaunchError::Assets(e))? } else { None }; info!("Building classpath"); let classpath = env::join_paths(downloads.values() .map(|job| job.get_path().as_java_path()) .chain(client_jar_path.iter().map(|p| p.as_path().as_java_path()))) .map_err(|e| LaunchError::LibraryClasspathError(e))? .into_string() .unwrap_or_else(|os| { warn!("Classpath contains invalid UTF-8. The game may not launch correctly."); os.to_string_lossy().to_string() }); trace!("Classpath: {classpath}"); info!("Resolving java runtime environment path"); let runtime_path; if let Some(ref profile_jre) = profile.get_java_runtime() { runtime_path = fs::canonicalize(profile_jre).await .map_err(|e| LaunchError::ResolveJavaRuntime {what: "resolving jre path", error: e})?; } else { let Some(ref java_ver) = ver.java_version else { warn!("Version {} does not specify java version information. You must select a runtime manually.", ver.id); return Err(LaunchError::MissingJavaRuntime); }; let runtime = self.java_runtimes.choose_runtime(java_ver.component.as_str()).await.map_err(LaunchError::JavaRuntimeRepo)?; runtime_path = self.java_runtimes.ensure_jre(java_ver.component.as_str(), runtime).await.map_err(LaunchError::JavaRuntimeRepo)?; } let Some(runtime_exe_path) = runner::find_java(runtime_path.as_path(), profile.is_legacy_launch()).await .map_err(|e| LaunchError::ResolveJavaRuntime {what: "finding java executable", error: e})? else { return Err(LaunchError::MissingJavaRuntime); }; debug!("Found runtime exe: {}", runtime_exe_path.display()); info!("Deriving launch arguments"); let info = LaunchInfo { launcher: self, feature_matcher: &feature_matcher, asset_index_name: asset_idx_name.map(|s| s.to_owned()), classpath, virtual_assets_path: game_assets, instance_home: inst_home.clone(), natives_path: natives_dir, client_jar: client_jar_path, version_id: ver.id.to_string(), version_type: ver.version_type.clone(), asset_index: asset_idx }; let Some(ref main_class) = ver.main_class else { return Err(LaunchError::MissingMainClass); }; // yuck let jvm_args = profile.iter_arguments().map(OsString::from) .chain(runner::build_arguments(&info, ver.as_ref(), ArgumentType::JVM).drain(..)) .chain(log_arg.iter().map(OsString::from)).collect(); let game_args = runner::build_arguments(&info, ver.as_ref(), ArgumentType::Game); let diff = Instant::now().duration_since(start); info!("Finished preparing launch for {} in {:.02} seconds!", ver.id, diff.as_secs_f32()); Ok(Launch { jvm_args, game_args, main_class: main_class.to_string(), instance_path: inst_home, runtime_path: runtime_exe_path, runtime_legacy_launch: profile.is_legacy_launch() }) } } #[derive(Debug)] enum LibraryError { InvalidName(String), IO { what: &'static str, error: io::Error } } impl Display for LibraryError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { LibraryError::InvalidName(name) => write!(f, "invalid name: {name}"), LibraryError::IO { what, error } => write!(f, "library i/o error ({what}): {error}"), } } } impl Error for LibraryError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { LibraryError::IO { error, .. } => Some(error), _ => None } } } #[derive(Debug)] struct LibraryExtractJob { source: PathBuf, rule: Option } const ARCH_BITS: &'static str = formatcp!("{}", usize::BITS); impl LibraryRepository { fn get_artifact_base_dir(name: &str) -> Option { let end_of_gid = name.find(':')?; Some(name[..end_of_gid].split('.').chain(name.split(':').skip(1).take(2)).collect()) } fn get_artifact_filename(name: &str, classifier: Option<&str>) -> Option { let n: Vec<&str> = name.splitn(4, ':').skip(1).collect(); struct LibReplace; impl SubFunc<'static> for LibReplace { fn substitute(&self, key: &str) -> Option> { match key { "arch" => Some(Cow::Borrowed(ARCH_BITS)), _ => None } } } if let Some(classifier) = classifier { match n.len() { 2 => Some(PathBuf::from(strsub::replace_string(format!("{}-{}-{}.jar", n[0], n[1], classifier).as_str(), &LibReplace).as_ref())), 3 => Some(PathBuf::from(strsub::replace_string(format!("{}-{}-{}-{}.jar", n[0], n[1], classifier, n[2]).as_str(), &LibReplace).as_ref())), _ => None } } else { match n.len() { 2 => Some(PathBuf::from(strsub::replace_string(format!("{}-{}.jar", n[0], n[1]).as_str(), &LibReplace).as_ref())), 3 => Some(PathBuf::from(strsub::replace_string(format!("{}-{}-{}.jar", n[0], n[1], n[2]).as_str(), &LibReplace).as_ref())), _ => None } } } fn get_artifact_path(name: &str, classifier: Option<&str>) -> Option { let Some(mut p) = Self::get_artifact_base_dir(name) else { return None; }; p.push(Self::get_artifact_filename(name, classifier)?); Some(p) } fn create_download(&self, lib: &Library, classifier: Option<&str>) -> Option { if let Some(ref url) = lib.url { let path = Self::get_artifact_path(lib.name.as_str(), classifier)?; let url = [url.as_str(), path.to_string_lossy().as_ref()].into_iter().collect::(); Some(VerifiedDownload::new(url.as_ref(), self.home.join(path).as_path(), lib.size, lib.sha1)) // TODO: could download sha1 } else if let Some(ref downloads) = lib.downloads { let dlinfo = downloads.get_download_info(classifier)?; // drinking game: take a shot once per heap allocation let path = self.home.join(dlinfo.path.as_ref().map(PathBuf::from).or_else(|| Self::get_artifact_path(lib.name.as_str(), classifier))?); Some(VerifiedDownload::new(dlinfo.url.as_ref()?, path.as_path(), dlinfo.size, dlinfo.sha1)) } else { let path = Self::get_artifact_path(lib.name.as_str(), classifier)?; let url = ["https://libraries.minecraft.net/", path.to_string_lossy().as_ref()].into_iter().collect::(); Some(VerifiedDownload::new(url.as_ref(), self.home.join(path).as_path(), lib.size, lib.sha1)) // TODO: could download sha1 } } async fn clean_old_natives(&self) -> Result { info!("Cleaning up old natives folders..."); let boot_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() - min(System::uptime(), 7u64*24*60*60); let readdir = match fs::read_dir(&self.natives).await { Ok(readdir) => readdir, Err(e) if e.kind() == ErrorKind::NotFound => return Ok(0), Err(e) => return Err(LaunchError::IO { what: "reading natives directory", error: e }) }; ReadDirStream::new(readdir) .map(|entry| Ok(async move { let entry = entry.map_err(|e| LaunchError::IO { what: "reading natives entry", error: e })?; let ftype = entry.file_type().await.map_err(|e| LaunchError::IO { what: "'stat'ing natives entry", error: e })?; if !ftype.is_dir() { return Ok(false); } let Some(ftime) = entry.file_name().to_str() .map_or(None, |s| constants::NATIVES_DIR_PATTERN.captures(s)) .map_or(None, |c| c.get(1)) .map_or(None, |cap| cap.as_str().parse::().ok()) else { return Ok(false); }; if ftime < boot_time { let path = entry.path(); info!("Deleting old natives directory {}", path.display()); fs::remove_dir_all(&path).await.map_err(|e| LaunchError::IO { what: "reading natives entry", error: e })?; return Ok(true); } Ok(false) })) .try_buffer_unordered(32) .try_fold(0usize, |accum, res| async move { match res { true => Ok(accum + 1), _ => Ok(accum) } }).await } async fn extract_natives<'lib>(&self, libs: Vec) -> Result { let libs = libs; fs::create_dir_all(&self.natives).await.map_err(|e| LaunchError::IO { what: "creating natives directory", error: e })?; let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); let natives_dir = self.natives.join(format!("{}{}-{}", constants::NATIVES_PREFIX, time, process::id())); // create_dir_all suppresses "AlreadyExists", but this is a fatal error here. fs::create_dir(&natives_dir).await.map_err(|e| LaunchError::IO { what: "creating natives directory", error: e })?; let (path_again, extracted) = tokio::task::spawn_blocking(move || { let mut tally = 0usize; for job in libs { debug!("Extracting natives for {}", job.source.display()); tally += extract::extract_zip(&job.source, &natives_dir, |name| job.rule.as_ref().is_none_or(|rules| rules.exclude.iter().filter(|ex| name.starts_with(ex.as_str())).next().is_none()))?; } Ok((natives_dir, tally)) }).await.unwrap().map_err(LaunchError::LibraryExtractError)?; info!("Done extracting natives! Copied {} files.", extracted); Ok(path_again) } } impl SystemInfo { fn new() -> SystemInfo { let os = match OS { "windows" => OperatingSystem::Windows, "macos" => OperatingSystem::MacOS, "linux" => OperatingSystem::Linux, _ => OperatingSystem::Unknown // could probably consider "hurd" and "*bsd" to be linux... }; let mut os_version = System::os_version().unwrap_or_default(); if os == OperatingSystem::Windows && (os_version.starts_with("10") || os_version.starts_with("11")) { os_version.replace_range(..2, "10.0"); // minecraft expects this funny business... } let mut arch = ARCH.to_owned(); if arch == "x86_64" { // this nomenclature is preferred, since some versions expect the arch containing "x86" to mean 32-bit. arch.replace_range(.., "amd64"); } SystemInfo { os, os_version, arch } } fn is_our_os(&self, os: OperatingSystem) -> bool { if self.os == OperatingSystem::Unknown { return false; } self.os == os } fn applies(&self, restriction: &OSRestriction) -> bool { restriction.os.is_none_or(|os| self.is_our_os(os)) && restriction.version.as_deref().is_none_or(|pat| pat.is_match(&self.os_version)) && restriction.arch.as_deref().is_none_or(|pat| pat.is_match(&self.arch)) } }