diff --git a/src/main/java/com/fasterxml/jackson/core/JsonPointer.java b/src/main/java/com/fasterxml/jackson/core/JsonPointer.java index 0f3a8acf0c..476ae8a135 100644 --- a/src/main/java/com/fasterxml/jackson/core/JsonPointer.java +++ b/src/main/java/com/fasterxml/jackson/core/JsonPointer.java @@ -65,9 +65,17 @@ public class JsonPointer implements Serializable /** * We will retain representation of the pointer, as a String, * so that {@link #toString} should be as efficient as possible. + *
+ * NOTE: starting with 2.14, there is no accompanying + * {@link #_asStringOffset} that MUST be considered with this String; */ protected final String _asString; + /** + * @since 2.14 + */ + protected final int _asStringOffset; + protected final String _matchingPropertyName; protected final int _matchingElementIndex; @@ -88,11 +96,15 @@ protected JsonPointer() { _matchingPropertyName = null; _matchingElementIndex = -1; _asString = ""; + _asStringOffset = 0; } // Constructor used for creating non-empty Segments - protected JsonPointer(String fullString, String segment, JsonPointer next) { + protected JsonPointer(String fullString, int fullStringOffset, + String segment, JsonPointer next) + { _asString = fullString; + _asStringOffset = fullStringOffset; _nextSegment = next; // Ok; may always be a property _matchingPropertyName = segment; @@ -100,9 +112,10 @@ protected JsonPointer(String fullString, String segment, JsonPointer next) { _matchingElementIndex = _parseIndex(segment); } - // @since 2.5 - protected JsonPointer(String fullString, String segment, int matchIndex, JsonPointer next) { + protected JsonPointer(String fullString, int fullStringOffset, + String segment, int matchIndex, JsonPointer next) { _asString = fullString; + _asStringOffset = fullStringOffset; _nextSegment = next; _matchingPropertyName = segment; _matchingElementIndex = matchIndex; @@ -199,11 +212,11 @@ public static JsonPointer forPath(JsonStreamContext context, if (seg == null) { // is this legal? seg = ""; } - tail = new JsonPointer(_fullPath(tail, seg), seg, tail); + tail = new JsonPointer(_fullPath(tail, seg), 0, seg, tail); } else if (context.inArray() || includeRoot) { int ix = context.getCurrentIndex(); String ixStr = String.valueOf(ix); - tail = new JsonPointer(_fullPath(tail, ixStr), ixStr, ix, tail); + tail = new JsonPointer(_fullPath(tail, ixStr), 0, ixStr, ix, tail); } // NOTE: this effectively drops ROOT node(s); should have 1 such node, // as the last one, but we don't have to care (probably some paths have @@ -517,7 +530,13 @@ public JsonPointer head() { /********************************************************** */ - @Override public String toString() { return _asString; } + @Override public String toString() { + if (_asStringOffset <= 0) { + return _asString; + } + return _asString.substring(_asStringOffset); + } + @Override public int hashCode() { return _asString.hashCode(); } @Override public boolean equals(Object o) { @@ -563,21 +582,21 @@ private final static int _parseIndex(String str) { return NumberInput.parseInt(str); } - protected static JsonPointer _parseTail(String fullPath) + protected static JsonPointer _parseTail(final String fullPath) { PointerParent parent = null; // first char is the contextual slash, skip int i = 1; - int end = fullPath.length(); + final int end = fullPath.length(); + int startOffset = 0; while (i < end) { char c = fullPath.charAt(i); if (c == '/') { // common case, got a segment - parent = new PointerParent(parent, fullPath, fullPath.substring(1, i)); - fullPath = fullPath.substring(i); - i = 1; - end = fullPath.length(); + parent = new PointerParent(parent, startOffset, fullPath.substring(startOffset + 1, i)); + startOffset = i; + ++i; continue; } ++i; @@ -591,10 +610,9 @@ protected static JsonPointer _parseTail(String fullPath) if (i < 0) { // end! return _buildPath(fullPath, segment, parent); } - parent = new PointerParent(parent, fullPath, segment); - fullPath = fullPath.substring(i); - i = 1; - end = fullPath.length(); + parent = new PointerParent(parent, startOffset, segment); + startOffset = i; + ++i; continue; } // otherwise, loop on @@ -603,11 +621,11 @@ protected static JsonPointer _parseTail(String fullPath) return _buildPath(fullPath, fullPath.substring(1), parent); } - private static JsonPointer _buildPath(String fullPath, String segment, + private static JsonPointer _buildPath(final String fullPath, String segment, PointerParent parent) { - JsonPointer curr = new JsonPointer(fullPath, segment, EMPTY); + JsonPointer curr = new JsonPointer(fullPath, 0, segment, EMPTY); for (; parent != null; parent = parent.parent) { - curr = new JsonPointer(parent.fullPath, parent.segment, curr); + curr = new JsonPointer(fullPath, parent.fullPathOffset, parent.segment, curr); } return curr; } @@ -669,7 +687,9 @@ protected JsonPointer _constructHead() // and from that, length of suffix to drop int suffixLength = last._asString.length(); JsonPointer next = _nextSegment; - return new JsonPointer(_asString.substring(0, _asString.length() - suffixLength), _matchingPropertyName, + // !!! TODO 07-Oct-2022, tatu: change to iterative, not recursive + return new JsonPointer(_asString.substring(0, _asString.length() - suffixLength), 0, + _matchingPropertyName, _matchingElementIndex, next._constructHead(suffixLength, last)); } @@ -680,7 +700,9 @@ protected JsonPointer _constructHead(int suffixLength, JsonPointer last) } JsonPointer next = _nextSegment; String str = _asString; - return new JsonPointer(str.substring(0, str.length() - suffixLength), _matchingPropertyName, + // !!! TODO 07-Oct-2022, tatu: change to iterative, not recursive + return new JsonPointer(str.substring(0, str.length() - suffixLength), 0, + _matchingPropertyName, _matchingElementIndex, next._constructHead(suffixLength, last)); } @@ -696,12 +718,12 @@ protected JsonPointer _constructHead(int suffixLength, JsonPointer last) */ private static class PointerParent { public final PointerParent parent; - public final String fullPath; + public final int fullPathOffset; public final String segment; - PointerParent(PointerParent pp, String fp, String sgm) { + PointerParent(PointerParent pp, int fpo, String sgm) { parent = pp; - fullPath = fp; + fullPathOffset = fpo; segment = sgm; } } diff --git a/src/test/java/com/fasterxml/jackson/core/fuzz/Fuzz51806JsonPointerParse818Test.java b/src/test/java/com/fasterxml/jackson/core/fuzz/Fuzz51806JsonPointerParse818Test.java index 81981a2509..e6a718f66c 100644 --- a/src/test/java/com/fasterxml/jackson/core/fuzz/Fuzz51806JsonPointerParse818Test.java +++ b/src/test/java/com/fasterxml/jackson/core/fuzz/Fuzz51806JsonPointerParse818Test.java @@ -7,8 +7,11 @@ // (reported as [core#818] public class Fuzz51806JsonPointerParse818Test extends BaseTest { - // Before fix, looks like this is enough to cause StackOverflowError - private final static int TOO_DEEP_PATH = 10_000; + // Before fix, StackOverflowError with 6_000 or so, + // and OOME with 20_000. + // After fixes will get progressively slower so limit size to + // keep test runs from making suite too slow + private final static int TOO_DEEP_PATH = 25_000; // Verify that a very deep/long (by number of segments) JsonPointer // may still be parsed ok, for "simple" case (no quoted chars) @@ -28,6 +31,16 @@ private void _testJsonPointer(String pathExpr) assertNotNull(p); // But also verify it didn't change assertEquals(pathExpr, p.toString()); + + // And then verify segment by segment, easiest way is to + // check that tail segment is proper substring + JsonPointer curr = p; + + while ((curr = curr.tail()) != null) { + String act = curr.toString(); + String exp = pathExpr.substring(pathExpr.length() - act.length()); + assertEquals(exp, act); + } } private String _generatePath(int depth, boolean escaped) {