1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-02-14 18:10:05 +00:00

Truncate longer error messages

We could do this in a more concise manner by wrapping Throwable rather
than reimplementing printStackTrace. However, doing this way allows us
to handle nested exceptions too.
This commit is contained in:
Jonathan Coates 2023-05-25 09:08:05 +01:00
parent 9d16fda266
commit ebed357f05
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
4 changed files with 184 additions and 4 deletions

View File

@ -14,6 +14,7 @@ import dan200.computercraft.core.asm.LuaMethod;
import dan200.computercraft.core.asm.ObjectSource;
import dan200.computercraft.core.computer.TimeoutState;
import dan200.computercraft.core.util.Nullability;
import dan200.computercraft.core.util.SanitisedError;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.squiddev.cobalt.*;
@ -142,7 +143,7 @@ public class CobaltLuaMachine implements ILuaMachine {
return MachineResult.TIMEOUT;
} catch (LuaError e) {
close();
LOG.warn("Top level coroutine errored", e);
LOG.warn("Top level coroutine errored: {}", new SanitisedError(e));
return MachineResult.error(e);
}
}

View File

@ -8,15 +8,13 @@ import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.ILuaContext;
import dan200.computercraft.core.computer.GlobalEnvironment;
import dan200.computercraft.core.computer.TimeoutState;
import dan200.computercraft.core.metrics.Metrics;
import dan200.computercraft.core.metrics.MetricsObserver;
/**
* Arguments used to construct a {@link ILuaMachine}.
*
* @param context The Lua context to execute main-thread tasks with.
* @param metrics A sink to submit metrics to. You do not need to submit task timings here, it should only be for additional
* metrics such as {@link Metrics#COROUTINES_CREATED}
* @param metrics A sink to submit metrics to.
* @param timeout The current timeout state. This should be used by the machine to interrupt its execution.
* @param apis APIs to inject into the global environment. Each API should be converted into a Lua object
* (following the same rules as any other value), and then set to all names in {@link ILuaAPI#getNames()}.

View File

@ -0,0 +1,113 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.util;
import java.util.HashSet;
import java.util.Set;
/**
* Wraps {@link Throwable}, which attempts to sanitise error messages.
* <p>
* This is intended for logging errors where the message content is supplied from untrusted sources. This isn't a
* perfect escaping mechanism, but ensures basic "unsafe" strings (i.e. ANSI escape sequences, long lines) are escaped.
*
* <h2>Example:</h2>
* <pre>{@code
* LOG.error("Some error occurred: {}", new TruncatedError(error));
* }</pre>
*
* @param error The error to wrap.
*/
public record SanitisedError(Throwable error) {
private static final int MAX_LENGTH = 200;
private static final String CAUSED_BY = "Caused by: ";
private static final String SUPPRESSED = "Suppressed: ";
@Override
public String toString() {
var output = new StringBuilder();
printStackTrace(output, error);
return output.toString();
}
public static void printStackTrace(StringBuilder output, Throwable error) {
appendMessage(output, error);
var trace = error.getStackTrace();
for (var traceElement : trace) output.append("\tat ").append(traceElement).append("\n");
var seen = new HashSet<Throwable>();
seen.add(error);
printAdditionalErrors(output, error, trace, "", seen);
}
private static void appendMessage(StringBuilder output, Throwable self) {
var message = self.toString();
var length = message.length();
for (int i = 0, limit = Math.min(MAX_LENGTH, length); i < limit; i++) {
var c = message.charAt(i);
switch (c) {
case '\\' -> output.append("\\\\");
case '\n' -> output.append("\\n");
case '\r' -> output.append("\\r");
case '\t' -> output.append("\\t");
default -> {
if (c >= ' ') {
output.append(c);
} else {
output.append("\\u{").append(Integer.toHexString(c)).append("}");
}
}
}
}
if (length > MAX_LENGTH) output.append("... (message truncated)");
output.append("\n");
}
private static void printAdditionalErrors(
StringBuilder output, Throwable self, StackTraceElement[] trace, String indent, Set<Throwable> seen
) {
// Print suppressed exceptions, if any
for (var se : self.getSuppressed()) printAdditionalError(output, se, trace, SUPPRESSED, indent + "\t", seen);
// Print cause, if any
var ourCause = self.getCause();
if (ourCause != null) printAdditionalError(output, ourCause, trace, CAUSED_BY, indent, seen);
}
private static void printAdditionalError(
StringBuilder output, Throwable self, StackTraceElement[] parent, String label, String indent, Set<Throwable> seen
) {
if (!seen.add(self)) {
output.append("[DUPLICATE ERROR: ");
appendMessage(output, self);
output.append("]\n");
return;
}
output.append(indent).append(label);
appendMessage(output, self);
// Find a common prefix with the parent exception and just print that.
var trace = self.getStackTrace();
int traceIdx, parentIdx;
for (
traceIdx = trace.length - 1, parentIdx = parent.length - 1;
traceIdx >= 0 && parentIdx >= 0;
traceIdx--, parentIdx--
) {
if (!trace[traceIdx].equals(parent[parentIdx])) break;
}
for (var i = 0; i <= traceIdx; i++) output.append(indent).append("\tat ").append(trace[traceIdx]).append("\n");
var remaining = trace.length - traceIdx - 1;
if (remaining >= 0) output.append(indent).append("\t... ").append(remaining).append(" more\n");
printAdditionalErrors(output, self, trace, indent + "\t", seen);
}
}

View File

@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.core.util;
import org.junit.jupiter.api.Test;
import java.io.PrintWriter;
import java.io.StringWriter;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.text.CharSequenceLength.hasLength;
import static org.junit.jupiter.api.Assertions.assertEquals;
class SanitisedErrorTest {
private void assertEquivalent(Throwable t) {
var truncatedOutput = new SanitisedError(t).toString();
var writer = new StringWriter();
try (var printWriter = new PrintWriter(writer)) {
t.printStackTrace(printWriter);
}
var actualOutput = writer.toString();
assertEquals(actualOutput, truncatedOutput);
}
@Test
public void testBasicException() {
assertEquivalent(new RuntimeException("A problem occurred"));
}
@Test
public void textExceptionWithCause() {
var inner = new RuntimeException("Inner error");
var outer = new RuntimeException("Outer error", inner);
assertEquivalent(outer);
}
@Test
public void textExceptionWithSuppressed() {
var inner = new RuntimeException("Inner error");
var outer = new RuntimeException("Outer error");
outer.addSuppressed(inner);
assertEquivalent(outer);
}
@Test
public void testTruncates() {
var error = new RuntimeException("Some message".repeat(100));
error.setStackTrace(new StackTraceElement[0]);
var message = new SanitisedError(error).toString();
assertThat(message, containsString("message truncated"));
assertThat(message, hasLength(lessThanOrEqualTo(250)));
}
@Test
public void testStrips() {
var error = new RuntimeException("Some message\n\r\t\033");
error.setStackTrace(new StackTraceElement[0]);
var message = new SanitisedError(error).toString();
assertThat(message, startsWith("java.lang.RuntimeException: Some message\\n\\r\\t\\u{1b}\n"));
}
}