diff --git a/_typos.toml b/_typos.toml index eb3c35ff04f..bbc7bc5e48c 100644 --- a/_typos.toml +++ b/_typos.toml @@ -34,5 +34,7 @@ Cpy = "Cpy" ND = "ND" [default.extend-identifiers] +# BTA dependency project name +HalpLibe = "HalpLibe" # Constant from the `zip` crate ZIP64_BYTES_THR = "ZIP64_BYTES_THR" 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 486561dcb7f..4c869b137cc 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/fixtures/dummy_data.sql b/apps/labrinth/fixtures/dummy_data.sql index 90d580ea3f5..17b27a8abf1 100644 --- a/apps/labrinth/fixtures/dummy_data.sql +++ b/apps/labrinth/fixtures/dummy_data.sql @@ -32,15 +32,16 @@ INSERT INTO loaders (id, loader) VALUES (9, 'datapack'); INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id) SELECT l.id, pt.id -FROM loaders l -CROSS JOIN project_types pt +FROM + loaders AS l + CROSS JOIN project_types AS pt WHERE (l.loader IN ('bukkit', 'waterfall') AND pt.name = 'plugin') OR (l.loader = 'datapack' AND pt.name = 'datapack'); INSERT INTO loaders_project_types_games (loader_id, project_type_id, game_id) SELECT lpt.joining_loader_id, lpt.joining_project_type_id, 1 -FROM loaders_project_types lpt +FROM loaders_project_types AS lpt WHERE lpt.joining_loader_id IN (7, 8, 9); -- Adds dummies to mrpack_loaders @@ -74,6 +75,11 @@ ON CONFLICT DO NOTHING; -- Sample game versions, loaders, categories -- Game versions is '2' INSERT INTO loader_field_enum_values (enum_id, value, metadata, created) +VALUES + (2, '1.3', '{"type":"snapshot","major":false}', '2012-07-25 22:00:00+00'), + (2, '1.13.2', '{"type":"release","major":false}', '2018-10-22 11:41:07+00'), + (2, '18w43b', '{"type":"snapshot","major":false}', '2018-10-24 15:02:30+00'); +INSERT INTO loader_field_enum_values (enum_id, value, metadata, created) VALUES (2, '1.20.1', '{"type":"release","major":false}', '2021-08-18 15:48:58.435729+00'); INSERT INTO loader_field_enum_values (enum_id, value, metadata, created) VALUES (2, '1.20.2', '{"type":"release","major":false}', '2021-08-18 15:48:59.435729+00'); diff --git a/apps/labrinth/src/routes/v3/version_creation.rs b/apps/labrinth/src/routes/v3/version_creation.rs index 08097b4d675..65953332e84 100644 --- a/apps/labrinth/src/routes/v3/version_creation.rs +++ b/apps/labrinth/src/routes/v3/version_creation.rs @@ -1003,6 +1003,7 @@ pub async fn upload_file( loaders.clone(), file_type, version_fields.to_vec(), + dependencies, &mut *transaction, redis, ) diff --git a/apps/labrinth/src/test/dummy_data.rs b/apps/labrinth/src/test/dummy_data.rs index 4b8038be1a7..880dfbe1fcb 100644 --- a/apps/labrinth/src/test/dummy_data.rs +++ b/apps/labrinth/src/test/dummy_data.rs @@ -21,7 +21,7 @@ use super::{ use super::{database::USER_USER_ID, get_json_val_str}; -pub const DUMMY_DATA_UPDATE: i64 = 9; +pub const DUMMY_DATA_UPDATE: i64 = 10; pub const DUMMY_CATEGORIES: &[&str] = &[ "combat", diff --git a/apps/labrinth/src/validate/fabric.rs b/apps/labrinth/src/validate/fabric.rs index 3d634e8f1b4..4fe380915e4 100644 --- a/apps/labrinth/src/validate/fabric.rs +++ b/apps/labrinth/src/validate/fabric.rs @@ -1,7 +1,10 @@ +use crate::database::models::legacy_loader_fields::MinecraftGameVersion; +use crate::models::projects::Loader; use crate::validate::{ SupportedGameVersions, ValidationError, ValidationResult, validate_pack_formats, }; +use eyre::eyre; use std::io::Cursor; use zip::ZipArchive; @@ -20,6 +23,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 +65,71 @@ 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], + all_game_versions: &[MinecraftGameVersion], +) -> Result<(), ValidationError> { + let release_date = |name: &str| { + all_game_versions + .iter() + .find(|version| version.version == name) + .map(|version| version.created) + .ok_or_else(|| { + eyre!( + "missing minecraft game version `{name}` required for loader validation" + ) + }) + }; + + for loader in loaders { + let (valid, requirement) = match loader.0.as_str() { + "fabric" => { + let minimum = release_date("18w43b")?; + ( + game_versions + .iter() + .all(|version| version.created >= minimum), + "`18w43b` or later", + ) + } + "legacy-fabric" => { + let supported = release_date("1.3")?..=release_date("1.13.2")?; + ( + game_versions + .iter() + .all(|version| supported.contains(&version.created)), + "between `1.3` and `1.13.2`, inclusive", + ) + } + "babric" | "bta-babric" => ( + game_versions + .iter() + .all(|version| version.version == "b1.7.3"), + "`b1.7.3` only", + ), + _ => continue, + }; + if game_versions.is_empty() || !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 1bb9815e029..a899048600d 100644 --- a/apps/labrinth/src/validate/mod.rs +++ b/apps/labrinth/src/validate/mod.rs @@ -1,8 +1,12 @@ use crate::database::PgTransaction; 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}; @@ -13,6 +17,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, }; @@ -37,8 +42,12 @@ pub mod project; mod quilt; mod resourcepack; mod rift; +mod risugami; mod shader; +// BTA loader must exist if HalpLibe project is selected as a dependency +const HALPLIBE_PROJECT_ID: &str = "IIu8YulV"; + #[derive(Error, Debug)] pub enum ValidationError { #[error("Unable to read Zip Archive: {0}")] @@ -95,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>, @@ -139,11 +156,13 @@ static VALIDATORS: &[&dyn Validator] = &[ &BungeeCordValidator, &VelocityValidator, &SpongeValidator, + &GeyserValidator, &CanvasShaderValidator, &ShaderValidator, &CoreShaderValidator, &DataPackValidator, &RiftValidator, + &RisugamiValidator, &NeoForgeValidator, ]; @@ -178,6 +197,7 @@ pub async fn validate_file( loaders: Vec, file_type: Option, version_fields: Vec, + dependencies: &[DependencyBuilder], transaction: &mut PgTransaction<'_>, redis: &RedisPool, ) -> Result { @@ -189,6 +209,18 @@ 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, + &all_game_versions, + )?; + validate_minecraft_file( data, file_extension, @@ -200,6 +232,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, @@ -217,6 +327,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 => { @@ -233,7 +349,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 4f637c66f20..51391cc0a8d 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 00000000000..b83afa03f68 --- /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) + } +} diff --git a/apps/labrinth/tests/loader_fields.rs b/apps/labrinth/tests/loader_fields.rs index 7d2eab670a4..4e5f30bb456 100644 --- a/apps/labrinth/tests/loader_fields.rs +++ b/apps/labrinth/tests/loader_fields.rs @@ -543,7 +543,10 @@ async fn get_loader_fields_variants() { "1.20.4", "1.20.3", "1.20.2", - "1.20.1" + "1.20.1", + "18w43b", + "1.13.2", + "1.3" ] ); }) diff --git a/apps/labrinth/tests/v2/tags.rs b/apps/labrinth/tests/v2/tags.rs index 8126d7a4eed..ee92894de4d 100644 --- a/apps/labrinth/tests/v2/tags.rs +++ b/apps/labrinth/tests/v2/tags.rs @@ -38,7 +38,10 @@ async fn get_tags() { "1.20.4", "1.20.3", "1.20.2", - "1.20.1" + "1.20.1", + "18w43b", + "1.13.2", + "1.3" ] .iter() .map(|s| s.to_string())