mirror of
https://github.com/mikefarah/yq.git
synced 2026-07-05 20:15:36 +00:00
Merge master: use clampSliceIndex in sliceStringNode, resolve conflicts
Co-authored-by: mikefarah <1151925+mikefarah@users.noreply.github.com>
This commit is contained in:
commit
1afa3c73dc
@ -80,7 +80,7 @@ func recurseNodeObjectEqual(lhs *CandidateNode, rhs *CandidateNode) bool {
|
|||||||
key := lhs.Content[index]
|
key := lhs.Content[index]
|
||||||
value := lhs.Content[index+1]
|
value := lhs.Content[index+1]
|
||||||
|
|
||||||
indexInRHS := findInArray(rhs, key)
|
indexInRHS := findKeyInMap(rhs, key)
|
||||||
|
|
||||||
if indexInRHS == -1 || !recursiveNodeEqual(value, rhs.Content[indexInRHS+1]) {
|
if indexInRHS == -1 || !recursiveNodeEqual(value, rhs.Content[indexInRHS+1]) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -300,6 +300,24 @@ func TestRecurseNodeObjectEqual(t *testing.T) {
|
|||||||
test.AssertResult(t, true, recurseNodeObjectEqual(obj1, obj2))
|
test.AssertResult(t, true, recurseNodeObjectEqual(obj1, obj2))
|
||||||
test.AssertResult(t, false, recurseNodeObjectEqual(obj1, obj3))
|
test.AssertResult(t, false, recurseNodeObjectEqual(obj1, obj3))
|
||||||
test.AssertResult(t, false, recurseNodeObjectEqual(obj1, obj4))
|
test.AssertResult(t, false, recurseNodeObjectEqual(obj1, obj4))
|
||||||
|
|
||||||
|
// A null key must not match a null value in the other map.
|
||||||
|
// Regression test for https://issues.oss-fuzz.com/issues/383860504
|
||||||
|
nullKey := &CandidateNode{Kind: ScalarNode, Tag: "!!null"}
|
||||||
|
nullVal := &CandidateNode{Kind: ScalarNode, Tag: "!!null"}
|
||||||
|
intKey := createScalarNode(2, "2")
|
||||||
|
intKey.Tag = "!!int"
|
||||||
|
intVal := &CandidateNode{Kind: ScalarNode, Tag: "!!null"}
|
||||||
|
|
||||||
|
mapWithNullKey := &CandidateNode{
|
||||||
|
Kind: MappingNode,
|
||||||
|
Content: []*CandidateNode{nullKey, nullVal},
|
||||||
|
}
|
||||||
|
mapWithIntKey := &CandidateNode{
|
||||||
|
Kind: MappingNode,
|
||||||
|
Content: []*CandidateNode{intKey, intVal},
|
||||||
|
}
|
||||||
|
test.AssertResult(t, false, recurseNodeObjectEqual(mapWithNullKey, mapWithIntKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseInt(t *testing.T) {
|
func TestParseInt(t *testing.T) {
|
||||||
|
|||||||
@ -527,6 +527,18 @@ var addOperatorScenarios = []expressionScenario{
|
|||||||
expression: `.a += [2]`,
|
expression: `.a += [2]`,
|
||||||
expectedError: "!!seq () cannot be added to a !!str (a)",
|
expectedError: "!!seq () cannot be added to a !!str (a)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Regression test for https://issues.oss-fuzz.com/issues/383860504
|
||||||
|
// Adding a map to itself must not panic when sequence keys contain
|
||||||
|
// single-entry mappings with a null key in one and a non-null key
|
||||||
|
// in the other.
|
||||||
|
skipDoc: true,
|
||||||
|
document: "? [{~: ~}]\n: v1\n? [{2: ~}]\n: v2",
|
||||||
|
expression: `. += .`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!map)::? [{~: ~}]\n: v1\n? [{2: ~}]\n: v2\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddOperatorScenarios(t *testing.T) {
|
func TestAddOperatorScenarios(t *testing.T) {
|
||||||
|
|||||||
@ -46,9 +46,9 @@ func containsObject(lhs *CandidateNode, rhs *CandidateNode) (bool, error) {
|
|||||||
rhsKey := rhs.Content[index]
|
rhsKey := rhs.Content[index]
|
||||||
rhsValue := rhs.Content[index+1]
|
rhsValue := rhs.Content[index+1]
|
||||||
log.Debugf("Looking for %v in the lhs", rhsKey.Value)
|
log.Debugf("Looking for %v in the lhs", rhsKey.Value)
|
||||||
lhsKeyIndex := findInArray(lhs, rhsKey)
|
lhsKeyIndex := findKeyInMap(lhs, rhsKey)
|
||||||
log.Debugf("index is %v", lhsKeyIndex)
|
log.Debugf("index is %v", lhsKeyIndex)
|
||||||
if lhsKeyIndex < 0 || lhsKeyIndex%2 != 0 {
|
if lhsKeyIndex < 0 {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
lhsValue := lhs.Content[lhsKeyIndex+1]
|
lhsValue := lhs.Content[lhsKeyIndex+1]
|
||||||
|
|||||||
@ -65,6 +65,16 @@ var containsOperatorScenarios = []expressionScenario{
|
|||||||
"D0, P[], (!!bool)::false\n",
|
"D0, P[], (!!bool)::false\n",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Regression: findInArray could match a null key against a null
|
||||||
|
// value at an earlier odd index, producing a false negative.
|
||||||
|
skipDoc: true,
|
||||||
|
document: "? 1\n: ~\n? ~\n: x",
|
||||||
|
expression: `contains({~: "x"})`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!bool)::true\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
description: "String contains substring",
|
description: "String contains substring",
|
||||||
document: `"foobar"`,
|
document: `"foobar"`,
|
||||||
|
|||||||
@ -155,8 +155,10 @@ func repeatString(lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error
|
|||||||
return nil, err
|
return nil, err
|
||||||
} else if count < 0 {
|
} else if count < 0 {
|
||||||
return nil, fmt.Errorf("cannot repeat string by a negative number (%v)", count)
|
return nil, fmt.Errorf("cannot repeat string by a negative number (%v)", count)
|
||||||
} else if count > 10000000 {
|
}
|
||||||
return nil, fmt.Errorf("cannot repeat string by more than 100 million (%v)", count)
|
maxResultLen := 10 * 1024 * 1024 // 10 MiB
|
||||||
|
if count > 0 && len(stringNode.Value) > maxResultLen/count {
|
||||||
|
return nil, fmt.Errorf("result of repeating string (%v bytes) by %v would exceed %v bytes", len(stringNode.Value), count, maxResultLen)
|
||||||
}
|
}
|
||||||
target.Value = strings.Repeat(stringNode.Value, count)
|
target.Value = strings.Repeat(stringNode.Value, count)
|
||||||
|
|
||||||
|
|||||||
@ -237,12 +237,11 @@ var multiplyOperatorScenarios = []expressionScenario{
|
|||||||
expectedError: "cannot repeat string by a negative number (-4)",
|
expectedError: "cannot repeat string by a negative number (-4)",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "Multiply string X by more than 100 million",
|
description: "Multiply string by count that exceeds result size limit",
|
||||||
// very large string.repeats causes a panic
|
|
||||||
skipDoc: true,
|
skipDoc: true,
|
||||||
document: `n: 100000001`,
|
document: `n: 100000001`,
|
||||||
expression: `"banana" * .n`,
|
expression: `"banana" * .n`,
|
||||||
expectedError: "cannot repeat string by more than 100 million (100000001)",
|
expectedError: "result of repeating string (6 bytes) by 100000001 would exceed 10485760 bytes",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "Multiply int node X string",
|
description: "Multiply int node X string",
|
||||||
@ -693,6 +692,27 @@ var multiplyOperatorScenarios = []expressionScenario{
|
|||||||
"D0, P[], (!!null)::null\n",
|
"D0, P[], (!!null)::null\n",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Regression test for https://issues.oss-fuzz.com/issues/418818862
|
||||||
|
// Large repeat count with a long string must not panic.
|
||||||
|
skipDoc: true,
|
||||||
|
expression: `"abc" * 99999999`,
|
||||||
|
expectedError: "result of repeating string (3 bytes) by 99999999 would exceed 10485760 bytes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Regression test for https://issues.oss-fuzz.com/issues/383195001
|
||||||
|
// Product of string length * repeat count must be bounded.
|
||||||
|
skipDoc: true,
|
||||||
|
expression: `"x" * 99999999`,
|
||||||
|
expectedError: "result of repeating string (1 bytes) by 99999999 would exceed 10485760 bytes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// The size guard must not overflow: len * count can wrap to
|
||||||
|
// a negative or small value on 64-bit, bypassing the check.
|
||||||
|
skipDoc: true,
|
||||||
|
expression: `"ab" * 4611686018427387904`,
|
||||||
|
expectedError: "result of repeating string (2 bytes) by 4611686018427387904 would exceed 10485760 bytes",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultiplyOperatorScenarios(t *testing.T) {
|
func TestMultiplyOperatorScenarios(t *testing.T) {
|
||||||
|
|||||||
@ -16,34 +16,33 @@ func getSliceNumber(d *dataTreeNavigator, context Context, node *CandidateNode,
|
|||||||
return parseInt(result.MatchingNodes.Front().Value.(*CandidateNode).Value)
|
return parseInt(result.MatchingNodes.Front().Value.(*CandidateNode).Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clampSliceIndex resolves a possibly-negative slice index against
|
||||||
|
// length and clamps the result to [0, length].
|
||||||
|
func clampSliceIndex(index, length int) int {
|
||||||
|
if index < 0 {
|
||||||
|
index += length
|
||||||
|
}
|
||||||
|
if index < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if index > length {
|
||||||
|
return length
|
||||||
|
}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
func sliceStringNode(lhsNode *CandidateNode, firstNumber int, secondNumber int) *CandidateNode {
|
func sliceStringNode(lhsNode *CandidateNode, firstNumber int, secondNumber int) *CandidateNode {
|
||||||
runes := []rune(lhsNode.Value)
|
runes := []rune(lhsNode.Value)
|
||||||
length := len(runes)
|
length := len(runes)
|
||||||
|
|
||||||
relativeFirstNumber := firstNumber
|
relativeFirstNumber := clampSliceIndex(firstNumber, length)
|
||||||
if relativeFirstNumber < 0 {
|
relativeSecondNumber := clampSliceIndex(secondNumber, length)
|
||||||
relativeFirstNumber = length + firstNumber
|
|
||||||
}
|
|
||||||
if relativeFirstNumber < 0 {
|
|
||||||
relativeFirstNumber = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
relativeSecondNumber := secondNumber
|
|
||||||
if relativeSecondNumber < 0 {
|
|
||||||
relativeSecondNumber = length + secondNumber
|
|
||||||
} else if relativeSecondNumber > length {
|
|
||||||
relativeSecondNumber = length
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("sliceStringNode: slice from %v to %v", relativeFirstNumber, relativeSecondNumber)
|
|
||||||
|
|
||||||
if relativeFirstNumber > length {
|
|
||||||
relativeFirstNumber = length
|
|
||||||
}
|
|
||||||
if relativeSecondNumber < relativeFirstNumber {
|
if relativeSecondNumber < relativeFirstNumber {
|
||||||
relativeSecondNumber = relativeFirstNumber
|
relativeSecondNumber = relativeFirstNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debugf("sliceStringNode: slice from %v to %v", relativeFirstNumber, relativeSecondNumber)
|
||||||
|
|
||||||
slicedString := string(runes[relativeFirstNumber:relativeSecondNumber])
|
slicedString := string(runes[relativeFirstNumber:relativeSecondNumber])
|
||||||
replacement := lhsNode.CreateReplacement(ScalarNode, lhsNode.Tag, slicedString)
|
replacement := lhsNode.CreateReplacement(ScalarNode, lhsNode.Tag, slicedString)
|
||||||
replacement.Style = lhsNode.Style
|
replacement.Style = lhsNode.Style
|
||||||
@ -76,28 +75,8 @@ func sliceArrayOperator(d *dataTreeNavigator, context Context, expressionNode *E
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
relativeFirstNumber := firstNumber
|
relativeFirstNumber := clampSliceIndex(firstNumber, len(lhsNode.Content))
|
||||||
if relativeFirstNumber < 0 {
|
relativeSecondNumber := clampSliceIndex(secondNumber, len(lhsNode.Content))
|
||||||
relativeFirstNumber = len(lhsNode.Content) + firstNumber
|
|
||||||
}
|
|
||||||
if relativeFirstNumber < 0 {
|
|
||||||
relativeFirstNumber = 0
|
|
||||||
} else if relativeFirstNumber > len(lhsNode.Content) {
|
|
||||||
relativeFirstNumber = len(lhsNode.Content)
|
|
||||||
}
|
|
||||||
|
|
||||||
relativeSecondNumber := secondNumber
|
|
||||||
if relativeSecondNumber < 0 {
|
|
||||||
relativeSecondNumber = len(lhsNode.Content) + secondNumber
|
|
||||||
}
|
|
||||||
if relativeSecondNumber < 0 {
|
|
||||||
relativeSecondNumber = 0
|
|
||||||
} else if relativeSecondNumber > len(lhsNode.Content) {
|
|
||||||
relativeSecondNumber = len(lhsNode.Content)
|
|
||||||
}
|
|
||||||
if relativeSecondNumber < relativeFirstNumber {
|
|
||||||
relativeSecondNumber = relativeFirstNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("calculateIndicesToTraverse: slice from %v to %v", relativeFirstNumber, relativeSecondNumber)
|
log.Debugf("calculateIndicesToTraverse: slice from %v to %v", relativeFirstNumber, relativeSecondNumber)
|
||||||
|
|
||||||
|
|||||||
@ -99,17 +99,32 @@ var sliceArrayScenarios = []expressionScenario{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// Regression test for https://issues.oss-fuzz.com/issues/438776028
|
||||||
|
// Negative second index that underflows after adjustment must
|
||||||
|
// clamp to zero, yielding an empty sequence.
|
||||||
skipDoc: true,
|
skipDoc: true,
|
||||||
document: `[cat, dog, frog]`,
|
document: `[a, b, c]`,
|
||||||
expression: `.[-100:]`,
|
expression: `.[0:-99999]`,
|
||||||
expected: []string{
|
expected: []string{
|
||||||
"D0, P[], (!!seq)::- cat\n- dog\n- frog\n",
|
"D0, P[], (!!seq)::[]\n",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// First-index underflow: without clamping, the loop starts at a
|
||||||
|
// negative index and panics on Content access.
|
||||||
skipDoc: true,
|
skipDoc: true,
|
||||||
document: `[cat, dog, frog]`,
|
document: `[a, b, c]`,
|
||||||
expression: `.[:-100]`,
|
expression: `.[-99999:3]`,
|
||||||
|
expected: []string{
|
||||||
|
"D0, P[], (!!seq)::- a\n- b\n- c\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Both indices underflow: both clamp to zero, yielding an empty
|
||||||
|
// sequence.
|
||||||
|
skipDoc: true,
|
||||||
|
document: `[a, b, c]`,
|
||||||
|
expression: `.[-99999:-99998]`,
|
||||||
expected: []string{
|
expected: []string{
|
||||||
"D0, P[], (!!seq)::[]\n",
|
"D0, P[], (!!seq)::[]\n",
|
||||||
},
|
},
|
||||||
@ -184,10 +199,10 @@ var sliceArrayScenarios = []expressionScenario{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
skipDoc: true,
|
description: "Slicing strings - Unicode",
|
||||||
description: "Unicode string slicing",
|
subdescription: "Indices are rune-based, so multibyte characters are handled correctly",
|
||||||
document: `greeting: héllo`,
|
document: `greeting: héllo`,
|
||||||
expression: `.greeting[1:3]`,
|
expression: `.greeting[1:3]`,
|
||||||
expected: []string{
|
expected: []string{
|
||||||
"D0, P[greeting], (!!str)::él\n",
|
"D0, P[greeting], (!!str)::él\n",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -36,9 +36,33 @@ func traversePathOperator(_ *dataTreeNavigator, context Context, expressionNode
|
|||||||
return context.ChildContext(matches), nil
|
return context.ChildContext(matches), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveAliasChain follows an alias chain iteratively, returning the
|
||||||
|
// first non-alias node. Returns an error if a cycle is detected.
|
||||||
|
func resolveAliasChain(node *CandidateNode) (*CandidateNode, error) {
|
||||||
|
if node.Kind != AliasNode {
|
||||||
|
return node, nil
|
||||||
|
}
|
||||||
|
visited := map[*CandidateNode]bool{}
|
||||||
|
for node.Kind == AliasNode {
|
||||||
|
if visited[node] {
|
||||||
|
return nil, fmt.Errorf("alias cycle detected")
|
||||||
|
}
|
||||||
|
visited[node] = true
|
||||||
|
log.Debug("its an alias!")
|
||||||
|
node = node.Alias
|
||||||
|
}
|
||||||
|
return node, nil
|
||||||
|
}
|
||||||
|
|
||||||
func traverse(context Context, matchingNode *CandidateNode, operation *Operation) (*list.List, error) {
|
func traverse(context Context, matchingNode *CandidateNode, operation *Operation) (*list.List, error) {
|
||||||
log.Debugf("Traversing %v", NodeToString(matchingNode))
|
log.Debugf("Traversing %v", NodeToString(matchingNode))
|
||||||
|
|
||||||
|
var err error
|
||||||
|
matchingNode, err = resolveAliasChain(matchingNode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if matchingNode.Tag == "!!null" && operation.Value != "[]" && !context.DontAutoCreate {
|
if matchingNode.Tag == "!!null" && operation.Value != "[]" && !context.DontAutoCreate {
|
||||||
log.Debugf("Guessing kind")
|
log.Debugf("Guessing kind")
|
||||||
// we must have added this automatically, lets guess what it should be now
|
// we must have added this automatically, lets guess what it should be now
|
||||||
@ -62,10 +86,6 @@ func traverse(context Context, matchingNode *CandidateNode, operation *Operation
|
|||||||
log.Debugf("its a sequence of %v things!", len(matchingNode.Content))
|
log.Debugf("its a sequence of %v things!", len(matchingNode.Content))
|
||||||
return traverseArray(matchingNode, operation, operation.Preferences.(traversePreferences))
|
return traverseArray(matchingNode, operation, operation.Preferences.(traversePreferences))
|
||||||
|
|
||||||
case AliasNode:
|
|
||||||
log.Debug("its an alias!")
|
|
||||||
matchingNode = matchingNode.Alias
|
|
||||||
return traverse(context, matchingNode, operation)
|
|
||||||
default:
|
default:
|
||||||
return list.New(), nil
|
return list.New(), nil
|
||||||
}
|
}
|
||||||
@ -129,7 +149,13 @@ func traverseNodesWithArrayIndices(context Context, indicesToTraverse []*Candida
|
|||||||
return context.ChildContext(matchingNodeMap), nil
|
return context.ChildContext(matchingNodeMap), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func traverseArrayIndices(context Context, matchingNode *CandidateNode, indicesToTraverse []*CandidateNode, prefs traversePreferences) (*list.List, error) { // call this if doc / alias like the other traverse
|
func traverseArrayIndices(context Context, matchingNode *CandidateNode, indicesToTraverse []*CandidateNode, prefs traversePreferences) (*list.List, error) {
|
||||||
|
var err error
|
||||||
|
matchingNode, err = resolveAliasChain(matchingNode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if matchingNode.Tag == "!!null" {
|
if matchingNode.Tag == "!!null" {
|
||||||
log.Debugf("OperatorArrayTraverse got a null - turning it into an empty array")
|
log.Debugf("OperatorArrayTraverse got a null - turning it into an empty array")
|
||||||
// auto vivification
|
// auto vivification
|
||||||
@ -142,9 +168,6 @@ func traverseArrayIndices(context Context, matchingNode *CandidateNode, indicesT
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch matchingNode.Kind {
|
switch matchingNode.Kind {
|
||||||
case AliasNode:
|
|
||||||
matchingNode = matchingNode.Alias
|
|
||||||
return traverseArrayIndices(context, matchingNode, indicesToTraverse, prefs)
|
|
||||||
case SequenceNode:
|
case SequenceNode:
|
||||||
return traverseArrayWithIndices(matchingNode, indicesToTraverse, prefs)
|
return traverseArrayWithIndices(matchingNode, indicesToTraverse, prefs)
|
||||||
case MappingNode:
|
case MappingNode:
|
||||||
|
|||||||
@ -665,6 +665,16 @@ var traversePathOperatorScenarios = []expressionScenario{
|
|||||||
"D0, P[a], (!!null)::null\n",
|
"D0, P[a], (!!null)::null\n",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Regression test for https://issues.oss-fuzz.com/issues/390467412
|
||||||
|
// go-yaml accepts cross-document alias references (invalid per
|
||||||
|
// YAML spec). A nested assignment on such an alias can create a
|
||||||
|
// circular alias node, which must not cause a stack overflow.
|
||||||
|
skipDoc: true,
|
||||||
|
document: "&-- a\n---\n*--",
|
||||||
|
expression: ". = (.x = 1)",
|
||||||
|
expectedError: "alias cycle detected",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTraversePathOperatorScenarios(t *testing.T) {
|
func TestTraversePathOperatorScenarios(t *testing.T) {
|
||||||
@ -682,3 +692,58 @@ func TestTraversePathOperatorAlignedToSpecScenarios(t *testing.T) {
|
|||||||
appendOperatorDocumentScenario(t, "traverse-read", fixedTraversePathOperatorScenarios)
|
appendOperatorDocumentScenario(t, "traverse-read", fixedTraversePathOperatorScenarios)
|
||||||
ConfiguredYamlPreferences.FixMergeAnchorToSpec = false
|
ConfiguredYamlPreferences.FixMergeAnchorToSpec = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression test for https://issues.oss-fuzz.com/issues/390467412
|
||||||
|
// A circular alias (alias pointing back to itself) must not cause a
|
||||||
|
// stack overflow. resolveAliasChain should detect the cycle and return
|
||||||
|
// an error; both traverse() and traverseArrayIndices() use it.
|
||||||
|
func TestTraverseAliasCycle(t *testing.T) {
|
||||||
|
aliasNode := &CandidateNode{
|
||||||
|
Kind: AliasNode,
|
||||||
|
}
|
||||||
|
aliasNode.Alias = aliasNode // A -> A
|
||||||
|
|
||||||
|
op := &Operation{
|
||||||
|
OperationType: traversePathOpType,
|
||||||
|
Value: "key",
|
||||||
|
StringValue: "key",
|
||||||
|
Preferences: traversePreferences{},
|
||||||
|
}
|
||||||
|
_, err := traverse(Context{}, aliasNode, op)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for alias cycle, got nil")
|
||||||
|
}
|
||||||
|
if err.Error() != "alias cycle detected" {
|
||||||
|
t.Fatalf("expected 'alias cycle detected', got %q", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same cycle must be caught through the array traversal path.
|
||||||
|
_, err = traverseArrayIndices(Context{}, aliasNode, nil, traversePreferences{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for alias cycle via traverseArrayIndices, got nil")
|
||||||
|
}
|
||||||
|
if err.Error() != "alias cycle detected" {
|
||||||
|
t.Fatalf("expected 'alias cycle detected', got %q", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTraverseAliasCycleChain(t *testing.T) {
|
||||||
|
nodeA := &CandidateNode{Kind: AliasNode}
|
||||||
|
nodeB := &CandidateNode{Kind: AliasNode}
|
||||||
|
nodeA.Alias = nodeB
|
||||||
|
nodeB.Alias = nodeA // A -> B -> A
|
||||||
|
|
||||||
|
op := &Operation{
|
||||||
|
OperationType: traversePathOpType,
|
||||||
|
Value: "key",
|
||||||
|
StringValue: "key",
|
||||||
|
Preferences: traversePreferences{},
|
||||||
|
}
|
||||||
|
_, err := traverse(Context{}, nodeA, op)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for alias cycle chain, got nil")
|
||||||
|
}
|
||||||
|
if err.Error() != "alias cycle detected" {
|
||||||
|
t.Fatalf("expected 'alias cycle detected', got %q", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user