Resource packs: a bad pack never blocks import or launch; hard-link fallback for downloads

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
levzzzandClaude Sonnet 5.5 committed 2026-10-31 09:15:43 +05:00
1 parent 9f2a4433ec
commit b499251187
4 files changed
+44 -14

No files matched your search

+19 -2
View File
@@ -268,8 +268,25 @@ pub(crate) fn store_download(
fs::rename(&part, dest).map_err(|e| e.to_string())
} else {
// Publish without replacing a file that appeared while the request was in flight.
fs::hard_link(&part, dest).map_err(|e| e.to_string())?;
fs::remove_file(&part).map_err(|e| e.to_string())
match fs::hard_link(&part, dest) {
Ok(()) => fs::remove_file(&part).map_err(|e| e.to_string()),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Err(e.to_string()),
Err(_) => {
// No hard links here (FAT, some network mounts): copy into a new file.
let mut out = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(dest)
.map_err(|e| e.to_string())?;
let written = out.write_all(&bytes).map_err(|e| e.to_string());
drop(out);
if written.is_err() {
let _ = fs::remove_file(dest);
}
let _ = fs::remove_file(&part);
written
}
}
}
});
if result.is_err() {
+5 -1
View File
@@ -25,7 +25,11 @@ pub fn read_json<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T, Strin
pub fn import(source: &Path, work: &Path, log: Log) -> Result<import::Pack, String> {
let pack = import::import(source)?;
fs::create_dir_all(work).map_err(|e| format!("{}: {e}", work.display()))?;
crate::resourcepacks::stage(source, work, &pack)?.save(work)?;
// A bad resource-pack entry must not stop the pack's mods from importing.
match crate::resourcepacks::stage(source, work, &pack).and_then(|r| r.save(work)) {
Ok(()) => {}
Err(e) => log(format!("resource packs not staged: {e}")),
}
write_json(&work.join("pack.json"), &pack)?;
let mut kinds = BTreeMap::<&str, usize>::new();
for f in &pack.files {
@@ -145,7 +145,8 @@ fn hostile_archive_paths_and_cross_layer_collisions_are_rejected_before_staging(
(&format!("overrides/resourcepacks/{name}"), b"bad"),
],
);
assert!(imported(&src, &t.work()).is_err(), "{name}");
// A bad entry leaves the packs out; it no longer fails the import.
assert!(imported(&src, &t.work()).is_ok(), "{name}");
assert!(available(&t.work()).is_empty());
assert!(!t.work().join("escape.zip").exists());
}
@@ -162,7 +163,7 @@ fn hostile_archive_paths_and_cross_layer_collisions_are_rejected_before_staging(
),
],
);
assert!(imported(&src, &t.work()).is_err());
assert!(imported(&src, &t.work()).is_ok());
assert!(available(&t.work()).is_empty());
}
@@ -209,7 +210,8 @@ fn symlinks_at_source_and_destination_are_rejected() {
put(&src.join("resourcepacks/good.zip"), b"good");
put(&t.0.join("outside.zip"), b"outside");
symlink(t.0.join("outside.zip"), src.join("resourcepacks/link.zip")).unwrap();
assert!(imported(&src, &t.work()).is_err());
// A bad entry leaves the packs out; it no longer fails the import.
assert!(imported(&src, &t.work()).is_ok());
fs::remove_file(src.join("resourcepacks/link.zip")).unwrap();
imported(&src, &t.work()).unwrap();
let home = t.0.join("home");
@@ -381,7 +383,8 @@ fn duplicate_archive_entries_are_rejected() {
}
}
fs::write(&src, bytes).unwrap();
assert!(imported(&src, &t.work()).is_err());
// A bad entry leaves the packs out; it no longer fails the import.
assert!(imported(&src, &t.work()).is_ok());
assert!(available(&t.work()).is_empty());
}
@@ -400,7 +403,8 @@ fn archive_symlinks_are_refused_and_file_counts_are_bounded() {
)
.unwrap();
zip.finish().unwrap();
assert!(imported(&src, &t.work()).is_err());
// A bad entry leaves the packs out; it no longer fails the import.
assert!(imported(&src, &t.work()).is_ok());
assert!(available(&t.work()).is_empty());
let mut files: Files = (0..MAX_FILES)
.map(|n| (format!("pack/{n}"), Vec::new()))
+11 -6
View File
@@ -214,7 +214,7 @@ pub fn import(path: &Path, log: Log) -> Result<Imported, String> {
pipeline::import(path, &work, log)?;
pipeline::inventory(&work, false, log)?;
pipeline::report(&work, log)?;
prepare_resourcepacks(&work)?;
prepare_resourcepacks(&work);
load(&work)
}
@@ -227,7 +227,7 @@ pub fn port(
log: Log,
) -> Result<Imported, String> {
pipeline::port(work, resources, from, log)?;
prepare_resourcepacks(work)?;
prepare_resourcepacks(work);
load(work)
}
@@ -301,7 +301,7 @@ pub fn apply_profile(cmd: &mut Command, work: &Path) -> Result<(), String> {
if mods.is_empty() && resourcepacks::available(work).is_empty() {
return Err("No content mods or resource packs are available: port the pack first.".into());
}
prepare_resourcepacks(work)?;
prepare_resourcepacks(work);
let home = profile_home(work);
fs::create_dir_all(&home).map_err(|e| format!("{}: {e}", home.display()))?;
let options = home.join("options.txt");
@@ -316,9 +316,14 @@ pub fn apply_profile(cmd: &mut Command, work: &Path) -> Result<(), String> {
Ok(())
}
fn prepare_resourcepacks(work: &Path) -> Result<(), String> {
let report = resourcepacks::install(work, &profile_home(work))?;
pipeline::write_json(&work.join("profile-resourcepacks.json"), &report)
/// Installs the staged resource packs into the profile. A pack that cannot be installed
/// is logged and left off; it never blocks importing, porting or launching the mods.
fn prepare_resourcepacks(work: &Path) {
let result = resourcepacks::install(work, &profile_home(work))
.and_then(|report| pipeline::write_json(&work.join("profile-resourcepacks.json"), &report));
if let Err(e) = result {
eprintln!("resource packs not installed: {e}");
}
}
#[cfg(test)]