v0.7.0 31c7fae

filter_cache.go RawHistoryFolder

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

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