diff --git a/patches/api/0479-Improve-Recipe-validation.patch b/patches/api/0479-Improve-Recipe-validation.patch new file mode 100644 index 0000000000..47d68fde83 --- /dev/null +++ b/patches/api/0479-Improve-Recipe-validation.patch @@ -0,0 +1,252 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Jake Potrebic +Date: Sun, 12 May 2024 10:42:42 -0700 +Subject: [PATCH] Improve Recipe validation + + +diff --git a/src/main/java/org/bukkit/inventory/CookingRecipe.java b/src/main/java/org/bukkit/inventory/CookingRecipe.java +index f7fa79393aef40027446b78bac8e9490cfafd8bc..a64dae15db89038f3eb599f2a41edf45990f4fda 100644 +--- a/src/main/java/org/bukkit/inventory/CookingRecipe.java ++++ b/src/main/java/org/bukkit/inventory/CookingRecipe.java +@@ -44,10 +44,10 @@ public abstract class CookingRecipe implements Recipe, + * @param cookingTime The cooking time (in ticks) + */ + public CookingRecipe(@NotNull NamespacedKey key, @NotNull ItemStack result, @NotNull RecipeChoice input, float experience, int cookingTime) { +- Preconditions.checkArgument(result.getType() != Material.AIR, "Recipe must have non-AIR result."); ++ Preconditions.checkArgument(!result.isEmpty(), "Recipe cannot have an empty result."); // Paper + this.key = key; + this.output = new ItemStack(result); +- this.ingredient = input; ++ this.ingredient = input.validate().clone(); // Paper + this.experience = experience; + this.cookingTime = cookingTime; + } +@@ -84,7 +84,7 @@ public abstract class CookingRecipe implements Recipe, + */ + @NotNull + public T setInputChoice(@NotNull RecipeChoice input) { +- this.ingredient = input; ++ this.ingredient = input.validate().clone(); // Paper + return (T) this; + } + +diff --git a/src/main/java/org/bukkit/inventory/CraftingRecipe.java b/src/main/java/org/bukkit/inventory/CraftingRecipe.java +index e4bf772f7e06f38215bee68f089b15a4fcb12817..37024b4736dd3897490ca51d08cf07901b01d59f 100644 +--- a/src/main/java/org/bukkit/inventory/CraftingRecipe.java ++++ b/src/main/java/org/bukkit/inventory/CraftingRecipe.java +@@ -18,7 +18,7 @@ public abstract class CraftingRecipe implements Recipe, Keyed { + + protected CraftingRecipe(@NotNull NamespacedKey key, @NotNull ItemStack result) { + Preconditions.checkArgument(key != null, "key cannot be null"); +- Preconditions.checkArgument(result.getType() != Material.AIR, "Recipe must have non-AIR result."); ++ Preconditions.checkArgument(!result.isEmpty(), "Recipe cannot have an empty result."); // Paper + this.key = key; + this.output = new ItemStack(result); + } +diff --git a/src/main/java/org/bukkit/inventory/MerchantRecipe.java b/src/main/java/org/bukkit/inventory/MerchantRecipe.java +index 39f9766a03d420340d79841197f75c8b1dd49f4a..4e59f5176fd6cf92457ad750081c253a58790b61 100644 +--- a/src/main/java/org/bukkit/inventory/MerchantRecipe.java ++++ b/src/main/java/org/bukkit/inventory/MerchantRecipe.java +@@ -79,6 +79,7 @@ public class MerchantRecipe implements Recipe { + this(result, uses, maxUses, experienceReward, villagerExperience, priceMultiplier, 0, 0, ignoreDiscounts); + } + public MerchantRecipe(@NotNull ItemStack result, int uses, int maxUses, boolean experienceReward, int villagerExperience, float priceMultiplier, int demand, int specialPrice, boolean ignoreDiscounts) { ++ Preconditions.checkArgument(!result.isEmpty(), "Recipe cannot have an empty result."); // Paper + this.ignoreDiscounts = ignoreDiscounts; + // Paper end + this.result = result; +@@ -101,11 +102,12 @@ public class MerchantRecipe implements Recipe { + @NotNull + @Override + public ItemStack getResult() { +- return result; ++ return result.clone(); // Paper + } + + public void addIngredient(@NotNull ItemStack item) { + Preconditions.checkState(ingredients.size() < 2, "MerchantRecipe can only have maximum 2 ingredients"); ++ Preconditions.checkArgument(!item.isEmpty(), "Recipe cannot have an empty itemstack ingredient."); // Paper + ingredients.add(item.clone()); + } + +@@ -117,6 +119,7 @@ public class MerchantRecipe implements Recipe { + Preconditions.checkState(ingredients.size() <= 2, "MerchantRecipe can only have maximum 2 ingredients"); + this.ingredients = new ArrayList(); + for (ItemStack item : ingredients) { ++ Preconditions.checkArgument(!item.isEmpty(), "Recipe cannot have an empty itemstack ingredient."); // Paper + this.ingredients.add(item.clone()); + } + } +diff --git a/src/main/java/org/bukkit/inventory/RecipeChoice.java b/src/main/java/org/bukkit/inventory/RecipeChoice.java +index db8bcc66bdc4bedfffb4705db6338eda4c0ad29a..c143b0d8b97d514566bac8413d0346cf50822aeb 100644 +--- a/src/main/java/org/bukkit/inventory/RecipeChoice.java ++++ b/src/main/java/org/bukkit/inventory/RecipeChoice.java +@@ -35,6 +35,13 @@ public interface RecipeChoice extends Predicate, Cloneable { + @Override + boolean test(@NotNull ItemStack itemStack); + ++ // Paper start - check valid ingredients ++ @org.jetbrains.annotations.ApiStatus.Internal ++ default @NotNull RecipeChoice validate() { ++ return this; ++ } ++ // Paper end - check valid ingredients ++ + /** + * Represents a choice of multiple matching Materials. + */ +@@ -185,7 +192,12 @@ public interface RecipeChoice extends Predicate, Cloneable { + public ExactChoice clone() { + try { + ExactChoice clone = (ExactChoice) super.clone(); +- clone.choices = new ArrayList<>(choices); ++ // Paper start - properly clone ++ clone.choices = new ArrayList<>(this.choices.size()); ++ for (ItemStack choice : this.choices) { ++ clone.choices.add(choice.clone()); ++ } ++ // Paper end - properly clone + return clone; + } catch (CloneNotSupportedException ex) { + throw new AssertionError(ex); +@@ -232,5 +244,15 @@ public interface RecipeChoice extends Predicate, Cloneable { + public String toString() { + return "ExactChoice{" + "choices=" + choices + '}'; + } ++ ++ // Paper start - check valid ingredients ++ @Override ++ public @NotNull RecipeChoice validate() { ++ if (this.choices.stream().anyMatch(s -> s.getType().isAir())) { ++ throw new IllegalArgumentException("RecipeChoice.ExactChoice cannot contain air"); ++ } ++ return this; ++ } ++ // Paper end - check valid ingredients + } + } +diff --git a/src/main/java/org/bukkit/inventory/ShapedRecipe.java b/src/main/java/org/bukkit/inventory/ShapedRecipe.java +index da878c6d4928ddbc16b50ace86d992685a2b7873..0547505987ee954adbac0cbe5a6ada3dfaedf034 100644 +--- a/src/main/java/org/bukkit/inventory/ShapedRecipe.java ++++ b/src/main/java/org/bukkit/inventory/ShapedRecipe.java +@@ -166,14 +166,15 @@ public class ShapedRecipe extends CraftingRecipe { + public ShapedRecipe setIngredient(char key, @NotNull RecipeChoice ingredient) { + Preconditions.checkArgument(ingredients.containsKey(key), "Symbol does not appear in the shape:", key); + +- ingredients.put(key, ingredient); ++ ingredients.put(key, ingredient.validate().clone()); // Paper + return this; + } + + // Paper start + @NotNull + public ShapedRecipe setIngredient(char key, @NotNull ItemStack item) { +- return setIngredient(key, new RecipeChoice.ExactChoice(item)); ++ Preconditions.checkArgument(!item.getType().isAir(), "Item cannot be air"); // Paper ++ return setIngredient(key, new RecipeChoice.ExactChoice(item.clone())); // Paper + } + // Paper end + +diff --git a/src/main/java/org/bukkit/inventory/ShapelessRecipe.java b/src/main/java/org/bukkit/inventory/ShapelessRecipe.java +index beb798482479c58a8628c314b510ab6349576ce8..6a8195ca224790e3130ec9923188e7b585797263 100644 +--- a/src/main/java/org/bukkit/inventory/ShapelessRecipe.java ++++ b/src/main/java/org/bukkit/inventory/ShapelessRecipe.java +@@ -131,7 +131,7 @@ public class ShapelessRecipe extends CraftingRecipe { + public ShapelessRecipe addIngredient(@NotNull RecipeChoice ingredient) { + Preconditions.checkArgument(ingredients.size() + 1 <= 9, "Shapeless recipes cannot have more than 9 ingredients"); + +- ingredients.add(ingredient); ++ ingredients.add(ingredient.validate().clone()); // Paper + return this; + } + +@@ -144,6 +144,8 @@ public class ShapelessRecipe extends CraftingRecipe { + @NotNull + public ShapelessRecipe addIngredient(int count, @NotNull ItemStack item) { + Preconditions.checkArgument(ingredients.size() + count <= 9, "Shapeless recipes cannot have more than 9 ingredients"); ++ Preconditions.checkArgument(!item.getType().isAir(), "Item cannot be air"); // Paper ++ item = item.clone(); // Paper + while (count-- > 0) { + ingredients.add(new RecipeChoice.ExactChoice(item)); + } +diff --git a/src/main/java/org/bukkit/inventory/SmithingRecipe.java b/src/main/java/org/bukkit/inventory/SmithingRecipe.java +index 1ef9a715a2736e88a16083c6873803a8bd6bcf29..99e40588c10141391762f37b5a326f8df06e5276 100644 +--- a/src/main/java/org/bukkit/inventory/SmithingRecipe.java ++++ b/src/main/java/org/bukkit/inventory/SmithingRecipe.java +@@ -44,12 +44,13 @@ public class SmithingRecipe implements Recipe, Keyed { + */ + @Deprecated + public SmithingRecipe(@NotNull NamespacedKey key, @NotNull ItemStack result, @NotNull RecipeChoice base, @NotNull RecipeChoice addition, boolean copyDataComponents) { ++ com.google.common.base.Preconditions.checkArgument(!result.isEmpty(), "Recipe cannot have an empty result."); // Paper + this.copyDataComponents = copyDataComponents; + // Paper end + this.key = key; + this.result = result; +- this.base = base; +- this.addition = addition; ++ this.base = base.validate().clone(); // Paper ++ this.addition = addition.validate().clone(); // Paper + } + + /** +diff --git a/src/main/java/org/bukkit/inventory/SmithingTransformRecipe.java b/src/main/java/org/bukkit/inventory/SmithingTransformRecipe.java +index 68e7132d77151b7b8312638d8bb79ea59e2fa5a6..d3a7070a51d15531fb6f917ca87196dfa08f83aa 100644 +--- a/src/main/java/org/bukkit/inventory/SmithingTransformRecipe.java ++++ b/src/main/java/org/bukkit/inventory/SmithingTransformRecipe.java +@@ -21,7 +21,7 @@ public class SmithingTransformRecipe extends SmithingRecipe { + */ + public SmithingTransformRecipe(@NotNull NamespacedKey key, @NotNull ItemStack result, @NotNull RecipeChoice template, @NotNull RecipeChoice base, @NotNull RecipeChoice addition) { + super(key, result, base, addition); +- this.template = template; ++ this.template = template.validate().clone(); // Paper + } + // Paper start + /** +diff --git a/src/main/java/org/bukkit/inventory/SmithingTrimRecipe.java b/src/main/java/org/bukkit/inventory/SmithingTrimRecipe.java +index ce36bb5b030f17e11f74e987235be143c1925aa7..6316112074a0708734106ca9de5ef968df52ce0e 100644 +--- a/src/main/java/org/bukkit/inventory/SmithingTrimRecipe.java ++++ b/src/main/java/org/bukkit/inventory/SmithingTrimRecipe.java +@@ -21,7 +21,7 @@ public class SmithingTrimRecipe extends SmithingRecipe implements ComplexRecipe + */ + public SmithingTrimRecipe(@NotNull NamespacedKey key, @NotNull RecipeChoice template, @NotNull RecipeChoice base, @NotNull RecipeChoice addition) { + super(key, new ItemStack(Material.AIR), base, addition); +- this.template = template; ++ this.template = template.validate().clone(); // Paper + } + // Paper start + /** +@@ -35,7 +35,7 @@ public class SmithingTrimRecipe extends SmithingRecipe implements ComplexRecipe + */ + public SmithingTrimRecipe(@NotNull NamespacedKey key, @NotNull RecipeChoice template, @NotNull RecipeChoice base, @NotNull RecipeChoice addition, boolean copyDataComponents) { + super(key, new ItemStack(Material.AIR), base, addition, copyDataComponents); +- this.template = template; ++ this.template = template.validate().clone(); // Paper + } + // Paper end + +diff --git a/src/main/java/org/bukkit/inventory/StonecuttingRecipe.java b/src/main/java/org/bukkit/inventory/StonecuttingRecipe.java +index bc3440eb72127824b3961fbdae583bb61385f65e..802c23a86d301a5336a97a8256182da7d792ec1d 100644 +--- a/src/main/java/org/bukkit/inventory/StonecuttingRecipe.java ++++ b/src/main/java/org/bukkit/inventory/StonecuttingRecipe.java +@@ -35,10 +35,10 @@ public class StonecuttingRecipe implements Recipe, Keyed { + * @param input The input choices. + */ + public StonecuttingRecipe(@NotNull NamespacedKey key, @NotNull ItemStack result, @NotNull RecipeChoice input) { +- Preconditions.checkArgument(result.getType() != Material.AIR, "Recipe must have non-AIR result."); ++ Preconditions.checkArgument(!result.isEmpty(), "Recipe cannot have an empty result."); // Paper + this.key = key; + this.output = new ItemStack(result); +- this.ingredient = input; ++ this.ingredient = input.validate().clone(); // Paper + } + + /** +@@ -73,7 +73,7 @@ public class StonecuttingRecipe implements Recipe, Keyed { + */ + @NotNull + public StonecuttingRecipe setInputChoice(@NotNull RecipeChoice input) { +- this.ingredient = input; ++ this.ingredient = input.validate().clone(); // Paper + return (StonecuttingRecipe) this; + } +