Skip to content

Commit 4f39761

Browse files
authored
Further reduce memory usage of JsonPointer (wrt #818) (#820)
1 parent 38d2454 commit 4f39761

File tree

2 files changed

+61
-26
lines changed

2 files changed

+61
-26
lines changed

src/main/java/com/fasterxml/jackson/core/JsonPointer.java

+46-24
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,17 @@ public class JsonPointer implements Serializable
6565
/**
6666
* We will retain representation of the pointer, as a String,
6767
* so that {@link #toString} should be as efficient as possible.
68+
*<p>
69+
* NOTE: starting with 2.14, there is no accompanying
70+
* {@link #_asStringOffset} that MUST be considered with this String;
6871
*/
6972
protected final String _asString;
7073

74+
/**
75+
* @since 2.14
76+
*/
77+
protected final int _asStringOffset;
78+
7179
protected final String _matchingPropertyName;
7280

7381
protected final int _matchingElementIndex;
@@ -88,21 +96,26 @@ protected JsonPointer() {
8896
_matchingPropertyName = null;
8997
_matchingElementIndex = -1;
9098
_asString = "";
99+
_asStringOffset = 0;
91100
}
92101

93102
// Constructor used for creating non-empty Segments
94-
protected JsonPointer(String fullString, String segment, JsonPointer next) {
103+
protected JsonPointer(String fullString, int fullStringOffset,
104+
String segment, JsonPointer next)
105+
{
95106
_asString = fullString;
107+
_asStringOffset = fullStringOffset;
96108
_nextSegment = next;
97109
// Ok; may always be a property
98110
_matchingPropertyName = segment;
99111
// but could be an index, if parsable
100112
_matchingElementIndex = _parseIndex(segment);
101113
}
102114

103-
// @since 2.5
104-
protected JsonPointer(String fullString, String segment, int matchIndex, JsonPointer next) {
115+
protected JsonPointer(String fullString, int fullStringOffset,
116+
String segment, int matchIndex, JsonPointer next) {
105117
_asString = fullString;
118+
_asStringOffset = fullStringOffset;
106119
_nextSegment = next;
107120
_matchingPropertyName = segment;
108121
_matchingElementIndex = matchIndex;
@@ -199,11 +212,11 @@ public static JsonPointer forPath(JsonStreamContext context,
199212
if (seg == null) { // is this legal?
200213
seg = "";
201214
}
202-
tail = new JsonPointer(_fullPath(tail, seg), seg, tail);
215+
tail = new JsonPointer(_fullPath(tail, seg), 0, seg, tail);
203216
} else if (context.inArray() || includeRoot) {
204217
int ix = context.getCurrentIndex();
205218
String ixStr = String.valueOf(ix);
206-
tail = new JsonPointer(_fullPath(tail, ixStr), ixStr, ix, tail);
219+
tail = new JsonPointer(_fullPath(tail, ixStr), 0, ixStr, ix, tail);
207220
}
208221
// NOTE: this effectively drops ROOT node(s); should have 1 such node,
209222
// as the last one, but we don't have to care (probably some paths have
@@ -517,7 +530,13 @@ public JsonPointer head() {
517530
/**********************************************************
518531
*/
519532

520-
@Override public String toString() { return _asString; }
533+
@Override public String toString() {
534+
if (_asStringOffset <= 0) {
535+
return _asString;
536+
}
537+
return _asString.substring(_asStringOffset);
538+
}
539+
521540
@Override public int hashCode() { return _asString.hashCode(); }
522541

523542
@Override public boolean equals(Object o) {
@@ -563,21 +582,21 @@ private final static int _parseIndex(String str) {
563582
return NumberInput.parseInt(str);
564583
}
565584

566-
protected static JsonPointer _parseTail(String fullPath)
585+
protected static JsonPointer _parseTail(final String fullPath)
567586
{
568587
PointerParent parent = null;
569588

570589
// first char is the contextual slash, skip
571590
int i = 1;
572-
int end = fullPath.length();
591+
final int end = fullPath.length();
592+
int startOffset = 0;
573593

574594
while (i < end) {
575595
char c = fullPath.charAt(i);
576596
if (c == '/') { // common case, got a segment
577-
parent = new PointerParent(parent, fullPath, fullPath.substring(1, i));
578-
fullPath = fullPath.substring(i);
579-
i = 1;
580-
end = fullPath.length();
597+
parent = new PointerParent(parent, startOffset, fullPath.substring(startOffset + 1, i));
598+
startOffset = i;
599+
++i;
581600
continue;
582601
}
583602
++i;
@@ -591,10 +610,9 @@ protected static JsonPointer _parseTail(String fullPath)
591610
if (i < 0) { // end!
592611
return _buildPath(fullPath, segment, parent);
593612
}
594-
parent = new PointerParent(parent, fullPath, segment);
595-
fullPath = fullPath.substring(i);
596-
i = 1;
597-
end = fullPath.length();
613+
parent = new PointerParent(parent, startOffset, segment);
614+
startOffset = i;
615+
++i;
598616
continue;
599617
}
600618
// otherwise, loop on
@@ -603,11 +621,11 @@ protected static JsonPointer _parseTail(String fullPath)
603621
return _buildPath(fullPath, fullPath.substring(1), parent);
604622
}
605623

606-
private static JsonPointer _buildPath(String fullPath, String segment,
624+
private static JsonPointer _buildPath(final String fullPath, String segment,
607625
PointerParent parent) {
608-
JsonPointer curr = new JsonPointer(fullPath, segment, EMPTY);
626+
JsonPointer curr = new JsonPointer(fullPath, 0, segment, EMPTY);
609627
for (; parent != null; parent = parent.parent) {
610-
curr = new JsonPointer(parent.fullPath, parent.segment, curr);
628+
curr = new JsonPointer(fullPath, parent.fullPathOffset, parent.segment, curr);
611629
}
612630
return curr;
613631
}
@@ -669,7 +687,9 @@ protected JsonPointer _constructHead()
669687
// and from that, length of suffix to drop
670688
int suffixLength = last._asString.length();
671689
JsonPointer next = _nextSegment;
672-
return new JsonPointer(_asString.substring(0, _asString.length() - suffixLength), _matchingPropertyName,
690+
// !!! TODO 07-Oct-2022, tatu: change to iterative, not recursive
691+
return new JsonPointer(_asString.substring(0, _asString.length() - suffixLength), 0,
692+
_matchingPropertyName,
673693
_matchingElementIndex, next._constructHead(suffixLength, last));
674694
}
675695

@@ -680,7 +700,9 @@ protected JsonPointer _constructHead(int suffixLength, JsonPointer last)
680700
}
681701
JsonPointer next = _nextSegment;
682702
String str = _asString;
683-
return new JsonPointer(str.substring(0, str.length() - suffixLength), _matchingPropertyName,
703+
// !!! TODO 07-Oct-2022, tatu: change to iterative, not recursive
704+
return new JsonPointer(str.substring(0, str.length() - suffixLength), 0,
705+
_matchingPropertyName,
684706
_matchingElementIndex, next._constructHead(suffixLength, last));
685707
}
686708

@@ -696,12 +718,12 @@ protected JsonPointer _constructHead(int suffixLength, JsonPointer last)
696718
*/
697719
private static class PointerParent {
698720
public final PointerParent parent;
699-
public final String fullPath;
721+
public final int fullPathOffset;
700722
public final String segment;
701723

702-
PointerParent(PointerParent pp, String fp, String sgm) {
724+
PointerParent(PointerParent pp, int fpo, String sgm) {
703725
parent = pp;
704-
fullPath = fp;
726+
fullPathOffset = fpo;
705727
segment = sgm;
706728
}
707729
}

src/test/java/com/fasterxml/jackson/core/fuzz/Fuzz51806JsonPointerParse818Test.java

+15-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
// (reported as [core#818]
88
public class Fuzz51806JsonPointerParse818Test extends BaseTest
99
{
10-
// Before fix, looks like this is enough to cause StackOverflowError
11-
private final static int TOO_DEEP_PATH = 10_000;
10+
// Before fix, StackOverflowError with 6_000 or so,
11+
// and OOME with 20_000.
12+
// After fixes will get progressively slower so limit size to
13+
// keep test runs from making suite too slow
14+
private final static int TOO_DEEP_PATH = 25_000;
1215

1316
// Verify that a very deep/long (by number of segments) JsonPointer
1417
// may still be parsed ok, for "simple" case (no quoted chars)
@@ -28,6 +31,16 @@ private void _testJsonPointer(String pathExpr)
2831
assertNotNull(p);
2932
// But also verify it didn't change
3033
assertEquals(pathExpr, p.toString());
34+
35+
// And then verify segment by segment, easiest way is to
36+
// check that tail segment is proper substring
37+
JsonPointer curr = p;
38+
39+
while ((curr = curr.tail()) != null) {
40+
String act = curr.toString();
41+
String exp = pathExpr.substring(pathExpr.length() - act.length());
42+
assertEquals(exp, act);
43+
}
3144
}
3245

3346
private String _generatePath(int depth, boolean escaped) {

0 commit comments

Comments
 (0)