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 {
74 ref path,
75 ref sha1,
76 size,
77 ref url,
78 }
79 | AssetItem::NativeAsset {
80 ref path,
81 ref sha1,
82 size,
83 ref url,
84 } => {
85 let dest = PathBuf::from(path);
86 let needs_download = if dest.exists() {
87 if sha1.is_empty() {
88 false
89 } else {
90 let dest_clone = dest.clone();
91 let expected = sha1.clone();
92 tokio::task::spawn_blocking(move || -> bool {
93 match get_file_hash(&dest_clone, HashAlgorithm::Sha1) {
94 Ok(actual) => actual != expected,
95 Err(_) => true,
96 }
97 })
98 .await
99 .unwrap_or(true)
100 }
101 } else {
102 true
103 };
104
105 if needs_download {
106 let folder = dest
107 .parent()
108 .map(|p| p.to_path_buf())
109 .unwrap_or_else(|| PathBuf::from("."));
110
111 let kind = match item {
112 AssetItem::NativeAsset { .. } => "natives",
113 _ => "assets",
114 };
115
116 Ok(Some(DownloadItem {
117 url: url.clone(),
118 path: dest.clone(),
119 folder,
120 name: dest
121 .file_name()
122 .map(|n| n.to_string_lossy().into_owned())
123 .unwrap_or_default(),
124 size,
125 r#type: Some(kind.into()),
126 sha1: if sha1.is_empty() {
127 None
128 } else {
129 Some(sha1.clone())
130 },
131 }))
132 } else {
133 Ok(None)
134 }
135 }
136 }
137 });
138 }
139
140 let mut pending: Vec<DownloadItem> = Vec::new();
141 while let Some(result) = tasks.join_next().await {
142 match result {
143 Ok(Ok(Some(item))) => pending.push(item),
144 Ok(Ok(None)) => {}
145 Ok(Err(e)) => return Err(e),
146 Err(e) => {
147 return Err(LaunchError::Io(std::io::Error::new(
148 std::io::ErrorKind::Other,
149 e.to_string(),
150 )))
151 }
152 }
153 }
154
155 Ok(pending)
156}
157
158pub async fn check_files(
165 bundle: &[AssetItem],
166 event_tx: &Sender<LaunchEvent>,
167 concurrency: u32,
168) -> Result<Vec<String>, LaunchError> {
169 let items: Vec<(PathBuf, String)> = bundle
170 .iter()
171 .filter_map(|item| match item {
172 AssetItem::Asset { path, sha1, .. } | AssetItem::NativeAsset { path, sha1, .. } => {
173 let p = PathBuf::from(path);
174 if p.exists() && !sha1.is_empty() {
175 Some((p, sha1.clone()))
176 } else {
177 None
178 }
179 }
180 AssetItem::CFile { .. } => None,
181 })
182 .collect();
183
184 let total = items.len();
185 let semaphore = Arc::new(Semaphore::new(concurrency as usize));
186 let counter = Arc::new(AtomicUsize::new(0));
187 let mut tasks: JoinSet<Result<Option<String>, LaunchError>> = JoinSet::new();
188
189 for (path, expected_sha1) in items {
190 let sem = Arc::clone(&semaphore);
191 let tx = event_tx.clone();
192 let counter = Arc::clone(&counter);
193
194 tasks.spawn(async move {
195 let _permit = sem.acquire().await.unwrap();
196
197 let current = counter.fetch_add(1, Ordering::Relaxed) + 1;
198 let _ = tx
199 .send(LaunchEvent::Check {
200 current,
201 total,
202 kind: "verify".into(),
203 })
204 .await;
205
206 let path_clone = path.clone();
207 let actual = tokio::task::spawn_blocking(move || {
208 get_file_hash(&path_clone, HashAlgorithm::Sha1)
209 })
210 .await
211 .map_err(|e| {
212 LaunchError::Io(std::io::Error::new(
213 std::io::ErrorKind::Other,
214 e.to_string(),
215 ))
216 })??;
217
218 if actual != expected_sha1 {
219 Ok(Some(path.to_string_lossy().into_owned()))
220 } else {
221 Ok(None)
222 }
223 });
224 }
225
226 let mut bad: Vec<String> = Vec::new();
227 while let Some(result) = tasks.join_next().await {
228 match result {
229 Ok(Ok(Some(path))) => bad.push(path),
230 Ok(Ok(None)) => {}
231 Ok(Err(e)) => return Err(e),
232 Err(e) => {
233 return Err(LaunchError::Io(std::io::Error::new(
234 std::io::ErrorKind::Other,
235 e.to_string(),
236 )))
237 }
238 }
239 }
240
241 Ok(bad)
242}
243
244#[cfg(test)]
247mod tests {
248 use super::*;
249 use tempfile::TempDir;
250 use tokio::sync::mpsc;
251
252 fn asset(path: &str, sha1: &str, size: u64, url: &str) -> AssetItem {
253 AssetItem::Asset {
254 path: path.into(),
255 sha1: sha1.into(),
256 size,
257 url: url.into(),
258 }
259 }
260
261 fn cfile(path: &str, content: &str) -> AssetItem {
262 AssetItem::CFile {
263 path: path.into(),
264 content: content.into(),
265 }
266 }
267
268 #[test]
269 fn get_total_size_sums_assets() {
270 let bundle = vec![
271 asset("/a", "aa", 100, "http://x"),
272 asset("/b", "bb", 200, "http://x"),
273 cfile("/c", "data"),
274 ];
275 assert_eq!(get_total_size(&bundle), 300);
276 }
277
278 #[test]
279 fn get_total_size_empty() {
280 assert_eq!(get_total_size(&[]), 0);
281 }
282
283 #[tokio::test]
284 async fn check_bundle_writes_cfile() {
285 let dir = TempDir::new().unwrap();
286 let path = dir.path().join("indexes").join("test.json");
287 let bundle = vec![cfile(&path.to_string_lossy(), r#"{"objects":{}}"#)];
288 let (tx, _rx) = mpsc::channel(16);
289 let pending = check_bundle(&bundle, &tx, 4).await.unwrap();
290 assert!(pending.is_empty());
291 assert!(path.exists());
292 let written = std::fs::read_to_string(&path).unwrap();
293 assert_eq!(written, r#"{"objects":{}}"#);
294 }
295
296 #[tokio::test]
297 async fn check_bundle_skips_existing_cfile() {
298 let dir = TempDir::new().unwrap();
299 let path = dir.path().join("file.json");
300 tokio::fs::write(&path, b"original").await.unwrap();
301
302 let bundle = vec![cfile(&path.to_string_lossy(), "new content")];
303 let (tx, _rx) = mpsc::channel(16);
304 check_bundle(&bundle, &tx, 4).await.unwrap();
305
306 let content = std::fs::read_to_string(&path).unwrap();
307 assert_eq!(content, "original");
308 }
309
310 #[tokio::test]
311 async fn check_bundle_missing_asset_added_to_pending() {
312 let dir = TempDir::new().unwrap();
313 let path = dir.path().join("missing.jar");
314 let bundle = vec![asset(
315 &path.to_string_lossy(),
316 "deadbeef",
317 42,
318 "http://example.com/a.jar",
319 )];
320
321 let (tx, _rx) = mpsc::channel(16);
322 let pending = check_bundle(&bundle, &tx, 4).await.unwrap();
323 assert_eq!(pending.len(), 1);
324 assert_eq!(pending[0].url, "http://example.com/a.jar");
325 }
326
327 #[tokio::test]
328 async fn check_bundle_correct_hash_skips_download() {
329 use sha1::{Digest, Sha1};
330
331 let dir = TempDir::new().unwrap();
332 let path = dir.path().join("asset.dat");
333 let content = b"hello world";
334 tokio::fs::write(&path, content).await.unwrap();
335
336 let mut hasher = Sha1::new();
337 hasher.update(content);
338 let sha1 = format!("{:x}", hasher.finalize());
339
340 let bundle = vec![asset(
341 &path.to_string_lossy(),
342 &sha1,
343 11,
344 "http://example.com/x",
345 )];
346 let (tx, _rx) = mpsc::channel(16);
347 let pending = check_bundle(&bundle, &tx, 4).await.unwrap();
348 assert!(pending.is_empty());
349 }
350
351 #[tokio::test]
352 async fn check_bundle_wrong_hash_queues_download() {
353 let dir = TempDir::new().unwrap();
354 let path = dir.path().join("asset.dat");
355 tokio::fs::write(&path, b"stale content").await.unwrap();
356
357 let bundle = vec![asset(
358 &path.to_string_lossy(),
359 "0000000000000000000000000000000000000000",
360 13,
361 "http://example.com/x",
362 )];
363 let (tx, _rx) = mpsc::channel(16);
364 let pending = check_bundle(&bundle, &tx, 4).await.unwrap();
365 assert_eq!(pending.len(), 1);
366 }
367
368 #[tokio::test]
369 async fn check_files_returns_empty_for_correct_files() {
370 use sha1::{Digest, Sha1};
371
372 let dir = TempDir::new().unwrap();
373 let path = dir.path().join("lib.jar");
374 let content = b"jar content";
375 tokio::fs::write(&path, content).await.unwrap();
376
377 let mut hasher = Sha1::new();
378 hasher.update(content);
379 let sha1 = format!("{:x}", hasher.finalize());
380
381 let bundle = vec![asset(&path.to_string_lossy(), &sha1, 11, "http://x")];
382 let (tx, _rx) = mpsc::channel(16);
383 let bad = check_files(&bundle, &tx, 4).await.unwrap();
384 assert!(bad.is_empty());
385 }
386
387 #[tokio::test]
388 async fn check_files_reports_corrupted_file() {
389 let dir = TempDir::new().unwrap();
390 let path = dir.path().join("lib.jar");
391 tokio::fs::write(&path, b"corrupted").await.unwrap();
392
393 let bundle = vec![asset(
394 &path.to_string_lossy(),
395 "0000000000000000000000000000000000000000",
396 9,
397 "http://x",
398 )];
399 let (tx, _rx) = mpsc::channel(16);
400 let bad = check_files(&bundle, &tx, 4).await.unwrap();
401 assert_eq!(bad.len(), 1);
402 }
403
404 #[tokio::test]
405 async fn check_files_skips_missing_files() {
406 let dir = TempDir::new().unwrap();
407 let path = dir.path().join("nonexistent.jar");
408 let bundle = vec![asset(&path.to_string_lossy(), "abc", 0, "http://x")];
409
410 let (tx, _rx) = mpsc::channel(16);
411 let bad = check_files(&bundle, &tx, 4).await.unwrap();
412 assert!(bad.is_empty());
413 }
414}