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}