1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4use crate::error::Result;
5
6const VERSION_MANIFEST_URL: &str =
7 "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
8
9#[derive(Debug, Deserialize, Serialize, Clone)]
10pub struct VersionManifest {
11 pub latest: Latest,
12 pub versions: Vec<VersionEntry>,
13}
14
15#[derive(Debug, Deserialize, Serialize, Clone)]
16pub struct Latest {
17 pub release: String,
18 pub snapshot: String,
19}
20
21#[derive(Debug, Deserialize, Serialize, Clone)]
22#[serde(rename_all = "camelCase")]
23pub struct VersionEntry {
24 pub id: String,
25 #[serde(rename = "type")]
26 pub version_type: String,
27 pub url: String,
28 pub sha1: String,
29 pub release_time: String,
30}
31
32#[derive(Debug, Deserialize, Serialize, Clone)]
33#[serde(rename_all = "camelCase")]
34pub struct VersionJson {
35 pub id: String,
36 pub main_class: String,
37 pub asset_index: AssetIndex,
38 pub assets: String,
39 pub java_version: JavaVersionInfo,
40 pub libraries: Vec<Library>,
41 pub arguments: Option<Arguments>,
42 pub minecraft_arguments: Option<String>,
44 pub downloads: ClientDownloads,
45}
46
47#[derive(Debug, Deserialize, Serialize, Clone)]
48pub struct AssetIndex {
49 pub id: String,
50 pub sha1: String,
51 pub url: String,
52 pub size: u64,
53}
54
55#[derive(Debug, Deserialize, Serialize, Clone)]
56#[serde(rename_all = "camelCase")]
57pub struct JavaVersionInfo {
58 pub major_version: u32,
59}
60
61#[derive(Debug, Deserialize, Serialize, Clone)]
62pub struct Arguments {
63 pub jvm: Vec<Argument>,
64 pub game: Vec<Argument>,
65}
66
67#[derive(Debug, Deserialize, Serialize, Clone)]
69#[serde(untagged)]
70pub enum Argument {
71 Simple(String),
72 Conditional {
73 rules: Vec<Rule>,
74 value: ArgumentValue,
75 },
76}
77
78#[derive(Debug, Deserialize, Serialize, Clone)]
79#[serde(untagged)]
80pub enum ArgumentValue {
81 Single(String),
82 Multiple(Vec<String>),
83}
84
85#[derive(Debug, Deserialize, Serialize, Clone)]
86pub struct Rule {
87 pub action: String,
88 pub os: Option<OsRule>,
89 pub features: Option<HashMap<String, bool>>,
90}
91
92#[derive(Debug, Deserialize, Serialize, Clone)]
93pub struct OsRule {
94 pub name: Option<String>,
95 pub arch: Option<String>,
96 pub version: Option<String>,
97}
98
99#[derive(Debug, Deserialize, Serialize, Clone)]
100pub struct Library {
101 pub name: String,
102 pub downloads: Option<LibraryDownloads>,
103 pub rules: Option<Vec<Rule>>,
104 pub natives: Option<HashMap<String, String>>,
105 pub url: Option<String>,
106}
107
108#[derive(Debug, Deserialize, Serialize, Clone)]
109pub struct LibraryDownloads {
110 pub artifact: Option<LibraryArtifact>,
111 pub classifiers: Option<HashMap<String, LibraryArtifact>>,
112}
113
114#[derive(Debug, Deserialize, Serialize, Clone)]
115pub struct LibraryArtifact {
116 pub path: String,
117 pub sha1: String,
118 pub url: String,
119 pub size: u64,
120}
121
122#[derive(Debug, Deserialize, Serialize, Clone)]
123pub struct ClientDownloads {
124 pub client: DownloadEntry,
125 pub server: Option<DownloadEntry>,
126}
127
128#[derive(Debug, Deserialize, Serialize, Clone)]
129pub struct DownloadEntry {
130 pub sha1: String,
131 pub size: u64,
132 pub url: String,
133}
134
135#[derive(Debug, Deserialize, Serialize, Clone)]
136pub struct AssetIndexData {
137 pub objects: HashMap<String, AssetObject>,
138}
139
140#[derive(Debug, Deserialize, Serialize, Clone)]
141pub struct AssetObject {
142 pub hash: String,
143 pub size: u64,
144}
145
146pub fn check_library_rule(rules: &[Rule]) -> bool {
148 let current_os = current_os_name();
149 let mut allowed = false;
150
151 for rule in rules {
152 if rule.features.is_some() {
155 continue;
156 }
157
158 let matches = if let Some(os) = &rule.os {
159 os.name.as_deref().map_or(true, |n| n == current_os)
160 } else {
161 true
162 };
163
164 if matches {
165 allowed = rule.action == "allow";
166 }
167 }
168 allowed
169}
170
171pub fn check_jvm_rule(rules: &[Rule]) -> bool {
173 let current_os = current_os_name();
174 let current_arch = current_arch();
175 let mut allowed = false;
176
177 for rule in rules {
178 if rule.features.is_some() {
180 continue;
181 }
182
183 let os_matches = if let Some(os) = &rule.os {
184 let name_ok = os.name.as_deref().map_or(true, |n| n == current_os);
185 let arch_ok = os.arch.as_deref().map_or(true, |a| a == current_arch);
186 name_ok && arch_ok
187 } else {
188 true
189 };
190
191 if os_matches {
192 allowed = rule.action == "allow";
193 }
194 }
195 allowed
196}
197
198fn current_os_name() -> &'static str {
199 if cfg!(target_os = "windows") {
200 "windows"
201 } else if cfg!(target_os = "macos") {
202 "osx"
203 } else {
204 "linux"
205 }
206}
207
208fn current_arch() -> &'static str {
209 if cfg!(target_arch = "x86_64") {
210 "x64"
211 } else if cfg!(target_arch = "aarch64") {
212 "arm64"
213 } else {
214 "x86"
215 }
216}
217
218pub async fn fetch_version_manifest() -> Result<VersionManifest> {
219 let client = reqwest::Client::new();
220 let manifest = client
221 .get(VERSION_MANIFEST_URL)
222 .send()
223 .await?
224 .json::<VersionManifest>()
225 .await?;
226 Ok(manifest)
227}
228
229pub async fn fetch_version_json(url: &str) -> Result<VersionJson> {
230 let client = reqwest::Client::new();
231 let json = client
232 .get(url)
233 .send()
234 .await?
235 .json::<VersionJson>()
236 .await?;
237 Ok(json)
238}
239
240pub async fn fetch_asset_index(url: &str) -> Result<AssetIndexData> {
241 let client = reqwest::Client::new();
242 let data = client
243 .get(url)
244 .send()
245 .await?
246 .json::<AssetIndexData>()
247 .await?;
248 Ok(data)
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 fn allow_rule(os_name: &str) -> Rule {
256 Rule {
257 action: "allow".to_string(),
258 os: Some(OsRule {
259 name: Some(os_name.to_string()),
260 arch: None,
261 version: None,
262 }),
263 features: None,
264 }
265 }
266
267 fn disallow_rule(os_name: &str) -> Rule {
268 Rule {
269 action: "disallow".to_string(),
270 os: Some(OsRule {
271 name: Some(os_name.to_string()),
272 arch: None,
273 version: None,
274 }),
275 features: None,
276 }
277 }
278
279 fn allow_all_rule() -> Rule {
280 Rule {
281 action: "allow".to_string(),
282 os: None,
283 features: None,
284 }
285 }
286
287 #[test]
288 fn allow_rule_matches_current_os() {
289 let os = if cfg!(target_os = "windows") { "windows" }
290 else if cfg!(target_os = "macos") { "osx" }
291 else { "linux" };
292 assert!(check_library_rule(&[allow_rule(os)]));
293 }
294
295 #[test]
296 fn allow_rule_does_not_match_other_os() {
297 let other = if cfg!(target_os = "windows") { "osx" } else { "windows" };
299 assert!(!check_library_rule(&[allow_rule(other)]));
301 }
302
303 #[test]
304 fn allow_all_then_disallow_current() {
305 let os = if cfg!(target_os = "windows") { "windows" }
306 else if cfg!(target_os = "macos") { "osx" }
307 else { "linux" };
308 let rules = vec![allow_all_rule(), disallow_rule(os)];
310 assert!(!check_library_rule(&rules));
311 }
312
313 #[test]
314 fn empty_rules_not_allowed() {
315 assert!(!check_library_rule(&[]));
317 }
318
319 #[tokio::test]
320 #[ignore = "requires network"]
321 async fn fetch_manifest_returns_versions() {
322 let manifest = fetch_version_manifest().await.unwrap();
323 assert!(!manifest.versions.is_empty());
324 assert!(!manifest.latest.release.is_empty());
325 }
326
327 #[tokio::test]
328 #[ignore = "requires network"]
329 async fn fetch_known_version_json() {
330 let manifest = fetch_version_manifest().await.unwrap();
331 let entry = manifest.versions.iter().find(|v| v.id == "1.21.4").unwrap();
332 let json = fetch_version_json(&entry.url).await.unwrap();
333 assert_eq!(json.id, "1.21.4");
334 assert!(!json.libraries.is_empty());
335 assert!(json.java_version.major_version >= 21);
336 }
337}