diff --git a/apps/frontend/src/components/ui/create-project-version/components/VersionFileRow.vue b/apps/frontend/src/components/ui/create-project-version/components/VersionFileRow.vue index 486561dcb7..4c869b137c 100644 --- a/apps/frontend/src/components/ui/create-project-version/components/VersionFileRow.vue +++ b/apps/frontend/src/components/ui/create-project-version/components/VersionFileRow.vue @@ -98,7 +98,6 @@ const versionTypes = computed( () => [ !editingVersion && { class: 'text-sm', value: 'primary', label: fileTypeLabels.primary }, - { class: 'text-sm', value: 'unknown', label: fileTypeLabels.unknown }, isDatapackProject.value && { class: 'text-sm', value: 'required-resource-pack', diff --git a/apps/labrinth/src/routes/v3/version_creation.rs b/apps/labrinth/src/routes/v3/version_creation.rs index 6a0b43eb39..8954559758 100644 --- a/apps/labrinth/src/routes/v3/version_creation.rs +++ b/apps/labrinth/src/routes/v3/version_creation.rs @@ -980,6 +980,7 @@ pub async fn upload_file( loaders.clone(), file_type, version_fields.to_vec(), + dependencies, &mut *transaction, redis, ) diff --git a/apps/labrinth/src/validate/fabric.rs b/apps/labrinth/src/validate/fabric.rs index 3d634e8f1b..1ce4f95c75 100644 --- a/apps/labrinth/src/validate/fabric.rs +++ b/apps/labrinth/src/validate/fabric.rs @@ -1,3 +1,5 @@ +use crate::database::models::legacy_loader_fields::MinecraftGameVersion; +use crate::models::projects::Loader; use crate::validate::{ SupportedGameVersions, ValidationError, ValidationResult, validate_pack_formats, @@ -7,6 +9,10 @@ use zip::ZipArchive; pub struct FabricValidator; +const FABRIC_18W43B_TIMESTAMP: i64 = 1_540_393_350; +const LEGACY_FABRIC_1_3_TIMESTAMP: i64 = 1_343_253_600; +const LEGACY_FABRIC_1_13_2_TIMESTAMP: i64 = 1_540_208_467; + impl super::Validator for FabricValidator { fn get_file_extensions(&self) -> &[&str] { &["jar"] @@ -20,6 +26,35 @@ impl super::Validator for FabricValidator { SupportedGameVersions::All } + fn ensure_required_loaders( + &self, + archive: &mut ZipArchive>, + loaders: &[Loader], + ) -> Result<(), ValidationError> { + if loaders.iter().any(|loader| loader.0 == "bta-babric") { + return Ok(()); + } + + let manifest = match archive.by_name("fabric.mod.json") { + Ok(manifest) => manifest, + Err(zip::result::ZipError::FileNotFound) => return Ok(()), + Err(error) => return Err(error.into()), + }; + + let Ok(metadata) = + serde_json::from_reader::<_, serde_json::Value>(manifest) + else { + return Ok(()); + }; + + if has_bta_loader_dependency(&metadata["depends"]["fabricloader"]) { + return Err(ValidationError::InvalidInput( + "files whose `fabric.mod.json` requires a `fabricloader` version containing `-bta.` must include the `bta-babric` loader".into(), + )); + } + Ok(()) + } + fn validate( &self, archive: &mut ZipArchive>, @@ -33,3 +68,48 @@ impl super::Validator for FabricValidator { Ok(validate_pack_formats(archive)) } } + +fn has_bta_loader_dependency(requirement: &serde_json::Value) -> bool { + match requirement { + serde_json::Value::String(range) => range.contains("-bta."), + serde_json::Value::Array(ranges) => { + ranges.iter().any(has_bta_loader_dependency) + } + _ => false, + } +} + +pub(super) fn validate_game_versions( + loaders: &[Loader], + game_versions: &[MinecraftGameVersion], +) -> Result<(), ValidationError> { + for loader in loaders { + let valid = !game_versions.is_empty() + && game_versions.iter().all(|version| match loader.0.as_str() { + "fabric" => { + version.created.timestamp() >= FABRIC_18W43B_TIMESTAMP + } + "legacy-fabric" => (LEGACY_FABRIC_1_3_TIMESTAMP + ..=LEGACY_FABRIC_1_13_2_TIMESTAMP) + .contains(&version.created.timestamp()), + "babric" | "bta-babric" => version.version == "b1.7.3", + _ => true, + }); + let requirement = match loader.0.as_str() { + "fabric" => "`18w43b` or later", + "legacy-fabric" => "between `1.3` and `1.13.2`, inclusive", + "babric" | "bta-babric" => "`b1.7.3` only", + _ => continue, + }; + if !valid { + return Err(ValidationError::InvalidInput( + format!( + "the `{}` loader requires minecraft versions {requirement}", + loader.0, + ) + .into(), + )); + } + } + Ok(()) +} diff --git a/apps/labrinth/src/validate/mod.rs b/apps/labrinth/src/validate/mod.rs index fed2a8d077..2b2115d3d3 100644 --- a/apps/labrinth/src/validate/mod.rs +++ b/apps/labrinth/src/validate/mod.rs @@ -1,9 +1,13 @@ use crate::database::PgTransaction; use crate::database::models::DatabaseError; use crate::database::models::legacy_loader_fields::MinecraftGameVersion; -use crate::database::models::loader_fields::VersionField; +use crate::database::models::loader_fields::{ + Loader as DBLoader, VersionField, +}; +use crate::database::models::version_item::DependencyBuilder; +use crate::models::ids::ProjectId; use crate::models::pack::PackFormat; -use crate::models::projects::{FileType, Loader}; +use crate::models::projects::{DependencyType, FileType, Loader}; use crate::validate::datapack::DataPackValidator; use crate::validate::fabric::FabricValidator; use crate::validate::forge::{ForgeValidator, LegacyForgeValidator}; @@ -14,6 +18,7 @@ use crate::validate::plugin::*; use crate::validate::quilt::QuiltValidator; use crate::validate::resourcepack::{PackValidator, TexturePackValidator}; use crate::validate::rift::RiftValidator; +use crate::validate::risugami::RisugamiValidator; use crate::validate::shader::{ CanvasShaderValidator, CoreShaderValidator, ShaderValidator, }; @@ -38,8 +43,11 @@ pub mod project; mod quilt; mod resourcepack; mod rift; +mod risugami; mod shader; +const HALPLIBE_PROJECT_ID: &str = "IIu8YulV"; + #[derive(Error, Debug)] pub enum ValidationError { #[error("Unable to read Zip Archive: {0}")] @@ -96,6 +104,14 @@ pub trait Validator: Sync { fn get_supported_loaders(&self) -> &[&str]; fn get_supported_game_versions(&self) -> SupportedGameVersions; + fn ensure_required_loaders( + &self, + _archive: &mut ZipArchive>, + _loaders: &[Loader], + ) -> Result<(), ValidationError> { + Ok(()) + } + fn validate( &self, archive: &mut ZipArchive>, @@ -140,11 +156,13 @@ static VALIDATORS: &[&dyn Validator] = &[ &BungeeCordValidator, &VelocityValidator, &SpongeValidator, + &GeyserValidator, &CanvasShaderValidator, &ShaderValidator, &CoreShaderValidator, &DataPackValidator, &RiftValidator, + &RisugamiValidator, &NeoForgeValidator, ]; @@ -179,6 +197,7 @@ pub async fn validate_file( loaders: Vec, file_type: Option, version_fields: Vec, + dependencies: &[DependencyBuilder], transaction: &mut PgTransaction<'_>, redis: &RedisPool, ) -> Result { @@ -190,6 +209,14 @@ pub async fn validate_file( MinecraftGameVersion::list(None, None, &mut *transaction, redis) .await?; + let available_loaders = DBLoader::list(&mut *transaction, redis).await?; + + validate_file_type_for_loaders(file_type, &loaders, &available_loaders)?; + + validate_dependencies(&loaders, dependencies)?; + + fabric::validate_game_versions(&loaders, &game_versions)?; + validate_minecraft_file( data, file_extension, @@ -201,6 +228,84 @@ pub async fn validate_file( .await } +fn validate_file_type_for_loaders( + file_type: Option, + loaders: &[Loader], + available_loaders: &[DBLoader], +) -> Result<(), ValidationError> { + if matches!( + file_type, + Some(FileType::SourcesJar | FileType::DevJar | FileType::JavadocJar) + ) { + let supports_jar_files = available_loaders + .iter() + .filter(|loader| { + loaders.iter().any(|selected| selected.0 == loader.loader) + }) + .flat_map(|loader| &loader.supported_project_types) + .any(|project_type| { + matches!(project_type.as_str(), "mod" | "plugin") + }); + if !supports_jar_files { + return Err(ValidationError::InvalidInput( + "sources, dev, and javadoc jars are only supported for mods and plugins".into(), + )); + } + } + Ok(()) +} + +fn validate_dependencies( + loaders: &[Loader], + dependencies: &[DependencyBuilder], +) -> Result<(), ValidationError> { + if loaders + .iter() + .any(|loader| matches!(loader.0.as_str(), "bta-babric" | "mrpack")) + { + return Ok(()); + } + if dependencies.iter().any(|dependency| { + dependency.dependency_type != DependencyType::Incompatible.as_str() + && dependency.project_id.is_some_and(|id| { + ProjectId::from(id).to_string() == HALPLIBE_PROJECT_ID + }) + }) { + return Err(ValidationError::InvalidInput( + "versions that depend on `halplibe` must include the `bta-babric` loader".into(), + )); + } + Ok(()) +} + +fn validate_additional_jar( + file: &MaybeProtectedZipFile, + file_extension: &str, + file_type: FileType, +) -> Result { + if file_extension != "jar" { + return Err(ValidationError::InvalidInput( + "sources, dev, and javadoc jars must use the `.jar` extension" + .into(), + )); + } + + let MaybeProtectedZipFile::Unprotected(archive) = file else { + return Err(ValidationError::InvalidInput( + "additional jars must be readable jar archives".into(), + )); + }; + + if file_type == FileType::SourcesJar + && !archive.file_names().any(|name| name.ends_with(".java")) + { + return Err(ValidationError::InvalidInput( + "sources jars must contain at least one `.java` file".into(), + )); + } + Ok(ValidationResult::Pass) +} + async fn validate_minecraft_file( data: Bytes, file_extension: String, @@ -218,6 +323,12 @@ async fn validate_minecraft_file( }, }; + if let MaybeProtectedZipFile::Unprotected(archive) = &mut zip { + for validator in VALIDATORS { + validator.ensure_required_loaders(archive, &loaders)?; + } + } + if let Some(file_type) = file_type { match file_type { FileType::RequiredResourcePack | FileType::OptionalResourcePack => { @@ -234,7 +345,9 @@ async fn validate_minecraft_file( )) }; } - FileType::DevJar | FileType::SourcesJar | FileType::JavadocJar => {}, + FileType::DevJar | FileType::SourcesJar | FileType::JavadocJar => { + return validate_additional_jar(&zip, &file_extension, file_type); + }, FileType::Unknown => {} } } diff --git a/apps/labrinth/src/validate/plugin.rs b/apps/labrinth/src/validate/plugin.rs index 4f637c66f2..51391cc0a8 100644 --- a/apps/labrinth/src/validate/plugin.rs +++ b/apps/labrinth/src/validate/plugin.rs @@ -1,9 +1,43 @@ +use crate::models::projects::Loader; use crate::validate::{ SupportedGameVersions, ValidationError, ValidationResult, }; use std::io::Cursor; use zip::ZipArchive; +/// Checks loader metadata without imposing file-format requirements on Geyser uploads. +pub struct GeyserValidator; + +impl super::Validator for GeyserValidator { + fn get_file_extensions(&self) -> &[&str] { + &[] + } + + fn get_supported_loaders(&self) -> &[&str] { + &[] + } + + fn get_supported_game_versions(&self) -> SupportedGameVersions { + SupportedGameVersions::All + } + + fn ensure_required_loaders( + &self, + archive: &mut ZipArchive>, + loaders: &[Loader], + ) -> Result<(), ValidationError> { + if archive.file_names().any(|name| name == "extension.yml") + && !loaders.iter().any(|loader| loader.0 == "geyser") + { + return Err(ValidationError::InvalidInput( + "files containing `extension.yml` in the root directory must use the Geyser Extension loader".into(), + )); + } + + Ok(()) + } +} + pub struct PluginYmlValidator; impl super::Validator for PluginYmlValidator { diff --git a/apps/labrinth/src/validate/risugami.rs b/apps/labrinth/src/validate/risugami.rs new file mode 100644 index 0000000000..b83afa03f6 --- /dev/null +++ b/apps/labrinth/src/validate/risugami.rs @@ -0,0 +1,61 @@ +use crate::validate::{ + SupportedGameVersions, ValidationError, ValidationResult, +}; +use std::io::Cursor; +use zip::ZipArchive; + +pub struct RisugamiValidator; + +impl super::Validator for RisugamiValidator { + fn get_file_extensions(&self) -> &[&str] { + &["zip", "jar"] + } + + fn get_supported_loaders(&self) -> &[&str] { + &["modloader"] + } + + fn get_supported_game_versions(&self) -> SupportedGameVersions { + SupportedGameVersions::All + } + + fn validate( + &self, + archive: &mut ZipArchive>, + ) -> Result { + if archive.file_names().any(|name| name == "instance.cfg") + && archive.file_names().any(|name| name == "mmc-pack.json") + { + return Err(ValidationError::InvalidInput( + "multimc and prism modpacks cannot be uploaded as risugami's modloader mods".into(), + )); + } + + if archive.file_names().any(|name| name == "level.dat") { + return Err(ValidationError::InvalidInput( + "minecraft worlds cannot be uploaded as risugami's modloader mods".into(), + )); + } + + for name in archive.file_names() { + let Some((_, extension)) = name.rsplit_once('.') else { + continue; + }; + if ["mcpack", "mcworld", "mctemplate"] + .iter() + .any(|blocked| extension.eq_ignore_ascii_case(blocked)) + { + return Err(ValidationError::InvalidInput( + "bedrock packs cannot be uploaded as risugami's modloader mods".into(), + )); + } + if extension.eq_ignore_ascii_case("sk") { + return Err(ValidationError::InvalidInput( + "skript packs cannot be uploaded as risugami's modloader mods".into(), + )); + } + } + + Ok(ValidationResult::Pass) + } +}