minecraft_java_rs_core/game/
bundle.rs1use std::path::PathBuf;
2use std::sync::atomic::{AtomicUsize, Ordering};
3use std::sync::Arc;
4
5use tokio::sync::mpsc::Sender;
6use tokio::sync::Semaphore;
7use tokio::task::JoinSet;
8
9use crate::error::LaunchError;
10use crate::launcher::events::LaunchEvent;
11use crate::models::minecraft::AssetItem;
12use crate::net::downloader::DownloadItem;
13use crate::utils::hash::{get_file_hash, HashAlgorithm};
14
15pub fn get_total_size(bundle: &[AssetItem]) -> u64 {
19 bundle
20 .iter()
21 .map(|item| match item {
22 AssetItem::Asset { size, .. } | AssetItem::NativeAsset { size, .. } => *size,
23 AssetItem::CFile { .. } => 0,
24 })
25 .sum()
26}
27
28pub async fn check_bundle(
35 bundle: &[AssetItem],
36 event_tx: &Sender<LaunchEvent>,
37 concurrency: u32,
38) -> Result<Vec<DownloadItem>, LaunchError> {
39 let total = bundle.len();
40 let semaphore = Arc::new(Semaphore::new(concurrency as usize));
41 let counter = Arc::new(AtomicUsize::new(0));
42 let mut tasks: JoinSet<Result<Option<DownloadItem>, LaunchError>> = JoinSet::new();
43
44 for item in bundle.iter().cloned() {
45 let sem = Arc::clone(&semaphore);
46 let tx = event_tx.clone();
47 let counter = Arc::clone(&counter);
48
49 tasks.spawn(async move {
50 let _permit = sem.acquire().await.unwrap();
51
52 let current = counter.fetch_add(1, Ordering::Relaxed) + 1;
53 let _ = tx
54 .send(LaunchEvent::Check {
55 current,
56 total,
57 kind: "bundle".into(),
58 })
59 .await;
60
61 match item {
62 AssetItem::CFile { path, content } => {
63 let dest = PathBuf::from(&path);
64 if let Some(parent) = dest.parent() {
65 tokio::fs::create_dir_all(parent).await?;
66 }
67 if !dest.exists() {
68 tokio::fs::write(&dest, content).await?;
69 }
70 Ok(None)
71 }
72
73 AssetItem::Asset { ref path, ref sha1, size, ref url }
74 | AssetItem::NativeAsset { ref path, ref sha1, size, ref url } => {
75 let dest = PathBuf::from(path);
76 let needs_download = if dest.exists() {
77 if sha1.is_empty() {
78 false
79 } else {
80 let dest_clone = dest.clone();
81 let expected = sha1.clone();
82 tokio::task::spawn_blocking(move || -> bool {
83 match get_file_hash(&dest_clone, HashAlgorithm::Sha1) {
84 Ok(actual) => actual != expected,
85 Err(_) => true,
86 }
87 })
88 .await
89 .unwrap_or(true)
90 }
91 } else {
92 true
93 };
94
95 if needs_download {
96 let folder = dest
97 .parent()
98 .map(|p| p.to_path_buf())
99 .unwrap_or_else(|| PathBuf::from("."));
100
101 let kind = match item {
102 AssetItem::NativeAsset { .. } => "natives",
103 _ => "assets",
104 };
105
106 Ok(Some(DownloadItem {
107 url: url.clone(),
108 path: dest.clone(),
109 folder,
110 name: dest
111 .file_name()
112 .map(|n| n.to_string_lossy().into_owned())
113 .unwrap_or_default(),
114 size,
115 r#type: Some(kind.into()),
116 sha1: Some(sha1.clone()),
117 }))
118 } else {
119 Ok(None)
120 }
121 }
122 }
123 });
124 }
125
126 let mut pending: Vec<DownloadItem> = Vec::new();
127 while let Some(result) = tasks.join_next().await {
128 match result {
129 Ok(Ok(Some(item))) => pending.push(item),
130 Ok(Ok(None)) => {}
131 Ok(Err(e)) => return Err(e),
132 Err(e) => return Err(LaunchError::Io(std::io::Error::new(
133 std::io::ErrorKind::Other,
134 e.to_string(),
135 ))),
136 }
137 }
138
139 Ok(pending)
140}
141
142pub async fn check_files(
149 bundle: &[AssetItem],
150 event_tx: &Sender<LaunchEvent>,
151 concurrency: u32,
152) -> Result<Vec<String>, LaunchError> {
153 let items: Vec<(PathBuf, String)> = bundle
154 .iter()
155 .filter_map(|item| match item {
156 AssetItem::Asset { path, sha1, .. } | AssetItem::NativeAsset { path, sha1, .. } => {
157 let p = PathBuf::from(path);
158 if p.exists() && !sha1.is_empty() {
159 Some((p, sha1.clone()))
160 } else {
161 None
162 }
163 }
164 AssetItem::CFile { .. } => None,
165 })
166 .collect();
167
168 let total = items.len();
169 let semaphore = Arc::new(Semaphore::new(concurrency as usize));
170 let counter = Arc::new(AtomicUsize::new(0));
171 let mut tasks: JoinSet<Result<Option<String>, LaunchError>> = JoinSet::new();
172
173 for (path, expected_sha1) in items {
174 let sem = Arc::clone(&semaphore);
175 let tx = event_tx.clone();
176 let counter = Arc::clone(&counter);
177
178 tasks.spawn(async move {
179 let _permit = sem.acquire().await.unwrap();
180
181 let current = counter.fetch_add(1, Ordering::Relaxed) + 1;
182 let _ = tx
183 .send(LaunchEvent::Check {
184 current,
185 total,
186 kind: "verify".into(),
187 })
188 .await;
189
190 let path_clone = path.clone();
191 let actual = tokio::task::spawn_blocking(move || {
192 get_file_hash(&path_clone, HashAlgorithm::Sha1)
193 })
194 .await
195 .map_err(|e| LaunchError::Io(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())))??;
196
197 if actual != expected_sha1 {
198 Ok(Some(path.to_string_lossy().into_owned()))
199 } else {
200 Ok(None)
201 }
202 });
203 }
204
205 let mut bad: Vec<String> = Vec::new();
206 while let Some(result) = tasks.join_next().await {
207 match result {
208 Ok(Ok(Some(path))) => bad.push(path),
209 Ok(Ok(None)) => {}
210 Ok(Err(e)) => return Err(e),
211 Err(e) => {
212 return Err(LaunchError::Io(std::io::Error::new(
213 std::io::ErrorKind::Other,
214 e.to_string(),
215 )))
216 }
217 }
218 }
219
220 Ok(bad)
221}
222
223#[cfg(test)]
226mod tests {
227 use super::*;
228 use tempfile::TempDir;
229 use tokio::sync::mpsc;
230
231 fn asset(path: &str, sha1: &str, size: u64, url: &str) -> AssetItem {
232 AssetItem::Asset {
233 path: path.into(),
234 sha1: sha1.into(),
235 size,
236 url: url.into(),
237 }
238 }
239
240 fn cfile(path: &str, content: &str) -> AssetItem {
241 AssetItem::CFile {
242 path: path.into(),
243 content: content.into(),
244 }
245 }
246
247 #[test]
248 fn get_total_size_sums_assets() {
249 let bundle = vec![
250 asset("/a", "aa", 100, "http://x"),
251 asset("/b", "bb", 200, "http://x"),
252 cfile("/c", "data"),
253 ];
254 assert_eq!(get_total_size(&bundle), 300);
255 }
256
257 #[test]
258 fn get_total_size_empty() {
259 assert_eq!(get_total_size(&[]), 0);
260 }
261
262 #[tokio::test]
263 async fn check_bundle_writes_cfile() {
264 let dir = TempDir::new().unwrap();
265 let path = dir.path().join("indexes").join("test.json");
266 let bundle = vec![cfile(
267 &path.to_string_lossy(),
268 r#"{"objects":{}}"#,
269 )];
270 let (tx, _rx) = mpsc::channel(16);
271 let pending = check_bundle(&bundle, &tx, 4).await.unwrap();
272 assert!(pending.is_empty());
273 assert!(path.exists());
274 let written = std::fs::read_to_string(&path).unwrap();
275 assert_eq!(written, r#"{"objects":{}}"#);
276 }
277
278 #[tokio::test]
279 async fn check_bundle_skips_existing_cfile() {
280 let dir = TempDir::new().unwrap();
281 let path = dir.path().join("file.json");
282 tokio::fs::write(&path, b"original").await.unwrap();
283
284 let bundle = vec![cfile(&path.to_string_lossy(), "new content")];
285 let (tx, _rx) = mpsc::channel(16);
286 check_bundle(&bundle, &tx, 4).await.unwrap();
287
288 let content = std::fs::read_to_string(&path).unwrap();
289 assert_eq!(content, "original");
290 }
291
292 #[tokio::test]
293 async fn check_bundle_missing_asset_added_to_pending() {
294 let dir = TempDir::new().unwrap();
295 let path = dir.path().join("missing.jar");
296 let bundle = vec![asset(&path.to_string_lossy(), "deadbeef", 42, "http://example.com/a.jar")];
297
298 let (tx, _rx) = mpsc::channel(16);
299 let pending = check_bundle(&bundle, &tx, 4).await.unwrap();
300 assert_eq!(pending.len(), 1);
301 assert_eq!(pending[0].url, "http://example.com/a.jar");
302 }
303
304 #[tokio::test]
305 async fn check_bundle_correct_hash_skips_download() {
306 use sha1::{Digest, Sha1};
307
308 let dir = TempDir::new().unwrap();
309 let path = dir.path().join("asset.dat");
310 let content = b"hello world";
311 tokio::fs::write(&path, content).await.unwrap();
312
313 let mut hasher = Sha1::new();
314 hasher.update(content);
315 let sha1 = format!("{:x}", hasher.finalize());
316
317 let bundle = vec![asset(&path.to_string_lossy(), &sha1, 11, "http://example.com/x")];
318 let (tx, _rx) = mpsc::channel(16);
319 let pending = check_bundle(&bundle, &tx, 4).await.unwrap();
320 assert!(pending.is_empty());
321 }
322
323 #[tokio::test]
324 async fn check_bundle_wrong_hash_queues_download() {
325 let dir = TempDir::new().unwrap();
326 let path = dir.path().join("asset.dat");
327 tokio::fs::write(&path, b"stale content").await.unwrap();
328
329 let bundle = vec![asset(&path.to_string_lossy(), "0000000000000000000000000000000000000000", 13, "http://example.com/x")];
330 let (tx, _rx) = mpsc::channel(16);
331 let pending = check_bundle(&bundle, &tx, 4).await.unwrap();
332 assert_eq!(pending.len(), 1);
333 }
334
335 #[tokio::test]
336 async fn check_files_returns_empty_for_correct_files() {
337 use sha1::{Digest, Sha1};
338
339 let dir = TempDir::new().unwrap();
340 let path = dir.path().join("lib.jar");
341 let content = b"jar content";
342 tokio::fs::write(&path, content).await.unwrap();
343
344 let mut hasher = Sha1::new();
345 hasher.update(content);
346 let sha1 = format!("{:x}", hasher.finalize());
347
348 let bundle = vec![asset(&path.to_string_lossy(), &sha1, 11, "http://x")];
349 let (tx, _rx) = mpsc::channel(16);
350 let bad = check_files(&bundle, &tx, 4).await.unwrap();
351 assert!(bad.is_empty());
352 }
353
354 #[tokio::test]
355 async fn check_files_reports_corrupted_file() {
356 let dir = TempDir::new().unwrap();
357 let path = dir.path().join("lib.jar");
358 tokio::fs::write(&path, b"corrupted").await.unwrap();
359
360 let bundle = vec![asset(&path.to_string_lossy(), "0000000000000000000000000000000000000000", 9, "http://x")];
361 let (tx, _rx) = mpsc::channel(16);
362 let bad = check_files(&bundle, &tx, 4).await.unwrap();
363 assert_eq!(bad.len(), 1);
364 }
365
366 #[tokio::test]
367 async fn check_files_skips_missing_files() {
368 let dir = TempDir::new().unwrap();
369 let path = dir.path().join("nonexistent.jar");
370 let bundle = vec![asset(&path.to_string_lossy(), "abc", 0, "http://x")];
371
372 let (tx, _rx) = mpsc::channel(16);
373 let bad = check_files(&bundle, &tx, 4).await.unwrap();
374 assert!(bad.is_empty());
375 }
376}