Skip to content

Commit 7fd8e90

Browse files
committed
refactor(pnpm/v6): rewrite the analyzer
1 parent 2949f62 commit 7fd8e90

File tree

12 files changed

+710
-344
lines changed

12 files changed

+710
-344
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/murphysecurity/murphysec
22

3-
go 1.22.4
3+
go 1.23.4
44

55
require (
66
github.com/AlecAivazis/survey/v2 v2.3.7

module/pnpm/process.go

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/murphysecurity/murphysec/model"
99
"github.com/murphysecurity/murphysec/module/pnpm/shared"
1010
v5 "github.com/murphysecurity/murphysec/module/pnpm/v5"
11+
v6 "github.com/murphysecurity/murphysec/module/pnpm/v6"
1112
v9 "github.com/murphysecurity/murphysec/module/pnpm/v9"
1213
"io"
1314
"os"
@@ -55,21 +56,11 @@ func processDir(ctx context.Context, dir string) (result processDirResult) {
5556
return
5657
}
5758
} else if versionNumber == 6 {
58-
// todo: v6 support need rewrite
59-
lockfile, e := parseV6Lockfile(data, false)
59+
result.trees, e = v6.Process(ctx, data, false)
6060
if e != nil {
6161
result.e = fmt.Errorf("v6: %w", e)
6262
return
6363
}
64-
items, e := lockfile.buildDependencyTree(false)
65-
if e != nil {
66-
result.e = fmt.Errorf("v6: %w", e)
67-
return
68-
}
69-
result.trees = []shared.DepTree{{
70-
Name: "",
71-
Dependencies: items,
72-
}}
7364
} else if versionNumber == 9 {
7465
result.trees, e = v9.Parse(ctx, bytes.NewReader(data))
7566
if e != nil {

module/pnpm/shared/circular.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package shared
2+
3+
import (
4+
"github.com/repeale/fp-go"
5+
"strconv"
6+
"strings"
7+
)
8+
9+
type CircularDetector struct {
10+
m map[[2]string]struct{}
11+
path [][2]string
12+
}
13+
14+
func (c *CircularDetector) Put(name, version string) {
15+
if len(c.m) != len(c.path) {
16+
panic("length inconsistent")
17+
}
18+
if c.m == nil {
19+
c.m = make(map[[2]string]struct{})
20+
}
21+
if _, ok := c.m[[2]string{name, version}]; ok {
22+
panic("circular dependency detected")
23+
}
24+
c.path = append(c.path, [2]string{name, version})
25+
c.m[[2]string{name, version}] = struct{}{}
26+
}
27+
28+
func (c *CircularDetector) Leave(name, version string) {
29+
if len(c.path) == 0 {
30+
panic("length underflow")
31+
}
32+
if c.path[len(c.path)-1] != [2]string{name, version} {
33+
panic("name@version inconsistent")
34+
}
35+
c.path = c.path[:len(c.path)-1]
36+
}
37+
38+
func (c *CircularDetector) Contains(name, version string) (ok bool) {
39+
_, ok = c.m[[2]string{name, version}]
40+
return
41+
}
42+
43+
func (c *CircularDetector) Error(name, version string) error {
44+
if !c.Contains(name, version) {
45+
return nil
46+
}
47+
var r CircleDetected
48+
r.path = make([][2]string, len(c.path)+1)
49+
copy(r.path, c.path)
50+
r.path[len(c.path)] = [2]string{name, version}
51+
return r
52+
}
53+
54+
type CircleDetected struct {
55+
path [][2]string
56+
}
57+
58+
func (e CircleDetected) Error() string {
59+
var mapper = fp.Map(func(v [2]string) string { return v[0] + "@" + v[1] })
60+
return "circle detected[length:" + strconv.Itoa(len(e.path)-1) + "]: " + strings.Join(mapper(e.path), " -> ")
61+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package shared
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestCircularDetector_Put(t *testing.T) {
8+
cd := &CircularDetector{m: make(map[[2]string]struct{})}
9+
cd.Put("a", "1.0")
10+
if len(cd.path) != 1 {
11+
t.Fatalf("expected path length 1, got %d", len(cd.path))
12+
}
13+
if cd.path[0] != [2]string{"a", "1.0"} {
14+
t.Fatalf("expected path [a 1.0], got %v", cd.path[0])
15+
}
16+
}
17+
18+
func TestCircularDetector_Put_CircularDependency(t *testing.T) {
19+
defer func() {
20+
if r := recover(); r == nil {
21+
t.Fatalf("expected panic due to circular dependency")
22+
}
23+
}()
24+
cd := &CircularDetector{m: make(map[[2]string]struct{})}
25+
cd.Put("a", "1.0")
26+
cd.Put("a", "1.0")
27+
}
28+
29+
func TestCircularDetector_Leave(t *testing.T) {
30+
cd := &CircularDetector{m: make(map[[2]string]struct{})}
31+
cd.Put("a", "1.0")
32+
cd.Leave("a", "1.0")
33+
if len(cd.path) != 0 {
34+
t.Fatalf("expected path length 0, got %d", len(cd.path))
35+
}
36+
}
37+
38+
func TestCircularDetector_Leave_LengthUnderflow(t *testing.T) {
39+
defer func() {
40+
if r := recover(); r == nil {
41+
t.Fatalf("expected panic due to length underflow")
42+
}
43+
}()
44+
cd := &CircularDetector{m: make(map[[2]string]struct{})}
45+
cd.Leave("a", "1.0")
46+
}
47+
48+
func TestCircularDetector_Leave_NameVersionInconsistent(t *testing.T) {
49+
defer func() {
50+
if r := recover(); r == nil {
51+
t.Fatalf("expected panic due to name@version inconsistent")
52+
}
53+
}()
54+
cd := &CircularDetector{m: make(map[[2]string]struct{})}
55+
cd.Put("a", "1.0")
56+
cd.Leave("b", "1.0")
57+
}
58+
59+
func TestCircularDetector_Contains(t *testing.T) {
60+
cd := &CircularDetector{m: make(map[[2]string]struct{})}
61+
cd.Put("a", "1.0")
62+
if !cd.Contains("a", "1.0") {
63+
t.Fatalf("expected to contain [email protected]")
64+
}
65+
if cd.Contains("b", "1.0") {
66+
t.Fatalf("expected not to contain [email protected]")
67+
}
68+
}
69+
70+
func TestCircularDetector_Error(t *testing.T) {
71+
cd := &CircularDetector{m: make(map[[2]string]struct{})}
72+
cd.Put("a", "1.0")
73+
err := cd.Error("a", "1.0")
74+
if err == nil {
75+
t.Fatalf("expected error due to circular dependency")
76+
}
77+
if err.Error() != "circle detected[length:1]: [email protected] -> [email protected]" {
78+
t.Fatalf("unexpected error message: %s", err.Error())
79+
}
80+
}

0 commit comments

Comments
 (0)