Skip to content

Commit

Permalink
Merge pull request #153 from kubescape/dynamic
Browse files Browse the repository at this point in the history
use single char for dynamic, add comparison func
  • Loading branch information
matthyx authored Sep 19, 2024
2 parents 83b5d7c + 1ec96ea commit c453c91
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 46 deletions.
74 changes: 51 additions & 23 deletions pkg/registry/file/dynamicpathdetector/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,42 +29,39 @@ func (ua *PathAnalyzer) AnalyzePath(p, identifier string) (string, error) {
func (ua *PathAnalyzer) processSegments(node *SegmentNode, p string) string {
var result strings.Builder
currentNode := node
start := 0
for i := range p {
if p[i] == '/' {
segment := p[start:i]
currentNode = ua.processSegment(currentNode, segment)
ua.updateNodeStats(currentNode)
result.WriteString(currentNode.SegmentName)
result.WriteByte('/')
start = i + 1
i := 0
for {
start := i
for i < len(p) && p[i] != '/' {
i++
}
}
// Process the last segment
if start < len(p) {
segment := p[start:]
segment := p[start:i]
currentNode = ua.processSegment(currentNode, segment)
ua.updateNodeStats(currentNode)
result.WriteString(currentNode.SegmentName)
i++
if len(p) < i {
break
}
result.WriteByte('/')
}
return result.String()
}

func (ua *PathAnalyzer) processSegment(node *SegmentNode, segment string) *SegmentNode {
if segment == DynamicIdentifier {
return ua.handleDynamicSegment(node)
} else if child, exists := node.Children[segment]; exists || node.IsNextDynamic() {
return ua.handleExistingSegment(node, child, exists)
} else {
return ua.handleNewSegment(node, segment)
}
}

func (ua *PathAnalyzer) handleExistingSegment(node *SegmentNode, child *SegmentNode, exists bool) *SegmentNode {
if exists {
} else if node.IsNextDynamic() {
if len(node.Children) > 1 {
temp := node.Children[DynamicIdentifier]
node.Children = map[string]*SegmentNode{}
node.Children[DynamicIdentifier] = temp
}
return node.Children[DynamicIdentifier]
} else if child, exists := node.Children[segment]; exists {
return child
} else {
return node.Children[DynamicIdentifier]
return ua.handleNewSegment(node, segment)
}
}

Expand Down Expand Up @@ -136,3 +133,34 @@ func shallowChildrenCopy(src, dst *SegmentNode) {
}
}
}

func CompareDynamic(dynamicPath, regularPath string) bool {
dynamicIndex, regularIndex := 0, 0
dynamicLen, regularLen := len(dynamicPath), len(regularPath)

for dynamicIndex < dynamicLen && regularIndex < regularLen {
// Find the next segment in dynamicPath
dynamicSegmentStart := dynamicIndex
for dynamicIndex < dynamicLen && dynamicPath[dynamicIndex] != '/' {
dynamicIndex++
}
dynamicSegment := dynamicPath[dynamicSegmentStart:dynamicIndex]

// Find the next segment in regularPath
regularSegmentStart := regularIndex
for regularIndex < regularLen && regularPath[regularIndex] != '/' {
regularIndex++
}
regularSegment := regularPath[regularSegmentStart:regularIndex]

if dynamicSegment != DynamicIdentifier && dynamicSegment != regularSegment {
return false
}

// Move to the next segment
dynamicIndex++
regularIndex++
}

return dynamicIndex > dynamicLen && regularIndex > regularLen
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func TestAnalyzeEndpoints(t *testing.T) {
name: "Test with multiple endpoints",
input: []types.HTTPEndpoint{
{
Endpoint: ":80/users/<dynamic>",
Endpoint: ":80/users/\u22ef",
Methods: []string{"GET"},
},
{
Expand All @@ -48,7 +48,7 @@ func TestAnalyzeEndpoints(t *testing.T) {
},
expected: []types.HTTPEndpoint{
{
Endpoint: ":80/users/<dynamic>",
Endpoint: ":80/users/\u22ef",
Methods: []string{"GET", "POST"},
},
},
Expand All @@ -57,17 +57,17 @@ func TestAnalyzeEndpoints(t *testing.T) {
name: "Test with dynamic segments",
input: []types.HTTPEndpoint{
{
Endpoint: ":80/users/123/posts/<dynamic>",
Endpoint: ":80/users/123/posts/\u22ef",
Methods: []string{"GET"},
},
{
Endpoint: ":80/users/<dynamic>/posts/101",
Endpoint: ":80/users/\u22ef/posts/101",
Methods: []string{"POST"},
},
},
expected: []types.HTTPEndpoint{
{
Endpoint: ":80/users/<dynamic>/posts/<dynamic>",
Endpoint: ":80/users/\u22ef/posts/\u22ef",
Methods: []string{"GET", "POST"},
},
},
Expand Down Expand Up @@ -111,19 +111,19 @@ func TestAnalyzeEndpoints(t *testing.T) {
name: "Test with dynamic segments and different headers",
input: []types.HTTPEndpoint{
{
Endpoint: ":80/x/123/posts/<dynamic>",
Endpoint: ":80/x/123/posts/\u22ef",
Methods: []string{"GET"},
Headers: json.RawMessage(`{"Content-Type": ["application/json"], "X-API-Key": ["key1"]}`),
},
{
Endpoint: ":80/x/<dynamic>/posts/101",
Endpoint: ":80/x/\u22ef/posts/101",
Methods: []string{"POST"},
Headers: json.RawMessage(`{"Content-Type": ["application/xml"], "Authorization": ["Bearer token"]}`),
},
},
expected: []types.HTTPEndpoint{
{
Endpoint: ":80/x/<dynamic>/posts/<dynamic>",
Endpoint: ":80/x/\u22ef/posts/\u22ef",
Methods: []string{"GET", "POST"},
Headers: json.RawMessage(`{"Authorization":["Bearer token"],"Content-Type":["<<UNORDERED>>","application/json","application/xml"],"X-API-Key":["key1"]}`),
},
Expand Down Expand Up @@ -158,7 +158,7 @@ func TestAnalyzeEndpointsWithThreshold(t *testing.T) {

expected := []types.HTTPEndpoint{
{
Endpoint: ":80/users/<dynamic>",
Endpoint: ":80/users/\u22ef",
Methods: []string{"GET"},
},
}
Expand Down Expand Up @@ -197,7 +197,7 @@ func TestAnalyzeEndpointsWithExactThreshold(t *testing.T) {
// Check that all endpoints are now merged into one dynamic endpoint
expected := []types.HTTPEndpoint{
{
Endpoint: ":80/users/<dynamic>",
Endpoint: ":80/users/\u22ef",
Methods: []string{"GET"},
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestAnalyzeOpensWithThreshold(t *testing.T) {

expected := []types.OpenCalls{
{
Path: "/home/<dynamic>/file.txt",
Path: "/home/\u22ef/file.txt",
Flags: []string{},
},
}
Expand All @@ -46,7 +46,7 @@ func TestAnalyzeOpensWithFlagMergingAndThreshold(t *testing.T) {
{Path: "/home/user4/file.txt", Flags: []string{"READ", "WRITE"}},
},
expected: []types.OpenCalls{
{Path: "/home/<dynamic>/file.txt", Flags: []string{"APPEND", "READ", "WRITE"}},
{Path: "/home/\u22ef/file.txt", Flags: []string{"APPEND", "READ", "WRITE"}},
},
},
{
Expand All @@ -71,7 +71,7 @@ func TestAnalyzeOpensWithFlagMergingAndThreshold(t *testing.T) {
{Path: "/var/log/app2.log", Flags: []string{"WRITE"}},
},
expected: []types.OpenCalls{
{Path: "/home/<dynamic>/common.txt", Flags: []string{"APPEND", "READ", "WRITE"}},
{Path: "/home/\u22ef/common.txt", Flags: []string{"APPEND", "READ", "WRITE"}},
{Path: "/var/log/app1.log", Flags: []string{"READ"}},
{Path: "/var/log/app2.log", Flags: []string{"WRITE"}},
},
Expand All @@ -89,8 +89,8 @@ func TestAnalyzeOpensWithFlagMergingAndThreshold(t *testing.T) {
{Path: "/home/user4/file2.txt", Flags: []string{"READ", "WRITE"}},
},
expected: []types.OpenCalls{
{Path: "/home/<dynamic>/file1.txt", Flags: []string{"APPEND", "READ", "WRITE"}},
{Path: "/home/<dynamic>/file2.txt", Flags: []string{"APPEND", "READ", "WRITE"}},
{Path: "/home/\u22ef/file1.txt", Flags: []string{"APPEND", "READ", "WRITE"}},
{Path: "/home/\u22ef/file2.txt", Flags: []string{"APPEND", "READ", "WRITE"}},
},
},
}
Expand Down
9 changes: 9 additions & 0 deletions pkg/registry/file/dynamicpathdetector/tests/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ func BenchmarkAnalyzeOpensVsDeflateStringer(b *testing.B) {
})
}

func BenchmarkCompareDynamic(b *testing.B) {
dynamicPath := "/api/\u22ef/\u22ef"
regularPath := "/api/users/123"
for i := 0; i < b.N; i++ {
_ = dynamicpathdetector.CompareDynamic(dynamicPath, regularPath)
}
b.ReportAllocs()
}

func generateMixedPaths(count int, fixedLength int) []string {
paths := make([]string, count)
staticSegments := []string{"users", "profile", "settings", "api", "v1", "posts", "organizations", "departments", "employees", "projects", "tasks", "categories", "subcategories", "items", "articles"}
Expand Down
97 changes: 90 additions & 7 deletions pkg/registry/file/dynamicpathdetector/tests/coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func TestDynamicSegments(t *testing.T) {
if err != nil {
t.Errorf("AnalyzePath() returned an error: %v", err)
}
expected := "/api/users/<dynamic>"
expected := "/api/users/\u22ef"
assert.Equal(t, expected, result)

// Test with one of the original IDs to ensure it's also marked as dynamic
Expand All @@ -77,7 +77,7 @@ func TestMultipleDynamicSegments(t *testing.T) {
// Test with the 100th unique user and post IDs (should trigger dynamic segments)
result, err := analyzer.AnalyzePath("/api/users/101/posts/1031", "api")
assert.NoError(t, err)
expected := "/api/users/<dynamic>/posts/<dynamic>"
expected := "/api/users/\u22ef/posts/\u22ef"
assert.Equal(t, expected, result)
}

Expand All @@ -96,7 +96,7 @@ func TestMixedStaticAndDynamicSegments(t *testing.T) {
// Test with the 100th unique user ID but same 'posts' segment (should trigger dynamic segment for users)
result, err := analyzer.AnalyzePath("/api/users/99/posts", "api")
assert.NoError(t, err)
expected := "/api/users/<dynamic>/posts"
expected := "/api/users/\u22ef/posts"
assert.Equal(t, expected, result)
}

Expand Down Expand Up @@ -124,7 +124,7 @@ func TestDynamicThreshold(t *testing.T) {
}

result, _ := analyzer.AnalyzePath("/api/users/991", "api")
assert.Equal(t, "/api/users/<dynamic>", result)
assert.Equal(t, "/api/users/\u22ef", result)
}

func TestEdgeCases(t *testing.T) {
Expand Down Expand Up @@ -154,14 +154,97 @@ func TestDynamicInsertion(t *testing.T) {
analyzer := dynamicpathdetector.NewPathAnalyzer(100)

// Insert a new path with a different identifier
result, err := analyzer.AnalyzePath("/api/users/<dynamic>", "api")
result, err := analyzer.AnalyzePath("/api/users/\u22ef", "api")
assert.NoError(t, err)
expected := "/api/users/<dynamic>"
expected := "/api/users/\u22ef"
assert.Equal(t, expected, result)

// Insert a new path with the same identifier
result, err = analyzer.AnalyzePath("/api/users/102", "api")
assert.NoError(t, err)
expected = "/api/users/<dynamic>"
expected = "/api/users/\u22ef"
assert.Equal(t, expected, result)
}

func TestCompareDynamic(t *testing.T) {
tests := []struct {
name string
dynamicPath string
regularPath string
want bool
}{
{
name: "Equal paths",
dynamicPath: "/api/users/123",
regularPath: "/api/users/123",
want: true,
},
{
name: "Different paths",
dynamicPath: "/api/users/123",
regularPath: "/api/users/456",
want: false,
},
{
name: "Dynamic segment at the end",
dynamicPath: "/api/users/\u22ef",
regularPath: "/api/users/123",
want: true,
},
{
name: "Dynamic segment at the end",
dynamicPath: "/api/users/\u22ef",
regularPath: "/api/users/123/posts",
want: false,
},
{
name: "Dynamic segment at the end, no match",
dynamicPath: "/api/users/\u22ef",
regularPath: "/api/apps/123",
want: false,
},
{
name: "Dynamic segment in the middle",
dynamicPath: "/api/\u22ef/123",
regularPath: "/api/users/123",
want: true,
},
{
name: "Dynamic segment in the middle, no match",
dynamicPath: "/api/\u22ef/123",
regularPath: "/api/users/456",
want: false,
},
{
name: "2 dynamic segments",
dynamicPath: "/api/\u22ef/\u22ef",
regularPath: "/api/users/123",
want: true,
},
{
name: "2 dynamic segments, no match",
dynamicPath: "/api/\u22ef/\u22ef",
regularPath: "/papi/users/456",
want: false,
},
{
name: "2 other dynamic segments",
dynamicPath: "/\u22ef/users/\u22ef",
regularPath: "/api/users/123",
want: true,
},
{
name: "2 other dynamic segments, no match",
dynamicPath: "/\u22ef/users/\u22ef",
regularPath: "/api/apps/456",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := dynamicpathdetector.CompareDynamic(tt.dynamicPath, tt.regularPath); got != tt.want {
t.Errorf("CompareDynamic() = %v, want %v", got, tt.want)
}
})
}
}
2 changes: 1 addition & 1 deletion pkg/registry/file/dynamicpathdetector/types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package dynamicpathdetector

const DynamicIdentifier string = "<dynamic>"
const DynamicIdentifier string = "\u22ef"

type SegmentNode struct {
SegmentName string
Expand Down

0 comments on commit c453c91

Please sign in to comment.