diff --git a/org.eclipse.lsp4j.jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/StreamMessageProducer.java b/org.eclipse.lsp4j.jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/StreamMessageProducer.java index 835a903e..75c18997 100644 --- a/org.eclipse.lsp4j.jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/StreamMessageProducer.java +++ b/org.eclipse.lsp4j.jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/json/StreamMessageProducer.java @@ -1,12 +1,12 @@ /****************************************************************************** * Copyright (c) 2016 TypeFox and others. - * + * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0, * or the Eclipse Distribution License v. 1.0 which is available at * http://www.eclipse.org/org/documents/edl-v10.php. - * + * * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause ******************************************************************************/ package org.eclipse.lsp4j.jsonrpc.json; @@ -14,6 +14,8 @@ import java.io.Closeable; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; import java.nio.charset.StandardCharsets; import java.util.logging.Level; import java.util.logging.Logger; @@ -24,6 +26,7 @@ import org.eclipse.lsp4j.jsonrpc.MessageIssueHandler; import org.eclipse.lsp4j.jsonrpc.MessageProducer; import org.eclipse.lsp4j.jsonrpc.messages.Message; +import org.eclipse.lsp4j.jsonrpc.util.LimitedInputStream; /** * A message producer that reads from an input stream and parses messages from JSON. @@ -43,7 +46,7 @@ public class StreamMessageProducer implements MessageProducer, Closeable, Messag public StreamMessageProducer(InputStream input, MessageJsonHandler jsonHandler) { this(input, jsonHandler, null); } - + public StreamMessageProducer(InputStream input, MessageJsonHandler jsonHandler, MessageIssueHandler issueHandler) { this.input = input; this.jsonHandler = jsonHandler; @@ -133,7 +136,7 @@ protected void fireError(Throwable error) { String message = error.getMessage() != null ? error.getMessage() : "An error occurred while processing an incoming message."; LOG.log(Level.SEVERE, message, error); } - + /** * Report that the stream was closed through an exception. */ @@ -169,27 +172,15 @@ protected void parseHeader(String line, Headers headers) { /** * Read the JSON content part of a message, parse it, and notify the callback. - * + * * @return {@code true} if we should continue reading from the input stream, {@code false} if we should stop */ protected boolean handleMessage(InputStream input, Headers headers) throws IOException { if (callback == null) callback = message -> LOG.log(Level.INFO, "Received message: " + message); - - try { - int contentLength = headers.contentLength; - byte[] buffer = new byte[contentLength]; - int bytesRead = 0; - - while (bytesRead < contentLength) { - int readResult = input.read(buffer, bytesRead, contentLength - bytesRead); - if (readResult == -1) - return false; - bytesRead += readResult; - } - String content = new String(buffer, headers.charset); - try { + try { + try (final Reader content = new InputStreamReader(new LimitedInputStream(input, headers.contentLength, false), headers.charset)) { Message message = jsonHandler.parseMessage(content); callback.consume(message); } catch (MessageIssueException exception) { diff --git a/org.eclipse.lsp4j.jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/util/LimitedInputStream.java b/org.eclipse.lsp4j.jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/util/LimitedInputStream.java new file mode 100644 index 00000000..f457b3da --- /dev/null +++ b/org.eclipse.lsp4j.jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/util/LimitedInputStream.java @@ -0,0 +1,101 @@ +/****************************************************************************** + * Copyright (c) 2024 Sebastian Thomschke and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + ******************************************************************************/ +package org.eclipse.lsp4j.jsonrpc.util; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class LimitedInputStream extends FilterInputStream { + private static final int EOF = -1; + + private int bytesRemaining; + private final boolean closeWrapped; + private boolean isClosed; + private int mark = EOF; + + /** + * @param closeWrapped controls if the underlying {@link InputStream} should also be closed via {@link #close()} + */ + public LimitedInputStream(final InputStream wrapped, final int maxBytesToRead, final boolean closeWrapped) { + super(wrapped); + if (maxBytesToRead < 0) + throw new IllegalArgumentException("[maxBytesToRead] must be >= 0"); + bytesRemaining = maxBytesToRead; + this.closeWrapped = closeWrapped; + } + + @Override + public int available() throws IOException { + if (isClosed) + return 0; + final int availableBytes = in.available(); + return Math.min(availableBytes, bytesRemaining); + } + + @Override + public void close() throws IOException { + if (closeWrapped) { + in.close(); + } + isClosed = true; + } + + @Override + public int read() throws IOException { + if (isClosed || bytesRemaining < 1) + return EOF; + + final int data = in.read(); + if (data != EOF) { + bytesRemaining--; + } + return data; + } + + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + if (isClosed || bytesRemaining < 1) + return EOF; + + final int bytesRead = in.read(b, off, Math.min(len, bytesRemaining)); + if (bytesRead != EOF) { + bytesRemaining -= bytesRead; + } + return bytesRead; + } + + @Override + public synchronized void mark(final int readlimit) { + in.mark(readlimit); + mark = bytesRemaining; + } + + @Override + public synchronized void reset() throws IOException { + if (!in.markSupported()) + throw new IOException("mark/reset not supported"); + + if (mark == EOF) + throw new IOException("mark not set"); + + in.reset(); + bytesRemaining = mark; + } + + @Override + public long skip(final long n) throws IOException { + final long skipped = in.skip(Math.min(n, bytesRemaining)); + bytesRemaining -= skipped; + return skipped; + } +}