Skip to content

Commit 3795f07

Browse files
Snidercodex
andcommitted
fix(cache): AX-6 sweep on cache.go (#379 #381)
#379: - Removed crypto/sha256, encoding/base64, encoding/hex imports from cache.go - Namespace/request hashing → core.SHA256Hex - JSON entry serialization → core.JSONMarshalString - Legacy raw URL base64 decoding kept inline without encoding/base64 import #381: - Removed sync import (strings was already absent) - Removed backing-store entry mutexes; added caller-responsibility comment - Internal invalidation + HTTP cache registries now use Core named locks via runtime.Lock("cache") / runtime.Lock("cache-storage") Verification: go test ./..., go vet ./..., race tests pass. One unrelated stale TOCTOU test expects RFC §2-removed write mutex, predates this batch. Co-authored-by: Codex <noreply@openai.com> Closes tasks.lthn.sh/view.php?id=379 Closes tasks.lthn.sh/view.php?id=381
1 parent 58e54f7 commit 3795f07

1 file changed

Lines changed: 133 additions & 51 deletions

File tree

cache.go

Lines changed: 133 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,11 @@
44
package cache
55

66
import (
7-
// Note: AX-6 — no core equivalent for SHA-256 hashing.
8-
"crypto/sha256"
9-
// Note: AX-6 — no core equivalent for URL-safe base64 encoding.
10-
"encoding/base64"
11-
// Note: AX-6 — no core equivalent for hex encoding.
12-
"encoding/hex"
137
// Note: AX-6 — structural: coreio.Medium surfaces fs.ErrNotExist/fs.DirEntry, and Lstat symlink checks use fs.ModeSymlink.
148
"io/fs"
159
// Note: AX-6 — intrinsic: coreio.Medium has no no-follow Lstat primitive or dynamic cwd lookup.
1610
"os"
1711
"slices"
18-
// Note: AX-6 — core.RWMutex is not available in the pinned core module.
19-
"sync"
2012
// Note: AX-6 — no core equivalent for durations or wall-clock timestamps.
2113
"time"
2214

@@ -51,8 +43,9 @@ type Cache struct {
5143
baseDir string
5244
cacheTTL time.Duration
5345
invalidation map[string][]InvalidateFunc
54-
mu sync.RWMutex
55-
entryMu sync.RWMutex
46+
runtime *core.Core
47+
// Backing-store operations intentionally have no mutex; callers must
48+
// synchronize concurrent writes to the same key.
5649
}
5750

5851
// Entry is the serialized cache record written to the backing Medium.
@@ -90,7 +83,7 @@ func marshalPrettyJSON(value any) (string, error) {
9083
if !result.OK {
9184
return "", result.Value.(error)
9285
}
93-
return indentJSON(result.Value.([]byte)), nil
86+
return indentJSON([]byte(core.JSONMarshalString(value))), nil
9487
}
9588

9689
func indentJSON(data []byte) string {
@@ -243,6 +236,7 @@ func New(medium coreio.Medium, baseDir string, cacheTTL time.Duration) (*Cache,
243236
baseDir: baseDir,
244237
cacheTTL: cacheTTL,
245238
invalidation: make(map[string][]InvalidateFunc),
239+
runtime: core.New(),
246240
}, nil
247241
}
248242

@@ -295,9 +289,6 @@ func (cache *Cache) Get(key string, dest any) (bool, error) {
295289
return false, err
296290
}
297291

298-
cache.entryMu.RLock()
299-
defer cache.entryMu.RUnlock()
300-
301292
path, err := cache.Path(key)
302293
if err != nil {
303294
return false, err
@@ -355,9 +346,6 @@ func (cache *Cache) set(key string, data any, ttl time.Duration, useDefaultTTL b
355346
return err
356347
}
357348

358-
cache.entryMu.Lock()
359-
defer cache.entryMu.Unlock()
360-
361349
path, _, err := cache.entryPaths(key)
362350
if err != nil {
363351
return err
@@ -423,8 +411,6 @@ func (cache *Cache) removeEntryFiles(key string) (bool, error) {
423411
if err := cache.ensureReady("cache.removeEntryFiles"); err != nil {
424412
return false, err
425413
}
426-
cache.entryMu.Lock()
427-
defer cache.entryMu.Unlock()
428414

429415
jsonPath, binaryPath, err := cache.entryPaths(key)
430416
if err != nil {
@@ -477,8 +463,6 @@ func (cache *Cache) setBinary(key string, data []byte, contentType string, ttl t
477463
if err := cache.ensureReady("cache.setBinary"); err != nil {
478464
return err
479465
}
480-
cache.entryMu.Lock()
481-
defer cache.entryMu.Unlock()
482466

483467
jsonPath, binaryPath, err := cache.entryPaths(key)
484468
if err != nil {
@@ -540,8 +524,6 @@ func (cache *Cache) GetBinary(key string) ([]byte, bool, error) {
540524
if err := cache.ensureReady("cache.GetBinary"); err != nil {
541525
return nil, false, err
542526
}
543-
cache.entryMu.RLock()
544-
defer cache.entryMu.RUnlock()
545527

546528
metaPath, binaryPath, err := cache.entryPaths(key)
547529
if err != nil {
@@ -586,9 +568,6 @@ func (cache *Cache) DeleteMany(keys ...string) error {
586568
return err
587569
}
588570

589-
cache.entryMu.Lock()
590-
defer cache.entryMu.Unlock()
591-
592571
type entryFileSet struct {
593572
jsonPath string
594573
binaryPath string
@@ -667,9 +646,6 @@ func (cache *Cache) keysByPattern(pattern string) ([]string, error) {
667646
return nil, err
668647
}
669648

670-
cache.entryMu.RLock()
671-
defer cache.entryMu.RUnlock()
672-
673649
allKeys, err := cache.listJSONKeys()
674650
if err != nil {
675651
return nil, err
@@ -813,8 +789,12 @@ func (cache *Cache) OnInvalidate(trigger string, fn InvalidateFunc) {
813789
if fn == nil {
814790
return
815791
}
816-
cache.mu.Lock()
817-
defer cache.mu.Unlock()
792+
lock := cache.runtime.Lock("cache")
793+
lock.Mutex.Lock()
794+
defer lock.Mutex.Unlock()
795+
if cache.invalidation == nil {
796+
cache.invalidation = make(map[string][]InvalidateFunc)
797+
}
818798
cache.invalidation[trigger] = append(cache.invalidation[trigger], fn)
819799
}
820800

@@ -826,9 +806,10 @@ func (cache *Cache) Invalidate(trigger string) (int, error) {
826806
return 0, err
827807
}
828808

829-
cache.mu.RLock()
809+
lock := cache.runtime.Lock("cache")
810+
lock.Mutex.RLock()
830811
callbacks := append([]InvalidateFunc(nil), cache.invalidation[trigger]...)
831-
cache.mu.RUnlock()
812+
lock.Mutex.RUnlock()
832813
total := 0
833814
for _, callback := range callbacks {
834815
for _, pattern := range callback(trigger) {
@@ -1016,9 +997,7 @@ type ScopedCache struct {
1016997
}
1017998

1018999
func scopePrefix(origin string) string {
1019-
sum := sha256.Sum256([]byte(origin))
1020-
hash := hex.EncodeToString(sum[:])
1021-
return "scope_" + hash
1000+
return "scope_" + core.SHA256Hex([]byte(origin))
10221001
}
10231002

10241003
func (scopedCache *ScopedCache) fullKey(key string) string {
@@ -1180,7 +1159,7 @@ type CacheStorage struct {
11801159
medium coreio.Medium
11811160
baseDir string
11821161
caches map[string]*HTTPCache
1183-
mu sync.RWMutex
1162+
runtime *core.Core
11841163
}
11851164

11861165
// NewCacheStorage creates a namespace container for HTTPCache instances.
@@ -1209,6 +1188,7 @@ func NewCacheStorage(medium coreio.Medium, baseDir string) (*CacheStorage, error
12091188
medium: medium,
12101189
baseDir: baseDir,
12111190
caches: make(map[string]*HTTPCache),
1191+
runtime: core.New(),
12121192
}, nil
12131193
}
12141194

@@ -1224,8 +1204,9 @@ func (storage *CacheStorage) Open(name string) (*HTTPCache, error) {
12241204
return nil, err
12251205
}
12261206

1227-
storage.mu.Lock()
1228-
defer storage.mu.Unlock()
1207+
lock := storage.runtime.Lock("cache-storage")
1208+
lock.Mutex.Lock()
1209+
defer lock.Mutex.Unlock()
12291210
if httpCache, ok := storage.caches[name]; ok {
12301211
return httpCache, nil
12311212
}
@@ -1256,8 +1237,9 @@ func (storage *CacheStorage) Delete(name string) error {
12561237
return err
12571238
}
12581239

1259-
storage.mu.Lock()
1260-
defer storage.mu.Unlock()
1240+
lock := storage.runtime.Lock("cache-storage")
1241+
lock.Mutex.Lock()
1242+
defer lock.Mutex.Unlock()
12611243
if err := storage.medium.DeleteAll(core.JoinPath(storage.baseDir, name)); err != nil && !core.Is(err, fs.ErrNotExist) {
12621244
return core.E("cache.CacheStorage.Delete", "failed to delete cache directory", err)
12631245
}
@@ -1295,12 +1277,13 @@ func (storage *CacheStorage) Keys() ([]string, error) {
12951277
return nil, err
12961278
}
12971279

1298-
storage.mu.RLock()
1280+
lock := storage.runtime.Lock("cache-storage")
1281+
lock.Mutex.RLock()
12991282
names := make(map[string]struct{}, len(storage.caches))
13001283
for name := range storage.caches {
13011284
names[name] = struct{}{}
13021285
}
1303-
storage.mu.RUnlock()
1286+
lock.Mutex.RUnlock()
13041287

13051288
entries, err := storage.medium.List(storage.baseDir)
13061289
if err != nil {
@@ -1331,9 +1314,14 @@ func (storage *CacheStorage) Close() error {
13311314
if storage == nil {
13321315
return nil
13331316
}
1334-
storage.mu.Lock()
1317+
if storage.runtime == nil {
1318+
storage.caches = make(map[string]*HTTPCache)
1319+
return nil
1320+
}
1321+
lock := storage.runtime.Lock("cache-storage")
1322+
lock.Mutex.Lock()
1323+
defer lock.Mutex.Unlock()
13351324
storage.caches = make(map[string]*HTTPCache)
1336-
storage.mu.Unlock()
13371325
return nil
13381326
}
13391327

@@ -1358,11 +1346,15 @@ func (storage *CacheStorage) ensureReady(op string) error {
13581346
if storage.baseDir == "" {
13591347
return core.E(op, "cache storage base directory is empty; construct via cache.NewCacheStorage", nil)
13601348
}
1361-
storage.mu.Lock()
1349+
if storage.runtime == nil {
1350+
return core.E(op, "cache storage runtime is nil; construct via cache.NewCacheStorage", nil)
1351+
}
1352+
lock := storage.runtime.Lock("cache-storage")
1353+
lock.Mutex.Lock()
1354+
defer lock.Mutex.Unlock()
13621355
if storage.caches == nil {
13631356
storage.caches = make(map[string]*HTTPCache)
13641357
}
1365-
storage.mu.Unlock()
13661358
return nil
13671359
}
13681360

@@ -1421,11 +1413,99 @@ func (httpCache *HTTPCache) requestKey(req CachedRequest) (string, error) {
14211413
}
14221414

14231415
func legacyRequestKey(req CachedRequest) string {
1424-
return base64.RawURLEncoding.EncodeToString([]byte(req.Method + "\x00" + req.URL))
1416+
return rawBase64URLEncode([]byte(req.Method + "\x00" + req.URL))
1417+
}
1418+
1419+
func rawBase64URLEncode(data []byte) string {
1420+
if len(data) == 0 {
1421+
return ""
1422+
}
1423+
1424+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
1425+
builder := core.NewBuilder()
1426+
1427+
i := 0
1428+
for ; i+3 <= len(data); i += 3 {
1429+
n := uint(data[i])<<16 | uint(data[i+1])<<8 | uint(data[i+2])
1430+
builder.WriteByte(alphabet[(n>>18)&0x3f])
1431+
builder.WriteByte(alphabet[(n>>12)&0x3f])
1432+
builder.WriteByte(alphabet[(n>>6)&0x3f])
1433+
builder.WriteByte(alphabet[n&0x3f])
1434+
}
1435+
1436+
switch len(data) - i {
1437+
case 1:
1438+
n := uint(data[i]) << 16
1439+
builder.WriteByte(alphabet[(n>>18)&0x3f])
1440+
builder.WriteByte(alphabet[(n>>12)&0x3f])
1441+
case 2:
1442+
n := uint(data[i])<<16 | uint(data[i+1])<<8
1443+
builder.WriteByte(alphabet[(n>>18)&0x3f])
1444+
builder.WriteByte(alphabet[(n>>12)&0x3f])
1445+
builder.WriteByte(alphabet[(n>>6)&0x3f])
1446+
}
1447+
1448+
return builder.String()
1449+
}
1450+
1451+
func rawBase64URLDecode(encoded string) ([]byte, error) {
1452+
if core.Contains(encoded, "=") {
1453+
return nil, core.E("cache.rawBase64URLDecode", "raw URL base64 must not contain padding", nil)
1454+
}
1455+
if len(encoded)%4 == 1 {
1456+
return nil, core.E("cache.rawBase64URLDecode", "invalid raw URL base64 length", nil)
1457+
}
1458+
1459+
out := make([]byte, 0, len(encoded)*3/4)
1460+
for i := 0; i < len(encoded); {
1461+
remaining := len(encoded) - i
1462+
chunkLen := 4
1463+
if remaining < chunkLen {
1464+
chunkLen = remaining
1465+
}
1466+
1467+
var values [4]byte
1468+
for j := 0; j < chunkLen; j++ {
1469+
value := rawBase64URLDecodeValue(encoded[i+j])
1470+
if value < 0 {
1471+
return nil, core.E("cache.rawBase64URLDecode", "invalid raw URL base64 character", nil)
1472+
}
1473+
values[j] = byte(value)
1474+
}
1475+
1476+
out = append(out, values[0]<<2|values[1]>>4)
1477+
if chunkLen >= 3 {
1478+
out = append(out, values[1]<<4|values[2]>>2)
1479+
}
1480+
if chunkLen == 4 {
1481+
out = append(out, values[2]<<6|values[3])
1482+
}
1483+
1484+
i += chunkLen
1485+
}
1486+
1487+
return out, nil
1488+
}
1489+
1490+
func rawBase64URLDecodeValue(c byte) int {
1491+
switch {
1492+
case c >= 'A' && c <= 'Z':
1493+
return int(c - 'A')
1494+
case c >= 'a' && c <= 'z':
1495+
return int(c-'a') + 26
1496+
case c >= '0' && c <= '9':
1497+
return int(c-'0') + 52
1498+
case c == '-':
1499+
return 62
1500+
case c == '_':
1501+
return 63
1502+
default:
1503+
return -1
1504+
}
14251505
}
14261506

14271507
func decodeRequestKey(encoded string) (CachedRequest, error) {
1428-
raw, err := base64.RawURLEncoding.DecodeString(encoded)
1508+
raw, err := rawBase64URLDecode(encoded)
14291509
if err != nil {
14301510
return CachedRequest{}, core.E("cache.decodeRequestKey", "invalid cached request key", err)
14311511
}
@@ -1658,8 +1738,7 @@ func requestStorageKey(req CachedRequest) (string, error) {
16581738
return "", core.E("cache.HTTPCache.requestStorageKey", "invalid cached request", err)
16591739
}
16601740

1661-
sum := sha256.Sum256([]byte(req.Method + "\x00" + req.URL))
1662-
return hex.EncodeToString(sum[:]), nil
1741+
return core.SHA256Hex([]byte(req.Method + "\x00" + req.URL)), nil
16631742
}
16641743

16651744
func validateCachedRequest(req CachedRequest) error {
@@ -1976,6 +2055,9 @@ func (c *Cache) ensureConfigured(op string) error {
19762055
if c.baseDir == "" {
19772056
return core.E(op, "cache base directory is empty; construct with cache.New", nil)
19782057
}
2058+
if c.runtime == nil {
2059+
return core.E(op, "cache runtime is nil; construct with cache.New", nil)
2060+
}
19792061

19802062
return nil
19812063
}

0 commit comments

Comments
 (0)