fix: java downloading reading entire archive into memory twice

This commit is contained in:
LynithDev 2025-11-15 20:26:16 +01:00
parent a4d07d3fa3
commit bb84df1091
13 changed files with 104 additions and 812 deletions

11
Cargo.lock generated
View file

@ -369,6 +369,15 @@ version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-tempfile"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8a57b75c36e16f4d015e60e6a177552976a99b6947724403c551bcfa7cb1207"
dependencies = [
"tokio",
]
[[package]]
name = "async-trait"
version = "0.1.88"
@ -3430,6 +3439,7 @@ dependencies = [
"anyhow",
"async-compression",
"async-stream",
"async-tempfile",
"async-trait",
"async_zip",
"base64 0.22.1",
@ -3469,7 +3479,6 @@ dependencies = [
"sysinfo",
"tauri",
"taurpc",
"tempfile",
"thiserror 2.0.12",
"tokio",
"tokio-stream",

View file

@ -126,7 +126,7 @@ dirs = { version = "=6.0.0" }
dunce = { version = "=1.0.5" }
flate2 = { version = "=1.1.1" }
sanitize-filename = { version = "=0.6.0" }
tempfile = { version = "=3.19.1" }
async-tempfile = { version = "=0.7.0" }
tokio = { version = "1", features = [ "full" ] }
tokio-stream = { version = "0.1", features = [ "fs" ] }
tokio-util = { version = "0.7", features = [ "compat" ] }

View file

@ -93,7 +93,7 @@ bytes = { workspace = true }
base64 = { workspace = true }
async-compression = { workspace = true }
dunce = { workspace = true }
tempfile = { workspace = true }
async-tempfile = { workspace = true }
sanitize-filename = { workspace = true }
# features[tauri]

View file

@ -0,0 +1,26 @@
use onelauncher_core::api::java;
use onelauncher_core::api::proxy::ProxyDynamic;
use onelauncher_core::error::LauncherResult;
use onelauncher_core::initialize_core;
use onelauncher_core::store::{CoreOptions, State};
#[tokio::main]
async fn main() -> LauncherResult<()> {
initialize_core(CoreOptions::default(), ProxyDynamic::new()).await?;
let _ = State::get().await?;
let packages = java::get_zulu_packages().await?;
let Some(pkg) = packages.first() else {
return Err(anyhow::anyhow!("no zulu package found").into());
};
println!("found package: {pkg:#?}");
let path = java::install_java_package(pkg).await?;
println!("java package installed at '{path:?}'");
Ok(())
}

View file

@ -1,16 +1,19 @@
use std::collections::HashMap;
use std::path::PathBuf;
use async_zip::base::read::mem::ZipFileReader;
use futures::TryStreamExt;
use interpulse::api::minecraft::VersionInfo;
use onelauncher_entity::{java_versions, setting_profiles};
use reqwest::Method;
use serde::{Deserialize, Serialize};
use tokio::io::AsyncWriteExt;
use crate::api::ingress::{init_ingress, init_ingress_opt, send_ingress};
use crate::constants::JAVA_BIN;
use crate::error::LauncherResult;
use crate::store::Dirs;
use crate::store::ingress::{IngressType, SubIngress};
use crate::utils::http::{fetch_advanced, fetch_json};
use crate::store::ingress::IngressType;
use crate::utils::http;
use crate::utils::io::{self, IOError};
pub mod dao;
@ -44,15 +47,11 @@ pub fn get_java_bin() -> PathBuf {
.join("Contents")
.join("Home")
.join("bin")
.join("java")
.join(JAVA_BIN)
}
#[cfg(target_os = "windows")]
#[cfg(not(target_os = "macos"))]
{
PathBuf::new().join("bin").join("javaw.exe")
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
PathBuf::new().join("bin").join("java")
PathBuf::new().join("bin").join(JAVA_BIN)
}
}
@ -65,7 +64,7 @@ pub struct JavaInfo {
}
#[onelauncher_macro::specta]
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct JavaPackage {
pub download_url: String,
pub name: PathBuf,
@ -88,15 +87,15 @@ pub async fn check_java_runtime(
.await?;
let id = id.as_ref();
let dir = io::tempdir()?;
let file = dir.path().join("JavaInfo.class");
let dir = io::tempdir().await?;
let file = dir.dir_path().join("JavaInfo.class");
id.map(async |id| send_ingress(id, 25.0).await);
io::write(&file, JAVA_INFO_CLASS).await?;
id.map(async |id| send_ingress(id, 25.0).await);
let java_info = tokio::process::Command::new(absolute_path)
.arg("-cp")
.arg(dir.path())
.arg(dir.dir_path())
.arg("JavaInfo")
.env_remove("_JAVA_OPTIONS")
.output()
@ -207,7 +206,7 @@ pub async fn prepare_java(major: u32) -> LauncherResult<java_versions::Model> {
fn find_java_in_path(mut found: Vec<PathBuf>) -> Vec<PathBuf> {
if let Ok(path) = std::env::var("PATH") {
for path_entry in path.split(':') {
let java = PathBuf::from(path_entry).join("java");
let java = PathBuf::from(path_entry).join(JAVA_BIN);
if java.exists() && java.is_file() && !found.contains(&java) {
found.push(java);
}
@ -264,7 +263,7 @@ fn internal_locate_java() -> Vec<PathBuf> {
if let Ok(entries) = std::fs::read_dir(base_path) {
for entry in entries.flatten() {
if entry.file_type().map_or(false, |ft| ft.is_dir()) {
let java_exe = entry.path().join("bin").join("java.exe");
let java_exe = entry.path().join(get_java_bin());
if java_exe.exists() {
java_homes.push(java_exe);
}
@ -276,14 +275,14 @@ fn internal_locate_java() -> Vec<PathBuf> {
// env vars
if let Ok(java_home) = std::env::var("JAVA_HOME") {
let path_buf = PathBuf::from(java_home);
if path_buf.join("bin").join("java.exe").exists() {
if path_buf.join("bin").join(JAVA_BIN).exists() {
java_homes.push(path_buf);
}
}
if let Ok(path) = std::env::var("PATH") {
for path_entry in path.split(';') {
let java_exe = PathBuf::from(path_entry).join("java.exe");
let java_exe = PathBuf::from(path_entry).join(JAVA_BIN);
if java_exe.exists() {
if let Some(bin_dir) = java_exe.parent() {
if let Some(java_home) = bin_dir.parent() {
@ -363,7 +362,7 @@ fn internal_locate_java() -> Vec<PathBuf> {
}
pub async fn get_zulu_packages() -> LauncherResult<Vec<JavaPackage>> {
fetch_json::<Vec<JavaPackage>>(
http::fetch_json::<Vec<JavaPackage>>(
reqwest::Method::GET,
format!(
"https://api.azul.com/metadata/v1/zulu/packages/?os={}&arch={}&archive_type=zip&java_package_type=jre&javafx_bundled=false&latest=true&release_status=ga&availability_types=CA&certifications=tck&page=1&page_size=100",
@ -384,16 +383,14 @@ pub async fn install_java_from_major(version: u32) -> LauncherResult<PathBuf> {
.find(|p| p.java_version.contains(&version))
.ok_or_else(|| anyhow::anyhow!("Could not find a java package for version {}", version))?;
install_java_package(package).await
install_java_package(&package).await
}
pub async fn install_java_package(package: JavaPackage) -> LauncherResult<PathBuf> {
pub async fn install_java_package(package: &JavaPackage) -> LauncherResult<PathBuf> {
const INGRESS_TOTAL: f64 = 100.0;
const INGRESS_TASKS: f64 = 4.0;
const INGRESS_STEP: f64 = INGRESS_TOTAL / INGRESS_TASKS;
let version = package.download_url;
let ingress_id = init_ingress(
IngressType::JavaPrepare,
&format!(
@ -404,37 +401,40 @@ pub async fn install_java_package(package: JavaPackage) -> LauncherResult<PathBu
)
.await?;
let file = fetch_advanced(
reqwest::Method::GET,
&version,
None,
None,
None,
Some(&SubIngress::new(&ingress_id, INGRESS_STEP)),
)
.await?;
// send the request
let res = http::request(Method::GET, &package.download_url).await?;
let java_dir = Dirs::get_java_dir().await?;
let archive = ZipFileReader::new(file.to_vec())
.await
.map_err(IOError::AsyncZipError)?;
if let Some(entry) = archive.file().entries().first()
&& let Some(dir) = entry
.filename()
.clone()
.into_string()
.unwrap()
.split('/')
.next()
{
let path = java_dir.join(dir);
if path.exists() {
io::remove_dir_all(&path).await?;
// prepare to download
let size = res.content_length().unwrap_or(0);
let mut tmp_archive = io::tempfile().await?;
let mut downloaded: u64 = 0;
let mut last_ingress_prog: f64 = 0.0;
// download to tmpfile
let mut stream = res.bytes_stream();
while let Some(bytes) = stream.try_next().await? {
let byte_len = bytes.len() as u64;
downloaded += byte_len;
tmp_archive.write_all(&bytes).await.map_err(IOError::from)?;
if size > 0 {
let absolute = (downloaded as f64 / size as f64) * INGRESS_STEP;
let delta = absolute - last_ingress_prog;
send_ingress(&ingress_id, delta).await?;
last_ingress_prog = absolute;
}
}
io::unzip_bytes(archive.data().to_vec(), &java_dir).await?;
tmp_archive.flush().await.map_err(IOError::from)?;
// extract file
io::extract_zip(tmp_archive.file_path(), &java_dir).await?;
send_ingress(&ingress_id, INGRESS_STEP).await?;
// drop (which in turn closes / deletes the tempfile)
drop(tmp_archive);
let mut base_path = java_dir.join(
package
@ -453,18 +453,10 @@ pub async fn install_java_package(package: JavaPackage) -> LauncherResult<PathBu
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(".");
base_path = base_path
.join(format!("zulu-{java_version}.jre"))
.join("Contents")
.join("Home")
.join("bin")
.join("java");
base_path = base_path.join(format!("zulu-{java_version}.jre"));
}
#[cfg(not(target_os = "macos"))]
{
base_path = base_path.join("bin").join(crate::constants::JAVA_BIN);
}
base_path = base_path.join(get_java_bin());
Ok(base_path)
}

View file

@ -18,6 +18,8 @@ pub struct Settings {
pub max_concurrent_requests: usize,
pub settings_version: u32,
pub native_window_frame: bool,
#[cfg(feature = "tauri")]
pub show_tanstack_dev_tools: bool,
}
@ -32,6 +34,8 @@ impl Default for Settings {
max_concurrent_requests: 25,
settings_version: 1,
native_window_frame: false,
#[cfg(feature = "tauri")]
show_tanstack_dev_tools: tauri::is_dev(),
}
}

View file

@ -4,12 +4,12 @@ use std::sync::Arc;
use async_compression::tokio::bufread::GzipDecoder;
use async_stream::try_stream;
use async_tempfile::{TempDir, TempFile};
use async_zip::StoredZipEntry;
use async_zip::base::read::WithoutEntry;
use futures::{Stream, TryStreamExt, pin_mut};
use serde::Serialize;
use serde::de::DeserializeOwned;
use tempfile::TempDir;
use tokio_util::compat::TokioAsyncWriteCompatExt;
/// A wrapper around generic and unhelpful [`std::io::Error`] messages.
@ -46,6 +46,12 @@ pub enum IOError {
#[skip]
async_zip::error::ZipError,
),
#[error("tempfile error: {0}")]
TempFileError(
#[from]
#[skip]
async_tempfile::Error,
),
}
impl<P: AsRef<std::path::Path>> From<(P, std::io::Error)> for IOError {
@ -256,13 +262,13 @@ pub async fn remove_file(path: impl AsRef<std::path::Path>) -> Result<(), IOErro
}
/// Creates a temporary directory.
pub fn tempdir() -> Result<TempDir, IOError> {
Ok(tempfile::tempdir()?)
pub async fn tempdir() -> Result<TempDir, IOError> {
Ok(TempDir::new().await?)
}
/// Creates a temporary file.
pub fn tempfile() -> Result<std::fs::File, IOError> {
Ok(tempfile::tempfile()?)
pub async fn tempfile() -> Result<TempFile, IOError> {
Ok(TempFile::new().await?)
}
/// Reads a zip archive from a byte array

View file

@ -1,33 +0,0 @@
[package]
name = "onelauncher_utils"
description = "rust utils for onelauncher"
version = { workspace = true }
license = { workspace = true }
edition = { workspace = true }
repository = { workspace = true }
documentation = { workspace = true }
readme = { workspace = true }
homepage = { workspace = true }
authors = { workspace = true }
[lints]
workspace = true
[dependencies]
serde = { workspace = true }
uuid = { workspace = true }
async-channel = { workspace = true }
futures = { workspace = true }
pin-project-lite = { workspace = true }
tokio = { workspace = true }
tempfile = { workspace = true }
sys-info = { workspace = true }
zip = { workspace = true }
dunce = { workspace = true }
async-compression = { workspace = true }
chrono = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
nom = { workspace = true }
regex = { workspace = true }
interpulse = { workspace = true }

View file

@ -1,14 +0,0 @@
{
"name": "@onelauncher/utils",
"version": "0.1.0-alpha.2",
"description": "Next-generation open source Minecraft launcher utils",
"license": "GPL-3.0-only",
"repository": {
"type": "git",
"url": "git@github.com:polyfrost/onelauncher",
"directory": "packages/utils"
},
"bugs": "https://github.com/polyfrost/onelauncher/issues",
"sideEffects": false,
"scripts": {}
}

View file

@ -1,245 +0,0 @@
//! **IO Utilities**
//!
//! Async wrapper around [`tokio::fs`] and [`std::io::Error`] for our error system.
use async_compression::tokio::bufread::GzipDecoder;
use std::io::Write;
use tempfile::NamedTempFile;
use tokio::io::AsyncReadExt;
use tokio::task::spawn_blocking;
/// A wrapper around generic and unhelpful [`std::io::Error`] messages.
#[derive(Debug, thiserror::Error)]
#[serde(tag = "type", content = "data")]
pub enum IOError {
/// A wrapped [`std::io::Error`] along with the path involved in the error.
#[error("error acessing path: {source}, path: {path}")]
IOErrorWrapper {
#[source]
source: std::io::Error,
path: String,
},
/// A wrapped [`zip::result::ZipError`].
#[error(transparent)]
ZipError(#[from] zip::result::ZipError),
/// A wrapped [`std::io::Error`].
#[error(transparent)]
IOError(#[from] std::io::Error),
}
impl<P: AsRef<std::path::Path>> From<(P, std::io::Error)> for IOError {
fn from((path, source): (P, std::io::Error)) -> Self {
Self::IOErrorWrapper {
source,
path: path.as_ref().to_string_lossy().to_string(),
}
}
}
#[derive(Debug, thiserror::Error)]
#[serde(tag = "type", content = "data")]
#[error("received a non UTF-8 path: <lossy_path='{}'>", .0.to_string_lossy())]
pub struct NonUtf8PathError(pub Box<std::path::Path>);
impl IOError {
/// Converts a [`std::io::Error`] into an [`IOError`].
#[must_use]
pub const fn from(source: std::io::Error) -> Self {
Self::IOError(source)
}
/// Converts a [`zip::result::ZipError`] into an [`IOError`].
#[must_use]
pub const fn from_zip(source: zip::result::ZipError) -> Self {
Self::ZipError(source)
}
/// Converts a [`std::io::Error`] and the path involved in the error into an [`IOErrror`].
pub fn with_path(source: std::io::Error, path: impl AsRef<std::path::Path>) -> Self {
let path = path.as_ref().to_string_lossy().to_string();
Self::IOErrorWrapper { source, path }
}
}
/// An OS specific wrapper of [`std::fs::canonicalize`], but on Windows it outputs the most compatible form of a path instead of UNC.
pub fn canonicalize(path: impl AsRef<std::path::Path>) -> Result<std::path::PathBuf, IOError> {
let path = path.as_ref();
dunce::canonicalize(path).map_err(|e| IOError::IOErrorWrapper {
source: e,
path: path.to_string_lossy().to_string(),
})
}
/// Returns a stream over the entries within a directory.
pub async fn read_dir(path: impl AsRef<std::path::Path>) -> Result<tokio::fs::ReadDir, IOError> {
let path = path.as_ref();
tokio::fs::read_dir(path)
.await
.map_err(|e| IOError::IOErrorWrapper {
source: e,
path: path.to_string_lossy().to_string(),
})
}
/// Creates a directory if they are missing.
pub async fn create_dir(path: impl AsRef<std::path::Path>) -> Result<(), IOError> {
let path = path.as_ref();
if path.exists() {
return Ok(());
}
tokio::fs::create_dir(path)
.await
.map_err(|e| IOError::IOErrorWrapper {
source: e,
path: path.to_string_lossy().to_string(),
})
}
/// Recursively creates a directory and all of its parent components if they are missing.
pub async fn create_dir_all(path: impl AsRef<std::path::Path>) -> Result<(), IOError> {
let path = path.as_ref();
tokio::fs::create_dir_all(path)
.await
.map_err(|e| IOError::IOErrorWrapper {
source: e,
path: path.to_string_lossy().to_string(),
})
}
/// Removes a directory at this path, after removing all its contents. Use carefully!
pub async fn remove_dir_all(path: impl AsRef<std::path::Path>) -> Result<(), IOError> {
let path = path.as_ref();
tokio::fs::remove_dir_all(path)
.await
.map_err(|e| IOError::IOErrorWrapper {
source: e,
path: path.to_string_lossy().to_string(),
})
}
/// Creates a future which will open a gzip compressed file for reading and read the entire contents into a string and return said string.
pub async fn read_gz_to_string(path: impl AsRef<std::path::Path>) -> Result<String, IOError> {
let mut f = tokio::fs::File::open(path).await?;
let mut buf = vec![];
f.read_to_end(&mut buf).await?;
let mut decoder = GzipDecoder::new(buf.as_slice());
let mut dst = String::new();
decoder.read_to_string(&mut dst).await?;
Ok(dst)
}
/// Creates a future which will open a file for reading and read the entire contents into a string and return said string.
pub async fn read_to_string(path: impl AsRef<std::path::Path>) -> Result<String, IOError> {
let path = path.as_ref();
tokio::fs::read_to_string(path)
.await
.map_err(|e| IOError::IOErrorWrapper {
source: e,
path: path.to_string_lossy().to_string(),
})
}
/// Reads the entire contents of a file into a bytes vector.
pub async fn read(path: impl AsRef<std::path::Path>) -> Result<Vec<u8>, IOError> {
let path = path.as_ref();
tokio::fs::read(path)
.await
.map_err(|e| IOError::IOErrorWrapper {
source: e,
path: path.to_string_lossy().to_string(),
})
}
/// Asynchrously write to a tempfile that is then transferred to an official [`AsRef<Path>`].
pub async fn write(
path: impl AsRef<std::path::Path>,
data: impl AsRef<[u8]>,
) -> Result<(), IOError> {
let path = path.as_ref().to_owned();
let data = data.as_ref().to_owned();
spawn_blocking(move || {
let cloned_path = path.clone();
sync_write(data, path).map_err(|e| IOError::IOErrorWrapper {
source: e,
path: cloned_path.to_string_lossy().to_string(),
})
})
.await
.map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "tokio task failed"))??;
Ok(())
}
/// Write to a tempfile that is then transferred to an official [`AsRef<Path>`].
fn sync_write(
data: impl AsRef<[u8]>,
path: impl AsRef<std::path::Path>,
) -> Result<(), std::io::Error> {
let mut tempfile = NamedTempFile::new_in(path.as_ref().parent().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::Other,
"failed to get parent directory of a tempfile",
)
})?)?;
tempfile.write_all(data.as_ref())?;
let tmp_path = tempfile.into_temp_path();
let path = path.as_ref();
// this is a sorta dangerous call but shouldnt matter because of async
tmp_path.persist(path)?;
std::io::Result::Ok(())
}
/// Renames a file or directory to a new name, replacing the original file if `to` already exists.
pub async fn rename(
from: impl AsRef<std::path::Path>,
to: impl AsRef<std::path::Path>,
) -> Result<(), IOError> {
let from = from.as_ref();
let to = to.as_ref();
tokio::fs::rename(from, to)
.await
.map_err(|e| IOError::IOErrorWrapper {
source: e,
path: from.to_string_lossy().to_string(),
})
}
/// Copies the contents of one file to another. This function will also copy the permission bits of the original file to the destination file. This function will overwrite the contents of to.
pub async fn copy(
from: impl AsRef<std::path::Path>,
to: impl AsRef<std::path::Path>,
) -> Result<u64, IOError> {
let from = from.as_ref();
let to = to.as_ref();
tokio::fs::copy(from, to)
.await
.map_err(|e| IOError::IOErrorWrapper {
source: e,
path: from.to_string_lossy().to_string(),
})
}
/// Removes a file from the filesystem.
pub async fn remove_file(path: impl AsRef<std::path::Path>) -> Result<(), IOError> {
let path = path.as_ref();
tokio::fs::remove_file(path)
.await
.map_err(|e| IOError::IOErrorWrapper {
source: e,
path: path.to_string_lossy().to_string(),
})
}
///// Returns the SHA512 hash of a file.
// pub fn sha512(path: impl AsRef<std::path::Path>) -> Result<String, IOError> {
// let path = path.as_ref();
// let mut file = std::fs::File::open(path)?;
// let mut hasher = sha2::Sha512::new();
// std::io::copy(&mut file, &mut hasher)?;
// Ok(format!("{:x}", hasher.finalize()))
// }

View file

@ -1,47 +0,0 @@
//! **`OneLauncher` Utilities**
//! Standard utilities for use within all Rust subprojects.
//!
//! - [`logging`]: Async utilities for log4j parsing with [`nom`].
//! - [`platform`]: Async utilities for managing OS-specific [`interpulse`] operations and rules.
//! - [`io`]: Async wrapper around [`tokio::fs`] and [`std::io::Error`] for our error system.
pub mod io;
pub mod logging;
pub mod platform;
/// Simple macro that takes a mutable reference and inserts it into a codeblock closure
/// as an owned reference.
///
/// mutable reference gets epically owned by free thinking macro!!!!! (not clickbait)
/// im going insane insane insane insane insane insane insane insane insane
#[macro_export]
macro_rules! ref_owned {
($id:ident = $init:expr => $transform:block) => {{
let mut it = $init;
{
let $id = &mut it;
$transform;
}
it
}};
}
/// Combines an iterator of `T` and an iterator of [`Option<T>`],
/// removing and ensuring the safety of any [`None`] values in the process.
pub fn chain_iterator<T>(
required: impl IntoIterator<Item = T>,
optional: impl IntoIterator<Item = Option<T>>,
) -> Vec<T> {
required
.into_iter()
.map(Some)
.chain(optional)
.flatten()
.collect()
}
#[inline]
#[must_use]
pub fn test_type_of<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}

View file

@ -1,318 +0,0 @@
//! Async utilities for parsing Log4j entries with [`nom`].
//! From <https://github.com/gorilla-devs/GDLauncher-Carbon/blob/develop/crates/carbon_parsing/src/log.rs>
use nom::branch::alt;
use nom::bytes::streaming::{tag, take_until};
use nom::character::streaming::{char, multispace0, u64};
use nom::combinator::{map, value};
use nom::error::ParseError;
use nom::multi::count;
use nom::sequence::{delimited, preceded, separated_pair, tuple};
use nom::IResult;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct LogEntry<'a> {
pub logger: &'a str,
pub level: LogLevel,
pub timestamp: u64,
pub thread_name: &'a str,
pub message: &'a str,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
fn whitespace<'a, O, E: ParseError<&'a str>>(
inner: impl FnMut(&'a str) -> IResult<&'a str, O, E>,
) -> impl FnMut(&'a str) -> IResult<&'a str, O, E> {
delimited(multispace0, inner, multispace0)
}
pub fn parse_logentry(input: &str) -> IResult<&str, LogEntry<'_>> {
let (o, (attributes, _, message)) = preceded(
multispace0,
alt((
delimited(
tag("<log4j:Event"),
tuple((attributes, tag(">"), whitespace(message))),
tag("</log4j:Event>"),
),
plain_text,
)),
)(input)?;
let Attributes {
logger,
level,
timestamp,
thread_name,
} = attributes;
Ok((
o,
LogEntry {
logger,
level,
timestamp,
thread_name,
message,
},
))
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Attributes<'a> {
pub logger: &'a str,
pub level: LogLevel,
pub timestamp: u64,
pub thread_name: &'a str,
}
fn attributes(input: &str) -> IResult<&str, Attributes<'_>> {
let (o, attributes) = count(whitespace(attribute), 4)(input)?;
macro_rules! extract_attribute {
($field:ident) => {{
let err = nom::Err::Error(nom::error::Error::from_error_kind(
o,
nom::error::ErrorKind::Alt,
));
let Attribute::$field(field) = attributes
.iter()
.copied()
.find(|a| matches!(a, Attribute::$field(_)))
.ok_or(err)?
else {
unreachable!();
};
field
}};
}
Ok((
o,
Attributes {
logger: extract_attribute!(Logger),
level: extract_attribute!(Level),
timestamp: extract_attribute!(Timestamp),
thread_name: extract_attribute!(ThreadName),
},
))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Attribute<'a> {
Logger(&'a str),
Level(LogLevel),
Timestamp(u64),
ThreadName(&'a str),
}
fn attribute(input: &str) -> IResult<&str, Attribute<'_>> {
alt((attr_logger, attr_timestamp, attr_level, attr_thread))(input)
}
fn attr_logger(input: &str) -> IResult<&str, Attribute<'_>> {
map(
separated_pair(
tag("logger"),
whitespace(char('=')),
map(quoted_string, Attribute::Logger),
),
|(_, attr)| attr,
)(input)
}
fn attr_timestamp(input: &str) -> IResult<&str, Attribute<'_>> {
map(
separated_pair(
tag("timestamp"),
whitespace(char('=')),
delimited(char('"'), map(u64, Attribute::Timestamp), char('"')),
),
|(_, attr)| attr,
)(input)
}
fn attr_level(input: &str) -> IResult<&str, Attribute<'_>> {
map(
separated_pair(
tag("level"),
whitespace(char('=')),
delimited(char('"'), map(level, Attribute::Level), char('"')),
),
|(_, attr)| attr,
)(input)
}
fn attr_thread(input: &str) -> IResult<&str, Attribute<'_>> {
map(
separated_pair(
tag("thread"),
whitespace(char('=')),
map(quoted_string, Attribute::ThreadName),
),
|(_, attr)| attr,
)(input)
}
fn quoted_string(input: &str) -> IResult<&str, &str> {
delimited(char('"'), take_until("\""), char('"'))(input)
}
fn level(input: &str) -> IResult<&str, LogLevel> {
alt((
value(LogLevel::Trace, tag("TRACE")),
value(LogLevel::Debug, tag("DEBUG")),
value(LogLevel::Info, tag("INFO")),
value(LogLevel::Warn, tag("WARN")),
value(LogLevel::Error, tag("ERROR")),
))(input)
}
/// Parses the message of the event.
fn message(input: &str) -> IResult<&str, &str> {
delimited(
tag("<log4j:Message>"),
whitespace(delimited(tag("<![CDATA["), take_until("]]>"), tag("]]>"))),
tag("</log4j:Message>"),
)(input)
}
#[allow(clippy::cast_sign_loss)]
fn plain_text(input: &str) -> IResult<&str, (Attributes<'_>, &str, &str)> {
map(take_until("<"), |text| {
(
Attributes {
logger: "OneLauncher",
level: LogLevel::Info,
timestamp: chrono::Local::now().timestamp_millis() as u64,
thread_name: "N/A",
},
"",
text,
)
})(input)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn parse_message() {
message(
r"<log4j:Message>
<![CDATA[200 Datafixer optimizations took 2000 years]]>
</log4j:Message>",
)
.unwrap();
}
#[test]
fn parse_attributes() {
let (_, attributes) = attributes(
r#"
logger="com.mojang.datafixers.DataFixerBuilder"
timestamp="3213819318239"
level="DEBUG"
thread="Datafixer"
>
"#,
)
.unwrap();
assert_eq!(
attributes,
Attributes {
logger: "com.mojang.datafixers.DataFixerBuilder",
level: LogLevel::Debug,
timestamp: 3_213_819_318_239,
thread_name: "Datafixer"
}
);
}
#[test]
fn parse_logger_attribute() {
let (_, attr) = attr_logger(r#"logger="com.mojang.datafixers.DataFixerBuilder""#).unwrap();
assert_eq!(
attr,
Attribute::Logger("com.mojang.datafixers.DataFixerBuilder")
);
}
#[test]
fn parse_level_attribute() {
let (_, attr) = attr_level(r#"level="INFO""#).unwrap();
assert_eq!(attr, Attribute::Level(LogLevel::Info));
}
#[test]
fn parse_timestamp_attribute() {
let (_, attr) = attr_timestamp(r#"timestamp="313213473127""#).unwrap();
assert_eq!(attr, Attribute::Timestamp(313_213_473_127));
}
#[test]
fn parse_thread_attribute() {
let (_, attr) = attr_thread(r#"thread="Datafixer""#).unwrap();
assert_eq!(attr, Attribute::ThreadName("Datafixer"));
}
#[test]
fn parse_quoted_string() {
let (_, res) = quoted_string(r#""girlskissing""#).unwrap();
assert_eq!(res, "girlskissing");
}
#[test]
fn parse_single_entry() {
let (_, entry) = parse_logentry(
r#"
<log4j:Event
logger="com.mojang.datafixers.DataFixerBuilder"
timestamp="3213712731731"
level="DEBUG"
thread="Datafixer"
>
<log4j:Message>
<![CDATA[200 Datafixer optimizations took 2000 years]]>
</log4j:Message>
</log4j:Event>
"#,
)
.unwrap();
assert_eq!(
entry,
LogEntry {
logger: "com.mojang.datafixers.DataFixerBuilder",
level: LogLevel::Debug,
timestamp: 3_213_712_731_731,
thread_name: "Datafixer",
message: "200 Datafixer optimizations took 2000 years",
}
);
}
// #[test]
// fn parse_sample_log_entries() {
// let mut input = include_str!("../../fixtures/test_log.xml");
// while let Ok((o, _)) = parse_logentry(input) {
// input = o;
// }
// assert_eq!(input, "exit code: 0");
// }
}

View file

@ -1,88 +0,0 @@
//! **Platform Utilities**
//!
//! Async utilities for managing OS-specific [`interpulse`] operations and rules.
use interpulse::api::minecraft::{Os, OsRule};
use regex::Regex;
/// An extension of [`Os`] for utilities on converting the current system's operating system
/// into an [`Os`] structure for simple management.
pub trait OsExt {
/// Get the [`Os`] of the current system
fn native() -> Self;
/// Get the [`Os`] of the current system along with it's arch from the `java_arch`.
fn native_arch(java_arch: &str) -> Self;
}
impl OsExt for Os {
fn native_arch(java_arch: &str) -> Self {
if std::env::consts::OS == "windows" {
if java_arch == "aarch64" {
Self::WindowsArm64
} else {
Self::Windows
}
} else if std::env::consts::OS == "linux" {
if java_arch == "aarch64" {
Self::LinuxArm64
} else if java_arch == "arm" {
Self::LinuxArm32
} else {
Self::Linux
}
} else if std::env::consts::OS == "macos" {
if java_arch == "aarch64" {
Self::OsxArm64
} else {
Self::Osx
}
} else {
Self::Unknown
}
}
fn native() -> Self {
match std::env::consts::OS {
"windows" => Self::Windows,
"macos" => Self::Osx,
"linux" => Self::Linux,
_ => Self::Unknown,
}
}
}
/// Handles an os-specific [`OsRule`], returning if the [`Os`] matches the rule.
#[must_use]
pub fn os_rule(rule: &OsRule, java_arch: &str, updated: bool) -> bool {
let mut rule_match = true;
if let Some(ref arch) = rule.arch {
rule_match &= !matches!(arch.as_str(), "x86" | "arm");
}
if let Some(name) = &rule.name {
if updated && (name != &Os::LinuxArm64 || name != &Os::LinuxArm32) {
rule_match &= &Os::native() == name || &Os::native_arch(java_arch) == name;
} else {
rule_match &= &Os::native_arch(java_arch) == name;
}
}
if let Some(version) = &rule.version {
if let Ok(regex) = Regex::new(version.as_str()) {
rule_match &= regex.is_match(&sys_info::os_release().unwrap_or_default());
}
}
rule_match
}
/// Gets the seperator between Java classpaths in metadata (`";"` on Windows, `";"` on others).
#[must_use]
pub fn classpath_separator(java_arch: &str) -> &'static str {
match Os::native_arch(java_arch) {
Os::Osx | Os::OsxArm64 | Os::Linux | Os::LinuxArm32 | Os::LinuxArm64 | Os::Unknown => ":",
Os::Windows | Os::WindowsArm64 => ";",
}
}