GUACAMOLE-2036: Implement the GuacamoleParser within the ReaderGuacamoleReader to support multibyte characters when reading instructions.

This commit is contained in:
Alexander Leitner
2025-02-24 20:24:14 -05:00
parent 355c6d4eff
commit c9f2e451cb
2 changed files with 77 additions and 164 deletions

View File

@@ -24,13 +24,13 @@ import java.io.IOException;
import java.io.Reader;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Arrays;
import org.apache.guacamole.GuacamoleConnectionClosedException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.GuacamoleUpstreamTimeoutException;
import org.apache.guacamole.protocol.GuacamoleInstruction;
import org.apache.guacamole.protocol.GuacamoleParser;
/**
* A GuacamoleReader which wraps a standard Java Reader, using that Reader as
@@ -38,6 +38,11 @@ import org.apache.guacamole.protocol.GuacamoleInstruction;
*/
public class ReaderGuacamoleReader implements GuacamoleReader {
/**
* The GuacamoleParser instance for parsing instructions.
*/
private GuacamoleParser parser = new GuacamoleParser();
/**
* Wrapped Reader to be used for all input.
*/
@@ -57,10 +62,10 @@ public class ReaderGuacamoleReader implements GuacamoleReader {
* The location within the received data buffer that parsing should begin
* when more data is read.
*/
private int parseStart;
private int parseStart = 0;
/**
* The buffer holding all received, unparsed data.
* The buffer holding all received data.
*/
private char[] buffer = new char[20480];
@@ -74,7 +79,7 @@ public class ReaderGuacamoleReader implements GuacamoleReader {
@Override
public boolean available() throws GuacamoleException {
try {
return input.ready() || usedLength != 0;
return input.ready() || usedLength > parseStart || parser.hasNext();
}
catch (IOException e) {
throw new GuacamoleServerException(e);
@@ -83,97 +88,47 @@ public class ReaderGuacamoleReader implements GuacamoleReader {
@Override
public char[] read() throws GuacamoleException {
GuacamoleInstruction instruction = readInstruction();
if (instruction == null)
return null;
return instruction.toString().toCharArray();
}
@Override
public GuacamoleInstruction readInstruction() throws GuacamoleException {
try {
// Loop until the parser has prepared a full instruction
while (!parser.hasNext()) {
// While we're blocking, or input is available
for (;;) {
// Parse as much data from the buffer as we can
int parsed = 0;
while (parseStart < usedLength && (parsed = parser.append(buffer, parseStart, usedLength - parseStart)) != 0) {
parseStart += parsed;
}
// Length of element
int elementLength = 0;
// Resume where we left off
int i = parseStart;
// Parse instruction in buffer
while (i < usedLength) {
// Read character
char readChar = buffer[i++];
// If digit, update length
if (readChar >= '0' && readChar <= '9')
elementLength = elementLength * 10 + readChar - '0';
// If not digit, check for end-of-length character
else if (readChar == '.') {
// Check if element present in buffer
if (i + elementLength < usedLength) {
// Get terminator
char terminator = buffer[i + elementLength];
// Move to character after terminator
i += elementLength + 1;
// Reset length
elementLength = 0;
// Continue here if necessary
parseStart = i;
// If terminator is semicolon, we have a full
// instruction.
if (terminator == ';') {
// Copy instruction data
char[] instruction = new char[i];
System.arraycopy(buffer, 0, instruction, 0, i);
// Update buffer
usedLength -= i;
parseStart = 0;
System.arraycopy(buffer, i, buffer, 0, usedLength);
return instruction;
}
// Handle invalid terminator characters
else if (terminator != ',')
throw new GuacamoleServerException("Element terminator of instruction was not ';' nor ','");
}
// Otherwise, read more data
else
break;
// If we still don't have a full instruction attempt to read more data into the buffer
if (!parser.hasNext()) {
// If we have already parsed some of the buffer and the buffer is almost full then we can trim the parsed data off the buffer
if (parseStart > 0 && buffer.length - usedLength < GuacamoleParser.INSTRUCTION_MAX_LENGTH) {
System.arraycopy(buffer, parseStart, buffer, 0, usedLength - parseStart);
usedLength -= parseStart;
parseStart = 0;
}
// Otherwise, parse error
else
throw new GuacamoleServerException("Non-numeric character in element length.");
// Read more instruction data into the buffer
int numRead = input.read(buffer, usedLength, buffer.length - usedLength);
if (numRead == -1)
break;
usedLength += numRead;
}
}
// If past threshold, resize buffer before reading
if (usedLength > buffer.length/2) {
char[] biggerBuffer = new char[buffer.length*2];
System.arraycopy(buffer, 0, biggerBuffer, 0, usedLength);
buffer = biggerBuffer;
}
// Attempt to fill buffer
int numRead = input.read(buffer, usedLength, buffer.length - usedLength);
if (numRead == -1)
return null;
// Update used length
usedLength += numRead;
} // End read loop
return parser.next();
}
catch (SocketTimeoutException e) {
@@ -188,80 +143,4 @@ public class ReaderGuacamoleReader implements GuacamoleReader {
}
@Override
public GuacamoleInstruction readInstruction() throws GuacamoleException {
// Get instruction
char[] instructionBuffer = read();
// If EOF, return EOF
if (instructionBuffer == null)
return null;
// Start of element
int elementStart = 0;
// Build list of elements
Deque<String> elements = new LinkedList<String>();
while (elementStart < instructionBuffer.length) {
// Find end of length
int lengthEnd = -1;
for (int i=elementStart; i<instructionBuffer.length; i++) {
if (instructionBuffer[i] == '.') {
lengthEnd = i;
break;
}
}
// read() is required to return a complete instruction. If it does
// not, this is a severe internal error.
if (lengthEnd == -1)
throw new GuacamoleServerException("Read returned incomplete instruction.");
// Parse length
int length = Integer.parseInt(new String(
instructionBuffer,
elementStart,
lengthEnd - elementStart
));
// Parse element from just after period
elementStart = lengthEnd + 1;
String element = new String(
instructionBuffer,
elementStart,
length
);
// Append element to list of elements
elements.addLast(element);
// Read terminator after element
elementStart += length;
char terminator = instructionBuffer[elementStart];
// Continue reading instructions after terminator
elementStart++;
// If we've reached the end of the instruction
if (terminator == ';')
break;
}
// Pull opcode off elements list
String opcode = elements.removeFirst();
// Create instruction
GuacamoleInstruction instruction = new GuacamoleInstruction(
opcode,
elements.toArray(new String[elements.size()])
);
// Return parsed instruction
return instruction;
}
}

View File

@@ -41,7 +41,7 @@ public class ReaderGuacamoleReaderTest {
public void testReader() throws GuacamoleException {
// Test string
final String test = "1.a,2.bc,3.def,10.helloworld;4.test,5.test2;0.;3.foo;";
final String test = "1.a,2.bc,3.def,10.helloworld;4.test,5.test2;0.;3.foo;1.\uD83E\uDD79;";
GuacamoleReader reader = new ReaderGuacamoleReader(new StringReader(test));
@@ -75,12 +75,46 @@ public class ReaderGuacamoleReaderTest {
assertEquals(0, instruction.getArgs().size());
assertEquals("foo", instruction.getOpcode());
// Validate fifth test instruction
instruction = reader.readInstruction();
assertNotNull(instruction);
assertEquals(0, instruction.getArgs().size());
assertEquals("\uD83E\uDD79", instruction.getOpcode());
// There should be no more instructions
instruction = reader.readInstruction();
assertNull(instruction);
}
/**
* Test of ReaderGuacamoleReader's read method.
*
* @throws GuacamoleException If an error occurs while reading the instructions.
*/
@Test
public void testRead() throws GuacamoleException {
// Test string containing multiple instructions
final String test = "3.foo,3.bar;2.az,4.bazz;";
ReaderGuacamoleReader reader = new ReaderGuacamoleReader(new StringReader(test));
// Expected character arrays for the instructions
char[] expectedFirstInstruction = "3.foo,3.bar;".toCharArray();
char[] expectedSecondInstruction = "2.az,4.bazz;".toCharArray();
// Read first instruction and verify
char[] firstInstructionChars = reader.read();
assertNotNull(firstInstructionChars);
assertArrayEquals(expectedFirstInstruction, firstInstructionChars);
// Read second instruction and verify
char[] secondInstructionChars = reader.read();
assertNotNull(secondInstructionChars);
assertArrayEquals(expectedSecondInstruction, secondInstructionChars);
// Verify that there are no more instructions
assertNull(reader.read());
}
}