diff options
| author | 2025-01-07 03:43:43 -0600 | |
|---|---|---|
| committer | 2025-01-07 03:43:43 -0600 | |
| commit | 5d68d164d9a7bff8f3015257f25eb71c44829ddf (patch) | |
| tree | c6fa8bd5e7c4dc4e14268f3c34138b5bf92d3746 /src | |
| parent | idr what I changed (diff) | |
untested moment (remove reqwest)
Diffstat (limited to 'src')
| -rw-r--r-- | src/launcher.rs | 71 | ||||
| -rw-r--r-- | src/launcher/constants.rs | 7 | ||||
| -rw-r--r-- | src/launcher/download.rs | 36 | ||||
| -rw-r--r-- | src/launcher/request.rs | 139 | ||||
| -rw-r--r-- | src/launcher/version.rs | 5 |
5 files changed, 250 insertions, 8 deletions
diff --git a/src/launcher.rs b/src/launcher.rs index 4c4f762..5dfc68a 100644 --- a/src/launcher.rs +++ b/src/launcher.rs @@ -2,16 +2,21 @@ mod constants; mod version; mod profile; mod strsub; +mod download; +mod request; use std::borrow::Cow; use std::collections::HashMap; use std::env::consts::{ARCH, OS}; use std::error::Error; use std::fmt::{Display, Formatter}; +use std::io::ErrorKind; use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; -use sha1_smol::Digest; +use sha1_smol::Sha1; use sysinfo::System; +use tokio::fs::File; +use tokio::io::AsyncReadExt; use version::VersionList; use profile::{Instance, Profile}; use crate::launcher::version::{VersionResolveError, VersionResult}; @@ -57,14 +62,22 @@ pub enum LaunchError { impl Display for LaunchError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self { - LaunchError::UnknownVersion(id) => write!(f, "Unknown version ID: {id}"), + LaunchError::UnknownVersion(id) => write!(f, "unknown version id: {id}"), LaunchError::LoadVersion(e) => write!(f, "error loading remote version: {e}"), LaunchError::ResolveVersion(e) => write!(f, "error resolving remote version: {e}") } } } -impl Error for LaunchError {} +impl Error for LaunchError { + fn cause(&self) -> Option<&dyn Error> { + match &self { + LaunchError::LoadVersion(e) => Some(e.as_ref()), + LaunchError::ResolveVersion(e) => Some(e), + _ => None + } + } +} impl Launcher { // FIXME: more descriptive error type por favor @@ -133,6 +146,23 @@ impl Launcher { } } +#[derive(Debug, Clone)] +enum LibraryError { + InvalidName(String), + IOError(ErrorKind) +} + +impl Display for LibraryError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + LibraryError::InvalidName(name) => write!(f, "invalid name: {name}"), + LibraryError::IOError(e) => write!(f, "io error reading library: {e}"), + } + } +} + +impl Error for LibraryError {} + impl LibraryRepository { fn get_artifact_base_dir(name: &str) -> Option<PathBuf> { let end_of_gid = name.find(':')?; @@ -167,8 +197,37 @@ impl LibraryRepository { Some(p) } - async fn should_redownload(&self, lib: &Library, classifier: Option<&str>) -> Result<bool, Box<dyn Error>> { - let path = Self::get_artifact_path(lib.name.as_str(), classifier); + async fn should_redownload(&self, lib: &Library, classifier: Option<&str>) -> Result<bool, LibraryError> { + let path = Self::get_artifact_path(lib.name.as_str(), classifier) + .map_or_else(|| Err(LibraryError::InvalidName(lib.name.clone())), |p| Ok(p))?; + + let mut f = match File::open(path).await { + Ok(f) => f, + Err(e) => return match e.kind() { + ErrorKind::NotFound => Ok(true), + e => Err(LibraryError::IOError(e)) + } + }; + + let mut data = [0u8; 4096]; + let mut sha1 = Sha1::new(); + + loop { + let n = match f.read(&mut data).await { + Ok(n) => n, + Err(e) => return match e.kind() { + ErrorKind::Interrupted => continue, + kind => Err(LibraryError::IOError(kind)) + } + }; + + if n == 0 { + break; // we reached the end of the file + } + + sha1.update(&data[..n]); + } + todo!() } @@ -185,7 +244,7 @@ impl SystemInfo { let mut os_version = System::os_version().unwrap_or_default(); if os == OperatingSystem::Windows && (os_version.starts_with("10") || os_version.starts_with("11")) { - os_version.replace_range(..2, "10.0"); // java expects this funny business... + os_version.replace_range(..2, "10.0"); // minecraft expects this funny business... } let mut arch = ARCH.to_owned(); diff --git a/src/launcher/constants.rs b/src/launcher/constants.rs index 8a9bd1a..698081b 100644 --- a/src/launcher/constants.rs +++ b/src/launcher/constants.rs @@ -1 +1,8 @@ +use const_format::formatcp; + +const PKG_NAME: &str = env!("CARGO_PKG_NAME"); +const PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); +const CRATE_NAME: &str = env!("CARGO_CRATE_NAME"); + +pub const USER_AGENT: &str = formatcp!("{PKG_NAME}/{PKG_VERSION} (in {CRATE_NAME})"); pub const URL_VERSION_MANIFEST: &str = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; diff --git a/src/launcher/download.rs b/src/launcher/download.rs new file mode 100644 index 0000000..4294d33 --- /dev/null +++ b/src/launcher/download.rs @@ -0,0 +1,36 @@ +use std::path::{Path, PathBuf}; +use sha1_smol::Digest; + +pub trait Download { + fn get_url(&self) -> &str; + fn get_path(&self) -> &Path; + fn get_expect_digest(&self) -> Option<Digest>; + fn get_expect_size(&self) -> Option<usize>; + + fn always_redownload(&self) -> bool; +} + +pub type DownloadJob = dyn Download + Sync + Send; + +pub struct MultiDownloader<'j, 'js> { + jobs: &'js [&'j DownloadJob], + nhandles: usize +} + +impl<'j, 'js> MultiDownloader<'j, 'js> { + pub fn new(jobs: &'js [&'j DownloadJob]) -> MultiDownloader<'j, 'js> { + Self::with_handles(jobs, 8) + } + + pub fn with_handles(jobs: &'js [&'j DownloadJob], nhandles: usize) -> MultiDownloader<'j, 'js> { + assert!(nhandles > 0); + + MultiDownloader { + jobs, nhandles + } + } + + fn do_it(&self) { + + } +}
\ No newline at end of file diff --git a/src/launcher/request.rs b/src/launcher/request.rs new file mode 100644 index 0000000..df89a8b --- /dev/null +++ b/src/launcher/request.rs @@ -0,0 +1,139 @@ +use std::error::Error; +use std::fmt::Display; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; +use curl::easy::{Easy}; +use tokio::sync::oneshot; +use tokio::sync::oneshot::Receiver; +use tokio::task; +use crate::launcher::constants::USER_AGENT; + +// yeah this is basically reqwest but bad (I did not want to rely on both reqwest and curl) + +#[derive(Clone, Copy)] +enum FetchState { + Primed, + Running, + Complete +} + +pub struct EasyFetch { + easy: Option<Easy>, + state: FetchState, + response: Option<Receiver<Result<FetchResult, curl::Error>>> +} + +impl EasyFetch { + fn new(easy: Easy) -> Self { + EasyFetch { + easy: Some(easy), + state: FetchState::Primed, + response: None + } + } + + pub fn get(url: &str) -> Self { + let mut easy = Easy::new(); + easy.useragent(USER_AGENT).expect("couldn't set user agent"); + easy.get(true).expect("couldn't set request method"); + easy.url(url).expect("couldn't set url"); + + Self::new(easy) + } +} + +#[derive(Debug)] +pub struct FetchResult { + easy: Easy, + response_code: u32, + data: Vec<u8>, +} + +#[derive(Debug)] +pub struct FetchResponseError(u32); + +impl FetchResponseError { + pub fn get_code(&self) -> u32 { + self.0 + } +} + +impl Display for FetchResponseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "http response: {}", self.0) + } +} + +impl Error for FetchResponseError {} + +impl FetchResult { + pub fn get_response_code(&self) -> u32 { + self.response_code + } + + pub fn get_data(&self) -> &[u8] { + &self.data + } + + pub fn get_data_string(&self) -> String { + String::from_utf8_lossy(&self.data).to_string() + } + + pub fn error_for_status(self) -> Result<Self, FetchResponseError> { + if self.response_code / 100 == 2 { + Ok(self) + } else { + Err(FetchResponseError(self.response_code)) + } + } +} + +impl Future for EasyFetch { + type Output = Result<FetchResult, curl::Error>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + let self_ref = self.get_mut(); + + match &self_ref.state { + FetchState::Primed => { + self_ref.state = FetchState::Running; + let mut easy = self_ref.easy.take().unwrap(); + let waker = cx.waker().clone(); + + let (tx, rx) = oneshot::channel::<Result<FetchResult, curl::Error>>(); + self_ref.response.replace(rx); + + task::spawn_blocking(move || { + let mut out_data: Vec<u8> = Vec::new(); + let mut transfer = easy.transfer(); + + transfer.write_function(|data| { + out_data.extend_from_slice(data); + Ok(data.len()) + }).expect("infallible curl operation failed"); + + let res = transfer.perform(); + drop(transfer); // have to explicitly drop to release borrow on "easy" + + out_data.shrink_to_fit(); + + tx.send(res.map(|_| FetchResult { + response_code: easy.response_code().expect("querying response code should not fail"), + data: out_data, + easy + })).expect("curl fetch reader hangup (this shouldn't happen)"); + waker.wake(); + }); + + Poll::Pending + }, + FetchState::Running => { + self_ref.state = FetchState::Complete; + Poll::Ready(self_ref.response.take().unwrap().try_recv() + .expect("curl fetch writer hangup or not ready (this shouldn't happen)")) + }, + FetchState::Complete => panic!("fetch polled after completion") + } + } +}
\ No newline at end of file diff --git a/src/launcher/version.rs b/src/launcher/version.rs index f4cdd6c..0337864 100644 --- a/src/launcher/version.rs +++ b/src/launcher/version.rs @@ -6,6 +6,7 @@ use std::path::{Path, PathBuf}; use log::{debug, info, warn}; use sha1_smol::Digest; +use super::request::EasyFetch; use crate::util; use crate::version::{*, manifest::*}; @@ -18,7 +19,7 @@ struct RemoteVersionList { impl RemoteVersionList { async fn new() -> Result<RemoteVersionList, Box<dyn Error>> { - let text = reqwest::get(URL_VERSION_MANIFEST).await?.error_for_status()?.text().await?; + let text = EasyFetch::get(URL_VERSION_MANIFEST).await?.error_for_status()?.get_data_string(); let manifest: VersionManifest = serde_json::from_str(text.as_str())?; let mut versions = HashMap::new(); @@ -45,7 +46,7 @@ impl RemoteVersionList { } // download it - let ver_text = reqwest::get(ver.url.as_str()).await?.error_for_status()?.text().await?; + let ver_text = EasyFetch::get(ver.url.as_str()).await?.error_for_status()?.get_data_string(); // make sure it's valid util::verify_sha1(ver.sha1, ver_text.as_str()) |
