use thiserror::Error;
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum SignatureProgramError {
#[error("the player program states no signature decipher function")]
MissingFunction,
#[error("the decipher function calls a helper object the player program does not state")]
MissingHelper,
#[error("the decipher function applies an unrecognized transformation: {0}")]
UnknownTransformation(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SignatureStep {
Reverse,
Drop(usize),
Swap(usize),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SignatureProgram {
steps: Vec<SignatureStep>,
}
impl SignatureProgram {
pub fn recover(player: &str) -> Result<Self, SignatureProgramError> {
let body = decipher_body(player).ok_or(SignatureProgramError::MissingFunction)?;
let helper = helper_name(body).ok_or(SignatureProgramError::MissingHelper)?;
let object = helper_object(player, &helper).ok_or(SignatureProgramError::MissingHelper)?;
body.split(';')
.filter_map(|call| helper_call(call, &helper))
.map(|(name, argument)| step(&object, name, argument))
.collect::<Result<Vec<_>, _>>()
.map(|steps| Self { steps })
}
#[must_use]
pub fn steps(&self) -> &[SignatureStep] {
&self.steps
}
#[must_use]
pub fn decipher(&self, signature: &str) -> String {
let mut characters: Vec<char> = signature.chars().collect();
for step in &self.steps {
if characters.is_empty() {
break;
}
match *step {
SignatureStep::Reverse => characters.reverse(),
SignatureStep::Drop(count) => {
characters.drain(..count.min(characters.len()));
}
SignatureStep::Swap(index) => {
let position = index % characters.len();
characters.swap(0, position);
}
}
}
characters.into_iter().collect()
}
}
fn decipher_body(player: &str) -> Option<&str> {
for marker in ["=a.split(\"\")", "=a.split('')"] {
let mut search = player;
let mut offset = 0;
while let Some(index) = search.find(marker) {
let start = offset + index + marker.len();
let tail = &player[start..];
if let Some(end) = tail.find("return a.join(") {
let body = tail.get(..end)?;
if !body.contains("function") {
return Some(body.trim_start_matches(';'));
}
}
offset = start;
search = &player[offset..];
}
}
None
}
fn helper_name(body: &str) -> Option<String> {
let call = body.split(';').map(str::trim).find(|call| {
call.split_once('.')
.is_some_and(|(head, tail)| !head.is_empty() && tail.contains('('))
})?;
call.split('.').next().map(str::to_owned)
}
fn helper_object(player: &str, name: &str) -> Option<String> {
let start = [
format!("var {name}={{"),
format!("var {name} = {{"),
format!("{name}={{"),
]
.into_iter()
.find_map(|marker| Some(player.find(&marker)? + marker.len()))?;
let tail = &player[start..];
let mut depth = 1_u32;
for (offset, character) in tail.char_indices() {
match character {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
return tail.get(..offset).map(str::to_owned);
}
}
_ => {}
}
}
None
}
fn helper_call<'a>(call: &'a str, helper: &str) -> Option<(&'a str, usize)> {
let call = call.trim();
let tail = call.strip_prefix(helper)?.strip_prefix('.')?;
let (name, arguments) = tail.split_once('(')?;
let argument = arguments.trim_end_matches(')').split(',').nth(1)?;
Some((name, argument.trim().parse().unwrap_or(0)))
}
fn step(object: &str, name: &str, argument: usize) -> Result<SignatureStep, SignatureProgramError> {
let marker = format!("{name}:function");
let start = object
.find(&marker)
.ok_or_else(|| SignatureProgramError::UnknownTransformation(name.to_owned()))?;
let body = &object[start..];
let body = body.split_once('}').map_or(body, |(head, _)| head);
if body.contains("reverse") {
Ok(SignatureStep::Reverse)
} else if body.contains("splice") {
Ok(SignatureStep::Drop(argument))
} else if body.contains('%') {
Ok(SignatureStep::Swap(argument))
} else {
Err(SignatureProgramError::UnknownTransformation(
name.to_owned(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
const PLAYER: &str = r#"
var _yt_player={};
(function(g){var window=this;var Kx={
xW:function(a){a.reverse()},
Jd:function(a,b){a.splice(0,b)},
Ll:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c}};
g.tj=function(a){a=a.split("");Kx.Ll(a,32);Kx.xW(a,58);Kx.Jd(a,2);Kx.Ll(a,7);
return a.join("")};
})(_yt_player);
"#;
#[test]
fn the_recovered_program_is_the_sequence_the_player_states() {
let program = SignatureProgram::recover(PLAYER).expect("decipher program");
assert_eq!(
program.steps(),
[
SignatureStep::Swap(32),
SignatureStep::Reverse,
SignatureStep::Drop(2),
SignatureStep::Swap(7),
]
);
}
#[test]
fn deciphering_applies_every_transformation_in_order() {
let program = SignatureProgram::recover(PLAYER).expect("decipher program");
let mut expected: Vec<char> = "abcdefghijklmnopqrstuvwxyz0123456789".chars().collect();
let position = 32 % expected.len();
expected.swap(0, position);
expected.reverse();
expected.drain(..2);
let position = 7 % expected.len();
expected.swap(0, position);
assert_eq!(
program.decipher("abcdefghijklmnopqrstuvwxyz0123456789"),
expected.into_iter().collect::<String>()
);
}
#[test]
fn deciphering_preserves_the_character_count_the_steps_denote() {
let program = SignatureProgram::recover(PLAYER).expect("decipher program");
let signature = "abcdefghijklmnopqrstuvwxyz0123456789";
assert_eq!(
program.decipher(signature).chars().count(),
signature.len() - 2
);
}
#[test]
fn a_script_stating_no_decipher_function_states_no_program() {
assert_eq!(
SignatureProgram::recover("var a = 1;"),
Err(SignatureProgramError::MissingFunction)
);
}
#[test]
fn a_decipher_function_without_its_helper_object_states_no_program() {
let orphan = r#"g.tj=function(a){a=a.split("");Kx.Ll(a,32);return a.join("")};"#;
assert_eq!(
SignatureProgram::recover(orphan),
Err(SignatureProgramError::MissingHelper)
);
}
#[test]
fn the_empty_program_leaves_a_signature_unchanged() {
assert_eq!(SignatureProgram::default().decipher("posed"), "posed");
}
}