Skip to content

Commit f1e40a5

Browse files
authored
Merge pull request #29 from SpecterOps/reach
feat: Reach, Cache and Bitmap Structure Work - BED-7379
2 parents adf92e0 + 7b9e056 commit f1e40a5

9 files changed

Lines changed: 679 additions & 162 deletions

File tree

algo/reach.go

Lines changed: 152 additions & 122 deletions
Large diffs are not rendered by default.

algo/scc.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ func (s ComponentGraph) HasMember(memberID uint64) bool {
136136
_, hasMember := s.memberComponentLookup[memberID]
137137
return hasMember
138138
}
139+
139140
func (s ComponentGraph) KnownMembers() cardinality.Duplex[uint64] {
140141
members := cardinality.NewBitmap64()
141142

@@ -155,6 +156,10 @@ func (s ComponentGraph) ContainingComponent(memberID uint64) (uint64, bool) {
155156
return component, inComponentDigraph
156157
}
157158

159+
func (s ComponentGraph) ComponentMembers(componentID uint64) cardinality.Duplex[uint64] {
160+
return s.componentMembers[componentID]
161+
}
162+
158163
func (s ComponentGraph) CollectComponentMembers(componentID uint64, members cardinality.Duplex[uint64]) {
159164
members.Or(s.componentMembers[componentID])
160165
}

cache/cache.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package cache
2+
3+
import "sync/atomic"
4+
5+
type Stats struct {
6+
hits *atomic.Int64
7+
misses *atomic.Int64
8+
size *atomic.Int64
9+
Capacity int
10+
}
11+
12+
func (s Stats) Combined(other Stats) Stats {
13+
var (
14+
hits = &atomic.Int64{}
15+
misses = &atomic.Int64{}
16+
size = &atomic.Int64{}
17+
capacity = s.Capacity + other.Capacity
18+
)
19+
20+
hits.Add(s.Hits())
21+
hits.Add(other.Hits())
22+
23+
misses.Add(s.Misses())
24+
misses.Add(other.Misses())
25+
26+
size.Add(s.Size())
27+
size.Add(other.Size())
28+
29+
return Stats{
30+
hits: hits,
31+
misses: misses,
32+
size: size,
33+
Capacity: capacity,
34+
}
35+
}
36+
37+
func (s Stats) Miss() {
38+
s.misses.Add(1)
39+
}
40+
41+
func (s Stats) Misses() int64 {
42+
return s.misses.Load()
43+
}
44+
45+
func (s Stats) Hit() {
46+
s.hits.Add(1)
47+
}
48+
49+
func (s Stats) Hits() int64 {
50+
return s.hits.Load()
51+
}
52+
53+
func (s Stats) Size() int64 {
54+
return s.size.Load()
55+
}
56+
57+
func (s Stats) Put() {
58+
s.size.Add(1)
59+
}
60+
61+
func (s Stats) Delete() {
62+
s.size.Add(-1)
63+
}
64+
65+
func NewStats(capacity int) Stats {
66+
return Stats{
67+
hits: &atomic.Int64{},
68+
misses: &atomic.Int64{},
69+
size: &atomic.Int64{},
70+
Capacity: capacity,
71+
}
72+
}
73+
74+
type Cache[K comparable, V any] interface {
75+
Put(key K, value V)
76+
Get(key K) (V, bool)
77+
Delete(key K)
78+
Stats() Stats
79+
}

cache/nemap.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package cache
2+
3+
import (
4+
"sync"
5+
)
6+
7+
// NonExpiringMapCache
8+
type NonExpiringMapCache[K comparable, V any] struct {
9+
store map[K]V
10+
stats Stats
11+
rwLock sync.RWMutex
12+
}
13+
14+
func NewNonExpiringMapCache[K comparable, V any](capacity int) Cache[K, V] {
15+
return &NonExpiringMapCache[K, V]{
16+
store: make(map[K]V, capacity),
17+
stats: NewStats(capacity),
18+
}
19+
}
20+
21+
func (s *NonExpiringMapCache[K, V]) Put(key K, value V) {
22+
s.rwLock.Lock()
23+
defer s.rwLock.Unlock()
24+
25+
if _, exists := s.store[key]; exists {
26+
s.store[key] = value
27+
} else if int(s.stats.Size()) < s.stats.Capacity {
28+
s.store[key] = value
29+
s.stats.Put()
30+
}
31+
}
32+
33+
func (s *NonExpiringMapCache[K, V]) Get(key K) (V, bool) {
34+
s.rwLock.RLock()
35+
defer s.rwLock.RUnlock()
36+
37+
value, hasValue := s.store[key]
38+
39+
if hasValue {
40+
s.stats.Hit()
41+
} else {
42+
s.stats.Miss()
43+
}
44+
45+
return value, hasValue
46+
}
47+
48+
func (s *NonExpiringMapCache[K, V]) Delete(key K) {
49+
s.rwLock.Lock()
50+
defer s.rwLock.Unlock()
51+
52+
_, exists := s.store[key]
53+
54+
if exists {
55+
delete(s.store, key)
56+
s.stats.Delete()
57+
}
58+
}
59+
60+
func (s *NonExpiringMapCache[K, V]) Stats() Stats {
61+
return s.stats
62+
}

cache/nemap_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package cache_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/specterops/dawgs/cache"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestNonExpiringMapCache_PutGet(t *testing.T) {
11+
type testValue struct {
12+
id int
13+
data string
14+
}
15+
16+
var (
17+
nemap = cache.NewNonExpiringMapCache[int, testValue](10)
18+
expected = testValue{id: 1, data: "one"}
19+
)
20+
21+
nemap.Put(1, expected)
22+
fetched, exists := nemap.Get(1)
23+
24+
require.True(t, exists)
25+
require.Equal(t, expected, fetched)
26+
}
27+
28+
func TestNonExpiringMapCache_UpdateExistingKey(t *testing.T) {
29+
nemap := cache.NewNonExpiringMapCache[string, int](5)
30+
31+
nemap.Put("k", 10)
32+
nemap.Put("k", 20)
33+
34+
fetched, exists := nemap.Get("k")
35+
36+
require.True(t, exists)
37+
require.Equal(t, 20, fetched)
38+
}

0 commit comments

Comments
 (0)