758 lines
24 KiB
Rust
758 lines
24 KiB
Rust
use crate::instance::QuickPlayType;
|
|
use crate::launcher::{download, parse_rules};
|
|
use crc32fast::Hasher;
|
|
use daedalus::minecraft::Library;
|
|
use fs4::tokio::AsyncFileExt;
|
|
use std::collections::{BTreeMap, HashSet};
|
|
use std::io::{Read, Write};
|
|
use std::path::{Component, Path, PathBuf};
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub(crate) struct NativeArchive {
|
|
pub library_name: String,
|
|
pub classifier: String,
|
|
pub archive_path: PathBuf,
|
|
pub sha1: Option<String>,
|
|
pub exclude: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct NativeEntry {
|
|
relative_path: PathBuf,
|
|
uncompressed_size: u64,
|
|
crc32: u32,
|
|
archive_path: PathBuf,
|
|
archive_entry_index: usize,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub(crate) struct NativePreparationReport {
|
|
pub verified: usize,
|
|
pub restored: Vec<PathBuf>,
|
|
}
|
|
|
|
pub(crate) fn resolve_native_archives(
|
|
libraries_dir: &Path,
|
|
caches_dir: &Path,
|
|
libraries: &[Library],
|
|
java_arch: &str,
|
|
minecraft_updated: bool,
|
|
) -> crate::Result<Vec<NativeArchive>> {
|
|
let mut archives = Vec::new();
|
|
let mut identities = HashSet::new();
|
|
|
|
for library in libraries {
|
|
if let Some(rules) = &library.rules
|
|
&& !parse_rules(
|
|
rules,
|
|
java_arch,
|
|
&QuickPlayType::None,
|
|
minecraft_updated,
|
|
)
|
|
{
|
|
continue;
|
|
}
|
|
if !library.downloadable || !download::is_native_library(library) {
|
|
continue;
|
|
}
|
|
let Some(classifier) =
|
|
download::library_native_classifier(library, java_arch)
|
|
else {
|
|
continue;
|
|
};
|
|
let native = library
|
|
.downloads
|
|
.as_ref()
|
|
.and_then(|downloads| downloads.classifiers.as_ref())
|
|
.and_then(|classifiers| classifiers.get(&classifier));
|
|
let classified_path = libraries_dir.join(
|
|
if let Some(path) = native.and_then(|native| native.path.as_deref())
|
|
{
|
|
path.to_owned()
|
|
} else {
|
|
download::native_library_artifact_path(library, &classifier)?
|
|
},
|
|
);
|
|
let (archive_path, sha1) = if let Some(native) = native {
|
|
let cached = caches_dir
|
|
.join("minecraft-natives")
|
|
.join(format!("{}.jar", native.sha1));
|
|
// Modern native archives are content-addressed by SHA1. Do not
|
|
// silently substitute the Maven artifact: it can be a different
|
|
// classifier/version and produce an ABI-incompatible DLL set.
|
|
if cached.is_file() {
|
|
(cached, Some(native.sha1.clone()))
|
|
} else if classified_path.is_file()
|
|
&& (native.sha1.len() != 40
|
|
|| sha1_file(&classified_path)? == native.sha1)
|
|
{
|
|
// Older instances downloaded classifiers directly into
|
|
// libraries/. Reuse that artifact when it is verifiably the
|
|
// requested archive (or when the legacy metadata has no
|
|
// usable SHA-1 to verify), allowing repair/import to recover
|
|
// without a redundant network download.
|
|
(classified_path, None)
|
|
} else {
|
|
(cached, Some(native.sha1.clone()))
|
|
}
|
|
} else {
|
|
(classified_path, None)
|
|
};
|
|
let identity = sha1.clone().unwrap_or_else(|| {
|
|
archive_path
|
|
.canonicalize()
|
|
.unwrap_or_else(|_| archive_path.clone())
|
|
.to_string_lossy()
|
|
.into_owned()
|
|
});
|
|
if !identities.insert(identity) {
|
|
tracing::debug!(
|
|
library = %library.name,
|
|
classifier,
|
|
"Skipped duplicate native archive"
|
|
);
|
|
continue;
|
|
}
|
|
archives.push(NativeArchive {
|
|
library_name: library.name.clone(),
|
|
classifier,
|
|
archive_path,
|
|
sha1,
|
|
exclude: library
|
|
.extract
|
|
.as_ref()
|
|
.and_then(|extract| extract.exclude.clone())
|
|
.unwrap_or_default(),
|
|
});
|
|
}
|
|
|
|
Ok(archives)
|
|
}
|
|
|
|
pub(crate) async fn prepare_native_libraries(
|
|
natives_root: &Path,
|
|
libraries_dir: &Path,
|
|
caches_dir: &Path,
|
|
libraries: &[Library],
|
|
version: &str,
|
|
java_arch: &str,
|
|
minecraft_updated: bool,
|
|
) -> crate::Result<NativePreparationReport> {
|
|
let archives = resolve_native_archives(
|
|
libraries_dir,
|
|
caches_dir,
|
|
libraries,
|
|
java_arch,
|
|
minecraft_updated,
|
|
)?;
|
|
materialize_native_directory(&archives, natives_root, version).await
|
|
}
|
|
|
|
pub(crate) async fn materialize_native_directory(
|
|
archives: &[NativeArchive],
|
|
natives_root: &Path,
|
|
version: &str,
|
|
) -> crate::Result<NativePreparationReport> {
|
|
let natives_dir = natives_root.join(version);
|
|
let lock_dir = natives_root.join(".locks");
|
|
tokio::fs::create_dir_all(&natives_dir).await?;
|
|
tokio::fs::create_dir_all(&lock_dir).await?;
|
|
let lock_path = lock_dir.join(format!("{}.lock", safe_version_id(version)));
|
|
let archives = archives.to_vec();
|
|
let version = version.to_string();
|
|
let lock = tokio::fs::File::options()
|
|
.create(true)
|
|
.write(true)
|
|
.truncate(false)
|
|
.open(&lock_path)
|
|
.await?;
|
|
let report = tokio::task::spawn_blocking(move || {
|
|
lock.lock_exclusive().map_err(|error| {
|
|
crate::ErrorKind::LauncherError(format!(
|
|
"Failed to lock native directory for Minecraft {version} at {}: {error}",
|
|
lock_path.display()
|
|
))
|
|
})?;
|
|
materialize_locked(&archives, &natives_dir, &version)
|
|
})
|
|
.await??;
|
|
Ok(report)
|
|
}
|
|
|
|
fn materialize_locked(
|
|
archives: &[NativeArchive],
|
|
natives_dir: &Path,
|
|
version: &str,
|
|
) -> crate::Result<NativePreparationReport> {
|
|
let manifest = build_manifest(archives)?;
|
|
let mut report = NativePreparationReport::default();
|
|
|
|
for entry in manifest.values() {
|
|
let target = natives_dir.join(&entry.relative_path);
|
|
if file_matches(&target, entry.uncompressed_size, entry.crc32)? {
|
|
report.verified += 1;
|
|
continue;
|
|
}
|
|
materialize_entry(entry, &target, version)?;
|
|
if !file_matches(&target, entry.uncompressed_size, entry.crc32)? {
|
|
return Err(native_error(format!(
|
|
"Native entry {} failed final validation after extraction from {}",
|
|
target.display(),
|
|
entry.archive_path.display()
|
|
)));
|
|
}
|
|
report.restored.push(entry.relative_path.clone());
|
|
}
|
|
|
|
tracing::debug!(
|
|
target = %natives_dir.display(),
|
|
archives = archives.len(),
|
|
entries = manifest.len(),
|
|
verified = report.verified,
|
|
restored = report.restored.len(),
|
|
"Prepared Minecraft native directory"
|
|
);
|
|
Ok(report)
|
|
}
|
|
|
|
fn build_manifest(
|
|
archives: &[NativeArchive],
|
|
) -> crate::Result<BTreeMap<PathBuf, NativeEntry>> {
|
|
let mut manifest = BTreeMap::new();
|
|
for archive in archives {
|
|
if !archive.archive_path.is_file() {
|
|
return Err(native_error(format!(
|
|
"Native archive for {} ({}) is missing at {}. Repair the instance while online and try again",
|
|
archive.library_name,
|
|
archive.classifier,
|
|
archive.archive_path.display()
|
|
)));
|
|
}
|
|
if let Some(expected_sha1) = archive.sha1.as_deref()
|
|
&& expected_sha1.len() == 40
|
|
{
|
|
let actual_sha1 = sha1_file(&archive.archive_path)?;
|
|
if actual_sha1 != expected_sha1 {
|
|
return Err(native_error(format!(
|
|
"Native archive for {} ({}) at {} has SHA1 {}, expected {}. Repair the instance while online and try again",
|
|
archive.library_name,
|
|
archive.classifier,
|
|
archive.archive_path.display(),
|
|
actual_sha1,
|
|
expected_sha1
|
|
)));
|
|
}
|
|
}
|
|
let file = std::fs::File::open(&archive.archive_path)?;
|
|
let mut zip = zip::ZipArchive::new(file).map_err(|error| {
|
|
native_error(format!(
|
|
"Failed to open native archive for {} ({}) at {}: {error}",
|
|
archive.library_name,
|
|
archive.classifier,
|
|
archive.archive_path.display()
|
|
))
|
|
})?;
|
|
for index in 0..zip.len() {
|
|
let entry = zip.by_index(index).map_err(|error| {
|
|
native_error(format!(
|
|
"Failed to inspect native archive {}: {error}",
|
|
archive.archive_path.display()
|
|
))
|
|
})?;
|
|
if entry.is_dir() {
|
|
continue;
|
|
}
|
|
let Some(relative_path) = safe_entry_path(entry.name()) else {
|
|
tracing::warn!(
|
|
archive = %archive.archive_path.display(),
|
|
entry = entry.name(),
|
|
"Ignored unsafe native archive entry"
|
|
);
|
|
continue;
|
|
};
|
|
let normalized = relative_path.to_string_lossy().replace('\\', "/");
|
|
if normalized == "META-INF" || normalized.starts_with("META-INF/") {
|
|
continue;
|
|
}
|
|
if archive.exclude.iter().any(|excluded| {
|
|
let excluded = excluded.replace('\\', "/");
|
|
normalized == excluded.trim_end_matches('/')
|
|
|| normalized.starts_with(&format!(
|
|
"{}/",
|
|
excluded.trim_end_matches('/')
|
|
))
|
|
}) {
|
|
continue;
|
|
}
|
|
if let Some(previous) = manifest.insert(
|
|
relative_path.clone(),
|
|
NativeEntry {
|
|
relative_path,
|
|
uncompressed_size: entry.size(),
|
|
crc32: entry.crc32(),
|
|
archive_path: archive.archive_path.clone(),
|
|
archive_entry_index: index,
|
|
},
|
|
) && (previous.uncompressed_size != entry.size()
|
|
|| previous.crc32 != entry.crc32())
|
|
{
|
|
tracing::debug!(
|
|
entry = normalized,
|
|
previous = %previous.archive_path.display(),
|
|
selected = %archive.archive_path.display(),
|
|
"Native entry is provided by multiple archives; using metadata order"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
Ok(manifest)
|
|
}
|
|
|
|
fn safe_entry_path(name: &str) -> Option<PathBuf> {
|
|
if name.is_empty()
|
|
|| name.contains('\\')
|
|
|| name.starts_with('/')
|
|
|| name.as_bytes().get(1) == Some(&b':')
|
|
{
|
|
return None;
|
|
}
|
|
let path = Path::new(name);
|
|
if path.is_absolute()
|
|
|| path.components().any(|component| {
|
|
matches!(
|
|
component,
|
|
Component::ParentDir
|
|
| Component::RootDir
|
|
| Component::Prefix(_)
|
|
)
|
|
})
|
|
{
|
|
return None;
|
|
}
|
|
Some(path.to_path_buf())
|
|
}
|
|
|
|
fn file_matches(path: &Path, size: u64, crc32: u32) -> crate::Result<bool> {
|
|
let Ok(metadata) = std::fs::symlink_metadata(path) else {
|
|
return Ok(false);
|
|
};
|
|
if !metadata.is_file() || metadata.len() != size {
|
|
return Ok(false);
|
|
}
|
|
let mut file = std::fs::File::open(path)?;
|
|
let mut hasher = Hasher::new();
|
|
let mut buffer = [0_u8; 64 * 1024];
|
|
loop {
|
|
let read = file.read(&mut buffer)?;
|
|
if read == 0 {
|
|
break;
|
|
}
|
|
hasher.update(&buffer[..read]);
|
|
}
|
|
Ok(hasher.finalize() == crc32)
|
|
}
|
|
|
|
fn sha1_file(path: &Path) -> crate::Result<String> {
|
|
let mut file = std::fs::File::open(path)?;
|
|
let mut hasher = sha1_smol::Sha1::new();
|
|
let mut buffer = [0_u8; 64 * 1024];
|
|
loop {
|
|
let read = file.read(&mut buffer)?;
|
|
if read == 0 {
|
|
break;
|
|
}
|
|
hasher.update(&buffer[..read]);
|
|
}
|
|
Ok(hasher.digest().to_string())
|
|
}
|
|
|
|
fn materialize_entry(
|
|
entry: &NativeEntry,
|
|
target: &Path,
|
|
version: &str,
|
|
) -> crate::Result<()> {
|
|
let parent = target.parent().ok_or_else(|| {
|
|
native_error(format!(
|
|
"Native target {} has no parent",
|
|
target.display()
|
|
))
|
|
})?;
|
|
std::fs::create_dir_all(parent)?;
|
|
let file = std::fs::File::open(&entry.archive_path)?;
|
|
let mut archive = zip::ZipArchive::new(file).map_err(|error| {
|
|
native_error(format!(
|
|
"Failed to open native archive {}: {error}",
|
|
entry.archive_path.display()
|
|
))
|
|
})?;
|
|
let mut source =
|
|
archive
|
|
.by_index(entry.archive_entry_index)
|
|
.map_err(|error| {
|
|
native_error(format!(
|
|
"Failed to read native entry {} from {}: {error}",
|
|
entry.relative_path.display(),
|
|
entry.archive_path.display()
|
|
))
|
|
})?;
|
|
let mut temporary = tempfile::Builder::new()
|
|
.prefix(&format!(".tmp-native-{}-", safe_version_id(version)))
|
|
.tempfile_in(parent)?;
|
|
let mut hasher = Hasher::new();
|
|
let mut written = 0_u64;
|
|
let mut buffer = [0_u8; 64 * 1024];
|
|
loop {
|
|
let read = source.read(&mut buffer)?;
|
|
if read == 0 {
|
|
break;
|
|
}
|
|
temporary.write_all(&buffer[..read])?;
|
|
hasher.update(&buffer[..read]);
|
|
written += read as u64;
|
|
}
|
|
if written != entry.uncompressed_size || hasher.finalize() != entry.crc32 {
|
|
return Err(native_error(format!(
|
|
"Native entry {} extracted from {} did not match its ZIP size and CRC32",
|
|
entry.relative_path.display(),
|
|
entry.archive_path.display()
|
|
)));
|
|
}
|
|
temporary.flush()?;
|
|
temporary.as_file().sync_all()?;
|
|
if let Ok(metadata) = std::fs::symlink_metadata(target) {
|
|
if metadata.file_type().is_dir() {
|
|
std::fs::remove_dir_all(target)?;
|
|
} else if !metadata.file_type().is_file() {
|
|
std::fs::remove_file(target)?;
|
|
}
|
|
}
|
|
temporary.persist(target).map_err(|error| {
|
|
native_error(format!(
|
|
"Failed to atomically replace native entry {}: {}",
|
|
target.display(),
|
|
error.error
|
|
))
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
fn safe_version_id(version: &str) -> String {
|
|
let sanitized: String = version
|
|
.chars()
|
|
.map(|character| {
|
|
if character.is_ascii_alphanumeric()
|
|
|| matches!(character, '.' | '-' | '_')
|
|
{
|
|
character
|
|
} else {
|
|
'_'
|
|
}
|
|
})
|
|
.collect();
|
|
if sanitized.is_empty() {
|
|
"unknown".to_string()
|
|
} else {
|
|
sanitized
|
|
}
|
|
}
|
|
|
|
fn native_error(message: String) -> crate::Error {
|
|
crate::ErrorKind::LauncherError(message).into()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn write_archive(path: &Path, entries: &[(&str, &[u8])]) {
|
|
let file = std::fs::File::create(path).unwrap();
|
|
let mut writer = zip::ZipWriter::new(file);
|
|
let options = zip::write::SimpleFileOptions::default()
|
|
.compression_method(zip::CompressionMethod::Stored);
|
|
for (name, contents) in entries {
|
|
writer.start_file(*name, options).unwrap();
|
|
writer.write_all(contents).unwrap();
|
|
}
|
|
writer.finish().unwrap();
|
|
}
|
|
|
|
fn archive(path: PathBuf) -> NativeArchive {
|
|
NativeArchive {
|
|
library_name: "org.lwjgl:lwjgl:3.2.2".to_string(),
|
|
classifier: "natives-windows".to_string(),
|
|
archive_path: path,
|
|
sha1: None,
|
|
exclude: Vec::new(),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn concurrent_launch_native_directories_do_not_replace_each_other() {
|
|
let archives = tempfile::tempdir().unwrap();
|
|
let first = tempfile::tempdir().unwrap();
|
|
let second = tempfile::tempdir().unwrap();
|
|
let source = archives.path().join("natives.jar");
|
|
write_archive(&source, &[("lwjgl.dll", b"first runtime")]);
|
|
materialize_native_directory(
|
|
&[archive(source.clone())],
|
|
first.path(),
|
|
"natives",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let first_dll = first.path().join("natives/lwjgl.dll");
|
|
#[cfg(windows)]
|
|
let _running_dll = {
|
|
use std::os::windows::fs::OpenOptionsExt;
|
|
std::fs::OpenOptions::new()
|
|
.read(true)
|
|
.share_mode(1)
|
|
.open(&first_dll)
|
|
.unwrap()
|
|
};
|
|
write_archive(&source, &[("lwjgl.dll", b"second runtime")]);
|
|
materialize_native_directory(
|
|
&[archive(source)],
|
|
second.path(),
|
|
"natives",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(std::fs::read(&first_dll).unwrap(), b"first runtime");
|
|
assert_eq!(
|
|
std::fs::read(second.path().join("natives/lwjgl.dll")).unwrap(),
|
|
b"second runtime"
|
|
);
|
|
drop(second);
|
|
assert!(first_dll.exists());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn repairs_missing_truncated_same_size_and_directory_entries() {
|
|
let root = tempfile::tempdir().unwrap();
|
|
let archive_path = root.path().join("natives.jar");
|
|
write_archive(
|
|
&archive_path,
|
|
&[
|
|
("missing.dll", b"missing"),
|
|
("bad.dll", b"correct"),
|
|
("folder.dll", b"file"),
|
|
],
|
|
);
|
|
let target = root.path().join("target");
|
|
std::fs::create_dir_all(target.join("version/folder.dll")).unwrap();
|
|
std::fs::write(target.join("version/bad.dll"), b"xxxxxxx").unwrap();
|
|
|
|
let report = materialize_native_directory(
|
|
&[archive(archive_path)],
|
|
&target,
|
|
"version",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(report.restored.len(), 3);
|
|
assert_eq!(
|
|
std::fs::read(target.join("version/missing.dll")).unwrap(),
|
|
b"missing"
|
|
);
|
|
assert_eq!(
|
|
std::fs::read(target.join("version/bad.dll")).unwrap(),
|
|
b"correct"
|
|
);
|
|
assert_eq!(
|
|
std::fs::read(target.join("version/folder.dll")).unwrap(),
|
|
b"file"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn preparation_is_idempotent_and_leaves_no_temporary_files() {
|
|
let root = tempfile::tempdir().unwrap();
|
|
let archive_path = root.path().join("natives.jar");
|
|
write_archive(&archive_path, &[("lwjgl.dll", b"native")]);
|
|
let archives = [archive(archive_path)];
|
|
let first =
|
|
materialize_native_directory(&archives, root.path(), "1.18.2")
|
|
.await
|
|
.unwrap();
|
|
let second =
|
|
materialize_native_directory(&archives, root.path(), "1.18.2")
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(first.restored, [PathBuf::from("lwjgl.dll")]);
|
|
assert_eq!(second.verified, 1);
|
|
assert!(second.restored.is_empty());
|
|
assert!(
|
|
std::fs::read_dir(root.path().join("1.18.2"))
|
|
.unwrap()
|
|
.flatten()
|
|
.all(|entry| !entry
|
|
.file_name()
|
|
.to_string_lossy()
|
|
.starts_with(".tmp-native-"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn modern_native_resolution_does_not_fallback_to_unverified_maven_artifact()
|
|
{
|
|
let root = tempfile::tempdir().unwrap();
|
|
let libraries_dir = root.path().join("libraries");
|
|
let caches_dir = root.path().join("caches");
|
|
let classified = libraries_dir
|
|
.join("org/lwjgl/lwjgl/3.2.2/lwjgl-3.2.2-natives-windows.jar");
|
|
std::fs::create_dir_all(classified.parent().unwrap()).unwrap();
|
|
write_archive(&classified, &[("lwjgl.dll", b"wrong")]);
|
|
let library: Library = serde_json::from_value(serde_json::json!({
|
|
"name": "org.lwjgl:lwjgl:3.2.2",
|
|
"natives": {"windows": "natives-windows"},
|
|
"downloads": {"classifiers": {"natives-windows": {
|
|
"sha1": "05359f3aa50d36352815fc662ea73e1c00d22170",
|
|
"size": 279593,
|
|
"url": "https://libraries.minecraft.net/native.jar"
|
|
}}}
|
|
}))
|
|
.unwrap();
|
|
|
|
let archives = resolve_native_archives(
|
|
&libraries_dir,
|
|
&caches_dir,
|
|
&[library],
|
|
"x86_64",
|
|
true,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(archives.len(), 1);
|
|
assert_eq!(
|
|
archives[0].archive_path,
|
|
caches_dir.join(
|
|
"minecraft-natives/05359f3aa50d36352815fc662ea73e1c00d22170.jar"
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn classifier_native_library_is_selected_for_preparation() {
|
|
let root = tempfile::tempdir().unwrap();
|
|
let library: Library = serde_json::from_value(serde_json::json!({
|
|
"name": "org.lwjgl:lwjgl:3.2.2:natives-windows",
|
|
"downloads": {
|
|
"artifact": {
|
|
"sha1": "abc",
|
|
"size": 1,
|
|
"url": "https://example.com/native.jar"
|
|
}
|
|
}
|
|
}))
|
|
.unwrap();
|
|
|
|
let archives = resolve_native_archives(
|
|
&root.path().join("libraries"),
|
|
&root.path().join("caches"),
|
|
&[library],
|
|
"x86_64",
|
|
true,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(archives.len(), 1);
|
|
assert_eq!(archives[0].classifier, "natives-windows");
|
|
assert_eq!(
|
|
archives[0].archive_path,
|
|
root.path().join(
|
|
"libraries/org/lwjgl/lwjgl/3.2.2/lwjgl-3.2.2-natives-windows.jar"
|
|
)
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn rejects_modern_archives_with_the_wrong_sha1() {
|
|
let root = tempfile::tempdir().unwrap();
|
|
let archive_path = root.path().join("natives.jar");
|
|
write_archive(&archive_path, &[("lwjgl.dll", b"native")]);
|
|
let mut archive = archive(archive_path);
|
|
archive.sha1 =
|
|
Some("0000000000000000000000000000000000000000".to_string());
|
|
|
|
let error = materialize_native_directory(
|
|
&[archive],
|
|
root.path(),
|
|
"1.18.2-forge-40.2.34",
|
|
)
|
|
.await
|
|
.unwrap_err();
|
|
|
|
assert!(error.to_string().contains("has SHA1"));
|
|
assert!(!root.path().join("1.18.2-forge-40.2.34/lwjgl.dll").exists());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn concurrent_preparation_is_serialized() {
|
|
let root = tempfile::tempdir().unwrap();
|
|
let archive_path = root.path().join("natives.jar");
|
|
write_archive(&archive_path, &[("lwjgl.dll", b"native")]);
|
|
let archives = vec![archive(archive_path)];
|
|
let root_path = root.path().to_path_buf();
|
|
let (left, right) = tokio::join!(
|
|
materialize_native_directory(&archives, &root_path, "1.18.2"),
|
|
materialize_native_directory(&archives, &root_path, "1.18.2"),
|
|
);
|
|
|
|
let reports = [left.unwrap(), right.unwrap()];
|
|
assert_eq!(
|
|
reports
|
|
.iter()
|
|
.map(|report| report.restored.len())
|
|
.sum::<usize>(),
|
|
1
|
|
);
|
|
assert_eq!(
|
|
reports.iter().map(|report| report.verified).sum::<usize>(),
|
|
1
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn unsafe_meta_inf_and_duplicate_entries_are_deterministic() {
|
|
let root = tempfile::tempdir().unwrap();
|
|
let first = root.path().join("first.jar");
|
|
let second = root.path().join("second.jar");
|
|
write_archive(
|
|
&first,
|
|
&[
|
|
("same.dll", b"first"),
|
|
("META-INF/MANIFEST.MF", b"meta"),
|
|
("excluded/skip.dll", b"skip"),
|
|
],
|
|
);
|
|
write_archive(
|
|
&second,
|
|
&[
|
|
("../evil.dll", b"evil"),
|
|
("C:/evil.dll", b"evil"),
|
|
("same.dll", b"later"),
|
|
],
|
|
);
|
|
|
|
let mut first = archive(first);
|
|
first.exclude = vec!["excluded/".to_string()];
|
|
materialize_native_directory(
|
|
&[first, archive(second)],
|
|
root.path(),
|
|
"version",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
std::fs::read(root.path().join("version/same.dll")).unwrap(),
|
|
b"later"
|
|
);
|
|
assert!(!root.path().join("evil.dll").exists());
|
|
assert!(!root.path().join("version/META-INF").exists());
|
|
assert!(!root.path().join("version/excluded").exists());
|
|
}
|
|
}
|