From 4e0fb60eef5ffc31250f82cb57d12a15e964a67c Mon Sep 17 00:00:00 2001 From: liumangmang Date: Tue, 10 Mar 2026 18:07:07 +0800 Subject: [PATCH] fix: avoid StackOverflowError when parsing upload payload --- .cursor/rules/Chinese.mdc | 6 -- code/src/main/java/RedisClipSync.java | 109 ++++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 12 deletions(-) delete mode 100644 .cursor/rules/Chinese.mdc diff --git a/.cursor/rules/Chinese.mdc b/.cursor/rules/Chinese.mdc deleted file mode 100644 index b2a078f..0000000 --- a/.cursor/rules/Chinese.mdc +++ /dev/null @@ -1,6 +0,0 @@ ---- -alwaysApply: true ---- - -Please execute the generate commit message command in Chinese. -生成 Git 提交信息(Commit Message)时,必须强制使用中文。 diff --git a/code/src/main/java/RedisClipSync.java b/code/src/main/java/RedisClipSync.java index 6e751b4..8a99292 100644 --- a/code/src/main/java/RedisClipSync.java +++ b/code/src/main/java/RedisClipSync.java @@ -51,7 +51,6 @@ public class RedisClipSync { // 公共上传通道 private static final String UPLOAD_CHANNEL = "global_clip_upload"; private static final String UNKNOWN_SLAVE_ID = "unknown"; - private static final Pattern UPLOAD_PAYLOAD_PATTERN = Pattern.compile("\\{\"slaveId\":\"((?:\\\\.|[^\"])*)\",\"content\":\"((?:\\\\.|[^\"])*)\",\"timestamp\":(\\d+)\\}"); private static final String MASTER_TO_SLAVE_LOG_FILE = "master_to_slave.log"; private static final String SLAVE_TO_MASTER_LOG_FILE = "slave_to_master.log"; private static final String LOG_ENTRY_SEPARATOR = "\n\n------------------\n\n"; @@ -551,14 +550,112 @@ public class RedisClipSync { return null; } - Matcher matcher = UPLOAD_PAYLOAD_PATTERN.matcher(message); - if (!matcher.matches()) { + // Avoid regex here: large clipboard payloads can trigger deep backtracking and StackOverflowError. + // We only need to parse a fixed JSON shape produced by buildUploadPayload(). + try { + String trimmed = message.trim(); + if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) { + return null; + } + + String slaveIdRaw = extractJsonStringField(trimmed, "slaveId"); + String contentRaw = extractJsonStringField(trimmed, "content"); + // timestamp is optional for parsing; we just validate it exists and is numeric when present + if (!hasJsonNumberField(trimmed, "timestamp")) { + return null; + } + + String slaveId = unescapeJson(slaveIdRaw); + String content = unescapeJson(contentRaw); + return new UploadMessage(slaveId, content); + } catch (Exception ignored) { return null; } + } - String slaveId = unescapeJson(matcher.group(1)); - String content = unescapeJson(matcher.group(2)); - return new UploadMessage(slaveId, content); + static String extractJsonStringField(String json, String fieldName) { + int namePos = indexOfJsonFieldName(json, fieldName); + if (namePos < 0) { + throw new IllegalArgumentException("missing field: " + fieldName); + } + + int colon = json.indexOf(':', namePos); + if (colon < 0) { + throw new IllegalArgumentException("bad json: missing colon for " + fieldName); + } + + int i = colon + 1; + while (i < json.length() && Character.isWhitespace(json.charAt(i))) { + i++; + } + if (i >= json.length() || json.charAt(i) != '"') { + throw new IllegalArgumentException("bad json: expected string for " + fieldName); + } + + return parseJsonStringBody(json, i); + } + + static boolean hasJsonNumberField(String json, String fieldName) { + int namePos = indexOfJsonFieldName(json, fieldName); + if (namePos < 0) { + return false; + } + int colon = json.indexOf(':', namePos); + if (colon < 0) { + return false; + } + + int i = colon + 1; + while (i < json.length() && Character.isWhitespace(json.charAt(i))) { + i++; + } + int start = i; + while (i < json.length()) { + char c = json.charAt(i); + if (c < '0' || c > '9') { + break; + } + i++; + } + return i > start; + } + + static int indexOfJsonFieldName(String json, String fieldName) { + // Find: "fieldName" (as a JSON string token) + String needle = "\"" + fieldName + "\""; + return json.indexOf(needle); + } + + /** + * Parse JSON string starting at the opening quote and return the raw (escaped) body. + * Example input: "a\\n\"b" -> returns: a\\n\"b (without surrounding quotes) + */ + static String parseJsonStringBody(String json, int openingQuoteIndex) { + if (openingQuoteIndex < 0 || openingQuoteIndex >= json.length() || json.charAt(openingQuoteIndex) != '"') { + throw new IllegalArgumentException("bad json: expected opening quote"); + } + + StringBuilder rawEscaped = new StringBuilder(); + int i = openingQuoteIndex + 1; + while (i < json.length()) { + char c = json.charAt(i); + if (c == '"') { + return rawEscaped.toString(); + } + if (c == '\\') { + if (i + 1 >= json.length()) { + throw new IllegalArgumentException("bad json: trailing backslash"); + } + rawEscaped.append('\\').append(json.charAt(i + 1)); + i += 2; + continue; + } + + rawEscaped.append(c); + i++; + } + + throw new IllegalArgumentException("bad json: unterminated string"); } static String escapeJson(String value) {