1use std::path::Path;
2
3use crate::downloaders::{Downloader, FileDownloader, get_lib_path};
4use crate::error::Result;
5
6use crate::downloaders::get_index_path;
7use crate::{downloaders::DownloadableObject, version_checker::VersionCheckResult};
8
9pub struct InstallationFixer {
10 data: Vec<DownloadableObject>,
11}
12
13impl InstallationFixer {
14 pub fn new(check_result: VersionCheckResult, installation_path: impl AsRef<Path>) -> Self {
15 let installation_path = installation_path.as_ref();
16 let mut fixer = Self { data: vec![] };
17 fixer.add_objects(&check_result, installation_path);
18 fixer.add_libs(&check_result, installation_path);
19 fixer.add_index(&check_result, installation_path);
20 fixer
21 }
22
23 pub async fn fix_installation(&mut self) -> Result<()> {
24 let files = std::mem::take(&mut self.data);
25
26 let mut downloader = Downloader::new(files);
27 downloader.complete().await
28 }
29
30 fn add_objects(&mut self, check_result: &VersionCheckResult, installation_path: &Path) {
31 self.data.extend(
32 check_result
33 .objects
34 .iter()
35 .map(|&f| DownloadableObject::from(f))
36 .map(|mut f| {
37 f.path = installation_path.join(f.path);
38 f
39 }),
40 );
41 }
42
43 fn add_libs(&mut self, check_result: &VersionCheckResult, installation_path: &Path) {
44 self.data.extend(
45 check_result
46 .libs
47 .iter()
48 .map(|&f| DownloadableObject::from(f))
49 .map(|mut f| {
50 f.path = get_lib_path(installation_path, &f.path);
51 f
52 }),
53 );
54 }
55
56 fn add_index(&mut self, check_result: &VersionCheckResult, installation_path: &Path) {
57 if let Some(mut idx) = check_result.index.map(DownloadableObject::from) {
58 idx.path = get_index_path(installation_path, &idx.path);
59 self.data.push(idx);
60 }
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use crate::mine_data_structs::minecraft::{Artifact, AssetIndex, Library, LibraryDownloads, ObjectData};
68
69 fn make_library(path: &str, sha1: &str) -> Library {
70 Library {
71 name: "test:lib:1.0".into(),
72 downloads: Some(LibraryDownloads {
73 artifact: Artifact {
74 path: path.into(),
75 sha1: sha1.into(),
76 size: 1024,
77 url: "https://example.com/lib.jar".into(),
78 },
79 }),
80 rules: None,
81 }
82 }
83
84 fn make_object(hash: &str) -> ObjectData {
85 ObjectData { hash: hash.into(), size: 512 }
86 }
87
88 fn make_index(id: &str, sha1: &str) -> AssetIndex {
89 AssetIndex {
90 id: id.into(),
91 sha1: sha1.into(),
92 size: 4096,
93 total_size: 100_000,
94 url: "https://example.com/index.json".into(),
95 }
96 }
97
98 #[test]
99 fn adds_objects() {
100 let obj = make_object("a1b2c3d4e5f6");
101 let result = VersionCheckResult {
102 objects: Box::new([&obj]),
103 libs: Box::new([]),
104 index: None,
105 client: None,
106 };
107 let fixer = InstallationFixer::new(result, "/tmp/test");
108
109 assert_eq!(fixer.data.len(), 1);
110 let entry = &fixer.data[0];
111 assert!(
112 entry.path.starts_with("/tmp/test"),
113 "expected path to start with installation path, got: {:?}",
114 entry.path,
115 );
116 assert!(
117 entry.path.to_string_lossy().contains("a1b2c3d4e5f6"),
118 "expected path to contain the object hash, got: {:?}",
119 entry.path,
120 );
121 assert!(
122 entry.url.contains("resources.download.minecraft.net"),
123 "expected URL to be a Minecraft resource URL, got: {}",
124 entry.url,
125 );
126 assert!(entry.hash.is_some());
127 }
128
129 #[test]
130 fn adds_libs() {
131 let lib = make_library("net/minecraft/client/1.21/client.jar", "ffgg1122");
132 let result = VersionCheckResult {
133 objects: Box::new([]),
134 libs: Box::new([&lib]),
135 index: None,
136 client: None,
137 };
138 let fixer = InstallationFixer::new(result, "/tmp/test");
139
140 assert_eq!(fixer.data.len(), 1);
141 let entry = &fixer.data[0];
142 assert!(
143 entry.path.starts_with("/tmp/test/libraries"),
144 "expected path under libraries/, got: {:?}",
145 entry.path,
146 );
147 assert!(
148 entry.path.to_string_lossy().contains("net/minecraft/client/1.21/client.jar"),
149 "expected path to contain artifact path, got: {:?}",
150 entry.path,
151 );
152 assert_eq!(entry.url, "https://example.com/lib.jar");
153 assert!(entry.hash.is_some());
154 }
155
156 #[test]
157 fn all_sources_accumulate() {
158 let obj = make_object("aaa111");
159 let lib = make_library("org/example/foo/1.0/foo.jar", "bbb222");
160 let idx = make_index("1.21", "ccc333");
161 let result = VersionCheckResult {
162 objects: Box::new([&obj]),
163 libs: Box::new([&lib]),
164 index: Some(&idx),
165 client: None,
166 };
167 let fixer = InstallationFixer::new(result, "/tmp/test");
168
169 assert_eq!(fixer.data.len(), 3);
170 let paths: Vec<_> = fixer.data.iter().map(|d| d.path.to_string_lossy().to_string()).collect();
171 assert!(paths.iter().any(|p| p.contains("aaa111")), "object missing");
172 assert!(paths.iter().any(|p| p.contains("org/example")), "library missing");
173 assert!(paths.iter().any(|p| p.contains("1.21")), "index id missing");
174 }
175
176 #[tokio::test]
177 async fn fix_installation_empty_returns_ok() {
178 let result = VersionCheckResult {
179 objects: Box::new([]),
180 libs: Box::new([]),
181 index: None,
182 client: None,
183 };
184 let mut fixer = InstallationFixer::new(result, "/tmp/test");
185
186 let outcome = fixer.fix_installation().await;
187
188 assert!(outcome.is_ok(), "expected Ok(()) with no downloads, got: {:?}", outcome);
189 assert!(fixer.data.is_empty());
190 }
191}