Vanilla works but fabric doesn't

This commit is contained in:
2025-08-24 14:16:09 +02:00
commit 997cf37d31
19 changed files with 2919 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
# Generated by Cargo
# will have compiled files and executables
debug
target
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Generated by cargo mutants
# Contains mutation testing data
**/mutants.out*/
# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
Generated
+1539
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "smallauncher_lib"
version = "0.1.1"
description = "smallauncher core library"
edition = "2021"
authors = ["Heráclito de Quadros Saldanha <[email protected]>"]
license = "MIT OR Unlicense"
keywords = ["smallauncher", "minecraft", "launcher"]
homepage = "https://smallauncher.com/"
[dependencies]
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.115"
ureq = { version = "2.9.6", features = ["json"] }
log = "0.4.21"
sha1 = "0.10.6"
hex = "0.4"
zip = "2.1.5"
oauth2 = { version = "5.0.0", default-features = false }
open = "5"
uuid = "1.13.1"
md5 = "0.7"
[lints.rust]
unsafe_code = "forbid"
+4
View File
@@ -0,0 +1,4 @@
use_field_init_shorthand = true
newline_style = "Unix"
hard_tabs = true
max_width = 150
+15
View File
@@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
use std::collections;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Assets {
pub objects: collections::HashMap<String, Asset>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Asset {
pub hash: String,
pub size: usize,
}
+75
View File
@@ -0,0 +1,75 @@
use std::collections;
use crate::*;
use serde::{Deserialize, Serialize};
impl JavaVersions {
pub const DEFAULT_URL: &'static str =
"https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json";
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct JavaVersions {
pub linux: PlatformVersion,
pub linux_i386: PlatformVersion,
pub mac_os: PlatformVersion,
pub mac_os_arm64: PlatformVersion,
pub windows_arm64: PlatformVersion,
pub windows_x64: PlatformVersion,
pub windows_x86: PlatformVersion,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct PlatformVersion {
pub java_runtime_alpha: Vec<Version>,
pub java_runtime_beta: Vec<Version>,
pub java_runtime_delta: Vec<Version>,
pub java_runtime_gamma: Vec<Version>,
pub java_runtime_gamma_snapshot: Vec<Version>,
pub jre_legacy: Vec<Version>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Version {
pub manifest: api::meta::Download,
pub version: VersionName,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct VersionName {
pub name: String,
pub released: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum File {
Directory,
Link { target: String },
File { executable: bool, downloads: Downloads },
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct Files {
pub files: collections::HashMap<String, File>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub struct Downloads {
pub raw: Download,
pub lzma: Option<Download>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub struct Download {
pub url: String,
pub size: usize,
pub sha1: String,
}
+45
View File
@@ -0,0 +1,45 @@
use serde::{Deserialize, Serialize};
impl Manifest {
pub const DEFAULT_URL: &'static str = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
#[inline(always)]
pub fn get_latest_snapshot(&self) -> Option<&Version> {
self.get_version(&self.latest.snapshot)
}
#[inline(always)]
pub fn get_latest_release(&self) -> Option<&Version> {
self.get_version(&self.latest.release)
}
pub fn get_version(&self, name: &str) -> Option<&Version> {
for version in &self.versions {
if version.id == name {
return Some(version);
}
}
None
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Manifest {
pub latest: Latest,
pub versions: Vec<Version>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Latest {
pub release: String,
pub snapshot: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Version {
pub id: String,
pub r#type: String,
pub url: String,
pub time: String,
pub release_time: String,
}
+216
View File
@@ -0,0 +1,216 @@
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Version {
pub id: String,
#[serde(default)]
pub arguments: Arguments,
pub asset_index: AssetIndex,
pub downloads: Downloads,
#[serde(default)]
pub java_version: JavaVersion,
pub libraries: Vec<Library>,
pub main_class: String,
pub r#type: VersionType,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "camelCase")]
pub struct JavaVersion {
pub component: String,
pub major_version: u16,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Arguments {
pub game: Vec<Argument>,
pub jvm: Vec<Argument>,
}
impl Default for Arguments {
fn default() -> Self {
Arguments {
game: vec![
Argument::String("--username".to_string()),
Argument::String("${auth_player_name}".to_string()),
Argument::String("--version".to_string()),
Argument::String("${version_name}".to_string()),
Argument::String("--gameDir".to_string()),
Argument::String("${game_directory}".to_string()),
Argument::String("--assetsDir".to_string()),
Argument::String("${assets_root}".to_string()),
Argument::String("--assetIndex".to_string()),
Argument::String("${assets_index_name}".to_string()),
Argument::String("--uuid".to_string()),
Argument::String("${auth_uuid}".to_string()),
Argument::String("--accessToken".to_string()),
Argument::String("${auth_access_token}".to_string()),
Argument::String("--clientId".to_string()),
Argument::String("${clientid}".to_string()),
Argument::String("--xuid".to_string()),
Argument::String("${auth_xuid}".to_string()),
Argument::String("--userType".to_string()),
Argument::String("${user_type}".to_string()),
Argument::String("--versionType".to_string()),
Argument::String("${version_type}".to_string()),
],
jvm: vec![
Argument::String("-Djava.library.path=${natives_directory}".to_string()),
Argument::String("-Djna.tmpdir=${natives_directory}".to_string()),
Argument::String("-Dorg.lwjgl.system.SharedLibraryExtractPath=${natives_directory}".to_string()),
Argument::String("-Dio.netty.native.workdir=${natives_directory}".to_string()),
Argument::String("-Dminecraft.launcher.brand=${launcher_name}".to_string()),
Argument::String("-Dminecraft.launcher.version=${launcher_version}".to_string()),
Argument::String("-cp".to_string()),
Argument::String("${classpath}".to_string()),
],
}
}
}
impl Default for JavaVersion {
#[inline(always)]
fn default() -> Self {
Self {
component: "jre-legacy".to_string(),
major_version: 8,
}
}
}
impl fmt::Display for VersionType {
#[inline(always)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Release => f.write_str("Release"),
Self::Snapshot => f.write_str("Snapshot"),
Self::OldBeta => f.write_str("OldBeta"),
Self::OldAlpha => f.write_str("OldAlpha"),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", untagged)]
pub enum Argument {
String(String),
Object(ArgumentRule),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ArgumentRule {
pub rules: Vec<Rule>,
pub value: ArgumentValue,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", untagged)]
pub enum ArgumentValue {
String(String),
List(Vec<String>),
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AssetIndex {
pub id: String,
pub sha1: String,
pub size: usize,
pub total_size: usize,
pub url: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Downloads {
pub client: Download,
pub server: Option<Download>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Download {
pub url: String,
pub size: usize,
pub sha1: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VersionType {
Release,
Snapshot,
OldBeta,
OldAlpha,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Library {
pub name: String,
pub downloads: Option<LibraryDownload>,
pub natives: Option<serde_json::Value>,
pub rules: Option<Vec<Rule>>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct LibraryDownload {
pub artifact: Option<Artifact>,
pub classifiers: Option<Classifiers>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct Classifiers {
pub natives_linux: Option<Artifact>,
pub natives_osx: Option<Artifact>,
pub natives_windows: Option<Artifact>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Artifact {
pub url: String,
pub path: String,
pub sha1: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Rule {
pub action: Action,
pub os: Option<Os>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Os {
pub name: Option<OsName>,
pub arch: Option<Arch>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum Arch {
X86,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum OsName {
Windows,
Osx,
Linux,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum Action {
Allow,
Disallow,
}
+33
View File
@@ -0,0 +1,33 @@
pub mod assets;
pub mod java;
pub mod manifest;
pub mod meta;
use serde::de::DeserializeOwned;
use crate::*;
pub(crate) fn get_from_url<T: DeserializeOwned>(url: &str) -> Result<T, error::Error> {
Ok(ureq::get(url).call()?.into_json()?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn java_version_api() {
let _java_versions: java::JavaVersions = get_from_url(java::JavaVersions::DEFAULT_URL).unwrap();
}
#[test]
fn manifest_api() {
let _manifest: manifest::Manifest = get_from_url(manifest::Manifest::DEFAULT_URL).unwrap();
}
#[test]
fn all_versions_api() {
let manifest: manifest::Manifest = get_from_url(manifest::Manifest::DEFAULT_URL).unwrap();
for version in manifest.versions {
let _version: meta::Version = get_from_url(&version.url).unwrap();
}
}
}
+147
View File
@@ -0,0 +1,147 @@
use std::io;
use std::net;
use crate::*;
pub const CLIENT_ID: &'static str = match option_env!("CLIENT_ID") {
Some(client_id) => client_id,
None => "74909cec-49b6-4fee-aa60-1b2a57ef72e1",
};
pub const AUTH_URI: &'static str = "https://login.live.com/oauth20_authorize.srf";
const HTTP_OK: &'static [u8] = b"
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 2
ok
";
#[derive(Debug, serde::Deserialize)]
pub struct Token {
pub access_token: String,
pub refresh_token: String,
}
#[derive(Debug, serde::Deserialize)]
pub struct AuthResponse {
pub access_token: String,
pub token_type: String,
pub expires_in: u64,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct XboxAuthResponse {
pub issue_instant: String,
pub not_after: String,
pub token: String,
pub display_claims: DisplayClaims,
}
#[derive(Debug, serde::Deserialize)]
pub struct DisplayClaims {
pub xui: Vec<Xui>,
}
#[derive(Debug, serde::Deserialize)]
pub struct Xui {
pub uhs: String,
}
#[derive(Debug, serde::Deserialize)]
pub struct Profile {
pub id: String,
pub name: String,
}
pub fn get_authorisation_code() -> error::Result<String> {
let auth_uri = oauth2::AuthUrl::new(AUTH_URI.to_string()).unwrap();
let redirect_uri = oauth2::RedirectUrl::new("http://localhost:6565".to_string()).unwrap();
let client = oauth2::basic::BasicClient::new(oauth2::ClientId::new(CLIENT_ID.to_string()))
.set_auth_uri(auth_uri)
.set_redirect_uri(redirect_uri);
let (auth_url, _) = client
.authorize_url(oauth2::CsrfToken::new_random)
.add_scope(oauth2::Scope::new("XboxLive.signin".to_string()))
.add_scope(oauth2::Scope::new("XboxLive.offline_access".to_string()))
.set_response_type(&oauth2::ResponseType::new("code".to_string()))
.url();
open::that_in_background(auth_url.as_str());
let listener = net::TcpListener::bind("127.0.0.1:6565")?;
for stream in listener.incoming() {
if let Ok(mut stream) = stream {
let mut reader = io::BufReader::new(&mut stream);
let mut request_line = String::new();
io::BufRead::read_line(&mut reader, &mut request_line)?;
let redirect_url = request_line.split_whitespace().nth(1).unwrap();
let url = oauth2::url::Url::parse(&format!("http://localhost:6565{redirect_url}"))?;
let mut pairs = url.query_pairs();
let code = pairs.next().unwrap().1.to_string();
io::Write::write(&mut stream, HTTP_OK)?;
return Ok(code);
}
}
unreachable!()
}
pub fn get_xbl_autentication(access_token: &str) -> error::Result<XboxAuthResponse> {
let body = serde_json::json!({
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": format!("d={access_token}"),
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
});
Ok(ureq::post("https://user.auth.xboxlive.com/user/authenticate")
.send_json(body)?
.into_json()?)
}
pub fn get_xsts_autentication(xbl_token: &str) -> error::Result<XboxAuthResponse> {
let body = serde_json::json!({
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [xbl_token]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT",
});
Ok(ureq::post("https://xsts.auth.xboxlive.com/xsts/authorize").send_json(body)?.into_json()?)
}
pub fn get_microsoft_token(authorisation_code: &str) -> error::Result<Token> {
Ok(ureq::post("https://login.live.com/oauth20_token.srf")
.send_form(&[
("client_id", CLIENT_ID),
("code", authorisation_code),
("grant_type", "authorization_code"),
("redirect_uri", "http://localhost:6565"),
])?
.into_json()?)
}
pub fn get_minecraft_auth(xsts_auth: &XboxAuthResponse) -> error::Result<AuthResponse> {
let user_hash = &xsts_auth.display_claims.xui.iter().next().unwrap().uhs;
let token = &xsts_auth.token;
let auth_body = serde_json::json!({
"identityToken": format!("XBL3.0 x={};{}", user_hash, token)
});
Ok(ureq::post("https://api.minecraftservices.com/authentication/login_with_xbox")
.send_json(auth_body)?
.into_json()?)
}
pub fn get_user_profile(access_token: &str) -> error::Result<Profile> {
Ok(ureq::get("https://api.minecraftservices.com/minecraft/profile")
.set("Authorization", &format!("Bearer {access_token}"))
.call()?
.into_json()?)
}
+59
View File
@@ -0,0 +1,59 @@
use std::fs;
use std::path;
use crate::*;
pub mod microsoft;
pub mod offline;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum Account {
Microsoft { name: String, uuid: String, access_token: String },
Offline { name: String, uuid: String },
}
pub fn auth_microsoft() -> error::Result<Account> {
let authorisation_code = microsoft::get_authorisation_code()?;
let token = microsoft::get_microsoft_token(&authorisation_code)?;
let xbl = microsoft::get_xbl_autentication(&token.access_token)?;
let xsts = microsoft::get_xsts_autentication(&xbl.token)?;
let auth = microsoft::get_minecraft_auth(&xsts)?;
let profile = microsoft::get_user_profile(&auth.access_token)?;
Ok(Account::Microsoft {
name: profile.name,
uuid: profile.id,
access_token: auth.access_token,
})
}
pub fn auth_offline(name: &str) -> Account {
let name = name.to_string();
let uuid = offline::offline_uuid(&name);
Account::Offline { name, uuid }
}
pub fn save(auth_path: &path::Path, account: &Account) -> error::Result<()> {
let data = serde_json::to_string_pretty(account)?;
let name = match &account {
Account::Microsoft { name, .. } => name,
Account::Offline { name, .. } => name,
};
if !auth_path.exists() {
fs::create_dir_all(auth_path)?;
}
let path = auth_path.join(format!("{name}.json"));
fs::write(path, data.as_bytes())?;
Ok(())
}
pub fn load(auth_path: &path::Path, name: &str) -> error::Result<Option<Account>> {
if !auth_path.exists() {
fs::create_dir_all(auth_path)?;
}
let path = auth_path.join(format!("{name}.json"));
if !path.exists() {
return Ok(None);
}
Ok(Some(serde_json::from_str(&fs::read_to_string(path)?)?))
}
+7
View File
@@ -0,0 +1,7 @@
pub fn offline_uuid(username: &str) -> String {
let mut hash = md5::compute(format!("OfflinePlayer:{}", username)).0;
hash[6] = hash[6] & 0x0f | 0x30; // uuid version 3
hash[8] = hash[8] & 0x3f | 0x80; // RFC4122 variant
uuid::Uuid::from_bytes(hash).to_string()
}
+175
View File
@@ -0,0 +1,175 @@
use crate::*;
use log::*;
use std::io::Write;
use std::{fs, io, path};
pub const RESOURCES_URL: &'static str = "https://resources.download.minecraft.net";
pub fn download<W: Write>(url: &str, writer: &mut W) -> Result<u64, error::Error> {
let mut reader = ureq::get(url).call()?.into_reader();
Ok(io::copy(&mut reader, writer)?)
}
pub fn download_minecraft_version(minecraft_path: &path::Path, jre_path: &path::Path, name: &str) -> Result<(), error::Error> {
let manifest: api::manifest::Manifest = api::get_from_url(api::manifest::Manifest::DEFAULT_URL)?;
let Some(version) = manifest.get_version(name) else {
return Err(error::Error::VersionNotFound);
};
let meta: api::meta::Version = api::get_from_url(&version.url)?;
let assets: api::assets::Assets = api::get_from_url(&meta.asset_index.url)?;
{
let data_meta = serde_json::to_string_pretty(&meta)?;
let data_assets = serde_json::to_string_pretty(&assets)?;
let path_client = path!(minecraft_path, "versions", &version.id, format!("{0}.jar", version.id));
let path_meta = path!(minecraft_path, "versions", &version.id, format!("{0}.json", version.id));
let path_assets = path!(minecraft_path, "assets", "indexes", format!("{0}.json", meta.asset_index.id));
let mut file_meta = file::create_or_open_file(&path_meta)?;
let mut file_assets = file::create_or_open_file(&path_assets)?;
if !file::file_hash(&meta.downloads.client.sha1, &path_client).unwrap_or_default() {
let mut file_client = file::create_or_open_file(&path_client)?;
info!("Downloading client: {path_client:?}");
download(&meta.downloads.client.url, &mut file_client)?;
}
file_meta.write(data_meta.as_bytes())?;
file_assets.write(data_assets.as_bytes())?;
}
{
//this file not is used per smallauncher to launch the game, but some mod installers (forge) expect this file exists
let profile_path = path!(minecraft_path, "launcher_profiles.json");
if !profile_path.exists() {
fs::write(profile_path, r#"{"profiles":{}}"#)?;
}
}
{
let java_versions: api::java::JavaVersions = api::get_from_url(api::java::JavaVersions::DEFAULT_URL)?;
let java_version = meta.java_version;
#[cfg(target_os = "linux")]
let version = java_versions.linux;
#[cfg(target_os = "windows")]
let version = java_versions.windows_x64;
#[cfg(target_os = "macos")]
let version = java_versions.mac_os;
let java_component = java_version.component;
let list = match java_component.as_str() {
"java-runtime-alpha" => version.java_runtime_alpha,
"java-runtime-beta" => version.java_runtime_beta,
"java-runtime-delta" => version.java_runtime_delta,
"java-runtime-gamma" => version.java_runtime_gamma,
"java-runtime-gamma-snapshot" => version.java_runtime_gamma_snapshot,
"jre-legacy" => version.jre_legacy,
_ => return Err(error::Error::JavaVersionNotSupported),
};
let Some(version) = list.get(0) else {
return Err(error::Error::JavaVersionNotSupported);
};
let files: api::java::Files = api::get_from_url(&version.manifest.url)?;
for (path, file) in files.files {
let path = path!(jre_path, &java_component, path);
match file {
api::java::File::Directory => fs::create_dir_all(path)?,
api::java::File::Link { .. } => {}
#[cfg(target_family = "unix")]
api::java::File::File { downloads, executable } => {
let file_download = downloads.raw;
use std::os::unix::fs::PermissionsExt;
if !file::file_hash(&file_download.sha1, &path).unwrap_or_default() {
let mut file = file::create_or_open_file(&path)?;
info!("Downloading java file: {path:?}");
download(&file_download.url, &mut file)?;
if executable {
let perm = fs::Permissions::from_mode(0o755);
file.set_permissions(perm)?;
}
}
}
#[cfg(not(target_family = "unix"))]
api::java::File::File { downloads, .. } => {
let file_download = downloads.raw;
if !file::file_hash(&file_download.sha1, &path).unwrap_or_default() {
let mut file = file::create_or_open_file(&path)?;
info!("Downloading java file: {path:?}");
download(&file_download.url, &mut file)?;
}
}
}
}
}
#[cfg(target_os = "linux")]
const OS_NAME: Option<api::meta::OsName> = Some(api::meta::OsName::Linux);
#[cfg(target_os = "windows")]
const OS_NAME: Option<api::meta::OsName> = Some(api::meta::OsName::Windows);
#[cfg(target_arch = "x86")]
const ARCH: Option<api::meta::Arch> = Some(api::meta::Arch::X86);
#[cfg(not(target_arch = "x86"))]
const ARCH: Option<api::meta::Arch> = None;
'l: for library in &meta.libraries {
let Some(downloads) = &library.downloads else {
continue;
};
if let Some(classifier) = &downloads.classifiers {
#[cfg(target_os = "linux")]
let Some(library_download) = &classifier.natives_linux
else {
continue;
};
#[cfg(target_os = "windows")]
let Some(library_download) = &classifier.natives_windows
else {
continue;
};
#[cfg(target_os = "macos")]
let Some(library_download) = &classifier.natives_osx
else {
continue;
};
let path = path!(minecraft_path, "libraries", &library_download.path);
if !file::file_hash(&library_download.sha1, &path).unwrap_or_default() {
let mut file = file::create_or_open_file(&path)?;
info!("Downloading native library: {}", library_download.path);
download(&library_download.url, &mut file)?;
}
};
if let Some(rules) = &library.rules {
for api::meta::Rule { os, action } in rules {
match (os, action) {
(Some(api::meta::Os { name: OS_NAME, arch: ARCH }), api::meta::Action::Allow) => {}
(Some(api::meta::Os { name: OS_NAME, arch: ARCH }), api::meta::Action::Disallow) => continue 'l,
(None, api::meta::Action::Allow) => {}
(None, api::meta::Action::Disallow) => continue 'l,
(_, api::meta::Action::Disallow) => {}
(_, api::meta::Action::Allow) => continue 'l,
}
}
}
if let Some(library_download) = &downloads.artifact {
let path = path!(minecraft_path, "libraries", &library_download.path);
if !file::file_hash(&library_download.sha1, &path).unwrap_or_default() {
let mut file = file::create_or_open_file(&path)?;
info!("Downloading library: {}", library_download.path);
download(&library_download.url, &mut file)?;
}
};
}
for (name, asset) in assets.objects {
let prefix_hash = &asset.hash[0..2];
let hash = &asset.hash;
let path = path!(minecraft_path, "assets", "objects", prefix_hash, hash);
let url = format!("{RESOURCES_URL}/{prefix_hash}/{hash}");
if !file::file_hash(hash, &path).unwrap_or_default() {
info!("Downloading asset: {name}");
let mut file = file::create_or_open_file(&path)?;
download(&url, &mut file)?;
}
}
Ok(())
}
+55
View File
@@ -0,0 +1,55 @@
pub type Result<T, E = Error> = core::result::Result<T, E>;
#[derive(Debug)]
pub enum Error {
VersionNotFound,
JavaVersionNotSupported,
Serialize(serde_json::Error),
Io(std::io::Error),
Network(ureq::Error),
OsString(std::ffi::OsString),
Zip(zip::result::ZipError),
ParseURL(oauth2::url::ParseError),
}
impl From<ureq::Error> for Error {
#[inline(always)]
fn from(value: ureq::Error) -> Self {
Self::Network(value)
}
}
impl From<std::io::Error> for Error {
#[inline(always)]
fn from(value: std::io::Error) -> Self {
Self::Io(value)
}
}
impl From<serde_json::Error> for Error {
#[inline(always)]
fn from(value: serde_json::Error) -> Self {
Self::Serialize(value)
}
}
impl From<std::ffi::OsString> for Error {
#[inline(always)]
fn from(value: std::ffi::OsString) -> Self {
Self::OsString(value)
}
}
impl From<zip::result::ZipError> for Error {
#[inline(always)]
fn from(value: zip::result::ZipError) -> Self {
Self::Zip(value)
}
}
impl From<oauth2::url::ParseError> for Error {
#[inline(always)]
fn from(value: oauth2::url::ParseError) -> Self {
Self::ParseURL(value)
}
}
+85
View File
@@ -0,0 +1,85 @@
use std::{
fs::{self, File},
path::{Path, PathBuf},
};
use serde_json::Value;
const META_SERVER: &str = "https://meta.fabricmc.net/";
const MAVEN_SERVER: &str = "https://maven.fabricmc.net/";
fn query_meta_json(path: &str) -> Value {
ureq::get(&format!("{META_SERVER}{path}")).call().unwrap().into_json().unwrap()
}
fn download_maven(url: &str, path: &Path) {
fs::create_dir_all(path.parent().unwrap()).unwrap();
let mut file = File::create(path).unwrap();
if url.starts_with(MAVEN_SERVER) {
crate::download::download(&format!("{url}"), &mut file).unwrap();
} else {
crate::download::download(&format!("{META_SERVER}{url}"), &mut file).unwrap();
}
}
fn library_from_json(json: &Value) -> Option<(String, String)> {
let name = json.get("name")?.as_str()?;
let url = json.get("url")?.as_str()?;
// Split "group:artifact:version"
let parts: Vec<&str> = name.splitn(3, ':').collect();
if parts.len() != 3 {
return None;
}
let group = parts[0].replace('.', "/");
let artifact = parts[1];
let version = parts[2];
// Build remote URL (like Java's getURL)
let jar_name = format!("{artifact}-{version}.jar");
let remote_path = format!("{}/{}/{}/{jar_name}", group, artifact, version);
let full_url = format!("{}{}", url, remote_path);
// Build local path (like Java's getPath)
let group_fs = parts[0].replace('.', &std::path::MAIN_SEPARATOR.to_string());
let local_path = PathBuf::from(group_fs)
.join(artifact)
.join(version)
.join(jar_name)
.to_string_lossy()
.replace(' ', "_");
Some((full_url, local_path))
}
pub fn install_fabric(minecraft_path: &Path, minecraft_version: &str, loader_version: &str) -> Result<(), crate::error::Error> {
let profile_name = format!("{minecraft_version}-fabric");
let versions_dir = minecraft_path.join("versions");
let profile_dir = versions_dir.join(&profile_name);
let profile_json = profile_dir.join(format!("{profile_name}.json"));
let libs_dir = minecraft_path.join("libraries");
let json = query_meta_json(&format!("v2/versions/loader/{minecraft_version}/{loader_version}/profile/json"));
fs::create_dir_all(&profile_dir).unwrap();
fs::write(&profile_json, json.to_string()).unwrap();
fs::copy(
versions_dir.join(minecraft_version).join(format!("{minecraft_version}.jar")),
profile_dir.join(profile_name + ".jar"),
)
.unwrap();
for lib_json in json.get("libraries").unwrap().as_array().unwrap() {
if let Some((url, path)) = library_from_json(lib_json) {
let lib_file = libs_dir.join(path);
download_maven(&url, &lib_file);
} else {
eprintln!("Failed to get json from {lib_json}")
}
}
Ok(())
}
+57
View File
@@ -0,0 +1,57 @@
use std::{ffi, fs, io, path};
use crate::*;
pub(crate) fn create_or_open_file<S: AsRef<ffi::OsStr>>(path: &S) -> Result<fs::File, io::Error> {
let path = path::Path::new(path);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
Ok(fs::File::create(&path)?)
}
pub(crate) fn file_hash<P: AsRef<path::Path>>(hash: &str, path: &P) -> Result<bool, error::Error> {
use sha1::Digest;
let mut file = fs::File::open(path)?;
let mut hasher = sha1::Sha1::new();
io::copy(&mut file, &mut hasher)?;
let reader_hash = hex::encode(hasher.finalize());
Ok(reader_hash == hash)
}
#[inline(always)]
pub(crate) fn from_json_file<T: serde::de::DeserializeOwned, P: AsRef<path::Path>>(path: P) -> Result<T, error::Error> {
Ok(serde_json::from_str(&fs::read_to_string(path)?)?)
}
#[macro_export]
macro_rules! path {
() => {
std::path::PathBuf::new()
};
($($segment:expr),+ $(,)?) => {
{
let mut path = std::path::PathBuf::new();
$(
path.push($segment);
)+
path
}
};
}
#[macro_export]
macro_rules! osStr {
() => {
std::ffi::OsString::new()
};
($($segment:expr),+ $(,)?) => {
{
let mut string = std::ffi::OsString::new();
$(
string.push($segment);
)+
string
}
};
}
+311
View File
@@ -0,0 +1,311 @@
use crate::*;
use std::ffi;
use std::fs;
use std::io;
use std::path;
use std::process;
const CLIENT_ID: &'static str = "42";
const LAUNCHER_NAME: &'static str = "miniLauncher";
const LAUNCHER_VERSION: &'static str = "42";
#[cfg(all(not(debug_assertions), target_os = "windows"))]
const DETACHED_PROCESS: u32 = 0x00000008;
fn merge(a: serde_json::Value, b: serde_json::Value) -> serde_json::Value {
match (a, b) {
(serde_json::Value::Object(mut a), serde_json::Value::Object(b)) => {
for (k, v2) in b {
if let Some(v1) = a.remove(&k) {
a.insert(k, merge(v1, v2));
} else {
a.insert(k, v2);
}
}
serde_json::Value::Object(a)
}
(serde_json::Value::Array(mut a), serde_json::Value::Array(b)) => {
a.extend(b);
serde_json::Value::Array(a)
}
(_, b) => b,
}
}
pub fn launch_minecraft_version(
game_path: &path::Path,
jre_path: &path::Path,
version_name: &str,
account: &auth::Account,
) -> Result<(), error::Error> {
let meta: api::meta::Version = {
let meta_path = path!(game_path, "versions", &version_name, format!("{version_name}.json"));
let mut map1: serde_json::Map<String, serde_json::Value> = file::from_json_file(&meta_path)?;
match map1.remove("inheritsFrom") {
Some(serde_json::Value::String(version_name)) => {
let meta_path = path!(game_path, "versions", &version_name, format!("{version_name}.json"));
let map2: serde_json::Map<String, serde_json::Value> = file::from_json_file(&meta_path)?;
let v1 = serde_json::Value::Object(map1);
let v2 = serde_json::Value::Object(map2);
serde_json::from_value(merge(v2, v1))?
}
_ => serde_json::from_value(serde_json::Value::Object(map1))?,
}
};
extract_natives(&meta, &game_path)?;
let main_class = &meta.main_class;
let java_component = &meta.java_version.component;
let jvm_args = generate_jvm_args(&meta, &game_path);
let game_args = generate_game_args(&meta, &game_path, account);
#[cfg(target_family = "unix")]
let jre_bin = path!(jre_path, java_component, "bin", "java");
#[cfg(target_family = "windows")]
let jre_bin = path!(jre_path, java_component, "bin", "java.exe");
let mut command = process::Command::new(jre_bin);
#[cfg(all(not(debug_assertions), target_os = "windows"))]
std::os::windows::process::CommandExt::creation_flags(&mut command, DETACHED_PROCESS);
command.current_dir(game_path).args(jvm_args).arg(main_class).args(game_args);
log::info!("Spawning command: {:?}", command);
command.spawn()?;
Ok(())
}
pub fn extract_natives(meta: &api::meta::Version, game_path: &path::Path) -> Result<(), error::Error> {
for library in &meta.libraries {
let Some(downloads) = &library.downloads else {
continue;
};
let Some(classifier) = &downloads.classifiers else {
continue;
};
#[cfg(target_os = "linux")]
let Some(library_download) = &classifier.natives_linux
else {
continue;
};
#[cfg(target_os = "windows")]
let Some(library_download) = &classifier.natives_windows
else {
continue;
};
#[cfg(target_os = "macos")]
let Some(library_download) = &classifier.natives_osx
else {
continue;
};
let path = path!(game_path, "libraries", &library_download.path);
let file = fs::File::open(&path)?;
let mut zip = zip::ZipArchive::new(file)?;
for i in 0..zip.len() {
let mut zip_file = zip.by_index(i)?;
let name = zip_file.name();
if !(name.ends_with(".so") || name.ends_with(".dll") || name.ends_with(".dylib")) {
continue;
}
let path = path!(game_path, "bin", name);
log::info!("Extracting {path:?}");
let mut file = file::create_or_open_file(&path)?;
io::copy(&mut zip_file, &mut file)?;
}
}
Ok(())
}
pub fn generate_game_args(meta: &api::meta::Version, game_path: &path::Path, account: &auth::Account) -> Vec<ffi::OsString> {
let mut args = Vec::new();
let arguments = &meta.arguments;
let assets_path = path!(game_path, "assets");
for arg in &arguments.game {
match arg {
api::meta::Argument::String(str) => match str.as_str() {
"${clientid}" => args.push(osStr!(CLIENT_ID)),
"${auth_player_name}" => {
match &account {
auth::Account::Microsoft { name, .. } => args.push(osStr!(name)),
auth::Account::Offline { name, .. } => args.push(osStr!(name)),
};
}
"${auth_uuid}" => {
match &account {
auth::Account::Microsoft { uuid, .. } => args.push(osStr!(uuid)),
auth::Account::Offline { uuid, .. } => args.push(osStr!(uuid)),
};
}
"${user_type}" => {
match &account {
auth::Account::Microsoft { .. } => args.push(osStr!("msa")),
auth::Account::Offline { .. } => args.push(osStr!()),
};
}
"${auth_access_token}" => match &account {
auth::Account::Microsoft { access_token, .. } => args.push(osStr!(access_token)),
auth::Account::Offline { .. } => args.push(osStr!()),
},
"${game_directory}" => args.push(osStr!(game_path)),
"${version_name}" => args.push(osStr!(&meta.id)),
"${assets_root}" => args.push(osStr!(&assets_path)),
"${assets_index_name}" => args.push(osStr!(&meta.asset_index.id)),
"${version_type}" => args.push(osStr!(meta.r#type.to_string())),
"--xuid" | "${auth_xuid}" => {}
str => args.push(osStr!(str)),
},
_ => {}
}
}
args.push(osStr!("--userProperties"));
args.push(osStr!("{}"));
args
}
pub fn generate_jvm_args(meta: &api::meta::Version, game_path: &path::Path) -> Vec<ffi::OsString> {
let mut args = Vec::new();
let arguments = &meta.arguments;
let natives_dir = path!(game_path, "bin");
let class_paths = get_class_paths(meta, game_path);
for arg in &arguments.jvm {
match arg {
api::meta::Argument::String(str) => match str.as_str() {
"-Dminecraft.launcher.version=${launcher_version}" => {
let value = osStr!("-Dminecraft.launcher.version=", LAUNCHER_VERSION);
args.push(value);
}
"-Dminecraft.launcher.brand=${launcher_name}" => {
let value = osStr!("-Dminecraft.launcher.brand=", LAUNCHER_NAME);
args.push(value);
}
"-Djava.library.path=${natives_directory}" => {
let value = osStr!("-Djava.library.path=", &natives_dir);
args.push(value);
}
"-Djna.tmpdir=${natives_directory}" => {
let value = osStr!("-Djna.tmpdir=", &natives_dir);
args.push(value);
}
"-Dorg.lwjgl.system.SharedLibraryExtractPath=${natives_directory}" => {
let value = osStr!("-Dorg.lwjgl.system.SharedLibraryExtractPath=", &natives_dir);
args.push(value);
}
"-Dio.netty.native.workdir=${natives_directory}" => {
let value = osStr!("-Dio.netty.native.workdir=", &natives_dir);
args.push(value);
}
"${classpath}" => args.push(class_paths.clone()),
_ => args.push(ffi::OsString::from(str)),
},
_ => {}
}
}
args
}
pub fn get_class_paths(meta: &api::meta::Version, game_path: &path::Path) -> ffi::OsString {
#[cfg(target_os = "linux")]
const OS_NAME: Option<api::meta::OsName> = Some(api::meta::OsName::Linux);
#[cfg(target_os = "windows")]
const OS_NAME: Option<api::meta::OsName> = Some(api::meta::OsName::Windows);
#[cfg(target_arch = "x86")]
const ARCH: Option<api::meta::Arch> = Some(api::meta::Arch::X86);
#[cfg(not(target_arch = "x86"))]
const ARCH: Option<api::meta::Arch> = None;
#[cfg(target_family = "unix")]
const SEPARATOR: &'static str = ":";
#[cfg(target_family = "windows")]
const SEPARATOR: &'static str = ";";
let mut list = ffi::OsString::new();
{
let path = path!(game_path, "versions", &meta.id, format!("{}.jar", meta.id));
list.push(path);
}
'l: for lib in &meta.libraries {
match lib {
api::meta::Library {
downloads: Some(api::meta::LibraryDownload {
artifact: Some(artifact), ..
}),
rules,
..
} => {
if let Some(rules) = rules {
for api::meta::Rule { os, action } in rules {
match (os, action) {
(Some(api::meta::Os { name: OS_NAME, arch: ARCH }), api::meta::Action::Allow) => {}
(Some(api::meta::Os { name: OS_NAME, arch: ARCH }), api::meta::Action::Disallow) => continue 'l,
(None, api::meta::Action::Allow) => {}
(None, api::meta::Action::Disallow) => continue 'l,
(_, api::meta::Action::Disallow) => {}
(_, api::meta::Action::Allow) => continue 'l,
}
}
}
list.push(SEPARATOR);
let path = path!(game_path, "libraries", &artifact.path);
list.push(path);
}
lib => {
let mut original_name: &str = &lib.name;
let mut name = "";
let mut version = "";
let mut path = path!();
if let Some(i) = original_name.find(":") {
let mut paths = &original_name[..i];
original_name = &original_name[(i + 1)..];
while let Some(i) = paths.find(".") {
path = path!(path, &paths[..i]);
paths = &paths[(i + 1)..];
}
path = path!(path, &paths);
}
if let Some(i) = original_name.find(":") {
name = &original_name[..i];
version = &original_name[(i + 1)..];
}
list.push(SEPARATOR);
let path = path!(game_path, "libraries", &path, name, version, format!("{name}-{version}.jar"));
list.push(path);
}
}
}
list
}
pub fn check_version_integrity(game_path: &path::Path, version_name: &str) -> bool {
let path_version = path!(game_path, "versions", version_name, format!("{version_name}.json"));
let path_client = path!(game_path, "versions", version_name, format!("{version_name}.jar"));
let Ok(version) = file::from_json_file::<api::meta::Version, _>(&path_version) else {
return false;
};
if !file::file_hash(&version.downloads.client.sha1, &path_client).unwrap_or_default() {
return false;
}
let path_assets = path!(game_path, "assets", "indexes", format!("{}.json", version.asset_index.id));
let Ok(assets) = file::from_json_file::<api::assets::Assets, _>(&path_assets) else {
return false;
};
for (_, asset) in assets.objects {
let prefix_hash = &asset.hash[0..2];
let hash = &asset.hash;
let path = path!(game_path, "assets", "objects", prefix_hash, hash);
if !file::file_hash(hash, &path).unwrap_or_default() {
return false;
}
}
true
}
pub fn list_versions(game_path: &path::Path) -> Result<Vec<String>, error::Error> {
let path = path!(game_path, "versions");
fs::read_dir(path)?
.map(|x| -> Result<String, error::Error> { Ok(x?.file_name().to_string_lossy().to_string()) })
.collect()
}
pub fn list_all_versions() -> Result<api::manifest::Manifest, error::Error> {
api::get_from_url(api::manifest::Manifest::DEFAULT_URL)
}
+36
View File
@@ -0,0 +1,36 @@
use std::path::PathBuf;
pub mod api;
pub mod auth;
pub mod download;
pub mod error;
pub mod fabric;
mod file;
pub mod launch;
pub fn download_version(version: &str) {
let base_path = PathBuf::from(std::env::home_dir().unwrap()).join(".saturn-launcher");
let jre_path = base_path.join("jre");
let instances_path = base_path.join("instances");
let instance = instances_path.join(version);
download::download_minecraft_version(&instance, &jre_path, version).unwrap()
}
pub fn launch_version(version: &str) {
let base_path = PathBuf::from(std::env::home_dir().unwrap()).join(".saturn-launcher");
let jre_path = base_path.join("jre");
let instances_path = base_path.join("instances");
let instance = instances_path.join(version);
launch::launch_minecraft_version(
&instance,
&jre_path,
&format!("{version}-fabric"),
&auth::Account::Offline {
name: "Daddy".into(),
uuid: "Daddy".into(),
},
)
.unwrap()
}
+14
View File
@@ -0,0 +1,14 @@
use std::path::PathBuf;
fn main() {
// download_version("1.21.4");
smallauncher_lib::launch_version("1.21.4");
// install_fabric("1.21.4", "0.17.2");
}
fn install_fabric(version: &str, loader: &str) {
let base_path = PathBuf::from(std::env::home_dir().unwrap()).join(".saturn-launcher");
let instances_path = base_path.join("instances");
let instance = instances_path.join(version);
smallauncher_lib::fabric::install_fabric(&instance, version, loader).unwrap()
}