Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/utils/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ func LoadConfig(fileName string) error {
config.NoColor, _ = strconv.ParseBool(value)
case "color":
config.Color, _ = strconv.ParseBool(value)
default:
// Ignore unknown config options for forward compatibility
}
}

Expand Down
23 changes: 22 additions & 1 deletion internal/utils/jsonutil.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package utils

import (
"fmt"
"strings"

"github.com/antchfx/xmlquery"
Expand Down Expand Up @@ -39,6 +40,11 @@ func NodeToJSON(node *xmlquery.Node, depth int) interface{} {
if text != "" {
textParts = append(textParts, text)
}
case xmlquery.CommentNode, xmlquery.DeclarationNode, xmlquery.ProcessingInstruction, xmlquery.NotationNode:
// Skip these in JSON output
default:
// Should be impossible: all valid child node types handled above
panic(fmt.Sprintf("unknown NodeType as child of DocumentNode: %v", child.Type))
}
}

Expand All @@ -53,8 +59,13 @@ func NodeToJSON(node *xmlquery.Node, depth int) interface{} {
case xmlquery.TextNode, xmlquery.CharDataNode:
return strings.TrimSpace(node.Data)

default:
case xmlquery.CommentNode:
// Comments passed as root, return empty
return nil

default:
// Should be impossible: DocumentNode, ElementNode, TextNode, CharDataNode, CommentNode are the only valid root nodes
panic(fmt.Sprintf("unknown NodeType passed to NodeToJSON: %v", node.Type))
}
}

Expand All @@ -79,6 +90,11 @@ func nodeToJSONInternal(node *xmlquery.Node, depth int) interface{} {
case xmlquery.ElementNode:
childResult := nodeToJSONInternal(child, depth-1)
addToResult(result, child.Data, childResult)
case xmlquery.CommentNode, xmlquery.ProcessingInstruction:
// Skip these in JSON output
default:
// Should be impossible: all valid element child types handled above
panic(fmt.Sprintf("unknown NodeType as child of ElementNode: %v", child.Type))
}
}

Expand All @@ -103,6 +119,11 @@ func getTextContent(node *xmlquery.Node) string {
}
case xmlquery.ElementNode:
parts = append(parts, getTextContent(child))
case xmlquery.CommentNode, xmlquery.ProcessingInstruction:
// Skip these when extracting text
default:
// Should be impossible: all valid element child types handled above
panic(fmt.Sprintf("unknown NodeType in getTextContent: %v", child.Type))
}
}
return strings.Join(parts, "\n")
Expand Down
115 changes: 115 additions & 0 deletions internal/utils/jsonutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,118 @@ func TestXmlToJSON(t *testing.T) {
assert.Equal(t, expectedJson, output.String())
}
}

func TestExhaustiveNodeTypeHandling(t *testing.T) {
// Test that all xmlquery node types are handled without panicking
// This verifies our exhaustive switch statements work correctly

xmlInput := `<?xml version="1.0"?>
<!DOCTYPE root>
<!-- This is a comment -->
<root>
<element>text content</element>
<cdata><![CDATA[raw & unescaped < > content]]></cdata>
<!-- another comment inside -->
<?processing-instruction data?>
<mixed>text<child>more</child>tail</mixed>
</root>`

node, err := xmlquery.Parse(strings.NewReader(xmlInput))
assert.NoError(t, err)

// Should not panic - this exercises all the node types
result := NodeToJSON(node, -1)
assert.NotNil(t, result)

// Verify the result is a map
resultMap, ok := result.(map[string]interface{})
assert.True(t, ok)

// Verify root element exists
root, ok := resultMap["root"]
assert.True(t, ok)

rootMap, ok := root.(map[string]interface{})
assert.True(t, ok)

// Verify CDATA is preserved as text
cdataElem, ok := rootMap["cdata"]
assert.True(t, ok)
assert.Contains(t, cdataElem, "raw & unescaped")
}

func TestUnknownNodeTypePanics(t *testing.T) {
// Test that unknown node types trigger defensive panics
// This ensures we catch issues if xmlquery adds new node types

t.Run("unknown NodeType passed to NodeToJSON panics", func(t *testing.T) {
// Create a node with an invalid type
invalidNode := &xmlquery.Node{
Type: xmlquery.NodeType(255), // Invalid type
Data: "test",
}

assert.Panics(t, func() {
NodeToJSON(invalidNode, -1)
}, "NodeToJSON should panic on unknown node type")
})

t.Run("unknown NodeType as child of DocumentNode panics", func(t *testing.T) {
// Create a document with an invalid child
doc := &xmlquery.Node{
Type: xmlquery.DocumentNode,
}
invalidChild := &xmlquery.Node{
Type: xmlquery.NodeType(255), // Invalid type
Data: "test",
}
doc.FirstChild = invalidChild
invalidChild.Parent = doc

assert.Panics(t, func() {
NodeToJSON(doc, -1)
}, "NodeToJSON should panic on unknown child type under DocumentNode")
})

t.Run("unknown NodeType as child of ElementNode panics", func(t *testing.T) {
// Create a document with an element that has an invalid child
doc := &xmlquery.Node{
Type: xmlquery.DocumentNode,
}
elem := &xmlquery.Node{
Type: xmlquery.ElementNode,
Data: "root",
Parent: doc,
}
invalidChild := &xmlquery.Node{
Type: xmlquery.NodeType(255), // Invalid type
Data: "test",
Parent: elem,
}
doc.FirstChild = elem
elem.FirstChild = invalidChild

assert.Panics(t, func() {
NodeToJSON(doc, -1)
}, "NodeToJSON should panic on unknown child type under ElementNode")
})

t.Run("unknown NodeType in getTextContent panics", func(t *testing.T) {
// getTextContent is called when extracting text from elements
// Create an element with mixed content including invalid node
elem := &xmlquery.Node{
Type: xmlquery.ElementNode,
Data: "test",
}
invalidChild := &xmlquery.Node{
Type: xmlquery.NodeType(255), // Invalid type
Data: "test",
Parent: elem,
}
elem.FirstChild = invalidChild

assert.Panics(t, func() {
getTextContent(elem)
}, "getTextContent should panic on unknown node type")
})
}
13 changes: 13 additions & 0 deletions internal/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ func FormatXml(reader io.Reader, writer io.Writer, indent string, colors int) er
_, _ = fmt.Fprint(writer, tagColor("<!"), string(typedToken), tagColor(">"))
_, _ = fmt.Fprint(writer, newline, strings.Repeat(indent, level))
default:
// Should be impossible: all xml.Token types handled above
panic(fmt.Sprintf("unknown xml.Token type: %T", token))
}
}

Expand Down Expand Up @@ -403,6 +405,9 @@ func FormatHtml(reader io.Reader, writer io.Writer, indent string, colors int) e
if level == 0 {
_, _ = fmt.Fprint(writer, newline)
}
default:
// Should be impossible: all html.TokenType values handled above
panic(fmt.Sprintf("unknown html.TokenType: %v", token))
}
}

Expand Down Expand Up @@ -469,6 +474,9 @@ func FormatJson(reader io.Reader, writer io.Writer, indent string, colors int) e
level--
}
_, _ = fmt.Fprint(writer, newline, strings.Repeat(indent, level), tagColor("]"))
default:
// Should be impossible: json.Delim can only be '{', '}', '[', ']'
panic(fmt.Sprintf("unknown json.Delim: %v", tokenType))
}
case string:
escapedToken := strconv.Quote(token.(string))
Expand All @@ -485,6 +493,9 @@ func FormatJson(reader io.Reader, writer io.Writer, indent string, colors int) e
_, _ = fmt.Fprintf(writer, "%s%v", prefix, valueColor(token))
case nil:
_, _ = fmt.Fprintf(writer, "%s%s", prefix, valueColor("null"))
default:
// Should be impossible: all json.Token types handled above
panic(fmt.Sprintf("unknown json.Token type: %T", token))
}

switch tokenState {
Expand All @@ -494,6 +505,8 @@ func FormatJson(reader io.Reader, writer io.Writer, indent string, colors int) e
suffix = "," + newline + strings.Repeat(indent, level)
case jsonTokenArrayComma:
suffix = "," + newline + strings.Repeat(indent, level)
default:
// Other token states don't affect suffix formatting
}

prefix = suffix
Expand Down