From 12157feeca85e527c898cf0b4d766352392d8e76 Mon Sep 17 00:00:00 2001 From: Anuken Date: Tue, 8 Sep 2026 10:10:48 -0400 Subject: [PATCH] Logic \uXXXX escape support --- core/src/mindustry/logic/LAssembler.java | 6 ++- core/src/mindustry/logic/LParser.java | 11 +++++ core/src/mindustry/logic/LStatement.java | 14 +++++- tests/src/test/java/LogicTests.java | 58 +++++++++++++++++++++++- 4 files changed, 86 insertions(+), 3 deletions(-) diff --git a/core/src/mindustry/logic/LAssembler.java b/core/src/mindustry/logic/LAssembler.java index 63be5c1d12..b5d7443639 100644 --- a/core/src/mindustry/logic/LAssembler.java +++ b/core/src/mindustry/logic/LAssembler.java @@ -81,7 +81,7 @@ public class LAssembler{ } } - /** Decodes \n, \" and \\ escape sequences in a string literal's contents (quotes already stripped). */ + /** Decodes \n, \", \\ and uXXXX escape sequences in a string literal's contents (quotes already stripped). */ static String unescape(String s){ if(s.indexOf('\\') == -1) return s; @@ -98,6 +98,10 @@ public class LAssembler{ out.append(next); i ++; continue; + }else if(next == 'u' && i + 5 < s.length()){ + out.append((char)Integer.parseInt(s.substring(i + 2, i + 6), 16)); + i += 5; + continue; } } out.append(c); diff --git a/core/src/mindustry/logic/LParser.java b/core/src/mindustry/logic/LParser.java index 3c07574e12..984c65e35e 100644 --- a/core/src/mindustry/logic/LParser.java +++ b/core/src/mindustry/logic/LParser.java @@ -56,6 +56,17 @@ public class LParser{ continue; } + //uXXXX: validate 4 hex digits + if(c == '\\' && pos + 1 < chars.length && chars[pos + 1] == 'u'){ + if(pos + 5 >= chars.length) error("Invalid \\u escape; expected 4 hex digits."); + for(int j = pos + 2; j <= pos + 5; j++){ + if(Character.digit(chars[j], 16) == -1) error("Invalid \\u escape; expected 4 hex digits."); + } + utflen += 3; //decoded char may take up to 3 bytes in modified UTF-8 + pos += 5; //consume u and the 4 hex digits + continue; + } + if(c == '\n'){ error("Missing closing quote \" before end of line."); }else if(c == '"'){ diff --git a/core/src/mindustry/logic/LStatement.java b/core/src/mindustry/logic/LStatement.java index 5f17caf888..8691fb06fd 100644 --- a/core/src/mindustry/logic/LStatement.java +++ b/core/src/mindustry/logic/LStatement.java @@ -99,13 +99,17 @@ public abstract class LStatement{ //escape characters that would otherwise break out of the string or corrupt it. for(int i = 1; i < value.length() - 1; i++){ char c = value.charAt(i); - //exception: allow escape sequences in strings: \n, \", \\ + //exception: allow escape sequences in strings: \n, \", \\, uXXXX if(c == '\\' && i + 1 < value.length() - 1){ char next = value.charAt(i + 1); if(next == '"' || next == '\\' || next == 'n'){ res.append(c).append(next); i ++; //consumed the escape target too continue; + }else if(next == 'u' && i + 5 < value.length() - 1 && isHex(value, i + 2)){ + res.append(value, i, i + 6); + i += 5; //consumed u and the 4 hex digits too + continue; } } switch(c){ @@ -136,6 +140,14 @@ public abstract class LStatement{ return value; } + /** True if the 4 characters at value[from..from+3] are all hex digits. */ + private static boolean isHex(String value, int from){ + for(int i = from; i < from + 4; i++){ + if(Character.digit(value.charAt(i), 16) == -1) return false; + } + return true; + } + protected static boolean logicLocalization(){ return Core.settings.getBool("logiclocalization", true); } diff --git a/tests/src/test/java/LogicTests.java b/tests/src/test/java/LogicTests.java index 0f8fe1f5a4..a7abccea83 100644 --- a/tests/src/test/java/LogicTests.java +++ b/tests/src/test/java/LogicTests.java @@ -83,6 +83,21 @@ public class LogicTests{ "escaped backslash sitting right before the closing quote", "set result \"end\\\\\"", "end\\" + ), + Arguments.of( + "\\u0041 decodes to the corresponding unicode character", + "set result \"\\u0041\"", + "A" + ), + Arguments.of( + "\\uf8ff decodes to the corresponding unicode character", + "set result \"\\uf8ff\\uf8ff\"", + "" + ), + Arguments.of( + "\\u escapes combined with other escapes in one string", + "set result \"\\u0041\\n\\u0042\\\\end\"", + "A\nB\\end" ) ); } @@ -140,7 +155,32 @@ public class LogicTests{ Arguments.of("a raw '#' in an unquoted value gets neutralized so it can't start a comment", "a#b", "a_b"), Arguments.of("a lone newline character is invalid on its own, same as a lone space", "\n", "invalid"), Arguments.of("a lone tab character is invalid on its own, same as a lone space", "\t", "invalid"), - Arguments.of("a lone '#' character is invalid on its own, same as a lone space", "#", "invalid") + Arguments.of("a lone '#' character is invalid on its own, same as a lone space", "#", "invalid"), + Arguments.of( + "a well-formed \\uXXXX escape inside a quoted value passes through untouched", + "\"\\u0041\"", + "\"\\u0041\"" + ), + Arguments.of( + "\\uXXXX combined with other escapes inside a quoted value", + "\"\\u0041\\n\\u0042\\\\end\"", + "\"\\u0041\\n\\u0042\\\\end\"" + ), + Arguments.of( + "a malformed \\u escape with too few digits still escapes the backslash instead of throwing", + "\"\\u12\"", + "\"\\\\u12\"" + ), + Arguments.of( + "a malformed \\u escape with non-hex digits still escapes the backslash instead of throwing", + "\"\\u12zz\"", + "\"\\\\u12zz\"" + ), + Arguments.of( + "a \\u escape truncated by the closing quote still escapes the backslash instead of throwing", + "\"a\\u123\"", + "\"a\\\\u123\"" + ) ); } @@ -232,6 +272,22 @@ public class LogicTests{ Arguments.of( "a string that never closes before the line ends, with trailing content after it", "set result \"asd\nset bar 5" + ), + Arguments.of( + "a \\u escape with non-hex digits is rejected", + "set result \"\\u12zz\"" + ), + Arguments.of( + "a \\u escape with non-hex digits is rejected", + "set result \"\\u1h23\"" + ), + Arguments.of( + "a \\u escape with non-hex digits is rejected", + "set result \"\\uhh23\"" + ), + Arguments.of( + "a \\u escape truncated by the closing quote is rejected", + "set result \"\\u12\"" ) ); }