From c26751239ae9ee518d6ef6f59b69417146df8d7e Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 27 Sep 2014 20:09:27 -0700 Subject: [PATCH] GUAC-868: Port Guacamole parser from libguac. Add GuacamoleInstruction constructor which takes a List. --- .../protocol/GuacamoleInstruction.java | 14 + .../guacamole/protocol/GuacamoleParser.java | 244 ++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleParser.java diff --git a/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleInstruction.java b/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleInstruction.java index f53bb287d..2c050d43b 100644 --- a/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleInstruction.java +++ b/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleInstruction.java @@ -58,6 +58,20 @@ public class GuacamoleInstruction { this.args = Collections.unmodifiableList(Arrays.asList(args)); } + /** + * Creates a new GuacamoleInstruction having the given Operation and + * list of arguments values. The list given will be used to back the + * internal list of arguments and the list returned by getArgs(). + * + * @param opcode The opcode of the instruction to create. + * @param args The list of argument values to provide in the new + * instruction if any. + */ + public GuacamoleInstruction(String opcode, List args) { + this.opcode = opcode; + this.args = Collections.unmodifiableList(args); + } + /** * Returns the opcode associated with this GuacamoleInstruction. * @return The opcode associated with this GuacamoleInstruction. diff --git a/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleParser.java b/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleParser.java new file mode 100644 index 000000000..b061d6bfa --- /dev/null +++ b/guacamole-common/src/main/java/org/glyptodon/guacamole/protocol/GuacamoleParser.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * 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.glyptodon.guacamole.protocol; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.GuacamoleServerException; + +/** + * Parser for the Guacamole protocol. Arbitrary instruction data is appended, + * and instructions are returned as a result. Invalid instructions result in + * exceptions. + * + * @author Michael Jumper + */ +public class GuacamoleParser implements Iterator { + + /** + * The maximum number of characters per instruction. + */ + public static final int INSTRUCTION_MAX_LENGTH = 8192; + + /** + * The maximum number of digits to allow per length prefix. + */ + public static final int INSTRUCTION_MAX_DIGITS = 5; + + /** + * The maximum number of elements per instruction, including the opcode. + */ + public static final int INSTRUCTION_MAX_ELEMENTS = 64; + + /** + * All possible states of the instruction parser. + */ + private enum State { + + /** + * The parser is currently waiting for data to complete the length prefix + * of the current element of the instruction. + */ + PARSING_LENGTH, + + /** + * The parser has finished reading the length prefix and is currently + * waiting for data to complete the content of the instruction. + */ + PARSING_CONTENT, + + /** + * The instruction has been fully parsed. + */ + COMPLETE, + + /** + * The instruction cannot be parsed because of a protocol error. + */ + ERROR + + } + + /** + * The latest parsed instruction, if any. + */ + private GuacamoleInstruction parsedInstruction; + + /** + * The parse state of the instruction. + */ + private State state = State.PARSING_LENGTH; + + /** + * The length of the current element, if known. + */ + private int elementLength = 0; + + /** + * The number of elements currently parsed. + */ + private int elementCount = 0; + + /** + * All currently parsed elements. + */ + private final String elements[] = new String[INSTRUCTION_MAX_ELEMENTS]; + + /** + * Appends data from the given buffer to the current instruction. + * + * @param chunk The buffer containing the data to append. + * @param offset The offset within the buffer where the data begins. + * @param length The length of the data to append. + * @return The number of characters appended, or 0 if complete instructions + * have already been parsed and must be read via next() before + * more data can be appended. + * @throws GuacamoleException If an error occurs while parsing the new data. + */ + public int append(char chunk[], int offset, int length) throws GuacamoleException { + + int charsParsed = 0; + + // Do not exceed maximum number of elements + if (elementCount == INSTRUCTION_MAX_ELEMENTS && state != State.COMPLETE) { + state = State.ERROR; + throw new GuacamoleServerException("Instruction contains too many elements."); + } + + // Parse element length + if (state == State.PARSING_LENGTH) { + + int parsedLength = elementLength; + while (charsParsed < length) { + + // Pull next character + char c = chunk[offset + charsParsed++]; + + // If digit, add to length + if (c >= '0' && c <= '9') + parsedLength = parsedLength*10 + c - '0'; + + // If period, switch to parsing content + else if (c == '.') { + state = State.PARSING_CONTENT; + break; + } + + // If not digit, parse error + else { + state = State.ERROR; + throw new GuacamoleServerException("Non-numeric character in element length."); + } + + } + + // If too long, parse error + if (parsedLength > INSTRUCTION_MAX_LENGTH) { + state = State.ERROR; + throw new GuacamoleServerException("Instruction exceeds maximum length."); + } + + // Save length + elementLength = parsedLength; + + } // end parse length + + // Parse element content, if available + if (state == State.PARSING_CONTENT && charsParsed + elementLength + 1 <= length) { + + // Read element + String element = new String(chunk, offset + charsParsed, elementLength); + charsParsed += elementLength; + elementLength = 0; + + // Read terminator char following element + char terminator = chunk[offset + charsParsed++]; + + // Add element to currently parsed elements + elements[elementCount++] = element; + + // If semicolon, store end-of-instruction + if (terminator == ';') { + state = State.COMPLETE; + parsedInstruction = new GuacamoleInstruction(elements[0], + Arrays.asList(elements).subList(1, elementCount)); + } + + // If comma, move on to next element + else if (terminator == ',') + state = State.PARSING_LENGTH; + + // Otherwise, parse error + else { + state = State.ERROR; + throw new GuacamoleServerException("Element terminator of instruction was not ';' nor ','"); + } + + } // end parse content + + return charsParsed; + + } + + /** + * Appends data from the given buffer to the current instruction. + * + * @param chunk The data to append. + * @return The number of characters appended, or 0 if complete instructions + * have already been parsed and must be read via next() before + * more data can be appended. + * @throws GuacamoleException If an error occurs while parsing the new data. + */ + public int append(char chunk[]) throws GuacamoleException { + return append(chunk, 0, chunk.length); + } + + @Override + public boolean hasNext() { + return state == State.COMPLETE; + } + + @Override + public GuacamoleInstruction next() { + + // No instruction to return if not yet complete + if (state != State.COMPLETE) + return null; + + // Reset for next instruction. + state = State.PARSING_LENGTH; + elementCount = 0; + elementLength = 0; + + return parsedInstruction; + + } + + @Override + public void remove() { + throw new UnsupportedOperationException("GuacamoleParser does not support remove()."); + } + +}