summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar bigfoot547 <[email protected]>2025-01-07 03:43:43 -0600
committerLibravatar bigfoot547 <[email protected]>2025-01-07 03:43:43 -0600
commit5d68d164d9a7bff8f3015257f25eb71c44829ddf (patch)
treec6fa8bd5e7c4dc4e14268f3c34138b5bf92d3746 /src
parentidr what I changed (diff)
untested moment (remove reqwest)
Diffstat (limited to 'src')
-rw-r--r--src/launcher.rs71
-rw-r--r--src/launcher/constants.rs7
-rw-r--r--src/launcher/download.rs36
-rw-r--r--src/launcher/request.rs139
-rw-r--r--src/launcher/version.rs5
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())