From d961aa91bce8c44e921741a8e7d71d3fe3679006 Mon Sep 17 00:00:00 2001 From: Jordan Date: Thu, 22 Jun 2023 11:24:03 +0100 Subject: [PATCH] fix: fix regex block masking (#2242) --- .../core/function/mask/BlockMaskBuilder.java | 374 +++++++----------- .../core/math/FastBitSet.java | 4 + .../world/block/FuzzyBlockState.java | 2 +- 3 files changed, 156 insertions(+), 224 deletions(-) diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/function/mask/BlockMaskBuilder.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/function/mask/BlockMaskBuilder.java index 5bfeb766c..2471ff262 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/function/mask/BlockMaskBuilder.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/function/mask/BlockMaskBuilder.java @@ -20,12 +20,11 @@ import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.block.BlockTypes; import com.sk89q.worldedit.world.block.BlockTypesCache; -import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -36,10 +35,12 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.function.BiPredicate; import java.util.function.Predicate; +import java.util.regex.Pattern; import java.util.stream.Collectors; +import static com.google.common.base.Preconditions.checkNotNull; + public class BlockMaskBuilder { private static final Operator GREATER = (a, b) -> a > b; @@ -63,58 +64,97 @@ public class BlockMaskBuilder { this.bitSets = bitSets; } - private boolean filterRegex(BlockType blockType, PropertyKey key, String regex) { + private boolean handleRegex(BlockType blockType, PropertyKey key, String regex, FuzzyStateAllowingBuilder builder) { Property property = blockType.getProperty(key); if (property == null) { return false; } - List values = property.getValues(); boolean result = false; + List values = property.getValues(); for (int i = 0; i < values.size(); i++) { - Object value = values.get(i); - if (!value.toString().matches(regex) && has(blockType, property, i)) { - filter(blockType, property, i); + if (values.get(i).toString().matches(regex)) { + builder.allow(property, i); result = true; } } return result; } - private boolean filterOperator(BlockType blockType, PropertyKey key, Operator operator, CharSequence value) { + private boolean handleOperator( + BlockType blockType, + PropertyKey key, + Operator operator, + CharSequence stringValue, + FuzzyStateAllowingBuilder builder + ) { Property property = blockType.getProperty(key); if (property == null) { return false; } - int index = property.getIndexFor(value); + int index = property.getIndexFor(stringValue); List values = property.getValues(); boolean result = false; for (int i = 0; i < values.size(); i++) { - if (!operator.test(index, i) && has(blockType, property, i)) { - filter(blockType, property, i); + if (operator.test(index, i)) { + builder.allow(property, i); result = true; } } return result; } - private boolean filterRegexOrOperator(BlockType type, PropertyKey key, Operator operator, CharSequence value) { - boolean result = false; - if (!type.hasProperty(key)) { - if (operator == EQUAL) { - result = bitSets[type.getInternalId()] != null; - remove(type); + private boolean handleRegexOrOperator( + BlockType type, + PropertyKey key, + Operator operator, + CharSequence value, + FuzzyStateAllowingBuilder builder + ) { + if (!type.hasProperty(key) && operator == EQUAL) { + return false; + } + if (value.length() == 0) { + return false; + } + if ((operator == EQUAL || operator == EQUAL_OR_NULL) && !StringMan.isAlphanumericUnd(value)) { + return handleRegex(type, key, value.toString(), builder); + } else { + return handleOperator(type, key, operator, value, builder); + } + } + + private void add(FuzzyStateAllowingBuilder builder) { + long[] states = bitSets[builder.getType().getInternalId()]; + if (states == ALL) { + bitSets[builder.getType().getInternalId()] = states = FastBitSet.create(builder.getType().getMaxStateId() + 1); + FastBitSet.unsetAll(states); + } + applyRecursive(0, builder.getType().getInternalId(), builder, states); + } + + private void applyRecursive( + int propertiesIndex, + int state, + FuzzyStateAllowingBuilder builder, + long[] states + ) { + AbstractProperty current = (AbstractProperty) builder.getType().getProperties().get(propertiesIndex); + List values = current.getValues(); + if (propertiesIndex + 1 < builder.getType().getProperties().size()) { + for (int i = 0; i < values.size(); i++) { + if (builder.allows(current) || builder.allows(current, i)) { + int newState = current.modifyIndex(state, i); + applyRecursive(propertiesIndex + 1, newState, builder, states); + } } } else { - if (value.length() == 0) { - return result; - } - if ((operator == EQUAL || operator == EQUAL_OR_NULL) && !StringMan.isAlphanumericUnd(value)) { - result = filterRegex(type, key, value.toString()); - } else { - result = filterOperator(type, key, operator, value); + for (int i = 0; i < values.size(); i++) { + if (builder.allows(current) || builder.allows(current, i)) { + int index = current.modifyIndex(state, i) >> BlockTypesCache.BIT_OFFSET; + FastBitSet.set(states, index); + } } } - return result; } public BlockMaskBuilder addRegex(final String input) throws InputParseException { @@ -130,26 +170,28 @@ public class BlockMaskBuilder { charSequence.setString(input); charSequence.setSubstring(0, propStart); - BlockType type = null; - List blockTypeList = null; + List blockTypeList; + List builders; if (StringMan.isAlphanumericUnd(charSequence)) { - type = BlockTypes.parse(charSequence.toString()); + BlockType type = BlockTypes.parse(charSequence.toString()); + blockTypeList = Collections.singletonList(type); + builders = Collections.singletonList(new FuzzyStateAllowingBuilder(type)); add(type); } else { String regex = charSequence.toString(); blockTypeList = new ArrayList<>(); - for (BlockType myType : BlockTypesCache.values) { - if (myType.getId().matches(regex)) { - blockTypeList.add(myType); - add(myType); + builders = new ArrayList<>(); + Pattern pattern = Pattern.compile("(minecraft:)?" + regex); + for (BlockType type : BlockTypesCache.values) { + if (pattern.matcher(type.getId()).find()) { + blockTypeList.add(type); + builders.add(new FuzzyStateAllowingBuilder(type)); + add(type); } } if (blockTypeList.isEmpty()) { throw new InputParseException(Caption.of("fawe.error.no-block-found", TextComponent.of(input))); } - if (blockTypeList.size() == 1) { - type = blockTypeList.get(0); - } } // Empty string charSequence.setSubstring(0, 0); @@ -169,11 +211,11 @@ public class BlockMaskBuilder { } case ']', ',' -> { charSequence.setSubstring(last, i); - if (key == null && PropertyKey.getByName(charSequence) == null) { + if (key == null && (key = PropertyKey.getByName(charSequence)) == null) { suggest( input, charSequence.toString(), - type != null ? Collections.singleton(type) : blockTypeList + blockTypeList ); } if (operator == null) { @@ -182,34 +224,20 @@ public class BlockMaskBuilder { () -> Arrays.asList("=", "~", "!", "<", ">", "<=", ">=") ); } - boolean filtered = false; - if (type != null) { - filtered = filterRegexOrOperator(type, key, operator, charSequence); - } else { - for (BlockType myType : blockTypeList) { - filtered |= filterRegexOrOperator(myType, key, operator, charSequence); + for (int index = 0; index < blockTypeList.size(); index++) { + if (!handleRegexOrOperator( + blockTypeList.get(index), + key, + operator, + charSequence, + builders.get(index) + )) { + // If we cannot find a matching property for all to mask, do not mask the block + blockTypeList.remove(index); + builders.remove(index); + index--; } } - if (!filtered) { - String value = charSequence.toString(); - final PropertyKey fKey = key; - Collection types = type != null ? Collections.singleton(type) : blockTypeList; - throw new SuggestInputParseException(Caption.of("fawe.error.no-value-for-input", input), () -> { - HashSet values = new HashSet<>(); - types.stream().filter(t -> t.hasProperty(fKey)).forEach(t -> { - Property p = t.getProperty(fKey); - for (int j = 0; j < p.getValues().size(); j++) { - if (has(t, p, j)) { - String o = p.getValues().get(j).toString(); - if (o.startsWith(value)) { - values.add(o); - } - } - } - }); - return new ArrayList<>(values); - }); - } // Reset state key = null; operator = null; @@ -235,7 +263,7 @@ public class BlockMaskBuilder { suggest( input, charSequence.toString(), - type != null ? Collections.singleton(type) : blockTypeList + blockTypeList ); } } @@ -245,13 +273,18 @@ public class BlockMaskBuilder { } } } + for (FuzzyStateAllowingBuilder builder : builders) { + if (builder.allows()) { + add(builder); + } + } } else { if (StringMan.isAlphanumericUnd(input)) { add(BlockTypes.parse(input)); } else { boolean success = false; for (BlockType myType : BlockTypesCache.values) { - if (myType.getId().matches(input)) { + if (myType.getId().matches("(minecraft:)?" + input)) { add(myType); success = true; } @@ -275,16 +308,6 @@ public class BlockMaskBuilder { return this; } - private boolean has(BlockType type, Property property, int index) { - AbstractProperty prop = (AbstractProperty) property; - long[] states = bitSets[type.getInternalId()]; - if (states == null) { - return false; - } - int localI = index << prop.getBitOffset() >> BlockTypesCache.BIT_OFFSET; - return (states == ALL || FastBitSet.get(states, localI)); - } - private void suggest(String input, String property, Collection finalTypes) throws InputParseException { throw new SuggestInputParseException(Caption.of("worldedit.error.parser.unknown-property", property, input), () -> { Set keys = PropertyKeySet.empty(); @@ -381,42 +404,6 @@ public class BlockMaskBuilder { return this; } - @SuppressWarnings({"unchecked", "rawtypes"}) - public BlockMaskBuilder filter( - Predicate typePredicate, - BiPredicate, T>> allowed - ) { - for (int i = 0; i < bitSets.length; i++) { - long[] states = bitSets[i]; - if (states == null) { - continue; - } - BlockType type = BlockTypes.get(i); - if (!typePredicate.test(type)) { - bitSets[i] = null; - continue; - } - List> properties = (List>) type.getProperties(); - for (AbstractProperty prop : properties) { - List values = prop.getValues(); - for (int j = 0; j < values.size(); j++) { - int localI = j << prop.getBitOffset() >> BlockTypesCache.BIT_OFFSET; - if (states == ALL || FastBitSet.get(states, localI)) { - if (!allowed.test(type, new AbstractMap.SimpleEntry(prop, values.get(j)))) { - if (states == ALL) { - bitSets[i] = states = FastBitSet.create(type.getMaxStateId() + 1); - FastBitSet.setAll(states); - } - FastBitSet.clear(states, localI); - reset(false); - } - } - } - } - } - return this; - } - public BlockMaskBuilder add(BlockType type) { bitSets[type.getInternalId()] = ALL; return this; @@ -478,117 +465,6 @@ public class BlockMaskBuilder { return this; } - @SuppressWarnings({"unchecked", "rawtypes"}) - public BlockMaskBuilder addAll( - Predicate typePredicate, - BiPredicate, ?>> propPredicate - ) { - for (int i = 0; i < bitSets.length; i++) { - long[] states = bitSets[i]; - if (states == ALL) { - continue; - } - BlockType type = BlockTypes.get(i); - if (!typePredicate.test(type)) { - continue; - } - for (AbstractProperty prop : (List>) type.getProperties()) { - List values = prop.getValues(); - for (int j = 0; j < values.size(); j++) { - int localI = j << prop.getBitOffset() >> BlockTypesCache.BIT_OFFSET; - if (states == null || !FastBitSet.get(states, localI)) { - if (propPredicate.test(type, new AbstractMap.SimpleEntry(prop, values.get(j)))) { - if (states == null) { - bitSets[i] = states = FastBitSet.create(type.getMaxStateId() + 1); - } - FastBitSet.set(states, localI); - reset(false); - } - } - } - } - } - return this; - } - - public BlockMaskBuilder add(BlockType type, Property property, int index) { - AbstractProperty prop = (AbstractProperty) property; - long[] states = bitSets[type.getInternalId()]; - if (states == ALL) { - return this; - } - - List values = property.getValues(); - int localI = index << prop.getBitOffset() >> BlockTypesCache.BIT_OFFSET; - if (states == null || !FastBitSet.get(states, localI)) { - if (states == null) { - bitSets[type.getInternalId()] = states = FastBitSet.create(type.getMaxStateId() + 1); - } - set(type, states, property, index); - reset(false); - } - return this; - } - - public BlockMaskBuilder filter(BlockType type, Property property, int index) { - AbstractProperty prop = (AbstractProperty) property; - long[] states = bitSets[type.getInternalId()]; - if (states == null) { - return this; - } - List values = property.getValues(); - int localI = index << prop.getBitOffset() >> BlockTypesCache.BIT_OFFSET; - if (states == ALL || FastBitSet.get(states, localI)) { - if (states == ALL) { - bitSets[type.getInternalId()] = states = FastBitSet.create(type.getMaxStateId() + 1); - FastBitSet.setAll(states); - } - clear(type, states, property, index); - reset(false); - } - return this; - } - - private void applyRecursive(List properties, int propertiesIndex, int state, long[] states, boolean set) { - AbstractProperty current = (AbstractProperty) properties.get(propertiesIndex); - List values = current.getValues(); - if (propertiesIndex + 1 < properties.size()) { - for (int i = 0; i < values.size(); i++) { - int newState = current.modifyIndex(state, i); - applyRecursive(properties, propertiesIndex + 1, newState, states, set); - } - } else { - for (int i = 0; i < values.size(); i++) { - int index = current.modifyIndex(state, i) >> BlockTypesCache.BIT_OFFSET; - if (set) { - FastBitSet.set(states, index); - } else { - FastBitSet.clear(states, index); - } - } - } - } - - private void set(BlockType type, long[] bitSet, Property property, int index) { - FastBitSet.set(bitSet, index); - if (type.getProperties().size() > 1) { - ArrayList properties = new ArrayList<>(type.getProperties()); - properties.remove(property); - int state = ((AbstractProperty) property).modifyIndex(type.getInternalId(), index); - applyRecursive(properties, 0, state, bitSet, true); - } - } - - private void clear(BlockType type, long[] bitSet, Property property, int index) { - FastBitSet.clear(bitSet, index); - if (type.getProperties().size() > 1) { - ArrayList properties = new ArrayList<>(type.getProperties()); - properties.remove(property); - int state = ((AbstractProperty) property).modifyIndex(type.getInternalId(), index); - applyRecursive(properties, 0, state, bitSet, false); - } - } - public BlockMaskBuilder optimize() { if (!optimizedStates) { for (int i = 0; i < bitSets.length; i++) { @@ -667,4 +543,56 @@ public class BlockMaskBuilder { } + private static class FuzzyStateAllowingBuilder { + + private final BlockType type; + private final Map, List> masked = new HashMap<>(); + + private FuzzyStateAllowingBuilder(BlockType type) { + this.type = type; + } + + private BlockType getType() { + return this.type; + } + + private List> getMaskedProperties() { + return masked + .entrySet() + .stream() + .filter(e -> !e.getValue().isEmpty()) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } + + private void allow(Property property, int index) { + checkNotNull(property); + if (!type.hasProperty(property.getKey())) { + throw new IllegalArgumentException(String.format( + "Property %s cannot be applied to block type %s", + property.getName(), + type.getId() + )); + } + masked.computeIfAbsent(property, k -> new ArrayList<>()).add(index); + } + + private boolean allows() { + //noinspection SimplifyStreamApiCallChains - Marginally faster like this + return !masked.isEmpty() && !masked.values().stream().anyMatch(List::isEmpty); + } + + private boolean allows(Property property) { + return !masked.containsKey(property); + } + + private boolean allows(Property property, int index) { + if (!masked.containsKey(property)) { + return true; + } + return masked.get(property).contains(index); + } + + } + } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/math/FastBitSet.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/math/FastBitSet.java index 92754b669..fa7199d83 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/math/FastBitSet.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/math/FastBitSet.java @@ -69,6 +69,10 @@ public class FastBitSet { Arrays.fill(bits, -1L); } + public static void unsetAll(long[] bits) { + Arrays.fill(bits, 0); + } + public static void and(long[] bits, final long[] other) { final int end = Math.min(other.length, bits.length); for (int i = 0; i < end; ++i) { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/FuzzyBlockState.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/FuzzyBlockState.java index ee4ab12ad..b32fee41f 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/FuzzyBlockState.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/FuzzyBlockState.java @@ -182,7 +182,7 @@ public class FuzzyBlockState extends BlockState { checkNotNull(property); checkNotNull(value); checkNotNull(type, "The type must be set before the properties!"); - type.getProperty(property.getName()); // Verify the property is valid for this type + checkNotNull(type.getProperty(property.getName())); // Verify the property is valid for this type values.put(property, value); return this; }