feat: malware warning modal changes (#6721)

* feat: improved warning modals

* fix: qa

* feat: install to play and update to play changes

* fix: dont warn for server projects as already reviewed

* fix: lint
This commit is contained in:
Calum H.
2026-07-14 20:47:10 +00:00
committed by GitHub
parent 8cca911775
commit 905204cc5f
58 changed files with 1244 additions and 1351 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ pub use self::paths::{get_full_path, get_mod_full_path};
pub use self::projects::{
InstallProjectWithDependenciesRequest, add_project_from_path,
add_project_from_version, install_project_with_dependencies,
remove_project, repair_managed_modrinth,
is_file_on_modrinth, remove_project, repair_managed_modrinth,
switch_project_version_with_dependencies, toggle_disable_project,
update_all_projects, update_managed_modrinth_version, update_project,
};
+16 -1
View File
@@ -1,7 +1,7 @@
use crate::event::emit::{emit_instance, emit_loading, init_loading};
use crate::event::{InstancePayloadType, LoadingBarType};
use crate::state::instances::adapters::sqlite::instance_rows;
use crate::state::{ProjectType, State};
use crate::state::{CacheBehaviour, CachedEntry, ProjectType, State};
use crate::util::fetch;
use modrinth_content_management::{
ContentType, ResolutionPreferences, ResolveContentPlan,
@@ -213,6 +213,21 @@ pub async fn add_project_from_path(
.await
}
#[tracing::instrument]
pub async fn is_file_on_modrinth(path: &Path) -> crate::Result<bool> {
let state = State::get().await?;
let (_, hash) = fetch::sha1_file_async(path).await?;
let files = CachedEntry::get_file_many(
&[&hash],
Some(CacheBehaviour::Bypass),
&state.pool,
&state.api_semaphore,
)
.await?;
Ok(!files.is_empty())
}
#[tracing::instrument]
pub async fn toggle_disable_project(
instance_id: &str,
+10 -2
View File
@@ -113,7 +113,8 @@ pub struct CreatePackInstance {
pub icon: Option<PathBuf>, // the icon for the instance
pub icon_url: Option<String>, // the URL icon for an instance during import
pub link: Option<InstanceLink>,
pub unknown_file: bool, // true when pack file isn't found on Modrinth via hash lookup
pub unknown_file: bool, // true when the mrpack archive isn't found on Modrinth via hash lookup
pub external_files_in_modpack: Vec<String>,
pub skip_install_profile: Option<bool>,
pub no_watch: Option<bool>,
}
@@ -130,6 +131,7 @@ impl Default for CreatePackInstance {
icon_url: None,
link: None,
unknown_file: false,
external_files_in_modpack: Vec::new(),
skip_install_profile: Some(true),
no_watch: Some(false),
}
@@ -149,7 +151,6 @@ pub struct CreatePack {
pub description: CreatePackDescription,
}
// The hash lookup only gates the unknown-pack warning, so avoid a long blocking scan for huge local packs.
const MAX_LOCAL_FILE_HASH_LOOKUP_SIZE: u64 = 1024 * 1024 * 1024;
#[derive(Clone, Debug)]
@@ -214,9 +215,16 @@ pub async fn get_instance_from_pack(
false
};
let external_files_in_modpack =
super::install_mrpack::get_external_files_from_mrpack(
&CreatePackFile::Path(path),
)
.await?;
Ok(CreatePackInstance {
name: file_name,
unknown_file: !is_known_file,
external_files_in_modpack,
..Default::default()
})
}
@@ -23,7 +23,7 @@ use async_zip::base::read::{WithEntry, ZipEntryReader};
use async_zip::tokio::read::fs::ZipFileReader as FsZipFileReader;
use futures::StreamExt;
use path_util::SafeRelativeUtf8UnixPathBuf;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::pin::Pin;
use std::sync::{
@@ -237,6 +237,96 @@ where
Ok((size, hasher.digest().to_string()))
}
pub(crate) async fn get_external_files_from_mrpack(
file: &CreatePackFile,
) -> crate::Result<Vec<String>> {
let mut zip_reader = MrpackZipReader::new(file).await?;
let Some(manifest_idx) =
zip_reader.file().entries().iter().position(|entry| {
matches!(entry.filename().as_str(), Ok("modrinth.index.json"))
})
else {
return Err(crate::Error::from(crate::ErrorKind::InputError(
"No pack manifest found in mrpack".to_string(),
)));
};
let manifest = zip_reader.read_entry_to_string(manifest_idx).await?;
let pack: PackFormat = serde_json::from_str(&manifest)?;
let mut candidates = pack
.files
.into_iter()
.filter_map(|file| {
let path = file.path.as_str();
let hash = file.hashes.get(&PackFileHash::Sha1)?.clone();
let file_name = path.rsplit('/').next()?.to_string();
Some((file_name, hash))
})
.collect::<Vec<_>>();
let override_entries = zip_reader
.file()
.entries()
.iter()
.enumerate()
.filter_map(|(index, entry)| {
let path = entry.filename().as_str().ok()?;
let relative_path = path
.strip_prefix("overrides/")
.or_else(|| path.strip_prefix("client-overrides/"))?;
if path.ends_with('/')
|| ProjectType::get_from_parent_folder(relative_path).is_none()
{
return None;
}
let file_name = relative_path.rsplit('/').next()?.to_string();
Some((index, file_name))
})
.collect::<Vec<_>>();
for (index, file_name) in override_entries {
let (_, hash) = zip_reader.hash_entry(index).await?;
candidates.push((file_name, hash));
}
if candidates.is_empty() {
return Ok(Vec::new());
}
let state = State::get().await?;
let hashes = candidates
.iter()
.map(|(_, hash)| hash.as_str())
.collect::<Vec<_>>();
let recognized_hashes = match CachedEntry::get_file_many(
&hashes,
None,
&state.pool,
&state.api_semaphore,
)
.await
{
Ok(files) => files
.into_iter()
.map(|file| file.hash)
.collect::<HashSet<_>>(),
Err(err) => {
tracing::warn!("Failed to look up files in imported mrpack: {err}");
HashSet::new()
}
};
let mut external_files = candidates
.into_iter()
.filter_map(|(file_name, hash)| {
(!recognized_hashes.contains(&hash)).then_some(file_name)
})
.collect::<Vec<_>>();
external_files.sort();
external_files.dedup();
Ok(external_files)
}
async fn extract_zip_entry<R>(
mut reader: ZipEntryReader<'_, R, WithEntry<'_>>,
path: &Path,