From ac4d105043ba12a82247941cee8da768e03a10a4 Mon Sep 17 00:00:00 2001 From: yangpeng Date: Tue, 4 Aug 2026 11:18:42 +0800 Subject: [PATCH 1/3] fix: bypass UNSAFE SWAR in readFieldNameHashCode on aarch64+JDK8 (#7732, #3763) Commit 291d74496 (2.0.55) introduced UNSAFE.getLong SWAR in JSONReaderUTF16.readFieldNameHashCode for 8-char stepping with containsSlashOrQuoteUTF16 bit ops. The C2 compiler on aarch64+JDK8 miscompiles this SWAR path, causing field-name boundary errors (illegal fieldName, expect ':'). Under high concurrency the miscompiled method is cached in CodeCache, making all subsequent JSON parsing fail permanently (#3763). Fix: add AARCH64_JDK8 platform flag in JDKUtils; in the three readFieldNameHashCode* entry points, fall back to the safe char[] scanning path (readFieldNameHashCode0) when running on aarch64+JDK8. Performance impact: only affects aarch64+JDK8 field-name parsing (SWAR -> char[]). All other platforms are unaffected. Test: Issue7732 regression test with real payloads from the issue report (LoginUser, Device, SipRouteCache). Issue3763 concurrency test unchanged. Both pass on x86. --- .../alibaba/fastjson2/JSONReaderUTF16.java | 12 ++ .../com/alibaba/fastjson2/util/JDKUtils.java | 8 ++ .../fastjson2/issues_7000/Issue7732.java | 125 ++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7732.java diff --git a/core/src/main/java/com/alibaba/fastjson2/JSONReaderUTF16.java b/core/src/main/java/com/alibaba/fastjson2/JSONReaderUTF16.java index 53aae19b9c..6ac896a338 100644 --- a/core/src/main/java/com/alibaba/fastjson2/JSONReaderUTF16.java +++ b/core/src/main/java/com/alibaba/fastjson2/JSONReaderUTF16.java @@ -1115,6 +1115,10 @@ public final long readFieldNameHashCodeUnquote() { @Override public final long readFieldNameHashCode() { + if (AARCH64_JDK8) { + return readFieldNameHashCode0(); + } + final char[] chars = this.chars; int ch = this.ch; if (ch == '/') { @@ -1181,6 +1185,10 @@ public final long readFieldNameHashCode() { @Override public final long readFieldNameHashCode(int keySize, int min, int max) { + if (AARCH64_JDK8) { + return readFieldNameHashCode0(); + } + final char[] chars = this.chars; int ch = this.ch; if (ch == '/') { @@ -1252,6 +1260,10 @@ public final long readFieldNameHashCode(int keySize, int min, int max) { @Override public long readFieldNameHashCodeE(int size0, int size1, int size3) { + if (AARCH64_JDK8) { + return readFieldNameHashCode0(); + } + final char[] chars = this.chars; int ch = this.ch; if (ch == '/') { diff --git a/core/src/main/java/com/alibaba/fastjson2/util/JDKUtils.java b/core/src/main/java/com/alibaba/fastjson2/util/JDKUtils.java index c105cd215a..4c5796c550 100644 --- a/core/src/main/java/com/alibaba/fastjson2/util/JDKUtils.java +++ b/core/src/main/java/com/alibaba/fastjson2/util/JDKUtils.java @@ -21,8 +21,11 @@ public class JDKUtils { public static final long ARRAY_CHAR_BASE_OFFSET; public static final int JVM_VERSION; + public static final String OS_ARCH; public static final Byte LATIN1 = 0; + public static final boolean AARCH64; public static final Byte UTF16 = 1; + public static final boolean AARCH64_JDK8; public static final Field FIELD_STRING_VALUE; public static final long FIELD_STRING_VALUE_OFFSET; @@ -122,6 +125,10 @@ public class JDKUtils { GRAAL = graal; ANDROID_SDK_INT = android_sdk_int; + String osArch = System.getProperty("os.arch"); + OS_ARCH = osArch; + AARCH64 = "aarch64".equals(osArch) || "arm64".equals(osArch); + boolean hasJavaSql = true; Class dataSourceClass = null; Class rowSetClass = null; @@ -145,6 +152,7 @@ public class JDKUtils { CLASS_TRANSIENT = transientClass; JVM_VERSION = jvmVersion; + AARCH64_JDK8 = AARCH64 && JVM_VERSION == 8 && !ANDROID && !GRAAL; if (JVM_VERSION == 8) { Field field = null; diff --git a/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7732.java b/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7732.java new file mode 100644 index 0000000000..bb1a96d993 --- /dev/null +++ b/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7732.java @@ -0,0 +1,125 @@ +package com.alibaba.fastjson2.issues_7000; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("regression") +public class Issue7732 { + private static final String LOGIN_USER_JSON = "{\"expireTime\":1785411580135,\"ifRemindResetPassword\":false,\"ipaddr\":\"10.10.10.10\",\"loginTime\":1785409780135,\"passwordExpiredDay\":0,\"permissions\":[\"::*\"],\"sysUser\":{\"admin\":true,\"apiCall\":0,\"businessCatalogType\":2,\"isEnableSecurityDownload\":0,\"isSingleLogin\":0,\"limitSingleLogin\":false,\"ptzLockTime\":1,\"status\":1,\"telephone\":\"2asmPb/g4knm0thZtw==\",\"tenantId\":1,\"tenantMgr\":false,\"userCode\":\"aaadmin\",\"userId\":12,\"userLevel\":3,\"userName\":\"tt\",\"userPassword\":\"9C5gafUuHPSOqYUzP4TksEOS8OhJo=\",\"userType\":3,\"videoPlayConcurrent\":50},\"token\":\"0:aaadmin:0ecac3d2-3daeaa\",\"userCode\":\"aaadmin\",\"userid\":12,\"username\":\"tt\"}"; + private static final String DEVICE_JSON = "{\"auth\":true,\"broadcastPushAfterAck\":false,\"channelCount\":0,\"deviceId\":\"44444444\",\"deviceType\":3,\"enableAudio\":true,\"expires\":86400,\"firmware\":\"V4.62.325\",\"hostAddress\":\"10.20.30.11:606\",\"ip\":\"10.20.30.11\",\"isStoreGbDeviceAlarm\":0,\"keepaliveIntervalTime\":60,\"keepaliveTime\":\"2026-07-30 18:54:53\",\"localIp\":\"10.20.30.145\",\"manufacturer\":\"HIKVISION\",\"mediaRouteId\":1,\"mobilePositionSubmissionInterval\":5,\"model\":\"DS-7616NX-I3\",\"name\":\"DeepinMind\",\"networkType\":4,\"password\":\"O11DE0OTg3 \",\"port\":25606,\"registerTime\":\"2026-07-30 08:37:46\",\"sipGatewayGroupId\":\"sipRoute_1\",\"sipGatewayRouteId\":2,\"sipServerId\":\"1\",\"ssrcCheck\":true,\"streamMode\":\"TCP-PASSIVE\",\"streamModeForParam\":1,\"subscribeCycleForAlarm\":0,\"subscribeCycleForCatalog\":3600,\"subscribeCycleForMobilePosition\":0,\"transport\":\"UDP\",\"updateTime\":\"2026-07-30 18:54:53\",\"useStatus\":1,\"vendorId\":2}"; + private static final String ROUTE_JSON = "{\"id\":2,\"routeCode\":\"sipRoute_1\",\"routeName\":\"信令路由1\"}"; + + @Test + public void testLoginUserPayload() { + LoginUser loginUser = JSON.parseObject(LOGIN_USER_JSON, LoginUser.class); + assertEquals("aaadmin", loginUser.userCode); + assertEquals("tt", loginUser.username); + assertTrue(loginUser.sysUser.admin); + + JSONObject object = JSON.parseObject(LOGIN_USER_JSON); + assertEquals("aaadmin", object.getString("userCode")); + } + + @Test + public void testDevicePayload() { + Device device = JSON.parseObject(DEVICE_JSON, Device.class); + assertEquals("44444444", device.deviceId); + assertEquals("sipRoute_1", device.sipGatewayGroupId); + + JSONObject object = JSON.parseObject(DEVICE_JSON); + assertEquals("DeepinMind", object.getString("name")); + } + + @Test + public void testRoutePayload() { + SipRouteCache route = JSON.parseObject(ROUTE_JSON, SipRouteCache.class); + assertEquals(2, route.id); + assertEquals("sipRoute_1", route.routeCode); + assertEquals("\u4fe1\u4ee4\u8def\u75311", route.routeName); + } + + public static class LoginUser { + public long expireTime; + public boolean ifRemindResetPassword; + public String ipaddr; + public long loginTime; + public int passwordExpiredDay; + public String[] permissions; + public SysUser sysUser; + public String token; + public String userCode; + public int userid; + public String username; + } + + public static class SysUser { + public boolean admin; + public int apiCall; + public int businessCatalogType; + public int isEnableSecurityDownload; + public int isSingleLogin; + public boolean limitSingleLogin; + public int ptzLockTime; + public int status; + public String telephone; + public int tenantId; + public boolean tenantMgr; + public String userCode; + public int userId; + public int userLevel; + public String userName; + public String userPassword; + public int userType; + public int videoPlayConcurrent; + } + + public static class Device { + public boolean auth; + public boolean broadcastPushAfterAck; + public int channelCount; + public String deviceId; + public int deviceType; + public boolean enableAudio; + public int expires; + public String firmware; + public String hostAddress; + public String ip; + public int isStoreGbDeviceAlarm; + public int keepaliveIntervalTime; + public String keepaliveTime; + public String localIp; + public String manufacturer; + public int mediaRouteId; + public int mobilePositionSubmissionInterval; + public String model; + public String name; + public int networkType; + public String password; + public int port; + public String registerTime; + public String sipGatewayGroupId; + public int sipGatewayRouteId; + public String sipServerId; + public boolean ssrcCheck; + public String streamMode; + public int streamModeForParam; + public int subscribeCycleForAlarm; + public int subscribeCycleForCatalog; + public int subscribeCycleForMobilePosition; + public String transport; + public String updateTime; + public int useStatus; + public int vendorId; + } + + public static class SipRouteCache { + public int id; + public String routeCode; + public String routeName; + } +} From 6d2eea33642adb55133e1f53e5e673ee687a65aa Mon Sep 17 00:00:00 2001 From: yangpeng Date: Tue, 25 Aug 2026 11:37:43 +0800 Subject: [PATCH 2/3] fix: address wenshao review on #7755 - drop (byte) cast, gate os.arch, add !OPENJ9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per wenshao/qwen-code review on PR #7755 (2026-08-14): * JSONReaderUTF16.readFieldNameHashCode0: drop (byte) cast so Latin-1 chars 0x80-0xFF pack unsigned, matching Fnv.hashCode64(char[], ...). Without this, parsing any UTF-16 JSON whose field name ends in 0x80-0xFF on aarch64+JDK8 mis-parses deterministically. * JDKUtils: move System.getProperty("os.arch") into the existing try/catch so SecurityException on PropertyPermission("os.arch","read") degrades gracefully instead of killing . * JDKUtils.AARCH64_JDK8: also exclude OPENJ9 - the workaround targets HotSpot C2 miscompilation only. * JSONReaderUTF16: add comments at the three AARCH64_JDK8 guards citing #7732/#3763 and noting retirement condition (JDK 8u472+). * Issue7732: add testLatin1FieldName test for 0x80-0xFF field names plus ¼½¾ round-trip, gating the fix. Refs: #7732, #3763, PR #7755 review by wenshao --- .../alibaba/fastjson2/JSONReaderUTF16.java | 35 ++++++++++--------- .../com/alibaba/fastjson2/util/JDKUtils.java | 8 +++-- .../fastjson2/issues_7000/Issue7732.java | 25 +++++++++++++ 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/com/alibaba/fastjson2/JSONReaderUTF16.java b/core/src/main/java/com/alibaba/fastjson2/JSONReaderUTF16.java index 6ac896a338..b58d504fd8 100644 --- a/core/src/main/java/com/alibaba/fastjson2/JSONReaderUTF16.java +++ b/core/src/main/java/com/alibaba/fastjson2/JSONReaderUTF16.java @@ -1115,6 +1115,7 @@ public final long readFieldNameHashCodeUnquote() { @Override public final long readFieldNameHashCode() { + // JDK 8 HotSpot C2 on aarch64 can miscompile the UNSAFE.getLong SWAR scan. if (AARCH64_JDK8) { return readFieldNameHashCode0(); } @@ -1185,6 +1186,7 @@ public final long readFieldNameHashCode() { @Override public final long readFieldNameHashCode(int keySize, int min, int max) { + // JDK 8 HotSpot C2 on aarch64 can miscompile the UNSAFE.getLong SWAR scan. if (AARCH64_JDK8) { return readFieldNameHashCode0(); } @@ -1260,6 +1262,7 @@ public final long readFieldNameHashCode(int keySize, int min, int max) { @Override public long readFieldNameHashCodeE(int size0, int size1, int size3) { + // JDK 8 HotSpot C2 on aarch64 can miscompile the UNSAFE.getLong SWAR scan. if (AARCH64_JDK8) { return readFieldNameHashCode0(); } @@ -1382,7 +1385,7 @@ private final long readFieldNameHashCode0() { if (c0 == quote) { nameValue = 0; } else if (c1 == quote && c0 != 0 && c0 != '\\' && c0 <= 0xFF) { - nameValue = (byte) c0; + nameValue = c0; this.nameLength = 1; this.nameEnd = offset + 1; offset += 2; @@ -1390,7 +1393,7 @@ private final long readFieldNameHashCode0() { && c0 != '\\' && c1 != '\\' && c0 <= 0xFF && c1 <= 0xFF ) { - nameValue = (((byte) c1) << 8) + nameValue = (c1 << 8) + c0; this.nameLength = 2; this.nameEnd = offset + 2; @@ -1399,7 +1402,7 @@ private final long readFieldNameHashCode0() { && c0 != '\\' && c1 != '\\' && c2 != '\\' && c0 <= 0xFF && c1 <= 0xFF && c2 <= 0xFF) { nameValue - = (((byte) c2) << 16) + = (c2 << 16) + (c1 << 8) + c0; this.nameLength = 3; @@ -1410,7 +1413,7 @@ private final long readFieldNameHashCode0() { && c0 <= 0xFF && c1 <= 0xFF && c2 <= 0xFF && c3 <= 0xFF ) { nameValue - = (((byte) c3) << 24) + = (((long) c3) << 24) + (c2 << 16) + (c1 << 8) + c0; @@ -1422,7 +1425,7 @@ private final long readFieldNameHashCode0() { && c0 <= 0xFF && c1 <= 0xFF && c2 <= 0xFF && c3 <= 0xFF && c4 <= 0xFF ) { nameValue - = (((long) ((byte) c4)) << 32) + = (((long) c4) << 32) + (((long) c3) << 24) + (((long) c2) << 16) + (((long) c1) << 8) @@ -1435,7 +1438,7 @@ private final long readFieldNameHashCode0() { && c0 <= 0xFF && c1 <= 0xFF && c2 <= 0xFF && c3 <= 0xFF && c4 <= 0xFF && c5 <= 0xFF ) { nameValue - = (((long) ((byte) c5)) << 40) + = (((long) c5) << 40) + (((long) c4) << 32) + (((long) c3) << 24) + (((long) c2) << 16) @@ -1449,7 +1452,7 @@ private final long readFieldNameHashCode0() { && c0 <= 0xFF && c1 <= 0xFF && c2 <= 0xFF && c3 <= 0xFF && c4 <= 0xFF && c5 <= 0xFF && c6 <= 0xFF ) { nameValue - = (((long) ((byte) c6)) << 48) + = (((long) c6) << 48) + (((long) c5) << 40) + (((long) c4) << 32) + (((long) c3) << 24) @@ -1464,7 +1467,7 @@ private final long readFieldNameHashCode0() { && c0 <= 0xFF && c1 <= 0xFF && c2 <= 0xFF && c3 <= 0xFF && c4 <= 0xFF && c5 <= 0xFF && c6 <= 0xFF && c7 <= 0xFF ) { nameValue - = (((long) ((byte) c7)) << 56) + = (((long) c7) << 56) + (((long) c6) << 48) + (((long) c5) << 40) + (((long) c4) << 32) @@ -1524,28 +1527,28 @@ private final long readFieldNameHashCode0() { switch (i) { case 0: - nameValue = (byte) c; + nameValue = c; break; case 1: - nameValue = (((byte) c) << 8) + (nameValue & 0xFFL); + nameValue = ((long) c << 8) + (nameValue & 0xFFL); break; case 2: - nameValue = (((byte) c) << 16) + (nameValue & 0xFFFFL); + nameValue = ((long) c << 16) + (nameValue & 0xFFFFL); break; case 3: - nameValue = (((byte) c) << 24) + (nameValue & 0xFFFFFFL); + nameValue = ((long) c << 24) + (nameValue & 0xFFFFFFL); break; case 4: - nameValue = (((long) (byte) c) << 32) + (nameValue & 0xFFFFFFFFL); + nameValue = ((long) c << 32) + (nameValue & 0xFFFFFFFFL); break; case 5: - nameValue = (((long) (byte) c) << 40L) + (nameValue & 0xFFFFFFFFFFL); + nameValue = ((long) c << 40L) + (nameValue & 0xFFFFFFFFFFL); break; case 6: - nameValue = (((long) (byte) c) << 48L) + (nameValue & 0xFFFFFFFFFFFFL); + nameValue = ((long) c << 48L) + (nameValue & 0xFFFFFFFFFFFFL); break; case 7: - nameValue = (((long) (byte) c) << 56L) + (nameValue & 0xFFFFFFFFFFFFFFL); + nameValue = ((long) c << 56L) + (nameValue & 0xFFFFFFFFFFFFFFL); break; default: break; diff --git a/core/src/main/java/com/alibaba/fastjson2/util/JDKUtils.java b/core/src/main/java/com/alibaba/fastjson2/util/JDKUtils.java index 4c5796c550..9fe3b1cb37 100644 --- a/core/src/main/java/com/alibaba/fastjson2/util/JDKUtils.java +++ b/core/src/main/java/com/alibaba/fastjson2/util/JDKUtils.java @@ -90,8 +90,10 @@ public class JDKUtils { } int jvmVersion = -1, android_sdk_int = -1; + String osArch = null; boolean openj9 = false, android = false, graal = false; try { + osArch = System.getProperty("os.arch"); String jvmName = System.getProperty("java.vm.name"); if (jvmName != null) { openj9 = jvmName.contains("OpenJ9"); @@ -125,7 +127,6 @@ public class JDKUtils { GRAAL = graal; ANDROID_SDK_INT = android_sdk_int; - String osArch = System.getProperty("os.arch"); OS_ARCH = osArch; AARCH64 = "aarch64".equals(osArch) || "arm64".equals(osArch); @@ -152,7 +153,10 @@ public class JDKUtils { CLASS_TRANSIENT = transientClass; JVM_VERSION = jvmVersion; - AARCH64_JDK8 = AARCH64 && JVM_VERSION == 8 && !ANDROID && !GRAAL; + // JDK 8 HotSpot C2 on aarch64 can miscompile the UNSAFE.getLong SWAR + // field-name scan. Keep the safe char[] path for #7732/#3763 until + // affected JDK 8 builds are no longer supported. + AARCH64_JDK8 = AARCH64 && JVM_VERSION == 8 && !ANDROID && !GRAAL && !OPENJ9; if (JVM_VERSION == 8) { Field field = null; diff --git a/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7732.java b/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7732.java index bb1a96d993..edcba7fe09 100644 --- a/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7732.java +++ b/core/src/test/java/com/alibaba/fastjson2/issues_7000/Issue7732.java @@ -2,9 +2,13 @@ import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.util.Fnv; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import java.lang.reflect.Method; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -43,6 +47,27 @@ public void testRoutePayload() { assertEquals("\u4fe1\u4ee4\u8def\u75311", route.routeName); } + @Test + public void testLatin1FieldName() throws Exception { + for (int length = 1; length <= 8; length++) { + StringBuilder fieldNameBuilder = new StringBuilder(length); + for (int i = 0; i < length; i++) { + fieldNameBuilder.append((char) (0x80 + i)); + } + String fieldName = fieldNameBuilder.toString(); + JSONReader reader = JSONReader.of("{\"" + fieldName + "\":\"v\"}"); + assertTrue(reader.nextIfObjectStart()); + Method readFieldNameHashCode0 = reader.getClass().getDeclaredMethod("readFieldNameHashCode0"); + readFieldNameHashCode0.setAccessible(true); + assertEquals(Fnv.hashCode64(fieldName), ((Number) readFieldNameHashCode0.invoke(reader)).longValue()); + assertEquals(fieldName, reader.getFieldName()); + } + + String fieldName = "\u00bc\u00bd\u00be"; + JSONObject object = JSON.parseObject("{\"" + fieldName + "\":\"v\"}"); + assertEquals("v", object.getString(fieldName)); + } + public static class LoginUser { public long expireTime; public boolean ifRemindResetPassword; From 6b2f28d8370c4e8c78d11247ea8b58d34e16eec7 Mon Sep 17 00:00:00 2001 From: yangpeng Date: Thu, 27 Aug 2026 18:18:20 +0800 Subject: [PATCH 3/3] fix: avoid unsafe grouped int writes on aarch64+jdk8 --- .../com/alibaba/fastjson2/util/IOUtils.java | 62 +++++++++++++++++++ .../alibaba/fastjson2/util/IOUtilsTest.java | 42 +++++++++++++ 2 files changed, 104 insertions(+) diff --git a/core/src/main/java/com/alibaba/fastjson2/util/IOUtils.java b/core/src/main/java/com/alibaba/fastjson2/util/IOUtils.java index 53b26ad2f5..b65c6cf5d9 100644 --- a/core/src/main/java/com/alibaba/fastjson2/util/IOUtils.java +++ b/core/src/main/java/com/alibaba/fastjson2/util/IOUtils.java @@ -1200,6 +1200,9 @@ public static int writeLocalTime(char[] buf, int off, LocalTime time) { } private static int writeInt4(byte[] buf, int off, int v) { + if (JDKUtils.AARCH64_JDK8) { + return writeIntNSafe(buf, off, v, 4); + } int v1 = (int) (v * 1374389535L >> 37); // v / 100; int v0 = v - v1 * 100; int v2 = PACKED_DIGITS[v1 & 0x7f] | (PACKED_DIGITS[v0 & 0x7f] << 16); @@ -1211,6 +1214,9 @@ private static int writeInt4(byte[] buf, int off, int v) { } private static int writeInt4(char[] buf, int off, int v) { + if (JDKUtils.AARCH64_JDK8) { + return writeIntNSafe(buf, off, v, 4); + } int v1 = (int) (v * 1374389535L >> 37); // v / 100; putLongUnaligned(buf, off, mergeInt64(v - v1 * 100, v1)); return off + 4; @@ -1225,18 +1231,27 @@ private static long mergeInt64(int v1, int v2) { } private static int writeInt3(byte[] buf, int off, int val) { + if (JDKUtils.AARCH64_JDK8) { + return writeInt3Safe(buf, off, val); + } int v = DIGITS_K_32[val & 0x3ff]; UNSAFE.putInt(buf, ARRAY_BYTE_BASE_OFFSET + off, v >> ((((byte) v) + 1) << 3)); return off + 3 - (byte) v; } private static int writeInt3(char[] buf, int off, int val) { + if (JDKUtils.AARCH64_JDK8) { + return writeInt3Safe(buf, off, val); + } long v = DIGITS_K_64[val & 0x3ff]; UNSAFE.putLong(buf, ARRAY_CHAR_BASE_OFFSET + ((long) off << 1), v >> ((((short) v) + 1) << 4)); return off + 3 - (byte) v; } private static int writeInt8(byte[] buf, int off, int v1, int v2) { + if (JDKUtils.AARCH64_JDK8) { + return writeInt8Safe(buf, off, v1, v2); + } int r1 = (int) (v1 * 1374389535L >> 37); // v1 / 100; int r2 = (int) (v2 * 1374389535L >> 37); // v2 / 100; long v = (PACKED_DIGITS[r1 & 0x7f]) @@ -1251,6 +1266,9 @@ private static int writeInt8(byte[] buf, int off, int v1, int v2) { } private static int writeInt8(char[] buf, int off, int v1, int v2) { + if (JDKUtils.AARCH64_JDK8) { + return writeInt8Safe(buf, off, v1, v2); + } int r1 = (int) (v1 * 1374389535L >> 37); // v1 / 100; long x1 = (PACKED_DIGITS_UTF16[r1 & 0x7f]) | ((long) PACKED_DIGITS_UTF16[(v1 - r1 * 100) & 0x7f] << 32); @@ -1266,6 +1284,50 @@ private static int writeInt8(char[] buf, int off, int v1, int v2) { return off + 8; } + static int writeInt3Safe(byte[] buf, int off, int value) { + int length = value < 100 ? (value < 10 ? 1 : 2) : 3; + for (int i = length; i > 0; i--) { + buf[off + i - 1] = (byte) ('0' + value % 10); + value /= 10; + } + return off + length; + } + + static int writeInt3Safe(char[] buf, int off, int value) { + int length = value < 100 ? (value < 10 ? 1 : 2) : 3; + for (int i = length; i > 0; i--) { + buf[off + i - 1] = (char) ('0' + value % 10); + value /= 10; + } + return off + length; + } + + static int writeIntNSafe(byte[] buf, int off, int value, int digits) { + for (int i = digits; i > 0; i--) { + buf[off + i - 1] = (byte) ('0' + value % 10); + value /= 10; + } + return off + digits; + } + + static int writeIntNSafe(char[] buf, int off, int value, int digits) { + for (int i = digits; i > 0; i--) { + buf[off + i - 1] = (char) ('0' + value % 10); + value /= 10; + } + return off + digits; + } + + static int writeInt8Safe(byte[] buf, int off, int high, int low) { + off = writeIntNSafe(buf, off, high, 4); + return writeIntNSafe(buf, off, low, 4); + } + + static int writeInt8Safe(char[] buf, int off, int high, int low) { + off = writeIntNSafe(buf, off, high, 4); + return writeIntNSafe(buf, off, low, 4); + } + /** * Writes a 64-bit long integer value to a byte array. * This method converts a long integer to its string representation and writes it diff --git a/core/src/test/java/com/alibaba/fastjson2/util/IOUtilsTest.java b/core/src/test/java/com/alibaba/fastjson2/util/IOUtilsTest.java index 61d8f50113..795a642e82 100644 --- a/core/src/test/java/com/alibaba/fastjson2/util/IOUtilsTest.java +++ b/core/src/test/java/com/alibaba/fastjson2/util/IOUtilsTest.java @@ -1,5 +1,6 @@ package com.alibaba.fastjson2.util; +import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONException; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -19,6 +20,47 @@ public void size() { assertEquals(Long.toString(Long.MAX_VALUE), toString1(Long.MAX_VALUE)); } + @Test + public void groupedIntSafeWrites() { + for (int value = 0; value <= 999; value++) { + String expected = Integer.toString(value); + byte[] bytes = new byte[5]; + char[] chars = new char[5]; + assertEquals(1 + expected.length(), IOUtils.writeInt3Safe(bytes, 1, value)); + assertEquals(expected, new String(bytes, 1, expected.length())); + assertEquals(1 + expected.length(), IOUtils.writeInt3Safe(chars, 1, value)); + assertEquals(expected, new String(chars, 1, expected.length())); + } + + for (int value = 0; value <= 9999; value += 7) { + String expected = String.format("%04d", value); + byte[] bytes = new byte[6]; + char[] chars = new char[6]; + assertEquals(1 + expected.length(), IOUtils.writeIntNSafe(bytes, 1, value, 4)); + assertEquals(expected, new String(bytes, 1, 4)); + assertEquals(1 + expected.length(), IOUtils.writeIntNSafe(chars, 1, value, 4)); + assertEquals(expected, new String(chars, 1, 4)); + } + + byte[] bytes = new byte[10]; + char[] chars = new char[10]; + assertEquals(9, IOUtils.writeInt8Safe(bytes, 1, 12, 3456)); + assertEquals("00123456", new String(bytes, 1, 8)); + assertEquals(9, IOUtils.writeInt8Safe(chars, 1, 12, 3456)); + assertEquals("00123456", new String(chars, 1, 8)); + } + + @Test + public void groupedIntArraySerialization() { + int[] values = {1, 12, 123, 1234, 12345678}; + String expected = "[1,12,123,1234,12345678]"; + for (int i = 0; i < 5000; i++) { + String actual = JSON.toJSONString(values); + assertEquals(expected, actual); + assertArrayEquals(expected.getBytes(StandardCharsets.UTF_8), JSON.toJSONBytes(values)); + } + } + static String toString(long i) { byte[] bytes = new byte[20]; int size = IOUtils.stringSize(i);