ssukka 0.2.1

HTML obfuscation library and CLI for Rust. Renders identically in browsers but is hard for humans to read.
Documentation
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-character 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. Requires the AST engine ([`ObfuscationConfig::js_ast`]); falls
    /// back to [`JsStringEncoding::Escapes`] when the AST engine 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,

    // ---- 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>` JavaScript 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,
    /// 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,

    // ---- 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,

            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,
            control_flow_flattening: false,
            dead_code_injection: false,
            dead_code_threshold: 0.4,

            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.control_flow_flattening
                || self.dead_code_injection
                || self.js_string_encoding == JsStringEncoding::Array)
    }
}