summaryrefslogtreecommitdiffstats
path: root/src/launcher
diff options
context:
space:
mode:
Diffstat (limited to 'src/launcher')
-rw-r--r--src/launcher/assets.rs322
-rw-r--r--src/launcher/constants.rs18
-rw-r--r--src/launcher/download.rs267
-rw-r--r--src/launcher/extract.rs136
-rw-r--r--src/launcher/jre.rs330
-rw-r--r--src/launcher/jre/arch.rs45
-rw-r--r--src/launcher/jre/download.rs195
-rw-r--r--src/launcher/jre/manifest.rs65
-rw-r--r--src/launcher/rules.rs114
-rw-r--r--src/launcher/runner.rs222
-rw-r--r--src/launcher/settings.rs232
-rw-r--r--src/launcher/strsub.rs192
-rw-r--r--src/launcher/version.rs398
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))
- }
-}