├── rustfmt.toml ├── libmcmeta ├── src │ ├── lib.rs │ └── models │ │ ├── mod.rs │ │ └── mojang.rs ├── Cargo.toml └── LICENSE ├── Cargo.toml ├── .gitignore ├── mcmeta ├── .env.example ├── src │ ├── errors.rs │ ├── routes │ │ ├── mojang.rs │ │ ├── mod.rs │ │ └── forge.rs │ ├── app_config.rs │ ├── upstream │ │ ├── mod.rs │ │ └── mojang │ │ │ └── mod.rs │ ├── storage │ │ ├── json.rs │ │ ├── mod.rs │ │ └── database.rs │ ├── main.rs │ └── utils.rs └── Cargo.toml ├── scripts └── add_rustfmt_hook.sh ├── README.md └── static └── mojang ├── minecraft-legacy-services.json ├── minecraft-experiments.json ├── minecraft-legacy-override.json └── minecraft-old-snapshots.json /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" -------------------------------------------------------------------------------- /libmcmeta/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod models; 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["libmcmeta", "mcmeta"] 3 | resolver = "2" 4 | 5 | [profile.dev.package.backtrace] 6 | opt-level = 3 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | 4 | .env 5 | /config 6 | /logs 7 | 8 | # downloaded metadata during development 9 | meta 10 | 11 | # dev envierment 12 | /.helix 13 | /.vscode 14 | 15 | # let this be self generated 16 | mcmeta/static/forge/forge-legacyinfo.json 17 | -------------------------------------------------------------------------------- /libmcmeta/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libmcmeta" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | chrono = { version = "0.4.40", features = ["serde"] } 10 | lazy_static = "1.5.0" 11 | merge = "0.1.0" 12 | serde = { version = "1.0.219", features = ["derive"] } 13 | serde_json = "1.0.140" 14 | serde_valid = "1.0.5" 15 | serde_with = "3.12.0" 16 | thiserror = "2.0.12" 17 | tracing = "0.1.41" 18 | 19 | [features] 20 | -------------------------------------------------------------------------------- /mcmeta/.env.example: -------------------------------------------------------------------------------- 1 | RUST_LOG=INFO 2 | 3 | # all the below varabile are set be default and do not all need to be provided 4 | # you only need to set the ones you want to change 5 | 6 | MCMETA__BIND_ADDRESS=127.0.0.1:9988 7 | 8 | MCMETA__STORAGE_FORMAT__TYPE=json 9 | MCMETA__STORAGE_FORMAT__META_DIRECTORY=./meta 10 | MCMETA__STORAGE_FORMAT__GENERATED_DIRECTORY=./generated 11 | 12 | MCMETA__METADATA__MAX_PARALLEL_FETCH_CONNECTIONS=8 13 | MCMETA__METADATA__STATIC_DIRECTORY=./static 14 | 15 | MCMETA__DEBUG_LOG__ENABLE=true 16 | MCMETA__DEBUG_LOG__PATH=./logs 17 | MCMETA__DEBUG_LOG__PREFIX=mcmeta.log 18 | MCMETA__DEBUG_LOG__LEVEL=DEBUG 19 | 20 | MCMETA_MOJANG__MANIFEST_URL=https://piston-meta.mojang.com/mc/game/version_manifest_v2.json 21 | 22 | MCMETA_FORGE__MAVEN_URL=https://files.minecraftforge.net/net/minecraftforge/forge/maven-metadata.json 23 | MCMETA_FORGE__PROMOTIONS_URL=https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json -------------------------------------------------------------------------------- /mcmeta/src/errors.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{get_json_context, get_json_context_back}; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug)] 5 | pub enum MetadataError { 6 | #[error( 7 | "Unable to deserialise json object at {line}:{column}. \n\tContext: `{ctx}` \n\nCaused by:\n\t{source}" 8 | )] 9 | BadJsonData { 10 | ctx: String, 11 | line: usize, 12 | column: usize, 13 | source: serde_json::Error, 14 | }, 15 | #[error("Unable to find version manifest in {0}")] 16 | MissingMojangVersionManifest(String), 17 | #[error("Errors during {0}: {1:?}")] 18 | BulkProcessingError(String, Vec), 19 | } 20 | 21 | impl MetadataError { 22 | pub fn from_json_err(err: serde_json::Error, body: &str) -> Self { 23 | let mut ctx = get_json_context_back(&err, body, 200); 24 | ctx.push_str(&get_json_context(&err, body, 200)); 25 | 26 | Self::BadJsonData { 27 | ctx, 28 | line: err.line(), 29 | column: err.column(), 30 | source: err, 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /mcmeta/src/routes/mojang.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | extract::{Path, State}, 3 | response::IntoResponse, 4 | }; 5 | use libmcmeta::models::mojang::{MinecraftVersion, MojangVersionManifest}; 6 | use std::sync::Arc; 7 | use tracing::instrument; 8 | 9 | use crate::storage::Storage; 10 | 11 | use super::{into_api_axum_responce, ServerState}; 12 | 13 | #[instrument] 14 | pub async fn raw_mojang_manifest(State(state): State>) -> impl IntoResponse { 15 | let manifest = state 16 | .upstream_storage 17 | .fetch_record::(["mojang"], "version_manifest_v2") 18 | .await; 19 | into_api_axum_responce(manifest, "Version manifest not found") 20 | } 21 | 22 | #[instrument] 23 | pub async fn raw_mojang_version( 24 | State(state): State>, 25 | Path(version): Path, 26 | ) -> impl IntoResponse { 27 | let result = state 28 | .upstream_storage 29 | .fetch_record::(["mojang", "versions"], &version) 30 | .await; 31 | into_api_axum_responce(result, format!("Version {} does not exits", &version)) 32 | } 33 | -------------------------------------------------------------------------------- /scripts/add_rustfmt_hook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # check that rustfmt installed, or else this hook doesn't make much sense 4 | command -v rustfmt >/dev/null 2>&1 || { echo >&2 "Rustfmt is required but it's not installed. Aborting."; exit 1; } 5 | 6 | # write a whole script to pre-commit hook 7 | # NOTE: it will overwrite pre-commit file! 8 | cat > .git/hooks/pre-commit <<'EOF' 9 | #!/bin/bash -e 10 | declare -a rust_files=() 11 | files=$(git diff-index --name-only HEAD) 12 | echo 'Formatting source files' 13 | for file in $files; do 14 | if [ ! -f "${file}" ]; then 15 | continue 16 | fi 17 | if [[ "${file}" == *.rs ]]; then 18 | rust_files+=("${file}") 19 | fi 20 | done 21 | if [ ${#rust_files[@]} -ne 0 ]; then 22 | command -v rustfmt >/dev/null 2>&1 || { echo >&2 "Rustfmt is required but it's not installed. Aborting."; exit 1; } 23 | $(command -v rustfmt) ${rust_files[@]} & 24 | fi 25 | wait 26 | if [ ${#rust_files[@]} -ne 0 ]; then 27 | git add ${rust_files[@]} 28 | echo "Formatting done, changed files: ${rust_files[@]}" 29 | else 30 | echo "No changes, formatting skipped" 31 | fi 32 | EOF 33 | 34 | chmod +x .git/hooks/pre-commit 35 | 36 | echo "Hooks updated" -------------------------------------------------------------------------------- /mcmeta/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mcmeta" 3 | version = "0.1.0" 4 | edition = "2024" 5 | description = "A Metadata server for Minecraft launchers and others" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | color-eyre = "0.6" 11 | eyre = "0.6" 12 | axum = "0.8.1" 13 | clap = { version = "4.5.32", features = ["derive"] } 14 | config = "0.15.11" 15 | dotenv = "0.15.0" 16 | futures = "0.3.31" 17 | git2 = "0.20.0" 18 | hyper = "1.6.0" 19 | lazy_static = "1.5.0" 20 | libmcmeta = { path = "../libmcmeta" } 21 | regex = "1.11.1" 22 | reqwest = { version = "0.12.14", features = ["json"] } 23 | serde = { version = "1.0.219", features = ["derive"] } 24 | serde_json = { version = "1.0", features = ["raw_value"] } 25 | serde_valid = "1.0.5" 26 | serde_path_to_error = "0.1" 27 | serde_with = "3.12.0" 28 | sha1 = "0.10.6" 29 | sha2 = "0.10.8" 30 | tempdir = "0.3.7" 31 | thiserror = "2.0.12" 32 | tokio = { version = "1.44.1", features = ["full"] } 33 | tracing = "0.1.41" 34 | tracing-appender = "0.2.3" 35 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 36 | zip = { version = "2.3.0", default-features = false, features = [ 37 | "aes-crypto", 38 | "bzip2", 39 | "deflate64", 40 | "deflate", 41 | "lzma", 42 | "zstd", 43 | "xz", 44 | "chrono", 45 | ] } 46 | async-trait = "0.1.88" 47 | enum_dispatch = "0.3.13" 48 | sqlx = { version = "0.8", features = [ 49 | "postgres", 50 | "runtime-tokio", 51 | "tls-rustls-aws-lc-rs", 52 | "derive", 53 | "migrate", 54 | "chrono", 55 | "json", 56 | ] } 57 | strum = { version = "0.27.1", features = ["derive"] } 58 | itertools = "0.14.0" 59 | chrono = "0.4" 60 | tracing-indicatif = "0.3.9" 61 | indicatif = "0.17.11" 62 | md-5 = "0.10" 63 | -------------------------------------------------------------------------------- /mcmeta/src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use serde::Serialize; 4 | 5 | use crate::{app_config::MetaConfig, storage::StorageImpl}; 6 | 7 | pub mod forge; 8 | pub mod mojang; 9 | 10 | #[allow(dead_code)] 11 | #[derive(Debug, Clone)] 12 | pub struct ServerState { 13 | pub config: Arc, 14 | pub upstream_storage: Arc, 15 | pub generated_storage: Arc, 16 | } 17 | 18 | #[derive(Serialize, Debug, Clone)] 19 | pub struct APIResponse { 20 | pub data: Option, 21 | pub error: Option, 22 | } 23 | 24 | pub fn into_api_axum_responce( 25 | result: Result, E>, 26 | not_found: impl Into, 27 | ) -> (axum::http::StatusCode, axum::Json>) 28 | where 29 | T: Serialize, 30 | E: AsRef, 31 | { 32 | match result { 33 | Ok(Some(t)) => ( 34 | axum::http::StatusCode::OK, 35 | axum::Json(APIResponse::from_some(t)), 36 | ), 37 | Ok(None) => ( 38 | axum::http::StatusCode::NOT_FOUND, 39 | axum::Json(APIResponse::from_err(not_found)), 40 | ), 41 | Err(err) => ( 42 | axum::http::StatusCode::INTERNAL_SERVER_ERROR, 43 | axum::Json(APIResponse::from_err(err.as_ref().to_string())), 44 | ), 45 | } 46 | } 47 | 48 | impl APIResponse { 49 | pub fn from_some(value: T) -> Self 50 | where 51 | T: Serialize, 52 | { 53 | APIResponse { 54 | data: Some(value), 55 | error: None, 56 | } 57 | } 58 | 59 | pub fn from_err(err: impl Into) -> Self { 60 | APIResponse { 61 | data: None, 62 | error: Some(err.into()), 63 | } 64 | } 65 | 66 | #[allow(dead_code)] 67 | pub fn from_result(result: Result) -> Self 68 | where 69 | T: Serialize, 70 | E: AsRef, 71 | { 72 | match result { 73 | Ok(d) => APIResponse { 74 | data: Some(d), 75 | error: None, 76 | }, 77 | Err(e) => APIResponse { 78 | data: None, 79 | error: Some(e.as_ref().to_string()), 80 | }, 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /mcmeta/src/routes/forge.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use axum::extract::State; 4 | use axum::{extract::Path, response::IntoResponse}; 5 | 6 | use libmcmeta::models::forge::{ 7 | ForgeInstallerManifestVersion, ForgeMavenMetadata, ForgeMavenPromotions, ForgeVersion, 8 | ForgeVersionMeta, 9 | }; 10 | use tracing::instrument; 11 | 12 | use crate::storage::Storage; 13 | 14 | use super::{into_api_axum_responce, ServerState}; 15 | 16 | #[instrument] 17 | pub async fn raw_forge_maven_meta(State(state): State>) -> impl IntoResponse { 18 | let result = state 19 | .upstream_storage 20 | .fetch_record::(["forge"], "maven-metadata") 21 | .await; 22 | into_api_axum_responce(result, "sorge Maven metadata not found") 23 | } 24 | 25 | #[instrument] 26 | pub async fn raw_forge_promotions(State(state): State>) -> impl IntoResponse { 27 | let result = state 28 | .upstream_storage 29 | .fetch_record::(["forge"], "promotions_slim") 30 | .await; 31 | into_api_axum_responce(result, "Forge Maven Promotions data not found") 32 | } 33 | 34 | #[instrument] 35 | pub async fn raw_forge_version( 36 | State(state): State>, 37 | Path(version): Path, 38 | ) -> impl IntoResponse { 39 | let result = state 40 | .upstream_storage 41 | .fetch_record::(["forge", "version_manifests"], &version) 42 | .await; 43 | into_api_axum_responce(result, format!("Version {} does not exist", version)) 44 | } 45 | 46 | #[instrument] 47 | pub async fn raw_forge_version_meta( 48 | State(state): State>, 49 | Path(version): Path, 50 | ) -> impl IntoResponse { 51 | let result = state 52 | .upstream_storage 53 | .fetch_record::(["forge", "files_manifests"], &version) 54 | .await; 55 | into_api_axum_responce(result, format!("Version {} does not exist", version)) 56 | } 57 | 58 | #[instrument] 59 | pub async fn raw_forge_version_installer( 60 | State(state): State>, 61 | Path(version): Path, 62 | ) -> impl IntoResponse { 63 | let result = state 64 | .upstream_storage 65 | .fetch_record::(["forge", "installer_manifests"], &version) 66 | .await; 67 | into_api_axum_responce(result, format!("Version {} does not exist", version)) 68 | } 69 | -------------------------------------------------------------------------------- /mcmeta/src/app_config.rs: -------------------------------------------------------------------------------- 1 | use eyre::{Context, Result}; 2 | use serde::Deserialize; 3 | 4 | #[derive(Deserialize, Debug, Clone)] 5 | #[serde(rename_all = "snake_case", tag = "type")] 6 | pub enum StorageFormat { 7 | Json { 8 | path: String, 9 | }, 10 | Database { 11 | connection_url: String, 12 | prefix: String, 13 | }, 14 | } 15 | 16 | #[derive(Deserialize, Debug, Clone)] 17 | pub struct GeneratedConfig { 18 | pub storage_format: StorageFormat, 19 | } 20 | 21 | #[derive(Debug, Deserialize, Clone)] 22 | pub struct UpstreamConfig { 23 | pub max_parallel_fetch_connections: usize, 24 | pub storage_format: StorageFormat, 25 | #[serde(default)] 26 | pub download: DownloadConfig, 27 | pub static_directory: String, 28 | } 29 | 30 | #[derive(Deserialize, Debug)] 31 | pub struct DebugLogConfig { 32 | pub enable: bool, 33 | pub path: String, 34 | pub prefix: String, 35 | pub level: String, 36 | } 37 | 38 | #[derive(Deserialize, Debug, Clone, Default)] 39 | pub struct DownloadConfig { 40 | pub mojang: crate::upstream::mojang::DownloadConfig, 41 | pub forge: crate::upstream::forge::DownloadConfig, 42 | } 43 | 44 | #[derive(Deserialize, Debug)] 45 | pub struct MetaConfig { 46 | pub bind_address: String, 47 | pub upstream: UpstreamConfig, 48 | pub generated: GeneratedConfig, 49 | pub debug_log: DebugLogConfig, 50 | } 51 | 52 | impl MetaConfig { 53 | pub fn from_config(path: &str) -> Result { 54 | let config = config::Config::builder() 55 | .set_default("bind_address", "127.0.0.1:8080")? 56 | .set_default("upstream.storage_format.type", "json")? 57 | .set_default("upstream.storage_format.path", "meta")? 58 | .set_default("upstream.max_parallel_fetch_connections", 20)? 59 | .set_default("upstream.static_directory", "static")? 60 | .set_default("generated.storage_format.type", "json")? 61 | .set_default("generated.storage_format.path", "generated")? 62 | .set_default("debug_log.enable", true)? 63 | .set_default("debug_log.path", "./logs")? 64 | .set_default("debug_log.prefix", "mcmeta.log")? 65 | .set_default("debug_log.level", "info")? 66 | // optionally add config from a file. this is optional though 67 | .add_source(config::File::from(std::path::Path::new(path)).required(false)) 68 | // environment overrides file 69 | .add_source(config::Environment::with_prefix("mcmeta").separator("__")) 70 | .build()?; 71 | 72 | config 73 | .try_deserialize::<'_, Self>() 74 | .wrap_err("Failed to parse config") 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /mcmeta/src/upstream/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod forge; 2 | pub mod mojang; 3 | 4 | use eyre::Result; 5 | use forge::ForgeUpdater; 6 | use mojang::MojangUpdater; 7 | use std::{path::PathBuf, sync::Arc}; 8 | use tracing::error; 9 | 10 | use crate::{app_config::UpstreamConfig, storage::StorageImpl, UpstreamSources}; 11 | 12 | pub fn progress_bar() -> indicatif::ProgressStyle { 13 | indicatif::ProgressStyle::with_template( 14 | "{spinner:.green} {msg} [{wide_bar:.cyan/blue}] {pos}/{len} (eta {eta})", 15 | ) 16 | .unwrap() 17 | .with_key( 18 | "eta", 19 | |state: &indicatif::ProgressState, w: &mut dyn std::fmt::Write| { 20 | write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap() 21 | }, 22 | ) 23 | } 24 | 25 | pub async fn download_binary_file( 26 | client: &reqwest::Client, 27 | path: &PathBuf, 28 | url: &str, 29 | ) -> Result<()> { 30 | if let Some(parent_dir) = path.parent() { 31 | if !parent_dir.exists() { 32 | std::fs::create_dir_all(parent_dir)?; 33 | } 34 | } 35 | 36 | let file_response = client.get(url).send().await?.error_for_status()?; 37 | 38 | let mut file = std::fs::File::create(path)?; 39 | let mut content = std::io::Cursor::new(file_response.bytes().await?); 40 | std::io::copy(&mut content, &mut file)?; 41 | 42 | Ok(()) 43 | } 44 | 45 | pub struct UpstreamMetadataUpdater { 46 | storage: Arc, 47 | config: UpstreamConfig, 48 | } 49 | 50 | impl UpstreamMetadataUpdater { 51 | pub fn new(config: UpstreamConfig, storage: Arc) -> Self { 52 | UpstreamMetadataUpdater { storage, config } 53 | } 54 | pub async fn update(&self, sources: &[UpstreamSources]) -> Result<()> { 55 | for source in sources { 56 | match source { 57 | UpstreamSources::All => unreachable!("All type marker should be filtered out"), 58 | UpstreamSources::Mojang => { 59 | let res = MojangUpdater::new(self.storage.clone(), self.config.clone()) 60 | .update() 61 | .await; 62 | if let Err(err) = res { 63 | error!("Error updating Mojang metadata: {:?}", err); 64 | } 65 | } 66 | UpstreamSources::Forge => { 67 | let res = ForgeUpdater::new(self.storage.clone(), self.config.clone()) 68 | .update() 69 | .await; 70 | if let Err(err) = res { 71 | error!("Error updating Forge metadata: {:?}", err); 72 | } 73 | } 74 | } 75 | } 76 | Ok(()) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!NOTE] 2 | > [Metabolism](https://github.com/PrismLauncher/metabolism) is the latest and most complete [meta](https://github.com/PrismLauncher/meta) reimplementation. This repo won't necessarily be archived, but you might want to check this out too. 3 | 4 | # Minecraft Metadata Server 5 | 6 | A server software designed for fetching Minecraft and Minecraft-related 7 | metadata (such as Forge, Fabric, Quilt and Liteloader) and serving them as a 8 | centralized source for metadata. 9 | 10 | The project is still at its early stages and will undergo drastic changes. 11 | 12 | ## Project structure 13 | 14 | metamc is split into 2 parts, both of which are written in Rust. 15 | 16 | ### libmcmeta 17 | 18 | A library which contains the data models and shared functions for client and 19 | server of mcmeta. It is licensed under LGPL-3.0-only. 20 | 21 | ### mcmeta 22 | 23 | A tool/server for generating and serving metadata files. It will do this by 24 | downloading existing metadata files (and in some cases, extract metadata from 25 | modloader installers) and then either serving these metadata files or 26 | generating them for usage somewhere else (like GitHub Pages). It is licensed 27 | under GPL-3.0-only. 28 | 29 | #### How to run this 30 | 31 | Since no binaries are released yet, you will have to clone and compile this 32 | repository yourself. Make sure you have Rust installed and then run: 33 | 34 | ```sh 35 | git clone https://github.com/PrismLauncher/mcmeta.git 36 | cd mcmeta/mcmeta 37 | 38 | export RUST_LOG=INFO 39 | export MCMETA__BIND_ADDRESS=127.0.0.1:9988 40 | export MCMETA__STORAGE_FORMAT__TYPE=json 41 | export MCMETA__STORAGE_FORMAT__META_DIRECTORY=../meta 42 | cargo run 43 | ``` 44 | 45 | #### Endpoints 46 | 47 | The following endpoints are currently implemented: 48 | 49 | - `GET /raw/mojang` for the Mojang version manifest, which contains all 50 | versions 51 | - `GET /raw/mojang/:version` for a specific Minecraft version, if it exists 52 | 53 | ## Goals 54 | 55 | Eventually, mcmeta should implement at least the following goals: 56 | 57 | - [ ] Fetching metadata 58 | - [x] Minecraft 59 | - [ ] Forge 60 | - [ ] Liteloader 61 | - [ ] Fabric 62 | - [ ] Quilt 63 | - [ ] Storing metadata 64 | - [x] JSON 65 | - [ ] Database 66 | - [ ] Offering metadata 67 | - [x] Minecraft 68 | - [x] Forge 69 | - [ ] Liteloader 70 | - [ ] Fabric 71 | - [ ] Quilt 72 | 73 | Some more ambitious goals that might or might not be implemented are: 74 | 75 | - [ ] MultiMC/Prism Launcher export 76 | - [ ] Static generation (metadata for launchers is stored) 77 | - [ ] Dynamic generation (metadata for launchers is generated on the fly) 78 | - [ ] Lazy-loading 79 | - Metadata isn't fetched until it is actually requested 80 | - Once fetched, metadata will stay in the database 81 | - [ ] FFI 82 | - The ability of being able to load libmcmeta as a shared library into other 83 | programming languages, like C++. 84 | 85 | Depending on the difficulty of the task, it might be implemented before others. 86 | 87 | ## Why? 88 | 89 | Currently metadata for Minecraft and modloaders is spread across multiple 90 | locations and in differing formats, making it difficult for launchers 91 | to provide installers for loaders. It doesn't have to be like this though. 92 | 93 | Launchers like MultiMC and Prism Launcher use scripts to generate metadata 94 | specific to their launcher. While this works for the context of a single 95 | launcher and its forks, it's not sustainable in the long run and doesn't 96 | invite for innovation to happen. The current formats also might not be 97 | efficient for both storage and usage in a launcher. 98 | -------------------------------------------------------------------------------- /static/mojang/minecraft-legacy-services.json: -------------------------------------------------------------------------------- 1 | [ 2 | "c0.30_01c", 3 | "inf-20100618", 4 | "a1.0.4", 5 | "a1.0.5_01", 6 | "a1.0.11", 7 | "a1.0.14", 8 | "a1.0.15", 9 | "a1.0.16", 10 | "a1.0.17_02", 11 | "a1.0.17_04", 12 | "a1.1.0", 13 | "a1.1.2", 14 | "a1.1.2_01", 15 | "a1.2.0", 16 | "a1.2.0_01", 17 | "a1.2.0_02", 18 | "a1.2.1", 19 | "a1.2.1_01", 20 | "a1.2.2a", 21 | "a1.2.2b", 22 | "a1.2.3", 23 | "a1.2.3_01", 24 | "a1.2.3_02", 25 | "a1.2.3_04", 26 | "a1.2.4_01", 27 | "a1.2.5", 28 | "a1.2.6", 29 | "b1.0", 30 | "b1.0_01", 31 | "b1.0.2", 32 | "b1.1_01", 33 | "b1.1_02", 34 | "b1.2", 35 | "b1.2_01", 36 | "b1.2_02", 37 | "b1.3b", 38 | "b1.3_01", 39 | "b1.4", 40 | "b1.4_01", 41 | "b1.5", 42 | "b1.5_01", 43 | "b1.6", 44 | "b1.6.1", 45 | "b1.6.2", 46 | "b1.6.3", 47 | "b1.6.4", 48 | "b1.6.5", 49 | "b1.6.6", 50 | "b1.7", 51 | "b1.7.2", 52 | "b1.7.3", 53 | "b1.8-pre1-2", 54 | "b1.8-pre2", 55 | "b1.8", 56 | "b1.8.1", 57 | "b1.9-pre1", 58 | "b1.9-pre2", 59 | "b1.9-pre3", 60 | "b1.9-pre4", 61 | "b1.9-pre5", 62 | "b1.9-pre6", 63 | "1.0", 64 | "11w47a", 65 | "11w48a", 66 | "11w49a", 67 | "11w50a", 68 | "12w01a", 69 | "1.1", 70 | "12w03a", 71 | "12w04a", 72 | "12w05a", 73 | "12w05b", 74 | "12w06a", 75 | "12w07b", 76 | "12w07a", 77 | "12w08a", 78 | "1.2", 79 | "1.2.1", 80 | "1.2.2", 81 | "1.2.3", 82 | "1.2.4", 83 | "1.2.5", 84 | "12w16a", 85 | "12w17a", 86 | "12w18a", 87 | "12w19a", 88 | "12w21a", 89 | "12w21b", 90 | "12w22a", 91 | "12w23a", 92 | "12w23b", 93 | "12w24a", 94 | "12w25a", 95 | "12w26a", 96 | "12w27a", 97 | "12w30a", 98 | "12w30b", 99 | "12w30c", 100 | "12w30d", 101 | "12w30e", 102 | "1.3", 103 | "1.3.1", 104 | "12w32a", 105 | "1.3.2", 106 | "12w34a", 107 | "12w34b", 108 | "12w36a", 109 | "12w37a", 110 | "12w38a", 111 | "12w38b", 112 | "12w39a", 113 | "12w39b", 114 | "12w40a", 115 | "12w40b", 116 | "12w41a", 117 | "12w41b", 118 | "12w42a", 119 | "12w42b", 120 | "1.4", 121 | "1.4.1", 122 | "1.4.2", 123 | "1.4.3", 124 | "1.4.4", 125 | "1.4.5", 126 | "12w49a", 127 | "12w50a", 128 | "12w50b", 129 | "1.4.6", 130 | "1.4.7", 131 | "13w01a", 132 | "13w01b", 133 | "13w02a", 134 | "13w02b", 135 | "13w03a", 136 | "13w04a", 137 | "13w05a", 138 | "13w05b", 139 | "13w06a", 140 | "13w07a", 141 | "13w11a", 142 | "13w09a", 143 | "13w09b", 144 | "13w09c", 145 | "13w10a", 146 | "13w10b", 147 | "1.5", 148 | "13w12~", 149 | "1.5.1", 150 | "1.5.2", 151 | "13w17a", 152 | "13w18a", 153 | "13w18b", 154 | "13w18c", 155 | "13w19a", 156 | "13w21a", 157 | "13w21b", 158 | "13w22a", 159 | "13w23a", 160 | "13w23b", 161 | "13w24a", 162 | "13w24b", 163 | "13w25a", 164 | "13w25b", 165 | "13w25c", 166 | "13w26a", 167 | "1.6", 168 | "1.6.1", 169 | "1.6.2", 170 | "13w36a", 171 | "13w36b", 172 | "13w37a", 173 | "1.6.3", 174 | "13w37b", 175 | "1.6.4", 176 | "13w38a", 177 | "13w38b", 178 | "13w38c", 179 | "13w39a", 180 | "13w39b", 181 | "13w41a", 182 | "13w41b", 183 | "13w42a", 184 | "13w42b", 185 | "13w43a", 186 | "1.7", 187 | "1.7.1", 188 | "1.7.2", 189 | "13w47a", 190 | "13w47b", 191 | "13w47c", 192 | "13w47d", 193 | "13w47e", 194 | "13w48a", 195 | "13w48b", 196 | "13w49a", 197 | "1.7.3", 198 | "1.7.4", 199 | "14w02a", 200 | "14w02b", 201 | "14w02c", 202 | "14w03a", 203 | "14w03b", 204 | "14w04a", 205 | "14w04b", 206 | "14w05a", 207 | "14w05b", 208 | "14w06a", 209 | "14w06b", 210 | "14w07a", 211 | "1.7.5", 212 | "14w08a", 213 | "14w10b", 214 | "14w10c", 215 | "1.7.6-pre1", 216 | "1.7.6-pre2", 217 | "14w11a" 218 | ] 219 | -------------------------------------------------------------------------------- /mcmeta/src/storage/json.rs: -------------------------------------------------------------------------------- 1 | use eyre::{Context, Result}; 2 | use std::path::PathBuf; 3 | use tracing::{info, instrument}; 4 | 5 | use crate::errors::MetadataError; 6 | 7 | use super::{ResourcePath, Storage}; 8 | use crate::utils::{deserialize_json_value_with_error_path, parse_json_with_error_context}; 9 | 10 | #[derive(Debug)] 11 | pub struct StorageJson { 12 | pub path: PathBuf, 13 | } 14 | 15 | impl StorageJson { 16 | #[instrument(skip(self))] 17 | async fn load_table_raw(&self, path: &ResourcePath, id: &str) -> Result { 18 | let path = path.to_json_path(&self.path).join(format!("{}.json", id)); 19 | if !path.exists() { 20 | return Ok(serde_json::Value::default()); 21 | } 22 | let table_json = tokio::fs::read_to_string(path).await?; 23 | if table_json.is_empty() { 24 | return Ok(serde_json::Value::default()); 25 | } 26 | let table = parse_json_with_error_context(&table_json)?; 27 | Ok(table) 28 | } 29 | 30 | #[instrument(skip(self))] 31 | #[allow(dead_code)] 32 | async fn load_table( 33 | &self, 34 | path: &ResourcePath, 35 | id: &str, 36 | ) -> Result> { 37 | Ok(self 38 | .load_table_raw(path, id) 39 | .await? 40 | .as_object() 41 | .cloned() 42 | .unwrap_or_default()) 43 | } 44 | 45 | #[instrument(skip(self))] 46 | async fn store_table( 47 | &self, 48 | path: &ResourcePath, 49 | id: &str, 50 | data: &serde_json::Value, 51 | ) -> Result<()> { 52 | let json_data = serde_json::to_string_pretty(&data)?; 53 | let path = path.to_json_path(&self.path).join(format!("{}.json", id)); 54 | if !path.parent().is_some_and(std::path::Path::exists) { 55 | let parent = path.parent().unwrap(); 56 | info!( 57 | "directory at {} does not exist, creating it", 58 | parent.display() 59 | ); 60 | tokio::fs::create_dir_all(parent) 61 | .await 62 | .with_context(|| "Failed to create directory")?; 63 | } 64 | tokio::fs::write(path, &json_data).await?; 65 | Ok(()) 66 | } 67 | } 68 | 69 | impl Storage for StorageJson { 70 | async fn store_record( 71 | &self, 72 | path: impl super::IntoResourcePath, 73 | id: impl AsRef, 74 | data: D, 75 | ) -> color_eyre::Result<()> 76 | where 77 | D: serde::Serialize, 78 | { 79 | let path = path.into_resource()?; 80 | let id = id.as_ref(); 81 | let data_value = serde_json::to_value(&data)?; 82 | self.store_table(&path, id, &data_value).await 83 | } 84 | 85 | async fn store_records( 86 | &self, 87 | path: impl super::IntoResourcePath, 88 | data: impl IntoIterator, 89 | ) -> Result<()> 90 | where 91 | D: serde::Serialize, 92 | K: AsRef, 93 | { 94 | let path = path.into_resource()?; 95 | for (id, record) in data { 96 | let id = id.as_ref(); 97 | let data_value = serde_json::to_value(&record)?; 98 | self.store_table(&path, id, &data_value).await?; 99 | } 100 | Ok(()) 101 | } 102 | 103 | async fn fetch_record( 104 | &self, 105 | path: impl super::IntoResourcePath, 106 | id: impl AsRef, 107 | ) -> Result> 108 | where 109 | D: serde::de::DeserializeOwned, 110 | { 111 | let path = path.into_resource()?; 112 | let id = id.as_ref(); 113 | let table = self.load_table_raw(&path, id).await?; 114 | deserialize_json_value_with_error_path(&table) 115 | } 116 | 117 | async fn fetch_records( 118 | &self, 119 | path: impl super::IntoResourcePath, 120 | ids: impl IntoIterator, 121 | ) -> Result> 122 | where 123 | K: AsRef, 124 | D: serde::de::DeserializeOwned, 125 | { 126 | let path = path.into_resource()?; 127 | let ids = ids 128 | .into_iter() 129 | .map(|id| id.as_ref().to_string()) 130 | .collect::>(); 131 | use futures::StreamExt; 132 | let results = futures::stream::iter(ids) 133 | .map(|id| async { 134 | self.load_table_raw(&path, &id) 135 | .await 136 | .and_then(|r| deserialize_json_value_with_error_path::(&r)) 137 | .map(|r| (id, r)) 138 | }) 139 | .buffer_unordered(5) 140 | .collect::>() 141 | .await; 142 | let (records, errors) = crate::utils::process_results(results); 143 | if !errors.is_empty() { 144 | Err( 145 | MetadataError::BulkProcessingError(String::from("Collecting Json Records"), errors) 146 | .into(), 147 | ) 148 | } else { 149 | Ok(records) 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /static/mojang/minecraft-experiments.json: -------------------------------------------------------------------------------- 1 | { 2 | "experiments": [ 3 | { 4 | "id": "1_19_deep_dark_experimental_snapshot-1", 5 | "wiki": "https://minecraft.wiki/w/Java_Edition_Deep_Dark_Experimental_Snapshot_1", 6 | "url": "https://launcher.mojang.com/v1/objects/b1e589c1d6ed73519797214bc796e53f5429ac46/1_19_deep_dark_experimental_snapshot-1.zip" 7 | }, 8 | { 9 | "id": "1_18_experimental-snapshot-7", 10 | "wiki": "https://minecraft.wiki/w/Java_Edition_1.18_Experimental_Snapshot_7", 11 | "url": "https://launcher.mojang.com/v1/objects/ab4ecebb133f56dd4c4c4c3257f030a947ddea84/1_18_experimental-snapshot-7.zip" 12 | }, 13 | { 14 | "id": "1_18_experimental-snapshot-6", 15 | "wiki": "https://minecraft.wiki/w/Java_Edition_1.18_Experimental_Snapshot_6", 16 | "url": "https://launcher.mojang.com/v1/objects/4697c84c6a347d0b8766759d5b00bc5a00b1b858/1_18_experimental-snapshot-6.zip" 17 | }, 18 | { 19 | "id": "1_18_experimental-snapshot-5", 20 | "wiki": "https://minecraft.wiki/w/Java_Edition_1.18_Experimental_Snapshot_5", 21 | "url": "https://launcher.mojang.com/v1/objects/d9cb7f6fb4e440862adfb40a385d83e3f8d154db/1_18_experimental-snapshot-5.zip" 22 | }, 23 | { 24 | "id": "1_18_experimental-snapshot-4", 25 | "wiki": "https://minecraft.wiki/w/Java_Edition_1.18_Experimental_Snapshot_4", 26 | "url": "https://launcher.mojang.com/v1/objects/b92a360cbae2eb896a62964ad8c06c3493b6c390/1_18_experimental-snapshot-4.zip" 27 | }, 28 | { 29 | "id": "1_18_experimental-snapshot-3", 30 | "wiki": "https://minecraft.wiki/w/Java_Edition_1.18_Experimental_Snapshot_3", 31 | "url": "https://launcher.mojang.com/v1/objects/846648ff9fe60310d584061261de43010e5c722b/1_18_experimental-snapshot-3.zip" 32 | }, 33 | { 34 | "id": "1_18_experimental-snapshot-2", 35 | "wiki": "https://minecraft.wiki/w/Java_Edition_1.18_Experimental_Snapshot_2", 36 | "url": "https://launcher.mojang.com/v1/objects/0adfe4f321aa45248fc88ac888bed5556633e7fb/1_18_experimental-snapshot-2.zip" 37 | }, 38 | { 39 | "id": "1_18_experimental-snapshot-1", 40 | "wiki": "https://minecraft.wiki/w/Java_Edition_1.18_Experimental_Snapshot_1", 41 | "url": "https://launcher.mojang.com/v1/objects/231bba2a21e18b8c60976e1f6110c053b7b93226/1_18_experimental-snapshot-1.zip" 42 | }, 43 | { 44 | "id": "1_16_combat-6", 45 | "wiki": "https://minecraft.wiki/w/Java_Edition_Combat_Test_8c", 46 | "url": "https://launcher.mojang.com/experiments/combat/ea08f7eb1f96cdc82464e27c0f95d23965083cfb/1_16_combat-6.zip" 47 | }, 48 | { 49 | "id": "1_16_combat-5", 50 | "wiki": "https://minecraft.wiki/w/Java_Edition_Combat_Test_8b", 51 | "url": "https://launcher.mojang.com/experiments/combat/9b2b984d635d373564b50803807225c75d7fd447/1_16_combat-5.zip" 52 | }, 53 | { 54 | "id": "1_16_combat-4", 55 | "wiki": "https://minecraft.wiki/w/Java_Edition_Combat_Test_8", 56 | "url": "https://archive.org/download/1-16-combat-4_202404/1_16_combat-4.zip" 57 | }, 58 | { 59 | "id": "1_16_combat-3", 60 | "wiki": "https://minecraft.wiki/w/Java_Edition_Combat_Test_7c", 61 | "url": "https://launcher.mojang.com/experiments/combat/2557b99d95588505e988886220779087d7d6b1e9/1_16_combat-3.zip" 62 | }, 63 | { 64 | "id": "1_16_combat-2", 65 | "wiki": "https://minecraft.wiki/w/Java_Edition_Combat_Test_7b", 66 | "url": "https://archive.org/download/Combat_Test_7ab/1_16_combat-2.zip" 67 | }, 68 | { 69 | "id": "1_16_combat-1", 70 | "wiki": "https://minecraft.wiki/w/Java_Edition_Combat_Test_7", 71 | "url": "https://archive.org/download/Combat_Test_7ab/1_16_combat-1.zip" 72 | }, 73 | { 74 | "id": "1_16_combat-0", 75 | "wiki": "https://minecraft.wiki/w/Java_Edition_Combat_Test_6", 76 | "url": "https://launcher.mojang.com/experiments/combat/5a8ceec8681ed96ab6ecb9607fb5d19c8a755559/1_16_combat-0.zip" 77 | }, 78 | { 79 | "id": "1_15_combat-6", 80 | "wiki": "https://minecraft.wiki/w/Java_Edition_Combat_Test_5", 81 | "url": "https://launcher.mojang.com/experiments/combat/52263d42a626b40c947e523128f7a195ec5af76a/1_15_combat-6.zip" 82 | }, 83 | { 84 | "id": "1_15_combat-1", 85 | "wiki": "https://minecraft.wiki/w/Java_Edition_Combat_Test_4", 86 | "url": "https://launcher.mojang.com/experiments/combat/ac11ea96f3bb2fa2b9b76ab1d20cacb1b1f7ef60/1_15_combat-1.zip" 87 | }, 88 | { 89 | "id": "1_14_combat-3", 90 | "wiki": "https://minecraft.wiki/w/Java_Edition_Combat_Test_3", 91 | "url": "https://launcher.mojang.com/experiments/combat/0f209c9c84b81c7d4c88b4632155b9ae550beb89/1_14_combat-3.zip" 92 | }, 93 | { 94 | "id": "1_14_combat-0", 95 | "wiki": "https://minecraft.wiki/w/Java_Edition_Combat_Test_2", 96 | "url": "https://launcher.mojang.com/experiments/combat/d164bb6ecc5fca9ac02878c85f11befae61ac1ca/1_14_combat-0.zip" 97 | }, 98 | { 99 | "id": "1_14_combat-212796", 100 | "wiki": "https://minecraft.wiki/w/Java_Edition_1.14.3_-_Combat_Test", 101 | "url": "https://launcher.mojang.com/experiments/combat/610f5c9874ba8926d5ae1bcce647e5f0e6e7c889/1_14_combat-212796.zip" 102 | } 103 | ] 104 | } 105 | -------------------------------------------------------------------------------- /mcmeta/src/storage/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use crate::app_config::StorageFormat; 4 | use enum_dispatch::enum_dispatch; 5 | use eyre::Result; 6 | use serde::{de::DeserializeOwned, Serialize}; 7 | use tracing::{debug, info, instrument}; 8 | 9 | mod database; 10 | mod json; 11 | 12 | use database::StorageDatabase; 13 | use json::StorageJson; 14 | 15 | #[derive(Clone)] 16 | pub struct ResourcePath { 17 | parts: Vec, 18 | } 19 | 20 | impl std::fmt::Debug for ResourcePath { 21 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { 22 | f.write_fmt(format_args!("ResourcePath({})", self.parts.join("/"))) 23 | } 24 | } 25 | 26 | impl ResourcePath { 27 | pub fn new(parts: T) -> Self 28 | where 29 | T: IntoIterator, 30 | I: Into, 31 | { 32 | let parts = parts.into_iter().map(Into::into).collect::>(); 33 | ResourcePath { parts } 34 | } 35 | 36 | pub fn join(&self, part: I) -> Self 37 | where 38 | I: Into, 39 | { 40 | let mut parts = self.parts.clone(); 41 | parts.push(part.into()); 42 | ResourcePath { parts } 43 | } 44 | 45 | pub fn to_json_path(&self, root: impl AsRef) -> PathBuf { 46 | let mut path = root.as_ref().to_path_buf(); 47 | for part in &self.parts { 48 | path = path.join(part); 49 | } 50 | path 51 | } 52 | 53 | pub fn to_db_path(&self, prefix: &str) -> String { 54 | format!("{}__{}", prefix, self.parts.join("__")) 55 | } 56 | } 57 | 58 | pub trait IntoResourcePath { 59 | fn into_resource(self) -> Result; 60 | } 61 | 62 | impl IntoResourcePath for R 63 | where 64 | R: IntoIterator, 65 | S: Into, 66 | { 67 | fn into_resource(self) -> Result { 68 | let parts = self.into_iter().map(Into::into).collect::>(); 69 | Ok(ResourcePath { parts }) 70 | } 71 | } 72 | 73 | impl IntoResourcePath for ResourcePath { 74 | fn into_resource(self) -> Result { 75 | Ok(self) 76 | } 77 | } 78 | 79 | impl IntoResourcePath for &ResourcePath { 80 | fn into_resource(self) -> Result { 81 | Ok(self.clone()) 82 | } 83 | } 84 | 85 | #[enum_dispatch(Storage)] 86 | #[derive(Debug)] 87 | pub enum StorageImpl { 88 | StorageJson, 89 | StorageDatabase, 90 | } 91 | 92 | #[enum_dispatch] 93 | pub trait Storage { 94 | /// store a record 95 | async fn store_record( 96 | &self, 97 | path: impl IntoResourcePath, 98 | id: impl AsRef, 99 | data: D, 100 | ) -> Result<()> 101 | where 102 | D: Serialize; 103 | /// store multiple records using an iterator of (id, record) pairs 104 | async fn store_records( 105 | &self, 106 | path: impl IntoResourcePath, 107 | data: impl IntoIterator, 108 | ) -> Result<()> 109 | where 110 | D: Serialize, 111 | K: AsRef; 112 | /// fetch a record 113 | async fn fetch_record( 114 | &self, 115 | path: impl IntoResourcePath, 116 | id: impl AsRef, 117 | ) -> Result> 118 | where 119 | D: DeserializeOwned; 120 | /// fetch multiple records taking an iterator of id's 121 | async fn fetch_records( 122 | &self, 123 | path: impl IntoResourcePath, 124 | ids: impl IntoIterator, 125 | ) -> Result> 126 | where 127 | K: AsRef, 128 | D: DeserializeOwned; 129 | } 130 | 131 | impl StorageFormat { 132 | #[instrument] 133 | pub async fn build(&self) -> Result { 134 | match self { 135 | StorageFormat::Json { path } => { 136 | let path = std::path::Path::new(path); 137 | if !path.exists() { 138 | info!( 139 | "Raw Metadata directory at {} does not exist, creating it", 140 | path.display() 141 | ); 142 | tokio::fs::create_dir_all(path).await?; 143 | } 144 | Ok(StorageJson { 145 | path: path.to_path_buf(), 146 | } 147 | .into()) 148 | } 149 | StorageFormat::Database { 150 | connection_url, 151 | prefix, 152 | } => { 153 | use sqlx::migrate::MigrateDatabase; 154 | if !sqlx::Postgres::database_exists(connection_url) 155 | .await 156 | .unwrap_or(false) 157 | { 158 | debug!("Database Storage: Creating database? {}", connection_url); 159 | sqlx::Postgres::create_database(connection_url).await?; 160 | } 161 | let client = sqlx::postgres::PgPoolOptions::new() 162 | .max_connections(5) 163 | .connect(connection_url) 164 | .await?; 165 | let db = StorageDatabase { 166 | connection: connection_url.to_owned(), 167 | prefix: prefix.to_owned(), 168 | client, 169 | known_tables: std::collections::HashSet::new(), 170 | }; 171 | 172 | Ok(db.into()) 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /mcmeta/src/storage/database.rs: -------------------------------------------------------------------------------- 1 | use eyre::Result; 2 | use sqlx::{Execute, Executor}; 3 | use tracing::{debug, info, instrument}; 4 | 5 | use super::{ResourcePath, Storage}; 6 | use crate::utils::deserialize_json_with_error_path; 7 | 8 | #[derive(Debug)] 9 | pub struct StorageDatabase { 10 | #[allow(dead_code)] 11 | pub connection: String, 12 | pub prefix: String, 13 | pub client: sqlx::Pool, 14 | pub known_tables: std::collections::HashSet, 15 | } 16 | 17 | #[derive(sqlx::FromRow)] 18 | pub struct DatabaseRecord { 19 | #[allow(dead_code)] 20 | pub id: String, 21 | pub record: String, 22 | } 23 | 24 | impl StorageDatabase { 25 | #[instrument(skip(self))] 26 | pub async fn ensure_table(&self, table_name: &str) -> Result<()> { 27 | if !self.known_tables.contains(table_name) { 28 | let mut query = sqlx::QueryBuilder::new("CREATE TABLE IF NOT EXISTS "); 29 | query.push(table_name); 30 | query.push(" (id VARCHAR(511) PRIMARY KEY, record JSON);"); 31 | let query = query.build(); 32 | debug!("Database Storage: Running query `{}`", query.sql()); 33 | let res = self.client.execute(query).await?; 34 | info!("Database Storage: Created Table? {:?}", res); 35 | } 36 | Ok(()) 37 | } 38 | 39 | #[instrument(skip(self, records))] 40 | pub async fn insert_or_update_records( 41 | &self, 42 | path: &ResourcePath, 43 | records: impl IntoIterator, serde_json::Value)>, 44 | ) -> Result<()> { 45 | let table_name = path.to_db_path(&self.prefix); 46 | self.ensure_table(&table_name).await?; 47 | let mut query_builder = 48 | sqlx::QueryBuilder::new(format!("INSERT INTO {} (id, record) ", &table_name)); 49 | let records = records 50 | .into_iter() 51 | .map(|(id, record)| Ok((id.as_ref().to_string(), serde_json::to_string(&record)?))) 52 | .collect::>>()?; 53 | query_builder.push_values(records, |mut b, (id, record)| { 54 | b.push_bind(id).push_bind(record); 55 | }); 56 | query_builder.push("ON CONFLICT (id) DO UPDATE SET record = excluded.record"); 57 | let query = query_builder.build(); 58 | debug!("Database Storage: Running query `{}`", query.sql()); 59 | let _ = query.execute(&self.client).await?; 60 | Ok(()) 61 | } 62 | 63 | #[instrument(skip(self))] 64 | pub async fn fetch_record(&self, path: &ResourcePath, id: &str) -> Result { 65 | let table_name = path.to_db_path(&self.prefix); 66 | let mut query_builder = sqlx::QueryBuilder::new(format!( 67 | "SELECT (id, record) FROM {} WHERE id = ", 68 | &table_name 69 | )); 70 | query_builder.push_bind(id); 71 | let query = query_builder.build_query_as::(); 72 | debug!("Database Storage: Running query `{}`", query.sql()); 73 | let record: DatabaseRecord = query.fetch_one(&self.client).await?; 74 | Ok(record) 75 | } 76 | 77 | #[instrument(skip(self, ids))] 78 | pub async fn fetch_records( 79 | &self, 80 | path: &ResourcePath, 81 | ids: impl IntoIterator, 82 | ) -> Result> 83 | where 84 | K: AsRef, 85 | { 86 | let table_name = path.to_db_path(&self.prefix); 87 | let mut query_builder = sqlx::QueryBuilder::new(format!( 88 | "SELECT (id, record) FROM {} WHERE id in (", 89 | &table_name 90 | )); 91 | let mut separated = query_builder.separated(", "); 92 | for id in ids { 93 | separated.push_bind(id.as_ref().to_string()); 94 | } 95 | separated.push_unseparated(") "); 96 | 97 | let query = query_builder.build_query_as::(); 98 | debug!("Database Storage: Running query `{}`", query.sql()); 99 | let records: Vec = query.fetch_all(&self.client).await?; 100 | Ok(records) 101 | } 102 | } 103 | 104 | impl Storage for StorageDatabase { 105 | async fn store_record( 106 | &self, 107 | path: impl super::IntoResourcePath, 108 | id: impl AsRef, 109 | data: D, 110 | ) -> Result<()> 111 | where 112 | D: serde::Serialize, 113 | { 114 | let path = path.into_resource()?; 115 | let id = id.as_ref(); 116 | let data_value = serde_json::to_value(&data)?; 117 | self.insert_or_update_records(&path, vec![(id, data_value)]) 118 | .await 119 | } 120 | 121 | async fn store_records( 122 | &self, 123 | path: impl super::IntoResourcePath, 124 | data: impl IntoIterator, 125 | ) -> Result<()> 126 | where 127 | D: serde::Serialize, 128 | K: AsRef, 129 | { 130 | let path = path.into_resource()?; 131 | self.insert_or_update_records( 132 | &path, 133 | data.into_iter() 134 | .map(|(id, record)| Ok((id, serde_json::to_value(&record)?))) 135 | .collect::>>()?, 136 | ) 137 | .await 138 | } 139 | 140 | async fn fetch_record( 141 | &self, 142 | path: impl super::IntoResourcePath, 143 | id: impl AsRef, 144 | ) -> Result> 145 | where 146 | D: serde::de::DeserializeOwned, 147 | { 148 | let path = path.into_resource()?; 149 | let id = id.as_ref(); 150 | let record = self.fetch_record(&path, id).await?; 151 | deserialize_json_with_error_path(&record.record) 152 | } 153 | 154 | async fn fetch_records( 155 | &self, 156 | path: impl super::IntoResourcePath, 157 | ids: impl IntoIterator, 158 | ) -> Result> 159 | where 160 | K: AsRef, 161 | D: serde::de::DeserializeOwned, 162 | { 163 | let path = path.into_resource()?; 164 | let records = self.fetch_records(&path, ids).await?; 165 | records 166 | .into_iter() 167 | .map(|r| deserialize_json_with_error_path(&r.record)) 168 | .collect::>>() 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /libmcmeta/LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /mcmeta/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{str::FromStr, sync::Arc}; 2 | 3 | use app_config::MetaConfig; 4 | use axum::{routing::get, Router}; 5 | 6 | use routes::ServerState; 7 | use storage::StorageImpl; 8 | use tracing::{info, warn}; 9 | 10 | use indicatif::ProgressState; 11 | use indicatif::ProgressStyle; 12 | use tracing::instrument; 13 | use tracing_indicatif::IndicatifLayer; 14 | use tracing_subscriber::layer::SubscriberExt; 15 | use tracing_subscriber::util::SubscriberInitExt; 16 | 17 | use tokio::signal::ctrl_c; 18 | #[cfg(target_family = "unix")] 19 | use tokio::signal::unix::{signal, SignalKind}; 20 | #[cfg(target_family = "windows")] 21 | use tokio::signal::windows::ctrl_close; 22 | 23 | use dotenv::dotenv; 24 | use eyre::Result; 25 | use tracing_subscriber::{filter, prelude::*}; 26 | 27 | mod app_config; 28 | mod errors; 29 | mod routes; 30 | mod storage; 31 | mod upstream; 32 | mod utils; 33 | 34 | #[macro_use] 35 | extern crate lazy_static; 36 | 37 | use clap::Parser; 38 | use upstream::UpstreamMetadataUpdater; 39 | 40 | #[derive(Parser, Debug)] 41 | #[command(author, version, about, long_about = None)] 42 | struct CliArgs { 43 | #[arg(short, long, value_name = "FILE")] 44 | config: Option, 45 | #[arg(long)] 46 | use_dotenv: bool, 47 | #[command(subcommand)] 48 | command: Option, 49 | } 50 | 51 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum, strum::EnumIter)] 52 | pub enum UpstreamSources { 53 | Mojang, 54 | Forge, 55 | All, 56 | } 57 | pub fn all_upstream_sources() -> Vec { 58 | use strum::IntoEnumIterator; 59 | UpstreamSources::iter() 60 | .filter(|i| !matches!(i, UpstreamSources::All)) 61 | .collect() 62 | } 63 | 64 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum, strum::EnumIter)] 65 | pub enum GeneratedSources { 66 | All, 67 | } 68 | pub fn all_generated_sources() -> Vec { 69 | use strum::IntoEnumIterator; 70 | GeneratedSources::iter() 71 | .filter(|i| !matches!(i, GeneratedSources::All)) 72 | .collect() 73 | } 74 | 75 | #[derive(Debug, clap::Subcommand)] 76 | enum Command { 77 | Fetch { 78 | #[arg(value_enum, short, long, default_values_t = all_upstream_sources())] 79 | sources: Vec, 80 | }, 81 | Generate { 82 | #[arg(value_enum, short, long, default_values_t = all_generated_sources())] 83 | sources: Vec, 84 | }, 85 | FetchAndGenerate { 86 | #[arg(value_enum, short, long, default_values_t = all_upstream_sources())] 87 | fetch: Vec, 88 | #[arg(value_enum, short, long, default_values_t = all_generated_sources())] 89 | generate: Vec, 90 | }, 91 | Serve { 92 | bind_address: Option, 93 | }, 94 | } 95 | fn elapsed_subsec(state: &ProgressState, writer: &mut dyn std::fmt::Write) { 96 | let seconds = state.elapsed().as_secs(); 97 | let sub_seconds = (state.elapsed().as_millis() % 1000) / 100; 98 | let _ = writer.write_str(&format!("{}.{}s", seconds, sub_seconds)); 99 | } 100 | 101 | #[tokio::main] 102 | async fn main() -> Result<()> { 103 | color_eyre::install()?; 104 | 105 | let mut config_path = String::from("./mcmeta.toml"); 106 | 107 | let args = CliArgs::parse(); 108 | 109 | if args.use_dotenv { 110 | dotenv().ok(); 111 | } 112 | 113 | if let Some(path) = args.config { 114 | config_path = path; 115 | } 116 | 117 | let config: Arc = Arc::new(MetaConfig::from_config(&config_path)?); 118 | 119 | let file_appender = 120 | tracing_appender::rolling::hourly(&config.debug_log.path, &config.debug_log.prefix); 121 | let (non_blocking_file, _guard) = tracing_appender::non_blocking(file_appender); 122 | 123 | let tracing_filter_level = 124 | tracing::Level::from_str(&config.debug_log.level).unwrap_or(tracing::Level::INFO); 125 | 126 | let indicatif_layer = IndicatifLayer::new() 127 | .with_progress_style( 128 | ProgressStyle::with_template( 129 | "{span_child_prefix}{span_fields} -- {span_name} {wide_msg} {elapsed_subsec}", 130 | ) 131 | .unwrap() 132 | .with_key("elapsed_subsec", elapsed_subsec), 133 | ) 134 | .with_span_child_prefix_symbol("↳ ") 135 | .with_span_child_prefix_indent(" "); 136 | 137 | let stdout_layer = tracing_subscriber::fmt::layer() 138 | .with_writer(indicatif_layer.get_stdout_writer()) 139 | .with_filter(filter::LevelFilter::from_level(tracing_filter_level)); 140 | 141 | let debug_log = tracing_subscriber::fmt::layer() 142 | .with_ansi(false) 143 | .with_level(true) 144 | .with_file(true) 145 | .with_line_number(true) 146 | .with_writer(non_blocking_file) 147 | .with_filter(filter::LevelFilter::from_level(tracing_filter_level)); 148 | 149 | if config.debug_log.enable { 150 | tracing_subscriber::registry() 151 | .with(stdout_layer) 152 | .with(indicatif_layer) 153 | .with(debug_log) 154 | .init(); 155 | } else { 156 | tracing_subscriber::registry() 157 | .with(stdout_layer) 158 | .with(indicatif_layer) 159 | .init(); 160 | } 161 | 162 | info!("Config: {:#?}", config); 163 | 164 | let upstream_storage = Arc::new(config.upstream.storage_format.build().await?); 165 | let generated_storage = Arc::new(config.generated.storage_format.build().await?); 166 | 167 | let command = args.command.unwrap_or_else(|| Command::FetchAndGenerate { 168 | fetch: all_upstream_sources(), 169 | generate: all_generated_sources(), 170 | }); 171 | 172 | #[cfg(target_family = "unix")] 173 | let mut sigterm = signal(SignalKind::terminate())?; 174 | #[cfg(target_family = "windows")] 175 | let mut sigterm = ctrl_close()?; 176 | 177 | tokio::select! { 178 | result = run(command, config, upstream_storage, generated_storage) => result, 179 | _ = sigterm.recv() => { 180 | handle_shutdown("Received SIGTERM"); 181 | std::process::exit(0); 182 | } 183 | _ = ctrl_c() => { 184 | handle_shutdown("Interrupted"); 185 | std::process::exit(130); 186 | } 187 | } 188 | } 189 | 190 | fn handle_shutdown(reason: &str) { 191 | warn!("{reason}! Shutting down ..."); 192 | println!("Everything is shutdown. Goodbye!"); 193 | } 194 | 195 | async fn run( 196 | command: Command, 197 | config: Arc, 198 | upstream_storage: Arc, 199 | generated_storage: Arc, 200 | ) -> Result<()> { 201 | match command { 202 | Command::Fetch { sources } => { 203 | let sources = if sources.contains(&UpstreamSources::All) { 204 | all_upstream_sources() 205 | } else { 206 | sources 207 | }; 208 | fetch(config, upstream_storage, &sources).await 209 | } 210 | Command::Generate { sources } => { 211 | let sources = if sources.contains(&GeneratedSources::All) { 212 | all_generated_sources() 213 | } else { 214 | sources 215 | }; 216 | generate(config, generated_storage, &sources).await 217 | } 218 | Command::FetchAndGenerate { 219 | fetch: to_fetch, 220 | generate: to_generate, 221 | } => { 222 | let to_fetch = if to_fetch.contains(&UpstreamSources::All) { 223 | all_upstream_sources() 224 | } else { 225 | to_fetch 226 | }; 227 | let to_generate = if to_generate.contains(&GeneratedSources::All) { 228 | all_generated_sources() 229 | } else { 230 | to_generate 231 | }; 232 | fetch(config.clone(), upstream_storage.clone(), &to_fetch).await?; 233 | generate(config, generated_storage, &to_generate).await 234 | } 235 | Command::Serve { bind_address } => { 236 | serve(bind_address, config, upstream_storage, generated_storage).await 237 | } 238 | } 239 | } 240 | 241 | #[instrument(skip_all)] 242 | async fn fetch( 243 | config: Arc, 244 | storage: Arc, 245 | sources: &[UpstreamSources], 246 | ) -> Result<()> { 247 | UpstreamMetadataUpdater::new(config.upstream.clone(), storage) 248 | .update(sources) 249 | .await 250 | } 251 | 252 | #[allow(dead_code)] 253 | #[instrument(skip_all)] 254 | async fn generate( 255 | config: Arc, 256 | storage: Arc, 257 | sources: &[GeneratedSources], 258 | ) -> Result<()> { 259 | Ok(()) 260 | } 261 | 262 | #[instrument(skip_all)] 263 | async fn serve( 264 | bind_address: Option, 265 | config: Arc, 266 | upstream_storage: Arc, 267 | generated_storage: Arc, 268 | ) -> eyre::Result<()> { 269 | // config 270 | // .storage_format 271 | // .update_upstream_metadata(&config.metadata) 272 | // .await?; 273 | 274 | let raw_mojang_routes = Router::new() 275 | .route("/", get(routes::mojang::raw_mojang_manifest)) 276 | .route("/:version", get(routes::mojang::raw_mojang_version)); 277 | let raw_forge_routes = Router::new() 278 | .route("/", get(routes::forge::raw_forge_maven_meta)) 279 | .route("/promotions", get(routes::forge::raw_forge_promotions)) 280 | .route("/:version", get(routes::forge::raw_forge_version)) 281 | .route("/:version/meta", get(routes::forge::raw_forge_version_meta)) 282 | .route( 283 | "/:version/installer", 284 | get(routes::forge::raw_forge_version_installer), 285 | ); 286 | 287 | let raw_routes = Router::new() 288 | .nest("/mojang", raw_mojang_routes) 289 | .nest("/forge", raw_forge_routes); 290 | 291 | let addr = bind_address.unwrap_or_else(|| config.bind_address.clone()); 292 | 293 | let http = Router::new() 294 | .nest("/raw", raw_routes) 295 | .with_state(Arc::new(ServerState { 296 | config, 297 | upstream_storage, 298 | generated_storage, 299 | })); 300 | 301 | info!("Starting server on {}", &addr); 302 | let listener = tokio::net::TcpListener::bind(&addr).await?; 303 | axum::serve(listener, http).await?; 304 | 305 | Ok(()) 306 | } 307 | -------------------------------------------------------------------------------- /mcmeta/src/utils.rs: -------------------------------------------------------------------------------- 1 | use eyre::Result; 2 | 3 | pub fn parse_json_with_error_context(json: &str) -> Result { 4 | let value: serde_json::Value = serde_json::from_str(json) 5 | .map_err(|err| crate::errors::MetadataError::from_json_err(err, json))?; 6 | Ok(value) 7 | } 8 | 9 | pub fn deserialize_json_with_error_path(json: &str) -> Result 10 | where 11 | D: serde::de::DeserializeOwned, 12 | { 13 | let value = parse_json_with_error_context(json)?; 14 | deserialize_json_value_with_error_path(&value) 15 | } 16 | 17 | pub fn deserialize_json_value_with_error_path(value: &serde_json::Value) -> Result 18 | where 19 | D: serde::de::DeserializeOwned, 20 | { 21 | let value = serde_path_to_error::deserialize(value)?; 22 | Ok(value) 23 | } 24 | 25 | fn json_matching_brace(c: char) -> char { 26 | match c { 27 | '[' => ']', 28 | ']' => '[', 29 | '{' => '}', 30 | '}' => '{', 31 | other => other, 32 | } 33 | } 34 | 35 | /// Attempts to read a complete json object at the error location from the provide body 36 | /// to provided context to a deserialisation error. only useful if the error was caused 37 | /// by a data mismatch not a syntax error or EOF. 38 | #[allow(dead_code)] 39 | pub fn get_json_context(err: &serde_json::Error, body: &str, max_len: usize) -> String { 40 | let line_offset = body 41 | .char_indices() 42 | .filter(|(_, c)| *c == '\n') 43 | .nth(err.line() - 1) 44 | .unwrap_or_default() 45 | .0; 46 | let mut ctx = body.split_at(line_offset).1.to_owned(); 47 | let offset = ctx.char_indices().nth(err.column()).unwrap().0; 48 | ctx = ctx.split_at(offset).1.to_owned(); 49 | 50 | let mut token_contexts: Vec = vec![]; 51 | let mut excape_next = false; 52 | let mut in_str = false; 53 | let mut found_close = false; 54 | let mut ctx_end = 0; 55 | let mut last_char: char = ctx.chars().next().unwrap_or_default(); 56 | let whitespace = [' ', '\t', '\n', '\r']; 57 | let mut whitespace_chain = 0; 58 | for (i, c) in ctx.char_indices() { 59 | if whitespace.contains(&c) 60 | && (['\'', '"'].contains(&last_char) || whitespace.contains(&last_char)) 61 | { 62 | whitespace_chain += 1; 63 | } else if !whitespace.contains(&c) && whitespace_chain > 0 { 64 | whitespace_chain = 0; 65 | } 66 | if [':', '[', ']', '{', '}'].contains(&c) && whitespace_chain > 0 && in_str { 67 | in_str = false; 68 | token_contexts.pop(); 69 | } 70 | if c == '\\' && in_str && !excape_next { 71 | excape_next = true; 72 | continue; 73 | } 74 | if c == '"' || c == '\'' { 75 | if in_str && !excape_next && token_contexts.last() == Some(&c) { 76 | in_str = false; 77 | found_close = true; 78 | token_contexts.pop(); 79 | } else { 80 | in_str = true; 81 | token_contexts.push(c); 82 | } 83 | } 84 | if (c == '[' || c == '{') && !in_str { 85 | token_contexts.push(c); 86 | } 87 | if (c == ']' || c == '}') 88 | && !in_str 89 | && token_contexts.last() == Some(&json_matching_brace(c)) 90 | { 91 | token_contexts.pop(); 92 | found_close = true; 93 | } 94 | if (c == ',') && !in_str { 95 | found_close = true; 96 | } 97 | 98 | if excape_next { 99 | excape_next = false; 100 | } 101 | if found_close && token_contexts.is_empty() { 102 | ctx_end = i; 103 | last_char = c; 104 | break; 105 | } 106 | } 107 | 108 | if ctx_end > 0 { 109 | ctx = ctx[..ctx_end].to_owned() + &last_char.to_string(); 110 | } 111 | 112 | if max_len > 0 && ctx.chars().count() > max_len { 113 | ctx = ctx 114 | .split_at(ctx.char_indices().nth(max_len).unwrap().0) 115 | .0 116 | .to_owned() 117 | + " ..."; 118 | } 119 | ctx 120 | } 121 | /// Attempts to read a complete json object just before the error location from the provide body 122 | /// to provided context to a deserialisation error. only useful if the error was caused 123 | /// by a data mismatch not a syntax error or EOF. 124 | pub fn get_json_context_back(err: &serde_json::Error, body: &str, max_len: usize) -> String { 125 | let line_offset = body 126 | .char_indices() 127 | .filter(|(_, c)| *c == '\n') 128 | .nth(err.line() - 1) 129 | .unwrap_or_default() 130 | .0; 131 | let (pre_line, ctx_line) = body.split_at(line_offset); 132 | let mut ctx = ctx_line.to_owned(); 133 | let offset = ctx.char_indices().nth(err.column()).unwrap().0; 134 | ctx = ctx.split_at(offset).0.to_owned(); 135 | ctx = pre_line.to_owned() + &ctx; 136 | 137 | let mut token_contexts: Vec = vec![]; 138 | let mut string_open_pre = false; 139 | let mut in_str = false; 140 | let mut found_open = false; 141 | let mut found_close = false; 142 | let mut ctx_end = 0; 143 | let mut last_char: char = ctx.chars().next_back().unwrap_or_default(); 144 | let whitespace = [' ', '\t', '\n', '\r']; 145 | let mut whitespace_chain = 0; 146 | for (i, c) in ctx.char_indices().rev() { 147 | if whitespace.contains(&c) 148 | && (['\'', '"'].contains(&last_char) || whitespace.contains(&last_char)) 149 | { 150 | whitespace_chain += 1; 151 | } else if !whitespace.contains(&c) && whitespace_chain > 0 { 152 | whitespace_chain = 0; 153 | } 154 | if [':', '[', ']', '{', '}'].contains(&c) && whitespace_chain > 0 && in_str { 155 | in_str = false; 156 | token_contexts.pop(); 157 | } 158 | if c == '\\' && !in_str && string_open_pre { 159 | token_contexts.push(last_char); 160 | found_open = false; 161 | continue; 162 | } 163 | if c == '"' || c == '\'' { 164 | if in_str && token_contexts.last() == Some(&c) { 165 | in_str = false; 166 | found_open = true; 167 | token_contexts.pop(); 168 | } else { 169 | in_str = true; 170 | found_close = true; 171 | token_contexts.push(c); 172 | } 173 | } 174 | if (c == ']' || c == '}') && !in_str { 175 | token_contexts.push(c); 176 | found_close = true; 177 | } 178 | if (c == '[' || c == '{') 179 | && !in_str 180 | && token_contexts.last() == Some(&json_matching_brace(c)) 181 | { 182 | token_contexts.pop(); 183 | found_open = true; 184 | } 185 | if (c == ',') && !in_str && found_close { 186 | found_open = true; 187 | } 188 | 189 | if !in_str && string_open_pre { 190 | string_open_pre = false; 191 | } 192 | 193 | if in_str { 194 | string_open_pre = true; 195 | } 196 | 197 | last_char = c; 198 | if found_open && token_contexts.is_empty() { 199 | ctx_end = i; 200 | break; 201 | } 202 | } 203 | 204 | if ctx_end > 0 { 205 | ctx = ctx[ctx_end..].to_owned(); 206 | } 207 | 208 | if max_len > 0 && ctx.chars().count() > max_len { 209 | ctx = "... ".to_owned() 210 | + ctx 211 | .split_at(ctx.char_indices().rev().nth(max_len).unwrap().0) 212 | .0; 213 | } 214 | ctx 215 | } 216 | 217 | pub enum HashAlgo { 218 | Sha1, 219 | Sha256, 220 | Md5, 221 | } 222 | 223 | #[allow(dead_code)] 224 | pub fn filehash(path: &std::path::PathBuf, algo: HashAlgo) -> Result { 225 | match algo { 226 | HashAlgo::Sha1 => { 227 | use sha1::{Digest, Sha1}; 228 | 229 | let mut hasher = Sha1::new(); 230 | let mut file = std::fs::File::open(path)?; 231 | let _bytes_written = std::io::copy(&mut file, &mut hasher)?; 232 | let hash_bytes = hasher.finalize(); 233 | Ok(format!("{:X}", hash_bytes)) 234 | } 235 | HashAlgo::Sha256 => { 236 | use sha2::{Digest, Sha256}; 237 | 238 | let mut hasher = Sha256::new(); 239 | let mut file = std::fs::File::open(path)?; 240 | let _bytes_written = std::io::copy(&mut file, &mut hasher)?; 241 | let hash_bytes = hasher.finalize(); 242 | Ok(format!("{:X}", hash_bytes)) 243 | } 244 | HashAlgo::Md5 => { 245 | use md5::{Digest, Md5}; 246 | 247 | let mut hasher = Md5::new(); 248 | let mut file = std::fs::File::open(path)?; 249 | let _bytes_written = std::io::copy(&mut file, &mut hasher)?; 250 | let hash_bytes = hasher.finalize(); 251 | Ok(format!("{:X}", hash_bytes)) 252 | } 253 | } 254 | } 255 | 256 | #[allow(dead_code)] 257 | pub fn hash(data: impl AsRef<[u8]>, algo: HashAlgo) -> String { 258 | match algo { 259 | HashAlgo::Sha1 => { 260 | use sha1::{Digest, Sha1}; 261 | 262 | let mut hasher = Sha1::new(); 263 | hasher.update(data); 264 | let hash_bytes = hasher.finalize(); 265 | format!("{:X}", hash_bytes) 266 | } 267 | HashAlgo::Sha256 => { 268 | use sha2::{Digest, Sha256}; 269 | 270 | let mut hasher = Sha256::new(); 271 | hasher.update(data); 272 | let hash_bytes = hasher.finalize(); 273 | format!("{:X}", hash_bytes) 274 | } 275 | HashAlgo::Md5 => { 276 | use md5::{Digest, Md5}; 277 | 278 | let mut hasher = Md5::new(); 279 | hasher.update(data); 280 | let hash_bytes = hasher.finalize(); 281 | format!("{:X}", hash_bytes) 282 | } 283 | } 284 | } 285 | 286 | ///Process a `Vec>` into separated `Vec` and `Vec` 287 | pub fn process_results(results: Vec>) -> (Vec, Vec) { 288 | use itertools::{Either, Itertools}; 289 | let (successes, failures): (Vec<_>, Vec<_>) = results.into_iter().partition_map(|r| match r { 290 | Ok(v) => Either::Left(v), 291 | Err(e) => Either::Right(e), 292 | }); 293 | (successes, failures) 294 | } 295 | 296 | #[allow(dead_code)] 297 | pub fn process_results_ok(results: Vec>) -> Vec { 298 | results 299 | .into_iter() 300 | .filter_map(|res: Result| res.ok()) 301 | .collect() 302 | } 303 | -------------------------------------------------------------------------------- /mcmeta/src/upstream/mojang/mod.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use libmcmeta::models::mojang::{ 4 | ExperimentEntry, ExperimentIndex, MinecraftVersion, MojangVersionManifest, 5 | MojangVersionManifestVersion, OldSnapshotEntry, OldSnapshotIndex, VersionDownload, 6 | VersionDownloads, 7 | }; 8 | use serde::Deserialize; 9 | use serde_valid::Validate; 10 | use tempdir::TempDir; 11 | use tracing::{debug, error, info, info_span, instrument, warn}; 12 | use tracing_indicatif::span_ext::IndicatifSpanExt; 13 | 14 | use eyre::{Context, Result}; 15 | 16 | use crate::{ 17 | app_config::UpstreamConfig, 18 | errors::MetadataError, 19 | storage::{ResourcePath, Storage, StorageImpl}, 20 | upstream::download_binary_file, 21 | utils::deserialize_json_with_error_path, 22 | }; 23 | 24 | fn default_download_url() -> String { 25 | "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json".to_string() 26 | } 27 | 28 | #[derive(Deserialize, Debug, Clone)] 29 | pub struct DownloadConfig { 30 | #[serde(default = "default_download_url")] 31 | pub manifest_url: String, 32 | } 33 | 34 | impl Default for DownloadConfig { 35 | fn default() -> Self { 36 | DownloadConfig { 37 | manifest_url: default_download_url(), 38 | } 39 | } 40 | } 41 | 42 | #[derive(Clone)] 43 | pub struct MojangUpdater { 44 | storage: Arc, 45 | config: Arc, 46 | client: Arc, 47 | } 48 | 49 | impl MojangUpdater { 50 | pub fn new(storage: Arc, config: UpstreamConfig) -> Self { 51 | MojangUpdater { 52 | storage, 53 | config: Arc::new(config), 54 | client: Arc::new(reqwest::Client::new()), 55 | } 56 | } 57 | 58 | #[instrument(skip(self))] 59 | pub async fn update(&self) -> Result<()> { 60 | info!("Checking for Mojang metadata"); 61 | 62 | self.update_mojang_metadata() 63 | .await 64 | .wrap_err_with(|| "Failed to update Mojang metadata.")?; 65 | self.update_mojang_static_metadata() 66 | .await 67 | .wrap_err_with(|| "Failed to update Mojang static metadata.")?; 68 | Ok(()) 69 | } 70 | 71 | #[instrument(skip(self))] 72 | pub async fn download_manifest(&self) -> Result { 73 | let config = &self.config.download.mojang; 74 | 75 | info!( 76 | "Fetching minecraft client manifest from {:#?}", 77 | &config.manifest_url 78 | ); 79 | 80 | let body = self 81 | .client 82 | .get(&config.manifest_url) 83 | .send() 84 | .await? 85 | .error_for_status()? 86 | .text() 87 | .await?; 88 | 89 | let manifest: MojangVersionManifest = deserialize_json_with_error_path(&body) 90 | .wrap_err("Failed to parse Mojang version manifest")?; 91 | manifest.validate()?; 92 | Ok(manifest) 93 | } 94 | 95 | #[instrument(skip(self))] 96 | pub async fn download_version_manifest(&self, version_url: &str) -> Result { 97 | info!( 98 | "Fetching minecraft version manifest from {:#?}", 99 | version_url 100 | ); 101 | 102 | let body = self 103 | .client 104 | .get(version_url) 105 | .send() 106 | .await? 107 | .error_for_status()? 108 | .text() 109 | .await?; 110 | let manifest: MinecraftVersion = 111 | deserialize_json_with_error_path(&body).wrap_err_with(|| { 112 | format!( 113 | "Failed to parse Mojang version manifest from: '{}'", 114 | version_url 115 | ) 116 | })?; 117 | manifest.validate()?; 118 | Ok(manifest) 119 | } 120 | 121 | #[instrument(skip(self))] 122 | pub async fn download_zipped_version(&self, version_url: &str) -> Result { 123 | use std::io::Read; 124 | 125 | info!("Fetching zipped version from {:#?}", version_url); 126 | 127 | let tmp_dir = TempDir::new("mcmeta_mojang_zip")?; 128 | let dest_path = { 129 | let url = reqwest::Url::parse(version_url)?; 130 | let fname = url 131 | .path_segments() 132 | .and_then(|segments| segments.last()) 133 | .and_then(|name| if name.is_empty() { None } else { Some(name) }) 134 | .unwrap_or("tmp.zip"); 135 | 136 | tmp_dir.path().join(fname) 137 | }; 138 | 139 | download_binary_file(&self.client, &dest_path, version_url).await?; 140 | 141 | let file = std::fs::File::open(&dest_path)?; 142 | 143 | let mut archive = zip::ZipArchive::new(file)?; 144 | 145 | let mut manifest: Option = None; 146 | for i in 0..archive.len() { 147 | let mut zfile = archive.by_index(i)?; 148 | if zfile.name().ends_with(".json") { 149 | debug!("Found {} as version json", zfile.name()); 150 | let mut contents = String::new(); 151 | zfile.read_to_string(&mut contents)?; 152 | 153 | manifest = Some(deserialize_json_with_error_path(&contents).wrap_err_with(||{ 154 | format!("Failed to parse zipped Mojang Minecraft version manifest '{}' in archive from '{}'", zfile.name(), version_url) 155 | })?); 156 | } 157 | } 158 | 159 | manifest.ok_or_else(|| { 160 | MetadataError::MissingMojangVersionManifest(version_url.to_string()).into() 161 | }) 162 | } 163 | 164 | pub fn root_path(&self) -> ResourcePath { 165 | ResourcePath::new(&["mojang".to_string()]) 166 | } 167 | 168 | pub fn versions_path(&self) -> ResourcePath { 169 | self.root_path().join("versions") 170 | } 171 | 172 | pub async fn load_manifest(&self) -> Result> { 173 | let record = self 174 | .storage 175 | .fetch_record(self.root_path(), "version_manifest_v2") 176 | .await?; 177 | Ok(record) 178 | } 179 | 180 | pub async fn store_manifest(&self, manifest: &MojangVersionManifest) -> Result<()> { 181 | self.storage 182 | .store_record(self.root_path(), "version_manifest_v2", manifest) 183 | .await?; 184 | Ok(()) 185 | } 186 | 187 | pub async fn load_minecraft_version(&self, id: &str) -> Result> { 188 | self.storage 189 | .fetch_record(self.versions_path(), id) 190 | .await 191 | .wrap_err_with(|| "Failed to load Minecraft Version") 192 | } 193 | 194 | pub async fn store_minecraft_version(&self, version: &MinecraftVersion) -> Result<()> { 195 | self.storage 196 | .store_record(self.versions_path(), &version.id, &version) 197 | .await 198 | .wrap_err_with(|| "Failed to store Minecraft Version") 199 | } 200 | 201 | #[instrument(skip(self))] 202 | pub async fn update_mojang_metadata(&self) -> Result<()> { 203 | use std::collections::{HashMap, HashSet}; 204 | 205 | info!("Acquiring remote Mojang metadata"); 206 | let remote_manifest = self.download_manifest().await?; 207 | let remote_versions: HashMap = HashMap::from_iter( 208 | remote_manifest 209 | .versions 210 | .iter() 211 | .map(|v| (v.id.clone(), v.clone())), 212 | ); 213 | let remote_ids = 214 | HashSet::::from_iter(remote_manifest.versions.iter().map(|v| v.id.clone())); 215 | 216 | let local_manifest = self.load_manifest().await?; 217 | let pending_ids: Vec<(String, bool)> = if let Some(local_manifest) = local_manifest { 218 | let local_versions: HashMap = HashMap::from_iter( 219 | local_manifest 220 | .versions 221 | .iter() 222 | .map(|v| (v.id.clone(), v.clone())), 223 | ); 224 | let local_ids = 225 | HashSet::::from_iter(local_manifest.versions.iter().map(|v| v.id.clone())); 226 | 227 | let mut diff: Vec<(String, bool)> = remote_ids 228 | .difference(&local_ids) 229 | .cloned() 230 | .map(|id| (id, false)) 231 | .collect(); 232 | let mut out_of_date: Vec<(String, bool)> = local_ids 233 | .iter() 234 | .filter_map(|id| { 235 | let remote_version = if let Some(rv) = remote_versions.get(id) { 236 | rv 237 | } else { 238 | warn!("Mojang version {} does not exist remotely", id); 239 | return None; 240 | }; 241 | 242 | let local_version = local_versions 243 | .get(id) 244 | .expect("local version to exist locally"); 245 | if remote_version.time > local_version.time 246 | || remote_version.sha1 != local_version.sha1 247 | { 248 | Some((id.clone(), true)) 249 | } else { 250 | None 251 | } 252 | }) 253 | .collect::>(); 254 | diff.append(&mut out_of_date); 255 | diff 256 | } else { 257 | info!("Local Mojang metadata does not exist, fetching all versions"); 258 | 259 | remote_ids.into_iter().map(|id| (id, true)).collect() 260 | }; 261 | 262 | use futures::StreamExt; 263 | 264 | let task_span = info_span!("download_mojang_versions"); 265 | task_span.pb_set_style(&super::progress_bar()); 266 | task_span.pb_set_message("Updating Mojang Versions ... "); 267 | task_span.pb_set_length(pending_ids.len().try_into().unwrap_or_default()); 268 | 269 | let task_span_entert = task_span.enter(); 270 | 271 | let results = { 272 | futures::stream::iter(pending_ids) 273 | .map(|(version, force_update)| { 274 | let ls = self.clone(); 275 | let v = remote_versions 276 | .get(&version) 277 | .expect("version to exist remotely") 278 | .clone(); 279 | let spn = task_span.clone(); 280 | tokio::spawn(async move { 281 | let res = ls 282 | .update_mojang_version_manifest(&v, force_update) 283 | .await 284 | .wrap_err_with(|| { 285 | format!("Failed to initialize Mojang version {}", v.id) 286 | }); 287 | spn.pb_inc(1); 288 | res 289 | }) 290 | }) 291 | .buffer_unordered(self.config.max_parallel_fetch_connections) 292 | .map(|t| match t { 293 | Ok(Ok(t)) => Ok(t), 294 | Ok(Err(e)) => Err(e).wrap_err("Task had an error"), 295 | Err(e) => Err::<(), eyre::Report>(e.into()).wrap_err("Task had a Join error"), 296 | }) 297 | .collect::>() 298 | .await 299 | }; 300 | 301 | std::mem::drop(task_span_entert); 302 | std::mem::drop(task_span); 303 | 304 | let (_, failures) = crate::utils::process_results(results); 305 | if !failures.is_empty() { 306 | Err(MetadataError::BulkProcessingError( 307 | "Updating Mojang Metadata".to_string(), 308 | failures, 309 | ) 310 | .into()) 311 | } else { 312 | // update the locally stored manifest 313 | self.store_manifest(&remote_manifest).await?; 314 | Ok(()) 315 | } 316 | } 317 | 318 | #[instrument(skip(self, version), fields(version = &version.id))] 319 | async fn update_mojang_version_manifest( 320 | &self, 321 | version: &MojangVersionManifestVersion, 322 | force_update: bool, 323 | ) -> Result<()> { 324 | let local_manifest = self.load_minecraft_version(&version.id).await?; 325 | if local_manifest.is_none() || force_update { 326 | info!( 327 | "Updating Mojang metadata for version {} to timestamp {}", 328 | &version.id, &version.time 329 | ); 330 | let version_manifest = self 331 | .download_version_manifest(&version.url) 332 | .await 333 | .inspect_err(|err| { 334 | warn!( 335 | "Error parsing manifest for version {}: {}", 336 | &version.id, err 337 | ) 338 | })?; 339 | self.store_minecraft_version(&version_manifest).await?; 340 | } 341 | Ok(()) 342 | } 343 | 344 | #[instrument(skip(self))] 345 | pub async fn update_mojang_static_metadata(&self) -> Result<()> { 346 | let static_dir = std::path::Path::new(&self.config.static_directory); 347 | 348 | let static_experiments_path = static_dir.join("mojang").join("minecraft-experiments.json"); 349 | if static_experiments_path.is_file() { 350 | let experiments = crate::utils::deserialize_json_with_error_path::( 351 | &std::fs::read_to_string(&static_experiments_path)?, 352 | )?; 353 | 354 | let task_span = info_span!("download_mojang_expriments"); 355 | task_span.pb_set_style(&super::progress_bar()); 356 | task_span.pb_set_message("Updating Mojang Experiments"); 357 | task_span.pb_set_length(experiments.experiments.len().try_into().unwrap_or_default()); 358 | 359 | let task_span_enter = task_span.enter(); 360 | 361 | use futures::StreamExt; 362 | let tasks = futures::stream::iter(experiments.experiments) 363 | .map(|experiment| { 364 | let ls = self.clone(); 365 | let e = experiment; 366 | let spn = task_span.clone(); 367 | 368 | tokio::spawn(async move { 369 | let res = ls.update_mojang_experiment(&e).await.wrap_err_with(|| { 370 | format!("Failed to initialize Mojang experiment {}", e.id) 371 | }); 372 | spn.pb_inc(1); 373 | res 374 | }) 375 | }) 376 | .buffer_unordered(self.config.max_parallel_fetch_connections); 377 | let results: Vec> = tasks 378 | .map(|t| -> Result<()> { 379 | match t { 380 | Ok(Ok(_)) => Ok(()), 381 | Ok(Err(e)) => Err(e).wrap_err("Task had an error"), 382 | Err(e) => { 383 | Err::<(), eyre::Report>(e.into()).wrap_err("Task had a Join error") 384 | } 385 | } 386 | }) 387 | .collect() 388 | .await; 389 | 390 | std::mem::drop(task_span_enter); 391 | std::mem::drop(task_span); 392 | 393 | let (_, errs) = crate::utils::process_results::<(), eyre::Report>(results); 394 | if !errs.is_empty() { 395 | return Err(MetadataError::BulkProcessingError( 396 | "Updating Mojang Experiment Metadata".to_string(), 397 | errs, 398 | ) 399 | .into()); 400 | } 401 | } 402 | 403 | let static_old_snapshots_path = static_dir 404 | .join("mojang") 405 | .join("minecraft-old-snapshots.json"); 406 | if static_old_snapshots_path.is_file() { 407 | let old_snapshots = crate::utils::deserialize_json_with_error_path::( 408 | &std::fs::read_to_string(&static_old_snapshots_path)?, 409 | )?; 410 | 411 | let task_span = info_span!("download_mojang_old_snapshots"); 412 | task_span.pb_set_style(&super::progress_bar()); 413 | task_span.pb_set_message("Updating Mojang Old Snapshots"); 414 | task_span.pb_set_length( 415 | old_snapshots 416 | .old_snapshots 417 | .len() 418 | .try_into() 419 | .unwrap_or_default(), 420 | ); 421 | 422 | let task_span_enter = task_span.enter(); 423 | use futures::StreamExt; 424 | let tasks = futures::stream::iter(old_snapshots.old_snapshots) 425 | .map(|snapshot| { 426 | let ls = self.clone(); 427 | let s = snapshot; 428 | let spn = task_span.clone(); 429 | tokio::spawn(async move { 430 | let res = ls.update_mojang_old_snapshot(&s).await.wrap_err_with(|| { 431 | format!("Failed to initialize Mojang experiment {}", s.id) 432 | }); 433 | spn.pb_inc(1); 434 | res 435 | }) 436 | }) 437 | .buffer_unordered(self.config.max_parallel_fetch_connections); 438 | let results = tasks 439 | .map(|t| match t { 440 | Ok(Ok(t)) => Ok(t), 441 | Ok(Err(e)) => { 442 | error!("Task had an error: {:?}", e); 443 | Err(e) 444 | } 445 | Err(e) => { 446 | error!("Task had a Join error: {:?}", e); 447 | Err(e.into()) 448 | } 449 | }) 450 | .collect::>() 451 | .await; 452 | 453 | std::mem::drop(task_span_enter); 454 | std::mem::drop(task_span); 455 | 456 | let (_, errs) = crate::utils::process_results::<(), eyre::Report>(results); 457 | if !errs.is_empty() { 458 | return Err(MetadataError::BulkProcessingError( 459 | "Updating Mojang Old Snapshot Metadata".to_string(), 460 | errs, 461 | ) 462 | .into()); 463 | } 464 | } 465 | Ok(()) 466 | } 467 | 468 | #[instrument(skip(self, version), fields(version = &version.id))] 469 | async fn update_mojang_experiment(&self, version: &ExperimentEntry) -> Result<()> { 470 | let local_version = self.load_minecraft_version(&version.id).await?; 471 | if local_version.is_none() { 472 | info!( 473 | "Mojang metadata for experiment {} does not exist, downloading it", 474 | &version.id 475 | ); 476 | let version_manifest = self 477 | .download_zipped_version(&version.url) 478 | .await 479 | .inspect_err(|err| { 480 | warn!( 481 | "Error parsing manifest for version {}: {}", 482 | &version.id, err 483 | ); 484 | })?; 485 | self.store_minecraft_version(&version_manifest).await?; 486 | } 487 | Ok(()) 488 | } 489 | 490 | #[instrument(skip(self, snapshot), fields(snapshot = &snapshot.id))] 491 | async fn update_mojang_old_snapshot(&self, snapshot: &OldSnapshotEntry) -> Result<()> { 492 | let local_version = self.load_minecraft_version(&snapshot.id).await?; 493 | if local_version.is_none() { 494 | info!( 495 | "Mojang metadata for old snapshot {} does not exist, downloading it", 496 | &snapshot.id 497 | ); 498 | 499 | let mut version_manifest = self 500 | .download_version_manifest(&snapshot.url) 501 | .await 502 | .inspect_err(|err| { 503 | warn!( 504 | "Error parsing manifest for version {}: {}", 505 | &snapshot.id, err 506 | ); 507 | })?; 508 | 509 | version_manifest.release_time = 510 | version_manifest.release_time.clone() + "T00:00:00+02:00"; 511 | version_manifest.time = version_manifest.release_time.clone(); 512 | 513 | version_manifest.downloads = Some(VersionDownloads { 514 | client: VersionDownload { 515 | url: snapshot.jar.clone(), 516 | sha1: snapshot.sha1.clone(), 517 | size: snapshot.size, 518 | }, 519 | server: None, 520 | windows_server: None, 521 | client_mappings: None, 522 | server_mappings: None, 523 | }); 524 | 525 | version_manifest.release_type = "old_snapshot".to_string(); 526 | 527 | self.store_minecraft_version(&version_manifest).await?; 528 | } 529 | Ok(()) 530 | } 531 | } 532 | -------------------------------------------------------------------------------- /static/mojang/minecraft-legacy-override.json: -------------------------------------------------------------------------------- 1 | { 2 | "versions": { 3 | "1.5.2": { 4 | "releaseTime": "2013-04-25T17:45:00+02:00", 5 | "+traits": ["legacyLaunch", "texturepacks"] 6 | }, 7 | "1.5.1": { 8 | "releaseTime": "2013-03-20T12:00:00+02:00", 9 | "+traits": ["legacyLaunch", "texturepacks"] 10 | }, 11 | "13w12~": { 12 | "+traits": ["legacyLaunch", "texturepacks"] 13 | }, 14 | "1.5": { 15 | "releaseTime": "2013-03-07T00:00:00+02:00", 16 | "+traits": ["legacyLaunch", "texturepacks"] 17 | }, 18 | "13w10b": { 19 | "+traits": ["legacyLaunch", "texturepacks"] 20 | }, 21 | "13w10a": { 22 | "+traits": ["legacyLaunch", "texturepacks"] 23 | }, 24 | "13w09c": { 25 | "+traits": ["legacyLaunch", "texturepacks"] 26 | }, 27 | "13w09b": { 28 | "+traits": ["legacyLaunch", "texturepacks"] 29 | }, 30 | "13w09a": { 31 | "+traits": ["legacyLaunch", "texturepacks"] 32 | }, 33 | "13w11a": { 34 | "+traits": ["legacyLaunch", "texturepacks"] 35 | }, 36 | "13w07a": { 37 | "+traits": ["legacyLaunch", "texturepacks"] 38 | }, 39 | "13w06a": { 40 | "+traits": ["legacyLaunch", "texturepacks"] 41 | }, 42 | "13w05b": { 43 | "+traits": ["legacyLaunch", "texturepacks"] 44 | }, 45 | "13w05a": { 46 | "+traits": ["legacyLaunch", "texturepacks"] 47 | }, 48 | "13w04a": { 49 | "+traits": ["legacyLaunch", "texturepacks"] 50 | }, 51 | "13w03a": { 52 | "+traits": ["legacyLaunch", "texturepacks"] 53 | }, 54 | "13w02b": { 55 | "+traits": ["legacyLaunch", "texturepacks"] 56 | }, 57 | "13w02a": { 58 | "+traits": ["legacyLaunch", "texturepacks"] 59 | }, 60 | "13w01b": { 61 | "+traits": ["legacyLaunch", "texturepacks"] 62 | }, 63 | "13w01a": { 64 | "+traits": ["legacyLaunch", "texturepacks"] 65 | }, 66 | "1.4.7": { 67 | "releaseTime": "2012-12-28T00:00:00+02:00", 68 | "+traits": ["legacyLaunch", "texturepacks"] 69 | }, 70 | "1.4.6": { 71 | "releaseTime": "2012-12-20T00:00:00+02:00", 72 | "+traits": ["legacyLaunch", "texturepacks"] 73 | }, 74 | "12w50b": { 75 | "+traits": ["legacyLaunch", "texturepacks"] 76 | }, 77 | "12w50a": { 78 | "+traits": ["legacyLaunch", "texturepacks"] 79 | }, 80 | "12w49a": { 81 | "+traits": ["legacyLaunch", "texturepacks"] 82 | }, 83 | "1.4.5": { 84 | "releaseTime": "2012-11-20T00:00:00+02:00", 85 | "+traits": ["legacyLaunch", "texturepacks"] 86 | }, 87 | "1.4.4": { 88 | "releaseTime": "2012-11-14T00:00:00+02:00", 89 | "+traits": ["legacyLaunch", "texturepacks"] 90 | }, 91 | "1.4.3": { 92 | "releaseTime": "2012-11-01T00:00:00+02:00", 93 | "+traits": ["legacyLaunch", "texturepacks"] 94 | }, 95 | "1.4.2": { 96 | "releaseTime": "2012-10-25T00:00:00+02:00", 97 | "+traits": ["legacyLaunch", "texturepacks"] 98 | }, 99 | "1.4.1": { 100 | "releaseTime": "2012-10-23T00:00:00+02:00", 101 | "+traits": ["legacyLaunch", "texturepacks"] 102 | }, 103 | "1.4": { 104 | "releaseTime": "2012-10-19T00:00:00+02:00", 105 | "+traits": ["legacyLaunch", "texturepacks"] 106 | }, 107 | "12w42b": { 108 | "+traits": ["legacyLaunch", "texturepacks"] 109 | }, 110 | "12w42a": { 111 | "+traits": ["legacyLaunch", "texturepacks"] 112 | }, 113 | "12w41b": { 114 | "+traits": ["legacyLaunch", "texturepacks"] 115 | }, 116 | "12w41a": { 117 | "+traits": ["legacyLaunch", "texturepacks"] 118 | }, 119 | "12w40b": { 120 | "+traits": ["legacyLaunch", "texturepacks"] 121 | }, 122 | "12w40a": { 123 | "+traits": ["legacyLaunch", "texturepacks"] 124 | }, 125 | "12w39b": { 126 | "+traits": ["legacyLaunch", "texturepacks"] 127 | }, 128 | "12w39a": { 129 | "+traits": ["legacyLaunch", "texturepacks"] 130 | }, 131 | "12w38b": { 132 | "+traits": ["legacyLaunch", "texturepacks"] 133 | }, 134 | "12w38a": { 135 | "+traits": ["legacyLaunch", "texturepacks"] 136 | }, 137 | "12w37a": { 138 | "+traits": ["legacyLaunch", "texturepacks"] 139 | }, 140 | "12w36a": { 141 | "+traits": ["legacyLaunch", "texturepacks"] 142 | }, 143 | "12w34b": { 144 | "+traits": ["legacyLaunch", "texturepacks"] 145 | }, 146 | "12w34a": { 147 | "+traits": ["legacyLaunch", "texturepacks"] 148 | }, 149 | "1.3.2": { 150 | "releaseTime": "2012-08-16T00:00:00+02:00", 151 | "+traits": ["legacyLaunch", "texturepacks"] 152 | }, 153 | "12w32a": { 154 | "+traits": ["legacyLaunch", "texturepacks"] 155 | }, 156 | "1.3.1": { 157 | "releaseTime": "2012-08-01T00:00:00+02:00", 158 | "+traits": ["legacyLaunch", "texturepacks"] 159 | }, 160 | "1.3": { 161 | "releaseTime": "2012-07-26T00:00:00+02:00", 162 | "+traits": ["legacyLaunch", "texturepacks"] 163 | }, 164 | "12w30e": { 165 | "+traits": ["legacyLaunch", "texturepacks"] 166 | }, 167 | "12w30d": { 168 | "+traits": ["legacyLaunch", "texturepacks"] 169 | }, 170 | "12w30c": { 171 | "+traits": ["legacyLaunch", "texturepacks"] 172 | }, 173 | "12w30b": { 174 | "+traits": ["legacyLaunch", "texturepacks"] 175 | }, 176 | "12w30a": { 177 | "+traits": ["legacyLaunch", "texturepacks"] 178 | }, 179 | "12w27a": { 180 | "+traits": ["legacyLaunch", "texturepacks"] 181 | }, 182 | "12w26a": { 183 | "+traits": ["legacyLaunch", "texturepacks"] 184 | }, 185 | "12w25a": { 186 | "+traits": ["legacyLaunch", "texturepacks"] 187 | }, 188 | "12w24a": { 189 | "+traits": ["legacyLaunch", "texturepacks"] 190 | }, 191 | "12w23b": { 192 | "+traits": ["legacyLaunch", "texturepacks"] 193 | }, 194 | "12w23a": { 195 | "+traits": ["legacyLaunch", "texturepacks"] 196 | }, 197 | "12w22a": { 198 | "+traits": ["legacyLaunch", "texturepacks"] 199 | }, 200 | "12w21b": { 201 | "+traits": ["legacyLaunch", "texturepacks"] 202 | }, 203 | "12w21a": { 204 | "+traits": ["legacyLaunch", "texturepacks"] 205 | }, 206 | "12w19a": { 207 | "+traits": ["legacyLaunch", "texturepacks"] 208 | }, 209 | "12w18a": { 210 | "+traits": ["legacyLaunch", "texturepacks"] 211 | }, 212 | "12w17a": { 213 | "+traits": ["legacyLaunch", "texturepacks"] 214 | }, 215 | "12w16a": { 216 | "+traits": ["legacyLaunch", "texturepacks"] 217 | }, 218 | "1.2.5": { 219 | "releaseTime": "2012-03-30T00:00:00+02:00", 220 | "+traits": ["legacyLaunch", "texturepacks"] 221 | }, 222 | "1.2.4": { 223 | "releaseTime": "2012-03-22T00:00:00+02:00", 224 | "+traits": ["legacyLaunch", "texturepacks"] 225 | }, 226 | "1.2.3": { 227 | "releaseTime": "2012-03-02T00:00:00+02:00", 228 | "+traits": ["legacyLaunch", "texturepacks"] 229 | }, 230 | "1.2.2": { 231 | "releaseTime": "2012-03-01T00:00:01+02:00", 232 | "+traits": ["legacyLaunch", "texturepacks"] 233 | }, 234 | "1.2.1": { 235 | "releaseTime": "2012-03-01T00:00:00+02:00", 236 | "+traits": ["legacyLaunch", "texturepacks"] 237 | }, 238 | "1.2": { 239 | "+traits": ["legacyLaunch", "texturepacks"] 240 | }, 241 | "12w08a": { 242 | "+traits": ["legacyLaunch", "texturepacks"] 243 | }, 244 | "12w07a": { 245 | "+traits": ["legacyLaunch", "texturepacks"] 246 | }, 247 | "12w07b": { 248 | "+traits": ["legacyLaunch", "texturepacks"] 249 | }, 250 | "12w06a": { 251 | "+traits": ["legacyLaunch", "texturepacks"] 252 | }, 253 | "12w05b": { 254 | "+traits": ["legacyLaunch", "texturepacks"] 255 | }, 256 | "12w05a": { 257 | "+traits": ["legacyLaunch", "texturepacks"] 258 | }, 259 | "12w04a": { 260 | "+traits": ["legacyLaunch", "texturepacks"] 261 | }, 262 | "12w03a": { 263 | "+traits": ["legacyLaunch", "texturepacks"] 264 | }, 265 | "1.1": { 266 | "releaseTime": "2012-01-12T00:00:00+02:00", 267 | "+traits": ["legacyLaunch", "texturepacks"] 268 | }, 269 | "12w01a": { 270 | "+traits": ["legacyLaunch", "texturepacks"] 271 | }, 272 | "11w50a": { 273 | "+traits": ["legacyLaunch", "texturepacks"] 274 | }, 275 | "11w49a": { 276 | "+traits": ["legacyLaunch", "texturepacks"] 277 | }, 278 | "11w48a": { 279 | "+traits": ["legacyLaunch", "texturepacks"] 280 | }, 281 | "11w47a": { 282 | "+traits": ["legacyLaunch", "texturepacks"] 283 | }, 284 | "1.0": { 285 | "releaseTime": "2011-11-18T00:00:00+02:00", 286 | "+traits": ["legacyLaunch", "texturepacks"] 287 | }, 288 | "b1.9-pre6": { 289 | "+traits": ["legacyLaunch", "texturepacks"] 290 | }, 291 | "b1.9-pre5": { 292 | "+traits": ["legacyLaunch", "texturepacks"] 293 | }, 294 | "b1.9-pre4": { 295 | "+traits": ["legacyLaunch", "texturepacks"] 296 | }, 297 | "b1.9-pre3": { 298 | "+traits": ["legacyLaunch", "texturepacks"] 299 | }, 300 | "b1.9-pre2": { 301 | "+traits": ["legacyLaunch", "texturepacks"] 302 | }, 303 | "b1.9-pre1": { 304 | "+traits": ["legacyLaunch", "texturepacks"] 305 | }, 306 | "b1.8.1": { 307 | "releaseTime": "2011-09-19T00:00:00+02:00", 308 | "+traits": ["legacyLaunch", "texturepacks"] 309 | }, 310 | "b1.8": { 311 | "releaseTime": "2011-09-15T00:00:00+02:00", 312 | "+traits": ["legacyLaunch", "texturepacks"] 313 | }, 314 | "b1.8-pre2": { 315 | "+traits": ["legacyLaunch", "texturepacks"] 316 | }, 317 | "b1.8-pre1-2": { 318 | "+traits": ["legacyLaunch", "texturepacks"] 319 | }, 320 | "b1.7.3": { 321 | "releaseTime": "2011-07-08T00:00:00+02:00", 322 | "+traits": ["legacyLaunch", "texturepacks"] 323 | }, 324 | "b1.7.2": { 325 | "releaseTime": "2011-07-01T00:00:00+02:00", 326 | "+traits": ["legacyLaunch", "texturepacks"] 327 | }, 328 | "b1.7": { 329 | "releaseTime": "2011-06-30T00:00:00+02:00", 330 | "+traits": ["legacyLaunch", "texturepacks"] 331 | }, 332 | "b1.6.6": { 333 | "releaseTime": "2011-05-31T00:00:00+02:00", 334 | "+traits": ["legacyLaunch", "texturepacks"] 335 | }, 336 | "b1.6.5": { 337 | "releaseTime": "2011-05-28T00:00:00+02:00", 338 | "+traits": ["legacyLaunch", "texturepacks"] 339 | }, 340 | "b1.6.4": { 341 | "releaseTime": "2011-05-26T00:00:04+02:00", 342 | "+traits": ["legacyLaunch", "texturepacks"] 343 | }, 344 | "b1.6.3": { 345 | "releaseTime": "2011-05-26T00:00:03+02:00", 346 | "+traits": ["legacyLaunch", "texturepacks"] 347 | }, 348 | "b1.6.2": { 349 | "releaseTime": "2011-05-26T00:00:02+02:00", 350 | "+traits": ["legacyLaunch", "texturepacks"] 351 | }, 352 | "b1.6.1": { 353 | "releaseTime": "2011-05-26T00:00:01+02:00", 354 | "+traits": ["legacyLaunch", "texturepacks"] 355 | }, 356 | "b1.6": { 357 | "releaseTime": "2011-05-26T00:00:00+02:00", 358 | "+traits": ["legacyLaunch", "texturepacks"] 359 | }, 360 | "b1.5_01": { 361 | "releaseTime": "2011-04-20T00:00:00+02:00", 362 | "+traits": ["legacyLaunch", "texturepacks"] 363 | }, 364 | "b1.5": { 365 | "releaseTime": "2011-04-19T00:00:00+02:00", 366 | "+traits": ["legacyLaunch", "texturepacks"] 367 | }, 368 | "b1.4_01": { 369 | "releaseTime": "2011-04-05T00:00:00+02:00", 370 | "+traits": ["legacyLaunch", "texturepacks"] 371 | }, 372 | "b1.4": { 373 | "releaseTime": "2011-03-31T00:00:00+02:00", 374 | "+traits": ["legacyLaunch", "texturepacks"] 375 | }, 376 | "b1.3_01": { 377 | "releaseTime": "2011-02-23T00:00:00+02:00", 378 | "+traits": ["legacyLaunch", "texturepacks"] 379 | }, 380 | "b1.3b": { 381 | "releaseTime": "2011-02-22T00:00:00+02:00", 382 | "+traits": ["legacyLaunch", "texturepacks"] 383 | }, 384 | "b1.2_02": { 385 | "releaseTime": "2011-01-21T00:00:00+02:00", 386 | "+traits": ["legacyLaunch", "texturepacks"] 387 | }, 388 | "b1.2_01": { 389 | "releaseTime": "2011-01-14T00:00:00+02:00", 390 | "+traits": ["legacyLaunch", "texturepacks"] 391 | }, 392 | "b1.2": { 393 | "releaseTime": "2011-01-13T00:00:00+02:00", 394 | "+traits": ["legacyLaunch", "texturepacks"] 395 | }, 396 | "b1.1_02": { 397 | "releaseTime": "2010-12-22T00:00:01+02:00", 398 | "+traits": ["legacyLaunch", "texturepacks"] 399 | }, 400 | "b1.1_01": { 401 | "releaseTime": "2010-12-22T00:00:00+02:00", 402 | "+traits": ["legacyLaunch", "texturepacks"] 403 | }, 404 | "b1.0.2": { 405 | "releaseTime": "2010-12-21T00:00:00+02:00", 406 | "+traits": ["legacyLaunch", "texturepacks"] 407 | }, 408 | "b1.0_01": { 409 | "releaseTime": "2010-12-20T00:00:01+02:00", 410 | "+traits": ["legacyLaunch", "texturepacks"] 411 | }, 412 | "b1.0": { 413 | "releaseTime": "2010-12-20T00:00:00+02:00", 414 | "+traits": ["legacyLaunch", "texturepacks"] 415 | }, 416 | "a1.2.6": { 417 | "releaseTime": "2010-12-03T00:00:00+02:00", 418 | "+traits": ["legacyLaunch", "texturepacks"] 419 | }, 420 | "a1.2.5": { 421 | "releaseTime": "2010-12-01T00:00:00+02:00", 422 | "+traits": ["legacyLaunch", "texturepacks"] 423 | }, 424 | "a1.2.4_01": { 425 | "releaseTime": "2010-11-30T00:00:00+02:00", 426 | "+traits": ["legacyLaunch", "texturepacks"] 427 | }, 428 | "a1.2.3_04": { 429 | "releaseTime": "2010-11-26T00:00:00+02:00", 430 | "+traits": ["legacyLaunch", "texturepacks"] 431 | }, 432 | "a1.2.3_02": { 433 | "releaseTime": "2010-11-25T00:00:00+02:00", 434 | "+traits": ["legacyLaunch", "texturepacks"] 435 | }, 436 | "a1.2.3_01": { 437 | "releaseTime": "2010-11-24T00:00:01+02:00", 438 | "+traits": ["legacyLaunch", "texturepacks"] 439 | }, 440 | "a1.2.3": { 441 | "releaseTime": "2010-11-24T00:00:00+02:00", 442 | "+traits": ["legacyLaunch", "texturepacks"] 443 | }, 444 | "a1.2.2b": { 445 | "releaseTime": "2010-11-10T00:00:01+02:00", 446 | "+traits": ["legacyLaunch", "texturepacks"] 447 | }, 448 | "a1.2.2a": { 449 | "releaseTime": "2010-11-10T00:00:00+02:00", 450 | "+traits": ["legacyLaunch", "texturepacks"] 451 | }, 452 | "a1.2.1_01": { 453 | "releaseTime": "2010-11-05T00:00:01+02:00", 454 | "+traits": ["legacyLaunch", "no-texturepacks"] 455 | }, 456 | "a1.2.1": { 457 | "releaseTime": "2010-11-05T00:00:00+02:00", 458 | "+traits": ["legacyLaunch", "no-texturepacks"] 459 | }, 460 | "a1.2.0_02": { 461 | "releaseTime": "2010-11-04T00:00:00+02:00", 462 | "+traits": ["legacyLaunch", "no-texturepacks"] 463 | }, 464 | "a1.2.0_01": { 465 | "releaseTime": "2010-10-31T00:00:00+02:00", 466 | "+traits": ["legacyLaunch", "no-texturepacks"] 467 | }, 468 | "a1.2.0": { 469 | "releaseTime": "2010-10-30T00:00:00+02:00", 470 | "+traits": ["legacyLaunch", "no-texturepacks"] 471 | }, 472 | "a1.1.2_01": { 473 | "releaseTime": "2010-09-23T00:00:00+02:00", 474 | "+traits": ["legacyLaunch", "no-texturepacks"], 475 | "+jvmArgs": ["-Djava.util.Arrays.useLegacyMergeSort=true"] 476 | }, 477 | "a1.1.2": { 478 | "releaseTime": "2010-09-20T00:00:00+02:00", 479 | "+traits": ["legacyLaunch", "no-texturepacks"], 480 | "+jvmArgs": ["-Djava.util.Arrays.useLegacyMergeSort=true"] 481 | }, 482 | "a1.1.0": { 483 | "releaseTime": "2010-09-13T00:00:00+02:00", 484 | "+traits": ["legacyLaunch", "no-texturepacks"], 485 | "+jvmArgs": ["-Djava.util.Arrays.useLegacyMergeSort=true"] 486 | }, 487 | "a1.0.17_04": { 488 | "releaseTime": "2010-08-23T00:00:00+02:00", 489 | "+traits": ["legacyLaunch", "no-texturepacks"], 490 | "+jvmArgs": ["-Djava.util.Arrays.useLegacyMergeSort=true"] 491 | }, 492 | "a1.0.17_02": { 493 | "releaseTime": "2010-08-20T00:00:00+02:00", 494 | "+traits": ["legacyLaunch", "no-texturepacks"], 495 | "+jvmArgs": ["-Djava.util.Arrays.useLegacyMergeSort=true"] 496 | }, 497 | "a1.0.16": { 498 | "releaseTime": "2010-08-12T00:00:00+02:00", 499 | "+traits": ["legacyLaunch", "no-texturepacks"], 500 | "+jvmArgs": ["-Djava.util.Arrays.useLegacyMergeSort=true"] 501 | }, 502 | "a1.0.15": { 503 | "releaseTime": "2010-08-04T00:00:00+02:00", 504 | "+traits": ["legacyLaunch", "no-texturepacks"], 505 | "+jvmArgs": ["-Djava.util.Arrays.useLegacyMergeSort=true"] 506 | }, 507 | "a1.0.14": { 508 | "releaseTime": "2010-07-30T00:00:00+02:00", 509 | "+traits": ["legacyLaunch", "no-texturepacks"], 510 | "+jvmArgs": ["-Djava.util.Arrays.useLegacyMergeSort=true"] 511 | }, 512 | "a1.0.11": { 513 | "releaseTime": "2010-07-23T00:00:00+02:00", 514 | "+traits": ["legacyLaunch", "no-texturepacks"], 515 | "+jvmArgs": ["-Djava.util.Arrays.useLegacyMergeSort=true"] 516 | }, 517 | "a1.0.5_01": { 518 | "releaseTime": "2010-07-13T00:00:00+02:00", 519 | "mainClass": "y", 520 | "+traits": ["legacyLaunch", "no-texturepacks"], 521 | "+jvmArgs": ["-Djava.util.Arrays.useLegacyMergeSort=true"] 522 | }, 523 | "a1.0.4": { 524 | "releaseTime": "2010-07-09T00:00:00+02:00", 525 | "mainClass": "ax", 526 | "+traits": ["legacyLaunch", "no-texturepacks"], 527 | "+jvmArgs": ["-Djava.util.Arrays.useLegacyMergeSort=true"] 528 | }, 529 | "inf-20100618": { 530 | "releaseTime": "2010-06-16T00:00:00+02:00", 531 | "mainClass": "net.minecraft.client.d", 532 | "appletClass": "net.minecraft.client.MinecraftApplet", 533 | "+traits": ["legacyLaunch", "no-texturepacks"], 534 | "+jvmArgs": ["-Djava.util.Arrays.useLegacyMergeSort=true"] 535 | }, 536 | "c0.30_01c": { 537 | "releaseTime": "2009-12-22T00:00:00+02:00", 538 | "mainClass": "com.mojang.minecraft.l", 539 | "appletClass": "com.mojang.minecraft.MinecraftApplet", 540 | "+traits": ["legacyLaunch", "no-texturepacks"] 541 | }, 542 | "c0.0.13a_03": { 543 | "releaseTime": "2009-05-22T00:00:00+02:00", 544 | "mainClass": "com.mojang.minecraft.c", 545 | "appletClass": "com.mojang.minecraft.MinecraftApplet", 546 | "+traits": ["legacyLaunch", "no-texturepacks"] 547 | }, 548 | "c0.0.13a": { 549 | "releaseTime": "2009-05-31T00:00:00+02:00", 550 | "mainClass": "com.mojang.minecraft.Minecraft", 551 | "appletClass": "com.mojang.minecraft.MinecraftApplet", 552 | "+traits": ["legacyLaunch", "no-texturepacks"] 553 | }, 554 | "c0.0.11a": { 555 | "releaseTime": "2009-05-17T00:00:00+02:00", 556 | "mainClass": "com.mojang.minecraft.Minecraft", 557 | "appletClass": "com.mojang.minecraft.MinecraftApplet", 558 | "+traits": ["legacyLaunch", "no-texturepacks"] 559 | }, 560 | "rd-161348": { 561 | "releaseTime": "2009-05-16T13:48:00+02:00", 562 | "mainClass": "com.mojang.minecraft.RubyDung", 563 | "+traits": ["no-texturepacks"] 564 | }, 565 | "rd-160052": { 566 | "releaseTime": "2009-05-16T00:52:00+02:00", 567 | "mainClass": "com.mojang.rubydung.RubyDung", 568 | "+traits": ["no-texturepacks"] 569 | }, 570 | "rd-20090515": { 571 | "mainClass": "com.mojang.rubydung.RubyDung", 572 | "+traits": ["no-texturepacks"] 573 | }, 574 | "rd-132328": { 575 | "releaseTime": "2009-05-13T23:28:00+02:00", 576 | "mainClass": "com.mojang.rubydung.RubyDung", 577 | "+traits": ["no-texturepacks"] 578 | }, 579 | "rd-132211": { 580 | "releaseTime": "2009-05-13T22:11:00+02:00", 581 | "mainClass": "com.mojang.rubydung.RubyDung", 582 | "+traits": ["no-texturepacks"] 583 | } 584 | } 585 | } 586 | -------------------------------------------------------------------------------- /libmcmeta/src/models/mod.rs: -------------------------------------------------------------------------------- 1 | use core::ops::Deref; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_valid::Validate; 4 | use serde_with::skip_serializing_none; 5 | use std::collections::HashMap; 6 | use std::{fmt::Display, str::FromStr}; 7 | use thiserror::Error; 8 | 9 | pub mod forge; 10 | pub mod mojang; 11 | 12 | #[derive(Error, Debug)] 13 | pub enum ModelError { 14 | #[error("Invalid Gradle specifier '{specifier}'")] 15 | InvalidGradleSpecifier { specifier: String }, 16 | } 17 | 18 | static META_FORMAT_VERSION: i32 = 1; 19 | 20 | /// A Gradle specifier. 21 | #[derive(Debug, PartialEq, Eq, Clone, Default)] 22 | pub struct GradleSpecifier { 23 | /// Group of the artifact. 24 | pub group: String, 25 | /// Artifact name. 26 | pub artifact: String, 27 | /// Version of the artifact. 28 | pub version: String, 29 | /// File extension of the artifact. 30 | pub extension: Option, 31 | /// Classifier of the artifact. 32 | pub classifier: Option, 33 | } 34 | 35 | impl GradleSpecifier { 36 | /// Returns the file name of the artifact. 37 | pub fn filename(&self) -> String { 38 | if let Some(classifier) = &self.classifier { 39 | format!( 40 | "{}-{}-{}.{}", 41 | self.artifact, 42 | self.version, 43 | classifier, 44 | self.extension.as_ref().unwrap_or(&"".to_string()) 45 | ) 46 | } else { 47 | format!( 48 | "{}-{}.{}", 49 | self.artifact, 50 | self.version, 51 | self.extension.as_ref().unwrap_or(&"".to_string()) 52 | ) 53 | } 54 | } 55 | 56 | /// Returns the base path of the artifact. 57 | pub fn base(&self) -> String { 58 | format!( 59 | "{}/{}/{}", 60 | self.group.replace('.', "/"), 61 | self.artifact, 62 | self.version 63 | ) 64 | } 65 | 66 | /// Returns the full path of the artifact. 67 | pub fn path(&self) -> String { 68 | format!("{}/{}", self.base(), self.filename()) 69 | } 70 | 71 | /// Returns `true` if the specifier is a LWJGL artifact. 72 | pub fn is_lwjgl(&self) -> bool { 73 | [ 74 | "org.lwjgl", 75 | "org.lwjgl.lwjgl", 76 | "net.java.jinput", 77 | "net.java.jutils", 78 | ] 79 | .contains(&self.group.as_str()) 80 | } 81 | 82 | /// Returns `true` if the specifier is a Log4j artifact. 83 | pub fn is_log4j(&self) -> bool { 84 | ["org.apache.logging.log4j"].contains(&self.group.as_str()) 85 | } 86 | } 87 | 88 | impl FromStr for GradleSpecifier { 89 | type Err = ModelError; 90 | 91 | fn from_str(s: &str) -> Result { 92 | let at_split = s.split('@').collect::>(); 93 | 94 | let components = at_split 95 | .first() 96 | .ok_or(ModelError::InvalidGradleSpecifier { 97 | specifier: s.to_string(), 98 | })? 99 | .split(':') 100 | .collect::>(); 101 | 102 | let group = components 103 | .first() 104 | .ok_or(ModelError::InvalidGradleSpecifier { 105 | specifier: s.to_string(), 106 | })? 107 | .to_string(); 108 | let artifact = components 109 | .get(1) 110 | .ok_or(ModelError::InvalidGradleSpecifier { 111 | specifier: s.to_string(), 112 | })? 113 | .to_string(); 114 | let version = components 115 | .get(2) 116 | .ok_or(ModelError::InvalidGradleSpecifier { 117 | specifier: s.to_string(), 118 | })? 119 | .to_string(); 120 | 121 | let mut extension = Some("jar".to_string()); 122 | if at_split.len() == 2 { 123 | extension = Some(at_split[1].to_string()); 124 | } 125 | 126 | let classifier = if components.len() == 4 { 127 | Some( 128 | components 129 | .get(3) 130 | .ok_or(ModelError::InvalidGradleSpecifier { 131 | specifier: s.to_string(), 132 | })? 133 | .to_string(), 134 | ) 135 | } else { 136 | None 137 | }; 138 | 139 | Ok(GradleSpecifier { 140 | group, 141 | artifact, 142 | version, 143 | extension, 144 | classifier, 145 | }) 146 | } 147 | } 148 | 149 | impl Display for GradleSpecifier { 150 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 151 | let extension = if let Some(ext) = &self.extension { 152 | if ext != "jar" { 153 | format!("@{}", ext) 154 | } else { 155 | String::new() 156 | } 157 | } else { 158 | String::new() 159 | }; 160 | 161 | if let Some(classifier) = self.classifier.as_ref() { 162 | write!( 163 | f, 164 | "{}:{}:{}:{}{}", 165 | self.group, self.artifact, self.version, classifier, extension 166 | ) 167 | } else { 168 | write!( 169 | f, 170 | "{}:{}:{}{}", 171 | self.group, self.artifact, self.version, extension 172 | ) 173 | } 174 | } 175 | } 176 | 177 | impl Serialize for GradleSpecifier { 178 | fn serialize(&self, serializer: S) -> Result 179 | where 180 | S: serde::Serializer, 181 | { 182 | serializer.serialize_str(&self.to_string()) 183 | } 184 | } 185 | 186 | impl<'de> Deserialize<'de> for GradleSpecifier { 187 | fn deserialize(deserializer: D) -> Result 188 | where 189 | D: serde::Deserializer<'de>, 190 | { 191 | let s = String::deserialize(deserializer)?; 192 | s.parse().map_err(serde::de::Error::custom) 193 | } 194 | } 195 | 196 | #[derive(Deserialize, Serialize, Debug, Clone, Validate, merge::Merge)] 197 | #[serde(rename_all = "camelCase")] 198 | pub struct MojangArtifactBase { 199 | #[merge(strategy = merge::option::overwrite_some)] 200 | pub sha1: Option, 201 | #[merge(strategy = merge::option::overwrite_some)] 202 | pub size: Option, 203 | #[merge(strategy = merge::overwrite)] 204 | pub url: String, 205 | #[serde(flatten)] 206 | #[merge(strategy = merge::hashmap::overwrite_key)] 207 | pub unknown: HashMap, 208 | } 209 | 210 | #[derive(Deserialize, Serialize, Debug, Clone, Validate, merge::Merge)] 211 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 212 | pub struct MojangAssets { 213 | #[merge(strategy = merge::option::overwrite_some)] 214 | pub sha1: Option, 215 | #[merge(strategy = merge::option::overwrite_some)] 216 | pub size: Option, 217 | #[merge(strategy = merge::overwrite)] 218 | pub url: String, 219 | #[merge(strategy = merge::overwrite)] 220 | pub id: String, 221 | #[merge(strategy = merge::overwrite)] 222 | pub total_size: i32, 223 | } 224 | 225 | #[derive(Deserialize, Serialize, Debug, Clone, Validate, merge::Merge)] 226 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 227 | pub struct MojangArtifact { 228 | #[merge(strategy = merge::option::overwrite_some)] 229 | pub sha1: Option, 230 | #[merge(strategy = merge::option::overwrite_some)] 231 | pub size: Option, 232 | #[merge(strategy = merge::overwrite)] 233 | pub url: String, 234 | #[merge(strategy = merge::option::overwrite_some)] 235 | pub path: Option, 236 | } 237 | 238 | /// ```json 239 | /// "rules": [ 240 | /// { 241 | /// "action": "allow" 242 | /// }, 243 | /// { 244 | /// "action": "disallow", 245 | /// "os": { 246 | /// "name": "osx" 247 | /// } 248 | /// } 249 | /// ] 250 | /// ``` 251 | #[derive(Deserialize, Serialize, Debug, Clone, Validate, merge::Merge)] 252 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 253 | pub struct MojangLibraryExtractRules { 254 | #[merge(strategy = merge::vec::append)] 255 | pub exclude: Vec, // TODO maybe drop this completely? 256 | } 257 | 258 | #[derive(Deserialize, Serialize, Debug, Clone, Validate, merge::Merge)] 259 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 260 | pub struct MojangLibraryDownloads { 261 | #[merge(strategy = merge::option::overwrite_some)] 262 | pub artifact: Option, 263 | #[merge(strategy = merge::option_hashmap::recurse_some)] 264 | pub classifiers: Option>, 265 | } 266 | 267 | #[derive(Deserialize, Serialize, Debug, Clone, Validate, merge::Merge)] 268 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 269 | pub struct OSRule { 270 | #[validate(custom(os_rule_name_must_be_os))] 271 | #[merge(strategy = merge::overwrite)] 272 | pub name: String, 273 | #[merge(strategy = merge::option::overwrite_some)] 274 | pub version: Option, 275 | } 276 | 277 | fn os_rule_name_must_be_os(name: &String) -> Result<(), serde_valid::validation::Error> { 278 | let valid_os_names = [ 279 | "osx", 280 | "linux", 281 | "windows", 282 | "windows-arm64", 283 | "osx-arm64", 284 | "linux-arm64", 285 | "linux-arm32", 286 | ]; 287 | if !valid_os_names.contains(&name.as_str()) { 288 | Err(serde_valid::validation::Error::Custom(format!( 289 | "`{}` not a valid os name", 290 | &name 291 | ))) 292 | } else { 293 | Ok(()) 294 | } 295 | } 296 | 297 | #[skip_serializing_none] 298 | #[derive(Deserialize, Serialize, Debug, Clone, Validate, merge::Merge)] 299 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 300 | pub struct MojangRule { 301 | #[validate(custom(mojang_rule_action_must_be_allow_disallow))] 302 | #[merge(strategy = merge::overwrite)] 303 | pub action: String, 304 | #[merge(strategy = merge::option::recurse)] 305 | pub os: Option, 306 | } 307 | 308 | fn mojang_rule_action_must_be_allow_disallow( 309 | action: &String, 310 | ) -> Result<(), serde_valid::validation::Error> { 311 | if !["allow", "disallow"].contains(&action.as_str()) { 312 | Err(serde_valid::validation::Error::Custom(format!( 313 | "`{}` not a valid action, must be `allow` or `disallow`", 314 | &action 315 | ))) 316 | } else { 317 | Ok(()) 318 | } 319 | } 320 | 321 | #[derive(Deserialize, Serialize, Debug, Clone, Validate, merge::Merge)] 322 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 323 | pub struct MojangRules { 324 | #[merge(strategy = merge::vec::append)] 325 | root: Vec, 326 | } 327 | 328 | impl Deref for MojangRules { 329 | type Target = Vec; 330 | 331 | fn deref(&self) -> &Self::Target { 332 | &self.root 333 | } 334 | } 335 | 336 | #[skip_serializing_none] 337 | #[derive(Deserialize, Serialize, Debug, Clone, Validate, merge::Merge, Default)] 338 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 339 | pub struct MojangLibrary { 340 | #[merge(strategy = merge::option::recurse)] 341 | pub extract: Option, 342 | #[merge(strategy = merge::option::overwrite_some)] 343 | pub name: Option, 344 | #[merge(strategy = merge::option::recurse)] 345 | pub downloads: Option, 346 | #[merge(strategy = merge::option_hashmap::overwrite_key_some)] 347 | pub natives: Option>, 348 | #[merge(strategy = merge::option::recurse)] 349 | pub rules: Option, 350 | } 351 | 352 | #[skip_serializing_none] 353 | #[derive(Deserialize, Serialize, Debug, Clone, Validate, merge::Merge, Default)] 354 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 355 | pub struct Library { 356 | #[merge(strategy = merge::option::recurse)] 357 | pub extract: Option, 358 | #[merge(strategy = merge::option::overwrite_some)] 359 | pub name: Option, 360 | #[merge(strategy = merge::option::recurse)] 361 | pub downloads: Option, 362 | #[merge(strategy = merge::option_hashmap::overwrite_key_some)] 363 | pub natives: Option>, 364 | #[merge(strategy = merge::option::recurse)] 365 | pub rules: Option, 366 | #[merge(strategy = merge::option::overwrite_some)] 367 | url: Option, 368 | #[serde(rename = "MMC-hint")] 369 | #[merge(strategy = merge::option::overwrite_some)] 370 | mmc_hint: Option, 371 | } 372 | 373 | impl From for Library { 374 | fn from(item: MojangLibrary) -> Self { 375 | Self { 376 | extract: item.extract, 377 | name: item.name, 378 | downloads: item.downloads, 379 | natives: item.natives, 380 | rules: item.rules, 381 | url: None, 382 | mmc_hint: None, 383 | } 384 | } 385 | } 386 | 387 | impl From for MojangLibrary { 388 | fn from(item: Library) -> Self { 389 | Self { 390 | extract: item.extract, 391 | name: item.name, 392 | downloads: item.downloads, 393 | natives: item.natives, 394 | rules: item.rules, 395 | } 396 | } 397 | } 398 | 399 | impl From<&MojangLibrary> for Library { 400 | fn from(item: &MojangLibrary) -> Self { 401 | Self { 402 | extract: item.extract.clone(), 403 | name: item.name.clone(), 404 | downloads: item.downloads.clone(), 405 | natives: item.natives.clone(), 406 | rules: item.rules.clone(), 407 | url: None, 408 | mmc_hint: None, 409 | } 410 | } 411 | } 412 | 413 | impl From<&Library> for MojangLibrary { 414 | fn from(item: &Library) -> Self { 415 | Self { 416 | extract: item.extract.clone(), 417 | name: item.name.clone(), 418 | downloads: item.downloads.clone(), 419 | natives: item.natives.clone(), 420 | rules: item.rules.clone(), 421 | } 422 | } 423 | } 424 | 425 | #[skip_serializing_none] 426 | #[derive(Deserialize, Serialize, Debug, Clone, Validate, merge::Merge, Default)] 427 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 428 | pub struct Dependency { 429 | #[merge(strategy = merge::overwrite)] 430 | pub uid: String, 431 | #[merge(strategy = merge::option::overwrite_some)] 432 | pub equals: Option, 433 | #[merge(strategy = merge::option::overwrite_some)] 434 | pub suggests: Option, 435 | } 436 | 437 | #[skip_serializing_none] 438 | #[derive(Deserialize, Serialize, Debug, Clone, Validate, merge::Merge, Default)] 439 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 440 | pub struct MetaVersion { 441 | #[merge(strategy = merge::overwrite)] 442 | pub format_version: i32, 443 | #[merge(strategy = merge::overwrite)] 444 | pub name: String, 445 | #[merge(strategy = merge::overwrite)] 446 | pub version: String, 447 | #[merge(strategy = merge::overwrite)] 448 | pub uid: String, 449 | #[serde(rename = "type")] 450 | #[merge(strategy = merge::option::overwrite_some)] 451 | pub version_type: Option, 452 | #[merge(strategy = merge::option::overwrite_some)] 453 | pub order: Option, 454 | #[merge(strategy = merge::option::overwrite_some)] 455 | pub volatile: Option, 456 | #[merge(strategy = merge::option_vec::append_some)] 457 | pub requires: Option>, 458 | #[merge(strategy = merge::option_vec::append_some)] 459 | pub conflicts: Option>, 460 | #[merge(strategy = merge::option_vec::append_some)] 461 | pub libraries: Option>, 462 | #[merge(strategy = merge::option::overwrite_some)] 463 | pub asset_index: Option, 464 | #[merge(strategy = merge::option_vec::append_some)] 465 | pub maven_files: Option>, 466 | #[merge(strategy = merge::option::overwrite_some)] 467 | pub main_jar: Option, 468 | #[merge(strategy = merge::option_vec::append_some)] 469 | pub jar_mods: Option>, 470 | #[merge(strategy = merge::option::overwrite_some)] 471 | pub main_class: Option, 472 | #[merge(strategy = merge::option::overwrite_some)] 473 | pub applet_class: Option, 474 | #[merge(strategy = merge::option::overwrite_some)] 475 | pub minecraft_arguments: Option, 476 | #[merge(strategy = merge::option::overwrite_some)] 477 | pub release_time: Option>, 478 | #[merge(strategy = merge::option_vec::append_some)] 479 | pub compatible_java_majors: Option>, 480 | #[merge(strategy = merge::option_vec::append_some)] 481 | pub additional_traits: Option>, 482 | #[serde(rename = "+tweakers")] 483 | #[merge(strategy = merge::option_vec::append_some)] 484 | pub additional_tweakers: Option>, 485 | #[serde(rename = "+jvmArgs")] 486 | #[merge(strategy = merge::option_vec::append_some)] 487 | pub additional_jvm_args: Option>, 488 | } 489 | 490 | #[derive(Deserialize, Serialize, Debug, Clone)] 491 | pub struct MetaIndexEntry { 492 | pub update_time: chrono::DateTime, 493 | pub hash: String, 494 | } 495 | 496 | pub mod validation { 497 | pub fn is_some(obj: Option) -> Result<(), serde_valid::validation::Error> { 498 | if obj.is_none() { 499 | return Err(serde_valid::validation::Error::Custom( 500 | "Must be some".to_string(), 501 | )); 502 | } 503 | Ok(()) 504 | } 505 | } 506 | 507 | pub mod merge { 508 | pub use merge::Merge; 509 | pub use merge::{bool, num, ord, vec}; 510 | 511 | /// generic overwrite strategy 512 | pub fn overwrite(left: &mut T, right: T) { 513 | *left = right 514 | } 515 | 516 | /// Merge strategies for `Option` 517 | pub mod option { 518 | /// Overwrite `left` with `right` only if `left` is `None`. 519 | pub fn overwrite_none(left: &mut Option, right: Option) { 520 | if left.is_none() { 521 | *left = right; 522 | } 523 | } 524 | 525 | /// Overwrite `left` with `right` only if `right` is `Some` 526 | pub fn overwrite_some(left: &mut Option, right: Option) { 527 | if let Some(new) = right { 528 | *left = Some(new); 529 | } 530 | } 531 | 532 | /// If both `left` and `right` are `Some`, recursively merge the two. 533 | /// Otherwise, fall back to `overwrite_none`. 534 | pub fn recurse(left: &mut Option, right: Option) { 535 | if let Some(new) = right { 536 | if let Some(original) = left { 537 | original.merge(new); 538 | } else { 539 | *left = Some(new); 540 | } 541 | } 542 | } 543 | } 544 | 545 | /// Merge strategies for `HashMap` 546 | pub mod hashmap { 547 | use std::collections::HashMap; 548 | use std::hash::Hash; 549 | 550 | pub fn recurse( 551 | left: &mut HashMap, 552 | right: HashMap, 553 | ) { 554 | use std::collections::hash_map::Entry; 555 | 556 | for (k, v) in right { 557 | match left.entry(k) { 558 | Entry::Occupied(mut existing) => existing.get_mut().merge(v), 559 | Entry::Vacant(empty) => { 560 | empty.insert(v); 561 | } 562 | } 563 | } 564 | } 565 | 566 | pub fn overwrite_key(left: &mut HashMap, right: HashMap) { 567 | for (k, v) in right { 568 | left.insert(k, v); 569 | } 570 | } 571 | } 572 | 573 | pub mod btreemap { 574 | use std::collections::BTreeMap; 575 | use std::hash::Hash; 576 | 577 | pub fn recurse( 578 | left: &mut BTreeMap, 579 | right: BTreeMap, 580 | ) { 581 | use std::collections::btree_map::Entry; 582 | 583 | for (k, v) in right { 584 | match left.entry(k) { 585 | Entry::Occupied(mut existing) => existing.get_mut().merge(v), 586 | Entry::Vacant(empty) => { 587 | empty.insert(v); 588 | } 589 | } 590 | } 591 | } 592 | 593 | pub fn overwrite_key( 594 | left: &mut BTreeMap, 595 | right: BTreeMap, 596 | ) { 597 | for (k, v) in right { 598 | left.insert(k, v); 599 | } 600 | } 601 | } 602 | 603 | /// Merge strategies for `Option` 604 | pub mod option_hashmap { 605 | use super::hashmap; 606 | use std::collections::HashMap; 607 | use std::hash::Hash; 608 | 609 | pub fn recurse_some( 610 | left: &mut Option>, 611 | right: Option>, 612 | ) { 613 | if let Some(new) = right { 614 | if let Some(original) = left { 615 | hashmap::recurse(original, new); 616 | } else { 617 | *left = Some(new); 618 | } 619 | } 620 | } 621 | 622 | pub fn overwrite_key_some( 623 | left: &mut Option>, 624 | right: Option>, 625 | ) { 626 | if let Some(new) = right { 627 | if let Some(original) = left { 628 | hashmap::overwrite_key(original, new); 629 | } else { 630 | *left = Some(new); 631 | } 632 | } 633 | } 634 | } 635 | 636 | pub mod option_btreemap { 637 | use super::btreemap; 638 | use std::collections::BTreeMap; 639 | use std::hash::Hash; 640 | 641 | pub fn recurse_some( 642 | left: &mut Option>, 643 | right: Option>, 644 | ) { 645 | if let Some(new) = right { 646 | if let Some(original) = left { 647 | btreemap::recurse(original, new); 648 | } else { 649 | *left = Some(new); 650 | } 651 | } 652 | } 653 | 654 | pub fn overwrite_key_some( 655 | left: &mut Option>, 656 | right: Option>, 657 | ) { 658 | if let Some(new) = right { 659 | if let Some(original) = left { 660 | btreemap::overwrite_key(original, new); 661 | } else { 662 | *left = Some(new); 663 | } 664 | } 665 | } 666 | } 667 | 668 | /// Merge strategies for `Option` 669 | pub mod option_vec { 670 | 671 | /// Append the contents of `right` to `left` if `left` and `right` are `Some` 672 | /// replace the option if `left` is `None` and `right` is `Some` 673 | pub fn append_some(left: &mut Option>, right: Option>) { 674 | if let Some(mut new) = right { 675 | if let Some(original) = left { 676 | original.append(&mut new); 677 | } else { 678 | *left = Some(new); 679 | } 680 | } 681 | } 682 | } 683 | } 684 | -------------------------------------------------------------------------------- /libmcmeta/src/models/mojang.rs: -------------------------------------------------------------------------------- 1 | use core::ops::Deref; 2 | use lazy_static::lazy_static; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_valid::Validate; 5 | use serde_with::skip_serializing_none; 6 | use std::collections::HashMap; 7 | 8 | use crate::models::{ 9 | GradleSpecifier, Library, MetaVersion, MojangArtifact, MojangArtifactBase, MojangAssets, 10 | MojangLibrary, MojangLibraryDownloads, META_FORMAT_VERSION, 11 | }; 12 | 13 | static SUPPORTED_LAUNCHER_VERSION: i32 = 21; 14 | static SUPPORTED_COMPLIANCE_LEVEL: i32 = 1; 15 | static DEFAULT_JAVA_MAJOR: i32 = 8; 16 | 17 | lazy_static! { 18 | static ref COMPATIBLE_JAVA_MAPPINGS: HashMap> = { 19 | let mut m = HashMap::new(); 20 | m.insert(16, vec![17]); 21 | m 22 | }; 23 | } 24 | 25 | #[skip_serializing_none] 26 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 27 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 28 | pub struct MojangVersionManifest { 29 | /// The latest version of Minecraft. 30 | pub latest: MojangVersionManifestLatest, 31 | /// A list of all versions of Minecraft. 32 | pub versions: Vec, 33 | } 34 | 35 | /// The latest version of Minecraft. 36 | #[skip_serializing_none] 37 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 38 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 39 | pub struct MojangVersionManifestLatest { 40 | /// The latest release version of Minecraft. 41 | pub release: String, 42 | /// The latest snapshot version of Minecraft. 43 | pub snapshot: String, 44 | } 45 | 46 | /// A version of Minecraft. 47 | #[skip_serializing_none] 48 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 49 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 50 | pub struct MojangVersionManifestVersion { 51 | /// The ID of the version. 52 | pub id: String, 53 | /// The type of the version. 54 | #[serde(rename = "type")] 55 | pub version_type: String, 56 | /// The URL to the version's JSON. 57 | pub url: String, 58 | /// The time the version was last updated. 59 | pub time: chrono::DateTime, 60 | /// The time the version was released. 61 | pub release_time: chrono::DateTime, 62 | /// Compliance level 63 | pub compliance_level: i32, 64 | /// The sha1 hash of the version's JSON. 65 | pub sha1: String, 66 | } 67 | 68 | #[skip_serializing_none] 69 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 70 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 71 | pub struct AssetIndex { 72 | pub id: String, 73 | pub sha1: String, 74 | pub size: i32, 75 | pub total_size: i32, 76 | pub url: String, 77 | } 78 | 79 | #[skip_serializing_none] 80 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 81 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 82 | pub struct VersionDownload { 83 | pub sha1: String, 84 | pub size: i32, 85 | pub url: String, 86 | } 87 | 88 | #[skip_serializing_none] 89 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 90 | #[serde(deny_unknown_fields)] 91 | pub struct VersionDownloads { 92 | pub client: VersionDownload, 93 | pub server: Option, 94 | pub windows_server: Option, 95 | pub client_mappings: Option, 96 | pub server_mappings: Option, 97 | } 98 | 99 | fn default_java_version_component() -> String { 100 | "jre-legacy".to_string() 101 | } 102 | fn default_java_version_major_version() -> i32 { 103 | 8 104 | } 105 | 106 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 107 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 108 | pub struct JavaVersion { 109 | #[serde(default = "default_java_version_component")] 110 | pub component: String, 111 | #[serde(default = "default_java_version_major_version")] 112 | pub major_version: i32, 113 | } 114 | 115 | impl Default for JavaVersion { 116 | fn default() -> Self { 117 | Self { 118 | component: default_java_version_component(), 119 | major_version: default_java_version_major_version(), 120 | } 121 | } 122 | } 123 | 124 | #[skip_serializing_none] 125 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 126 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 127 | pub struct VersionLibraryDownloadInfo { 128 | pub path: String, 129 | pub sha1: String, 130 | pub size: i32, 131 | pub url: String, 132 | } 133 | 134 | #[skip_serializing_none] 135 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 136 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 137 | pub struct VersionLibraryClassifiers { 138 | pub javadoc: Option, 139 | #[serde(rename = "natives-linux")] 140 | pub natives_linux: Option, 141 | #[serde(rename = "natives-macos")] 142 | pub natives_macos: Option, 143 | #[serde(rename = "natives-osx")] 144 | pub natives_osx: Option, 145 | #[serde(rename = "natives-windows")] 146 | pub natives_windows: Option, 147 | #[serde(rename = "natives-windows-32")] 148 | pub natives_windows_32: Option, 149 | #[serde(rename = "natives-windows-64")] 150 | pub natives_windows_64: Option, 151 | #[serde(rename = "linux-x86_64")] 152 | pub linux_x86_64: Option, 153 | pub sources: Option, 154 | } 155 | 156 | #[skip_serializing_none] 157 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 158 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 159 | pub struct VersionLibraryNatives { 160 | pub linux: Option, 161 | pub osx: Option, 162 | pub windows: Option, 163 | } 164 | 165 | #[skip_serializing_none] 166 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 167 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 168 | pub struct VersionLibraryDownloads { 169 | pub artifact: Option, 170 | pub classifiers: Option, 171 | } 172 | 173 | #[skip_serializing_none] 174 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 175 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 176 | pub struct VersionLibraryExtract { 177 | pub exclude: Vec, 178 | } 179 | 180 | #[skip_serializing_none] 181 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 182 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 183 | pub struct VersionLibrary { 184 | pub name: String, 185 | pub downloads: VersionLibraryDownloads, 186 | pub natives: Option, 187 | pub extract: Option, 188 | #[validate] 189 | pub rules: Option>, 190 | } 191 | 192 | #[skip_serializing_none] 193 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 194 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 195 | pub struct ManifestRule { 196 | pub action: String, 197 | pub os: Option, 198 | #[validate] 199 | pub features: Option, 200 | } 201 | 202 | #[skip_serializing_none] 203 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 204 | pub struct ManifestRuleFeatures { 205 | pub is_demo_user: Option, 206 | pub has_custom_resolution: Option, 207 | pub has_quick_plays_support: Option, 208 | pub is_quick_play_singleplayer: Option, 209 | pub is_quick_play_multiplayer: Option, 210 | pub is_quick_play_realms: Option, 211 | #[serde(flatten)] 212 | #[validate(custom(validate_empty_unknown_key_map))] 213 | pub unknown: HashMap, 214 | } 215 | 216 | fn validate_empty_unknown_key_map( 217 | map: &HashMap, 218 | ) -> Result<(), serde_valid::validation::Error> { 219 | if !map.is_empty() { 220 | return Err(serde_valid::validation::Error::Custom(format!( 221 | "There are unknown keys present: {:?}", 222 | map 223 | ))); 224 | } 225 | 226 | Ok(()) 227 | } 228 | 229 | #[skip_serializing_none] 230 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 231 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 232 | pub struct ManifestRuleOS { 233 | pub name: Option, 234 | pub version: Option, 235 | pub arch: Option, 236 | } 237 | 238 | #[skip_serializing_none] 239 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 240 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 241 | pub struct VersionLogging { 242 | pub client: VersionLoggingClient, 243 | } 244 | 245 | #[skip_serializing_none] 246 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 247 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 248 | pub struct VersionLoggingClient { 249 | pub argument: String, 250 | pub file: VersionLoggingClientFile, 251 | #[serde(rename = "type")] 252 | pub logging_type: String, 253 | } 254 | 255 | #[skip_serializing_none] 256 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 257 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 258 | pub struct VersionLoggingClientFile { 259 | pub id: String, 260 | pub sha1: String, 261 | pub size: i32, 262 | pub url: String, 263 | } 264 | 265 | #[skip_serializing_none] 266 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 267 | #[serde(untagged)] 268 | pub enum VersionArgument { 269 | String(String), 270 | Object(#[validate] VersionArgumentObject), 271 | } 272 | 273 | #[skip_serializing_none] 274 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 275 | #[serde(untagged)] 276 | pub enum VersionArgumentValue { 277 | String(String), 278 | Array(Vec), 279 | } 280 | 281 | #[skip_serializing_none] 282 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 283 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 284 | pub struct VersionArgumentObject { 285 | #[validate] 286 | pub rules: Vec, 287 | pub value: VersionArgumentValue, 288 | } 289 | 290 | #[skip_serializing_none] 291 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 292 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 293 | pub struct VersionArguments { 294 | #[validate] 295 | pub game: Vec, 296 | #[validate] 297 | pub jvm: Vec, 298 | } 299 | 300 | #[skip_serializing_none] 301 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 302 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 303 | pub struct MinecraftVersion { 304 | pub asset_index: AssetIndex, 305 | pub assets: String, 306 | pub compliance_level: Option, 307 | pub downloads: Option, 308 | pub id: String, 309 | pub java_version: Option, 310 | #[validate] 311 | pub libraries: Vec, 312 | pub logging: Option, 313 | pub main_class: String, 314 | pub minecraft_arguments: Option, 315 | #[validate] 316 | pub arguments: Option, 317 | pub minimum_launcher_version: i32, 318 | pub release_time: String, 319 | pub time: String, 320 | #[serde(rename = "type")] 321 | pub release_type: String, 322 | } 323 | 324 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 325 | pub struct ExperimentEntry { 326 | pub id: String, 327 | pub url: String, 328 | pub wiki: Option, 329 | } 330 | 331 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 332 | pub struct ExperimentIndex { 333 | pub experiments: Vec, 334 | } 335 | 336 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 337 | pub struct OldSnapshotEntry { 338 | pub id: String, 339 | pub url: String, 340 | pub wiki: Option, 341 | pub jar: String, 342 | pub sha1: String, 343 | pub size: i32, 344 | } 345 | 346 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 347 | pub struct OldSnapshotIndex { 348 | pub old_snapshots: Vec, 349 | } 350 | 351 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 352 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 353 | pub struct LegacyOverrideEntry { 354 | main_class: Option, 355 | applet_class: Option, 356 | pub release_time: Option>, 357 | #[serde(rename = "+traits")] 358 | additional_traits: Option>, 359 | #[serde(rename = "+jvmArgs")] 360 | additional_jvm_args: Option>, 361 | } 362 | 363 | impl LegacyOverrideEntry { 364 | pub fn apply_onto_meta_version(self, meta_version: &mut MetaVersion, legacy: bool) { 365 | // simply hard override classes 366 | 367 | meta_version.main_class = self.main_class.clone(); 368 | meta_version.applet_class = self.applet_class.clone(); 369 | 370 | // if we have an updated release time (more correct than Mojang), use it 371 | if let Some(release_time) = &self.release_time { 372 | meta_version.release_time = Some(*release_time); 373 | } 374 | 375 | // add traits, if any 376 | if let Some(mut additional_traits) = self.additional_traits { 377 | if meta_version.additional_traits.is_none() { 378 | meta_version.additional_traits = Some(vec![]); 379 | } 380 | meta_version 381 | .additional_traits 382 | .as_mut() 383 | .unwrap() 384 | .append(&mut additional_traits); 385 | } 386 | 387 | if let Some(mut additional_jvm_args) = self.additional_jvm_args { 388 | if meta_version.additional_jvm_args.is_none() { 389 | meta_version.additional_jvm_args = Some(vec![]); 390 | } 391 | meta_version 392 | .additional_jvm_args 393 | .as_mut() 394 | .unwrap() 395 | .append(&mut additional_jvm_args); 396 | } 397 | 398 | if legacy { 399 | // remove all libraries - they are not needed for legacy 400 | meta_version.libraries = None; 401 | // remove minecraft arguments - we use our own hardcoded ones 402 | meta_version.minecraft_arguments = None; 403 | } 404 | } 405 | } 406 | 407 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 408 | pub struct LegacyOverrideIndex { 409 | versions: HashMap, 410 | } 411 | 412 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 413 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 414 | pub struct LibraryPatch { 415 | #[serde(rename = "match")] 416 | pub patch_match: Vec, 417 | #[serde(rename = "override")] 418 | pub patch_override: Option, 419 | pub additional_libraries: Option>, 420 | #[serde(default = "default_library_patch_patch_additional_libraries")] 421 | pub patch_additional_libraries: bool, 422 | } 423 | 424 | fn default_library_patch_patch_additional_libraries() -> bool { 425 | false 426 | } 427 | 428 | impl LibraryPatch { 429 | pub fn applies(&self, target: &Library) -> bool { 430 | if let Some(name) = &target.name { 431 | self.patch_match.contains(name) 432 | } else { 433 | false 434 | } 435 | } 436 | } 437 | 438 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 439 | pub struct LibraryPatches { 440 | root: Vec, 441 | } 442 | 443 | impl Deref for LibraryPatches { 444 | type Target = Vec; 445 | 446 | fn deref(&self) -> &Self::Target { 447 | &self.root 448 | } 449 | } 450 | 451 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 452 | pub struct MojangArgumentObject {} 453 | 454 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 455 | #[serde(untagged)] 456 | pub enum MojangArgument { 457 | String(String), 458 | Object(MojangArgumentObject), 459 | } 460 | 461 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 462 | pub struct MojangArguments { 463 | pub game: Option>, // mixture of strings and objects 464 | pub jvm: Option>, 465 | } 466 | 467 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 468 | pub struct MojangLoggingArtifact { 469 | id: String, 470 | } 471 | 472 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 473 | pub struct MojangLogging { 474 | file: MojangLoggingArtifact, 475 | argument: String, 476 | #[serde(rename = "type")] 477 | #[validate(custom(mojang_logging_validate_type))] 478 | logging_type: String, 479 | } 480 | 481 | fn mojang_logging_validate_type( 482 | logging_type: &String, 483 | ) -> Result<(), serde_valid::validation::Error> { 484 | let valid_logging_types = ["log4j2-xml"]; 485 | if !valid_logging_types.contains(&logging_type.as_str()) { 486 | Err(serde_valid::validation::Error::Custom(format!( 487 | "invalid log type: {}", 488 | &logging_type 489 | ))) 490 | } else { 491 | Ok(()) 492 | } 493 | } 494 | 495 | #[derive(Deserialize, Serialize, Debug, Clone, Validate)] 496 | #[serde(rename_all = "camelCase", deny_unknown_fields)] 497 | pub struct MojangVersion { 498 | #[serde(rename = "_comment_", alias = "_comment", alias = "comment")] 499 | pub comment: Option>, 500 | pub id: String, // TODO: optional? 501 | pub arguments: Option, 502 | pub asset_index: Option, 503 | pub assets: Option, 504 | pub downloads: Option>, // TODO improve this? 505 | pub libraries: Option>, // TODO: optional? 506 | pub main_class: Option, 507 | pub applet_class: Option, 508 | pub process_arguments: Option, 509 | pub minecraft_arguments: Option, 510 | #[validate(custom(mojang_version_validate_minimum_launcher_version))] 511 | pub minimum_launcher_version: Option, 512 | pub release_time: Option>, 513 | pub time: Option>, 514 | #[serde(rename = "type")] 515 | pub version_type: Option, 516 | pub inherits_from: Option, 517 | pub logging: Option>, // TODO improve this? 518 | #[validate(custom(mojang_version_validate_compliance_level))] 519 | pub compliance_level: Option, 520 | pub java_version: Option, 521 | } 522 | 523 | fn mojang_version_validate_minimum_launcher_version( 524 | minimum_launcher_version: &Option, 525 | ) -> Result<(), serde_valid::validation::Error> { 526 | if let Some(minimum_launcher_version) = minimum_launcher_version { 527 | if minimum_launcher_version <= &SUPPORTED_LAUNCHER_VERSION { 528 | return Ok(()); 529 | } 530 | } 531 | Err(serde_valid::validation::Error::Custom(format!( 532 | "Invalid launcher version `{:?}`", 533 | &minimum_launcher_version 534 | ))) 535 | } 536 | 537 | fn mojang_version_validate_compliance_level( 538 | compliance_level: &Option, 539 | ) -> Result<(), serde_valid::validation::Error> { 540 | if let Some(compliance_level) = compliance_level { 541 | if compliance_level <= &SUPPORTED_COMPLIANCE_LEVEL { 542 | return Ok(()); 543 | } 544 | } 545 | Err(serde_valid::validation::Error::Custom(format!( 546 | "Invalid compliance level `{:?}`", 547 | &compliance_level 548 | ))) 549 | } 550 | 551 | impl MojangVersion { 552 | pub fn to_meta_version(&self, name: &str, uid: &str, version: &str) -> MetaVersion { 553 | let mut main_jar = None; 554 | let mut addn_traits = None; 555 | let mut new_type = self.version_type.clone(); 556 | let mut compatible_java_majors; 557 | if !self.id.is_empty() { 558 | let downloads = self.downloads.clone().expect("Missing downloads"); 559 | let client_download = downloads 560 | .get("client") 561 | .expect("Missing `client` in downloads"); 562 | let artifact = MojangArtifact { 563 | url: client_download.url.clone(), 564 | sha1: client_download.sha1.clone(), 565 | size: client_download.size, 566 | path: None, 567 | }; 568 | let downloads = MojangLibraryDownloads { 569 | artifact: Some(artifact), 570 | classifiers: None, 571 | }; 572 | main_jar = Some(Library { 573 | name: Some(GradleSpecifier { 574 | group: "com.mojang".to_string(), 575 | artifact: "minecraft".to_string(), 576 | version: self.id.clone(), 577 | classifier: Some("client".to_string()), 578 | extension: None, 579 | }), 580 | downloads: Some(downloads), 581 | extract: None, 582 | natives: None, 583 | rules: None, 584 | url: None, 585 | mmc_hint: None, 586 | }); 587 | } 588 | match self.compliance_level { 589 | None => {} 590 | Some(0) => {} 591 | Some(1) => { 592 | if addn_traits.is_none() { 593 | addn_traits = Some(vec![]); 594 | } 595 | } 596 | Some(l) => { 597 | panic!("Unsupported compliance level {}", l); 598 | } 599 | } 600 | 601 | let mut major = DEFAULT_JAVA_MAJOR; 602 | 603 | if let Some(java_version) = &self.java_version { 604 | major = java_version.major_version; 605 | } 606 | 607 | compatible_java_majors = Some(vec![major]); 608 | 609 | if let Some(mappings) = COMPATIBLE_JAVA_MAPPINGS.get(&major) { 610 | compatible_java_majors 611 | .as_mut() 612 | .unwrap() 613 | .append(&mut mappings.clone()); 614 | } 615 | 616 | if let Some(t) = &new_type { 617 | if t == "pending" { 618 | new_type = Some("experiment".to_string()); 619 | } 620 | } 621 | 622 | let new_libs = self 623 | .libraries 624 | .as_ref() 625 | .map(|libraries| libraries.iter().map(|lib| lib.into()).collect()); 626 | 627 | MetaVersion { 628 | format_version: META_FORMAT_VERSION, 629 | name: name.to_string(), 630 | uid: uid.to_string(), 631 | version: version.to_string(), 632 | asset_index: self.asset_index.clone(), 633 | libraries: new_libs, 634 | main_class: self.main_class.clone(), 635 | minecraft_arguments: self.minecraft_arguments.clone(), 636 | release_time: self.release_time, 637 | version_type: new_type, 638 | compatible_java_majors, 639 | additional_traits: addn_traits, 640 | main_jar, 641 | order: None, 642 | volatile: None, 643 | requires: None, 644 | conflicts: None, 645 | maven_files: None, 646 | jar_mods: None, 647 | applet_class: None, 648 | additional_tweakers: None, 649 | additional_jvm_args: None, 650 | } 651 | } 652 | } 653 | 654 | #[cfg(test)] 655 | mod tests { 656 | 657 | use serde_valid::Validate; 658 | 659 | #[test] 660 | fn test_deserialization() { 661 | // meta dir is ./meta 662 | let cwd = std::env::current_dir().unwrap(); 663 | let meta_dir = cwd.join("../meta/mojang"); 664 | println!("meta_dir: {:?}", meta_dir); 665 | 666 | let version_manifest = serde_json::from_str::( 667 | &std::fs::read_to_string(meta_dir.join("version_manifest_v2.json")).unwrap(), 668 | ); 669 | if let Err(e) = version_manifest { 670 | panic!("Failed to deserialize version manifest: {:?}", e); 671 | } 672 | 673 | // loop through all files in meta_dir/versions 674 | for entry in std::fs::read_dir(meta_dir.join("versions")).unwrap() { 675 | let entry = entry.unwrap(); 676 | let path = entry.path(); 677 | if path.is_file() { 678 | let version = serde_json::from_str::( 679 | &std::fs::read_to_string(path).unwrap(), 680 | ); 681 | if let Err(e) = version { 682 | panic!( 683 | "Failed to deserialize version {}: {:?}", 684 | entry.file_name().to_str().unwrap(), 685 | e 686 | ); 687 | } 688 | if let Err(e) = version.unwrap().validate() { 689 | panic!( 690 | "Failed to validate version {}: \n{}\n", 691 | entry.file_name().to_str().unwrap(), 692 | serde_json::to_string_pretty(&e).unwrap() 693 | ) 694 | } 695 | } 696 | } 697 | } 698 | } 699 | -------------------------------------------------------------------------------- /static/mojang/minecraft-old-snapshots.json: -------------------------------------------------------------------------------- 1 | { 2 | "old_snapshots": [ 3 | { 4 | "id": "1_2", 5 | "wiki": "https://minecraft.wiki/w/Java_Edition_1.2", 6 | "url": "https://archive.org/download/Minecraft-JSONs/1.2.json", 7 | "sha1": "a2064011425a5e5befd9dee5eeb4f968ddf5ac77", 8 | "size": 3988919, 9 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/1_2/minecraft.jar" 10 | }, 11 | { 12 | "id": "11w47a", 13 | "wiki": "https://minecraft.wiki/w/Java_Edition_11w47a", 14 | "url": "https://archive.org/download/Minecraft-JSONs/11w47a.json", 15 | "sha1": "4e327918708d22e7443fbadefb9831ca04af4b90", 16 | "size": 2242242, 17 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/11w47a/minecraft.jar" 18 | }, 19 | { 20 | "id": "11w48a", 21 | "wiki": "https://minecraft.wiki/w/Java_Edition_11w48a", 22 | "url": "https://archive.org/download/Minecraft-JSONs/11w48a.json", 23 | "sha1": "fede770abe88a19e844d99dda611a7d18184155a", 24 | "size": 2242604, 25 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/11w48a/minecraft.jar" 26 | }, 27 | { 28 | "id": "11w49a", 29 | "wiki": "https://minecraft.wiki/w/Java_Edition_11w49a", 30 | "url": "https://archive.org/download/Minecraft-JSONs/11w49a.json", 31 | "sha1": "6f92a726e6b8b64f66c7e4d236f983c278d5af54", 32 | "size": 3510866, 33 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/11w49a/minecraft.jar" 34 | }, 35 | { 36 | "id": "11w50a", 37 | "wiki": "https://minecraft.wiki/w/Java_Edition_11w50a", 38 | "url": "https://archive.org/download/Minecraft-JSONs/11w50a.json", 39 | "sha1": "f4981ba0fee00a16d8dc9ec87bf2c4fdb51e4b7c", 40 | "size": 3509701, 41 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/11w50a/minecraft.jar" 42 | }, 43 | { 44 | "id": "12w01a", 45 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w01a", 46 | "url": "https://archive.org/download/Minecraft-JSONs/12w01a.json", 47 | "sha1": "653a9cf55884b6bc4dcf3c574331e04bd5ad1032", 48 | "size": 3839447, 49 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w01a/minecraft.jar" 50 | }, 51 | { 52 | "id": "12w03a", 53 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w03a", 54 | "url": "https://archive.org/download/Minecraft-JSONs/12w03a.json", 55 | "sha1": "e581c7c9dd57cbf73f72b833be5eff6109187df0", 56 | "size": 3875210, 57 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w03a/minecraft.jar" 58 | }, 59 | { 60 | "id": "12w04a", 61 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w04a", 62 | "url": "https://archive.org/download/Minecraft-JSONs/12w04a.json", 63 | "sha1": "4911c473e856ec8102b8419eb36d0f54dad029a0", 64 | "size": 3911974, 65 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w04a/minecraft.jar" 66 | }, 67 | { 68 | "id": "12w05a", 69 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w05a", 70 | "url": "https://archive.org/download/Minecraft-JSONs/12w05a.json", 71 | "sha1": "28328e67b82564335aa8280095a0716a2eb790de", 72 | "size": 3931639, 73 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w05a/minecraft.jar" 74 | }, 75 | { 76 | "id": "12w05b", 77 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w05b", 78 | "url": "https://archive.org/download/Minecraft-JSONs/12w05b.json", 79 | "sha1": "75fbc4a39a244d0f1eb842ff8385e992e2b47dd5", 80 | "size": 3931694, 81 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w05b/minecraft.jar" 82 | }, 83 | { 84 | "id": "12w06a", 85 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w06a", 86 | "url": "https://archive.org/download/Minecraft-JSONs/12w06a.json", 87 | "sha1": "a8403c0d4c0cdb65722d864d9cf42663b8aab08b", 88 | "size": 3934973, 89 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w06a/minecraft.jar" 90 | }, 91 | { 92 | "id": "12w07a", 93 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w07a", 94 | "url": "https://archive.org/download/Minecraft-JSONs/12w07a.json", 95 | "sha1": "e7ad115b29612b893972f0817030d993bc56fb7e", 96 | "size": 3956252, 97 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w07a/minecraft.jar" 98 | }, 99 | { 100 | "id": "12w07b", 101 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w07b", 102 | "url": "https://archive.org/download/Minecraft-JSONs/12w07b.json", 103 | "sha1": "0eea35d588fc2cee5d397472aa3565f48c220217", 104 | "size": 3956323, 105 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w07b/minecraft.jar" 106 | }, 107 | { 108 | "id": "12w08a", 109 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w08a", 110 | "url": "https://archive.org/download/Minecraft-JSONs/12w08a.json", 111 | "sha1": "db2fcfdd23526b0f381ef2f3f2fd049d36227230", 112 | "size": 3981486, 113 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w08a/minecraft.jar" 114 | }, 115 | { 116 | "id": "12w16a", 117 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w16a", 118 | "url": "https://archive.org/download/Minecraft-JSONs/12w16a.json", 119 | "sha1": "6b0a9fe3ac275f79ac6d259f4279752274ec05f8", 120 | "size": 4080437, 121 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w16a/minecraft.jar" 122 | }, 123 | { 124 | "id": "12w17a", 125 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w17a", 126 | "url": "https://archive.org/download/Minecraft-JSONs/12w17a.json", 127 | "sha1": "17d41f8a07e054040ba34e523593bdea7f0fb6ba", 128 | "size": 4114768, 129 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w17a/minecraft.jar" 130 | }, 131 | { 132 | "id": "12w18a", 133 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w18a", 134 | "url": "https://archive.org/download/Minecraft-JSONs/12w18a.json", 135 | "sha1": "9e9ab992317048bee9158ad9d1e2bc758db2b4af", 136 | "size": 4317820, 137 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w18a/minecraft.zip/bin/minecraft.jar" 138 | }, 139 | { 140 | "id": "12w19a", 141 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w19a", 142 | "url": "https://archive.org/download/Minecraft-JSONs/12w19a.json", 143 | "sha1": "474aaac9a8b1dcbf312a5c09c7eae4a6aa401225", 144 | "size": 4343792, 145 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w19a/minecraft.zip/bin/minecraft.jar" 146 | }, 147 | { 148 | "id": "12w21a", 149 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w21a", 150 | "url": "https://archive.org/download/Minecraft-JSONs/12w21a.json", 151 | "sha1": "e755423a04b0efde01e035a9d651acadeba0aef9", 152 | "size": 4409586, 153 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w21a/minecraft.jar" 154 | }, 155 | { 156 | "id": "12w21b", 157 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w21b", 158 | "url": "https://archive.org/download/Minecraft-JSONs/12w21b.json", 159 | "sha1": "84437ded4839b29d34f83e9f3bab07cc48980faf", 160 | "size": 4499708, 161 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w21b/minecraft.jar" 162 | }, 163 | { 164 | "id": "12w22a", 165 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w22a", 166 | "url": "https://archive.org/download/Minecraft-JSONs/12w22a.json", 167 | "sha1": "3631a714cb465d39f5cb5c18aa23abf38031b359", 168 | "size": 4542344, 169 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w22a/minecraft.jar" 170 | }, 171 | { 172 | "id": "12w23a", 173 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w23a", 174 | "url": "https://archive.org/download/Minecraft-JSONs/12w23a.json", 175 | "sha1": "4a5a8e3349ea2e9d67fa4dde6ec68d385bff46f0", 176 | "size": 4543912, 177 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w23a/minecraft.jar" 178 | }, 179 | { 180 | "id": "12w23b", 181 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w23b", 182 | "url": "https://archive.org/download/Minecraft-JSONs/12w23b.json", 183 | "sha1": "e107667bcbb4443afc160a7eeb8f347acc9826f8", 184 | "size": 4543928, 185 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w23b/minecraft.jar" 186 | }, 187 | { 188 | "id": "12w24a", 189 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w24a", 190 | "url": "https://archive.org/download/Minecraft-JSONs/12w24a.json", 191 | "sha1": "e479c425ffe6ca3512d97ad0e02a8cd85356bf83", 192 | "size": 4540049, 193 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w24a/minecraft.jar" 194 | }, 195 | { 196 | "id": "12w25a", 197 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w25a", 198 | "url": "https://archive.org/download/Minecraft-JSONs/12w25a.json", 199 | "sha1": "eddf53994e40ecc44f582d4b47b9a441844909b6", 200 | "size": 4556548, 201 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w25a/minecraft.jar" 202 | }, 203 | { 204 | "id": "12w26a", 205 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w26a", 206 | "url": "https://archive.org/download/Minecraft-JSONs/12w26a.json", 207 | "sha1": "2d1e782a4c4435fe921027ae464a272945cca925", 208 | "size": 4573075, 209 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w26a/minecraft.jar" 210 | }, 211 | { 212 | "id": "12w27a", 213 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w27a", 214 | "url": "https://archive.org/download/Minecraft-JSONs/12w27a.json", 215 | "sha1": "5e69b80f9c757bdc8275c1f6ce7e71820fe6d79a", 216 | "size": 4584956, 217 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w27a/minecraft.jar" 218 | }, 219 | { 220 | "id": "12w30a", 221 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w30a", 222 | "url": "https://archive.org/download/Minecraft-JSONs/12w30a.json", 223 | "sha1": "368215d7fd38ee3e829725e11b3f193d45801128", 224 | "size": 4584574, 225 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w30a/minecraft.jar" 226 | }, 227 | { 228 | "id": "12w30b", 229 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w30b", 230 | "url": "https://archive.org/download/Minecraft-JSONs/12w30b.json", 231 | "sha1": "9d1e450cdb300ec426b50762e031796a8349aa1c", 232 | "size": 4584593, 233 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w30b/minecraft.jar" 234 | }, 235 | { 236 | "id": "12w30c", 237 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w30c", 238 | "url": "https://archive.org/download/Minecraft-JSONs/12w30c.json", 239 | "sha1": "92817a0c3f3c913ad68bdb082ac1f147db986282", 240 | "size": 4584617, 241 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w30c/minecraft.jar" 242 | }, 243 | { 244 | "id": "12w30d", 245 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w30d", 246 | "url": "https://archive.org/download/Minecraft-JSONs/12w30d.json", 247 | "sha1": "a5e7508de2d3993cb5222d8e4f8415226745d6ff", 248 | "size": 4585459, 249 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w30d/minecraft.jar" 250 | }, 251 | { 252 | "id": "12w30e", 253 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w30e", 254 | "url": "https://archive.org/download/Minecraft-JSONs/12w30e.json", 255 | "sha1": "1a37562cda14028dae15b331bfd36108e617a477", 256 | "size": 4585506, 257 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w30e/minecraft.jar" 258 | }, 259 | { 260 | "id": "12w32a", 261 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w32a", 262 | "url": "https://archive.org/download/Minecraft-JSONs/12w32a.json", 263 | "sha1": "13183e023c8918ed08c302c2fe1438f61b53d094", 264 | "size": 4628354, 265 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w32a/minecraft.jar" 266 | }, 267 | { 268 | "id": "12w34a", 269 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w34a", 270 | "url": "https://archive.org/download/Minecraft-JSONs/12w34a.json", 271 | "sha1": "41769085c020f4651b5b5dd50a6f83be2b000b29", 272 | "size": 4676139, 273 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w34a/minecraft.jar" 274 | }, 275 | { 276 | "id": "12w34b", 277 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w34b", 278 | "url": "https://archive.org/download/Minecraft-JSONs/12w34b.json", 279 | "sha1": "5fb51efc8f07ea57ffc2a02a7dac8a2835651b61", 280 | "size": 4682004, 281 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w34b/minecraft.jar" 282 | }, 283 | { 284 | "id": "12w36a", 285 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w36a", 286 | "url": "https://archive.org/download/Minecraft-JSONs/12w36a.json", 287 | "sha1": "914bd89686c4621da327d50375a1edbdd9c177da", 288 | "size": 4705667, 289 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w36a/minecraft.jar" 290 | }, 291 | { 292 | "id": "12w37a", 293 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w37a", 294 | "url": "https://archive.org/download/Minecraft-JSONs/12w37a.json", 295 | "sha1": "50ea0bac2c91b13c0881bbf99aad66a046533781", 296 | "size": 4727781, 297 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w37a/minecraft.jar" 298 | }, 299 | { 300 | "id": "12w38a", 301 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w38a", 302 | "url": "https://archive.org/download/Minecraft-JSONs/12w38a.json", 303 | "sha1": "69e5a531fa615eb870345feb25f26126fe95586b", 304 | "size": 4752649, 305 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w38a/minecraft.jar" 306 | }, 307 | { 308 | "id": "12w38b", 309 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w38b", 310 | "url": "https://archive.org/download/Minecraft-JSONs/12w38b.json", 311 | "sha1": "867505cb4934016bf46cb8c7833ef0eaef8d39d9", 312 | "size": 4767044, 313 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w38b/minecraft.jar" 314 | }, 315 | { 316 | "id": "12w39a", 317 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w39a", 318 | "url": "https://archive.org/download/Minecraft-JSONs/12w39a.json", 319 | "sha1": "65247c02036156b9f34c17f7d8bb053641afd0e7", 320 | "size": 4768937, 321 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w39a/minecraft.jar" 322 | }, 323 | { 324 | "id": "12w39b", 325 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w39b", 326 | "url": "https://archive.org/download/Minecraft-JSONs/12w39b.json", 327 | "sha1": "620d02bfd74204462a810874f83929d0b8b0b936", 328 | "size": 4766448, 329 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w39b/minecraft.jar" 330 | }, 331 | { 332 | "id": "12w40a", 333 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w40a", 334 | "url": "https://archive.org/download/Minecraft-JSONs/12w40a.json", 335 | "sha1": "434652551e93fdfb4de30cbe64310037777f7eff", 336 | "size": 4884173, 337 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w40a/minecraft.jar" 338 | }, 339 | { 340 | "id": "12w40b", 341 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w40b", 342 | "url": "https://archive.org/download/Minecraft-JSONs/12w40b.json", 343 | "sha1": "1612e0fa6062f764844c5a71ff89660c311f38ae", 344 | "size": 4884732, 345 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w40b/minecraft.jar" 346 | }, 347 | { 348 | "id": "12w41a", 349 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w41a", 350 | "url": "https://archive.org/download/Minecraft-JSONs/12w41a.json", 351 | "sha1": "7327bcd4da0d194565d6ee732b1fa48e8b14b347", 352 | "size": 4900512, 353 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w41a/minecraft.jar" 354 | }, 355 | { 356 | "id": "12w41b", 357 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w41b", 358 | "url": "https://archive.org/download/Minecraft-JSONs/12w41b.json", 359 | "sha1": "d73a5b6919d10689811c11d1c3debcd817050039", 360 | "size": 4900976, 361 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w41b/minecraft.jar" 362 | }, 363 | { 364 | "id": "12w42a", 365 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w42a", 366 | "url": "https://archive.org/download/Minecraft-JSONs/12w42a.json", 367 | "sha1": "0b10f7afbd54392b387a23c34547cb0f30d48998", 368 | "size": 4919860, 369 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w42a/minecraft.jar" 370 | }, 371 | { 372 | "id": "12w42b", 373 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w42b", 374 | "url": "https://archive.org/download/Minecraft-JSONs/12w42b.json", 375 | "sha1": "74024eab7588bd33dd53baa756fd4deb92557b0a", 376 | "size": 4921744, 377 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w42b/minecraft.jar" 378 | }, 379 | { 380 | "id": "12w49a", 381 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w49a", 382 | "url": "https://archive.org/download/Minecraft-JSONs/12w49a.json", 383 | "sha1": "a5a4cf65cf89207eb6ad7371c9237973865eba81", 384 | "size": 4990865, 385 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w49a/minecraft.jar" 386 | }, 387 | { 388 | "id": "12w50a", 389 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w50a", 390 | "url": "https://archive.org/download/Minecraft-JSONs/12w50a.json", 391 | "sha1": "96a6427720aef608a594ed1e0291e77cba398155", 392 | "size": 5004175, 393 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w50a/minecraft.jar" 394 | }, 395 | { 396 | "id": "12w50b", 397 | "wiki": "https://minecraft.wiki/w/Java_Edition_12w50b", 398 | "url": "https://archive.org/download/Minecraft-JSONs/12w50b.json", 399 | "sha1": "73dc6efe46fef478cc5ed123e711872450e193fd", 400 | "size": 5005360, 401 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/12w50b/minecraft.jar" 402 | }, 403 | { 404 | "id": "13w01a", 405 | "wiki": "https://minecraft.wiki/w/Java_Edition_13w01a", 406 | "url": "https://archive.org/download/Minecraft-JSONs/13w01a.json", 407 | "sha1": "e3256fe44cd7c6a1bf45570337e634b030589878", 408 | "size": 5033591, 409 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/13w01a/minecraft.jar" 410 | }, 411 | { 412 | "id": "13w01b", 413 | "wiki": "https://minecraft.wiki/w/Java_Edition_13w01b", 414 | "url": "https://archive.org/download/Minecraft-JSONs/13w01b.json", 415 | "sha1": "87f9f88eb3dcc80dcf818e44af774ab7ff63eb66", 416 | "size": 5035543, 417 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/13w01b/minecraft.jar" 418 | }, 419 | { 420 | "id": "13w02a", 421 | "wiki": "https://minecraft.wiki/w/Java_Edition_13w02a", 422 | "url": "https://archive.org/download/Minecraft-JSONs/13w02a.json", 423 | "sha1": "e9a57e8d5dcddcc9d919054c19b10eb71fcc304e", 424 | "size": 5499864, 425 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/13w02a/minecraft.jar" 426 | }, 427 | { 428 | "id": "13w02b", 429 | "wiki": "https://minecraft.wiki/w/Java_Edition_13w02b", 430 | "url": "https://archive.org/download/Minecraft-JSONs/13w02b.json", 431 | "sha1": "9289953c82ce69ec3d2e59a6044a9c900a99478f", 432 | "size": 5363159, 433 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/13w02b/minecraft.jar" 434 | }, 435 | { 436 | "id": "13w03a", 437 | "wiki": "https://minecraft.wiki/w/Java_Edition_13w03a", 438 | "url": "https://archive.org/download/Minecraft-JSONs/13w03a.json", 439 | "sha1": "6a2d3ffa88b7f5e0949f041193c6525d1c4cc22e", 440 | "size": 6401672, 441 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/13w03a/minecraft.jar" 442 | }, 443 | { 444 | "id": "13w04a", 445 | "wiki": "https://minecraft.wiki/w/Java_Edition_13w04a", 446 | "url": "https://archive.org/download/Minecraft-JSONs/13w04a.json", 447 | "sha1": "dff06285694aab7771682f949d51bca98ce52359", 448 | "size": 6426112, 449 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/13w04a/minecraft.jar" 450 | }, 451 | { 452 | "id": "13w05a", 453 | "wiki": "https://minecraft.wiki/w/Java_Edition_13w05a", 454 | "url": "https://archive.org/download/Minecraft-JSONs/13w05a.json", 455 | "sha1": "7808f090cb92afc8084545dd2ea305773bbd5e6e", 456 | "size": 6442319, 457 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/13w05a/minecraft.jar" 458 | }, 459 | { 460 | "id": "13w05b", 461 | "wiki": "https://minecraft.wiki/w/Java_Edition_13w05b", 462 | "url": "https://archive.org/download/Minecraft-JSONs/13w05b.json", 463 | "sha1": "72074d7cb843229292f71ae917dcefbc0f01461d", 464 | "size": 6442459, 465 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/13w05b/minecraft.jar" 466 | }, 467 | { 468 | "id": "13w06a", 469 | "wiki": "https://minecraft.wiki/w/Java_Edition_13w06a", 470 | "url": "https://archive.org/download/Minecraft-JSONs/13w06a.json", 471 | "sha1": "da409ce9f9c910c08cc729aadc6f592b8ff813cb", 472 | "size": 6445893, 473 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/13w06a/minecraft.jar" 474 | }, 475 | { 476 | "id": "13w07a", 477 | "wiki": "https://minecraft.wiki/w/Java_Edition_13w07a", 478 | "url": "https://archive.org/download/Minecraft-JSONs/13w07a.json", 479 | "sha1": "61f7dad52c34838be7a1e7d37a2370ac847ab87a", 480 | "size": 6510193, 481 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/13w07a/minecraft.jar" 482 | }, 483 | { 484 | "id": "13w09a", 485 | "wiki": "https://minecraft.wiki/w/Java_Edition_13w09a", 486 | "url": "https://archive.org/download/Minecraft-JSONs/13w09a.json", 487 | "sha1": "9ac49c55ca76eedfc985fa245dd0682e08b34982", 488 | "size": 5574252, 489 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/13w09a/minecraft.jar" 490 | }, 491 | { 492 | "id": "13w09b", 493 | "wiki": "https://minecraft.wiki/w/Java_Edition_13w09b", 494 | "url": "https://archive.org/download/Minecraft-JSONs/13w09b.json", 495 | "sha1": "635161d84725b1988f814c890fe5841ad99121e1", 496 | "size": 5578604, 497 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/13w09b/minecraft.jar" 498 | }, 499 | { 500 | "id": "13w09c", 501 | "wiki": "https://minecraft.wiki/w/Java_Edition_13w09c", 502 | "url": "https://archive.org/download/Minecraft-JSONs/13w09c.json", 503 | "sha1": "1367ef1410c2ce7ac0f1c58727aa4883c8677469", 504 | "size": 5533426, 505 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/13w09c/minecraft.jar" 506 | }, 507 | { 508 | "id": "13w10a", 509 | "wiki": "https://minecraft.wiki/w/Java_Edition_13w10a", 510 | "url": "https://archive.org/download/Minecraft-JSONs/13w10a.json", 511 | "sha1": "9162bca3ba8a77da2cd26cda1e46ca89a44bac4a", 512 | "size": 5534991, 513 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/13w10a/minecraft.jar" 514 | }, 515 | { 516 | "id": "13w10b", 517 | "wiki": "https://minecraft.wiki/w/Java_Edition_13w10b", 518 | "url": "https://archive.org/download/Minecraft-JSONs/13w10b.json", 519 | "sha1": "21e35ffe1772d1cf89aea653c7a883acb54b13a3", 520 | "size": 5555235, 521 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/13w10b/minecraft.jar" 522 | }, 523 | { 524 | "id": "13w11a", 525 | "wiki": "https://minecraft.wiki/w/Java_Edition_13w11a", 526 | "url": "https://archive.org/download/Minecraft-JSONs/13w11a.json", 527 | "sha1": "bec6c96bc4413ea3092428aba93d7425fe6a4ea9", 528 | "size": 5556608, 529 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/13w11a/minecraft.jar" 530 | }, 531 | { 532 | "id": "13w12~", 533 | "wiki": "https://minecraft.wiki/w/Java_Edition_13w12~", 534 | "url": "https://archive.org/download/Minecraft-JSONs/13w12~.json", 535 | "sha1": "66d6c6b5205ae1e8f0ad3eb78ccf66500f39c0c7", 536 | "size": 5561634, 537 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/13w12_/minecraft.jar" 538 | }, 539 | { 540 | "id": "b1_8-pre1", 541 | "wiki": "https://minecraft.wiki/w/Java_Edition_b1.8-pre1-2", 542 | "url": "https://archive.org/download/Minecraft-JSONs/b1.8-pre1-2.json", 543 | "sha1": "6789c69ede3aedf83b800c76bea56855d38a0afc", 544 | "size": 1893151, 545 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/1_8-pre/minecraft.jar" 546 | }, 547 | { 548 | "id": "b1_8-pre2", 549 | "wiki": "https://minecraft.wiki/w/Java_Edition_b1.8-pre2", 550 | "url": "https://archive.org/download/Minecraft-JSONs/b1.8-pre2.json", 551 | "sha1": "44191f2895bf1e064269c9279778f2e3e9c3c9c7", 552 | "size": 1897780, 553 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/1_8-pre2/minecraft.jar" 554 | }, 555 | { 556 | "id": "b1_9-pre1", 557 | "wiki": "https://minecraft.wiki/w/Java_Edition_b1.9-pre1", 558 | "url": "https://archive.org/download/Minecraft-JSONs/b1.9-pre1.json", 559 | "sha1": "fdeef0129af130aa00702e53c37c5c4029b7d50e", 560 | "size": 1966908, 561 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/1_9-pre/minecraft.jar" 562 | }, 563 | { 564 | "id": "b1_9-pre2", 565 | "wiki": "https://minecraft.wiki/w/Java_Edition_b1.9-pre2", 566 | "url": "https://archive.org/download/Minecraft-JSONs/b1.9-pre2.json", 567 | "sha1": "b0d40cf43b625631af65e2a645c34b533251da0e", 568 | "size": 1988123, 569 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/1_9-pre2/minecraft.jar" 570 | }, 571 | { 572 | "id": "b1_9-pre3", 573 | "wiki": "https://minecraft.wiki/w/Java_Edition_b1.9-pre3", 574 | "url": "https://archive.org/download/Minecraft-JSONs/b1.9-pre3.json", 575 | "sha1": "5b7fe76a602b7511c97740e36dc25040ccb6e76b", 576 | "size": 2087104, 577 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/1_9-pre3/minecraft.jar" 578 | }, 579 | { 580 | "id": "b1_9-pre4", 581 | "wiki": "https://minecraft.wiki/w/Java_Edition_b1.9-pre4", 582 | "url": "https://archive.org/download/Minecraft-JSONs/b1.9-pre4.json", 583 | "sha1": "5c4831d9705f2e00e3cd993e89b822636492932a", 584 | "size": 2147107, 585 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/1_9-pre4/minecraft.jar" 586 | }, 587 | { 588 | "id": "b1_9-pre5", 589 | "wiki": "https://minecraft.wiki/w/Java_Edition_b1.9-pre5", 590 | "url": "https://archive.org/download/Minecraft-JSONs/b1.9-pre5.json", 591 | "sha1": "e109b297d2c4ee7a0bd6aed72f38f7e3185654cf", 592 | "size": 2211261, 593 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/1_9-pre5/minecraft.jar" 594 | }, 595 | { 596 | "id": "b1_9-pre6", 597 | "wiki": "https://minecraft.wiki/w/Java_Edition_b1.9-pre6", 598 | "url": "https://archive.org/download/Minecraft-JSONs/b1.9-pre6.json", 599 | "sha1": "f0983e65cd1c0768b0d1fec471ce4f69173b8126", 600 | "size": 2239270, 601 | "jar": "https://archive.org/download/assets.minecraft.net-2013-11-13/assets.minecraft.net/1_9-pre6/minecraft.jar" 602 | } 603 | ] 604 | } 605 | --------------------------------------------------------------------------------