Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Commit

Permalink
Add filter list debugging tools
Browse files Browse the repository at this point in the history
  • Loading branch information
cuba committed Oct 18, 2023
1 parent 2e085bc commit 715c1ca
Show file tree
Hide file tree
Showing 10 changed files with 560 additions and 21 deletions.
8 changes: 8 additions & 0 deletions Sources/Brave/Frontend/Settings/SettingsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,14 @@ class SettingsViewController: TableViewController {
let vc = AdblockDebugMenuTableViewController(style: .grouped)
self.navigationController?.pushViewController(vc, animated: true)
}, accessory: .disclosureIndicator, cellClass: MultilineValue1Cell.self),
Row(
text: "Debug Filter lists",
selection: { [unowned self] in
if #available(iOS 16.0, *) {
let vc = UIHostingController(rootView: FilterListToolsView())
self.navigationController?.present(vc, animated: true)
}
}, accessory: .disclosureIndicator, cellClass: MultilineValue1Cell.self),
Row(
text: "View URP Logs",
selection: { [unowned self] in
Expand Down
9 changes: 3 additions & 6 deletions Sources/Brave/WebFilters/AdblockResourceDownloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,17 +139,14 @@ public actor AdblockResourceDownloader: Sendable {
guard !modes.isEmpty else { return }

do {
guard let filterSet = try resource.downloadedString() else {
guard let fileURL = resource.downloadedFileURL else {
assertionFailure("This file was downloaded successfully so it should not be nil")
return
}

let result = try AdblockEngine.contentBlockerRules(fromFilterSet: filterSet)

// try to compile
try await ContentBlockerManager.shared.compile(
encodedContentRuleList: result.rulesJSON, for: blocklistType,
modes: modes
try await ContentBlockerManager.shared.compileRuleList(
savedTo: fileURL, for: blocklistType, modes: modes
)
} catch {
ContentBlockerManager.log.error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Data
import Shared
import Preferences
import BraveShields
import BraveCore
import os.log

/// A class that aids in the managment of rule lists on the rule store.
Expand Down Expand Up @@ -83,6 +84,22 @@ actor ContentBlockerManager {
case filterList(componentId: String, isAlwaysAggressive: Bool)
case customFilterList(uuid: String)

var engineSource: CachedAdBlockEngine.Source? {
switch self {
case .generic(let genericBlocklistType):
switch genericBlocklistType {
case .blockAds:
return .adBlock
case .blockCookies, .blockTrackers:
return nil
}
case .filterList(let componentId, _):
return .filterList(componentId: componentId)
case .customFilterList(let uuid):
return .filterListURL(uuid: uuid)
}
}

private var identifier: String {
switch self {
case .generic(let type):
Expand Down Expand Up @@ -169,6 +186,13 @@ actor ContentBlockerManager {
}
}

/// Compile the rule list found in the given local URL using the specified modes
func compileRuleList(savedTo fileURL: URL, for type: BlocklistType, options: CompileOptions = [], modes: [BlockingMode]) async throws {
let filterSet = try loadRuleList(savedTo: fileURL, for: type)
let result = try AdblockEngine.contentBlockerRules(fromFilterSet: filterSet)
try await compile(encodedContentRuleList: result.rulesJSON, for: type, options: options, modes: modes)
}

/// Compile the given resource and store it in cache for the given blocklist type using all allowed modes
func compile(encodedContentRuleList: String, for type: BlocklistType, options: CompileOptions = []) async throws {
try await self.compile(encodedContentRuleList: encodedContentRuleList, for: type, modes: type.allowedModes)
Expand Down Expand Up @@ -210,6 +234,16 @@ actor ContentBlockerManager {
}
}

/// Load the rule list from file and attach any additional debug rules
private func loadRuleList(savedTo fileURL: URL, for type: BlocklistType) throws -> String {
if let source = type.engineSource, let additionalRules = try source.loadAdditionalRules() {
let file = try String(contentsOf: fileURL, encoding: .utf8)
return [file, additionalRules].joined(separator: "\n")
} else {
return try String(contentsOf: fileURL, encoding: .utf8)
}
}

private func set(mode: BlockingMode, forRuleList ruleList: [[String: Any?]]) -> [[String: Any?]] {
guard let lastRule = ruleList.last else { return ruleList }

Expand Down
254 changes: 254 additions & 0 deletions Sources/Brave/WebFilters/Debug/FilterListDetailsToolsView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
//
// SwiftUIView.swift
//
//
// Created by Jacob on 2023-10-16.
//

import SwiftUI
import BraveCore

@available(iOS 16.0, *)
struct FilterListDetailsToolsView: View {
struct FilterListDetails {
let source: CachedAdBlockEngine.Source
let title: String
let externalURL: String?
let lazyInfo: AdBlockStats.LazyFilterListInfo?
var compiledInfo: CachedAdBlockEngine.FilterListInfo?
var compiledResource: CachedAdBlockEngine.ResourcesInfo?
var customRules: String
}

@Binding var details: FilterListDetails
@State var rulesError: Error?
@State var isRecompiled = false
@State var isCopied = false
@State var isResourceCopied = false
@Binding var customRules: String

var isSaved: Bool {
return details.customRules == customRules
}

var body: some View {
List {
if let lazyInfo = details.lazyInfo {
Section("File") {
VStack(alignment: .leading) {
HStack {
Label("Rules", systemImage: "doc.plaintext")
HStack {
Text("v") + Text(lazyInfo.filterListInfo.version)
}.foregroundStyle(.secondary)

Spacer()

if isCopied {
Image(systemName: "checkmark")
.foregroundStyle(.green)
} else {
Button("Copy", systemImage: "doc.on.clipboard") {
UIPasteboard.general.string = lazyInfo.filterListInfo.localFileURL.path(percentEncoded: false)
isCopied = true

Task {
try await Task.sleep(for: .seconds(2))
isCopied = false
}
}
.labelStyle(.iconOnly)
.buttonStyle(.borderedProminent)
.foregroundStyle(.white)
.font(.caption)
}
}

Text(lazyInfo.filterListInfo.localFileURL.path(percentEncoded: false))
.foregroundStyle(.secondary)
.font(.caption)
}
}
}

if let compiledInfo = details.compiledInfo {
Section("Engine") {
HStack {
Label("Version", systemImage: "hammer")
HStack {
Text("v") + Text(compiledInfo.version)
}.foregroundStyle(.secondary)
}

if let version = details.compiledResource?.version {
HStack {
Label("Resources", systemImage: "doc.plaintext")
HStack {
Text("v") + Text(version)
}.foregroundStyle(.secondary)
}
}
}
}

if details.lazyInfo?.filterListInfo.fileType == .text {
Section {
TextEditor(text: $customRules)
.frame(height: 300)
if let error = rulesError {
Text(String(describing: error))
.foregroundStyle(.red)
}
} header: {
HStack {
Text("Additional rules")
.frame(maxWidth: .infinity, alignment: .leading)
if isSaved {
Image(systemName: "checkmark")
.foregroundStyle(.green)
} else {
Button("Save", systemImage: "arrow.down.doc") {
saveCustomRules(for: details.source)
}
.buttonStyle(.borderedProminent)
.foregroundStyle(.white)
.font(.caption)
}
}
}
}
}
.toolbar {
ToolbarItem(placement: .automatic) {
Button("Load", systemImage: "hammer") {
guard let lazyInfo = details.lazyInfo else { return }
saveCustomRules(for: details.source)
isRecompiled = true

Task {
guard let resourcesInfo = await AdBlockStats.shared.resourcesInfo else { return }

await AdBlockStats.shared.compile(
filterListInfo: lazyInfo.filterListInfo,
resourcesInfo: resourcesInfo,
isAlwaysAggressive: lazyInfo.isAlwaysAggressive,
force: true
)

details.compiledInfo = await AdBlockStats.shared.cachedEngines[details.source]?.filterListInfo
details.compiledResource = await AdBlockStats.shared.cachedEngines[details.source]?.resourcesInfo

try await Task.sleep(for: .seconds(2))
isRecompiled = false
}
}
.buttonStyle(.borderedProminent)
.foregroundStyle(.white)
.font(.caption)
}
}
.onChange(of: details.source, perform: { value in
self.isCopied = false
self.isRecompiled = false
})
.navigationTitle(details.title)
.onDisappear(perform: {
saveCustomRules(for: details.source)
})
}

private func saveCustomRules(for source: CachedAdBlockEngine.Source) {
do {
if !customRules.isEmpty {
_ = try AdblockEngine(rules: customRules)
try source.save(additionalRules: customRules)
} else {
try source.deleteAdditionalRules()
}

details.customRules = customRules
} catch {
self.rulesError = error
}
}

}

#if swift(>=5.9)
@available(iOS 16.0, *)
#Preview {
FilterListDetailsToolsView(
details: .constant(FilterListDetailsToolsView.FilterListDetails(
source: .adBlock, title: "AdBlock", externalURL: nil, lazyInfo: nil,
compiledInfo: nil, compiledResource: nil, customRules: ""
)),
customRules: .constant("")
)
}
#endif

extension CachedAdBlockEngine.Source {
/// The directory to which we should store our debug files into
private static var debugFolderDirectory: FileManager.SearchPathDirectory {
return FileManager.SearchPathDirectory.applicationSupportDirectory
}

/// The file to save additional rules to
private var debugFolderName: String {
switch self {
case .adBlock: return "debug/ad-block"
case .filterListURL(let uuid): return "debug/\(uuid)"
case .filterList(let componentId): return "debug/\(componentId)"
}
}

private var debugFolderURL: URL? {
guard let folderURL = Self.debugFolderDirectory.url else { return nil }
return folderURL.appendingPathComponent(debugFolderName)
}

/// Get the cache folder for this resource
///
/// - Note: Returns nil if the cache folder does not exist
private var createdDebugFolderURL: URL? {
guard let folderURL = debugFolderURL else { return nil }

if FileManager.default.fileExists(atPath: folderURL.path) {
return folderURL
} else {
return nil
}
}

func loadAdditionalRules() throws -> String? {
guard let folderURL = createdDebugFolderURL else { return nil }
let fileURL = folderURL.appendingPathComponent("additional-rules", conformingTo: .text)
guard FileManager.default.fileExists(atPath: fileURL.path) else { return nil }
return try String(contentsOf: fileURL, encoding: .utf8)
}

func save(additionalRules: String) throws {
let folderURL = try getOrCreateDebugFolder()
let fileURL = folderURL.appendingPathComponent("additional-rules", conformingTo: .text)
try additionalRules.write(to: fileURL, atomically: true, encoding: .utf8)
}

func deleteAdditionalRules() throws {
guard let folderURL = createdDebugFolderURL else { return }
let fileURL = folderURL.appendingPathComponent("additional-rules", conformingTo: .text)
guard FileManager.default.fileExists(atPath: fileURL.path) else { return }
try FileManager.default.removeItem(at: fileURL)
}

/// Get or create a debug folder for this filter list
private func getOrCreateDebugFolder() throws -> URL {
guard let folderURL = FileManager.default.getOrCreateFolder(
name: debugFolderName,
location: Self.debugFolderDirectory
) else {
throw ResourceFileError.failedToCreateCacheFolder
}

return folderURL
}
}
Loading

0 comments on commit 715c1ca

Please sign in to comment.