diff --git a/Dockerfile b/Dockerfile index 9fdd1f452..577f08f2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.52.1 as build +FROM rust:1.55.0 as build ENV PKG_CONFIG_ALLOW_CROSS=1 WORKDIR /usr/src/labrinth diff --git a/sqlx-data.json b/sqlx-data.json index d68fbc658..6d2f34403 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -1707,68 +1707,6 @@ "nullable": [] } }, - "59db001df11e9730d089eca19a73ca6f934c6fd199e6411955072c767f9a2551": { - "query": "\n SELECT n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type,\n ARRAY_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route || ', ' || na.action_route_method) actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.id = $1\n GROUP BY n.id, n.user_id;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "user_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "text", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "link", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "created", - "type_info": "Timestamptz" - }, - { - "ordinal": 5, - "name": "read", - "type_info": "Bool" - }, - { - "ordinal": 6, - "name": "notification_type", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "actions", - "type_info": "TextArray" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - null - ] - } - }, "5a03c653f1ff3339a01422ee4267a66157e6da9a51cc7d9beb0f87d59c3a444c": { "query": "\n SELECT d.dependent_id, d.dependency_id, d.mod_dependency_id\n FROM versions v\n INNER JOIN dependencies d ON d.dependent_id = v.id\n WHERE v.mod_id = $1\n ", "describe": { @@ -2763,26 +2701,6 @@ ] } }, - "7e73d3a17807f57ba6def5ff718e6dcb3a65ef8da653d839560b24635334cf05": { - "query": "\n SELECT m.title FROM mods m\n WHERE id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "title", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - } - }, "7f1696cee355c03f474fda2283669c60046833db88b3e2befd62a1fea7a12c70": { "query": "\n INSERT INTO downloads (\n version_id, identifier\n )\n VALUES (\n $1, $2\n )\n ", "describe": { @@ -3105,32 +3023,6 @@ "nullable": [] } }, - "8c25a870b9306d653caaa4c324122ecd928796107b9d2fcdeaba82c7fcbbbebc": { - "query": "\n SELECT m.title, m.id FROM mods m\n WHERE m.team_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false - ] - } - }, "8d491f3ccbddbd1e1bbea62d04090b2214d10182e3bfac7d8374ac183514f352": { "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.published published,\n m.updated updated,\n m.team_id team_id, m.license license, m.slug slug,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, pt.name project_type_name, u.username username,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT lo.loader, ',') loaders, STRING_AGG(DISTINCT gv.version, ',') versions,\n STRING_AGG(DISTINCT mg.image_url, ',') gallery\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n LEFT OUTER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id\n LEFT OUTER JOIN game_versions gv ON gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n INNER JOIN statuses s ON s.id = m.status\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN licenses l ON m.license = l.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $2\n INNER JOIN users u ON tm.user_id = u.id\n WHERE m.id = $1\n GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id, u.id;\n ", "describe": { @@ -3303,6 +3195,38 @@ "nullable": [] } }, + "9348309884811e8b22f33786ae7c0f259f37f3c90e545f00761a641570107160": { + "query": "\n SELECT m.title title, m.id id, pt.name project_type\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n WHERE m.team_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "project_type", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false + ] + } + }, "94a823b6e8b2610d72843008706c448432aab21690b4727aea77ad687a98f634": { "query": "\n DELETE FROM dependencies WHERE mod_dependency_id = NULL AND dependency_id = NULL\n ", "describe": { @@ -3871,6 +3795,32 @@ ] } }, + "adbe17a5ad3cea333b30b5d6111aff713a8f7dc79ded21f5ba942c4f1108aa8f": { + "query": "\n SELECT m.title title, pt.name project_type\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n WHERE m.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "project_type", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false + ] + } + }, "b030a9e0fdb75eee8ee50aafdcb6063a073e2aa53cc70d40ed46437c1d0dfe80": { "query": "\n INSERT INTO mods_gallery (\n mod_id, image_url, featured, title, description\n )\n VALUES (\n $1, $2, $3, $4, $5\n )\n ", "describe": { @@ -5642,6 +5592,62 @@ ] } }, + "ea96ab7c1290f4caddcef8ecf2aec0216654faca05ff760ffa553ad3e32827f5": { + "query": "\n SELECT n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type\n FROM notifications n\n WHERE n.id = $1\n GROUP BY n.id, n.user_id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "text", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "link", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "read", + "type_info": "Bool" + }, + { + "ordinal": 6, + "name": "notification_type", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true + ] + } + }, "ebef881a0dae70e990814e567ed3de9565bb29b772782bc974c953af195fd6d7": { "query": "\n SELECT n.id FROM notifications n\n WHERE n.user_id = $1\n ", "describe": { diff --git a/src/database/models/categories.rs b/src/database/models/categories.rs index ddd4288ac..8a9105b80 100644 --- a/src/database/models/categories.rs +++ b/src/database/models/categories.rs @@ -14,6 +14,7 @@ pub struct Loader { pub supported_project_types: Vec, } +#[derive(Clone)] pub struct GameVersion { pub id: GameVersionId, pub version: String, diff --git a/src/database/models/notification_item.rs b/src/database/models/notification_item.rs index 56c383769..f23bc25ba 100644 --- a/src/database/models/notification_item.rs +++ b/src/database/models/notification_item.rs @@ -123,10 +123,8 @@ impl Notification { let (notifications, actions) = futures::join!( sqlx::query!( " - SELECT n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type, - ARRAY_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route || ', ' || na.action_route_method) actions + SELECT n.user_id, n.title, n.text, n.link, n.created, n.read, n.type notification_type FROM notifications n - LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id WHERE n.id = $1 GROUP BY n.id, n.user_id; ", diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index 8a757e8ac..6aba3087b 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -662,7 +662,7 @@ impl Version { for hash in hashes? { let entry = hashes_map .entry(FileId(hash.file_id)) - .or_insert(HashMap::new()); + .or_insert_with(HashMap::new); if let Some(raw_hash) = hash.hash { entry.insert(hash.algorithm, raw_hash.into_bytes()); @@ -695,8 +695,8 @@ impl Version { dependencies: dependencies? .into_iter() .map(|x| QueryDependency { - project_id: x.mod_dependency_id.map(|x| ProjectId(x)), - version_id: x.dependency_id.map(|x| VersionId(x)), + project_id: x.mod_dependency_id.map(ProjectId), + version_id: x.dependency_id.map(VersionId), dependency_type: x.dependency_type, }) .collect(), diff --git a/src/health/pod.rs b/src/health/pod.rs index 87986146e..5ed55c430 100644 --- a/src/health/pod.rs +++ b/src/health/pod.rs @@ -10,8 +10,8 @@ pub struct PodInfo { impl PodInfo { pub fn new() -> Self { Self { - pod_name: dotenv::var("POD_NAME").unwrap_or("DEV".to_string()), - node_name: dotenv::var("NODE_NAME").unwrap_or("self-hosted".to_string()), + pod_name: dotenv::var("POD_NAME").unwrap_or_else(|_| "DEV".to_string()), + node_name: dotenv::var("NODE_NAME").unwrap_or_else(|_| "self-hosted".to_string()), pod_id: Arc::new(RwLock::new(None)), } } diff --git a/src/health/scheduler.rs b/src/health/scheduler.rs index f2a465579..e7f5a19b2 100644 --- a/src/health/scheduler.rs +++ b/src/health/scheduler.rs @@ -114,7 +114,7 @@ where fn call(&mut self, req: ServiceRequest) -> Self::Future { // The request has started. - let pattern_or_path = req.match_pattern().unwrap_or("unknown".to_string()); + let pattern_or_path = req.match_pattern().unwrap_or_else(|| "unknown".to_string()); let counter = self .counters .current_requests diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 8a200cbe9..8a198355d 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -129,7 +129,7 @@ pub fn teams_config(cfg: &mut web::ServiceConfig) { pub fn notifications_config(cfg: &mut web::ServiceConfig) { cfg.service(notifications::notifications_get); - cfg.service(notifications::notification_delete); + cfg.service(notifications::notifications_delete); cfg.service( web::scope("notification") diff --git a/src/routes/project_creation.rs b/src/routes/project_creation.rs index c370d63b0..9ee2683a0 100644 --- a/src/routes/project_creation.rs +++ b/src/routes/project_creation.rs @@ -510,7 +510,7 @@ pub async fn project_create_inner( &*project_create_data.project_type, version_data.loaders.clone(), version_data.game_versions.clone(), - &all_game_versions, + all_game_versions.clone(), false, &mut transaction, ) diff --git a/src/routes/projects.rs b/src/routes/projects.rs index 8c21e68fb..245a51d9e 100644 --- a/src/routes/projects.rs +++ b/src/routes/projects.rs @@ -373,7 +373,7 @@ pub async fn project_edit( } } - let status_id = database::models::StatusId::get_id(&status, &mut *transaction) + let status_id = database::models::StatusId::get_id(status, &mut *transaction) .await? .ok_or_else(|| { ApiError::InvalidInputError( @@ -423,17 +423,15 @@ pub async fn project_edit( .await?; for category in categories { - let category_id = database::models::categories::Category::get_id( - &category, - &mut *transaction, - ) - .await? - .ok_or_else(|| { - ApiError::InvalidInputError(format!( - "Category {} does not exist.", - category.clone() - )) - })?; + let category_id = + database::models::categories::Category::get_id(category, &mut *transaction) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError(format!( + "Category {} does not exist.", + category.clone() + )) + })?; sqlx::query!( " diff --git a/src/routes/teams.rs b/src/routes/teams.rs index 0ec99531b..ce454901e 100644 --- a/src/routes/teams.rs +++ b/src/routes/teams.rs @@ -218,7 +218,9 @@ pub async fn add_team_member( let result = sqlx::query!( " - SELECT m.title, m.id FROM mods m + SELECT m.title title, m.id id, pt.name project_type + FROM mods m + INNER JOIN project_types pt ON pt.id = m.project_type WHERE m.team_id = $1 ", team_id as crate::database::models::ids::TeamId @@ -234,7 +236,7 @@ pub async fn add_team_member( "Team invite from {} to join the team for project {}", current_user.username, result.title ), - link: format!("project/{}", ProjectId(result.id as u64)), + link: format!("/{}/{}", result.project_type, ProjectId(result.id as u64)), actions: vec![ NotificationActionBuilder { title: "Accept".to_string(), @@ -361,7 +363,7 @@ pub async fn transfer_ownership( TeamMember::edit_team_member( id.into(), current_user.id.into(), - None, + Some(Permissions::ALL), Some(crate::models::teams::DEFAULT_ROLE.to_string()), None, &mut transaction, diff --git a/src/routes/users.rs b/src/routes/users.rs index 9ac7a242e..81bce74e9 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -323,8 +323,7 @@ pub async fn user_icon_edit( } let bytes = - read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB") - .await?; + read_from_payload(&mut payload, 2097152, "Icons must be smaller than 2MiB").await?; let upload_data = file_host .upload_file( diff --git a/src/routes/version_creation.rs b/src/routes/version_creation.rs index d59271e1d..f255228b5 100644 --- a/src/routes/version_creation.rs +++ b/src/routes/version_creation.rs @@ -288,7 +288,7 @@ async fn version_create_inner( &*project_type, version_data.loaders, version_data.game_versions, - &all_game_versions, + all_game_versions.clone(), false, &mut transaction, ) @@ -308,8 +308,10 @@ async fn version_create_inner( let result = sqlx::query!( " - SELECT m.title FROM mods m - WHERE id = $1 + SELECT m.title title, pt.name project_type + FROM mods m + INNER JOIN project_types pt ON pt.id = m.project_type + WHERE m.id = $1 ", builder.project_id as crate::database::models::ids::ProjectId ) @@ -344,7 +346,10 @@ async fn version_create_inner( result.title, version_data.version_number.clone() ), - link: format!("project/{}/version/{}", project_id, version_id), + link: format!( + "/{}/{}/version/{}", + result.project_type, project_id, version_id + ), actions: vec![], } .insert_many(users, &mut *transaction) @@ -544,7 +549,7 @@ async fn upload_file_to_version_inner( .into_iter() .map(GameVersion) .collect(), - &all_game_versions, + all_game_versions.clone(), true, &mut transaction, ) @@ -579,7 +584,7 @@ pub async fn upload_file( project_type: &str, loaders: Vec, game_versions: Vec, - all_game_versions: &[models::categories::GameVersion], + all_game_versions: Vec, ignore_primary: bool, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result<(), CreateError> { @@ -614,13 +619,14 @@ pub async fn upload_file( } let validation_result = validate_file( - &data, - file_extension, - project_type, + data.clone().into(), + file_extension.to_string(), + project_type.to_string(), loaders, game_versions, all_game_versions, - )?; + ) + .await?; let upload_data = file_host .upload_file( diff --git a/src/routes/versions.rs b/src/routes/versions.rs index b802c27e1..d19911692 100644 --- a/src/routes/versions.rs +++ b/src/routes/versions.rs @@ -259,8 +259,8 @@ pub async fn version_edit( let builders = dependencies .iter() .map(|x| database::models::version_item::DependencyBuilder { - project_id: x.project_id.clone().map(|x| x.into()), - version_id: x.version_id.clone().map(|x| x.into()), + project_id: x.project_id.map(|x| x.into()), + version_id: x.version_id.map(|x| x.into()), dependency_type: x.dependency_type.to_string(), }) .collect::>(); diff --git a/src/search/indexing/mod.rs b/src/search/indexing/mod.rs index 80ff2580c..af5828b46 100644 --- a/src/search/indexing/mod.rs +++ b/src/search/indexing/mod.rs @@ -80,7 +80,7 @@ async fn update_index_helper<'a>( name: &'static str, rule: &'static str, ) -> Result, IndexingError> { - update_index(&client, name, { + update_index(client, name, { let mut rules = default_rules(); rules.push_back(rule); rules.into() @@ -159,11 +159,11 @@ async fn add_to_index(index: Index<'_>, mods: &[UploadSearchProject]) -> Result< async fn create_and_add_to_index<'a>( client: &'a Client<'a>, - projects: &'a Vec, + projects: &'a [UploadSearchProject], name: &'static str, rule: &'static str, ) -> Result<(), IndexingError> { - let index = create_index(&client, name, || { + let index = create_index(client, name, || { let mut relevance_rules = default_rules(); relevance_rules.push_back(rule); relevance_rules.into() diff --git a/src/validate/fabric.rs b/src/validate/fabric.rs index 6038aef41..d9dd3232e 100644 --- a/src/validate/fabric.rs +++ b/src/validate/fabric.rs @@ -28,7 +28,7 @@ impl super::Validator for FabricValidator { fn validate( &self, - archive: &mut ZipArchive>, + archive: &mut ZipArchive>, ) -> Result { archive.by_name("fabric.mod.json").map_err(|_| { ValidationError::InvalidInputError("No fabric.mod.json present for Fabric file.".into()) @@ -39,7 +39,7 @@ impl super::Validator for FabricValidator { .any(|name| name.ends_with("refmap.json") || name.ends_with(".class")) { return Ok(ValidationResult::Warning( - "Fabric mod file is a source file!".into(), + "Fabric mod file is a source file!", )); } diff --git a/src/validate/forge.rs b/src/validate/forge.rs index dd254df2f..43333f7f9 100644 --- a/src/validate/forge.rs +++ b/src/validate/forge.rs @@ -28,7 +28,7 @@ impl super::Validator for ForgeValidator { fn validate( &self, - archive: &mut ZipArchive>, + archive: &mut ZipArchive>, ) -> Result { archive.by_name("META-INF/mods.toml").map_err(|_| { ValidationError::InvalidInputError("No mods.toml present for Forge file.".into()) @@ -36,7 +36,7 @@ impl super::Validator for ForgeValidator { if !archive.file_names().any(|name| name.ends_with(".class")) { return Ok(ValidationResult::Warning( - "Forge mod file is a source file!".into(), + "Forge mod file is a source file!", )); } @@ -71,7 +71,7 @@ impl super::Validator for LegacyForgeValidator { fn validate( &self, - archive: &mut ZipArchive>, + archive: &mut ZipArchive>, ) -> Result { archive.by_name("mcmod.info").map_err(|_| { ValidationError::InvalidInputError("No mcmod.info present for Forge file.".into()) @@ -79,7 +79,7 @@ impl super::Validator for LegacyForgeValidator { if !archive.file_names().any(|name| name.ends_with(".class")) { return Ok(ValidationResult::Warning( - "Forge mod file is a source file!".into(), + "Forge mod file is a source file!", )); } diff --git a/src/validate/mod.rs b/src/validate/mod.rs index 95b9df907..4b217c4ac 100644 --- a/src/validate/mod.rs +++ b/src/validate/mod.rs @@ -21,6 +21,8 @@ pub enum ValidationError { SerdeError(#[from] serde_json::Error), #[error("Invalid Input: {0}")] InvalidInputError(std::borrow::Cow<'static, str>), + #[error("Error while managing threads")] + BlockingError, } #[derive(Eq, PartialEq)] @@ -45,7 +47,7 @@ pub trait Validator: Sync { fn get_supported_game_versions(&self) -> SupportedGameVersions; fn validate( &self, - archive: &mut ZipArchive>, + archive: &mut ZipArchive>, ) -> Result; } @@ -57,48 +59,52 @@ static VALIDATORS: [&dyn Validator; 4] = [ ]; /// The return value is whether this file should be marked as primary or not, based on the analysis of the file -pub fn validate_file( - data: &[u8], - file_extension: &str, - project_type: &str, +pub async fn validate_file( + data: bytes::Bytes, + file_extension: String, + project_type: String, loaders: Vec, game_versions: Vec, - all_game_versions: &[crate::database::models::categories::GameVersion], + all_game_versions: Vec, ) -> Result { - let reader = std::io::Cursor::new(data); - let mut zip = zip::ZipArchive::new(reader)?; + Ok(actix_web::web::block(move || { + let reader = std::io::Cursor::new(data); + let mut zip = zip::ZipArchive::new(reader)?; - let mut visited = false; - for validator in &VALIDATORS { - if validator.get_project_types().contains(&project_type) - && loaders - .iter() - .any(|x| validator.get_supported_loaders().contains(&&*x.0)) - && game_version_supported( - &game_versions, - all_game_versions, - validator.get_supported_game_versions(), - ) - { - if validator.get_file_extensions().contains(&file_extension) { - return validator.validate(&mut zip); - } else { - visited = true; + let mut visited = false; + for validator in &VALIDATORS { + if validator.get_project_types().contains(&&*project_type) + && loaders + .iter() + .any(|x| validator.get_supported_loaders().contains(&&*x.0)) + && game_version_supported( + &game_versions, + &all_game_versions, + validator.get_supported_game_versions(), + ) + { + if validator.get_file_extensions().contains(&&*file_extension) { + return validator.validate(&mut zip); + } else { + visited = true; + } } } - } - if visited { - Err(ValidationError::InvalidInputError( - format!( - "File extension {} is invalid for input file", - file_extension - ) - .into(), - )) - } else { - Ok(ValidationResult::Pass) - } + if visited { + Err(ValidationError::InvalidInputError( + format!( + "File extension {} is invalid for input file", + file_extension + ) + .into(), + )) + } else { + Ok(ValidationResult::Pass) + } + }) + .await + .map_err(|_| ValidationError::BlockingError)?) } fn game_version_supported( diff --git a/src/validate/pack.rs b/src/validate/pack.rs index 621b90729..6d32fb915 100644 --- a/src/validate/pack.rs +++ b/src/validate/pack.rs @@ -1,19 +1,60 @@ +use crate::models::projects::SideType; use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult}; use serde::{Deserialize, Serialize}; use std::io::{Cursor, Read}; use zip::ZipArchive; +use validator::Validate; +use crate::util::validate::validation_errors_to_string; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Validate)] #[serde(rename_all = "camelCase")] pub struct PackFormat<'a> { pub game: &'a str, pub format_version: i32, + #[validate(length(min = 3, max = 512))] pub version_id: &'a str, + #[validate(length(min = 3, max = 512))] pub name: &'a str, + #[validate(length(max = 2048))] pub summary: Option<&'a str>, + #[validate] + pub files: Vec>, pub dependencies: std::collections::HashMap, } +#[derive(Serialize, Deserialize, Validate)] +pub struct PackFile<'a> { + pub path: &'a str, + pub hashes: std::collections::HashMap, + pub env: std::collections::HashMap, + #[validate(custom(function = "validate_download_url"))] + pub downloads: Vec<&'a str>, +} + +fn validate_download_url(values: &Vec<&str>) -> Result<(), validator::ValidationError> { + for value in values { + if !validator::validate_url(*value) { + return Err(validator::ValidationError::new("invalid URL")); + } + } + + Ok(()) +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(rename_all = "camelCase")] +pub enum FileHash { + Sha1, + Sha512, +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Hash)] +#[serde(rename_all = "camelCase")] +pub enum EnvType { + Client, + Server, +} + #[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum PackDependency { @@ -60,7 +101,7 @@ impl super::Validator for PackValidator { fn validate( &self, - archive: &mut ZipArchive>, + archive: &mut ZipArchive>, ) -> Result { let mut file = archive .by_name("index.json") @@ -71,6 +112,10 @@ impl super::Validator for PackValidator { let pack: PackFormat = serde_json::from_str(&contents)?; + pack + .validate() + .map_err(|err| ValidationError::InvalidInputError(validation_errors_to_string(err, None).into()))?; + if pack.game != "minecraft" { return Err(ValidationError::InvalidInputError( format!("Game {0} does not exist!", pack.game).into(),