diff options
Diffstat (limited to 'src/launcher')
| -rw-r--r-- | src/launcher/assets.rs | 322 | ||||
| -rw-r--r-- | src/launcher/constants.rs | 18 | ||||
| -rw-r--r-- | src/launcher/download.rs | 267 | ||||
| -rw-r--r-- | src/launcher/extract.rs | 136 | ||||
| -rw-r--r-- | src/launcher/jre.rs | 330 | ||||
| -rw-r--r-- | src/launcher/jre/arch.rs | 45 | ||||
| -rw-r--r-- | src/launcher/jre/download.rs | 195 | ||||
| -rw-r--r-- | src/launcher/jre/manifest.rs | 65 | ||||
| -rw-r--r-- | src/launcher/rules.rs | 114 | ||||
| -rw-r--r-- | src/launcher/runner.rs | 222 | ||||
| -rw-r--r-- | src/launcher/settings.rs | 232 | ||||
| -rw-r--r-- | src/launcher/strsub.rs | 192 | ||||
| -rw-r--r-- | src/launcher/version.rs | 398 |
13 files changed, 0 insertions, 2536 deletions
diff --git a/src/launcher/assets.rs b/src/launcher/assets.rs deleted file mode 100644 index 7c5dcf3..0000000 --- a/src/launcher/assets.rs +++ /dev/null @@ -1,322 +0,0 @@ -use std::error::Error; -use std::ffi::OsStr; -use std::fmt::{Display, Formatter}; -use std::io::ErrorKind; -use std::path::{Path, PathBuf}; -use std::path::Component::Normal; -use futures::{stream, TryStreamExt}; -use log::{debug, info, warn}; -use reqwest::Client; -use sha1_smol::Sha1; -use tokio::{fs, io}; -use tokio::fs::File; -use crate::assets::{Asset, AssetIndex}; -use crate::launcher::download::{MultiDownloader, VerifiedDownload}; -use crate::util; -use crate::util::{FileVerifyError, IntegrityError}; -use crate::version::DownloadInfo; - -const INDEX_PATH: &str = "indexes"; -const OBJECT_PATH: &str = "objects"; - -pub struct AssetRepository { - online: bool, - home: PathBuf -} - -#[derive(Debug)] -pub enum AssetError { - InvalidId(Option<String>), - IO { what: &'static str, error: io::Error }, - IndexParse(serde_json::Error), - Offline, - MissingURL, - DownloadIndex(reqwest::Error), - Integrity(IntegrityError), - AssetObjectDownload, - AssetVerifyError(FileVerifyError), - AssetNameError(&'static str) -} - -impl Display for AssetError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - AssetError::InvalidId(None) => f.write_str("missing asset index id"), - AssetError::InvalidId(Some(id)) => write!(f, "invalid asset index id: {}", id), - AssetError::IO { what, error } => write!(f, "i/o error ({}): {}", what, error), - AssetError::IndexParse(error) => write!(f, "error parsing asset index: {}", error), - AssetError::Offline => f.write_str("cannot download asset index while offline"), - AssetError::MissingURL => f.write_str("missing asset index URL"), - AssetError::DownloadIndex(e) => write!(f, "error downloading asset index: {}", e), - AssetError::Integrity(e) => write!(f, "asset index integrity error: {}", e), - AssetError::AssetObjectDownload => f.write_str("asset object download failed"), - AssetError::AssetVerifyError(e) => write!(f, "error verifying asset object: {e}"), - AssetError::AssetNameError(e) => write!(f, "invalid asset name: {e}") - } - } -} - -impl Error for AssetError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - AssetError::IO { error, .. } => Some(error), - AssetError::IndexParse(error) => Some(error), - AssetError::DownloadIndex(error) => Some(error), - AssetError::Integrity(error) => Some(error), - AssetError::AssetVerifyError(error) => Some(error), - _ => None - } - } -} - -impl From<(&'static str, io::Error)> for AssetError { - fn from((what, error): (&'static str, io::Error)) -> Self { - AssetError::IO { what, error } - } -} - -impl AssetRepository { - pub async fn new(online: bool, home: impl AsRef<Path>) -> Result<AssetRepository, io::Error> { - let home = home.as_ref().to_owned(); - - match fs::create_dir_all(&home).await { - Ok(_) => (), - Err(e) => match e.kind() { - ErrorKind::AlreadyExists => (), - _ => return Err(e) - } - }; - - Ok(AssetRepository { - online, - home - }) - } - - pub fn get_home(&self) -> &Path { - self.home.as_path() - } - - fn get_index_path(&self, id: &str) -> Result<PathBuf, AssetError> { - let mut indexes_path: PathBuf = [self.home.as_ref(), OsStr::new(INDEX_PATH)].iter().collect(); - let Some(Normal(path)) = Path::new(id).components().last() else { - return Err(AssetError::InvalidId(Some(id.into()))); - }; - - let path = path.to_str().ok_or(AssetError::InvalidId(Some(path.to_string_lossy().into())))?; - - // FIXME: change this once "add_extension" is stabilized - indexes_path.push(format!("{}.json", path)); - - Ok(indexes_path) - } - - pub async fn load_index(&self, index: &DownloadInfo, id: Option<&str>) -> Result<AssetIndex, AssetError> { - let Some(id) = id else { - return Err(AssetError::InvalidId(None)); - }; - - info!("Loading asset index {}", id); - - let path = self.get_index_path(id)?; - debug!("Asset index {} is located at {}", id, path.display()); - - match util::verify_file(&path, index.size, index.sha1).await { - Ok(_) => { - debug!("Asset index {} verified on disk. Loading it.", id); - let idx_data = fs::read_to_string(&path).await.map_err(|e| AssetError::IO { - what: "reading asset index", - error: e - })?; - - return serde_json::from_str(&idx_data).map_err(AssetError::IndexParse); - }, - Err(FileVerifyError::Open(_, e)) => match e.kind() { - ErrorKind::NotFound => { - debug!("Asset index {} not found on disk. Must download it.", id); - }, - _ => return Err(("opening asset index", e).into()) - }, - Err(FileVerifyError::Integrity(_, e)) => { - info!("Asset index {} has mismatched integrity: {}, must download it.", id, e); - let _ = fs::remove_file(&path).await.map_err(|e| warn!("Error deleting modified index {}: {} (ignored)", id, e)); - }, - Err(FileVerifyError::Read(_, e)) => return Err(("reading asset index", e).into()) - } - - if !self.online { - warn!("Must download asset index {}, but the launcher is in offline mode. Please try again in online mode.", id); - return Err(AssetError::Offline); - } - - let Some(url) = index.url.as_ref() else { - return Err(AssetError::MissingURL); - }; - - debug!("Downloading asset index {} from {}", id, url); - - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).await.map_err(|e| AssetError::IO { - what: "creating asset index folder", - error: e - })?; - } - - let idx_text = reqwest::get(url).await - .map_err(AssetError::DownloadIndex)? - .text().await - .map_err(AssetError::DownloadIndex)?; - - if index.size.is_some_and(|s| s != idx_text.len()) { - return Err(AssetError::Integrity(IntegrityError::SizeMismatch { - expect: index.size.unwrap(), - actual: idx_text.len() - })); - } - - if let Some(expect) = index.sha1 { - let actual = Sha1::from(&idx_text).digest(); - - if actual != expect { - return Err(AssetError::Integrity(IntegrityError::Sha1Mismatch { expect, actual })); - } - } - - debug!("Saving downloaded asset index to {}", path.display()); - fs::write(&path, &idx_text).await.map_err(|e| AssetError::IO { - what: "writing asset index", - error: e - })?; - - serde_json::from_str(&idx_text).map_err(AssetError::IndexParse) - } - - fn get_object_url(obj: &Asset) -> String { - format!("{}{:02x}/{}", super::constants::URL_RESOURCE_BASE, obj.hash.bytes()[0], obj.hash) - } - - pub fn get_object_path(&self, obj: &Asset) -> PathBuf { - let hex_digest = obj.hash.to_string(); - [self.home.as_ref(), OsStr::new(OBJECT_PATH), OsStr::new(&hex_digest[..2]), OsStr::new(&hex_digest)].iter().collect() - } - - async fn ensure_dir(path: impl AsRef<Path>) -> Result<(), io::Error> { - match fs::create_dir(path).await { - Ok(_) => Ok(()), - Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(()), - Err(e) => Err(e) - } - } - - pub async fn ensure_assets(&self, index: &AssetIndex) -> Result<(), AssetError> { - let mut downloads = Vec::new(); - let objects_path = [self.home.as_ref(), OsStr::new(OBJECT_PATH)].iter().collect::<PathBuf>(); - - Self::ensure_dir(&objects_path).await.map_err(|e| AssetError::IO { - what: "creating objects directory", - error: e - })?; - - for object in index.objects.values() { - let path = self.get_object_path(object); - - Self::ensure_dir(path.parent().unwrap()).await.map_err(|error| AssetError::IO { error, what: "creating directory for object" })?; - - downloads.push(VerifiedDownload::new(&Self::get_object_url(object), &path, Some(object.size), Some(object.hash))); - } - - if self.online { - info!("Downloading {} asset objects...", downloads.len()); - let client = Client::new(); - MultiDownloader::with_concurrent(downloads.iter_mut(), 32).perform(&client).await - .inspect_err(|e| warn!("asset download failed: {e}")) - .try_fold((), |_, _| async {Ok(())}) - .await - .map_err(|_| AssetError::AssetObjectDownload)?; - } else { - info!("Verifying {} asset objects...", downloads.len()); - super::download::verify_files(downloads.iter_mut()).await.map_err(AssetError::AssetVerifyError)?; - } - - Ok(()) - } - - pub async fn reconstruct_assets(&self, index: &AssetIndex, instance_path: &Path, index_id: Option<&str>) -> Result<Option<PathBuf>, AssetError> { - let target_path: PathBuf; - let Some(index_id) = index_id else { - return Err(AssetError::InvalidId(None)); - }; - - if index.virtual_assets { - target_path = [self.home.as_ref(), OsStr::new("virtual"), OsStr::new(index_id)].iter().collect(); - } else if index.map_to_resources { - target_path = [instance_path, Path::new("resources")].iter().collect(); - } else { - info!("This asset index does not request a virtual assets folder. Nothing to be done."); - return Ok(None); - } - - info!("Reconstructing virtual assets for {}", index_id); - - fs::create_dir_all(&target_path).await.map_err(|e| AssetError::from(("creating virtual assets directory", e)))?; - - stream::iter(index.objects.values() - .map(|object| { - let obj_path = util::check_path(object.name.as_str()).map_err(AssetError::AssetNameError)?; - let obj_path = target_path.join(obj_path); - - Ok((object, obj_path)) - })) - .try_filter_map(|(object, obj_path)| async move { - match util::verify_file(&obj_path, Some(object.size), Some(object.hash)).await { - Ok(_) => { - debug!("Not copying asset {}, integrity matches.", object.name); - Ok(None) - } - Err(FileVerifyError::Open(_, e)) if e.kind() == ErrorKind::NotFound => { - debug!("Copying asset {}, file does not exist.", object.name); - Ok(Some((object, obj_path))) - }, - Err(FileVerifyError::Integrity(_, e)) => { - debug!("Copying asset {}: {}", object.name, e); - Ok(Some((object, obj_path))) - }, - Err(e) => { - debug!("Error while reconstructing assets: {e}"); - Err(AssetError::AssetVerifyError(e)) - } - } - }) - .try_for_each_concurrent(32, |(object, obj_path)| async move { - if let Some(parent) = obj_path.parent() { - fs::create_dir_all(parent).await - .inspect_err(|e| debug!("Error creating directory for asset object {}: {e}", object.name)) - .map_err(|e| AssetError::from(("creating asset object directory", e)))?; - } - - let mut fromfile = File::open(self.get_object_path(object)).await - .map_err(|e| AssetError::from(("opening source object", e)))?; - let mut tofile = File::create(&obj_path).await - .map_err(|e| AssetError::from(("creating target object", e)))?; - - io::copy(&mut fromfile, &mut tofile).await.map_err(|e| AssetError::from(("copying asset object", e)))?; - debug!("Copied object {} to {}.", object.name, obj_path.display()); - Ok(()) - }).await.map(|_| Some(target_path)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_it() { - let digest_str = "ad1115931887a73cd596300f2c93f84adf39521d"; - assert_eq!(AssetRepository::get_object_url(&Asset { - name: String::from("test"), - hash: digest_str.parse().unwrap(), - size: 0usize - }), "https://resources.download.minecraft.net/ad/ad1115931887a73cd596300f2c93f84adf39521d"); - } -} diff --git a/src/launcher/constants.rs b/src/launcher/constants.rs deleted file mode 100644 index 4506ab5..0000000 --- a/src/launcher/constants.rs +++ /dev/null @@ -1,18 +0,0 @@ -use lazy_static::lazy_static; -use regex::Regex; - -pub const URL_VERSION_MANIFEST: &str = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; -pub const URL_RESOURCE_BASE: &str = "https://resources.download.minecraft.net/"; -pub const URL_JRE_MANIFEST: &str = "https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"; - -pub const NATIVES_PREFIX: &str = "natives-"; - -pub const DEF_INSTANCE_NAME: &str = "default"; -pub const DEF_PROFILE_NAME: &str = "default"; - -// https://github.com/unmojang/FjordLauncher/pull/14/files -// https://login.live.com/oauth20_authorize.srf?client_id=00000000402b5328&redirect_uri=ms-xal-00000000402b5328://auth&response_type=token&display=touch&scope=service::user.auth.xboxlive.com::MBI_SSL%20offline_access&prompt=select_account - -lazy_static! { - pub static ref NATIVES_DIR_PATTERN: Regex = Regex::new("^natives-(\\d+)").unwrap(); -} diff --git a/src/launcher/download.rs b/src/launcher/download.rs deleted file mode 100644 index 132cd7f..0000000 --- a/src/launcher/download.rs +++ /dev/null @@ -1,267 +0,0 @@ -use std::error::Error; -use std::fmt::{Debug, Display, Formatter}; -use std::path::{Path, PathBuf}; -use futures::{stream, StreamExt, TryStream, TryStreamExt}; -use log::debug; -use reqwest::{Client, Method, RequestBuilder}; -use sha1_smol::{Digest, Sha1}; -use tokio::fs; -use tokio::fs::File; -use tokio::io::{self, AsyncWriteExt}; -use crate::util; -use crate::util::{FileVerifyError, IntegrityError, USER_AGENT}; - -pub trait Download: Debug + Display { - // return Ok(None) to skip downloading this file - async fn prepare(&mut self, client: &Client) -> Result<Option<RequestBuilder>, Box<dyn Error>>; - async fn handle_chunk(&mut self, chunk: &[u8]) -> Result<(), Box<dyn Error>>; - async fn finish(&mut self) -> Result<(), Box<dyn Error>>; -} - -pub trait FileDownload: Download { - fn get_path(&self) -> &Path; -} - -pub struct MultiDownloader<'j, T: Download + 'j, I: Iterator<Item = &'j mut T>> { - jobs: I, - nconcurrent: usize -} - -#[derive(Debug, Clone, Copy)] -pub enum Phase { - Prepare, - Send, - Receive, - HandleChunk, - Finish -} - -impl Display for Phase { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - /* an error occurred while (present participle) ... */ - Self::Prepare => f.write_str("preparing the request"), - Self::Send => f.write_str("sending the request"), - Self::Receive => f.write_str("receiving response data"), - Self::HandleChunk => f.write_str("handling response data"), - Self::Finish => f.write_str("finishing the request"), - } - } -} - -pub struct PhaseDownloadError<'j, T: Download> { - phase: Phase, - inner: Box<dyn Error>, - job: &'j T -} - -impl<T: Download> Debug for PhaseDownloadError<'_, T> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("PhaseDownloadError") - .field("phase", &self.phase) - .field("inner", &self.inner) - .field("job", &self.job) - .finish() - } -} - -impl<T: Download> Display for PhaseDownloadError<'_, T> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "error while {} ({}): {}", self.phase, self.job, self.inner) - } -} - -impl<T: Download> Error for PhaseDownloadError<'_, T> { - fn source(&self) -> Option<&(dyn Error + 'static)> { - Some(&*self.inner) - } -} - -impl<'j, T: Download> PhaseDownloadError<'j, T> { - fn new(phase: Phase, inner: Box<dyn Error>, job: &'j T) -> Self { - PhaseDownloadError { - phase, inner, job - } - } -} - -impl<'j, T: Download + 'j, I: Iterator<Item = &'j mut T>> MultiDownloader<'j, T, I> { - pub fn new(jobs: I) -> MultiDownloader<'j, T, I> { - Self::with_concurrent(jobs, 24) - } - - pub fn with_concurrent(jobs: I, n: usize) -> MultiDownloader<'j, T, I> { - assert!(n > 0); - - MultiDownloader { - jobs, - nconcurrent: n - } - } - - pub async fn perform(self, client: &'j Client) -> impl TryStream<Ok = (), Error = PhaseDownloadError<'j, T>> { - stream::iter(self.jobs).map(move |job| Ok(async move { - macro_rules! map_err { - ($result:expr, $phase:expr, $job:expr) => { - match $result { - Ok(v) => v, - Err(e) => return Err(PhaseDownloadError::new($phase, e.into(), $job)) - } - } - } - - let Some(rq) = map_err!(job.prepare(client).await, Phase::Prepare, job) else { - return Ok(()) - }; - - let rq = rq.header(reqwest::header::USER_AGENT, USER_AGENT); - - let mut data = map_err!(map_err!(rq.send().await, Phase::Send, job).error_for_status(), Phase::Send, job).bytes_stream(); - - while let Some(bytes) = data.next().await { - let bytes = map_err!(bytes, Phase::Receive, job); - - map_err!(job.handle_chunk(bytes.as_ref()).await, Phase::HandleChunk, job); - } - - job.finish().await.map_err(|e| PhaseDownloadError::new(Phase::Finish, e, job))?; - - Ok(()) - })).try_buffer_unordered(self.nconcurrent) - } -} - -pub struct VerifiedDownload { - url: String, - expect_size: Option<usize>, - expect_sha1: Option<Digest>, - - path: PathBuf, - file: Option<File>, - sha1: Sha1, - tally: usize -} - -impl Debug for VerifiedDownload { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("VerifiedDownload") - .field("url", &self.url) - .field("expect_size", &self.expect_size) - .field("expect_sha1", &self.expect_sha1) - .field("path", &self.path).finish() - } -} - -impl Display for VerifiedDownload { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "downloading {} to {}", self.url, self.path.display()) - } -} - -impl VerifiedDownload { - pub fn new(url: &str, path: &Path, expect_size: Option<usize>, expect_sha1: Option<Digest>) -> VerifiedDownload { - VerifiedDownload { - url: url.to_owned(), - path: path.to_owned(), - - expect_size, - expect_sha1, - - file: None, - sha1: Sha1::new(), - tally: 0 - } - } - - pub fn with_size(mut self, expect: usize) -> VerifiedDownload { - self.expect_size = Some(expect); - self - } - - pub fn with_sha1(mut self, expect: Digest) -> VerifiedDownload { - self.expect_sha1.replace(expect); - self - } - - pub fn get_url(&self) -> &str { - &self.url - } - - pub fn get_expect_size(&self) -> Option<usize> { - self.expect_size - } - - pub fn get_expect_sha1(&self) -> Option<Digest> { - self.expect_sha1 - } - - pub async fn make_dirs(&self) -> Result<(), io::Error> { - fs::create_dir_all(self.path.parent().expect("download created with no containing directory (?)")).await - } - - async fn open_output(&mut self) -> Result<(), io::Error> { - self.file.replace(File::create(&self.path).await?); - Ok(()) - } -} - -impl Download for VerifiedDownload { - async fn prepare(&mut self, client: &Client) -> Result<Option<RequestBuilder>, Box<dyn Error>> { - if !util::should_download(&self.path, self.expect_size, self.expect_sha1).await? { - return Ok(None) - } - - // potentially racy to close the file and reopen it... :/ - self.open_output().await?; - - Ok(Some(client.request(Method::GET, &self.url))) - } - - async fn handle_chunk(&mut self, chunk: &[u8]) -> Result<(), Box<dyn Error>> { - self.file.as_mut().unwrap().write_all(chunk).await?; - self.tally += chunk.len(); - self.sha1.update(chunk); - - Ok(()) - } - - async fn finish(&mut self) -> Result<(), Box<dyn Error>> { - let digest = self.sha1.digest(); - - if let Some(d) = self.expect_sha1 { - if d != digest { - debug!("Could not download {}: sha1 mismatch (exp {}, got {}).", self.path.display(), d, digest); - return Err(IntegrityError::Sha1Mismatch { expect: d, actual: digest }.into()); - } - } else if let Some(s) = self.expect_size { - if s != self.tally { - debug!("Could not download {}: size mismatch (exp {}, got {}).", self.path.display(), s, self.tally); - return Err(IntegrityError::SizeMismatch { expect: s, actual: self.tally }.into()); - } - } - - debug!("Successfully downloaded {} ({} bytes).", self.path.display(), self.tally); - - // release the file descriptor (don't want to wait until it's dropped automatically because idk when that would be) - drop(self.file.take().unwrap()); - - Ok(()) - } -} - -impl FileDownload for VerifiedDownload { - fn get_path(&self) -> &Path { - &self.path - } -} - -pub async fn verify_files(files: impl Iterator<Item = &mut VerifiedDownload>) -> Result<(), FileVerifyError> { - stream::iter(files) - .map(|dl| Ok(async move { - debug!("Verifying library {}", dl.get_path().display()); - util::verify_file(dl.get_path(), dl.get_expect_size(), dl.get_expect_sha1()).await - })) - .try_buffer_unordered(32) - .try_fold((), |_, _| async {Ok(())}) - .await -} diff --git a/src/launcher/extract.rs b/src/launcher/extract.rs deleted file mode 100644 index 8c5f2b8..0000000 --- a/src/launcher/extract.rs +++ /dev/null @@ -1,136 +0,0 @@ -use std::error::Error; -use std::fmt::{Display, Formatter}; -use std::{fs, io, os}; -use std::fs::File; -use std::io::{BufReader, Error as IOError, Read}; -use std::path::{Path, PathBuf}; -use log::{debug, trace}; -use zip::result::ZipError; -use zip::ZipArchive; -use crate::util; - -#[derive(Debug)] -pub enum ZipExtractError { - IO { what: &'static str, error: IOError }, - Zip { what: &'static str, error: ZipError }, - InvalidEntry { why: &'static str, name: String } -} - -impl From<(&'static str, IOError)> for ZipExtractError { - fn from((what, error): (&'static str, IOError)) -> Self { - ZipExtractError::IO { what, error } - } -} - -impl From<(&'static str, ZipError)> for ZipExtractError { - fn from((what, error): (&'static str, ZipError)) -> Self { - ZipExtractError::Zip { what, error } - } -} - -impl Display for ZipExtractError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - ZipExtractError::IO { what, error } => write!(f, "i/o error ({what}): {error}"), - ZipExtractError::Zip { what, error } => write!(f, "zip error ({what}): {error}"), - ZipExtractError::InvalidEntry { why, name } => write!(f, "invalid entry in zip file ({why}): {name}") - } - } -} - -impl Error for ZipExtractError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - ZipExtractError::IO { error, .. } => Some(error), - ZipExtractError::Zip { error, .. } => Some(error), - _ => None - } - } -} - -fn check_entry_path(name: &str) -> Result<&Path, ZipExtractError> { - util::check_path(name).map_err(|e| ZipExtractError::InvalidEntry { - why: e, - name: name.to_owned() - }) -} - -#[cfg(unix)] -fn extract_symlink(path: impl AsRef<Path>, target: &str) -> io::Result<()> { - os::unix::fs::symlink(target, path) -} - -#[cfg(windows)] -fn extract_symlink(path: impl AsRef<Path>, target: &str) -> io::Result<()> { - os::windows::fs::symlink_file(target, path) -} - -#[cfg(not(any(unix, windows)))] -fn extract_symlink(path: impl AsRef<Path>, _target: &str) -> io::Result<()> { - warn!("Refusing to extract symbolic link to {}. I don't know how to do it on this platform!", path.as_ref().display()); - Ok(()) -} - -pub fn extract_zip<F>(zip_path: impl AsRef<Path>, extract_root: impl AsRef<Path>, condition: F) -> Result<usize, ZipExtractError> -where - F: Fn(&str) -> bool -{ - debug!("Extracting zip file {} into {}", zip_path.as_ref().display(), extract_root.as_ref().display()); - - fs::create_dir_all(&extract_root).map_err(|e| ZipExtractError::from(("create extract root", e)))?; - - let mut extracted = 0usize; - - let file = File::open(&zip_path).map_err(|e| ZipExtractError::from(("extract zip file (open)", e)))?; - let read = BufReader::new(file); - - let mut archive = ZipArchive::new(read).map_err(|e| ZipExtractError::from(("read zip archive", e)))?; - - // create directories - for n in 0..archive.len() { - let entry = archive.by_index(n).map_err(|e| ZipExtractError::from(("read zip entry (1)", e)))?; - if !entry.is_dir() { continue; } - - let name = entry.name(); - if !condition(name) { - continue; - } - - let entry_path = check_entry_path(name)?; - let entry_path: PathBuf = [extract_root.as_ref(), entry_path].iter().collect(); - - trace!("Extracting directory {} from {}", entry.name(), zip_path.as_ref().display()); - fs::create_dir_all(entry_path).map_err(|e| ZipExtractError::from(("extract directory", e)))?; - } - - // extract the files - for n in 0..archive.len() { - let mut entry = archive.by_index(n).map_err(|e| ZipExtractError::from(("read zip entry (2)", e)))?; - let name = entry.name(); - - if entry.is_dir() { continue; } - - if !condition(name) { - continue; - } - - let entry_path = check_entry_path(name)?; - let entry_path: PathBuf = [extract_root.as_ref(), entry_path].iter().collect(); - - if entry.is_symlink() { - let mut target = String::new(); - entry.read_to_string(&mut target).map_err(|e| ZipExtractError::from(("read to symlink target", e)))?; - - trace!("Extracting symbolic link {} -> {} from {}", entry.name(), target, zip_path.as_ref().display()); - extract_symlink(entry_path.as_path(), target.as_str()).map_err(|e| ZipExtractError::from(("extract symlink", e)))?; - } else if entry.is_file() { - let mut outfile = File::create(&entry_path).map_err(|e| ZipExtractError::from(("extract zip entry (open)", e)))?; - - trace!("Extracting file {} from {}", entry.name(), zip_path.as_ref().display()); - io::copy(&mut entry, &mut outfile).map_err(|e| ZipExtractError::from(("extract zip entry (write)", e)))?; - extracted += 1; - } - } - - Ok(extracted) -} diff --git a/src/launcher/jre.rs b/src/launcher/jre.rs deleted file mode 100644 index 31034b5..0000000 --- a/src/launcher/jre.rs +++ /dev/null @@ -1,330 +0,0 @@ -use std::error::Error; -use std::fmt::{Debug, Display, Formatter}; -use std::path::{Component, Path, PathBuf}; -use std::sync::Arc; -use futures::{stream, StreamExt, TryStreamExt}; -use log::{debug, info, warn}; -use reqwest::Client; -use tokio::{fs, io, io::ErrorKind}; - -mod arch; -mod manifest; -mod download; - -use arch::JRE_ARCH; -use manifest::JavaRuntimesManifest; -use manifest::JavaRuntimeManifest; -use crate::launcher::download::MultiDownloader; -use crate::launcher::jre::download::{LzmaDownloadError, LzmaDownloadJob}; -use crate::launcher::jre::manifest::JavaRuntimeFile; -use crate::util; -use crate::util::{EnsureFileError, IntegrityError}; -use crate::version::DownloadInfo; -use super::constants; - -pub struct JavaRuntimeRepository { - online: bool, - home: PathBuf, - manifest: JavaRuntimesManifest -} - -impl JavaRuntimeRepository { - pub async fn new(home: impl AsRef<Path>, online: bool) -> Result<Self, JavaRuntimeError> { - info!("Java runtime architecture is \"{}\".", JRE_ARCH); - - fs::create_dir_all(&home).await.map_err(|e| JavaRuntimeError::IO { what: "creating home directory", error: e })?; - - let manifest_path = home.as_ref().join("manifest.json"); - match util::ensure_file(manifest_path.as_path(), Some(constants::URL_JRE_MANIFEST), None, None, online, true).await { - Ok(_) => (), - Err(EnsureFileError::Offline) => { - info!("Launcher is offline, cannot download runtime manifest."); - }, - Err(e) => return Err(JavaRuntimeError::EnsureFile(e)) - }; - - let manifest_file = fs::read_to_string(&manifest_path).await - .map_err(|e| JavaRuntimeError::IO { what: "reading runtimes manifest", error: e })?; - - Ok(JavaRuntimeRepository { - online, - home: home.as_ref().to_path_buf(), - manifest: serde_json::from_str(&manifest_file).map_err(|e| JavaRuntimeError::Deserialize { what: "runtimes manifest", error: e })?, - }) - } - - fn get_component_dir(&self, component: &str) -> PathBuf { - [self.home.as_path(), Path::new(JRE_ARCH), Path::new(component)].into_iter().collect() - } - - async fn load_runtime_manifest(&self, component: &str, info: &DownloadInfo) -> Result<JavaRuntimeManifest, JavaRuntimeError> { - let comp_dir = self.get_component_dir(component); - let manifest_path = comp_dir.join("manifest.json"); - - debug!("Ensuring manifest for runtime {JRE_ARCH}.{component}"); - - fs::create_dir_all(comp_dir.as_path()).await - .inspect_err(|e| warn!("Failed to create directory for JRE component {}: {}", component, e)) - .map_err(|e| JavaRuntimeError::IO { what: "creating component directory", error: e })?; - - util::ensure_file(&manifest_path, info.url.as_deref(), info.size, info.sha1, self.online, false).await - .map_err(JavaRuntimeError::EnsureFile)?; - - let manifest_file = fs::read_to_string(&manifest_path).await - .map_err(|e| JavaRuntimeError::IO { what: "reading runtimes manifest", error: e })?; - - serde_json::from_str(&manifest_file).map_err(|e| JavaRuntimeError::Deserialize { what: "runtime manifest", error: e }) - } - - // not very descriptive function name - pub async fn choose_runtime(&self, component: &str) -> Result<JavaRuntimeManifest, JavaRuntimeError> { - let Some(runtime_components) = self.manifest.get(JRE_ARCH) else { - return Err(JavaRuntimeError::UnsupportedArch(JRE_ARCH)); - }; - - let Some(runtime_component) = runtime_components.get(component) else { - return Err(JavaRuntimeError::UnsupportedComponent { arch: JRE_ARCH, component: component.to_owned() }); - }; - - let Some(runtime) = runtime_component.iter().find(|r| r.availability.progress == 100) else { - if !runtime_components.is_empty() { - warn!("Weird: the only java runtimes in {JRE_ARCH}.{component} has a progress of less than 100!"); - } - - return Err(JavaRuntimeError::UnsupportedComponent { arch: JRE_ARCH, component: component.to_owned() }); - }; - - self.load_runtime_manifest(component, &runtime.manifest).await - } - - fn clean_up_runtime_sync(path: &Path, manifest: Arc<JavaRuntimeManifest>) -> Result<(), io::Error> { - for entry in walkdir::WalkDir::new(path).contents_first(true) { - let entry = entry?; - let rel_path = entry.path().strip_prefix(path).expect("walkdir escaped root (???)"); - - if !rel_path.components().any(|c| !matches!(&c, Component::CurDir)) { - // if this path is trivial (points at the root), ignore it - continue; - } - - let rel_path_str = if std::path::MAIN_SEPARATOR != '/' { - rel_path.to_str().map(|s| s.replace(std::path::MAIN_SEPARATOR, "/")) - } else { - rel_path.to_str().map(String::from) - }; - - if !rel_path_str.as_ref().is_some_and(|s| manifest.files.get(s) - .is_some_and(|f| (f.is_file() == entry.file_type().is_file()) - || (f.is_directory() == entry.file_type().is_dir()) - || (f.is_link() == entry.file_type().is_symlink()))) { - // path is invalid utf-8, extraneous, or of the wrong type - debug!("File {} is extraneous or of wrong type ({:?}). Deleting it.", entry.path().display(), entry.file_type()); - - if entry.file_type().is_dir() { - std::fs::remove_dir(entry.path())?; - } else { - std::fs::remove_file(entry.path())?; - } - } - } - - Ok(()) - } - - async fn clean_up_runtime(path: &Path, manifest: Arc<JavaRuntimeManifest>) -> Result<(), io::Error> { - let (tx, rx) = tokio::sync::oneshot::channel(); - - let path = path.to_owned(); - let manifest = manifest.clone(); - - tokio::task::spawn_blocking(move || { - let res = Self::clean_up_runtime_sync(&path, manifest); - let _ = tx.send(res); - }).await.expect("clean_up_runtime_sync panicked"); - - rx.await.expect("clean_up_runtime_sync hung up") - } - - async fn ensure_jre_dirs(&self, path: &Path, manifest: &JavaRuntimeManifest) -> Result<(), JavaRuntimeError> { - stream::iter(manifest.files.iter().filter(|(_, f)| f.is_directory())) - .map::<Result<&String, JavaRuntimeError>, _>(|(name, _)| Ok(name)) - .try_for_each(|name| async move { - let ent_path = util::check_path(name).map_err(JavaRuntimeError::MalformedManifest)?; - let ent_path = [path, ent_path].into_iter().collect::<PathBuf>(); - - match fs::metadata(&ent_path).await { - Ok(meta) => { - if !meta.is_dir() { - debug!("Deleting misplaced file at {}", ent_path.display()); - fs::remove_file(&ent_path).await.map_err(|e| JavaRuntimeError::IO { - what: "deleting misplaced file", - error: e - })?; - } - }, - Err(e) if e.kind() == ErrorKind::NotFound => (), - Err(e) => return Err(JavaRuntimeError::IO { what: "'stat'ing directory", error: e }) - } - - match fs::create_dir(&ent_path).await { - Ok(_) => { - debug!("Created directory at {}", ent_path.display()); - Ok(()) - }, - Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(()), - Err(e) => { - warn!("Could not create directory {} for JRE!", ent_path.display()); - Err(JavaRuntimeError::IO { what: "creating directory", error: e }) - } - } - }).await - } - - async fn ensure_jre_files(path: &Path, manifest: &JavaRuntimeManifest) -> Result<(), JavaRuntimeError> { - let mut downloads = Vec::new(); - for (name, file) in manifest.files.iter().filter(|(_, f)| f.is_file()) { - let file_path = util::check_path(name).map_err(JavaRuntimeError::MalformedManifest)?; - let file_path = [path, file_path].into_iter().collect::<PathBuf>(); - - downloads.push(LzmaDownloadJob::try_from((file, file_path)).map_err(|e| { - match e { - LzmaDownloadError::MissingURL => JavaRuntimeError::MalformedManifest("runtime manifest missing URL"), - LzmaDownloadError::NotAFile => unreachable!("we just made sure this was a file") - } - })?); - } - - let dl = MultiDownloader::new(downloads.iter_mut()); - let client = Client::new(); - - dl.perform(&client).await - .inspect_err(|e| warn!("jre file download failed: {e}")) - .try_fold((), |_, _| async { Ok(()) }) - .await - .map_err(|_| JavaRuntimeError::MultiDownloadError) - } - - async fn ensure_links(root_path: &Path, manifest: &JavaRuntimeManifest) -> Result<(), JavaRuntimeError> { - stream::iter(manifest.files.iter().filter(|(_, f)| f.is_link())) - .map::<Result<_, JavaRuntimeError>, _>(|(name, file)| Ok(async move { - let JavaRuntimeFile::Link { target } = file else { - unreachable!(); - }; - - let target_exp = PathBuf::from(target); - - let path = util::check_path(name.as_str()).map_err(JavaRuntimeError::MalformedManifest)?; - let link_path = [root_path, path].into_iter().collect::<PathBuf>(); - - match fs::read_link(&link_path).await { - Ok(target_path) => { - if target_path == target_exp { - debug!("Symbolic link at {} matches! Nothing to be done.", link_path.display()); - return Ok(()) - } - - debug!("Symbolic link at {} does not match (exp {}, got {}). Recreating it.", link_path.display(), target_exp.display(), target_path.display()); - fs::remove_file(&link_path).await.map_err(|e| JavaRuntimeError::IO { - what: "deleting bad symlink", - error: e - })?; - } - Err(e) if e.kind() == ErrorKind::NotFound => (), - Err(e) => return Err(JavaRuntimeError::IO { what: "reading jre symlink", error: e }) - } - - debug!("Creating symbolic link at {} to {}", link_path.display(), target_exp.display()); - - let symlink; - #[cfg(unix)] - { - symlink = |targ, path| async { fs::symlink(targ, path).await }; - } - - #[cfg(windows)] - { - symlink = |targ, path| async { fs::symlink_file(targ, path).await }; - } - - #[cfg(not(any(unix, windows)))] - { - symlink = |_, _| async { Ok(()) }; - } - - symlink(target_exp, link_path).await.map_err(|e| JavaRuntimeError::IO { - what: "creating symlink", - error: e - })?; - - Ok(()) - })) - .try_buffer_unordered(32) - .try_fold((), |_, _| async { Ok(()) }).await - } - - pub async fn ensure_jre(&self, component: &str, manifest: JavaRuntimeManifest) -> Result<PathBuf, JavaRuntimeError> { - let runtime_path = self.get_component_dir(component); - let runtime_path = runtime_path.join("runtime"); - let manifest = Arc::new(manifest); - - fs::create_dir_all(&runtime_path).await - .map_err(|e| JavaRuntimeError::IO { what: "creating runtime directory", error: e })?; - - debug!("Cleaning up JRE directory for {component}"); - Self::clean_up_runtime(runtime_path.as_path(), manifest.clone()).await - .map_err(|e| JavaRuntimeError::IO { what: "cleaning up runtime directory", error: e })?; - - debug!("Building directory structure for {component}"); - self.ensure_jre_dirs(&runtime_path, manifest.as_ref()).await?; - - debug!("Downloading JRE files for {component}"); - Self::ensure_jre_files(&runtime_path, manifest.as_ref()).await?; - - debug!("Ensuring symbolic links for {component}"); - Self::ensure_links(&runtime_path, manifest.as_ref()).await?; - - Ok(runtime_path) - } -} - -#[derive(Debug)] -pub enum JavaRuntimeError { - EnsureFile(EnsureFileError), - IO { what: &'static str, error: io::Error }, - Download { what: &'static str, error: reqwest::Error }, - Deserialize { what: &'static str, error: serde_json::Error }, - UnsupportedArch(&'static str), - UnsupportedComponent { arch: &'static str, component: String }, - MalformedManifest(&'static str), - Integrity(IntegrityError), - MultiDownloadError -} - -impl Display for JavaRuntimeError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - JavaRuntimeError::EnsureFile(e) => std::fmt::Display::fmt(e, f), - JavaRuntimeError::IO { what, error } => write!(f, "i/o error ({}): {}", what, error), - JavaRuntimeError::Download { what, error } => write!(f, "error downloading {}: {}", what, error), - JavaRuntimeError::Deserialize { what, error } => write!(f, "error deserializing ({what}): {error}"), - JavaRuntimeError::UnsupportedArch(arch) => write!(f, r#"unsupported architecture "{arch}""#), - JavaRuntimeError::UnsupportedComponent { arch, component } => write!(f, r#"unsupported component "{component}" for architecture "{arch}""#), - JavaRuntimeError::MalformedManifest(what) => write!(f, "malformed runtime manifest: {what} (launcher bug?)"), - JavaRuntimeError::Integrity(e) => std::fmt::Display::fmt(e, f), - JavaRuntimeError::MultiDownloadError => f.write_str("error in multi downloader (see logs for more details)") - } - } -} - -impl Error for JavaRuntimeError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - JavaRuntimeError::EnsureFile(error) => Some(error), - JavaRuntimeError::IO { error, .. } => Some(error), - JavaRuntimeError::Download { error, .. } => Some(error), - JavaRuntimeError::Deserialize { error, .. } => Some(error), - JavaRuntimeError::Integrity(error) => Some(error), - _ => None - } - } -} diff --git a/src/launcher/jre/arch.rs b/src/launcher/jre/arch.rs deleted file mode 100644 index e984171..0000000 --- a/src/launcher/jre/arch.rs +++ /dev/null @@ -1,45 +0,0 @@ -use cfg_if::cfg_if; - -macro_rules! define_arch { - ($arch:expr) => { - pub const JRE_ARCH: &str = $arch; - } -} - -cfg_if! { - if #[cfg(target_os = "windows")] { - cfg_if! { - if #[cfg(target_arch = "x86_64")] { - define_arch!("windows-x64"); - } else if #[cfg(target_arch = "x86")] { - define_arch!("windows-x86"); - } else if #[cfg(target_arch = "aarch64")] { - define_arch!("windows-arm64"); - } else { - define_arch!("gamecore"); - } - } - } else if #[cfg(target_os = "linux")] { - cfg_if! { - if #[cfg(target_arch = "x86_64")] { - define_arch!("linux"); - } else if #[cfg(target_arch = "x86")] { - define_arch!("linux-i386"); - } else { - define_arch!("gamecore"); - } - } - } else if #[cfg(target_os = "macos")] { - cfg_if! { - if #[cfg(target_arch = "aarch64")] { - define_arch!("mac-os-arm64"); - } else if #[cfg(target_arch = "x86_64")] { - define_arch!("mac-os"); - } else { - define_arch!("gamecore"); - } - } - } else { - define_arch!("gamecore"); - } -} diff --git a/src/launcher/jre/download.rs b/src/launcher/jre/download.rs deleted file mode 100644 index ddf1ff6..0000000 --- a/src/launcher/jre/download.rs +++ /dev/null @@ -1,195 +0,0 @@ -use std::error::Error; -use std::fmt::{Debug, Display, Formatter}; -use std::io::Write; -use std::path::{PathBuf}; -use log::debug; -use lzma_rs::decompress; -use reqwest::{Client, RequestBuilder}; -use sha1_smol::{Digest, Sha1}; -use tokio::io::AsyncWriteExt; -use tokio::fs::File; -use crate::launcher::download::Download; -use crate::launcher::jre::manifest::JavaRuntimeFile; -use crate::util; -use crate::util::IntegrityError; -use crate::version::DownloadInfo; - -pub enum LzmaDownloadError { - NotAFile, - MissingURL -} - -pub struct LzmaDownloadJob { - url: String, - path: PathBuf, - inflate: bool, - executable: bool, - - raw_size: Option<usize>, - raw_sha1: Option<Digest>, - - raw_sha1_st: Sha1, - raw_tally: usize, - - stream: Option<decompress::Stream<Vec<u8>>>, - out_file: Option<File> -} - -impl LzmaDownloadJob { - fn new_inflate(raw: &DownloadInfo, lzma: &DownloadInfo, exe: bool, path: PathBuf) -> Result<Self, LzmaDownloadError> { - Ok(LzmaDownloadJob { - url: lzma.url.as_ref().map_or_else(|| Err(LzmaDownloadError::MissingURL), |u| Ok(u.to_owned()))?, - path, - inflate: true, - executable: exe, - - raw_size: raw.size, - raw_sha1: raw.sha1, - - raw_sha1_st: Sha1::new(), - raw_tally: 0, - - stream: Some(decompress::Stream::new(Vec::new())), - out_file: None - }) - } - - fn new_raw(raw: &DownloadInfo, exe: bool, path: PathBuf) -> Result<Self, LzmaDownloadError> { - Ok(LzmaDownloadJob { - url: raw.url.as_ref().map_or_else(|| Err(LzmaDownloadError::MissingURL), |u| Ok(u.to_owned()))?, - path, - inflate: false, - executable: exe, - - raw_size: raw.size, - raw_sha1: raw.sha1, - - raw_sha1_st: Sha1::new(), - raw_tally: 0, - - stream: None, - out_file: None - }) - } -} - -impl TryFrom<(&JavaRuntimeFile, PathBuf)> for LzmaDownloadJob { - type Error = LzmaDownloadError; - - fn try_from((file, path): (&JavaRuntimeFile, PathBuf)) -> Result<Self, Self::Error> { - if !file.is_file() { - return Err(LzmaDownloadError::NotAFile); - } - - let JavaRuntimeFile::File { executable, downloads } = file else { - unreachable!("we just made sure this was a file"); - }; - - match downloads.lzma.as_ref() { - Some(lzma) => LzmaDownloadJob::new_inflate(&downloads.raw, lzma, *executable, path), - None => LzmaDownloadJob::new_raw(&downloads.raw, *executable, path) - } - } -} - -impl Debug for LzmaDownloadJob { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("LzmaDownloadJob") - .field("url", &self.url) - .field("path", &self.path) - .field("inflate", &self.inflate) - .finish() - } -} - -impl Display for LzmaDownloadJob { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - if self.inflate { - write!(f, "download and inflate {} to {}", &self.url, self.path.display()) - } else { - write!(f, "download {} to {}", &self.url, self.path.display()) - } - } -} - -impl Download for LzmaDownloadJob { - async fn prepare(&mut self, client: &Client) -> Result<Option<RequestBuilder>, Box<dyn Error>> { - if !util::should_download(&self.path, self.raw_size, self.raw_sha1).await? { - return Ok(None) - } - - let mut options = File::options(); - - #[cfg(unix)] - { - options.mode(match self.executable { - true => 0o775, - _ => 0o664 - }); - } - - let file = options.create(true).write(true).truncate(true).open(&self.path).await?; - self.out_file = Some(file); - - Ok(Some(client.get(&self.url))) - } - - async fn handle_chunk(&mut self, chunk: &[u8]) -> Result<(), Box<dyn Error>> { - let out_file = self.out_file.as_mut().expect("output file gone"); - - if let Some(ref mut stream) = self.stream { - stream.write_all(chunk)?; - let buf = stream.get_output_mut().expect("stream output missing before finish()"); - - out_file.write_all(buf.as_slice()).await?; - - self.raw_sha1_st.update(buf.as_slice()); - self.raw_tally += buf.len(); - - buf.truncate(0); - } else { - out_file.write_all(chunk).await?; - - self.raw_sha1_st.update(chunk); - self.raw_tally += chunk.len(); - } - - Ok(()) - } - - async fn finish(&mut self) -> Result<(), Box<dyn Error>> { - let mut out_file = self.out_file.take().expect("output file gone"); - - if let Some(stream) = self.stream.take() { - let buf = stream.finish()?; - - out_file.write_all(buf.as_slice()).await?; - - self.raw_sha1_st.update(buf.as_slice()); - self.raw_tally += buf.len(); - } - - let inf_digest = self.raw_sha1_st.digest(); - if let Some(sha1) = self.raw_sha1 { - if inf_digest != sha1 { - debug!("Could not download {}: sha1 mismatch (exp {}, got {}).", self.path.display(), sha1, inf_digest); - return Err(IntegrityError::Sha1Mismatch { - expect: sha1, - actual: inf_digest - }.into()); - } - } - - if let Some(size) = self.raw_size { - if self.raw_tally != size { - debug!("Could not download {}: size mismatch (exp {}, got {}).", self.path.display(), size, self.raw_tally); - return Err(IntegrityError::SizeMismatch { - expect: size, - actual: self.raw_tally - }.into()); - } - } - - Ok(()) - } -} diff --git a/src/launcher/jre/manifest.rs b/src/launcher/jre/manifest.rs deleted file mode 100644 index 3fd6484..0000000 --- a/src/launcher/jre/manifest.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::collections::HashMap; -use indexmap::IndexMap; -use serde::Deserialize; -use crate::version::DownloadInfo; - -#[derive(Debug, Deserialize)] -pub struct Availability { - pub group: u32, // unknown meaning - pub progress: u32 // unknown meaning -} - -#[derive(Debug, Deserialize)] -pub struct Version { - pub name: String, - pub version: String -} - -#[derive(Debug, Deserialize)] -pub struct JavaRuntimeInfo { - // I don't see how half of this information is useful with how the JRE system currently functions -figboot - pub availability: Availability, - pub manifest: DownloadInfo, - //pub version: Version -} - -pub type JavaRuntimesManifest = HashMap<String, HashMap<String, Vec<JavaRuntimeInfo>>>; - -#[derive(Debug, Deserialize)] -pub struct FileDownloads { - pub lzma: Option<DownloadInfo>, - pub raw: DownloadInfo -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase", tag = "type")] -pub enum JavaRuntimeFile { - File { - #[serde(default)] - executable: bool, - downloads: Box<FileDownloads> - }, - Directory, - Link { - target: String - } -} - -impl JavaRuntimeFile { - pub fn is_file(&self) -> bool { - matches!(*self, JavaRuntimeFile::File { .. }) - } - - pub fn is_directory(&self) -> bool { - matches!(*self, JavaRuntimeFile::Directory) - } - - pub fn is_link(&self) -> bool { - matches!(*self, JavaRuntimeFile::Link { .. }) - } -} - -#[derive(Debug, Deserialize)] -pub struct JavaRuntimeManifest { - pub files: IndexMap<String, JavaRuntimeFile> -} diff --git a/src/launcher/rules.rs b/src/launcher/rules.rs deleted file mode 100644 index 29a36d1..0000000 --- a/src/launcher/rules.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::error::Error; -use std::fmt::Display; -use crate::version::{Argument, CompatibilityRule, CompleteVersion, FeatureMatcher, Library, OSRestriction, RuleAction}; -use super::SystemInfo; - -#[derive(Debug)] -pub struct IncompatibleError { - what: &'static str, - reason: Option<String> -} - -impl Display for IncompatibleError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(reason) = self.reason.as_ref() { - write!(f, "{} incompatible: {}", self.what, reason) - } else { - write!(f, "{} incompatible", self.what) - } - } -} - -impl Error for IncompatibleError {} - -mod seal { - pub trait CompatCheckInner { - const WHAT: &'static str; - - fn get_rules(&self) -> Option<impl IntoIterator<Item = &super::CompatibilityRule>>; - fn get_incompatibility_reason(&self) -> Option<&str>; - } -} - -pub trait CompatCheck: seal::CompatCheckInner { - fn rules_apply(&self, system: &SystemInfo, feature_matcher: &impl FeatureMatcher) -> Result<(), IncompatibleError> { - let Some(rules) = self.get_rules() else { return Ok(()) }; - let mut action = RuleAction::Disallow; - - fn match_os(os: &OSRestriction, system: &SystemInfo) -> bool { - os.os.is_none_or(|o| system.is_our_os(o)) - && os.version.as_ref().is_none_or(|v| v.is_match(system.os_version.as_str())) - && os.arch.as_ref().is_none_or(|a| a.is_match(system.arch.as_str())) - } - - for rule in rules { - if rule.os.as_ref().is_none_or(|o| match_os(o, system)) - && rule.features_match(feature_matcher) { - action = rule.action; - } - } - - if action == RuleAction::Disallow { - Err(IncompatibleError { - what: Self::WHAT, - reason: self.get_incompatibility_reason().map(|s| s.to_owned()) - }) - } else { - Ok(()) - } - } -} - -// trivial -impl seal::CompatCheckInner for CompatibilityRule { - const WHAT: &'static str = "rule"; - - fn get_rules(&self) -> Option<impl IntoIterator<Item = &CompatibilityRule>> { - Some(Some(self)) - } - - fn get_incompatibility_reason(&self) -> Option<&str> { - None - } -} - -impl seal::CompatCheckInner for CompleteVersion { - const WHAT: &'static str = "version"; - - fn get_rules(&self) -> Option<impl IntoIterator<Item = &CompatibilityRule>> { - self.compatibility_rules.as_ref() - } - - fn get_incompatibility_reason(&self) -> Option<&str> { - self.incompatibility_reason.as_deref() - } -} - -impl seal::CompatCheckInner for Library { - const WHAT: &'static str = "library"; - - fn get_rules(&self) -> Option<impl IntoIterator<Item = &CompatibilityRule>> { - self.rules.as_ref() - } - - fn get_incompatibility_reason(&self) -> Option<&str> { - None - } -} - -impl seal::CompatCheckInner for Argument { - const WHAT: &'static str = "argument"; - - fn get_rules(&self) -> Option<impl IntoIterator<Item = &CompatibilityRule>> { - self.rules.as_ref() - } - - fn get_incompatibility_reason(&self) -> Option<&str> { - None - } -} - -impl CompatCheck for CompatibilityRule {} -impl CompatCheck for CompleteVersion {} -impl CompatCheck for Library {} -impl CompatCheck for Argument {}
\ No newline at end of file diff --git a/src/launcher/runner.rs b/src/launcher/runner.rs deleted file mode 100644 index afdfc7f..0000000 --- a/src/launcher/runner.rs +++ /dev/null @@ -1,222 +0,0 @@ -use std::borrow::Cow; -use std::ffi::{OsStr, OsString}; -use std::iter; -use std::path::{Path, PathBuf}; -use std::process::Command; -use log::{debug, warn}; -use tokio::{fs, io}; -use crate::util::AsJavaPath; -use crate::version::{CompleteVersion, FeatureMatcher, OperatingSystem}; -use super::rules::CompatCheck; -use super::strsub::{self, SubFunc}; -use super::{Launch, LaunchInfo}; - -#[derive(Clone, Copy)] -struct LaunchArgSub<'a, 'l, F: FeatureMatcher>(&'a LaunchInfo<'l, F>); - -// FIXME: this is not correct -#[cfg(windows)] -const PATH_SEP: &str = ";"; - -#[cfg(not(windows))] -const PATH_SEP: &str = ":"; - -impl<'rep, F: FeatureMatcher> SubFunc<'rep> for LaunchArgSub<'rep, '_, F> { - fn substitute(&self, key: &str) -> Option<Cow<'rep, str>> { - match key { - "assets_index_name" => self.0.asset_index_name.as_ref().map(|s| Cow::Borrowed(s.as_str())), - "assets_root" => Some(self.0.launcher.assets.get_home().as_java_path().to_string_lossy()), - "auth_access_token" => Some(Cow::Borrowed("-")), // TODO - "auth_player_name" => Some(Cow::Borrowed("Player")), // TODO - "auth_session" => Some(Cow::Borrowed("-")), // TODO - "auth_uuid" => Some(Cow::Borrowed("00000000-0000-0000-0000-000000000000")), // TODO - "auth_xuid" => Some(Cow::Borrowed("00000000-0000-0000-0000-000000000000")), // TODO - "classpath" => Some(Cow::Borrowed(self.0.classpath.as_str())), - "classpath_separator" => Some(Cow::Borrowed(PATH_SEP)), - "game_assets" => self.0.virtual_assets_path.as_ref() - .map(|s| s.as_path().as_java_path().to_string_lossy()), - "game_directory" => Some(self.0.instance_home.as_java_path().to_string_lossy()), - "language" => Some(Cow::Borrowed("en-us")), // ??? - "launcher_name" => Some(Cow::Borrowed("ozone (olauncher 3)")), // TODO - "launcher_version" => Some(Cow::Borrowed("yeah")), // TODO - "library_directory" => Some(self.0.launcher.libraries.home.as_java_path().to_string_lossy()), - "natives_directory" => Some(self.0.natives_path.as_java_path().to_string_lossy()), - "primary_jar" => self.0.client_jar.as_ref().map(|p| p.as_path().as_java_path().to_string_lossy()), - "quickPlayMultiplayer" => None, // TODO - "quickPlayPath" => None, // TODO - "quickPlayRealms" => None, // TODO - "quickPlaySingleplayer" => None, // TODO - "resolution_height" => None, // TODO - "resolution_width" => None, // TODO - "user_properties" => Some(Cow::Borrowed("{}")), // TODO - "user_property_map" => Some(Cow::Borrowed("[]")), // TODO - "user_type" => Some(Cow::Borrowed("legacy")), // TODO - "version_name" => Some(Cow::Borrowed(self.0.version_id.as_ref())), - "version_type" => self.0.version_type.as_ref().map(|s| Cow::Borrowed(s.to_str())), - _ => { - if let Some(asset_key) = key.strip_prefix("asset=") { - return self.0.asset_index.as_ref().and_then(|idx| idx.objects.get(asset_key)) - .map(|obj| Cow::Owned(self.0.launcher.assets.get_object_path(obj).as_java_path().to_string_lossy().into_owned())) - } - - None - } - } - } -} - -#[derive(Clone, Copy)] -pub enum ArgumentType { - Jvm, - Game -} - -pub fn build_arguments<F: FeatureMatcher>(launch: &LaunchInfo<'_, F>, version: &CompleteVersion, arg_type: ArgumentType) -> Vec<OsString> { - let sub = LaunchArgSub(launch); - let system_info = &launch.launcher.system_info; - - if let Some(arguments) = version.arguments.as_ref().and_then(|args| match arg_type { - ArgumentType::Jvm => args.jvm.as_ref(), - ArgumentType::Game => args.game.as_ref() - }) { - arguments.iter() - .filter(|wa| wa.rules_apply(system_info, launch.feature_matcher).is_ok()) - .flat_map(|wa| &wa.value) - .map(|s| OsString::from(strsub::replace_string(s, &sub).into_owned())).collect() - } else if let Some(arguments) = version.minecraft_arguments.as_ref() { - match arg_type { - ArgumentType::Jvm => { - [ - "-Djava.library.path=${natives_directory}", - "-Dminecraft.launcher.brand=${launcher_name}", - "-Dminecraft.launcher.version=${launcher_version}", - "-Dminecraft.client.jar=${primary_jar}", - "-cp", - "${classpath}" - ].into_iter() - .chain(iter::once("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump") - .take_while(|_| system_info.os == OperatingSystem::Windows)) - .chain(iter::once(["-Dos.name=Windows 10", "-Dos.version=10.0"]) - .take_while(|_| launch.feature_matcher.matches("__ozone_win10_hack")) - .flatten()) - .chain(iter::once(["-Xdock:icon=${asset=icons/minecraft.icns}", "-Xdock:name=Minecraft"]) - .take_while(|_| system_info.os == OperatingSystem::MacOS) - .flatten()) - .map(|s| OsString::from(strsub::replace_string(s, &sub).into_owned())) - .collect() - }, - ArgumentType::Game => { - arguments.split(' ') - .chain(iter::once("--demo") - .take_while(|_| launch.feature_matcher.matches("is_demo_user"))) - .chain(iter::once(["--width", "${resolution_width}", "--height", "${resolution_height}"]) - .take_while(|_| launch.feature_matcher.matches("has_custom_resolution")) - .flatten()) - .map(|s| OsString::from(strsub::replace_string(s, &sub).into_owned())) - .collect() - } - } - } else { - Vec::default() - } -} - -pub fn run_the_game(launch: &Launch) -> Result<(), Box<dyn std::error::Error>> { - if launch.runtime_legacy_launch { - Command::new(launch.runtime_path.as_path().as_java_path()) - .args(launch.jvm_args.iter() - .map(|o| o.as_os_str()) - .chain(iter::once(OsStr::new(launch.main_class.as_str()))) - .chain(launch.game_args.iter().map(|o| o.as_os_str()))) - .current_dir(launch.instance_path.as_path().as_java_path()).spawn()?.wait()?; - } else { - todo!("jni launch not supported :(") - } - - Ok(()) -} - -#[allow(dead_code)] -mod windows { - pub const JNI_SEARCH_PATH: Option<&str> = Some("server/jvm.dll"); - pub const JAVA_SEARCH_PATH: Option<&str> = Some("bin/java.exe"); - pub const JRE_PLATFORM_KNOWN: bool = true; -} - -#[allow(dead_code)] -mod linux { - pub const JNI_SEARCH_PATH: Option<&str> = Some("server/libjvm.so"); - pub const JAVA_SEARCH_PATH: Option<&str> = Some("bin/java"); - pub const JRE_PLATFORM_KNOWN: bool = true; -} - -#[allow(dead_code)] -mod macos { - pub const JNI_SEARCH_PATH: Option<&str> = Some("server/libjvm.dylib"); - pub const JAVA_SEARCH_PATH: Option<&str> = Some("bin/java"); - pub const JRE_PLATFORM_KNOWN: bool = true; -} - -#[allow(dead_code)] -mod unknown { - pub const JNI_SEARCH_PATH: Option<&str> = None; - pub const JAVA_SEARCH_PATH: Option<&str> = None; - pub const JRE_PLATFORM_KNOWN: bool = false; -} - -#[cfg(target_os = "windows")] -use self::windows::*; -#[cfg(target_os = "linux")] -use self::linux::*; -#[cfg(target_os = "macos")] -use self::macos::*; -#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] -use self::unknown::*; - -fn search_java_sync(base: impl AsRef<Path>, legacy: bool) -> Result<Option<PathBuf>, io::Error> { - assert!(JRE_PLATFORM_KNOWN); - let search_path = Path::new(match legacy { - true => JAVA_SEARCH_PATH, - _ => JNI_SEARCH_PATH - }.unwrap()); - - let walker = walkdir::WalkDir::new(base.as_ref()).into_iter() - .filter_map(|e| e.ok()) - .filter(|e| e.file_type().is_dir()); - - for entry in walker { - let check_path = [base.as_ref(), entry.path(), Path::new(search_path)].into_iter().collect::<PathBuf>(); - match std::fs::metadata(check_path.as_path()) { - Err(e) if e.kind() == io::ErrorKind::NotFound => (), - Err(e) => return Err(e), - Ok(meta) if meta.is_file() => return Ok(Some(check_path)), - _ => () - } - } - - Ok(None) // not found (sadface) -} - -//noinspection RsConstantConditionIf -pub async fn find_java(base: impl AsRef<Path>, legacy: bool) -> Result<Option<PathBuf>, io::Error> { - let meta = fs::metadata(&base).await?; - if meta.is_dir() { // do search - if !JRE_PLATFORM_KNOWN { - warn!("Unknown platform! Cannot search for java executable in {}. Please specify the executable file manually.", base.as_ref().display()); - return Ok(None); - } - - let (tx, rx) = tokio::sync::oneshot::channel(); - let base = base.as_ref().to_path_buf(); // idc - - tokio::task::spawn_blocking(move || { - let res = search_java_sync(base, legacy); - let _ = tx.send(res); // I really don't care if the reader hung up - }).await.expect("jre search panicked"); - - rx.await.expect("jre search didn't send us a result") - } else { // we are pointed directly at a file. assume it's what we want - debug!("JRE path {} is a file ({}). Assuming it's what we want.", base.as_ref().display(), legacy); - Ok(Some(base.as_ref().to_path_buf())) - } -} diff --git a/src/launcher/settings.rs b/src/launcher/settings.rs deleted file mode 100644 index 8453653..0000000 --- a/src/launcher/settings.rs +++ /dev/null @@ -1,232 +0,0 @@ -use std::collections::HashMap; -use std::error::Error; -use std::fmt::{Display, Formatter}; -use std::io::ErrorKind; -use std::path::{Path, PathBuf}; -use log::warn; -use serde::{Deserialize, Serialize}; -use tokio::{fs, io}; -use tokio::fs::File; -use tokio::io::AsyncWriteExt; -use super::constants; - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct SettingsInner { - profiles: HashMap<String, Profile>, - instances: HashMap<String, Instance> -} - -pub struct Settings { - path: Option<PathBuf>, - inner: SettingsInner -} - -#[derive(Debug)] -pub enum SettingsError { - IO { what: &'static str, error: io::Error }, - Format(serde_json::Error), - Inconsistent(String) -} - -impl Display for SettingsError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - SettingsError::IO { what, error } => write!(f, "settings i/o error ({}): {}", what, error), - SettingsError::Format(err) => write!(f, "settings format error: {}", err), - SettingsError::Inconsistent(err) => write!(f, "inconsistent settings: {}", err), - } - } -} - -impl Error for SettingsError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - SettingsError::IO { error: err, .. } => Some(err), - SettingsError::Format(err) => Some(err), - _ => None - } - } -} - -impl Default for SettingsInner { - fn default() -> Self { - SettingsInner { - instances: [(String::from(constants::DEF_INSTANCE_NAME), PathBuf::from(constants::DEF_INSTANCE_NAME).into())].into_iter().collect(), - profiles: [(String::from(constants::DEF_PROFILE_NAME), Profile::new(constants::DEF_INSTANCE_NAME))].into_iter().collect() - } - } -} - -impl Settings { - async fn load_inner(path: impl AsRef<Path>) -> Result<SettingsInner, SettingsError> { - match fs::read_to_string(&path).await { - Ok(data) => serde_json::from_str(data.as_str()).map_err(SettingsError::Format), - Err(e) if e.kind() == ErrorKind::NotFound => Ok(SettingsInner::default()), - Err(e) => Err(SettingsError::IO { what: "loading settings", error: e }) - } - } - - fn check_consistent(mut inner: SettingsInner, path: Option<impl AsRef<Path>>) -> Result<Settings, SettingsError> { - inner.profiles.retain(|name, profile| { - if !inner.instances.contains_key(&profile.instance) { - warn!("Settings inconsistency: profile {} refers to instance {}, which does not exist. Ignoring this profile.", name, profile.instance); - false - } else { - true - } - }); - - // there will be more checks later maybe - - Ok(Settings { - path: path.map(|p| p.as_ref().to_owned()), - inner - }) - } - - pub async fn load(path: impl AsRef<Path>) -> Result<Settings, SettingsError> { - Self::check_consistent(Self::load_inner(&path).await?, Some(path)) - } - - pub fn get_path(&self) -> Option<&Path> { - self.path.as_deref() - } - - pub async fn save_to(&self, path: impl AsRef<Path>) -> Result<(), SettingsError> { - let path = path.as_ref(); - - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).await - .map_err(|e| SettingsError::IO { what: "saving settings (creating directory)", error: e })?; - } - - let mut file = File::create(path).await - .map_err(|e| SettingsError::IO { what: "saving settings (open)", error: e })?; - - file.write_all(serde_json::to_string_pretty(&self.inner).map_err(SettingsError::Format)?.as_bytes()).await - .map_err(|e| SettingsError::IO { what: "saving settings (write)", error: e })?; - - Ok(()) - } - - pub async fn save(&self) -> Result<(), SettingsError> { - self.save_to(self.path.as_ref().expect("save() called on Settings instance not loaded from file")).await - } - - pub fn get_instance(&self, name: &str) -> Option<&Instance> { - self.inner.instances.get(name) - } - - pub fn get_profile(&self, name: &str) -> Option<&Profile> { - self.inner.profiles.get(name) - } - - pub fn get_instance_for(&self, profile: &Profile) -> &Instance { - self.inner.instances.get(&profile.instance).unwrap() - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct Instance { - path: PathBuf // relative to launcher home (or absolute) -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(rename_all = "snake_case")] -pub enum ProfileVersion { - LatestSnapshot, - LatestRelease, - #[serde(untagged)] - Specific(String) -} - -#[derive(Deserialize, Serialize, Debug, Clone, Copy)] -pub struct Resolution { - width: u32, - height: u32 -} - -impl Default for Resolution { - fn default() -> Self { - Resolution { width: 864, height: 480 } - } -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct Profile { - game_version: ProfileVersion, - java_runtime: Option<String>, - instance: String, - - #[serde(default)] - jvm_arguments: Vec<String>, - #[serde(default)] - legacy_launch: bool, - - resolution: Option<Resolution> -} - -impl<P: AsRef<Path>> From<P> for Instance { - fn from(path: P) -> Self { - Self { path: path.as_ref().into() } - } -} - -impl Instance { - pub async fn get_path(&self, home: impl AsRef<Path>) -> Result<PathBuf, io::Error> { - let path = self.path.as_path(); - - if path.is_relative() { - Ok([home.as_ref(), Path::new("instances"), path].iter().collect::<PathBuf>()) - } else { - fs::canonicalize(path).await - } - } -} - -const DEF_JVM_ARGUMENTS: [&str; 7] = [ - "-Xmx2G", - "-XX:+UnlockExperimentalVMOptions", - "-XX:+UseG1GC", - "-XX:G1NewSizePercent=20", - "-XX:G1ReservePercent=20", - "-XX:MaxGCPauseMillis=50", - "-XX:G1HeapRegionSize=32M" -]; - -impl Profile { - fn new(instance_name: &str) -> Self { - Self { - game_version: ProfileVersion::LatestRelease, - java_runtime: None, - instance: instance_name.into(), - jvm_arguments: DEF_JVM_ARGUMENTS.iter().map(|s| String::from(*s)).collect(), - legacy_launch: false, - resolution: None - } - } - - pub fn get_version(&self) -> &ProfileVersion { - &self.game_version - } - - pub fn get_instance_name(&self) -> &str { - &self.instance - } - - pub fn iter_arguments(&self) -> impl Iterator<Item = &String> { - self.jvm_arguments.iter() - } - - pub fn get_resolution(&self) -> Option<Resolution> { - self.resolution - } - - pub fn get_java_runtime(&self) -> Option<&String> { - self.java_runtime.as_ref() - } - - pub fn is_legacy_launch(&self) -> bool { - self.legacy_launch - } -} diff --git a/src/launcher/strsub.rs b/src/launcher/strsub.rs deleted file mode 100644 index 5764405..0000000 --- a/src/launcher/strsub.rs +++ /dev/null @@ -1,192 +0,0 @@ -// a cheap-o implementation of StrSubstitutor from apache commons -// (does not need to support recursive evaluation or preserving escapes, it was never enabled in - -use std::borrow::Cow; - -const ESCAPE: char = '$'; -const VAR_BEGIN: &str = "${"; -const VAR_END: &str = "}"; -const VAR_DEFAULT: &str = ":-"; - -pub trait SubFunc<'rep> { - fn substitute(&self, key: &str) -> Option<Cow<'rep, str>>; -} - -/* NOTE: the in-place implementation has been replaced for the following reasons: - * - it was annoying to get lifetimes to work, so you could only either pass a trait implementation - * or a closure - * - it was probably slower than doing it out-of-place anyway, since you keep having to copy the - * tail of the string for each replacement - */ - -// handles ${replacements} on this string IN-PLACE. Calls the "sub" function for each key it receives. -// if "sub" returns None, it will use a default value or ignore the ${substitution}. -// There are no "invalid inputs" and this function should never panic unless "sub" panics. -/*pub fn replace_string(input: &mut String, sub: impl SubFunc) { - let mut cursor = input.len(); - while let Some(idx) = input[..cursor].rfind(VAR_BEGIN) { - // note: for some reason, apache processes escapes BEFORE checking if it's even a valid - // replacement expression. strange behavior IMO. - if let Some((pidx, ESCAPE)) = prev_char(input.as_ref(), idx) { - // this "replacement" is escaped. remove the escape marker and continue. - input.remove(pidx); - cursor = pidx; - continue; - } - - let Some(endidx) = input[idx..cursor].find(VAR_END).map(|v| v + idx) else { - // unclosed replacement expression. ignore. - cursor = idx; - continue; - }; - - let spec = &input[(idx + VAR_BEGIN.len())..endidx]; - let name; - let def_opt; - - if let Some(def) = spec.find(VAR_DEFAULT) { - name = &spec[..def]; - def_opt = Some(&spec[(def + VAR_DEFAULT.len())..]); - } else { - name = spec; - def_opt = None; - } - - if let Some(sub_val) = sub.substitute(name).map_or_else(|| def_opt.map(|d| Cow::Owned(d.to_owned())), |v| Some(v)) { - input.replace_range(idx..(endidx + VAR_END.len()), sub_val.as_ref()); - } - - cursor = idx; - } -}*/ - -pub fn replace_string<'inp, 'rep>(input: &'inp str, sub: &impl SubFunc<'rep>) -> Cow<'inp, str> { - let mut ret: Option<String> = None; - let mut cursor = 0usize; - - while let Some(idx) = input[cursor..].find(VAR_BEGIN) { - let idx = idx + cursor; // make idx an absolute index into 'input' - let spec_start = idx + VAR_BEGIN.len(); // the start of the "spec" (area inside {}) - - // first, check if this is escaped - if let Some((prev_idx, ESCAPE)) = input[..idx].char_indices().next_back() { - let s = ret.get_or_insert_default(); - s.push_str(&input[cursor..prev_idx]); - - // advance past this so we don't match it again - s.push_str(&input[idx..spec_start]); - cursor = spec_start; - continue; - } - - // now, find the closing tag - let Some(spec_end) = input[spec_start..].find(VAR_END).map(|v| v + spec_start) else { - break; // reached the end of the string - }; - - let full_spec = &input[spec_start..spec_end]; - - // check for a default argument - let (name, def) = if let Some(defidx) = full_spec.find(VAR_DEFAULT) { - (&full_spec[..defidx], Some(&full_spec[(defidx + VAR_DEFAULT.len())..])) - } else { - (full_spec, None) - }; - - let after = spec_end + VAR_END.len(); - if let Some(subst) = sub.substitute(name).map_or_else(|| def.map(Cow::Borrowed), Some) { - let s = ret.get_or_insert_default(); - s.push_str(&input[cursor..idx]); - s.push_str(subst.as_ref()); - } else { - ret.get_or_insert_default().push_str(&input[cursor..after]); - } - - cursor = after; - } - - if let Some(ret) = ret.as_mut() { - ret.push_str(&input[cursor..]); - } - - ret.map_or(Cow::Borrowed(input), Cow::Owned) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[derive(Clone, Copy)] - struct TestSub; - impl SubFunc<'static> for TestSub { - fn substitute(&self, key: &str) -> Option<Cow<'static, str>> { - match key { - "exists" => Some(Cow::Borrowed("value123")), - "empty" => None, - "borger" => Some(Cow::Borrowed("\u{1f354}")), - _ => panic!("replace_fun called with unexpected key: {}", key) - } - } - } - - #[test] - fn test_standard_replace() { - assert_eq!(replace_string("this has ${exists} and more", &TestSub), "this has value123 and more"); - assert_eq!(replace_string("multiple ${exists} repl${exists}ace", &TestSub), "multiple value123 replvalue123ace"); - assert_eq!(replace_string("${exists}${exists}", &TestSub), "value123value123"); - } - - #[test] - fn test_empty_replace() { - assert_eq!(replace_string("this has ${empty} and more", &TestSub), "this has ${empty} and more"); - assert_eq!(replace_string("multiple ${empty} repl${empty}ace", &TestSub), "multiple ${empty} repl${empty}ace"); - assert_eq!(replace_string("${empty}${empty}", &TestSub), "${empty}${empty}"); - } - - #[test] - fn test_homogenous_replace() { - assert_eq!(replace_string("some ${exists} and ${empty} ...", &TestSub), "some value123 and ${empty} ..."); - assert_eq!(replace_string("some ${empty} and ${exists} ...", &TestSub), "some ${empty} and value123 ..."); - assert_eq!(replace_string("${exists}${empty}", &TestSub), "value123${empty}"); - assert_eq!(replace_string("${empty}${exists}", &TestSub), "${empty}value123"); - } - - #[test] - fn test_default_replace() { - assert_eq!(replace_string("some ${exists:-def1} and ${empty:-def2} ...", &TestSub), "some value123 and def2 ..."); - assert_eq!(replace_string("some ${empty:-def1} and ${exists:-def2} ...", &TestSub), "some def1 and value123 ..."); - assert_eq!(replace_string("abc${empty:-}def", &TestSub), "abcdef"); - assert_eq!(replace_string("${empty:-}${empty:-}", &TestSub), ""); - } - - #[test] - fn test_escape() { - assert_eq!(replace_string("an $${escaped} replacement (${exists})", &TestSub), "an ${escaped} replacement (value123)"); - assert_eq!(replace_string("${exists}$${escaped}${exists}", &TestSub), "value123${escaped}value123"); - - // make sure this weird behavior is preserved... (the original code seemed to show it) - assert_eq!(replace_string("some $${ else", &TestSub), "some ${ else"); - } - - #[test] - fn test_weird() { - assert_eq!(replace_string("${exists}", &TestSub), "value123"); - assert_eq!(replace_string("$${empty}", &TestSub), "${empty}"); - assert_eq!(replace_string("${empty:-a}", &TestSub), "a"); - assert_eq!(replace_string("${empty:-}", &TestSub), ""); - } - - // these make sure it doesn't chop up multibyte characters illegally - #[test] - fn test_multibyte_surround() { - assert_eq!(replace_string("\u{1f354}$${}\u{1f354}", &TestSub), "\u{1f354}${}\u{1f354}"); - assert_eq!(replace_string("\u{1f354}${exists}\u{1f354}${empty:-}\u{1f354}", &TestSub), "\u{1f354}value123\u{1f354}\u{1f354}"); - } - - #[test] - fn test_multibyte_replace() { - assert_eq!(replace_string("borger ${borger}", &TestSub), "borger \u{1f354}"); - assert_eq!(replace_string("${exists:-\u{1f354}}${empty:-\u{1f354}}", &TestSub), "value123\u{1f354}"); - assert_eq!(replace_string("${borger}$${}${borger}", &TestSub), "\u{1f354}${}\u{1f354}"); - } -} diff --git a/src/launcher/version.rs b/src/launcher/version.rs deleted file mode 100644 index 0f55223..0000000 --- a/src/launcher/version.rs +++ /dev/null @@ -1,398 +0,0 @@ -use std::{collections::{BTreeMap, HashMap}, error::Error, io::ErrorKind}; -use std::borrow::Cow; -use std::collections::HashSet; -use std::fmt::Display; -use std::path::{Path, PathBuf}; - -use log::{debug, info, warn}; -use sha1_smol::Digest; -use tokio::{fs, io}; -use crate::launcher::settings::ProfileVersion; -use crate::util; -use crate::version::{*, manifest::*}; - -use super::constants::*; - -#[derive(Debug)] -pub enum VersionError { - IO { what: String, error: io::Error }, - Request { what: String, error: reqwest::Error }, - MalformedObject { what: String, error: serde_json::Error }, - VersionIntegrity { id: String, expect: Digest, got: Digest } -} - -impl Display for VersionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - VersionError::IO { what, error } => write!(f, "i/o error ({what}): {error}"), - VersionError::Request { what, error } => write!(f, "request error ({what}): {error}"), - VersionError::MalformedObject { what, error } => write!(f, "malformed {what}: {error}"), - VersionError::VersionIntegrity { id, expect, got } => write!(f, "version {id} integrity mismatch (expect {expect}, got {got})") - } - } -} - -impl Error for VersionError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - VersionError::IO { error, .. } => Some(error), - VersionError::Request { error, .. } => Some(error), - VersionError::MalformedObject { error, .. } => Some(error), - _ => None - } - } -} - -struct RemoteVersionList { - versions: HashMap<String, VersionManifestVersion>, - latest: LatestVersions -} - -impl RemoteVersionList { - async fn new() -> Result<RemoteVersionList, VersionError> { - debug!("Looking up remote version manifest."); - let text = reqwest::get(URL_VERSION_MANIFEST).await - .and_then(|r| r.error_for_status()) - .map_err(|e| VersionError::Request { what: "download version manifest".into(), error: e })? - .text().await.map_err(|e| VersionError::Request { what: "download version manifest (decode)".into(), error: e })?; - - debug!("Parsing version manifest."); - let manifest: VersionManifest = serde_json::from_str(text.as_str()).map_err(|e| VersionError::MalformedObject { what: "version manifest".into(), error: e })?; - - let mut versions = HashMap::new(); - for v in manifest.versions { - versions.insert(v.id.clone(), v); - } - - debug!("Done loading remote versions!"); - Ok(RemoteVersionList { - versions, - latest: manifest.latest - }) - } - - async fn download_version(&self, ver: &VersionManifestVersion, path: &Path) -> Result<CompleteVersion, VersionError> { - // ensure parent directory exists - info!("Downloading version {}.", ver.id); - tokio::fs::create_dir_all(path.parent().expect("version .json has no parent (impossible)")).await - .inspect_err(|e| warn!("failed to create {} parent dirs: {e}", path.display())) - .map_err(|e| VersionError::IO { what: format!("creating version directory for {}", path.display()), error: e })?; - - // download it - let ver_text = reqwest::get(ver.url.as_str()).await - .and_then(|r| r.error_for_status()) - .map_err(|e| VersionError::Request { what: format!("download version {} from {}", ver.id, ver.url), error: e })? - .text().await.map_err(|e| VersionError::Request { what: format!("download version {} from {} (receive)", ver.id, ver.url), error: e })?; - - debug!("Validating downloaded {}...", ver.id); - // make sure it's valid - util::verify_sha1(ver.sha1, ver_text.as_str()) - .map_err(|e| VersionError::VersionIntegrity { - id: ver.id.clone(), - expect: ver.sha1, - got: e - })?; - - // make sure it's well-formed - let cver: CompleteVersion = serde_json::from_str(ver_text.as_str()).map_err(|e| VersionError::MalformedObject { what: format!("complete version {}", ver.id), error: e })?; - - debug!("Saving version {}...", ver.id); - - // write it out - tokio::fs::write(path, ver_text).await - .inspect_err(|e| warn!("Failed to save version {}: {}", ver.id, e)) - .map_err(|e| VersionError::IO { what: format!("writing version file at {}", path.display()), error: e })?; - - info!("Done downloading and verifying {}!", ver.id); - - Ok(cver) - } -} - -struct LocalVersionList { - versions: BTreeMap<String, CompleteVersion> -} - -#[derive(Debug)] -enum LocalVersionError { - Sha1Mismatch { exp: Digest, got: Digest }, - VersionMismatch { fname: String, json: String }, - Unknown(Box<dyn Error>) -} - -impl Display for LocalVersionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - LocalVersionError::Sha1Mismatch { exp, got } => { - write!(f, "sha1 mismatch (exp {exp}, got {got})") - }, - LocalVersionError::VersionMismatch { fname, json } => { - write!(f, "version ID mismatch (filename {fname}, json {json})") - }, - LocalVersionError::Unknown(err) => { - write!(f, "unknown version error: {err}") - } - } - } -} - -impl Error for LocalVersionError {} - -impl LocalVersionList { - async fn load_version(path: &Path, sha1: Option<Digest>) -> Result<CompleteVersion, LocalVersionError> { - // grumble grumble I don't like reading in the whole file at once - info!("Loading local version at {}.", path.display()); - let ver = tokio::fs::read_to_string(path).await.map_err(|e| LocalVersionError::Unknown(Box::new(e)))?; - if let Some(digest_exp) = sha1 { - debug!("Verifying local version {}.", path.display()); - util::verify_sha1(digest_exp, ver.as_str()) - .map_err(|got| { - warn!("Local version sha1 mismatch: {} (exp: {}, got: {})", path.display(), digest_exp, got); - LocalVersionError::Sha1Mismatch { exp: digest_exp.to_owned(), got } - })?; - } - - let ver: CompleteVersion = serde_json::from_str(ver.as_str()).map_err(|e| { - warn!("Invalid version JSON {}: {}", path.display(), e); - LocalVersionError::Unknown(Box::new(e)) - })?; - - let fname_id = path.file_stem() - .expect("tried to load a local version with no path") // should be impossible - .to_str() - .expect("tried to load a local version with invalid UTF-8 filename"); // we already checked if the filename is valid UTF-8 at this point - - if fname_id == ver.id.as_str() { - info!("Loaded local version {}.", ver.id); - Ok(ver) - } else { - warn!("Local version {} has a version ID conflict (filename: {}, json: {})!", path.display(), fname_id, ver.id); - Err(LocalVersionError::VersionMismatch { fname: fname_id.to_owned(), json: ver.id }) - } - } - - async fn load_versions(home: &Path, skip: impl Fn(&str) -> bool) -> Result<LocalVersionList, VersionError> { - info!("Loading local versions."); - let mut rd = tokio::fs::read_dir(home).await.map_err(|e| VersionError::IO { what: format!("open local versions directory {}", home.display()), error: e })?; - let mut versions = BTreeMap::new(); - - while let Some(ent) = rd.next_entry().await.map_err(|e| VersionError::IO { what: format!("read local versions directory {}", home.display()), error: e })? { - if !ent.file_type().await.map_err(|e| VersionError::IO { what: format!("version entry metadata {}", ent.path().display()), error: e} )?.is_dir() { continue; } - - // when the code is fugly - let path = match ent.file_name().to_str() { - Some(s) => { - if skip(s) { - debug!("Skipping local version {s} because (I assume) it is remotely tracked."); - continue - } - - /* FIXME: once https://github.com/rust-lang/rust/issues/127292 is closed, - * use add_extension to avoid extra heap allocations (they hurt my feelings) */ - let mut path = ent.path(); - - // can't use set_extension since s might contain a . (like 1.8.9) - path.push(format!("{s}.json")); - path - }, - - /* We just ignore directories with names that contain invalid unicode. Unfortunately, the laucher - * will not be supporting such custom versions. Name your version something sensible please. */ - None => { - warn!("Ignoring a local version {} because its id contains invalid unicode.", ent.file_name().to_string_lossy()); - continue - } - }; - - match Self::load_version(&path, None).await { - Ok(v) => { - versions.insert(v.id.clone(), v); - }, - Err(e) => { - // FIXME: just display the filename without to_string_lossy when https://github.com/rust-lang/rust/issues/120048 is closed - warn!("Ignoring local version {}: {e}", ent.file_name().to_string_lossy()); - } - } - } - - info!("Loaded {} local version(s).", versions.len()); - Ok(LocalVersionList { versions }) - } -} - -pub struct VersionList { - remote: Option<RemoteVersionList>, - local: LocalVersionList, - home: PathBuf -} - -pub enum VersionResult<'a> { - Complete(&'a CompleteVersion), - Remote(&'a VersionManifestVersion), - None -} - -impl<'a> From<&'a CompleteVersion> for VersionResult<'a> { - fn from(value: &'a CompleteVersion) -> Self { - Self::Complete(value) - } -} - -impl<'a> From<&'a VersionManifestVersion> for VersionResult<'a> { - fn from(value: &'a VersionManifestVersion) -> Self { - Self::Remote(value) - } -} - -impl<'a, T: Into<VersionResult<'a>>> From<Option<T>> for VersionResult<'a> { - fn from(value: Option<T>) -> Self { - value.map_or(VersionResult::None, |v| v.into()) - } -} - -#[derive(Debug)] -pub enum VersionResolveError { - InheritanceLoop(String), - MissingVersion(String), - VersionLoad(VersionError) -} - -impl Display for VersionResolveError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - VersionResolveError::InheritanceLoop(s) => write!(f, "inheritance loop (saw {s} twice)"), - VersionResolveError::MissingVersion(s) => write!(f, "unknown version {s}"), - VersionResolveError::VersionLoad(err) => write!(f, "version load error: {err}") - } - } -} - -impl Error for VersionResolveError {} - -impl VersionList { - async fn create_dir_for(home: &Path) -> Result<(), io::Error> { - debug!("Creating versions directory."); - match fs::create_dir(home).await { - Ok(_) => Ok(()), - Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(()), - Err(e) => { - debug!("failed to create version home: {}", e); - Err(e) - } - } - } - - pub async fn online(home: &Path) -> Result<VersionList, VersionError> { - Self::create_dir_for(home).await.map_err(|e| VersionError::IO { what: format!("create version directory {}", home.display()), error: e })?; - - let remote = RemoteVersionList::new().await?; - let local = LocalVersionList::load_versions(home, |s| remote.versions.contains_key(s)).await?; - - Ok(VersionList { - remote: Some(remote), - local, - home: home.to_path_buf() - }) - } - - pub async fn offline(home: &Path) -> Result<VersionList, VersionError> { - Self::create_dir_for(home).await.map_err(|e| VersionError::IO { what: format!("create version directory {}", home.display()), error: e })?; - - let local = LocalVersionList::load_versions(home, |_| false).await?; - - Ok(VersionList { - remote: None, - local, - home: home.to_path_buf() - }) - } - - pub fn is_online(&self) -> bool { - self.remote.is_some() - } - - pub fn get_version_lazy(&self, id: &str) -> VersionResult { - self.remote.as_ref().and_then(|r| r.versions.get(id).map(VersionResult::from)) - .or_else(|| self.local.versions.get(id).map(VersionResult::from)) - .unwrap_or(VersionResult::None) - } - - pub fn get_profile_version_id<'v>(&self, ver: &'v ProfileVersion) -> Option<Cow<'v, str>> { - match ver { - ProfileVersion::LatestRelease => self.remote.as_ref().map(|r| Cow::Owned(r.latest.release.clone())), - ProfileVersion::LatestSnapshot => self.remote.as_ref().map(|r| Cow::Owned(r.latest.snapshot.clone())), - ProfileVersion::Specific(ver) => Some(Cow::Borrowed(ver)) - } - } - - pub fn get_remote_version(&self, id: &str) -> Option<&VersionManifestVersion> { - let remote = self.remote.as_ref().expect("get_remote_version called in offline mode!"); - - remote.versions.get(id) - } - - pub async fn load_remote_version(&self, ver: &VersionManifestVersion) -> Result<CompleteVersion, VersionError> { - let remote = self.remote.as_ref().expect("load_remote_version called in offline mode!"); - - let id = ver.id.as_str(); - let mut ver_path = self.home.join(id); - ver_path.push(format!("{id}.json")); - - debug!("Loading local copy of remote version {}", ver.id); - - match LocalVersionList::load_version(ver_path.as_path(), Some(ver.sha1)).await { - Ok(v) => return Ok(v), - Err(e) => { - info!("Redownloading {id}, since the local copy could not be loaded: {e}"); - } - } - - remote.download_version(ver, ver_path.as_path()).await - } - - pub async fn resolve_version<'v>(&self, ver: &'v CompleteVersion) -> Result<Cow<'v, CompleteVersion>, VersionResolveError> { - let mut seen: HashSet<String> = HashSet::new(); - seen.insert(ver.id.clone()); - - let Some(inherit) = ver.inherits_from.as_ref() else { - return Ok(Cow::Borrowed(ver)); - }; - - if *inherit == ver.id { - warn!("Version {} directly inherits from itself!", ver.id); - return Err(VersionResolveError::InheritanceLoop(ver.id.clone())); - } - - debug!("Resolving version inheritance: {} (inherits from {})", ver.id, inherit); - - let mut ver = ver.clone(); - let mut inherit = inherit.clone(); - - loop { - if !seen.insert(inherit.clone()) { - warn!("Version inheritance loop detected in {}: {} transitively inherits from itself.", ver.id, inherit); - return Err(VersionResolveError::InheritanceLoop(inherit)); - } - - let inherited_ver = match self.get_version_lazy(inherit.as_str()) { - VersionResult::Complete(v) => Cow::Borrowed(v), - VersionResult::Remote(v) => - Cow::Owned(self.load_remote_version(v).await.map_err(VersionResolveError::VersionLoad)?), - VersionResult::None => { - warn!("Cannot resolve version {}, it inherits an unknown version {inherit}", ver.id); - return Err(VersionResolveError::MissingVersion(inherit)); - } - }; - - ver.apply_child(inherited_ver.as_ref()); - - let Some(new_inherit) = inherited_ver.inherits_from.as_ref() else { - break - }; - - inherit.replace_range(.., new_inherit.as_str()); - } - - Ok(Cow::Owned(ver)) - } -} |
