From 7c415f1882ba9a863ae3bd8e20837350d4ba1b11 Mon Sep 17 00:00:00 2001 From: baranoveg Date: Tue, 18 Aug 2026 16:31:05 +0300 Subject: [PATCH 01/13] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Fix AI review comments --- .../util/tostring/CircularStringBuilder.java | 99 +++++++++ .../internal/util/tostring/SBLengthLimit.java | 15 ++ .../util/tostring/SBLimitedLength.java | 194 ++++++++++++++++++ .../CircularStringBuilderSelfTest.java | 108 ++++++++++ .../tostring/SBLimitedLengthSelfTest.java | 174 ++++++++++++++++ .../testsuites/IgniteUtilSelfTestSuite.java | 4 +- 6 files changed, 592 insertions(+), 2 deletions(-) create mode 100644 modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java index 1bd54ba879b2d..4f1f9c1d50419 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java @@ -177,6 +177,105 @@ public int getSkipped() { return skipped; } + /** + * Performs an in-place rightward shift of elements within the circular buffer. + * This is used to create space for new data by moving a block of existing elements. + *

+ * The shift is executed in reverse order (from the end of the block to the beginning) + * to prevent overwriting source elements before they are copied. + * + * @param shift The starting offset from the 'finishAt' index, defining the beginning + * of the block to be moved. + * @param moveSteps The number of elements to be shifted to the right. + */ + private void shiftRight(int shift, int moveSteps) { + for (int i = 0; i < moveSteps; i++) { + int pointer = (finishAt + shift - i) % value.length; + value[pointer] = value[(value.length + pointer - shift) % value.length]; + } + } + + /** + * Performs a leftward shift of elements in the circular buffer. + * Copies elements from a source position to a destination position, + * effectively overwriting a range of values. + *

+ * @param shift The offset for the source element. + * @param shiftsCnt The count of elements to shift. + */ + private void shiftLeft(int shift, int shiftsCnt) { + for (int i = 0; i < shiftsCnt; i++) { + int pointer = (finishAt + 1 + i) % value.length; + value[pointer] = value[(pointer + shift) % value.length]; + } + } + + /** + * Inserts a substring from the source string into the buffer at the specified tail position. + *

+ * The insertion is performed in reverse order (from the last character to the first) to + * prevent overwriting source data in the buffer before it is copied. This is a common + * technique for in-place buffer manipulation. + * + * @param src The source string to copy characters from. + * @param tailEndOffset The physical index in the buffer where the last character will be placed. + * @param insertCnt The number of characters from the source string to insert. + */ + private void insertStringTail(String src, int tailEndOffset, int insertCnt) { + for (int i = 0; i < insertCnt; i++) + value[(value.length + tailEndOffset - i - 1) % value.length] = src.charAt(src.length() - 1 - i); + } + + /** + * Inserts a string into the buffer at the specified logical offset. + * This method is optimized to minimize the number of elements moved by choosing + * to shift elements from the closest end (left or right) to the insertion point. + * + * @param offset The logical position (accounting for skipped characters) + * at which to insert. + * @param valToInsert The string to be inserted. + * @throws StringIndexOutOfBoundsException if the offset is invalid. + */ + public void insert(int offset, String valToInsert) { + int curLength = length(); + int offsetInsideBuf = offset - skipped; + if (offset < 0 || offsetInsideBuf > curLength) + throw new StringIndexOutOfBoundsException("Offset " + offset + " out of bounds for length " + curLength); + if (valToInsert == null) + valToInsert = "null"; + int insertLength = valToInsert.length(); + if (insertLength == 0) + return; + if (offsetInsideBuf == curLength) { + append(valToInsert); + return; + } + int spareSpace = value.length - curLength; + int insertCnt = Math.min(valToInsert.length(), spareSpace + offsetInsideBuf); + if (insertCnt <= 0) { + skipped += valToInsert.length(); + return; + } + int bufStartShiftedOffset = full ? (finishAt + 1) % value.length : 0; + int shiftedOffset = (bufStartShiftedOffset + offsetInsideBuf) % value.length; + int moveRightCnt = ((shiftedOffset <= finishAt ? 0 : curLength) + finishAt + 1) - shiftedOffset; + if (!full || offset - skipped > curLength / 2) { + shiftRight(insertCnt, moveRightCnt); + int charsToSkip = Math.max(0, insertCnt - spareSpace); + finishAt = (finishAt + insertCnt) % value.length; + shiftedOffset = (shiftedOffset + insertCnt) % value.length; + full = curLength + insertCnt >= value.length; + insertStringTail(valToInsert, shiftedOffset, insertCnt); + skipped += charsToSkip; + } + else { + int moveLeftCnt = (curLength - moveRightCnt - insertCnt); + shiftLeft(insertCnt, moveLeftCnt); + insertStringTail(valToInsert, shiftedOffset, insertCnt); + skipped += valToInsert.length(); + } + } + /** {@inheritDoc} */ @Override public String toString() { // Create a copy, don't share the array diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java index 58a070eef3ac3..f7e320842dae4 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java @@ -75,10 +75,25 @@ CircularStringBuilder getTail() { return new CircularStringBuilder(TAIL_LEN); } + /** */ + CircularStringBuilder createTail() { + return getTail(); + } + + /** */ + int getTailLengthLimit() { + return TAIL_LEN; + } + /** * @return {@code True} if reached limit. */ boolean overflowed(SBLimitedLength sb) { return sb.impl().length() > HEAD_LEN; } + + /** */ + int getHeadLengthLimit() { + return HEAD_LEN; + } } diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java index d478e3cbcc391..2984843cb9b7e 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java @@ -18,6 +18,7 @@ package org.apache.ignite.internal.util.tostring; import java.util.Arrays; + import org.apache.ignite.internal.util.GridStringBuilder; /** @@ -91,6 +92,12 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(Object obj) { if (lenLimit.overflowed(this)) { + if (tail == null) { + // Tail not created yet, force creation via onWrite + onWrite(length(), 0); + if (tail == null) + tail = lenLimit.createTail(); + } tail.append(obj); return this; } @@ -105,6 +112,12 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(String str) { if (lenLimit.overflowed(this)) { + if (tail == null) { + // Tail not created yet, force creation via onWrite + onWrite(length(), 0); + if (tail == null) + tail = lenLimit.createTail(); + } tail.append(str); return this; } @@ -119,6 +132,12 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(StringBuffer sb) { if (lenLimit.overflowed(this)) { + if (tail == null) { + // Tail not created yet, force creation via onWrite + onWrite(length(), 0); + if (tail == null) + tail = lenLimit.createTail(); + } tail.append(sb); return this; } @@ -133,6 +152,12 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(CharSequence s) { if (lenLimit.overflowed(this)) { + if (tail == null) { + // Tail not created yet, force creation via onWrite + onWrite(length(), 0); + if (tail == null) + tail = lenLimit.createTail(); + } tail.append(s); return this; } @@ -147,6 +172,12 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(CharSequence s, int start, int end) { if (lenLimit.overflowed(this)) { + if (tail == null) { + // Tail not created yet, force creation via onWrite + onWrite(length(), 0); + if (tail == null) + tail = lenLimit.createTail(); + } tail.append(s.subSequence(start, end)); return this; } @@ -161,6 +192,12 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(char[] str) { if (lenLimit.overflowed(this)) { + if (tail == null) { + // Tail not created yet, force creation via onWrite + onWrite(length(), 0); + if (tail == null) + tail = lenLimit.createTail(); + } tail.append(str); return this; } @@ -175,6 +212,12 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(char[] str, int offset, int len) { if (lenLimit.overflowed(this)) { + if (tail == null) { + // Tail not created yet, force creation via onWrite + onWrite(length(), 0); + if (tail == null) + tail = lenLimit.createTail(); + } tail.append(Arrays.copyOfRange(str, offset, len)); return this; } @@ -189,6 +232,12 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(boolean b) { if (lenLimit.overflowed(this)) { + if (tail == null) { + // Tail not created yet, force creation via onWrite + onWrite(length(), 0); + if (tail == null) + tail = lenLimit.createTail(); + } tail.append(b); return this; } @@ -203,6 +252,12 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(char c) { if (lenLimit.overflowed(this)) { + if (tail == null) { + // Tail not created yet, force creation via onWrite + onWrite(length(), 0); + if (tail == null) + tail = lenLimit.createTail(); + } tail.append(c); return this; } @@ -217,6 +272,12 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(int i) { if (lenLimit.overflowed(this)) { + if (tail == null) { + // Tail not created yet, force creation via onWrite + onWrite(length(), 0); + if (tail == null) + tail = lenLimit.createTail(); + } tail.append(i); return this; } @@ -231,6 +292,12 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(long lng) { if (lenLimit.overflowed(this)) { + if (tail == null) { + // Tail not created yet, force creation via onWrite + onWrite(length(), 0); + if (tail == null) + tail = lenLimit.createTail(); + } tail.append(lng); return this; } @@ -245,6 +312,12 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(float f) { if (lenLimit.overflowed(this)) { + if (tail == null) { + // Tail not created yet, force creation via onWrite + onWrite(length(), 0); + if (tail == null) + tail = lenLimit.createTail(); + } tail.append(f); return this; } @@ -259,6 +332,12 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(double d) { if (lenLimit.overflowed(this)) { + if (tail == null) { + // Tail not created yet, force creation via onWrite + onWrite(length(), 0); + if (tail == null) + tail = lenLimit.createTail(); + } tail.append(d); return this; } @@ -273,6 +352,12 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder appendCodePoint(int codePoint) { if (lenLimit.overflowed(this)) { + if (tail == null) { + // Tail not created yet, force creation via onWrite + onWrite(length(), 0); + if (tail == null) + tail = lenLimit.createTail(); + } tail.append(codePoint); return this; } @@ -311,4 +396,113 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { public boolean isOverflowed() { return lenLimit.overflowed(this); } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int offset, String str) { + int headLengthLimit = lenLimit.getHeadLengthLimit(); + if (offset < headLengthLimit) { + impl().insert(offset, str); + if (lenLimit.overflowed(this)) { + String tailCandidate = impl().substring(headLengthLimit); + if (tail == null) + tail = lenLimit.createTail(); + tail.insert(0, tailCandidate); + impl().setLength(headLengthLimit); + } + return this; + } + // INVARIANT: tail is guaranteed to exist when offset >= headLengthLimit, + // because overflow would have created tail before head could reach this offset. + assert tail != null; + tail.insert(offset - headLengthLimit, str); + return this; + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int idx, char[] str, int off, int len) { + return i(idx, new String(str, off, len)); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int off, Object obj) { + return i(off, String.valueOf(obj)); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int off, char[] str) { + return i(off, new String(str)); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int dstOff, CharSequence s) { + return i(dstOff, s.toString()); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int dstOff, CharSequence s, int start, int end) { + return i(dstOff, s.subSequence(start, end).toString()); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int off, boolean b) { + return i(off, String.valueOf(b)); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int off, char c) { + return i(off, String.valueOf(c)); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int off, int i) { + return i(off, String.valueOf(i)); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int off, long l) { + return i(off, String.valueOf(l)); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int off, float f) { + return i(off, String.valueOf(f)); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int off, double d) { + return i(off, String.valueOf(d)); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder d(int start, int end) { + throw new UnsupportedOperationException("Not supported by this implementation"); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder d(int idx) { + throw new UnsupportedOperationException("Not supported by this implementation"); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder r(int start, int end, String str) { + throw new UnsupportedOperationException("Not supported by this implementation"); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder nl() { + return a(org.apache.ignite.internal.util.CommonUtils.nl()); + } + + /** {@inheritDoc} */ + @Override public int length() { + int length = super.length(); + if (tail != null) + length += tail.getSkipped() + tail.length(); + return length; + } + + /** {@inheritDoc} */ + @Override public void setLength(int len) { + throw new UnsupportedOperationException("setLength is not supported by this implementation"); + } } diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/CircularStringBuilderSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/CircularStringBuilderSelfTest.java index f9bf453663793..3818686f9a3d3 100644 --- a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/CircularStringBuilderSelfTest.java +++ b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/CircularStringBuilderSelfTest.java @@ -69,4 +69,112 @@ private void testSB(int capacity, String pattern, int num, String expected) { assertEquals(expected, csb.toString()); } + + /** + * @throws Exception If failed. + */ + @Test + public void testCSBInsert() { + testSBInsert(5, "123456789", 4, "new", "56789"); + testSBInsert(5, "123456789", 5, "new", "w6789"); + testSBInsert(5, "123456789", 6, "new", "ew789"); + testSBInsert(5, "123456789", 7, "new", "new89"); + testSBInsert(5, "123456789", 8, "new", "8new9"); + testSBInsert(5, "123456789", 9, "new", "89new"); + testSBInsert(5, "1", 0, "new", "new1"); + testSBInsert(5, "12", 0, "new", "new12"); + testSBInsert(5, "123", 0, "new", "ew123"); + testSBInsert(5, "1234", 0, "new", "w1234"); + testSBInsert(2, "1", 0, "new", "w1"); + testSBInsert(2, "12", 0, "new", "12"); + testSBInsert(3, "12", 0, "new", "w12"); + testSBInsert(3, "12", 1, "new", "ew2"); + } + + /** + * Assertions to ensure {@link CircularStringBuilder#substring(int, int)} method works + */ + @Test + public void testSubstring() { + CircularStringBuilder circularStrBuilder = new CircularStringBuilder(5); + circularStrBuilder.append("abc"); + assertEquals("abc", circularStrBuilder.substring(0, 3)); + assertEquals("ab", circularStrBuilder.substring(0, 2)); + assertEquals("bc", circularStrBuilder.substring(1, 3)); + circularStrBuilder.append("de"); + assertEquals("abc", circularStrBuilder.substring(0, 3)); + assertEquals("ab", circularStrBuilder.substring(0, 2)); + assertEquals("bc", circularStrBuilder.substring(1, 3)); + assertEquals("abcde", circularStrBuilder.substring(0, 5)); + assertEquals("de", circularStrBuilder.substring(3, 5)); + assertEquals("abc", circularStrBuilder.substring(0, 3)); + circularStrBuilder.append("fg"); + assertEquals("cdefg", circularStrBuilder.substring(2, 7)); + assertEquals("cdef", circularStrBuilder.substring(2, 6)); + assertEquals("defg", circularStrBuilder.substring(3, 7)); + assertEquals("cdefg", circularStrBuilder.substring(0, 7)); + circularStrBuilder.append("hi"); + assertEquals("efg", circularStrBuilder.substring(0, 7)); + assertEquals("efghi", circularStrBuilder.substring(0, 9)); + circularStrBuilder.append("j"); + assertEquals("fghi", circularStrBuilder.substring(0, 9)); + assertEquals("fghij", circularStrBuilder.substring(0, 10)); + assertEquals("ghij", circularStrBuilder.substring(6, 10)); + assertEquals("", circularStrBuilder.substring(0, 5)); + assertEquals("f", circularStrBuilder.substring(0, 6)); + } + + /** + * Assertions to ensure {@link CircularStringBuilder#substring(int, int)} method works + */ + @Test + public void testSubstringWithCharSequenceAppend() { + CircularStringBuilder circularStrBuilder = new CircularStringBuilder(5); + circularStrBuilder.append("abc".toCharArray(), 0, 3); + assertEquals("abc", circularStrBuilder.substring(0, 3)); + assertEquals("ab", circularStrBuilder.substring(0, 2)); + assertEquals("bc", circularStrBuilder.substring(1, 3)); + circularStrBuilder.append("de".toCharArray(), 0, 2); + assertEquals("abc", circularStrBuilder.substring(0, 3)); + assertEquals("ab", circularStrBuilder.substring(0, 2)); + assertEquals("bc", circularStrBuilder.substring(1, 3)); + assertEquals("abcde", circularStrBuilder.substring(0, 5)); + assertEquals("de", circularStrBuilder.substring(3, 5)); + assertEquals("abc", circularStrBuilder.substring(0, 3)); + circularStrBuilder.append("fg".toCharArray(), 0, 2); + assertEquals("cdefg", circularStrBuilder.substring(2, 7)); + assertEquals("cdef", circularStrBuilder.substring(2, 6)); + assertEquals("defg", circularStrBuilder.substring(3, 7)); + assertEquals("cdefg", circularStrBuilder.substring(0, 7)); + circularStrBuilder.append("ashi".toCharArray(), 2, 2); + assertEquals("efg", circularStrBuilder.substring(0, 7)); + assertEquals("efghi", circularStrBuilder.substring(0, 9)); + circularStrBuilder.append("j".toCharArray(), 0, 1); + circularStrBuilder.append("j".toCharArray(), 1, 0); + assertEquals("fghi", circularStrBuilder.substring(0, 9)); + assertEquals("fghij", circularStrBuilder.substring(0, 10)); + assertEquals("ghij", circularStrBuilder.substring(6, 10)); + assertEquals("", circularStrBuilder.substring(0, 5)); + assertEquals("f", circularStrBuilder.substring(0, 6)); + } + + /** + * Test ring buffer method {@link CircularStringBuilder#insert(int, String)} + * @param capacity ring buffer capacity + * @param firstVal value to append to buffer before test + * @param offset insert offset argument + * @param insertSubstring insert substring argument + * @param expectedResult expected ring buffer state + * (to assert it equals to {@link CircularStringBuilder#toString()}) + */ + private void testSBInsert(int capacity, + String firstVal, + int offset, + String insertSubstring, + String expectedResult) { + CircularStringBuilder csb = new CircularStringBuilder(capacity); + csb.append(firstVal); + csb.insert(offset, insertSubstring); + assertEquals(expectedResult, csb.toString()); + } } diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java new file mode 100644 index 0000000000000..ad0e37531eaae --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java @@ -0,0 +1,174 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.util.tostring; + +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.apache.ignite.testframework.junits.common.GridCommonTest; +import org.junit.Assert; +import org.junit.Test; + +/** + * Test suite to ensure SBLimitedLength works by design + */ +@GridCommonTest(group = "Utils") +public class SBLimitedLengthSelfTest extends GridCommonAbstractTest { + /** Ensure all append operations are working fine */ + @Test + public void testAppend() { + SBLimitedLength strBuilder = getStrBuilder(5, 50); + strBuilder.a(1); + Assert.assertEquals("1", strBuilder.toString()); + strBuilder.a(2L); + Assert.assertEquals("12", strBuilder.toString()); + strBuilder.a(3f); + Assert.assertEquals("123.0", strBuilder.toString()); + strBuilder.a(4d); + Assert.assertEquals("123.04.0", strBuilder.toString()); + strBuilder.a('5'); + Assert.assertEquals("123.04.05", strBuilder.toString()); + strBuilder.a(true); + Assert.assertEquals("123.04.05true", strBuilder.toString()); + Object obj = "6"; + strBuilder.a(obj); + Assert.assertEquals("123.04.05true6", strBuilder.toString()); + strBuilder.a("7"); + Assert.assertEquals("123.04.05true67", strBuilder.toString()); + strBuilder.a(new StringBuilder().append("8")); + Assert.assertEquals("123.04.05true678", strBuilder.toString()); + CharSequence charSeq = "9"; + strBuilder.a(charSeq); + Assert.assertEquals("123.04.05true6789", strBuilder.toString()); + strBuilder.a(charSeq, 0, 1); + Assert.assertEquals("123.04.05true67899", strBuilder.toString()); + strBuilder.a(new char[]{'a'}); + Assert.assertEquals("123.04.05true67899a", strBuilder.toString()); + strBuilder.a(new char[]{'b', 'c', 'd'}, 0, 2); + Assert.assertEquals("123.04.05true67899abc", strBuilder.toString()); + } + + /** */ + @Test + public void testDoesNotThrowNPEOnHeadOverflow() { + SBLimitedLength sbLimitedLength = new SBLimitedLength(256); + sbLimitedLength.initLimit(new SBLengthLimit()); + sbLimitedLength.a("a".repeat(7999)); + sbLimitedLength.i(7000, "asd"); + sbLimitedLength.a("a".repeat(10)); + String result = sbLimitedLength.toString(); + Assert.assertNotNull(result); + Assert.assertFalse(result.isEmpty()); + Assert.assertTrue(result.contains("asd")); + } + + /** Ensure all insert operations are working fine */ + @Test + public void testInsert() { + SBLimitedLength strBuilder = getStrBuilder(5, 50); + strBuilder.i(0, 1); + Assert.assertEquals("1", strBuilder.toString()); + strBuilder.i(0, 2L); + Assert.assertEquals("21", strBuilder.toString()); + strBuilder.i(0, 3f); + Assert.assertEquals("3.021", strBuilder.toString()); + strBuilder.i(0, 4d); + Assert.assertEquals("4.03.021", strBuilder.toString()); + strBuilder.i(0, true); + Assert.assertEquals("true4.03.021", strBuilder.toString()); + strBuilder.i(0, '5'); + Assert.assertEquals("5true4.03.021", strBuilder.toString()); + strBuilder.i(1, "6"); + Assert.assertEquals("56true4.03.021", strBuilder.toString()); + strBuilder.i(2, new char[] {'a', 'b', 'c', 'd'}); + Assert.assertEquals("56abcdtrue4.03.021", strBuilder.toString()); + strBuilder.i(5, new char[] {'e', 'f', 'g', 'i'}, 0, 3); + Assert.assertEquals("56abcefgdtrue4.03.021", strBuilder.toString()); + Object obj = "h"; + strBuilder.i(6, obj); + Assert.assertEquals("56abcehfgdtrue4.03.021", strBuilder.toString()); + CharSequence charSeq = "ijk"; + strBuilder.i(7, charSeq); + Assert.assertEquals("56abcehijkfgdtrue4.03.021", strBuilder.toString()); + strBuilder.i(8, charSeq, 0, 2); + Assert.assertEquals("56abcehiijjkfgdtrue4.03.021", strBuilder.toString()); + } + + /** Ensure toString works as expected */ + @Test + public void testToString() { + SBLimitedLength strBuilder = getStrBuilder(2, 2); + strBuilder.a("ab"); + Assert.assertEquals("ab", strBuilder.toString()); + strBuilder.a("cd"); + Assert.assertEquals("abcd", strBuilder.toString()); + strBuilder.a("ef"); + Assert.assertEquals("ab... and 4 skipped ...ef", strBuilder.toString()); + } + + /** Ensure all operations that could possibly reduce length are prohibited */ + @Test + public void testLengthReduceOperationsAreProhibited() { + SBLimitedLength strBuilder = getStrBuilder(2, 2); + assertThrows(UnsupportedOperationException.class, () -> strBuilder.d(0)); + assertThrows(UnsupportedOperationException.class, () -> strBuilder.d(0, 0)); + assertThrows(UnsupportedOperationException.class, () -> strBuilder.r(0, 0, "asd")); + assertThrows(UnsupportedOperationException.class, () -> strBuilder.setLength(0)); + } + + /** + * Assert {@link Runnable#run()} will throw specified exception + * @param expectedExceptionClass Expected exception class. + * @param runnable Runnable. + */ + private void assertThrows(Class expectedExceptionClass, Runnable runnable) { + boolean eIsSpotted = false; + try { + runnable.run(); + } + catch (Throwable throwable) { + if (expectedExceptionClass.isAssignableFrom(throwable.getClass())) + eIsSpotted = true; + } + finally { + Assert.assertTrue(eIsSpotted); + } + } + + /** + * Get {@link SBLimitedLength} instance with specific head and tail length + * to simplify test cases + * @param headLength Head length. + * @param tailLength Tail length. + */ + private SBLimitedLength getStrBuilder(int headLength, int tailLength) { + SBLimitedLength sbLimitedLength = new SBLimitedLength(0); + sbLimitedLength.initLimit(new SBLengthLimit() { + + @Override int getHeadLengthLimit() { + return headLength; + } + + @Override int getTailLengthLimit() { + return tailLength; + } + }); + return sbLimitedLength; + } +} + + + diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteUtilSelfTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteUtilSelfTestSuite.java index ad2129d4d2e2a..5b5d98fc87b1c 100644 --- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteUtilSelfTestSuite.java +++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteUtilSelfTestSuite.java @@ -36,7 +36,6 @@ import org.apache.ignite.internal.util.distributed.DistributedProcessClientAwaitTest; import org.apache.ignite.internal.util.distributed.DistributedProcessCoordinatorLeftTest; import org.apache.ignite.internal.util.distributed.DistributedProcessErrorHandlingTest; -import org.apache.ignite.internal.util.distributed.DistributedProcessResultMarshallingTest; import org.apache.ignite.internal.util.nio.GridNioDelimitedBufferSelfTest; import org.apache.ignite.internal.util.nio.GridNioSelfTest; import org.apache.ignite.internal.util.nio.GridNioServerTest; @@ -51,6 +50,7 @@ import org.apache.ignite.internal.util.tostring.GridToStringBuilderSelfTest; import org.apache.ignite.internal.util.tostring.IncludeSensitiveAtomicTest; import org.apache.ignite.internal.util.tostring.IncludeSensitiveTransactionalTest; +import org.apache.ignite.internal.util.tostring.SBLimitedLengthSelfTest; import org.apache.ignite.internal.util.tostring.TransactionSensitiveDataTest; import org.apache.ignite.lang.GridByteArrayListSelfTest; import org.apache.ignite.spi.discovery.ClusterMetricsSelfTest; @@ -92,6 +92,7 @@ GridStringBuilderFactorySelfTest.class, GridToStringBuilderSelfTest.class, CircularStringBuilderSelfTest.class, + SBLimitedLengthSelfTest.class, GridByteArrayListSelfTest.class, GridMBeanSelfTest.class, GridMBeanDisableSelfTest.class, @@ -148,7 +149,6 @@ DistributedProcessErrorHandlingTest.class, DistributedProcessCoordinatorLeftTest.class, DistributedProcessClientAwaitTest.class, - DistributedProcessResultMarshallingTest.class, BasicRateLimiterTest.class, From 30d035187255aaa7c584a6de582b13c18ce59aec Mon Sep 17 00:00:00 2001 From: baranoveg Date: Tue, 18 Aug 2026 17:48:14 +0300 Subject: [PATCH 02/13] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. --- .../util/tostring/SBLimitedLength.java | 112 +++++------------- 1 file changed, 28 insertions(+), 84 deletions(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java index 2984843cb9b7e..8def309c070db 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java @@ -92,12 +92,8 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(Object obj) { if (lenLimit.overflowed(this)) { - if (tail == null) { - // Tail not created yet, force creation via onWrite - onWrite(length(), 0); - if (tail == null) - tail = lenLimit.createTail(); - } + if (tail == null) + tail = lenLimit.createTail(); tail.append(obj); return this; } @@ -112,12 +108,8 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(String str) { if (lenLimit.overflowed(this)) { - if (tail == null) { - // Tail not created yet, force creation via onWrite - onWrite(length(), 0); - if (tail == null) - tail = lenLimit.createTail(); - } + if (tail == null) + tail = lenLimit.createTail(); tail.append(str); return this; } @@ -132,12 +124,8 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(StringBuffer sb) { if (lenLimit.overflowed(this)) { - if (tail == null) { - // Tail not created yet, force creation via onWrite - onWrite(length(), 0); - if (tail == null) - tail = lenLimit.createTail(); - } + if (tail == null) + tail = lenLimit.createTail(); tail.append(sb); return this; } @@ -152,12 +140,8 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(CharSequence s) { if (lenLimit.overflowed(this)) { - if (tail == null) { - // Tail not created yet, force creation via onWrite - onWrite(length(), 0); - if (tail == null) - tail = lenLimit.createTail(); - } + if (tail == null) + tail = lenLimit.createTail(); tail.append(s); return this; } @@ -172,12 +156,8 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(CharSequence s, int start, int end) { if (lenLimit.overflowed(this)) { - if (tail == null) { - // Tail not created yet, force creation via onWrite - onWrite(length(), 0); - if (tail == null) - tail = lenLimit.createTail(); - } + if (tail == null) + tail = lenLimit.createTail(); tail.append(s.subSequence(start, end)); return this; } @@ -192,12 +172,8 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(char[] str) { if (lenLimit.overflowed(this)) { - if (tail == null) { - // Tail not created yet, force creation via onWrite - onWrite(length(), 0); - if (tail == null) - tail = lenLimit.createTail(); - } + if (tail == null) + tail = lenLimit.createTail(); tail.append(str); return this; } @@ -212,12 +188,8 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(char[] str, int offset, int len) { if (lenLimit.overflowed(this)) { - if (tail == null) { - // Tail not created yet, force creation via onWrite - onWrite(length(), 0); - if (tail == null) - tail = lenLimit.createTail(); - } + if (tail == null) + tail = lenLimit.createTail(); tail.append(Arrays.copyOfRange(str, offset, len)); return this; } @@ -232,12 +204,8 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(boolean b) { if (lenLimit.overflowed(this)) { - if (tail == null) { - // Tail not created yet, force creation via onWrite - onWrite(length(), 0); - if (tail == null) - tail = lenLimit.createTail(); - } + if (tail == null) + tail = lenLimit.createTail(); tail.append(b); return this; } @@ -252,12 +220,8 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(char c) { if (lenLimit.overflowed(this)) { - if (tail == null) { - // Tail not created yet, force creation via onWrite - onWrite(length(), 0); - if (tail == null) - tail = lenLimit.createTail(); - } + if (tail == null) + tail = lenLimit.createTail(); tail.append(c); return this; } @@ -272,12 +236,8 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(int i) { if (lenLimit.overflowed(this)) { - if (tail == null) { - // Tail not created yet, force creation via onWrite - onWrite(length(), 0); - if (tail == null) - tail = lenLimit.createTail(); - } + if (tail == null) + tail = lenLimit.createTail(); tail.append(i); return this; } @@ -292,12 +252,8 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(long lng) { if (lenLimit.overflowed(this)) { - if (tail == null) { - // Tail not created yet, force creation via onWrite - onWrite(length(), 0); - if (tail == null) - tail = lenLimit.createTail(); - } + if (tail == null) + tail = lenLimit.createTail(); tail.append(lng); return this; } @@ -312,12 +268,8 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(float f) { if (lenLimit.overflowed(this)) { - if (tail == null) { - // Tail not created yet, force creation via onWrite - onWrite(length(), 0); - if (tail == null) - tail = lenLimit.createTail(); - } + if (tail == null) + tail = lenLimit.createTail(); tail.append(f); return this; } @@ -332,12 +284,8 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(double d) { if (lenLimit.overflowed(this)) { - if (tail == null) { - // Tail not created yet, force creation via onWrite - onWrite(length(), 0); - if (tail == null) - tail = lenLimit.createTail(); - } + if (tail == null) + tail = lenLimit.createTail(); tail.append(d); return this; } @@ -352,12 +300,8 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder appendCodePoint(int codePoint) { if (lenLimit.overflowed(this)) { - if (tail == null) { - // Tail not created yet, force creation via onWrite - onWrite(length(), 0); - if (tail == null) - tail = lenLimit.createTail(); - } + if (tail == null) + tail = lenLimit.createTail(); tail.append(codePoint); return this; } From d020d750415df13e4f485e4e1bf06dbd3e46b85a Mon Sep 17 00:00:00 2001 From: baranoveg Date: Tue, 18 Aug 2026 18:24:37 +0300 Subject: [PATCH 03/13] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Add substring and append(char[], int, int) methods Co-authored-by: GigaCode Assistant --- .../util/tostring/CircularStringBuilder.java | 91 +++++++++++++++++++ .../tostring/SBLimitedLengthSelfTest.java | 47 ++++++++++ 2 files changed, 138 insertions(+) diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java index 4f1f9c1d50419..51da8be7ae1be 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java @@ -138,6 +138,58 @@ public CircularStringBuilder append(String str) { return this; } + /** + * Appends the specified character array (or a subrange) to this circular buffer. + * + * @param str the character array. + * @param off the offset within the array. + * @param len the number of characters to append. + * @return a reference to this object. + */ + public CircularStringBuilder append(char[] str, int off, int len) { + if (str == null) + return appendNull(); + + if (len >= value.length) { + // Data bigger or equal to value length + System.arraycopy(str, off + len - value.length, value, 0, value.length); + + skipped += len - value.length + finishAt + 1; + + finishAt = value.length - 1; + + full = true; + } + else { + // Data smaller than value length + if (value.length - finishAt - 1 < len) { + // Data doesn't fit into remaining part of value array + int firstPart = value.length - finishAt - 1; + + if (firstPart > 0) + System.arraycopy(str, off, value, finishAt + 1, firstPart); + + System.arraycopy(str, off + firstPart, value, 0, len - firstPart); + + skipped += full ? len : len - firstPart; + + finishAt = finishAt + len - value.length; + + full = true; + } + else { + // Whole data fits into remaining part of value array + System.arraycopy(str, off, value, finishAt + 1, len); + + skipped += full ? len : 0; + + finishAt += len; + } + } + + return this; + } + /** * Append StringBuffer * @@ -291,4 +343,43 @@ public void insert(int offset, String valToInsert) { else return new String(value, 0, finishAt + 1); } + + /** + * Returns a substring from the logical sequence of characters, accounting for + * the circular buffer structure and any skipped characters. + * + *

This method first validates the indices against the total logical length + * (skipped + visible characters). If the requested range is empty or fully within + * the skipped portion, an empty string is returned for efficiency. + * + *

It then calculates the physical indices in the internal array. If the + * substring wraps around the end of the circular buffer, it performs a two-part + * copy operation to assemble the result. + * + * @param beginIdx the beginning index, inclusive. + * @param endIdx the ending index, exclusive. + * @return a new String containing the specified subsequence. + * @throws StringIndexOutOfBoundsException if beginIdx or endIdx are negative, + * or if endIdx is greater than the total logical length. + * @throws IllegalArgumentException if beginIdx is greater than endIdx. + */ + public String substring(int beginIdx, int endIdx) { + if (beginIdx < 0 || endIdx < 0 || endIdx > skipped + length()) + throw new StringIndexOutOfBoundsException( + "Index out of bounds: beginIdx=" + beginIdx + ", endIdx=" + endIdx); + if (beginIdx > endIdx) + throw new IllegalArgumentException( + "Begin index cannot be greater than end index: beginIdx=" + beginIdx + ", endIdx=" + endIdx); + if (endIdx <= skipped || beginIdx == endIdx) return ""; + char resultArr[] = new char[Math.max(skipped, endIdx) - Math.max(skipped, beginIdx)]; + int effectiveBeginIdx = ((full ? (finishAt + 1) : 0) + Math.max(skipped, beginIdx) - skipped) % value.length; + int effectiveEndIdx = ((full ? (finishAt + 1) : 0) + Math.max(skipped, endIdx) - skipped) % value.length; + if (effectiveBeginIdx >= effectiveEndIdx) { + System.arraycopy(value, effectiveBeginIdx, resultArr, 0, length() - effectiveBeginIdx); + System.arraycopy(value, 0, resultArr, length() - effectiveBeginIdx, effectiveEndIdx); + } + else + System.arraycopy(value, effectiveBeginIdx, resultArr, 0, effectiveEndIdx - effectiveBeginIdx); + return new String(resultArr); + } } diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java index ad0e37531eaae..1b7a5901a6911 100644 --- a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java +++ b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java @@ -75,6 +75,53 @@ public void testDoesNotThrowNPEOnHeadOverflow() { Assert.assertTrue(result.contains("asd")); } + /** + * Test that simulates the NPE scenario from handleRecursion. + * When tail is null but overflowed() returns true, append operations should not throw NPE. + */ + @Test + public void testNPEProtectionWithNullTail() { + // Create SBLimitedLength with very small head limit and force tail creation manually + SBLimitedLength sbLimitedLength = new SBLimitedLength(256); + + // We use a custom SBLengthLimit to trigger the exact scenario + sbLimitedLength.initLimit(new SBLengthLimit() { + @Override + int getHeadLengthLimit() { + return 3; + } + + @Override + int getTailLengthLimit() { + return 10; + } + + @Override + boolean overflowed(SBLimitedLength sb) { + return sb.impl().length() > 3; + } + + @Override + void onWrite(SBLimitedLength sb, int writtenLen) { + super.onWrite(sb, writtenLen); + } + }); + + // Append enough data to overflow head + sbLimitedLength.a("abcd"); + + // At this point, tail should have been created by onWrite + Assert.assertNotNull("Tail should not be null", sbLimitedLength.getTail()); + + // Now simulate handleRecursion scenario: insert followed by append + sbLimitedLength.i(1, "XY"); + sbLimitedLength.a("Z"); + + String result = sbLimitedLength.toString(); + Assert.assertTrue("Result should contain inserted data", result.contains("XY")); + Assert.assertTrue("Result should contain appended data", result.contains("Z")); + } + /** Ensure all insert operations are working fine */ @Test public void testInsert() { From 93a68b68a428529e74fd964bad0a5a5ec73f663f Mon Sep 17 00:00:00 2001 From: baranoveg Date: Wed, 19 Aug 2026 11:30:44 +0300 Subject: [PATCH 04/13] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. --- .../util/tostring/SBLimitedLength.java | 147 +++--------------- .../tostring/SBLimitedLengthSelfTest.java | 62 +------- 2 files changed, 27 insertions(+), 182 deletions(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java index 8def309c070db..06f8e2dd968a6 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java @@ -92,8 +92,7 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(Object obj) { if (lenLimit.overflowed(this)) { - if (tail == null) - tail = lenLimit.createTail(); + initTailIfAbsent(); tail.append(obj); return this; } @@ -108,8 +107,7 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(String str) { if (lenLimit.overflowed(this)) { - if (tail == null) - tail = lenLimit.createTail(); + initTailIfAbsent(); tail.append(str); return this; } @@ -124,8 +122,7 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(StringBuffer sb) { if (lenLimit.overflowed(this)) { - if (tail == null) - tail = lenLimit.createTail(); + initTailIfAbsent(); tail.append(sb); return this; } @@ -140,8 +137,7 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(CharSequence s) { if (lenLimit.overflowed(this)) { - if (tail == null) - tail = lenLimit.createTail(); + initTailIfAbsent(); tail.append(s); return this; } @@ -156,8 +152,7 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(CharSequence s, int start, int end) { if (lenLimit.overflowed(this)) { - if (tail == null) - tail = lenLimit.createTail(); + initTailIfAbsent(); tail.append(s.subSequence(start, end)); return this; } @@ -172,8 +167,7 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(char[] str) { if (lenLimit.overflowed(this)) { - if (tail == null) - tail = lenLimit.createTail(); + initTailIfAbsent(); tail.append(str); return this; } @@ -188,8 +182,7 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(char[] str, int offset, int len) { if (lenLimit.overflowed(this)) { - if (tail == null) - tail = lenLimit.createTail(); + initTailIfAbsent(); tail.append(Arrays.copyOfRange(str, offset, len)); return this; } @@ -204,8 +197,7 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(boolean b) { if (lenLimit.overflowed(this)) { - if (tail == null) - tail = lenLimit.createTail(); + initTailIfAbsent(); tail.append(b); return this; } @@ -220,8 +212,7 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(char c) { if (lenLimit.overflowed(this)) { - if (tail == null) - tail = lenLimit.createTail(); + initTailIfAbsent(); tail.append(c); return this; } @@ -236,8 +227,7 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(int i) { if (lenLimit.overflowed(this)) { - if (tail == null) - tail = lenLimit.createTail(); + initTailIfAbsent(); tail.append(i); return this; } @@ -252,8 +242,7 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(long lng) { if (lenLimit.overflowed(this)) { - if (tail == null) - tail = lenLimit.createTail(); + initTailIfAbsent(); tail.append(lng); return this; } @@ -268,8 +257,7 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(float f) { if (lenLimit.overflowed(this)) { - if (tail == null) - tail = lenLimit.createTail(); + initTailIfAbsent(); tail.append(f); return this; } @@ -284,8 +272,7 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder a(double d) { if (lenLimit.overflowed(this)) { - if (tail == null) - tail = lenLimit.createTail(); + initTailIfAbsent(); tail.append(d); return this; } @@ -300,8 +287,7 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder appendCodePoint(int codePoint) { if (lenLimit.overflowed(this)) { - if (tail == null) - tail = lenLimit.createTail(); + initTailIfAbsent(); tail.append(codePoint); return this; } @@ -341,102 +327,6 @@ public boolean isOverflowed() { return lenLimit.overflowed(this); } - /** {@inheritDoc} */ - @Override public GridStringBuilder i(int offset, String str) { - int headLengthLimit = lenLimit.getHeadLengthLimit(); - if (offset < headLengthLimit) { - impl().insert(offset, str); - if (lenLimit.overflowed(this)) { - String tailCandidate = impl().substring(headLengthLimit); - if (tail == null) - tail = lenLimit.createTail(); - tail.insert(0, tailCandidate); - impl().setLength(headLengthLimit); - } - return this; - } - // INVARIANT: tail is guaranteed to exist when offset >= headLengthLimit, - // because overflow would have created tail before head could reach this offset. - assert tail != null; - tail.insert(offset - headLengthLimit, str); - return this; - } - - /** {@inheritDoc} */ - @Override public GridStringBuilder i(int idx, char[] str, int off, int len) { - return i(idx, new String(str, off, len)); - } - - /** {@inheritDoc} */ - @Override public GridStringBuilder i(int off, Object obj) { - return i(off, String.valueOf(obj)); - } - - /** {@inheritDoc} */ - @Override public GridStringBuilder i(int off, char[] str) { - return i(off, new String(str)); - } - - /** {@inheritDoc} */ - @Override public GridStringBuilder i(int dstOff, CharSequence s) { - return i(dstOff, s.toString()); - } - - /** {@inheritDoc} */ - @Override public GridStringBuilder i(int dstOff, CharSequence s, int start, int end) { - return i(dstOff, s.subSequence(start, end).toString()); - } - - /** {@inheritDoc} */ - @Override public GridStringBuilder i(int off, boolean b) { - return i(off, String.valueOf(b)); - } - - /** {@inheritDoc} */ - @Override public GridStringBuilder i(int off, char c) { - return i(off, String.valueOf(c)); - } - - /** {@inheritDoc} */ - @Override public GridStringBuilder i(int off, int i) { - return i(off, String.valueOf(i)); - } - - /** {@inheritDoc} */ - @Override public GridStringBuilder i(int off, long l) { - return i(off, String.valueOf(l)); - } - - /** {@inheritDoc} */ - @Override public GridStringBuilder i(int off, float f) { - return i(off, String.valueOf(f)); - } - - /** {@inheritDoc} */ - @Override public GridStringBuilder i(int off, double d) { - return i(off, String.valueOf(d)); - } - - /** {@inheritDoc} */ - @Override public GridStringBuilder d(int start, int end) { - throw new UnsupportedOperationException("Not supported by this implementation"); - } - - /** {@inheritDoc} */ - @Override public GridStringBuilder d(int idx) { - throw new UnsupportedOperationException("Not supported by this implementation"); - } - - /** {@inheritDoc} */ - @Override public GridStringBuilder r(int start, int end, String str) { - throw new UnsupportedOperationException("Not supported by this implementation"); - } - - /** {@inheritDoc} */ - @Override public GridStringBuilder nl() { - return a(org.apache.ignite.internal.util.CommonUtils.nl()); - } - /** {@inheritDoc} */ @Override public int length() { int length = super.length(); @@ -445,8 +335,11 @@ public boolean isOverflowed() { return length; } - /** {@inheritDoc} */ - @Override public void setLength(int len) { - throw new UnsupportedOperationException("setLength is not supported by this implementation"); + /** + * + */ + private void initTailIfAbsent() { + if (tail == null) + tail = lenLimit.createTail(); } } diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java index 1b7a5901a6911..e9fa8b8d67328 100644 --- a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java +++ b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java @@ -86,23 +86,19 @@ public void testNPEProtectionWithNullTail() { // We use a custom SBLengthLimit to trigger the exact scenario sbLimitedLength.initLimit(new SBLengthLimit() { - @Override - int getHeadLengthLimit() { + @Override int getHeadLengthLimit() { return 3; } - @Override - int getTailLengthLimit() { + @Override int getTailLengthLimit() { return 10; } - @Override - boolean overflowed(SBLimitedLength sb) { + @Override boolean overflowed(SBLimitedLength sb) { return sb.impl().length() > 3; } - @Override - void onWrite(SBLimitedLength sb, int writtenLen) { + @Override void onWrite(SBLimitedLength sb, int writtenLen) { super.onWrite(sb, writtenLen); } }); @@ -122,38 +118,6 @@ void onWrite(SBLimitedLength sb, int writtenLen) { Assert.assertTrue("Result should contain appended data", result.contains("Z")); } - /** Ensure all insert operations are working fine */ - @Test - public void testInsert() { - SBLimitedLength strBuilder = getStrBuilder(5, 50); - strBuilder.i(0, 1); - Assert.assertEquals("1", strBuilder.toString()); - strBuilder.i(0, 2L); - Assert.assertEquals("21", strBuilder.toString()); - strBuilder.i(0, 3f); - Assert.assertEquals("3.021", strBuilder.toString()); - strBuilder.i(0, 4d); - Assert.assertEquals("4.03.021", strBuilder.toString()); - strBuilder.i(0, true); - Assert.assertEquals("true4.03.021", strBuilder.toString()); - strBuilder.i(0, '5'); - Assert.assertEquals("5true4.03.021", strBuilder.toString()); - strBuilder.i(1, "6"); - Assert.assertEquals("56true4.03.021", strBuilder.toString()); - strBuilder.i(2, new char[] {'a', 'b', 'c', 'd'}); - Assert.assertEquals("56abcdtrue4.03.021", strBuilder.toString()); - strBuilder.i(5, new char[] {'e', 'f', 'g', 'i'}, 0, 3); - Assert.assertEquals("56abcefgdtrue4.03.021", strBuilder.toString()); - Object obj = "h"; - strBuilder.i(6, obj); - Assert.assertEquals("56abcehfgdtrue4.03.021", strBuilder.toString()); - CharSequence charSeq = "ijk"; - strBuilder.i(7, charSeq); - Assert.assertEquals("56abcehijkfgdtrue4.03.021", strBuilder.toString()); - strBuilder.i(8, charSeq, 0, 2); - Assert.assertEquals("56abcehiijjkfgdtrue4.03.021", strBuilder.toString()); - } - /** Ensure toString works as expected */ @Test public void testToString() { @@ -162,32 +126,20 @@ public void testToString() { Assert.assertEquals("ab", strBuilder.toString()); strBuilder.a("cd"); Assert.assertEquals("abcd", strBuilder.toString()); - strBuilder.a("ef"); - Assert.assertEquals("ab... and 4 skipped ...ef", strBuilder.toString()); - } - - /** Ensure all operations that could possibly reduce length are prohibited */ - @Test - public void testLengthReduceOperationsAreProhibited() { - SBLimitedLength strBuilder = getStrBuilder(2, 2); - assertThrows(UnsupportedOperationException.class, () -> strBuilder.d(0)); - assertThrows(UnsupportedOperationException.class, () -> strBuilder.d(0, 0)); - assertThrows(UnsupportedOperationException.class, () -> strBuilder.r(0, 0, "asd")); - assertThrows(UnsupportedOperationException.class, () -> strBuilder.setLength(0)); } /** * Assert {@link Runnable#run()} will throw specified exception - * @param expectedExceptionClass Expected exception class. + * @param expectedECls Expected exception class. * @param runnable Runnable. */ - private void assertThrows(Class expectedExceptionClass, Runnable runnable) { + private void assertThrows(Class expectedECls, Runnable runnable) { boolean eIsSpotted = false; try { runnable.run(); } catch (Throwable throwable) { - if (expectedExceptionClass.isAssignableFrom(throwable.getClass())) + if (expectedECls.isAssignableFrom(throwable.getClass())) eIsSpotted = true; } finally { From 8398284df8734dd9767962a4771f5deb284d1f1b Mon Sep 17 00:00:00 2001 From: baranoveg Date: Wed, 19 Aug 2026 12:08:51 +0300 Subject: [PATCH 05/13] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. --- .../util/tostring/CircularStringBuilder.java | 190 ------------------ .../CircularStringBuilderSelfTest.java | 108 ---------- 2 files changed, 298 deletions(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java index 51da8be7ae1be..1bd54ba879b2d 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java @@ -138,58 +138,6 @@ public CircularStringBuilder append(String str) { return this; } - /** - * Appends the specified character array (or a subrange) to this circular buffer. - * - * @param str the character array. - * @param off the offset within the array. - * @param len the number of characters to append. - * @return a reference to this object. - */ - public CircularStringBuilder append(char[] str, int off, int len) { - if (str == null) - return appendNull(); - - if (len >= value.length) { - // Data bigger or equal to value length - System.arraycopy(str, off + len - value.length, value, 0, value.length); - - skipped += len - value.length + finishAt + 1; - - finishAt = value.length - 1; - - full = true; - } - else { - // Data smaller than value length - if (value.length - finishAt - 1 < len) { - // Data doesn't fit into remaining part of value array - int firstPart = value.length - finishAt - 1; - - if (firstPart > 0) - System.arraycopy(str, off, value, finishAt + 1, firstPart); - - System.arraycopy(str, off + firstPart, value, 0, len - firstPart); - - skipped += full ? len : len - firstPart; - - finishAt = finishAt + len - value.length; - - full = true; - } - else { - // Whole data fits into remaining part of value array - System.arraycopy(str, off, value, finishAt + 1, len); - - skipped += full ? len : 0; - - finishAt += len; - } - } - - return this; - } - /** * Append StringBuffer * @@ -229,105 +177,6 @@ public int getSkipped() { return skipped; } - /** - * Performs an in-place rightward shift of elements within the circular buffer. - * This is used to create space for new data by moving a block of existing elements. - *

- * The shift is executed in reverse order (from the end of the block to the beginning) - * to prevent overwriting source elements before they are copied. - * - * @param shift The starting offset from the 'finishAt' index, defining the beginning - * of the block to be moved. - * @param moveSteps The number of elements to be shifted to the right. - */ - private void shiftRight(int shift, int moveSteps) { - for (int i = 0; i < moveSteps; i++) { - int pointer = (finishAt + shift - i) % value.length; - value[pointer] = value[(value.length + pointer - shift) % value.length]; - } - } - - /** - * Performs a leftward shift of elements in the circular buffer. - * Copies elements from a source position to a destination position, - * effectively overwriting a range of values. - *

- * @param shift The offset for the source element. - * @param shiftsCnt The count of elements to shift. - */ - private void shiftLeft(int shift, int shiftsCnt) { - for (int i = 0; i < shiftsCnt; i++) { - int pointer = (finishAt + 1 + i) % value.length; - value[pointer] = value[(pointer + shift) % value.length]; - } - } - - /** - * Inserts a substring from the source string into the buffer at the specified tail position. - *

- * The insertion is performed in reverse order (from the last character to the first) to - * prevent overwriting source data in the buffer before it is copied. This is a common - * technique for in-place buffer manipulation. - * - * @param src The source string to copy characters from. - * @param tailEndOffset The physical index in the buffer where the last character will be placed. - * @param insertCnt The number of characters from the source string to insert. - */ - private void insertStringTail(String src, int tailEndOffset, int insertCnt) { - for (int i = 0; i < insertCnt; i++) - value[(value.length + tailEndOffset - i - 1) % value.length] = src.charAt(src.length() - 1 - i); - } - - /** - * Inserts a string into the buffer at the specified logical offset. - * This method is optimized to minimize the number of elements moved by choosing - * to shift elements from the closest end (left or right) to the insertion point. - * - * @param offset The logical position (accounting for skipped characters) - * at which to insert. - * @param valToInsert The string to be inserted. - * @throws StringIndexOutOfBoundsException if the offset is invalid. - */ - public void insert(int offset, String valToInsert) { - int curLength = length(); - int offsetInsideBuf = offset - skipped; - if (offset < 0 || offsetInsideBuf > curLength) - throw new StringIndexOutOfBoundsException("Offset " + offset + " out of bounds for length " + curLength); - if (valToInsert == null) - valToInsert = "null"; - int insertLength = valToInsert.length(); - if (insertLength == 0) - return; - if (offsetInsideBuf == curLength) { - append(valToInsert); - return; - } - int spareSpace = value.length - curLength; - int insertCnt = Math.min(valToInsert.length(), spareSpace + offsetInsideBuf); - if (insertCnt <= 0) { - skipped += valToInsert.length(); - return; - } - int bufStartShiftedOffset = full ? (finishAt + 1) % value.length : 0; - int shiftedOffset = (bufStartShiftedOffset + offsetInsideBuf) % value.length; - int moveRightCnt = ((shiftedOffset <= finishAt ? 0 : curLength) + finishAt + 1) - shiftedOffset; - if (!full || offset - skipped > curLength / 2) { - shiftRight(insertCnt, moveRightCnt); - int charsToSkip = Math.max(0, insertCnt - spareSpace); - finishAt = (finishAt + insertCnt) % value.length; - shiftedOffset = (shiftedOffset + insertCnt) % value.length; - full = curLength + insertCnt >= value.length; - insertStringTail(valToInsert, shiftedOffset, insertCnt); - skipped += charsToSkip; - } - else { - int moveLeftCnt = (curLength - moveRightCnt - insertCnt); - shiftLeft(insertCnt, moveLeftCnt); - insertStringTail(valToInsert, shiftedOffset, insertCnt); - skipped += valToInsert.length(); - } - } - /** {@inheritDoc} */ @Override public String toString() { // Create a copy, don't share the array @@ -343,43 +192,4 @@ public void insert(int offset, String valToInsert) { else return new String(value, 0, finishAt + 1); } - - /** - * Returns a substring from the logical sequence of characters, accounting for - * the circular buffer structure and any skipped characters. - * - *

This method first validates the indices against the total logical length - * (skipped + visible characters). If the requested range is empty or fully within - * the skipped portion, an empty string is returned for efficiency. - * - *

It then calculates the physical indices in the internal array. If the - * substring wraps around the end of the circular buffer, it performs a two-part - * copy operation to assemble the result. - * - * @param beginIdx the beginning index, inclusive. - * @param endIdx the ending index, exclusive. - * @return a new String containing the specified subsequence. - * @throws StringIndexOutOfBoundsException if beginIdx or endIdx are negative, - * or if endIdx is greater than the total logical length. - * @throws IllegalArgumentException if beginIdx is greater than endIdx. - */ - public String substring(int beginIdx, int endIdx) { - if (beginIdx < 0 || endIdx < 0 || endIdx > skipped + length()) - throw new StringIndexOutOfBoundsException( - "Index out of bounds: beginIdx=" + beginIdx + ", endIdx=" + endIdx); - if (beginIdx > endIdx) - throw new IllegalArgumentException( - "Begin index cannot be greater than end index: beginIdx=" + beginIdx + ", endIdx=" + endIdx); - if (endIdx <= skipped || beginIdx == endIdx) return ""; - char resultArr[] = new char[Math.max(skipped, endIdx) - Math.max(skipped, beginIdx)]; - int effectiveBeginIdx = ((full ? (finishAt + 1) : 0) + Math.max(skipped, beginIdx) - skipped) % value.length; - int effectiveEndIdx = ((full ? (finishAt + 1) : 0) + Math.max(skipped, endIdx) - skipped) % value.length; - if (effectiveBeginIdx >= effectiveEndIdx) { - System.arraycopy(value, effectiveBeginIdx, resultArr, 0, length() - effectiveBeginIdx); - System.arraycopy(value, 0, resultArr, length() - effectiveBeginIdx, effectiveEndIdx); - } - else - System.arraycopy(value, effectiveBeginIdx, resultArr, 0, effectiveEndIdx - effectiveBeginIdx); - return new String(resultArr); - } } diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/CircularStringBuilderSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/CircularStringBuilderSelfTest.java index 3818686f9a3d3..f9bf453663793 100644 --- a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/CircularStringBuilderSelfTest.java +++ b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/CircularStringBuilderSelfTest.java @@ -69,112 +69,4 @@ private void testSB(int capacity, String pattern, int num, String expected) { assertEquals(expected, csb.toString()); } - - /** - * @throws Exception If failed. - */ - @Test - public void testCSBInsert() { - testSBInsert(5, "123456789", 4, "new", "56789"); - testSBInsert(5, "123456789", 5, "new", "w6789"); - testSBInsert(5, "123456789", 6, "new", "ew789"); - testSBInsert(5, "123456789", 7, "new", "new89"); - testSBInsert(5, "123456789", 8, "new", "8new9"); - testSBInsert(5, "123456789", 9, "new", "89new"); - testSBInsert(5, "1", 0, "new", "new1"); - testSBInsert(5, "12", 0, "new", "new12"); - testSBInsert(5, "123", 0, "new", "ew123"); - testSBInsert(5, "1234", 0, "new", "w1234"); - testSBInsert(2, "1", 0, "new", "w1"); - testSBInsert(2, "12", 0, "new", "12"); - testSBInsert(3, "12", 0, "new", "w12"); - testSBInsert(3, "12", 1, "new", "ew2"); - } - - /** - * Assertions to ensure {@link CircularStringBuilder#substring(int, int)} method works - */ - @Test - public void testSubstring() { - CircularStringBuilder circularStrBuilder = new CircularStringBuilder(5); - circularStrBuilder.append("abc"); - assertEquals("abc", circularStrBuilder.substring(0, 3)); - assertEquals("ab", circularStrBuilder.substring(0, 2)); - assertEquals("bc", circularStrBuilder.substring(1, 3)); - circularStrBuilder.append("de"); - assertEquals("abc", circularStrBuilder.substring(0, 3)); - assertEquals("ab", circularStrBuilder.substring(0, 2)); - assertEquals("bc", circularStrBuilder.substring(1, 3)); - assertEquals("abcde", circularStrBuilder.substring(0, 5)); - assertEquals("de", circularStrBuilder.substring(3, 5)); - assertEquals("abc", circularStrBuilder.substring(0, 3)); - circularStrBuilder.append("fg"); - assertEquals("cdefg", circularStrBuilder.substring(2, 7)); - assertEquals("cdef", circularStrBuilder.substring(2, 6)); - assertEquals("defg", circularStrBuilder.substring(3, 7)); - assertEquals("cdefg", circularStrBuilder.substring(0, 7)); - circularStrBuilder.append("hi"); - assertEquals("efg", circularStrBuilder.substring(0, 7)); - assertEquals("efghi", circularStrBuilder.substring(0, 9)); - circularStrBuilder.append("j"); - assertEquals("fghi", circularStrBuilder.substring(0, 9)); - assertEquals("fghij", circularStrBuilder.substring(0, 10)); - assertEquals("ghij", circularStrBuilder.substring(6, 10)); - assertEquals("", circularStrBuilder.substring(0, 5)); - assertEquals("f", circularStrBuilder.substring(0, 6)); - } - - /** - * Assertions to ensure {@link CircularStringBuilder#substring(int, int)} method works - */ - @Test - public void testSubstringWithCharSequenceAppend() { - CircularStringBuilder circularStrBuilder = new CircularStringBuilder(5); - circularStrBuilder.append("abc".toCharArray(), 0, 3); - assertEquals("abc", circularStrBuilder.substring(0, 3)); - assertEquals("ab", circularStrBuilder.substring(0, 2)); - assertEquals("bc", circularStrBuilder.substring(1, 3)); - circularStrBuilder.append("de".toCharArray(), 0, 2); - assertEquals("abc", circularStrBuilder.substring(0, 3)); - assertEquals("ab", circularStrBuilder.substring(0, 2)); - assertEquals("bc", circularStrBuilder.substring(1, 3)); - assertEquals("abcde", circularStrBuilder.substring(0, 5)); - assertEquals("de", circularStrBuilder.substring(3, 5)); - assertEquals("abc", circularStrBuilder.substring(0, 3)); - circularStrBuilder.append("fg".toCharArray(), 0, 2); - assertEquals("cdefg", circularStrBuilder.substring(2, 7)); - assertEquals("cdef", circularStrBuilder.substring(2, 6)); - assertEquals("defg", circularStrBuilder.substring(3, 7)); - assertEquals("cdefg", circularStrBuilder.substring(0, 7)); - circularStrBuilder.append("ashi".toCharArray(), 2, 2); - assertEquals("efg", circularStrBuilder.substring(0, 7)); - assertEquals("efghi", circularStrBuilder.substring(0, 9)); - circularStrBuilder.append("j".toCharArray(), 0, 1); - circularStrBuilder.append("j".toCharArray(), 1, 0); - assertEquals("fghi", circularStrBuilder.substring(0, 9)); - assertEquals("fghij", circularStrBuilder.substring(0, 10)); - assertEquals("ghij", circularStrBuilder.substring(6, 10)); - assertEquals("", circularStrBuilder.substring(0, 5)); - assertEquals("f", circularStrBuilder.substring(0, 6)); - } - - /** - * Test ring buffer method {@link CircularStringBuilder#insert(int, String)} - * @param capacity ring buffer capacity - * @param firstVal value to append to buffer before test - * @param offset insert offset argument - * @param insertSubstring insert substring argument - * @param expectedResult expected ring buffer state - * (to assert it equals to {@link CircularStringBuilder#toString()}) - */ - private void testSBInsert(int capacity, - String firstVal, - int offset, - String insertSubstring, - String expectedResult) { - CircularStringBuilder csb = new CircularStringBuilder(capacity); - csb.append(firstVal); - csb.insert(offset, insertSubstring); - assertEquals(expectedResult, csb.toString()); - } } From cd4e38e64ea51d63f2eca6adfb7eddcaa7ff8874 Mon Sep 17 00:00:00 2001 From: baranoveg Date: Wed, 19 Aug 2026 12:28:20 +0300 Subject: [PATCH 06/13] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. --- .../ignite/internal/util/tostring/SBLengthLimit.java | 10 ---------- .../ignite/internal/util/tostring/SBLimitedLength.java | 9 --------- 2 files changed, 19 deletions(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java index f7e320842dae4..c671f3f41b86c 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java @@ -80,20 +80,10 @@ CircularStringBuilder createTail() { return getTail(); } - /** */ - int getTailLengthLimit() { - return TAIL_LEN; - } - /** * @return {@code True} if reached limit. */ boolean overflowed(SBLimitedLength sb) { return sb.impl().length() > HEAD_LEN; } - - /** */ - int getHeadLengthLimit() { - return HEAD_LEN; - } } diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java index 06f8e2dd968a6..3871b202955bc 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java @@ -18,7 +18,6 @@ package org.apache.ignite.internal.util.tostring; import java.util.Arrays; - import org.apache.ignite.internal.util.GridStringBuilder; /** @@ -327,14 +326,6 @@ public boolean isOverflowed() { return lenLimit.overflowed(this); } - /** {@inheritDoc} */ - @Override public int length() { - int length = super.length(); - if (tail != null) - length += tail.getSkipped() + tail.length(); - return length; - } - /** * */ From b700b125f4db008a1632b3476d44b135197c892c Mon Sep 17 00:00:00 2001 From: baranoveg Date: Wed, 19 Aug 2026 12:35:49 +0300 Subject: [PATCH 07/13] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. --- .../internal/util/tostring/SBLengthLimit.java | 5 ---- .../util/tostring/SBLimitedLength.java | 2 +- .../tostring/SBLimitedLengthSelfTest.java | 27 ++++--------------- 3 files changed, 6 insertions(+), 28 deletions(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java index c671f3f41b86c..58a070eef3ac3 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java @@ -75,11 +75,6 @@ CircularStringBuilder getTail() { return new CircularStringBuilder(TAIL_LEN); } - /** */ - CircularStringBuilder createTail() { - return getTail(); - } - /** * @return {@code True} if reached limit. */ diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java index 3871b202955bc..8b8e3be92e30d 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java @@ -331,6 +331,6 @@ public boolean isOverflowed() { */ private void initTailIfAbsent() { if (tail == null) - tail = lenLimit.createTail(); + tail = lenLimit.getTail(); } } diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java index e9fa8b8d67328..9b49e9878d81e 100644 --- a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java +++ b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java @@ -30,7 +30,7 @@ public class SBLimitedLengthSelfTest extends GridCommonAbstractTest { /** Ensure all append operations are working fine */ @Test public void testAppend() { - SBLimitedLength strBuilder = getStrBuilder(5, 50); + SBLimitedLength strBuilder = getStrBuilder(5); strBuilder.a(1); Assert.assertEquals("1", strBuilder.toString()); strBuilder.a(2L); @@ -55,10 +55,6 @@ public void testAppend() { Assert.assertEquals("123.04.05true6789", strBuilder.toString()); strBuilder.a(charSeq, 0, 1); Assert.assertEquals("123.04.05true67899", strBuilder.toString()); - strBuilder.a(new char[]{'a'}); - Assert.assertEquals("123.04.05true67899a", strBuilder.toString()); - strBuilder.a(new char[]{'b', 'c', 'd'}, 0, 2); - Assert.assertEquals("123.04.05true67899abc", strBuilder.toString()); } /** */ @@ -86,13 +82,6 @@ public void testNPEProtectionWithNullTail() { // We use a custom SBLengthLimit to trigger the exact scenario sbLimitedLength.initLimit(new SBLengthLimit() { - @Override int getHeadLengthLimit() { - return 3; - } - - @Override int getTailLengthLimit() { - return 10; - } @Override boolean overflowed(SBLimitedLength sb) { return sb.impl().length() > 3; @@ -121,7 +110,7 @@ public void testNPEProtectionWithNullTail() { /** Ensure toString works as expected */ @Test public void testToString() { - SBLimitedLength strBuilder = getStrBuilder(2, 2); + SBLimitedLength strBuilder = getStrBuilder(2); strBuilder.a("ab"); Assert.assertEquals("ab", strBuilder.toString()); strBuilder.a("cd"); @@ -151,18 +140,12 @@ private void assertThrows(Class expectedECls, Runnable runn * Get {@link SBLimitedLength} instance with specific head and tail length * to simplify test cases * @param headLength Head length. - * @param tailLength Tail length. */ - private SBLimitedLength getStrBuilder(int headLength, int tailLength) { + private SBLimitedLength getStrBuilder(int headLength) { SBLimitedLength sbLimitedLength = new SBLimitedLength(0); sbLimitedLength.initLimit(new SBLengthLimit() { - - @Override int getHeadLengthLimit() { - return headLength; - } - - @Override int getTailLengthLimit() { - return tailLength; + @Override boolean overflowed(SBLimitedLength sb) { + return sb.impl().length() > headLength; } }); return sbLimitedLength; From c71d5d7a4a7e8940c5de03b7a51119c5b1f57d26 Mon Sep 17 00:00:00 2001 From: baranoveg Date: Wed, 19 Aug 2026 12:54:46 +0300 Subject: [PATCH 08/13] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. --- .../org/apache/ignite/testsuites/IgniteUtilSelfTestSuite.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteUtilSelfTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteUtilSelfTestSuite.java index 5b5d98fc87b1c..2a75e761ce044 100644 --- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteUtilSelfTestSuite.java +++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteUtilSelfTestSuite.java @@ -36,6 +36,7 @@ import org.apache.ignite.internal.util.distributed.DistributedProcessClientAwaitTest; import org.apache.ignite.internal.util.distributed.DistributedProcessCoordinatorLeftTest; import org.apache.ignite.internal.util.distributed.DistributedProcessErrorHandlingTest; +import org.apache.ignite.internal.util.distributed.DistributedProcessResultMarshallingTest; import org.apache.ignite.internal.util.nio.GridNioDelimitedBufferSelfTest; import org.apache.ignite.internal.util.nio.GridNioSelfTest; import org.apache.ignite.internal.util.nio.GridNioServerTest; @@ -149,6 +150,7 @@ DistributedProcessErrorHandlingTest.class, DistributedProcessCoordinatorLeftTest.class, DistributedProcessClientAwaitTest.class, + DistributedProcessResultMarshallingTest.class, BasicRateLimiterTest.class, From bcc863a52507b0e45155289e7bc63fb74bf56c25 Mon Sep 17 00:00:00 2001 From: baranoveg Date: Wed, 19 Aug 2026 13:00:18 +0300 Subject: [PATCH 09/13] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. --- .../tostring/SBLimitedLengthSelfTest.java | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java index 9b49e9878d81e..2a42f3b8fd454 100644 --- a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java +++ b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java @@ -117,25 +117,6 @@ public void testToString() { Assert.assertEquals("abcd", strBuilder.toString()); } - /** - * Assert {@link Runnable#run()} will throw specified exception - * @param expectedECls Expected exception class. - * @param runnable Runnable. - */ - private void assertThrows(Class expectedECls, Runnable runnable) { - boolean eIsSpotted = false; - try { - runnable.run(); - } - catch (Throwable throwable) { - if (expectedECls.isAssignableFrom(throwable.getClass())) - eIsSpotted = true; - } - finally { - Assert.assertTrue(eIsSpotted); - } - } - /** * Get {@link SBLimitedLength} instance with specific head and tail length * to simplify test cases From ad3875f3e9752c43580960e467b0c8057f5b3374 Mon Sep 17 00:00:00 2001 From: baranoveg Date: Thu, 20 Aug 2026 10:24:08 +0300 Subject: [PATCH 10/13] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. --- .../tostring/SBLimitedLengthSelfTest.java | 33 +++---------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java index 2a42f3b8fd454..c92532092a7aa 100644 --- a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java +++ b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java @@ -77,34 +77,11 @@ public void testDoesNotThrowNPEOnHeadOverflow() { */ @Test public void testNPEProtectionWithNullTail() { - // Create SBLimitedLength with very small head limit and force tail creation manually - SBLimitedLength sbLimitedLength = new SBLimitedLength(256); - - // We use a custom SBLengthLimit to trigger the exact scenario - sbLimitedLength.initLimit(new SBLengthLimit() { - - @Override boolean overflowed(SBLimitedLength sb) { - return sb.impl().length() > 3; - } - - @Override void onWrite(SBLimitedLength sb, int writtenLen) { - super.onWrite(sb, writtenLen); - } - }); - - // Append enough data to overflow head - sbLimitedLength.a("abcd"); - - // At this point, tail should have been created by onWrite - Assert.assertNotNull("Tail should not be null", sbLimitedLength.getTail()); - - // Now simulate handleRecursion scenario: insert followed by append - sbLimitedLength.i(1, "XY"); - sbLimitedLength.a("Z"); - - String result = sbLimitedLength.toString(); - Assert.assertTrue("Result should contain inserted data", result.contains("XY")); - Assert.assertTrue("Result should contain appended data", result.contains("Z")); + SBLimitedLength sb = new SBLimitedLength(256); + sb.initLimit(new SBLengthLimit()); + sb.a("a".repeat(8000)); + sb.i(0, "@0"); + sb.a("x"); } /** Ensure toString works as expected */ From 474147aa1d778ff5ba59b7cb29a133c1a1212e60 Mon Sep 17 00:00:00 2001 From: Egor Baranov <48474422+EgorBaranovEnjoysTyping@users.noreply.github.com> Date: Tue, 25 Aug 2026 18:18:19 +0300 Subject: [PATCH 11/13] Update modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java Co-authored-by: Dmitry Werner --- .../apache/ignite/internal/util/tostring/SBLimitedLength.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java index 8b8e3be92e30d..51c39e0fa2cba 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java @@ -326,9 +326,7 @@ public boolean isOverflowed() { return lenLimit.overflowed(this); } - /** - * - */ + /** */ private void initTailIfAbsent() { if (tail == null) tail = lenLimit.getTail(); From 0cc9bee26327e283464def57038b91dfbf6fdd6a Mon Sep 17 00:00:00 2001 From: baranoveg Date: Tue, 25 Aug 2026 18:22:33 +0300 Subject: [PATCH 12/13] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. --- .../tostring/SBLimitedLengthSelfTest.java | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java index c92532092a7aa..c32b5853ca279 100644 --- a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java +++ b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java @@ -19,7 +19,6 @@ import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; import org.apache.ignite.testframework.junits.common.GridCommonTest; -import org.junit.Assert; import org.junit.Test; /** @@ -30,31 +29,31 @@ public class SBLimitedLengthSelfTest extends GridCommonAbstractTest { /** Ensure all append operations are working fine */ @Test public void testAppend() { - SBLimitedLength strBuilder = getStrBuilder(5); + SBLimitedLength strBuilder = stringBuilder(5); strBuilder.a(1); - Assert.assertEquals("1", strBuilder.toString()); + assertEquals("1", strBuilder.toString()); strBuilder.a(2L); - Assert.assertEquals("12", strBuilder.toString()); + assertEquals("12", strBuilder.toString()); strBuilder.a(3f); - Assert.assertEquals("123.0", strBuilder.toString()); + assertEquals("123.0", strBuilder.toString()); strBuilder.a(4d); - Assert.assertEquals("123.04.0", strBuilder.toString()); + assertEquals("123.04.0", strBuilder.toString()); strBuilder.a('5'); - Assert.assertEquals("123.04.05", strBuilder.toString()); + assertEquals("123.04.05", strBuilder.toString()); strBuilder.a(true); - Assert.assertEquals("123.04.05true", strBuilder.toString()); + assertEquals("123.04.05true", strBuilder.toString()); Object obj = "6"; strBuilder.a(obj); - Assert.assertEquals("123.04.05true6", strBuilder.toString()); + assertEquals("123.04.05true6", strBuilder.toString()); strBuilder.a("7"); - Assert.assertEquals("123.04.05true67", strBuilder.toString()); + assertEquals("123.04.05true67", strBuilder.toString()); strBuilder.a(new StringBuilder().append("8")); - Assert.assertEquals("123.04.05true678", strBuilder.toString()); + assertEquals("123.04.05true678", strBuilder.toString()); CharSequence charSeq = "9"; strBuilder.a(charSeq); - Assert.assertEquals("123.04.05true6789", strBuilder.toString()); + assertEquals("123.04.05true6789", strBuilder.toString()); strBuilder.a(charSeq, 0, 1); - Assert.assertEquals("123.04.05true67899", strBuilder.toString()); + assertEquals("123.04.05true67899", strBuilder.toString()); } /** */ @@ -66,9 +65,9 @@ public void testDoesNotThrowNPEOnHeadOverflow() { sbLimitedLength.i(7000, "asd"); sbLimitedLength.a("a".repeat(10)); String result = sbLimitedLength.toString(); - Assert.assertNotNull(result); - Assert.assertFalse(result.isEmpty()); - Assert.assertTrue(result.contains("asd")); + assertNotNull(result); + assertFalse(result.isEmpty()); + assertTrue(result.contains("asd")); } /** @@ -87,11 +86,11 @@ public void testNPEProtectionWithNullTail() { /** Ensure toString works as expected */ @Test public void testToString() { - SBLimitedLength strBuilder = getStrBuilder(2); + SBLimitedLength strBuilder = stringBuilder(2); strBuilder.a("ab"); - Assert.assertEquals("ab", strBuilder.toString()); + assertEquals("ab", strBuilder.toString()); strBuilder.a("cd"); - Assert.assertEquals("abcd", strBuilder.toString()); + assertEquals("abcd", strBuilder.toString()); } /** @@ -99,7 +98,7 @@ public void testToString() { * to simplify test cases * @param headLength Head length. */ - private SBLimitedLength getStrBuilder(int headLength) { + private SBLimitedLength stringBuilder(int headLength) { SBLimitedLength sbLimitedLength = new SBLimitedLength(0); sbLimitedLength.initLimit(new SBLengthLimit() { @Override boolean overflowed(SBLimitedLength sb) { From 475433a1cb47775cb77e17f9d27f1331714d7ec6 Mon Sep 17 00:00:00 2001 From: baranoveg Date: Tue, 25 Aug 2026 18:40:37 +0300 Subject: [PATCH 13/13] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. --- .../internal/util/tostring/SBLimitedLengthSelfTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java index c32b5853ca279..7c5fd104996c2 100644 --- a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java +++ b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java @@ -21,9 +21,7 @@ import org.apache.ignite.testframework.junits.common.GridCommonTest; import org.junit.Test; -/** - * Test suite to ensure SBLimitedLength works by design - */ +/** Test suite to ensure SBLimitedLength works by design */ @GridCommonTest(group = "Utils") public class SBLimitedLengthSelfTest extends GridCommonAbstractTest { /** Ensure all append operations are working fine */ @@ -100,11 +98,13 @@ public void testToString() { */ private SBLimitedLength stringBuilder(int headLength) { SBLimitedLength sbLimitedLength = new SBLimitedLength(0); + sbLimitedLength.initLimit(new SBLengthLimit() { @Override boolean overflowed(SBLimitedLength sb) { return sb.impl().length() > headLength; } }); + return sbLimitedLength; } }