Skip to content

Commit

Permalink
refactor(pnpm/v6): rewrite the analyzer
Browse files Browse the repository at this point in the history
  • Loading branch information
iseki0 committed Dec 9, 2024
1 parent 2949f62 commit 7fd8e90
Show file tree
Hide file tree
Showing 12 changed files with 710 additions and 344 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/murphysecurity/murphysec

go 1.22.4
go 1.23.4

require (
github.com/AlecAivazis/survey/v2 v2.3.7
Expand Down
13 changes: 2 additions & 11 deletions module/pnpm/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/murphysecurity/murphysec/model"
"github.com/murphysecurity/murphysec/module/pnpm/shared"
v5 "github.com/murphysecurity/murphysec/module/pnpm/v5"
v6 "github.com/murphysecurity/murphysec/module/pnpm/v6"
v9 "github.com/murphysecurity/murphysec/module/pnpm/v9"
"io"
"os"
Expand Down Expand Up @@ -55,21 +56,11 @@ func processDir(ctx context.Context, dir string) (result processDirResult) {
return
}
} else if versionNumber == 6 {
// todo: v6 support need rewrite
lockfile, e := parseV6Lockfile(data, false)
result.trees, e = v6.Process(ctx, data, false)
if e != nil {
result.e = fmt.Errorf("v6: %w", e)
return
}
items, e := lockfile.buildDependencyTree(false)
if e != nil {
result.e = fmt.Errorf("v6: %w", e)
return
}
result.trees = []shared.DepTree{{
Name: "",
Dependencies: items,
}}
} else if versionNumber == 9 {
result.trees, e = v9.Parse(ctx, bytes.NewReader(data))
if e != nil {
Expand Down
61 changes: 61 additions & 0 deletions module/pnpm/shared/circular.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package shared

import (
"github.com/repeale/fp-go"
"strconv"
"strings"
)

type CircularDetector struct {
m map[[2]string]struct{}
path [][2]string
}

func (c *CircularDetector) Put(name, version string) {
if len(c.m) != len(c.path) {
panic("length inconsistent")
}
if c.m == nil {
c.m = make(map[[2]string]struct{})
}
if _, ok := c.m[[2]string{name, version}]; ok {
panic("circular dependency detected")
}
c.path = append(c.path, [2]string{name, version})
c.m[[2]string{name, version}] = struct{}{}
}

func (c *CircularDetector) Leave(name, version string) {
if len(c.path) == 0 {
panic("length underflow")
}
if c.path[len(c.path)-1] != [2]string{name, version} {
panic("name@version inconsistent")
}
c.path = c.path[:len(c.path)-1]
}

func (c *CircularDetector) Contains(name, version string) (ok bool) {
_, ok = c.m[[2]string{name, version}]
return
}

func (c *CircularDetector) Error(name, version string) error {
if !c.Contains(name, version) {
return nil
}
var r CircleDetected
r.path = make([][2]string, len(c.path)+1)
copy(r.path, c.path)
r.path[len(c.path)] = [2]string{name, version}
return r
}

type CircleDetected struct {
path [][2]string
}

func (e CircleDetected) Error() string {
var mapper = fp.Map(func(v [2]string) string { return v[0] + "@" + v[1] })
return "circle detected[length:" + strconv.Itoa(len(e.path)-1) + "]: " + strings.Join(mapper(e.path), " -> ")
}
80 changes: 80 additions & 0 deletions module/pnpm/shared/circular_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package shared

import (
"testing"
)

func TestCircularDetector_Put(t *testing.T) {
cd := &CircularDetector{m: make(map[[2]string]struct{})}
cd.Put("a", "1.0")
if len(cd.path) != 1 {
t.Fatalf("expected path length 1, got %d", len(cd.path))
}
if cd.path[0] != [2]string{"a", "1.0"} {
t.Fatalf("expected path [a 1.0], got %v", cd.path[0])
}
}

func TestCircularDetector_Put_CircularDependency(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("expected panic due to circular dependency")
}
}()
cd := &CircularDetector{m: make(map[[2]string]struct{})}
cd.Put("a", "1.0")
cd.Put("a", "1.0")
}

func TestCircularDetector_Leave(t *testing.T) {
cd := &CircularDetector{m: make(map[[2]string]struct{})}
cd.Put("a", "1.0")
cd.Leave("a", "1.0")
if len(cd.path) != 0 {
t.Fatalf("expected path length 0, got %d", len(cd.path))
}
}

func TestCircularDetector_Leave_LengthUnderflow(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("expected panic due to length underflow")
}
}()
cd := &CircularDetector{m: make(map[[2]string]struct{})}
cd.Leave("a", "1.0")
}

func TestCircularDetector_Leave_NameVersionInconsistent(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("expected panic due to name@version inconsistent")
}
}()
cd := &CircularDetector{m: make(map[[2]string]struct{})}
cd.Put("a", "1.0")
cd.Leave("b", "1.0")
}

func TestCircularDetector_Contains(t *testing.T) {
cd := &CircularDetector{m: make(map[[2]string]struct{})}
cd.Put("a", "1.0")
if !cd.Contains("a", "1.0") {
t.Fatalf("expected to contain [email protected]")
}
if cd.Contains("b", "1.0") {
t.Fatalf("expected not to contain [email protected]")
}
}

func TestCircularDetector_Error(t *testing.T) {
cd := &CircularDetector{m: make(map[[2]string]struct{})}
cd.Put("a", "1.0")
err := cd.Error("a", "1.0")
if err == nil {
t.Fatalf("expected error due to circular dependency")
}
if err.Error() != "circle detected[length:1]: [email protected] -> [email protected]" {
t.Fatalf("unexpected error message: %s", err.Error())
}
}
Loading

0 comments on commit 7fd8e90

Please sign in to comment.