1package filtering
2
3import (
4 "context"
5 "fmt"
6 "sync"
7
8 "github.com/bnema/dumber/internal/logging"
9 "github.com/bnema/puregotk-webkit/webkit"
10 "github.com/bnema/puregotk/v4/gio"
11)
12
13// Store wraps webkit.UserContentFilterStore with Go-friendly async operations.
14type Store struct {
15 inner *webkit.UserContentFilterStore
16 path string
17 mu sync.Mutex
18}
19
20// NewStore creates a new Store at the given path.
21// The path is where WebKit will store compiled filter bytecode.
22func NewStore(storagePath string) *Store {
23 inner := webkit.NewUserContentFilterStore(storagePath)
24 if inner == nil {
25 return nil
26 }
27 return &Store{
28 inner: inner,
29 path: storagePath,
30 }
31}
32
33// Path returns the storage path for compiled filters.
34func (s *Store) Path() string {
35 return s.path
36}
37
38// CompileResult holds the result of a filter compilation.
39type CompileResult struct {
40 Filter *webkit.UserContentFilter
41 Err error
42}
43
44// Compile compiles a JSON filter file and stores it with the given identifier.
45// This is an async operation that may take several seconds for large filter sets.
46// The JSON file must be in Safari Content Blocker format.
47func (s *Store) Compile(ctx context.Context, identifier, jsonPath string) (*webkit.UserContentFilter, error) {
48 log := logging.FromContext(ctx).With().
49 Str("component", "filter-store").
50 Str("identifier", identifier).
51 Str("json_path", jsonPath).
52 Logger()
53
54 s.mu.Lock()
55 defer s.mu.Unlock()
56
57 // Create GFile from path
58 file := gio.FileNewForPath(jsonPath)
59 if file == nil {
60 return nil, fmt.Errorf("failed to create GFile for path: %s", jsonPath)
61 }
62
63 log.Debug().Msg("starting filter compilation")
64
65 // Use a channel to receive the async result
66 resultCh := make(chan CompileResult, 1)
67
68 // Create async callback
69 cb := gio.AsyncReadyCallback(func(_ uintptr, resPtr uintptr, _ uintptr) {
70 if resPtr == 0 {
71 resultCh <- CompileResult{Err: fmt.Errorf("nil async result")}
72 return
73 }
74
75 res := &gio.AsyncResultBase{Ptr: resPtr}
76 filter, err := s.inner.SaveFromFileFinish(res)
77 resultCh <- CompileResult{Filter: filter, Err: err}
78 })
79
80 // Start async compilation
81 s.inner.SaveFromFile(identifier, file, nil, &cb, 0)
82
83 // Wait for result or context cancellation
84 select {
85 case result := <-resultCh:
86 if result.Err != nil {
87 log.Error().Err(result.Err).Msg("filter compilation failed")
88 return nil, result.Err
89 }
90 log.Info().Msg("filter compilation completed")
91 return result.Filter, nil
92 case <-ctx.Done():
93 log.Warn().Msg("filter compilation canceled")
94 return nil, ctx.Err()
95 }
96}
97
98// LoadResult holds the result of loading a compiled filter.
99type LoadResult struct {
100 Filter *webkit.UserContentFilter
101 Err error
102}
103
104// Load loads a previously compiled filter by its identifier.
105// This is fast (~50ms) as it loads pre-compiled bytecode.
106func (s *Store) Load(ctx context.Context, identifier string) (*webkit.UserContentFilter, error) {
107 log := logging.FromContext(ctx).With().
108 Str("component", "filter-store").
109 Str("identifier", identifier).
110 Logger()
111
112 s.mu.Lock()
113 defer s.mu.Unlock()
114
115 log.Debug().Msg("loading compiled filter")
116
117 // Use a channel to receive the async result
118 resultCh := make(chan LoadResult, 1)
119
120 // Create async callback
121 cb := gio.AsyncReadyCallback(func(_ uintptr, resPtr uintptr, _ uintptr) {
122 if resPtr == 0 {
123 resultCh <- LoadResult{Err: fmt.Errorf("nil async result")}
124 return
125 }
126
127 res := &gio.AsyncResultBase{Ptr: resPtr}
128 filter, err := s.inner.LoadFinish(res)
129 resultCh <- LoadResult{Filter: filter, Err: err}
130 })
131
132 // Start async load
133 s.inner.Load(identifier, nil, &cb, 0)
134
135 // Wait for result or context cancellation
136 select {
137 case result := <-resultCh:
138 if result.Err != nil {
139 log.Debug().Err(result.Err).Msg("filter load failed (may not exist)")
140 return nil, result.Err
141 }
142 log.Debug().Msg("filter loaded successfully")
143 return result.Filter, nil
144 case <-ctx.Done():
145 log.Warn().Msg("filter load canceled")
146 return nil, ctx.Err()
147 }
148}
149
150// FetchIdentifiersResult holds the result of fetching filter identifiers.
151type FetchIdentifiersResult struct {
152 Identifiers []string
153 Err error
154}
155
156// FetchIdentifiers returns all stored filter identifiers.
157func (s *Store) FetchIdentifiers(ctx context.Context) ([]string, error) {
158 log := logging.FromContext(ctx).With().
159 Str("component", "filter-store").
160 Logger()
161
162 s.mu.Lock()
163 defer s.mu.Unlock()
164
165 log.Debug().Msg("fetching filter identifiers")
166
167 // Use a channel to receive the async result
168 resultCh := make(chan FetchIdentifiersResult, 1)
169
170 // Create async callback
171 cb := gio.AsyncReadyCallback(func(_ uintptr, resPtr uintptr, _ uintptr) {
172 if resPtr == 0 {
173 resultCh <- FetchIdentifiersResult{Err: fmt.Errorf("nil async result")}
174 return
175 }
176
177 res := &gio.AsyncResultBase{Ptr: resPtr}
178 identifiers := s.inner.FetchIdentifiersFinish(res)
179 resultCh <- FetchIdentifiersResult{Identifiers: identifiers}
180 })
181
182 // Start async fetch
183 s.inner.FetchIdentifiers(nil, &cb, 0)
184
185 // Wait for result or context cancellation
186 select {
187 case result := <-resultCh:
188 if result.Err != nil {
189 log.Error().Err(result.Err).Msg("failed to fetch identifiers")
190 return nil, result.Err
191 }
192 log.Debug().Int("count", len(result.Identifiers)).Msg("fetched filter identifiers")
193 return result.Identifiers, nil
194 case <-ctx.Done():
195 log.Warn().Msg("fetch identifiers canceled")
196 return nil, ctx.Err()
197 }
198}
199
200// Remove removes a compiled filter by its identifier.
201func (s *Store) Remove(ctx context.Context, identifier string) error {
202 log := logging.FromContext(ctx).With().
203 Str("component", "filter-store").
204 Str("identifier", identifier).
205 Logger()
206
207 s.mu.Lock()
208 defer s.mu.Unlock()
209
210 log.Debug().Msg("removing filter")
211
212 // Use a channel to receive the async result
213 resultCh := make(chan error, 1)
214
215 // Create async callback
216 cb := gio.AsyncReadyCallback(func(_ uintptr, resPtr uintptr, _ uintptr) {
217 if resPtr == 0 {
218 resultCh <- fmt.Errorf("nil async result")
219 return
220 }
221
222 res := &gio.AsyncResultBase{Ptr: resPtr}
223 _, err := s.inner.RemoveFinish(res)
224 resultCh <- err
225 })
226
227 // Start async remove
228 s.inner.Remove(identifier, nil, &cb, 0)
229
230 // Wait for result or context cancellation
231 select {
232 case err := <-resultCh:
233 if err != nil {
234 log.Error().Err(err).Msg("failed to remove filter")
235 return err
236 }
237 log.Info().Msg("filter removed")
238 return nil
239 case <-ctx.Done():
240 log.Warn().Msg("remove filter canceled")
241 return ctx.Err()
242 }
243}
244
245// HasCompiledFilter checks if a compiled filter exists for the given identifier.
246func (s *Store) HasCompiledFilter(ctx context.Context, identifier string) bool {
247 identifiers, err := s.FetchIdentifiers(ctx)
248 if err != nil {
249 return false
250 }
251 for _, id := range identifiers {
252 if id == identifier {
253 return true
254 }
255 }
256 return false
257}