v0.9.0 636c44c

filter_cache.go RawHistoryFolder

1package cache
2
3import (
4	"encoding/binary"
5	"fmt"
6	"io"
7	"log"
8	"os"
9	"syscall"
10	"time"
11	"unsafe"
12
13	"github.com/bnema/dumber/internal/logging"
14)
15
16// FilterData represents the compiled filter data for caching
17type FilterData struct {
18	Version       uint32
19	Created       time.Time
20	NetworkRules  []NetworkRule
21	CosmeticRules []CosmeticRule
22}
23
24// NetworkRule represents a compiled network filtering rule
25type NetworkRule struct {
26	Pattern      string
27	Domain       string
28	Action       uint8  // 0=block, 1=allow
29	ResourceType uint32 // Bitmask of resource types
30	Priority     uint8
31}
32
33// CosmeticRule represents a compiled cosmetic filtering rule
34type CosmeticRule struct {
35	Domain   string
36	Selector string
37}
38
39const (
40	// Cache format version for compatibility
41	cacheFormatVersion = uint32(1)
42
43	// Magic bytes to identify cache files
44	cacheMagic = uint64(0x44554D4245524301) // "DUMBERC" + version
45
46	// File permissions
47	cacheFilePerms = 0644
48)
49
50// FilterCache provides high-performance binary filter caching with memory mapping
51type FilterCache struct {
52	path     string
53	mmapData []byte
54	size     int64
55}
56
57// CacheHeader represents the binary cache file header
58type CacheHeader struct {
59	Magic     uint64 // Magic bytes for file identification
60	Version   uint32 // Cache format version
61	DataSize  uint64 // Size of data section
62	RuleCount uint32 // Number of filter rules
63	Created   int64  // Unix timestamp when cache was created
64	Checksum  uint64 // CRC64 checksum of data section
65}
66
67// FilterRule represents a compiled filter rule in binary format
68type FilterRule struct {
69	Type         uint8  // Rule type (0=network, 1=cosmetic)
70	Priority     uint8  // Rule priority
71	Action       uint8  // Action to take (0=block, 1=allow, etc.)
72	Flags        uint8  // Additional flags
73	PatternLen   uint16 // Length of pattern string
74	DomainLen    uint16 // Length of domain string
75	ResourceType uint32 // Bitmask of resource types
76	// Followed by: Pattern (variable length), Domain (variable length)
77}
78
79// NewFilterCache creates a new filter cache instance
80func NewFilterCache(path string) *FilterCache {
81	return &FilterCache{
82		path: path,
83	}
84}
85
86// LoadMapped loads the cache file using memory mapping for zero-copy access
87func (fc *FilterCache) LoadMapped() (*FilterData, error) {
88	file, err := os.Open(fc.path)
89	if err != nil {
90		return nil, fmt.Errorf("failed to open cache file: %w", err)
91	}
92	defer file.Close()
93
94	// Get file size
95	stat, err := file.Stat()
96	if err != nil {
97		return nil, fmt.Errorf("failed to stat cache file: %w", err)
98	}
99
100	fc.size = stat.Size()
101	if fc.size < int64(unsafe.Sizeof(CacheHeader{})) {
102		return nil, fmt.Errorf("cache file too small")
103	}
104
105	// Memory map the file for read-only access
106	fc.mmapData, err = syscall.Mmap(int(file.Fd()), 0, int(fc.size), syscall.PROT_READ, syscall.MAP_SHARED)
107	if err != nil {
108		return nil, fmt.Errorf("failed to mmap cache file: %w", err)
109	}
110
111	// Parse the header
112	header, err := fc.parseHeader()
113	if err != nil {
114		if unmapErr := fc.Unmap(); unmapErr != nil {
115			log.Printf("[filter-cache] failed to unmap after header parse error: %v", unmapErr)
116		}
117		return nil, fmt.Errorf("invalid cache header: %w", err)
118	}
119
120	// Validate magic and version
121	if header.Magic != cacheMagic {
122		if unmapErr := fc.Unmap(); unmapErr != nil {
123			log.Printf("[filter-cache] failed to unmap after magic validation: %v", unmapErr)
124		}
125		return nil, fmt.Errorf("invalid cache magic bytes")
126	}
127
128	if header.Version != cacheFormatVersion {
129		if unmapErr := fc.Unmap(); unmapErr != nil {
130			log.Printf("[filter-cache] failed to unmap after version validation: %v", unmapErr)
131		}
132		return nil, fmt.Errorf("unsupported cache version: %d", header.Version)
133	}
134
135	// Parse filter data without copying
136	data, err := fc.parseInPlace(header)
137	if err != nil {
138		fc.Unmap()
139		return nil, fmt.Errorf("failed to parse cache data: %w", err)
140	}
141
142	logging.Info(fmt.Sprintf("Loaded binary cache: %d rules, %d bytes (memory-mapped)",
143		header.RuleCount, fc.size))
144
145	return data, nil
146}
147
148// parseHeader extracts the cache header from memory-mapped data
149func (fc *FilterCache) parseHeader() (*CacheHeader, error) {
150	if len(fc.mmapData) < int(unsafe.Sizeof(CacheHeader{})) {
151		return nil, fmt.Errorf("insufficient data for header")
152	}
153
154	header := (*CacheHeader)(unsafe.Pointer(&fc.mmapData[0]))
155	return header, nil
156}
157
158// parseInPlace parses filter data directly from memory-mapped data without copying
159func (fc *FilterCache) parseInPlace(header *CacheHeader) (*FilterData, error) {
160	data := &FilterData{
161		Version:       header.Version,
162		NetworkRules:  make([]NetworkRule, 0, header.RuleCount),
163		CosmeticRules: make([]CosmeticRule, 0),
164	}
165
166	// Start parsing after the header
167	offset := int(unsafe.Sizeof(CacheHeader{}))
168	remaining := header.RuleCount
169
170	for remaining > 0 && offset < len(fc.mmapData) {
171		// Parse rule header
172		if offset+int(unsafe.Sizeof(FilterRule{})) > len(fc.mmapData) {
173			break
174		}
175
176		ruleHeader := (*FilterRule)(unsafe.Pointer(&fc.mmapData[offset]))
177		offset += int(unsafe.Sizeof(FilterRule{}))
178
179		// Extract pattern string (zero-copy)
180		if offset+int(ruleHeader.PatternLen) > len(fc.mmapData) {
181			break
182		}
183		pattern := string(fc.mmapData[offset : offset+int(ruleHeader.PatternLen)])
184		offset += int(ruleHeader.PatternLen)
185
186		// Extract domain string (zero-copy)
187		if offset+int(ruleHeader.DomainLen) > len(fc.mmapData) {
188			break
189		}
190		domain := string(fc.mmapData[offset : offset+int(ruleHeader.DomainLen)])
191		offset += int(ruleHeader.DomainLen)
192
193		// Create appropriate rule type
194		switch ruleHeader.Type {
195		case 0: // Network rule
196			data.NetworkRules = append(data.NetworkRules, NetworkRule{
197				Pattern:      pattern,
198				Domain:       domain,
199				Action:       ruleHeader.Action,
200				ResourceType: ruleHeader.ResourceType,
201				Priority:     ruleHeader.Priority,
202			})
203		case 1: // Cosmetic rule
204			data.CosmeticRules = append(data.CosmeticRules, CosmeticRule{
205				Domain:   domain,
206				Selector: pattern,
207			})
208		}
209
210		remaining--
211	}
212
213	return data, nil
214}
215
216// Write saves filter data to cache in binary format
217func (fc *FilterCache) Write(data *FilterData) error {
218	// Create temporary file for atomic write
219	tmpPath := fc.path + ".tmp"
220	file, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, cacheFilePerms)
221	if err != nil {
222		return fmt.Errorf("failed to create cache file: %w", err)
223	}
224	defer file.Close()
225
226	// Calculate total rule count
227	totalRules := uint32(len(data.NetworkRules) + len(data.CosmeticRules))
228
229	// Write header
230	header := CacheHeader{
231		Magic:     cacheMagic,
232		Version:   data.Version,
233		RuleCount: totalRules,
234		Created:   data.Created.Unix(),
235		DataSize:  0, // Will be calculated
236		Checksum:  0, // Will be calculated
237	}
238
239	if err := binary.Write(file, binary.LittleEndian, header); err != nil {
240		os.Remove(tmpPath)
241		return fmt.Errorf("failed to write header: %w", err)
242	}
243
244	dataStart, _ := file.Seek(0, io.SeekCurrent)
245
246	// Write network rules
247	for _, rule := range data.NetworkRules {
248		if err := fc.writeNetworkRule(file, rule); err != nil {
249			os.Remove(tmpPath)
250			return fmt.Errorf("failed to write network rule: %w", err)
251		}
252	}
253
254	// Write cosmetic rules
255	for _, rule := range data.CosmeticRules {
256		if err := fc.writeCosmeticRule(file, rule); err != nil {
257			os.Remove(tmpPath)
258			return fmt.Errorf("failed to write cosmetic rule: %w", err)
259		}
260	}
261
262	// Calculate data size
263	dataEnd, _ := file.Seek(0, io.SeekCurrent)
264	dataSize := uint64(dataEnd - dataStart)
265
266	// Update header with data size
267	if _, err := file.Seek(0, io.SeekStart); err != nil {
268		os.Remove(tmpPath)
269		return err
270	}
271
272	header.DataSize = dataSize
273	// TODO: Calculate and set checksum
274
275	if err := binary.Write(file, binary.LittleEndian, header); err != nil {
276		os.Remove(tmpPath)
277		return fmt.Errorf("failed to update header: %w", err)
278	}
279
280	if err := file.Sync(); err != nil {
281		os.Remove(tmpPath)
282		return fmt.Errorf("failed to sync cache file: %w", err)
283	}
284
285	file.Close()
286
287	// Atomic rename
288	if err := os.Rename(tmpPath, fc.path); err != nil {
289		os.Remove(tmpPath)
290		return fmt.Errorf("failed to rename cache file: %w", err)
291	}
292
293	logging.Info(fmt.Sprintf("Wrote binary cache: %d rules, %d bytes", totalRules, dataSize))
294	return nil
295}
296
297// writeNetworkRule writes a network rule in binary format
298func (fc *FilterCache) writeNetworkRule(file *os.File, rule NetworkRule) error {
299	patternBytes := []byte(rule.Pattern)
300	domainBytes := []byte(rule.Domain)
301
302	ruleHeader := FilterRule{
303		Type:         0, // Network rule
304		Priority:     rule.Priority,
305		Action:       rule.Action,
306		Flags:        0,
307		PatternLen:   uint16(len(patternBytes)),
308		DomainLen:    uint16(len(domainBytes)),
309		ResourceType: rule.ResourceType,
310	}
311
312	// Write rule header
313	if err := binary.Write(file, binary.LittleEndian, ruleHeader); err != nil {
314		return err
315	}
316
317	// Write pattern
318	if _, err := file.Write(patternBytes); err != nil {
319		return err
320	}
321
322	// Write domain
323	if _, err := file.Write(domainBytes); err != nil {
324		return err
325	}
326
327	return nil
328}
329
330// writeCosmeticRule writes a cosmetic rule in binary format
331func (fc *FilterCache) writeCosmeticRule(file *os.File, rule CosmeticRule) error {
332	selectorBytes := []byte(rule.Selector)
333	domainBytes := []byte(rule.Domain)
334
335	ruleHeader := FilterRule{
336		Type:         1, // Cosmetic rule
337		Priority:     0,
338		Action:       0, // Hide
339		Flags:        0,
340		PatternLen:   uint16(len(selectorBytes)),
341		DomainLen:    uint16(len(domainBytes)),
342		ResourceType: 0,
343	}
344
345	// Write rule header
346	if err := binary.Write(file, binary.LittleEndian, ruleHeader); err != nil {
347		return err
348	}
349
350	// Write selector
351	if _, err := file.Write(selectorBytes); err != nil {
352		return err
353	}
354
355	// Write domain
356	if _, err := file.Write(domainBytes); err != nil {
357		return err
358	}
359
360	return nil
361}
362
363// Unmap releases the memory-mapped data
364func (fc *FilterCache) Unmap() error {
365	if fc.mmapData != nil {
366		if err := syscall.Munmap(fc.mmapData); err != nil {
367			return fmt.Errorf("failed to unmap cache data: %w", err)
368		}
369		fc.mmapData = nil
370	}
371	return nil
372}
373
374// Size returns the size of the cache file in bytes
375func (fc *FilterCache) Size() int64 {
376	return fc.size
377}
378
379// IsValid checks if the cache file exists and has valid format
380func (fc *FilterCache) IsValid() bool {
381	file, err := os.Open(fc.path)
382	if err != nil {
383		return false
384	}
385	defer file.Close()
386
387	var magic uint64
388	if err := binary.Read(file, binary.LittleEndian, &magic); err != nil {
389		return false
390	}
391
392	return magic == cacheMagic
393}
394
395// Remove deletes the cache file
396func (fc *FilterCache) Remove() error {
397	fc.Unmap() // Unmap first if mapped
398	return os.Remove(fc.path)
399}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close