/* * This file is part of Mixin, licensed under the MIT License (MIT). * * Copyright (c) SpongePowered <https://www.spongepowered.org> * 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.regex.Matcher; import java.util.regex.Pattern; import org.spongepowered.asm.lib.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: * * <blockquote><pre><token>(<constraint>)</pre></blockquote> * * <p><b>token</b> is normalised to uppercase and must be provided by the * environment.</p> * * <p><b>constraint</b> is an integer range specified in one of the * following formats: * * <dl> * <dt><pre>()</pre></dt> * <dd>The token value must be present in the environment, but can have * any value</dd> * <dt><pre>(1234)</pre></dt> * <dd>The token value must be <em>exactly equal to </em> <code>1234 * </code></dd> * <dt><pre>(1234+) *(1234-) *(1234>) *</pre></dt> * <dd>All of these variants mean the same thing, and can be read as "1234 * or greater"</dd> * <dt><pre>(<1234)</pre></dt> * <dd><em>Less than</em> 123</dd> * <dt><pre>(<=1234)</pre></dt> * <dd><em>Less than or equal to</em> 1234 (equivalent to <code>1234< * </code>)</dd> * <dt><pre>(>1234)</pre></dt> * <dd><em>Greater than</em> 1234</dd> * <dt><pre>(>=1234)</pre></dt> * <dd><em>Greater than or equal to</em> 1234 (equivalent to <code>1234 * ></code>)</dd> * <dt><pre>(1234-1300)</pre></dt> * <dd>Value must be <em>between</em> 1234 and 1300 (inclusive)</dd> * <dt><pre>(1234+10)</pre></dt> * <dd>Value must be <em>between</em> 1234 and 1234+10 (1234-1244 * inclusive)</dd> * </dl> * * <p>All whitespace is ignored in constraint declarations. The following * declarations are equivalent:</p> * * <blockquote><pre>token(123-456) *token ( 123 - 456 )</pre></blockquote> * * <p>Multiple constraints should be separated by semicolon (<code>;</code>) * and are conjoined by an implied logical <code>AND</code> operator. That * is: all constraints must pass for the constraint to be considered valid. * </p> */ 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().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); } }