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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
use std::path::PathBuf;
/// JavaScript string-literal encoding strategy.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum JsStringEncoding {
/// Leave string literals untouched.
None,
/// Per-char escapes randomized over `\xHH` / `\uXXXX` / `\u{..}`
/// (strict-mode-safe, token-level). Default.
#[default]
Escapes,
/// Hoist string literals into a base64 array decoded by an injected
/// runtime prelude. Needs the AST engine ([`ObfuscationConfig::js_ast`]);
/// falls back to [`JsStringEncoding::Escapes`] when it is off.
Array,
}
/// Configuration for HTML obfuscation.
///
/// Cosmetic transforms default to `true`. Transforms that change the DOM,
/// output size, runtime cost, or accessibility are opt-in (`false`). Build via
/// the [`crate::Obfuscator`] builder.
#[derive(Debug, Clone)]
pub struct ObfuscationConfig {
// HTML (cosmetic, on by default)
pub remove_comments: bool,
pub collapse_whitespace: bool,
pub encode_text_entities: bool,
pub encode_attr_entities: bool,
pub shuffle_attributes: bool,
pub randomize_tag_case: bool,
/// Insert empty comments inside long words so naive raw-HTML scrapers see
/// fragmented text; browsers and content extractors read it intact. Opt-in.
pub split_words: bool,
// CSS (cosmetic, on by default)
pub rename_classes: bool,
pub rename_ids: bool,
pub minify_css: bool,
pub unicode_escape_selectors: bool,
// JS (cosmetic, on by default)
pub js_string_encoding: JsStringEncoding,
pub minify_js: bool,
// Honeypots / decoys (opt-in)
/// Inject invisible decoy links, fields, and classes to trap scrapers.
pub inject_honeypots: bool,
/// Number of decoy nodes to inject (when [`Self::inject_honeypots`]).
pub honeypot_count: usize,
// Structural obfuscation (opt-in, WebCloak-style)
/// Move text content into encoded data-attributes and restore it
/// client-side via an injected script. Resists static scrapers but
/// requires JS execution and degrades no-JS / SEO / accessibility.
pub structural_obfuscation: bool,
// AST-based JS engine (opt-in, oxc)
/// Route `<script>` JS through the oxc AST pipeline instead of the token
/// state machine. Required by mangling / string arrays / CFF / dead code.
pub js_ast: bool,
/// Scope-aware renaming of local JS bindings (requires [`Self::js_ast`]).
pub mangle_identifiers: bool,
/// Rename local JS bindings to plausible-but-misleading names instead of
/// short ones, to mislead LLM cleanup passes (requires [`Self::js_ast`]).
pub poison_names: bool,
/// Flatten sequential control flow into a switch dispatcher (requires AST).
pub control_flow_flattening: bool,
/// Inject opaque-predicate-guarded dead code (requires AST).
pub dead_code_injection: bool,
/// Fraction (0.0..=1.0) of eligible sites that receive dead code.
pub dead_code_threshold: f32,
/// Inject a self-check that disables `console` if the emitted script was
/// beautified/tampered (deters casual beautify-and-run; requires AST).
pub self_defending: bool,
// Watermark / provenance (opt-in)
/// Embed this id once as invisible zero-width characters in the text, so a
/// scraped/leaked copy can be traced. May affect screen readers.
pub watermark: Option<u64>,
// Machine-readable AI opt-out (opt-in)
/// Inject `<meta>` opt-out signals (`robots: noai`, TDM reservation) into
/// `<head>`. A polite, legally-recognized signal; widely ignored on its own.
pub emit_ai_opt_out: bool,
// External resources (opt-in, local files only, stays offline)
/// Inline and obfuscate `<link rel=stylesheet>` / `<script src>` whose URL
/// resolves to a **local file** under [`Self::base_dir`]. Never fetches
/// over the network.
pub inline_local_resources: bool,
/// Base directory used to resolve local resource paths.
pub base_dir: Option<PathBuf>,
// Polymorphism / determinism
/// Randomize *which* optional cosmetic transforms run and their intensity
/// on each invocation, so identical input yields structurally different
/// output every time (signature/cache evasion). Ignored when a `seed` is set.
pub polymorphic: bool,
/// Optional seed for deterministic output.
pub seed: Option<u64>,
}
impl Default for ObfuscationConfig {
fn default() -> Self {
Self {
remove_comments: true,
collapse_whitespace: true,
encode_text_entities: true,
encode_attr_entities: true,
shuffle_attributes: true,
randomize_tag_case: true,
split_words: false,
rename_classes: true,
rename_ids: true,
minify_css: true,
unicode_escape_selectors: true,
js_string_encoding: JsStringEncoding::Escapes,
minify_js: true,
inject_honeypots: false,
honeypot_count: 6,
structural_obfuscation: false,
js_ast: false,
mangle_identifiers: false,
poison_names: false,
control_flow_flattening: false,
dead_code_injection: false,
dead_code_threshold: 0.4,
self_defending: false,
watermark: None,
emit_ai_opt_out: false,
inline_local_resources: false,
base_dir: None,
polymorphic: false,
seed: None,
}
}
}
impl ObfuscationConfig {
/// Whether any AST-only JS transform is requested.
pub fn wants_ast(&self) -> bool {
self.js_ast
&& (self.mangle_identifiers
|| self.poison_names
|| self.control_flow_flattening
|| self.dead_code_injection
|| self.self_defending
|| self.js_string_encoding == JsStringEncoding::Array)
}
}