Merge changes from patch branch back to main.

This commit is contained in:
Michael Jumper
2025-02-25 12:41:04 -08:00
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.io.Reader;
import java.net.SocketException; import java.net.SocketException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.util.Deque; import java.util.Arrays;
import java.util.LinkedList;
import org.apache.guacamole.GuacamoleConnectionClosedException; import org.apache.guacamole.GuacamoleConnectionClosedException;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.GuacamoleUpstreamTimeoutException; import org.apache.guacamole.GuacamoleUpstreamTimeoutException;
import org.apache.guacamole.protocol.GuacamoleInstruction; import org.apache.guacamole.protocol.GuacamoleInstruction;
import org.apache.guacamole.protocol.GuacamoleParser;
/** /**
* A GuacamoleReader which wraps a standard Java Reader, using that Reader as * 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 { public class ReaderGuacamoleReader implements GuacamoleReader {
/**
* The GuacamoleParser instance for parsing instructions.
*/
private GuacamoleParser parser = new GuacamoleParser();
/** /**
* Wrapped Reader to be used for all input. * 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 * The location within the received data buffer that parsing should begin
* when more data is read. * 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]; private char[] buffer = new char[20480];
@@ -74,7 +79,7 @@ public class ReaderGuacamoleReader implements GuacamoleReader {
@Override @Override
public boolean available() throws GuacamoleException { public boolean available() throws GuacamoleException {
try { try {
return input.ready() || usedLength != 0; return input.ready() || usedLength > parseStart || parser.hasNext();
} }
catch (IOException e) { catch (IOException e) {
throw new GuacamoleServerException(e); throw new GuacamoleServerException(e);
@@ -83,97 +88,47 @@ public class ReaderGuacamoleReader implements GuacamoleReader {
@Override @Override
public char[] read() throws GuacamoleException { public char[] read() throws GuacamoleException {
GuacamoleInstruction instruction = readInstruction();
if (instruction == null)
return null;
return instruction.toString().toCharArray();
}
@Override
public GuacamoleInstruction readInstruction() throws GuacamoleException {
try { try {
// Loop until the parser has prepared a full instruction
while (!parser.hasNext()) {
// While we're blocking, or input is available // Parse as much data from the buffer as we can
for (;;) { int parsed = 0;
while (parseStart < usedLength && (parsed = parser.append(buffer, parseStart, usedLength - parseStart)) != 0) {
parseStart += parsed;
}
// Length of element // If we still don't have a full instruction attempt to read more data into the buffer
int elementLength = 0; if (!parser.hasNext()) {
// 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 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 // Read more instruction data into the buffer
else int numRead = input.read(buffer, usedLength, buffer.length - usedLength);
throw new GuacamoleServerException("Non-numeric character in element length."); 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 return parser.next();
int numRead = input.read(buffer, usedLength, buffer.length - usedLength);
if (numRead == -1)
return null;
// Update used length
usedLength += numRead;
} // End read loop
} }
catch (SocketTimeoutException e) { 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 { public void testReader() throws GuacamoleException {
// Test string // 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)); GuacamoleReader reader = new ReaderGuacamoleReader(new StringReader(test));
@@ -75,12 +75,46 @@ public class ReaderGuacamoleReaderTest {
assertEquals(0, instruction.getArgs().size()); assertEquals(0, instruction.getArgs().size());
assertEquals("foo", instruction.getOpcode()); 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 // There should be no more instructions
instruction = reader.readInstruction(); instruction = reader.readInstruction();
assertNull(instruction); 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());
}
} }