OneLauncher-Cracked/packages/oneclient_java/src/install.rs

329 lines
9.8 KiB
Rust

use std::path::{Path, PathBuf};
use oneclient_net::ResponseNotifyOptions;
use crate::data::{
java_executable_relative_path, JavaPackage, PackageArchive,
};
use oneclient_events::{GroupedProgressSession, TaskCategory, TaskPhase};
use oneclient_common::paths;
use oneclient_events::EventBus;
use oneclient_net::RequestClient;
use crate::error::JavaResult;
#[tracing::instrument(level = "debug", skip(net, events, progress))]
pub async fn install_package(
package: &JavaPackage,
net: &RequestClient,
events: &EventBus,
progress: Option<&GroupedProgressSession>,
) -> JavaResult<PathBuf> {
let java_dir = paths::java_dir()?;
polyio::create_dir_all(&java_dir).await?;
let archive_path = java_dir.join(polyio::sanitize_path(&package.name));
let major = package.java_version.first().copied().unwrap_or(0);
// 0 means unknown the real total comes from Content-Length once headers land
let expected_size = package.size.unwrap_or(0);
let child = progress.map(|session| {
session.child(
format!("{} {}", package.vendor, major),
expected_size,
TaskCategory::Java,
)
});
let notify = match &child {
Some(child) => ResponseNotifyOptions::grouped(child.clone()),
None => ResponseNotifyOptions::standalone(format!(
"Installing {} {}",
package.vendor, major
))
.done_label(format!("Installed {} {}", package.vendor, major)),
};
if package.checksum.is_none() {
tracing::warn!(
vendor = %package.vendor,
major,
"vendor published no usable checksum; installing runtime unverified"
);
}
oneclient_net::download_verified(
net,
events,
&package.download_url,
&archive_path,
package.checksum.as_ref(),
expected_size,
Some(notify),
)
.await?;
let extract_root = java_dir.join(stem_without_archive(&package.name));
if let Some(child) = &child {
child.set_phase(TaskPhase::Extracting);
}
//events.notify("Extracting package...").body(format!("{} {}", package.vendor, major)).send();
match package.archive {
PackageArchive::Zip => polyio::extract_zip(&archive_path, &extract_root).await?,
PackageArchive::TarGz => polyio::extract_tar_gz(&archive_path, &extract_root).await?,
}
// Walks the extract root and stats candidates, so it stays off the caller's
// executor for the same reason the extraction above does
let executable = {
let extract_root = extract_root.clone();
let package = package.clone();
tokio::task::spawn_blocking(move || resolve_installed_executable(&extract_root, &package))
.await
.map_err(|err| polyio::IOError::from(std::io::Error::other(err)))?
};
#[cfg(unix)]
{
let _ = tokio::process::Command::new("chmod")
.arg("755")
.arg(&executable)
.output()
.await;
}
let _ = polyio::remove_file(&archive_path).await;
tracing::info!(vendor = %package.vendor, major, "installed Java runtime");
Ok(executable)
}
/// The directory this crate extracted for `executable`, or `None` when the
/// runtime sits outside the launcher's java dir which is where a folder the
/// user added themselves points
#[tracing::instrument(level = "debug")]
fn managed_install_root(executable: &Path) -> JavaResult<Option<PathBuf>> {
let java_dir = paths::java_dir()?;
let Ok(root) = polyio::canonicalize(&java_dir) else {
return Ok(None);
};
// Refuses to resolve once the executable is gone which is the safe answer
// there is no way left to prove what the path pointed at
let canon = match polyio::ensure_under(executable, [&root]) {
Ok(Some(canon)) => canon,
Ok(None) => return Ok(None),
Err(err) => {
tracing::debug!("could not resolve the java runtime path: {err}");
return Ok(None);
}
};
let Ok(relative) = canon.strip_prefix(&root) else {
return Ok(None);
};
match relative.components().next() {
Some(std::path::Component::Normal(name)) => Ok(Some(root.join(name))),
_ => Ok(None),
}
}
/// Whether [`remove_installed_package`] would take this runtime's files with
/// it so the UI can say up front what removing it costs
#[must_use]
pub fn is_launcher_managed(executable: &Path) -> bool {
managed_install_root(executable).is_ok_and(|root| root.is_some())
}
/// Only ever touches OneClient's own java dir a runtime the user added from
/// their own folder is left on disk untouched
#[tracing::instrument(level = "debug")]
pub async fn remove_installed_package(executable: &Path) -> JavaResult<bool> {
let Some(install_root) = managed_install_root(executable)? else {
tracing::debug!("java runtime is not launcher-managed leaving its files alone");
return Ok(false);
};
polyio::remove_dir_all(&install_root).await?;
tracing::info!(path = %install_root.display(), "removed installed Java runtime files");
Ok(true)
}
#[tracing::instrument(level = "debug")]
fn resolve_installed_executable(extract_root: &Path, package: &JavaPackage) -> PathBuf {
let mut base_path = extract_root.to_path_buf();
if let Ok(entries) = std::fs::read_dir(extract_root) {
let valid_dirs: Vec<PathBuf> = entries
.flatten()
.filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
.map(|e| e.path())
.collect();
if valid_dirs.len() == 1 {
base_path = valid_dirs[0].clone();
}
}
#[cfg(not(target_os = "macos"))]
{
let _ = package;
let subfolder_exec = base_path.join(java_executable_relative_path());
if subfolder_exec.exists() {
subfolder_exec
} else {
extract_root.join(java_executable_relative_path())
}
}
#[cfg(target_os = "macos")]
{
use crate::vendors::JavaVendor;
if !base_path.join("Contents").join("Home").exists()
&& let Ok(entries) = std::fs::read_dir(&base_path)
&& let Some(dir) = entries.flatten().find(|entry| {
let file_name = entry.file_name();
let name = file_name.to_string_lossy();
(name.ends_with(".jre") || name.ends_with(".jdk") || name.contains("zulu"))
&& entry.path().join("Contents").join("Home").exists()
}) {
base_path = dir.path();
}
if package.vendor == JavaVendor::Zulu
&& let Some(major) = package.java_version.first() {
let zulu_bundle = base_path.join(format!("zulu-{major}.jre"));
if zulu_bundle.join("Contents").join("Home").exists() {
base_path = zulu_bundle;
}
}
base_path.join(java_executable_relative_path())
}
}
fn stem_without_archive(name: &str) -> String {
let path = std::path::Path::new(name);
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(name);
if stem.ends_with(".tar") {
std::path::Path::new(stem)
.file_stem()
.and_then(|s| s.to_str())
.map(String::from)
.unwrap_or_else(|| stem.to_string())
} else {
stem.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicU32, Ordering};
/// Keeps the whole suite inside a temp tree the override is a `OnceLock` so
/// the first caller wins and every case works under one java dir
fn java_dir() -> PathBuf {
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
paths::set_launcher_dir(
std::env::temp_dir().join(format!("oneclient-java-test-{}", std::process::id())),
);
});
let dir = paths::java_dir().unwrap();
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn install_runtime(root: &Path) -> (PathBuf, PathBuf) {
static N: AtomicU32 = AtomicU32::new(0);
let install = root.join(format!("zulu21-{}", N.fetch_add(1, Ordering::Relaxed)));
let bin = install.join("zulu21.0.12").join("bin");
std::fs::create_dir_all(&bin).unwrap();
let executable = bin.join("java");
std::fs::write(&executable, b"").unwrap();
(install, executable)
}
#[test]
fn resolves_the_extracted_directory_not_the_executable_parent() {
let java_dir = java_dir();
let (install, executable) = install_runtime(&java_dir);
let resolved = managed_install_root(&executable).unwrap().unwrap();
assert_eq!(resolved, polyio::canonicalize(&install).unwrap());
}
#[tokio::test]
async fn removing_a_managed_runtime_deletes_its_files() {
let java_dir = java_dir();
let (install, executable) = install_runtime(&java_dir);
assert!(remove_installed_package(&executable).await.unwrap());
assert!(!install.exists());
assert!(java_dir.exists(), "the java dir itself must survive");
}
#[tokio::test]
async fn a_runtime_outside_the_java_dir_keeps_its_files() {
let java_dir = java_dir();
let elsewhere = java_dir.parent().unwrap().join("user-picked-jdk");
let (_, executable) = install_runtime(&elsewhere);
assert_eq!(managed_install_root(&executable).unwrap(), None);
assert!(!remove_installed_package(&executable).await.unwrap());
assert!(executable.exists(), "a folder the user added is not ours to delete");
std::fs::remove_dir_all(&elsewhere).unwrap();
}
#[test]
fn a_traversal_back_out_of_the_java_dir_is_not_managed() {
let java_dir = java_dir();
let elsewhere = java_dir.parent().unwrap().join("traversal-jdk");
let (_, executable) = install_runtime(&elsewhere);
let sneaky = java_dir
.join("..")
.join("traversal-jdk")
.join(executable.strip_prefix(&elsewhere).unwrap());
assert_eq!(managed_install_root(&sneaky).unwrap(), None);
std::fs::remove_dir_all(&elsewhere).unwrap();
}
#[test]
fn the_managed_flag_matches_what_removal_would_actually_delete() {
let java_dir = java_dir();
let (_, ours) = install_runtime(&java_dir);
let elsewhere = java_dir.parent().unwrap().join("flagged-jdk");
let (_, theirs) = install_runtime(&elsewhere);
assert!(is_launcher_managed(&ours));
assert!(!is_launcher_managed(&theirs));
std::fs::remove_dir_all(&elsewhere).unwrap();
}
#[test]
fn the_java_dir_itself_is_never_managed() {
let java_dir = java_dir();
assert_eq!(managed_install_root(&java_dir).unwrap(), None);
}
}