fix(app-lib, labrinth): stricter mrpack file path validation (#4482)

* fix(app-lib, labrinth): stricter mrpack file path validation

* chore: run `cargo fmt`

* tweak: reject reserved Windows device names in mrpacks too
This commit is contained in:
Alejandro González
2025-10-04 12:35:30 +02:00
committed by GitHub
parent a13647b9e2
commit ab6e9dd5d7
16 changed files with 235 additions and 78 deletions

View File

@@ -6,6 +6,7 @@ use crate::state::{CachedEntry, LinkedData, ProfileInstallStage, SideType};
use crate::util::fetch::{fetch, fetch_advanced, write_cached_icon};
use crate::util::io;
use path_util::SafeRelativeUtf8UnixPathBuf;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -27,7 +28,7 @@ pub struct PackFormat {
#[derive(Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PackFile {
pub path: String,
pub path: SafeRelativeUtf8UnixPathBuf,
pub hashes: HashMap<PackFileHash, String>,
pub env: Option<HashMap<EnvType, SideType>>,
pub downloads: Vec<String>,

View File

@@ -18,8 +18,8 @@ use super::install_from::{
generate_pack_from_version_id,
};
use crate::data::ProjectType;
use std::io::Cursor;
use std::path::{Component, PathBuf};
use std::io::{Cursor, ErrorKind};
use std::path::PathBuf;
/// Install a pack
/// Wrapper around install_pack_files that generates a pack creation description, and
@@ -169,31 +169,22 @@ pub async fn install_zipped_mrpack_files(
)
.await?;
let project_path = project.path.to_string();
let path = profile::get_full_path(&profile_path)
.await?
.join(project.path.as_str());
let path =
std::path::Path::new(&project_path).components().next();
if let Some(Component::CurDir | Component::Normal(_)) = path
{
let path = profile::get_full_path(&profile_path)
.await?
.join(&project_path);
cache_file_hash(
file.clone(),
&profile_path,
project.path.as_str(),
project.hashes.get(&PackFileHash::Sha1).map(|x| &**x),
ProjectType::get_from_parent_folder(&path),
&state.pool,
)
.await?;
cache_file_hash(
file.clone(),
&profile_path,
&project_path,
project
.hashes
.get(&PackFileHash::Sha1)
.map(|x| &**x),
ProjectType::get_from_parent_folder(&path),
&state.pool,
)
.await?;
write(&path, &file, &state.io_semaphore).await?;
write(&path, &file, &state.io_semaphore).await?;
}
Ok(())
}
},
@@ -377,9 +368,10 @@ pub async fn remove_all_related_files(
if let Some(metadata) = &project.metadata
&& to_remove.contains(&metadata.project_id)
{
let path = profile_full_path.join(file_path);
if path.exists() {
io::remove_file(&path).await?;
match io::remove_file(profile_full_path.join(file_path)).await {
Ok(_) => (),
Err(err) if err.kind() == ErrorKind::NotFound => (),
Err(err) => return Err(err.into()),
}
}
}
@@ -387,9 +379,12 @@ pub async fn remove_all_related_files(
// Iterate over all Modrinth project file paths in the json, and remove them
// (There should be few, but this removes any files the .mrpack intended as Modrinth projects but were unrecognized)
for file in pack.files {
let path: PathBuf = profile_full_path.join(file.path);
if path.exists() {
io::remove_file(&path).await?;
match io::remove_file(profile_full_path.join(file.path.as_str()))
.await
{
Ok(_) => (),
Err(err) if err.kind() == ErrorKind::NotFound => (),
Err(err) => return Err(err.into()),
}
}
@@ -412,11 +407,16 @@ pub async fn remove_all_related_files(
}
// Remove this file if a corresponding one exists in the filesystem
let existing_file = profile::get_full_path(&profile_path)
.await?
.join(&new_path);
if existing_file.exists() {
io::remove_file(&existing_file).await?;
match io::remove_file(
profile::get_full_path(&profile_path)
.await?
.join(&new_path),
)
.await
{
Ok(_) => (),
Err(err) if err.kind() == ErrorKind::NotFound => (),
Err(err) => return Err(err.into()),
}
}
}

View File

@@ -18,6 +18,7 @@ use crate::util::io::{self, IOError};
pub use crate::{State, state::Profile};
use async_zip::tokio::write::ZipFileWriter;
use async_zip::{Compression, ZipEntryBuilder};
use path_util::SafeRelativeUtf8UnixPathBuf;
use serde_json::json;
use std::collections::{HashMap, HashSet};
@@ -497,11 +498,12 @@ pub async fn export_mrpack(
let version_id = version_id.unwrap_or("1.0.0".to_string());
let mut packfile =
create_mrpack_json(&profile, version_id, description).await?;
let included_candidates_set =
HashSet::<_>::from_iter(included_export_candidates.iter());
let included_candidates_set = HashSet::<_>::from_iter(
included_export_candidates.iter().map(|x| x.as_str()),
);
packfile
.files
.retain(|f| included_candidates_set.contains(&f.path));
.retain(|f| included_candidates_set.contains(f.path.as_str()));
// Build vec of all files in the folder
let mut path_list = Vec::new();
@@ -575,8 +577,8 @@ pub async fn export_mrpack(
#[tracing::instrument]
pub async fn get_pack_export_candidates(
profile_path: &str,
) -> crate::Result<Vec<String>> {
let mut path_list: Vec<String> = Vec::new();
) -> crate::Result<Vec<SafeRelativeUtf8UnixPathBuf>> {
let mut path_list = Vec::new();
let profile_base_dir = get_full_path(profile_path).await?;
let mut read_dir = io::read_dir(&profile_base_dir).await?;
@@ -610,18 +612,19 @@ pub async fn get_pack_export_candidates(
fn pack_get_relative_path(
profile_path: &PathBuf,
path: &PathBuf,
) -> crate::Result<String> {
Ok(path
.strip_prefix(profile_path)
.map_err(|_| {
crate::ErrorKind::FSError(format!(
"Path {path:?} does not correspond to a profile"
))
})?
.components()
.map(|c| c.as_os_str().to_string_lossy().to_string())
.collect::<Vec<_>>()
.join("/"))
) -> crate::Result<SafeRelativeUtf8UnixPathBuf> {
Ok(SafeRelativeUtf8UnixPathBuf::try_from(
path.strip_prefix(profile_path)
.map_err(|_| {
crate::ErrorKind::FSError(format!(
"Path {path:?} does not correspond to a profile"
))
})?
.components()
.map(|c| c.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/"),
)?)
}
/// Run Minecraft using a profile and the default credentials, logged in credentials,
@@ -896,7 +899,15 @@ pub async fn create_mrpack_json(
.collect();
Some(Ok(PackFile {
path,
path: match path.try_into() {
Ok(path) => path,
Err(_) => {
return Some(Err(crate::ErrorKind::OtherError(
"Invalid file path in project".into(),
)
.as_error()));
}
},
hashes,
env: Some(env),
downloads,