mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-31 05:33:00 +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:
		| @@ -14,6 +14,7 @@ import dan200.computercraft.core.asm.LuaMethod; | |||||||
| import dan200.computercraft.core.asm.ObjectSource; | import dan200.computercraft.core.asm.ObjectSource; | ||||||
| import dan200.computercraft.core.computer.TimeoutState; | import dan200.computercraft.core.computer.TimeoutState; | ||||||
| import dan200.computercraft.core.util.Nullability; | import dan200.computercraft.core.util.Nullability; | ||||||
|  | import dan200.computercraft.core.util.SanitisedError; | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
| import org.squiddev.cobalt.*; | import org.squiddev.cobalt.*; | ||||||
| @@ -142,7 +143,7 @@ public class CobaltLuaMachine implements ILuaMachine { | |||||||
|             return MachineResult.TIMEOUT; |             return MachineResult.TIMEOUT; | ||||||
|         } catch (LuaError e) { |         } catch (LuaError e) { | ||||||
|             close(); |             close(); | ||||||
|             LOG.warn("Top level coroutine errored", e); |             LOG.warn("Top level coroutine errored: {}", new SanitisedError(e)); | ||||||
|             return MachineResult.error(e); |             return MachineResult.error(e); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -8,15 +8,13 @@ import dan200.computercraft.api.lua.ILuaAPI; | |||||||
| import dan200.computercraft.api.lua.ILuaContext; | import dan200.computercraft.api.lua.ILuaContext; | ||||||
| import dan200.computercraft.core.computer.GlobalEnvironment; | import dan200.computercraft.core.computer.GlobalEnvironment; | ||||||
| import dan200.computercraft.core.computer.TimeoutState; | import dan200.computercraft.core.computer.TimeoutState; | ||||||
| import dan200.computercraft.core.metrics.Metrics; |  | ||||||
| import dan200.computercraft.core.metrics.MetricsObserver; | import dan200.computercraft.core.metrics.MetricsObserver; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Arguments used to construct a {@link ILuaMachine}. |  * Arguments used to construct a {@link ILuaMachine}. | ||||||
|  * |  * | ||||||
|  * @param context    The Lua context to execute main-thread tasks with. |  * @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 |  * @param metrics    A sink to submit metrics to. | ||||||
|  *                   metrics such as {@link Metrics#COROUTINES_CREATED} |  | ||||||
|  * @param timeout    The current timeout state. This should be used by the machine to interrupt its execution. |  * @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 |  * @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()}. |  *                   (following the same rules as any other value), and then set to all names in {@link ILuaAPI#getNames()}. | ||||||
|   | |||||||
| @@ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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")); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Jonathan Coates
					Jonathan Coates