You've already forked AstralRinth
forked from didirus/AstralRinth
feat(labrinth): allow protected resource and data packs to pass validation (#3792)
* fix(labrinth): return version artifact size exceeded error eagerly Now we don't wait until the result memory buffer has grown to a size greater than the maximum allowed, and instead we return such an error before the buffer is grown with the current chunk, which should reduce memory usage. * fix(labrinth): proper supported game versions range for datapacks * feat(labrinth): allow protected resource and data packs to pass validation
This commit is contained in:
committed by
GitHub
parent
97e4d8e132
commit
fb30c0ba2b
@@ -32,11 +32,13 @@ pub async fn read_from_field(
|
|||||||
) -> Result<BytesMut, CreateError> {
|
) -> Result<BytesMut, CreateError> {
|
||||||
let mut bytes = BytesMut::new();
|
let mut bytes = BytesMut::new();
|
||||||
while let Some(chunk) = field.next().await {
|
while let Some(chunk) = field.next().await {
|
||||||
if bytes.len() >= cap {
|
let chunk = chunk?;
|
||||||
|
|
||||||
|
if bytes.len().saturating_add(chunk.len()) > cap {
|
||||||
return Err(CreateError::InvalidInput(String::from(err_msg)));
|
return Err(CreateError::InvalidInput(String::from(err_msg)));
|
||||||
} else {
|
|
||||||
bytes.extend_from_slice(&chunk?);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bytes.extend_from_slice(&chunk);
|
||||||
}
|
}
|
||||||
Ok(bytes)
|
Ok(bytes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::validate::{
|
use crate::validate::{
|
||||||
SupportedGameVersions, ValidationError, ValidationResult,
|
MaybeProtectedZipFile, PLAUSIBLE_PACK_REGEX, SupportedGameVersions,
|
||||||
|
ValidationError, ValidationResult,
|
||||||
};
|
};
|
||||||
use std::io::Cursor;
|
use chrono::DateTime;
|
||||||
use zip::ZipArchive;
|
|
||||||
|
|
||||||
pub struct DataPackValidator;
|
pub struct DataPackValidator;
|
||||||
|
|
||||||
@@ -16,19 +16,29 @@ impl super::Validator for DataPackValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
fn get_supported_game_versions(&self) -> SupportedGameVersions {
|
||||||
SupportedGameVersions::All
|
// Time since release of 17w43a, 2017-10-25, which introduced datapacks
|
||||||
|
SupportedGameVersions::PastDate(
|
||||||
|
DateTime::from_timestamp(1508889600, 0).unwrap(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(
|
fn validate_maybe_protected_zip(
|
||||||
&self,
|
&self,
|
||||||
archive: &mut ZipArchive<Cursor<bytes::Bytes>>,
|
file: &mut MaybeProtectedZipFile,
|
||||||
) -> Result<ValidationResult, ValidationError> {
|
) -> Result<ValidationResult, ValidationError> {
|
||||||
if archive.by_name("pack.mcmeta").is_err() {
|
if match file {
|
||||||
return Ok(ValidationResult::Warning(
|
MaybeProtectedZipFile::Unprotected(archive) => {
|
||||||
|
archive.by_name("pack.mcmeta").is_ok()
|
||||||
|
}
|
||||||
|
MaybeProtectedZipFile::MaybeProtected { data, .. } => {
|
||||||
|
PLAUSIBLE_PACK_REGEX.is_match(data)
|
||||||
|
}
|
||||||
|
} {
|
||||||
|
Ok(ValidationResult::Pass)
|
||||||
|
} else {
|
||||||
|
Ok(ValidationResult::Warning(
|
||||||
"No pack.mcmeta present for datapack file. Tip: Make sure pack.mcmeta is in the root directory of your datapack!",
|
"No pack.mcmeta present for datapack file. Tip: Make sure pack.mcmeta is in the root directory of your datapack!",
|
||||||
));
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ValidationResult::Pass)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,14 @@ use crate::validate::rift::RiftValidator;
|
|||||||
use crate::validate::shader::{
|
use crate::validate::shader::{
|
||||||
CanvasShaderValidator, CoreShaderValidator, ShaderValidator,
|
CanvasShaderValidator, CoreShaderValidator, ShaderValidator,
|
||||||
};
|
};
|
||||||
|
use bytes::Bytes;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use std::io::Cursor;
|
use std::io::{self, Cursor};
|
||||||
|
use std::mem;
|
||||||
|
use std::sync::LazyLock;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
|
use zip::result::ZipError;
|
||||||
|
|
||||||
mod datapack;
|
mod datapack;
|
||||||
mod fabric;
|
mod fabric;
|
||||||
@@ -80,14 +84,43 @@ pub enum SupportedGameVersions {
|
|||||||
Custom(Vec<MinecraftGameVersion>),
|
Custom(Vec<MinecraftGameVersion>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum MaybeProtectedZipFile {
|
||||||
|
Unprotected(ZipArchive<Cursor<Bytes>>),
|
||||||
|
MaybeProtected { read_error: ZipError, data: Bytes },
|
||||||
|
}
|
||||||
|
|
||||||
pub trait Validator: Sync {
|
pub trait Validator: Sync {
|
||||||
fn get_file_extensions(&self) -> &[&str];
|
fn get_file_extensions(&self) -> &[&str];
|
||||||
fn get_supported_loaders(&self) -> &[&str];
|
fn get_supported_loaders(&self) -> &[&str];
|
||||||
fn get_supported_game_versions(&self) -> SupportedGameVersions;
|
fn get_supported_game_versions(&self) -> SupportedGameVersions;
|
||||||
|
|
||||||
fn validate(
|
fn validate(
|
||||||
&self,
|
&self,
|
||||||
archive: &mut ZipArchive<Cursor<bytes::Bytes>>,
|
archive: &mut ZipArchive<Cursor<bytes::Bytes>>,
|
||||||
) -> Result<ValidationResult, ValidationError>;
|
) -> Result<ValidationResult, ValidationError> {
|
||||||
|
// By default, any non-protected ZIP archive is valid
|
||||||
|
let _ = archive;
|
||||||
|
Ok(ValidationResult::Pass)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_maybe_protected_zip(
|
||||||
|
&self,
|
||||||
|
file: &mut MaybeProtectedZipFile,
|
||||||
|
) -> Result<ValidationResult, ValidationError> {
|
||||||
|
// By default, validate that the ZIP file is not protected, and if so,
|
||||||
|
// delegate to the inner validate method with a known good archive
|
||||||
|
match file {
|
||||||
|
MaybeProtectedZipFile::Unprotected(archive) => {
|
||||||
|
self.validate(archive)
|
||||||
|
}
|
||||||
|
MaybeProtectedZipFile::MaybeProtected { read_error, .. } => {
|
||||||
|
Err(ValidationError::Zip(mem::replace(
|
||||||
|
read_error,
|
||||||
|
ZipError::Io(io::Error::other("ZIP archive reading error")),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static ALWAYS_ALLOWED_EXT: &[&str] = &["zip", "txt"];
|
static ALWAYS_ALLOWED_EXT: &[&str] = &["zip", "txt"];
|
||||||
@@ -113,6 +146,29 @@ static VALIDATORS: &[&dyn Validator] = &[
|
|||||||
&NeoForgeValidator,
|
&NeoForgeValidator,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// A regex that matches a potentially protected ZIP archive containing
|
||||||
|
/// a vanilla Minecraft pack, with a requisite `pack.mcmeta` file.
|
||||||
|
///
|
||||||
|
/// Please note that this regex avoids false negatives at the cost of false
|
||||||
|
/// positives being possible, i.e. it may match files that are not actually
|
||||||
|
/// Minecraft packs, but it will not miss packs that the game can load.
|
||||||
|
static PLAUSIBLE_PACK_REGEX: LazyLock<regex::bytes::Regex> =
|
||||||
|
LazyLock::new(|| {
|
||||||
|
regex::bytes::RegexBuilder::new(concat!(
|
||||||
|
r"\x50\x4b\x01\x02", // CEN signature
|
||||||
|
r".{24}", // CEN fields
|
||||||
|
r"[\x0B\x0C]\x00", // CEN file name length
|
||||||
|
r".{16}", // More CEN fields
|
||||||
|
r"pack\.mcmeta/?", // CEN file name
|
||||||
|
r".*", // Rest of CEN entries and records
|
||||||
|
r"\x50\x4b\x05\x06", // EOCD signature
|
||||||
|
))
|
||||||
|
.unicode(false)
|
||||||
|
.dot_matches_new_line(true)
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
/// The return value is whether this file should be marked as primary or not, based on the analysis of the file
|
/// The return value is whether this file should be marked as primary or not, based on the analysis of the file
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn validate_file(
|
pub async fn validate_file(
|
||||||
@@ -144,7 +200,7 @@ pub async fn validate_file(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn validate_minecraft_file(
|
async fn validate_minecraft_file(
|
||||||
data: bytes::Bytes,
|
data: Bytes,
|
||||||
file_extension: String,
|
file_extension: String,
|
||||||
loaders: Vec<Loader>,
|
loaders: Vec<Loader>,
|
||||||
game_versions: Vec<MinecraftGameVersion>,
|
game_versions: Vec<MinecraftGameVersion>,
|
||||||
@@ -152,13 +208,18 @@ async fn validate_minecraft_file(
|
|||||||
file_type: Option<FileType>,
|
file_type: Option<FileType>,
|
||||||
) -> Result<ValidationResult, ValidationError> {
|
) -> Result<ValidationResult, ValidationError> {
|
||||||
actix_web::web::block(move || {
|
actix_web::web::block(move || {
|
||||||
let reader = Cursor::new(data);
|
let mut zip = match ZipArchive::new(Cursor::new(Bytes::clone(&data))) {
|
||||||
let mut zip = ZipArchive::new(reader)?;
|
Ok(zip) => MaybeProtectedZipFile::Unprotected(zip),
|
||||||
|
Err(read_error) => MaybeProtectedZipFile::MaybeProtected {
|
||||||
|
read_error,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(file_type) = file_type {
|
if let Some(file_type) = file_type {
|
||||||
match file_type {
|
match file_type {
|
||||||
FileType::RequiredResourcePack | FileType::OptionalResourcePack => {
|
FileType::RequiredResourcePack | FileType::OptionalResourcePack => {
|
||||||
return PackValidator.validate(&mut zip);
|
return PackValidator.validate_maybe_protected_zip(&mut zip);
|
||||||
}
|
}
|
||||||
FileType::Unknown => {}
|
FileType::Unknown => {}
|
||||||
}
|
}
|
||||||
@@ -177,7 +238,7 @@ async fn validate_minecraft_file(
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
if validator.get_file_extensions().contains(&&*file_extension) {
|
if validator.get_file_extensions().contains(&&*file_extension) {
|
||||||
let result = validator.validate(&mut zip)?;
|
let result = validator.validate_maybe_protected_zip(&mut zip)?;
|
||||||
match result {
|
match result {
|
||||||
ValidationResult::PassWithPackDataAndFiles { .. } => {
|
ValidationResult::PassWithPackDataAndFiles { .. } => {
|
||||||
saved_result = Some(result);
|
saved_result = Some(result);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::validate::{
|
use crate::validate::{
|
||||||
SupportedGameVersions, ValidationError, ValidationResult,
|
MaybeProtectedZipFile, PLAUSIBLE_PACK_REGEX, SupportedGameVersions,
|
||||||
|
ValidationError, ValidationResult,
|
||||||
};
|
};
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
@@ -23,17 +24,24 @@ impl super::Validator for PackValidator {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(
|
fn validate_maybe_protected_zip(
|
||||||
&self,
|
&self,
|
||||||
archive: &mut ZipArchive<Cursor<bytes::Bytes>>,
|
file: &mut MaybeProtectedZipFile,
|
||||||
) -> Result<ValidationResult, ValidationError> {
|
) -> Result<ValidationResult, ValidationError> {
|
||||||
if archive.by_name("pack.mcmeta").is_err() {
|
if match file {
|
||||||
return Ok(ValidationResult::Warning(
|
MaybeProtectedZipFile::Unprotected(archive) => {
|
||||||
"No pack.mcmeta present for pack file. Tip: Make sure pack.mcmeta is in the root directory of your pack!",
|
archive.by_name("pack.mcmeta").is_ok()
|
||||||
));
|
}
|
||||||
|
MaybeProtectedZipFile::MaybeProtected { data, .. } => {
|
||||||
|
PLAUSIBLE_PACK_REGEX.is_match(data)
|
||||||
|
}
|
||||||
|
} {
|
||||||
|
Ok(ValidationResult::Pass)
|
||||||
|
} else {
|
||||||
|
Ok(ValidationResult::Warning(
|
||||||
|
"No pack.mcmeta present for resourcepack file. Tip: Make sure pack.mcmeta is in the root directory of your pack!",
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ValidationResult::Pass)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use crate::validate::{
|
use crate::validate::{
|
||||||
SupportedGameVersions, ValidationError, ValidationResult,
|
MaybeProtectedZipFile, PLAUSIBLE_PACK_REGEX, SupportedGameVersions,
|
||||||
|
ValidationError, ValidationResult,
|
||||||
};
|
};
|
||||||
use std::io::Cursor;
|
use std::{io::Cursor, sync::LazyLock};
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
|
|
||||||
pub struct ShaderValidator;
|
pub struct ShaderValidator;
|
||||||
@@ -83,25 +84,42 @@ impl super::Validator for CoreShaderValidator {
|
|||||||
SupportedGameVersions::All
|
SupportedGameVersions::All
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate(
|
fn validate_maybe_protected_zip(
|
||||||
&self,
|
&self,
|
||||||
archive: &mut ZipArchive<Cursor<bytes::Bytes>>,
|
file: &mut MaybeProtectedZipFile,
|
||||||
) -> Result<ValidationResult, ValidationError> {
|
) -> Result<ValidationResult, ValidationError> {
|
||||||
if archive.by_name("pack.mcmeta").is_err() {
|
static VANILLA_SHADER_CEN_ENTRY_REGEX: LazyLock<regex::bytes::Regex> =
|
||||||
return Ok(ValidationResult::Warning(
|
LazyLock::new(|| {
|
||||||
"No pack.mcmeta present for pack file. Tip: Make sure pack.mcmeta is in the root directory of your pack!",
|
regex::bytes::RegexBuilder::new(concat!(
|
||||||
));
|
r"\x50\x4b\x01\x02", // CEN signature
|
||||||
};
|
r".{24}", // CEN fields
|
||||||
|
r".{2}", // CEN file name length
|
||||||
|
r".{16}", // More CEN fields
|
||||||
|
r"assets/minecraft/shaders/", // CEN file name
|
||||||
|
))
|
||||||
|
.unicode(false)
|
||||||
|
.dot_matches_new_line(true)
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
if !archive
|
if match file {
|
||||||
.file_names()
|
MaybeProtectedZipFile::Unprotected(archive) => {
|
||||||
.any(|x| x.starts_with("assets/minecraft/shaders/"))
|
archive.by_name("pack.mcmeta").is_ok()
|
||||||
{
|
&& archive
|
||||||
return Ok(ValidationResult::Warning(
|
.file_names()
|
||||||
"No shaders folder present for vanilla shaders.",
|
.any(|x| x.starts_with("assets/minecraft/shaders/"))
|
||||||
));
|
}
|
||||||
|
MaybeProtectedZipFile::MaybeProtected { data, .. } => {
|
||||||
|
PLAUSIBLE_PACK_REGEX.is_match(data)
|
||||||
|
&& VANILLA_SHADER_CEN_ENTRY_REGEX.is_match(data)
|
||||||
|
}
|
||||||
|
} {
|
||||||
|
Ok(ValidationResult::Pass)
|
||||||
|
} else {
|
||||||
|
Ok(ValidationResult::Warning(
|
||||||
|
"No pack.mcmeta or vanilla shaders folder present for pack file. Tip: Make sure pack.mcmeta is in the root directory of your pack!",
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ValidationResult::Pass)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user