Replace WTF::move with WTFMove
[WebKit-https.git] / Source / WebCore / contentextensions / ContentExtensionCompiler.cpp
1 /*
2  * Copyright (C) 2015 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #include "config.h"
27 #include "ContentExtensionCompiler.h"
28
29 #if ENABLE(CONTENT_EXTENSIONS)
30
31 #include "CombinedURLFilters.h"
32 #include "CompiledContentExtension.h"
33 #include "ContentExtensionActions.h"
34 #include "ContentExtensionError.h"
35 #include "ContentExtensionParser.h"
36 #include "ContentExtensionRule.h"
37 #include "ContentExtensionsDebugging.h"
38 #include "DFABytecodeCompiler.h"
39 #include "DFACombiner.h"
40 #include "NFA.h"
41 #include "NFAToDFA.h"
42 #include "URLFilterParser.h"
43 #include <wtf/CurrentTime.h>
44 #include <wtf/DataLog.h>
45 #include <wtf/text/CString.h>
46 #include <wtf/text/StringBuilder.h>
47
48 namespace WebCore {
49 namespace ContentExtensions {
50
51 static void serializeSelector(Vector<SerializedActionByte>& actions, const String& selector)
52 {
53     // Append action type (1 byte).
54     actions.append(static_cast<SerializedActionByte>(ActionType::CSSDisplayNoneSelector));
55     // Append Selector length (4 bytes).
56     unsigned selectorLength = selector.length();
57     actions.resize(actions.size() + sizeof(unsigned));
58     *reinterpret_cast<unsigned*>(&actions[actions.size() - sizeof(unsigned)]) = selectorLength;
59     bool wideCharacters = !selector.is8Bit();
60     actions.append(wideCharacters);
61     // Append Selector.
62     if (wideCharacters) {
63         unsigned startIndex = actions.size();
64         actions.resize(actions.size() + sizeof(UChar) * selectorLength);
65         for (unsigned i = 0; i < selectorLength; ++i)
66             *reinterpret_cast<UChar*>(&actions[startIndex + i * sizeof(UChar)]) = selector[i];
67     } else {
68         for (unsigned i = 0; i < selectorLength; ++i)
69             actions.append(selector[i]);
70     }
71 }
72
73 struct PendingDisplayNoneActions {
74     Vector<String> selectors;
75     Vector<unsigned> clientLocations;
76 };
77 typedef HashMap<Trigger, PendingDisplayNoneActions, TriggerHash, TriggerHashTraits> PendingDisplayNoneActionsMap;
78
79 static void resolvePendingDisplayNoneActions(Vector<SerializedActionByte>& actions, Vector<unsigned>& actionLocations, PendingDisplayNoneActionsMap& pendingDisplayNoneActionsMap)
80 {
81     for (auto& slot : pendingDisplayNoneActionsMap) {
82         PendingDisplayNoneActions& pendingActions = slot.value;
83
84         StringBuilder combinedSelectors;
85         for (unsigned i = 0; i < pendingActions.selectors.size(); ++i) {
86             if (i)
87                 combinedSelectors.append(',');
88             combinedSelectors.append(pendingActions.selectors[i]);
89         }
90
91         unsigned actionLocation = actions.size();
92         serializeSelector(actions, combinedSelectors.toString());
93         for (unsigned clientLocation : pendingActions.clientLocations)
94             actionLocations[clientLocation] = actionLocation;
95     }
96     pendingDisplayNoneActionsMap.clear();
97 }
98
99 static Vector<unsigned> serializeActions(const Vector<ContentExtensionRule>& ruleList, Vector<SerializedActionByte>& actions)
100 {
101     ASSERT(!actions.size());
102
103     Vector<unsigned> actionLocations;
104
105     // Order only matters because of IgnorePreviousRules. All other identical actions can be combined between each IgnorePreviousRules
106     // and CSSDisplayNone strings can be combined if their triggers are identical.
107     typedef HashMap<uint32_t, uint32_t, DefaultHash<uint32_t>::Hash, WTF::UnsignedWithZeroKeyHashTraits<uint32_t>> ActionMap;
108     ActionMap blockLoadActionsMap;
109     ActionMap blockCookiesActionsMap;
110     PendingDisplayNoneActionsMap cssDisplayNoneActionsMap;
111     ActionMap ignorePreviousRuleActionsMap;
112     ActionMap makeHTTPSActionsMap;
113
114     for (unsigned ruleIndex = 0; ruleIndex < ruleList.size(); ++ruleIndex) {
115         const ContentExtensionRule& rule = ruleList[ruleIndex];
116         ActionType actionType = rule.action().type();
117
118         if (actionType == ActionType::IgnorePreviousRules) {
119             resolvePendingDisplayNoneActions(actions, actionLocations, cssDisplayNoneActionsMap);
120
121             blockLoadActionsMap.clear();
122             blockCookiesActionsMap.clear();
123             cssDisplayNoneActionsMap.clear();
124             makeHTTPSActionsMap.clear();
125         } else
126             ignorePreviousRuleActionsMap.clear();
127
128         // Anything with domain is just pushed.
129         // We could try to merge domains but that case is not common in practice.
130         if (!rule.trigger().domains.isEmpty()) {
131             actionLocations.append(actions.size());
132
133             if (actionType == ActionType::CSSDisplayNoneSelector)
134                 serializeSelector(actions, rule.action().stringArgument());
135             else
136                 actions.append(static_cast<SerializedActionByte>(actionType));
137             continue;
138         }
139
140         ResourceFlags flags = rule.trigger().flags;
141         unsigned actionLocation = std::numeric_limits<unsigned>::max();
142         
143         auto findOrMakeActionLocation = [&] (ActionMap& map) 
144         {
145             const auto existingAction = map.find(flags);
146             if (existingAction == map.end()) {
147                 actionLocation = actions.size();
148                 actions.append(static_cast<SerializedActionByte>(actionType));
149                 map.set(flags, actionLocation);
150             } else
151                 actionLocation = existingAction->value;
152         };
153
154         switch (actionType) {
155         case ActionType::CSSDisplayNoneStyleSheet:
156         case ActionType::InvalidAction:
157             RELEASE_ASSERT_NOT_REACHED();
158
159         case ActionType::CSSDisplayNoneSelector: {
160             const auto addResult = cssDisplayNoneActionsMap.add(rule.trigger(), PendingDisplayNoneActions());
161             PendingDisplayNoneActions& pendingDisplayNoneActions = addResult.iterator->value;
162             pendingDisplayNoneActions.selectors.append(rule.action().stringArgument());
163             pendingDisplayNoneActions.clientLocations.append(actionLocations.size());
164
165             actionLocation = std::numeric_limits<unsigned>::max();
166             break;
167         }
168         case ActionType::IgnorePreviousRules:
169             findOrMakeActionLocation(ignorePreviousRuleActionsMap);
170             break;
171         case ActionType::BlockLoad:
172             findOrMakeActionLocation(blockLoadActionsMap);
173             break;
174         case ActionType::BlockCookies:
175             findOrMakeActionLocation(blockCookiesActionsMap);
176             break;
177         case ActionType::MakeHTTPS:
178             findOrMakeActionLocation(makeHTTPSActionsMap);
179             break;
180         }
181
182         actionLocations.append(actionLocation);
183     }
184     resolvePendingDisplayNoneActions(actions, actionLocations, cssDisplayNoneActionsMap);
185     return actionLocations;
186 }
187
188 typedef HashSet<uint64_t, DefaultHash<uint64_t>::Hash, WTF::UnsignedWithZeroKeyHashTraits<uint64_t>> UniversalActionSet;
189
190 static void addUniversalActionsToDFA(DFA& dfa, const UniversalActionSet& universalActions)
191 {
192     if (universalActions.isEmpty())
193         return;
194
195     DFANode& root = dfa.nodes[dfa.root];
196     ASSERT(!root.actionsLength());
197     unsigned actionsStart = dfa.actions.size();
198     dfa.actions.reserveCapacity(dfa.actions.size() + universalActions.size());
199     for (uint64_t action : universalActions)
200         dfa.actions.uncheckedAppend(action);
201     unsigned actionsEnd = dfa.actions.size();
202
203     unsigned actionsLength = actionsEnd - actionsStart;
204     RELEASE_ASSERT_WITH_MESSAGE(actionsLength < std::numeric_limits<uint16_t>::max(), "Too many uncombined actions that match everything");
205     root.setActions(actionsStart, static_cast<uint16_t>(actionsLength));
206 }
207
208 std::error_code compileRuleList(ContentExtensionCompilationClient& client, String&& ruleList)
209 {
210     Vector<ContentExtensionRule> parsedRuleList;
211     auto parserError = parseRuleList(ruleList, parsedRuleList);
212     ruleList = String();
213     if (parserError)
214         return parserError;
215
216 #if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING
217     double patternPartitioningStart = monotonicallyIncreasingTime();
218 #endif
219
220     Vector<SerializedActionByte> actions;
221     Vector<unsigned> actionLocations = serializeActions(parsedRuleList, actions);
222     client.writeActions(WTFMove(actions));
223     LOG_LARGE_STRUCTURES(actions, actions.capacity() * sizeof(SerializedActionByte));
224     actions.clear();
225
226     UniversalActionSet universalActionsWithoutDomains;
227     UniversalActionSet universalActionsWithDomains;
228
229     // FIXME: These don't all need to be in memory at the same time.
230     CombinedURLFilters filtersWithoutDomains;
231     CombinedURLFilters filtersWithDomains;
232     CombinedURLFilters domainFilters;
233     URLFilterParser filtersWithoutDomainParser(filtersWithoutDomains);
234     URLFilterParser filtersWithDomainParser(filtersWithDomains);
235     
236     for (unsigned ruleIndex = 0; ruleIndex < parsedRuleList.size(); ++ruleIndex) {
237         const ContentExtensionRule& contentExtensionRule = parsedRuleList[ruleIndex];
238         const Trigger& trigger = contentExtensionRule.trigger();
239         ASSERT(trigger.urlFilter.length());
240
241         // High bits are used for flags. This should match how they are used in DFABytecodeCompiler::compileNode.
242         ASSERT(!trigger.flags || ActionFlagMask & (static_cast<uint64_t>(trigger.flags) << 32));
243         ASSERT(!(~ActionFlagMask & (static_cast<uint64_t>(trigger.flags) << 32)));
244         uint64_t actionLocationAndFlags = (static_cast<uint64_t>(trigger.flags) << 32) | static_cast<uint64_t>(actionLocations[ruleIndex]);
245         URLFilterParser::ParseStatus status = URLFilterParser::Ok;
246         if (trigger.domains.isEmpty()) {
247             ASSERT(trigger.domainCondition == Trigger::DomainCondition::None);
248             status = filtersWithoutDomainParser.addPattern(trigger.urlFilter, trigger.urlFilterIsCaseSensitive, actionLocationAndFlags);
249             if (status == URLFilterParser::MatchesEverything) {
250                 universalActionsWithoutDomains.add(actionLocationAndFlags);
251                 status = URLFilterParser::Ok;
252             }
253             if (status != URLFilterParser::Ok) {
254                 dataLogF("Error while parsing %s: %s\n", trigger.urlFilter.utf8().data(), URLFilterParser::statusString(status).utf8().data());
255                 return ContentExtensionError::JSONInvalidRegex;
256             }
257         } else {
258             if (trigger.domainCondition == Trigger::DomainCondition::IfDomain)
259                 actionLocationAndFlags |= IfDomainFlag;
260             else {
261                 ASSERT(trigger.domainCondition == Trigger::DomainCondition::UnlessDomain);
262                 ASSERT(!(actionLocationAndFlags & IfDomainFlag));
263             }
264             
265             status = filtersWithDomainParser.addPattern(trigger.urlFilter, trigger.urlFilterIsCaseSensitive, actionLocationAndFlags);
266             if (status == URLFilterParser::MatchesEverything) {
267                 universalActionsWithDomains.add(actionLocationAndFlags);
268                 status = URLFilterParser::Ok;
269             }
270             if (status != URLFilterParser::Ok) {
271                 dataLogF("Error while parsing %s: %s\n", trigger.urlFilter.utf8().data(), URLFilterParser::statusString(status).utf8().data());
272                 return ContentExtensionError::JSONInvalidRegex;
273             }
274             for (const String& domain : trigger.domains)
275                 domainFilters.addDomain(actionLocationAndFlags, domain);
276         }
277         ASSERT(status == URLFilterParser::Ok);
278     }
279     LOG_LARGE_STRUCTURES(parsedRuleList, parsedRuleList.capacity() * sizeof(ContentExtensionRule)); // Doesn't include strings.
280     LOG_LARGE_STRUCTURES(actionLocations, actionLocations.capacity() * sizeof(unsigned));
281     parsedRuleList.clear();
282     actionLocations.clear();
283
284 #if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING
285     double patternPartitioningEnd = monotonicallyIncreasingTime();
286     dataLogF("    Time spent partitioning the rules into groups: %f\n", (patternPartitioningEnd - patternPartitioningStart));
287 #endif
288
289     LOG_LARGE_STRUCTURES(filtersWithoutDomains, filtersWithoutDomains.memoryUsed());
290     LOG_LARGE_STRUCTURES(filtersWithDomains, filtersWithDomains.memoryUsed());
291     LOG_LARGE_STRUCTURES(domainFilters, domainFilters.memoryUsed());
292
293 #if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING
294     unsigned machinesWithoutDomainsCount = 0;
295     unsigned totalBytecodeSizeForMachinesWithoutDomains = 0;
296     unsigned machinesWithDomainsCount = 0;
297     unsigned totalBytecodeSizeForMachinesWithDomains = 0;
298     double totalNFAToByteCodeBuildTimeStart = monotonicallyIncreasingTime();
299 #endif
300
301     // Smaller maxNFASizes risk high compiling and interpreting times from having too many DFAs,
302     // larger maxNFASizes use too much memory when compiling.
303     const unsigned maxNFASize = 75000;
304     
305     bool firstNFAWithoutDomainsSeen = false;
306
307     auto lowerFiltersWithoutDomainsDFAToBytecode = [&](DFA&& dfa)
308     {
309 #if CONTENT_EXTENSIONS_STATE_MACHINE_DEBUGGING
310         dataLogF("filtersWithoutDomains DFA\n");
311         dfa.debugPrintDot();
312 #endif
313         ASSERT_WITH_MESSAGE(!dfa.nodes[dfa.root].hasActions(), "All actions on the DFA root should come from regular expressions that match everything.");
314
315         if (!firstNFAWithoutDomainsSeen) {
316             // Put all the universal actions on the first DFA.
317             addUniversalActionsToDFA(dfa, universalActionsWithoutDomains);
318         }
319
320         Vector<DFABytecode> bytecode;
321         DFABytecodeCompiler compiler(dfa, bytecode);
322         compiler.compile();
323         LOG_LARGE_STRUCTURES(bytecode, bytecode.capacity() * sizeof(uint8_t));
324 #if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING
325         ++machinesWithoutDomainsCount;
326         totalBytecodeSizeForMachinesWithoutDomains += bytecode.size();
327 #endif
328         client.writeFiltersWithoutDomainsBytecode(WTFMove(bytecode));
329
330         firstNFAWithoutDomainsSeen = true;
331     };
332
333     const unsigned smallDFASize = 100;
334     DFACombiner smallFiltersWithoutDomainsDFACombiner;
335     filtersWithoutDomains.processNFAs(maxNFASize, [&](NFA&& nfa) {
336 #if CONTENT_EXTENSIONS_STATE_MACHINE_DEBUGGING
337         dataLogF("filtersWithoutDomains NFA\n");
338         nfa.debugPrintDot();
339 #endif
340
341         LOG_LARGE_STRUCTURES(nfa, nfa.memoryUsed());
342         DFA dfa = NFAToDFA::convert(nfa);
343         LOG_LARGE_STRUCTURES(dfa, dfa.memoryUsed());
344
345         if (dfa.graphSize() < smallDFASize)
346             smallFiltersWithoutDomainsDFACombiner.addDFA(WTFMove(dfa));
347         else {
348             dfa.minimize();
349             lowerFiltersWithoutDomainsDFAToBytecode(WTFMove(dfa));
350         }
351     });
352
353
354     smallFiltersWithoutDomainsDFACombiner.combineDFAs(smallDFASize, [&](DFA&& dfa) {
355         LOG_LARGE_STRUCTURES(dfa, dfa.memoryUsed());
356         lowerFiltersWithoutDomainsDFAToBytecode(WTFMove(dfa));
357     });
358
359     ASSERT(filtersWithoutDomains.isEmpty());
360
361     if (!firstNFAWithoutDomainsSeen) {
362         // Our bytecode interpreter expects to have at least one DFA, so if we haven't seen any
363         // create a dummy one and add any universal actions.
364
365         DFA dummyDFA = DFA::empty();
366         addUniversalActionsToDFA(dummyDFA, universalActionsWithoutDomains);
367
368         Vector<DFABytecode> bytecode;
369         DFABytecodeCompiler compiler(dummyDFA, bytecode);
370         compiler.compile();
371         LOG_LARGE_STRUCTURES(bytecode, bytecode.capacity() * sizeof(uint8_t));
372         client.writeFiltersWithoutDomainsBytecode(WTFMove(bytecode));
373     }
374     LOG_LARGE_STRUCTURES(universalActionsWithoutDomains, universalActionsWithoutDomains.capacity() * sizeof(unsigned));
375     universalActionsWithoutDomains.clear();
376     
377     bool firstNFAWithDomainsSeen = false;
378     auto lowerFiltersWithDomainsDFAToBytecode = [&](DFA&& dfa)
379     {
380         if (!firstNFAWithDomainsSeen) {
381             // Put all the universal actions on the first DFA.
382             addUniversalActionsToDFA(dfa, universalActionsWithDomains);
383         }
384
385         Vector<DFABytecode> bytecode;
386         DFABytecodeCompiler compiler(dfa, bytecode);
387         compiler.compile();
388         LOG_LARGE_STRUCTURES(bytecode, bytecode.capacity() * sizeof(uint8_t));
389 #if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING
390         ++machinesWithDomainsCount;
391         totalBytecodeSizeForMachinesWithDomains += bytecode.size();
392 #endif
393         client.writeFiltersWithDomainsBytecode(WTFMove(bytecode));
394
395         firstNFAWithDomainsSeen = true;
396     };
397
398     DFACombiner smallFiltersWithDomainsDFACombiner;
399     filtersWithDomains.processNFAs(maxNFASize, [&](NFA&& nfa) {
400 #if CONTENT_EXTENSIONS_STATE_MACHINE_DEBUGGING
401         dataLogF("filtersWithDomains NFA\n");
402         nfa.debugPrintDot();
403 #endif
404         LOG_LARGE_STRUCTURES(nfa, nfa.memoryUsed());
405         DFA dfa = NFAToDFA::convert(nfa);
406 #if CONTENT_EXTENSIONS_STATE_MACHINE_DEBUGGING
407         dataLogF("filtersWithDomains PRE MINIMIZING DFA\n");
408         dfa.debugPrintDot();
409 #endif
410         LOG_LARGE_STRUCTURES(dfa, dfa.memoryUsed());
411
412         ASSERT_WITH_MESSAGE(!dfa.nodes[dfa.root].hasActions(), "Filters with domains that match everything are not allowed right now.");
413
414         if (dfa.graphSize() < smallDFASize)
415             smallFiltersWithDomainsDFACombiner.addDFA(WTFMove(dfa));
416         else {
417             dfa.minimize();
418             lowerFiltersWithDomainsDFAToBytecode(WTFMove(dfa));
419         }
420     });
421     smallFiltersWithDomainsDFACombiner.combineDFAs(smallDFASize, [&](DFA&& dfa) {
422         LOG_LARGE_STRUCTURES(dfa, dfa.memoryUsed());
423         lowerFiltersWithDomainsDFAToBytecode(WTFMove(dfa));
424     });
425     ASSERT(filtersWithDomains.isEmpty());
426     
427     if (!firstNFAWithDomainsSeen) {
428         // Our bytecode interpreter expects to have at least one DFA, so if we haven't seen any
429         // create a dummy one and add any universal actions.
430
431         DFA dummyDFA = DFA::empty();
432         addUniversalActionsToDFA(dummyDFA, universalActionsWithDomains);
433         
434         Vector<DFABytecode> bytecode;
435         DFABytecodeCompiler compiler(dummyDFA, bytecode);
436         compiler.compile();
437         LOG_LARGE_STRUCTURES(bytecode, bytecode.capacity() * sizeof(uint8_t));
438         client.writeFiltersWithDomainsBytecode(WTFMove(bytecode));
439     }
440     LOG_LARGE_STRUCTURES(universalActionsWithDomains, universalActionsWithDomains.capacity() * sizeof(unsigned));
441     universalActionsWithDomains.clear();
442
443     domainFilters.processNFAs(maxNFASize, [&](NFA&& nfa) {
444 #if CONTENT_EXTENSIONS_STATE_MACHINE_DEBUGGING
445         dataLogF("domainFilters NFA\n");
446         nfa.debugPrintDot();
447 #endif
448         LOG_LARGE_STRUCTURES(nfa, nfa.memoryUsed());
449         DFA dfa = NFAToDFA::convert(nfa);
450 #if CONTENT_EXTENSIONS_STATE_MACHINE_DEBUGGING
451         dataLogF("domainFilters DFA\n");
452         dfa.debugPrintDot();
453 #endif
454         LOG_LARGE_STRUCTURES(dfa, dfa.memoryUsed());
455         // Minimizing this DFA would not be effective because all actions are unique
456         // and because of the tree-like structure of this DFA.
457         ASSERT_WITH_MESSAGE(!dfa.nodes[dfa.root].hasActions(), "There should not be any domains that match everything.");
458
459         Vector<DFABytecode> bytecode;
460         DFABytecodeCompiler compiler(dfa, bytecode);
461         compiler.compile();
462         LOG_LARGE_STRUCTURES(bytecode, bytecode.capacity() * sizeof(uint8_t));
463         client.writeDomainFiltersBytecode(WTFMove(bytecode));
464     });
465     ASSERT(domainFilters.isEmpty());    
466     
467 #if CONTENT_EXTENSIONS_PERFORMANCE_REPORTING
468     double totalNFAToByteCodeBuildTimeEnd = monotonicallyIncreasingTime();
469     dataLogF("    Time spent building and compiling the DFAs: %f\n", (totalNFAToByteCodeBuildTimeEnd - totalNFAToByteCodeBuildTimeStart));
470
471     dataLogF("    Number of machines without domain filters: %d (total bytecode size = %d)\n", machinesWithoutDomainsCount, totalBytecodeSizeForMachinesWithoutDomains);
472     dataLogF("    Number of machines with domain filters: %d (total bytecode size = %d)\n", machinesWithDomainsCount, totalBytecodeSizeForMachinesWithDomains);
473 #endif
474
475     client.finalize();
476
477     return { };
478 }
479
480 } // namespace ContentExtensions
481 } // namespace WebCore
482
483 #endif // ENABLE(CONTENT_EXTENSIONS)