Phase 2 — JS deobfuscator (rquickjs + ress)

Port NewPipeExtractor's JS pipeline: player.js fetch + cache, sig and
nsig function extraction, deobfuscation, sticky-error caching.

src/youtube/js/
  * runtime.rs        — rquickjs wrapper (mirrors utils/JavaScript.java)
                        compile_or_throw + run(snippet, name, parameter)
  * lexer.rs          — match_to_closing_brace via the `ress` JS scanner
                        (NPE's lexer is derived from the same crate
                        upstream)
  * extractor.rs      — iframe_api → embed page fallback for player.js
                        URL, regex-driven hash extraction, clean-and-fetch
  * signature.rs      — 6 sig fn name regexes (front-most-recent),
                        deobf-function-body via lexer w/ regex fallback,
                        helper-object + global-string-array extraction,
                        signatureTimestamp, snippet assembler
  * nsig.rs           — 8 nsig fn name regexes (incl. array-indirection),
                        body via lexer w/ regex fallback, fixupFunction
                        early-return strip
  * player_manager.rs — orchestrator + sticky-error cache mirroring
                        YoutubeJavaScriptPlayerManager

PORT DEVIATIONS from NPE (each flagged in code):
  * dropped the 6th sig fn name regex (used Java backref \2; Rust's
    `regex` crate is backtracking-free, so we substitute a loose form
    that NPE itself half-broke per audit Track B §2.1)
  * dropped the Java atomic group `(?>...)` from helper-object regex —
    Rust's NFA is already linear-time
  * nsig fixup substitutes `(?:"undefined"|'undefined')` for the
    \1 backref; harmless loosening
  * sig and nsig assembled snippets prepend `var` — QuickJS rejects
    bare-assignment to undeclared identifiers; NPE relied on Rhino's
    non-strict mode

Tests:
  * 43 lib unit tests (up from 7 in Phase 1)
  * 7 Phase 2 offline integration tests against a hand-crafted
    minified synthetic player.js — exercises the full sig pipeline
    (build_deobfuscator → runtime::run) and nsig fixup_function
  * 7 Phase 1 live smoke tests still green

57/57 total green.
This commit is contained in:
Sulkta 2026-05-24 16:53:19 -07:00
parent 2a2367a3d0
commit 6ad0c52aaf
12 changed files with 1536 additions and 0 deletions

62
src/youtube/js/mod.rs Normal file
View file

@ -0,0 +1,62 @@
// JS deobfuscator subsystem — mirrors NPE's player.js / sig / nsig pipeline.
//
// Public surface is the `player_manager` module (mirrors NPE's
// YoutubeJavaScriptPlayerManager — the sole public class in the subsystem):
// * signature_timestamp(video_id)
// * deobfuscate_signature(video_id, obfuscated)
// * url_with_throttling_parameter_deobfuscated(video_id, url)
// * throttling_parameter_cache_size()
// * clear_all_caches()
// * clear_throttling_parameters_cache()
//
// Everything else (runtime / lexer / extractor / signature / nsig) is
// crate-private plumbing.
pub mod extractor;
pub mod lexer;
pub mod nsig;
pub mod player_manager;
pub mod runtime;
pub mod signature;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum DeobfError {
#[error("could not fetch iframe_api: {0}")]
FetchIframe(String),
#[error("could not fetch embed page: {0}")]
FetchEmbed(String),
#[error("could not extract player.js URL")]
PlayerUrlMissing,
#[error("could not fetch player.js: {0}")]
FetchPlayerCode(String),
#[error("invalid player.js URL: {0}")]
InvalidPlayerUrl(String),
#[error("could not find sig deobf function via any pattern")]
SigFuncNotFound,
#[error("could not parse sig deobf function body: {0}")]
SigBodyParseFailed(String),
#[error("could not find sig helper object")]
SigHelperMissing,
#[error("could not find sig global array")]
SigGlobalArrayMissing,
#[error("could not extract signature timestamp")]
SigTimestampMissing,
#[error("could not find nsig deobf function via any pattern")]
NsigFuncNotFound,
#[error("could not parse nsig deobf function body: {0}")]
NsigBodyParseFailed(String),
#[error("nsig array indirection failed: {0}")]
NsigArrayLookupFailed(String),
#[error("js compile failed: {0}")]
JsCompileFailed(String),
#[error("js runtime failed: {0}")]
JsRuntimeFailed(String),
#[error("nsig output was empty (function neutered?)")]
NsigEmpty,
#[error("downloader not initialized")]
DownloaderMissing,
}
pub use player_manager::PlayerManager;