ltk_webkit/wpe/
content_filter.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! Content blocking: rules in the JSON format of Safari's content blockers, compiled by WebKit and applied to every view through the user content manager they share.

use std::ffi::{ c_void, CString };
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use std::ptr;
use std::sync::OnceLock;
use std::sync::atomic::{ AtomicUsize, Ordering };

use gio::ffi::GAsyncResult;
use glib_sys::{ g_bytes_new, g_bytes_unref, g_error_free, gpointer, GError };
use gobject_sys::{ g_object_unref, GObject };
use wpe_webkit_sys::*;

static MANAGER:    OnceLock<usize> = OnceLock::new();
/// Moves with every call, so a compilation that finishes after a later call is dropped.
static GENERATION: AtomicUsize     = AtomicUsize::new( 0 );

/// Lives for the process; given to every view at construction.
pub( crate ) fn user_content_manager() -> *mut WebKitUserContentManager
{
	// SAFETY: a plain constructor; the reference is never released.
	*MANAGER.get_or_init( || unsafe { webkit_user_content_manager_new() } as usize ) as *mut WebKitUserContentManager
}

/// Replace the content filter with `rules_json`. WebKit compiles it in the background, into `store_dir`, so loads that start right away may still go unfiltered; pages already loaded keep what they have until their next load.
pub fn set_content_filter( store_dir: &Path, identifier: &str, rules_json: &str )
{
	let ( Ok( dir ), Ok( identifier ) ) = ( CString::new( store_dir.as_os_str().as_bytes() ), CString::new( identifier ) ) else { return };
	let generation = GENERATION.fetch_add( 1, Ordering::Relaxed ) + 1;
	// SAFETY: WebKit copies the strings and keeps its own references on the bytes and the store while it works; `on_saved` runs exactly once.
	unsafe
	{
		let store = webkit_user_content_filter_store_new( dir.as_ptr() );
		if store.is_null() { return; }
		let bytes = g_bytes_new( rules_json.as_ptr() as *const c_void, rules_json.len() );
		webkit_user_content_filter_store_save( store, identifier.as_ptr(), bytes, ptr::null_mut(), Some( on_saved ), generation as gpointer );
		g_bytes_unref( bytes );
		g_object_unref( store as *mut GObject );
	}
}

pub fn remove_content_filters()
{
	GENERATION.fetch_add( 1, Ordering::Relaxed );
	// SAFETY: the manager lives for the process.
	unsafe { webkit_user_content_manager_remove_all_filters( user_content_manager() ); }
}

unsafe extern "C" fn on_saved( source: *mut GObject, result: *mut GAsyncResult, user_data: gpointer )
{
	let mut err: *mut GError = ptr::null_mut();
	// SAFETY: `source` is the store the save was started on, alive for the callback; the filter is a full reference.
	unsafe
	{
		let filter = webkit_user_content_filter_store_save_finish( source as *mut WebKitUserContentFilterStore, result, &mut err );
		if filter.is_null()
		{
			eprintln!( "[ltk-webkit] content filter not compiled: {}", super::error_message_or_default( err, "unknown" ) );
			if !err.is_null() { g_error_free( err ); }
			return;
		}
		if user_data as usize == GENERATION.load( Ordering::Relaxed )
		{
			let manager = user_content_manager();
			webkit_user_content_manager_remove_all_filters( manager );
			webkit_user_content_manager_add_filter( manager, filter );
		}
		webkit_user_content_filter_unref( filter );
	}
}