Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ brave-shields-cli filters add "||example.com^"
brave-shields-cli filters add "example.com##h1"
brave-shields-cli filters remove "||example.com^"
brave-shields-cli filters clear
brave-shields-cli filters load rules.txt
```

#### profiles
Expand Down
33 changes: 33 additions & 0 deletions src/commands/filters.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::fs;
use std::path::Path;

use anyhow::{Context, Result};
Expand Down Expand Up @@ -97,6 +98,38 @@ pub fn run_remove(brave_dir: &Path, filter: &str, channel: Channel, force: bool)
Ok(())
}

pub fn run_load(brave_dir: &Path, file_path: &str, channel: Channel, force: bool) -> Result<()> {
let content = fs::read_to_string(file_path)
.with_context(|| format!("failed to read filter file: {}", file_path))?;
let rules: Vec<&str> = content
.lines()
.filter(|line| !line.trim().is_empty())
.collect();

let ls_path = brave_dir.join("Local State");
preferences::check_brave_not_running(channel, force)?;

write_all_filters(&ls_path, &rules.join("\n"))?;

eprintln!("Loaded {} filter rules from {}", rules.len(), file_path);
Ok(())
}

/// Write an arbitrary string as the full set of custom filters.
pub fn write_all_filters(ls_path: &Path, filters: &str) -> Result<()> {
local_state::locked_read_modify_write(ls_path, |state| {
if !state.get("brave").is_some_and(|v| v.is_object()) {
state["brave"] = serde_json::json!({});
}
if !state["brave"].get("ad_block").is_some_and(|v| v.is_object()) {
state["brave"]["ad_block"] = serde_json::json!({});
}
state["brave"]["ad_block"]["custom_filters"] =
serde_json::Value::String(filters.to_string());
})
.context("failed to write custom filters")
}

pub fn run_clear(brave_dir: &Path, channel: Channel, force: bool) -> Result<()> {
let ls_path = brave_dir.join("Local State");
preferences::check_brave_not_running(channel, force)?;
Expand Down
8 changes: 8 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ enum FiltersAction {
},
/// Remove all custom filter rules
Clear,
/// Load custom filter rules from a file (replaces existing filters)
Load {
/// Path to a file containing filter rules (one per line)
file: String,
},
}

fn main() -> Result<()> {
Expand All @@ -121,6 +126,9 @@ fn main() -> Result<()> {
commands::filters::run_remove(&brave_dir, rule, channel, cli.force)?
}
FiltersAction::Clear => commands::filters::run_clear(&brave_dir, channel, cli.force)?,
FiltersAction::Load { file } => {
commands::filters::run_load(&brave_dir, file, channel, cli.force)?
}
}
return Ok(());
}
Expand Down
92 changes: 92 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -792,3 +792,95 @@ fn test_set_then_get_with_pattern() {
let output: Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(output["settings"]["shields"], "off");
}

// ==================== FILTERS LOAD command tests ====================

fn write_rules_file(dir: &Path, rules: &[&str]) -> std::path::PathBuf {
let path = dir.join("rules.txt");
fs::write(&path, rules.join("\n")).unwrap();
path
}

#[test]
fn test_filters_load() {
let tmp = setup_fixture(&json!({}));
let rules_path = write_rules_file(tmp.path(), &["||example.com^", "example.com##h1"]);
let (_, _, success) = run_cmd(
&["filters", "load", rules_path.to_str().unwrap()],
tmp.path(),
);
assert!(success);
let ls = read_local_state(tmp.path());
let filters = ls["brave"]["ad_block"]["custom_filters"].as_str().unwrap();
assert!(filters.contains("||example.com^"));
assert!(filters.contains("example.com##h1"));
}

#[test]
fn test_filters_load_replaces_existing() {
let ls = local_state_with_filters("||old.com^");
let tmp = setup_fixture_with_local_state(&json!({}), &ls);
let rules_path = write_rules_file(tmp.path(), &["||new.com^"]);
let (_, _, success) = run_cmd(
&["filters", "load", rules_path.to_str().unwrap()],
tmp.path(),
);
assert!(success);
let ls = read_local_state(tmp.path());
let filters = ls["brave"]["ad_block"]["custom_filters"].as_str().unwrap();
assert!(!filters.contains("||old.com^"));
assert!(filters.contains("||new.com^"));
}

#[test]
fn test_filters_load_skips_blank_lines() {
let tmp = setup_fixture(&json!({}));
let rules_path = write_rules_file(tmp.path(), &["||a.com^", "", "||b.com^", "", ""]);
let (_, _, success) = run_cmd(
&["filters", "load", rules_path.to_str().unwrap()],
tmp.path(),
);
assert!(success);
let (stdout, _, _) = run_cmd(&["filters", "list"], tmp.path());
let output: Value = serde_json::from_str(&stdout).unwrap();
let filters = output["custom_filters"].as_array().unwrap();
assert_eq!(filters.len(), 2);
}

#[test]
fn test_filters_load_then_list() {
let tmp = setup_fixture(&json!({}));
let rules_path = write_rules_file(tmp.path(), &["||a.com^", "||b.com^", "||c.com^"]);
run_cmd(
&["filters", "load", rules_path.to_str().unwrap()],
tmp.path(),
);
let (stdout, _, success) = run_cmd(&["filters", "list"], tmp.path());
assert!(success);
let output: Value = serde_json::from_str(&stdout).unwrap();
let filters = output["custom_filters"].as_array().unwrap();
assert_eq!(filters.len(), 3);
}

#[test]
fn test_filters_load_nonexistent_file() {
let tmp = setup_fixture(&json!({}));
let (_, _, success) = run_cmd(
&["filters", "load", "/nonexistent/rules.txt"],
tmp.path(),
);
assert!(!success);
}

#[test]
fn test_filters_load_preserves_other_local_state() {
let ls = local_state_with_filters("||old.com^");
let tmp = setup_fixture_with_local_state(&json!({}), &ls);
let rules_path = write_rules_file(tmp.path(), &["||new.com^"]);
run_cmd(
&["filters", "load", rules_path.to_str().unwrap()],
tmp.path(),
);
let ls = read_local_state(tmp.path());
assert_eq!(ls["profile"]["last_used"], "Default");
}
Loading