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}