use md5::{Digest, Md5};
use nte_patcher::{
crypto::aes_cbc,
error::Error,
manager::DownloadManager,
model::{Config, ResTask},
parser::{get_config, get_reslist},
unzip::extract,
};
use reqwest::Url;
use std::{
fs::read,
sync::Arc,
sync::atomic::{AtomicU64, Ordering},
};
use tempfile::tempdir;
async fn download_file(client: &reqwest::Client, url: &str, path: &str) -> Result<(), Error> {
let response = client.get(url).send().await?.error_for_status()?;
let mut file = tokio::fs::File::create(path).await.map_err(Error::Io)?;
let mut stream = response.bytes_stream();
use futures_util::StreamExt;
while let Some(chunk) = stream.next().await {
let data = chunk.map_err(Error::Network)?;
tokio::io::AsyncWriteExt::write_all(&mut file, &data)
.await
.map_err(Error::Io)?;
}
Ok(())
}
fn compute_md5(file_path: &str) -> Result<String, Error> {
let data = read(file_path)?;
let mut hasher = Md5::new();
hasher.update(&data);
Ok(hex::encode(hasher.finalize()))
}
#[tokio::test]
async fn test_perfectworld_cdn_workflow() -> Result<(), Error> {
let base_url = "https://ntecdn1.perfectworld.com/clientRes/publish_PC";
let temp_dir = tempdir().map_err(Error::Io)?;
let base_path = temp_dir.path();
let bucket_dir = base_path.join("bucket");
let game_dir = base_path.join("game");
tokio::fs::create_dir_all(&bucket_dir)
.await
.map_err(Error::Io)?;
tokio::fs::create_dir_all(&game_dir)
.await
.map_err(Error::Io)?;
let config_url = Url::parse(&format!("{}/Version/Windows/config.xml", base_url))?;
let config: Config = get_config(config_url).await?;
let version = config.resversion.to_string();
let reslist_zip_url = format!(
"{}/Version/Windows/version/{}/ResList.bin.zip",
base_url, version
);
let reslist_zip_path = base_path.join("ResList.bin.zip");
let client = reqwest::Client::builder().build().map_err(Error::Network)?;
download_file(
&client,
&reslist_zip_url,
reslist_zip_path.to_str().unwrap(),
)
.await?;
let zip_data = read(&reslist_zip_path)?;
extract(&zip_data, base_path)?;
let reslist_bin_path = base_path.join("ResList.bin");
let reslist_xml_path = base_path.join("ResList.xml");
let key = b"3000001@Patcher0";
let iv = b"PatcherSDK000000";
aes_cbc(&reslist_bin_path, &reslist_xml_path, key, iv)?;
let actual_listhash = compute_md5(reslist_xml_path.to_str().unwrap())?;
assert_eq!(
config.extra.listhash, actual_listhash,
"ResList MD5 mismatch"
);
let reslist = get_reslist(&reslist_xml_path)?;
let tasks = ResTask::from_reslist(reslist);
assert!(!tasks.is_empty(), "ResList should contain tasks");
let mut normal_tasks = Vec::new();
let mut pak_tasks = Vec::new();
let mut block_tasks = Vec::new();
for task in tasks {
match task.task_type {
nte_patcher::model::TaskType::Normal => normal_tasks.push(task),
nte_patcher::model::TaskType::Pak { .. } => pak_tasks.push(task),
nte_patcher::model::TaskType::Block { .. } => block_tasks.push(task),
}
}
normal_tasks.sort_by_key(|t| t.filesize);
pak_tasks.sort_by_key(|t| t.filesize);
block_tasks.sort_by_key(|t| t.filesize);
let mut small_tasks: Vec<ResTask> = normal_tasks.into_iter().take(3).collect();
small_tasks.extend(pak_tasks.into_iter().take(1));
small_tasks.extend(block_tasks.into_iter().take(1));
let _total_expected_bytes: u64 = small_tasks.iter().map(|t| t.filesize).sum();
let config = nte_patcher::config::PatcherConfig {
base_url: base_url.to_string(),
bucket_dir: bucket_dir.clone(),
game_dir: game_dir.clone(),
max_concurrent_tasks: 4,
retry_count: 3,
tcp_keepalive_secs: 60,
};
let manager = DownloadManager::new(config);
let downloaded_bytes = Arc::new(AtomicU64::new(0));
let downloaded_bytes_clone = downloaded_bytes.clone();
manager
.start_all(small_tasks.clone(), move |_task| {
let downloaded_bytes_clone = downloaded_bytes_clone.clone();
(
move |bytes| {
downloaded_bytes_clone.fetch_add(bytes, Ordering::Relaxed);
},
move || {},
)
})
.await?;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
for task in &small_tasks {
match &task.task_type {
nte_patcher::model::TaskType::Pak { entries } => {
for entry in entries {
let target_path = game_dir.join(&entry.name);
assert!(
target_path.exists(),
"Pak entry {:?} should exist",
entry.name
);
}
}
_ => {
let target_path = game_dir.join(&task.target_path);
assert!(
target_path.exists(),
"Target file {:?} should exist",
task.target_path
);
let meta = tokio::fs::metadata(&target_path).await.map_err(Error::Io)?;
assert_eq!(
meta.len(),
task.filesize,
"Size mismatch for {:?}",
task.target_path
);
}
}
}
Ok(())
}