/* * This file is part of Mixin, licensed under the MIT License (MIT). * * Copyright (c) SpongePowered * Copyright (c) contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.spongepowered.asm.util; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.objectweb.asm.tree.AnnotationNode; import org.spongepowered.asm.util.throwables.ConstraintViolationException; import org.spongepowered.asm.util.throwables.InvalidConstraintException; /** * Parser for constraints */ public final class ConstraintParser { /** * A constraint. Constraints are parsed from string expressions which are * always of the form: * *
<token>(<constraint>)
* *

token is normalised to uppercase and must be provided by the * environment.

* *

constraint is an integer range specified in one of the * following formats: * *

*
()
*
The token value must be present in the environment, but can have * any value
*
(1234)
*
The token value must be exactly equal to 1234 *
*
(1234+)
     *(1234-)
     *(1234>)
     *
*
All of these variants mean the same thing, and can be read as "1234 * or greater"
*
(<1234)
*
Less than 123
*
(<=1234)
*
Less than or equal to 1234 (equivalent to 1234< * )
*
(>1234)
*
Greater than 1234
*
(>=1234)
*
Greater than or equal to 1234 (equivalent to 1234 * >)
*
(1234-1300)
*
Value must be between 1234 and 1300 (inclusive)
*
(1234+10)
*
Value must be between 1234 and 1234+10 (1234-1244 * inclusive)
*
* *

All whitespace is ignored in constraint declarations. The following * declarations are equivalent:

* *
token(123-456)
     *token   (   123 - 456   )
* *

Multiple constraints should be separated by semicolon (;) * and are conjoined by an implied logical AND operator. That * is: all constraints must pass for the constraint to be considered valid. *

*/ public static class Constraint { public static final Constraint NONE = new Constraint(); private static final Pattern pattern = Pattern.compile("^([A-Z0-9\\-_\\.]+)\\((?:(<|<=|>|>=|=)?([0-9]+)(<|(-)([0-9]+)?|>|(\\+)([0-9]+)?)?)?\\)$"); private final String expr; private String token; private String[] constraint; private int min = Integer.MIN_VALUE; private int max = Integer.MAX_VALUE; private Constraint next; Constraint(String expr) { this.expr = expr; Matcher matcher = Constraint.pattern.matcher(expr); if (!matcher.matches()) { throw new InvalidConstraintException("Constraint syntax was invalid parsing: " + this.expr); } this.token = matcher.group(1); this.constraint = new String[] { matcher.group(2), matcher.group(3), matcher.group(4), matcher.group(5), matcher.group(6), matcher.group(7), matcher.group(8) }; this.parse(); } private Constraint() { this.expr = null; this.token = "*"; this.constraint = new String[0]; } private void parse() { if (!this.has(1)) { return; } this.max = this.min = this.val(1); boolean hasModifier = this.has(0); if (this.has(4)) { if (hasModifier) { throw new InvalidConstraintException("Unexpected modifier '" + this.elem(0) + "' in " + this.expr + " parsing range"); } this.max = this.val(4); if (this.max < this.min) { throw new InvalidConstraintException("Invalid range specified '" + this.max + "' is less than " + this.min + " in " + this.expr); } return; } else if (this.has(6)) { if (hasModifier) { throw new InvalidConstraintException("Unexpected modifier '" + this.elem(0) + "' in " + this.expr + " parsing range"); } this.max = this.min + this.val(6); return; } if (hasModifier) { if (this.has(3)) { throw new InvalidConstraintException("Unexpected trailing modifier '" + this.elem(3) + "' in " + this.expr); } String leading = this.elem(0); if (">".equals(leading)) { this.min++; this.max = Integer.MAX_VALUE; } else if (">=".equals(leading)) { this.max = Integer.MAX_VALUE; } else if ("<".equals(leading)) { this.max = --this.min; this.min = Integer.MIN_VALUE; } else if ("<=".equals(leading)) { this.max = this.min; this.min = Integer.MIN_VALUE; } } else if (this.has(2)) { String trailing = this.elem(2); if ("<".equals(trailing)) { this.max = this.min; this.min = Integer.MIN_VALUE; } else { this.max = Integer.MAX_VALUE; } } } private boolean has(int index) { return this.constraint[index] != null; } private String elem(int index) { return this.constraint[index]; } private int val(int index) { return this.constraint[index] != null ? Integer.parseInt(this.constraint[index]) : 0; } void append(Constraint next) { if (this.next != null) { this.next.append(next); return; } this.next = next; } public String getToken() { return this.token; } public int getMin() { return this.min; } public int getMax() { return this.max; } /** * Checks the current token against the environment and throws a * {@link ConstraintViolationException} if the constraint is invalid * * @param environment environment to fetch constraints * @throws ConstraintViolationException if constraint is not valid */ public void check(ITokenProvider environment) throws ConstraintViolationException { if (this != Constraint.NONE) { Integer value = environment.getToken(this.token); if (value == null) { throw new ConstraintViolationException("The token '" + this.token + "' could not be resolved in " + environment, this); } if (value.intValue() < this.min) { throw new ConstraintViolationException("Token '" + this.token + "' has a value (" + value + ") which is less than the minimum value " + this.min + " in " + environment, this, value.intValue()); } if (value.intValue() > this.max) { throw new ConstraintViolationException("Token '" + this.token + "' has a value (" + value + ") which is greater than the maximum value " + this.max + " in " + environment, this, value.intValue()); } } if (this.next != null) { this.next.check(environment); } } /** * Gets a human-readable description of the range expressed by this * constraint */ public String getRangeHumanReadable() { if (this.min == Integer.MIN_VALUE && this.max == Integer.MAX_VALUE) { return "ANY VALUE"; } else if (this.min == Integer.MIN_VALUE) { return String.format("less than or equal to %d", this.max); } else if (this.max == Integer.MAX_VALUE) { return String.format("greater than or equal to %d", this.min); } else if (this.min == this.max) { return String.format("%d", this.min); } return String.format("between %d and %d", this.min, this.max); } @Override public String toString() { return String.format("Constraint(%s [%d-%d])", this.token, this.min, this.max); } } private ConstraintParser() { } /** * Parse the supplied expression as a constraint and returns a new * Constraint. Returns {@link Constraint#NONE} if the constraint could not * be parsed or is empty. * * @param expr constraint expression to parse * @return parsed constraint */ public static Constraint parse(String expr) { if (expr == null || expr.length() == 0) { return Constraint.NONE; } String[] exprs = expr.replaceAll("\\s", "").toUpperCase(Locale.ROOT).split(";"); Constraint head = null; for (String subExpr : exprs) { Constraint next = new Constraint(subExpr); if (head == null) { head = next; } else { head.append(next); } } return head != null ? head : Constraint.NONE; } /** * Parse a constraint expression on the supplied annotation as a constraint * and returns a new Constraint. Returns {@link Constraint#NONE} if the * constraint could not be parsed or is empty. * * @param annotation annotation containing the constraint expression to * parse * @return parsed constraint */ public static Constraint parse(AnnotationNode annotation) { String constraints = Annotations.getValue(annotation, "constraints", ""); return ConstraintParser.parse(constraints); } }