Skip to main content

rsynko_reqwest/
signature.rs

1//! Recovers Youtube's signature decipher program from the player script and applies it.
2//!
3//! The player states the decipher as one function over a character array, whose body is a sequence
4//! of calls into a helper object holding exactly three primitive transformations: reversing the
5//! array, dropping a prefix of it, and swapping the first character with one at an index. Those
6//! primitives are what the whole program means, so recovering it is recovering that sequence.
7
8use thiserror::Error;
9
10/// Denotes failure to recover the decipher program from a player script.
11#[derive(Clone, Debug, Error, PartialEq, Eq)]
12pub enum SignatureProgramError {
13    /// Denotes a script stating no decipher function.
14    #[error("the player program states no signature decipher function")]
15    MissingFunction,
16    /// Denotes a decipher function whose helper object is absent.
17    #[error("the decipher function calls a helper object the player program does not state")]
18    MissingHelper,
19    /// Denotes a helper call this interpreter does not recognize.
20    #[error("the decipher function applies an unrecognized transformation: {0}")]
21    UnknownTransformation(String),
22}
23
24/// Denotes one primitive transformation of the signature characters.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum SignatureStep {
27    /// Reverses the remaining characters.
28    Reverse,
29    /// Drops the stated number of leading characters.
30    Drop(usize),
31    /// Exchanges the first character with the one at the stated index, modulo the length.
32    Swap(usize),
33}
34
35/// Denotes the decipher program one player script states.
36#[derive(Clone, Debug, Default, PartialEq, Eq)]
37pub struct SignatureProgram {
38    steps: Vec<SignatureStep>,
39}
40
41impl SignatureProgram {
42    /// Recovers the decipher program stated by one player script.
43    ///
44    /// # Errors
45    ///
46    /// Returns the reason the script states no interpretable program.
47    pub fn recover(player: &str) -> Result<Self, SignatureProgramError> {
48        let body = decipher_body(player).ok_or(SignatureProgramError::MissingFunction)?;
49        let helper = helper_name(body).ok_or(SignatureProgramError::MissingHelper)?;
50        let object = helper_object(player, &helper).ok_or(SignatureProgramError::MissingHelper)?;
51        body.split(';')
52            .filter_map(|call| helper_call(call, &helper))
53            .map(|(name, argument)| step(&object, name, argument))
54            .collect::<Result<Vec<_>, _>>()
55            .map(|steps| Self { steps })
56    }
57
58    /// Observes the transformations the program applies, in application order.
59    #[must_use]
60    pub fn steps(&self) -> &[SignatureStep] {
61        &self.steps
62    }
63
64    /// Answers one signature challenge by applying every transformation in order.
65    #[must_use]
66    pub fn decipher(&self, signature: &str) -> String {
67        let mut characters: Vec<char> = signature.chars().collect();
68        for step in &self.steps {
69            if characters.is_empty() {
70                break;
71            }
72            match *step {
73                SignatureStep::Reverse => characters.reverse(),
74                SignatureStep::Drop(count) => {
75                    characters.drain(..count.min(characters.len()));
76                }
77                SignatureStep::Swap(index) => {
78                    let position = index % characters.len();
79                    characters.swap(0, position);
80                }
81            }
82        }
83        characters.into_iter().collect()
84    }
85}
86
87/// Observes the body of the function the player states over the signature characters.
88fn decipher_body(player: &str) -> Option<&str> {
89    // The function is recognized by what it does rather than by its name: it splits the signature
90    // into characters and joins them back, and the player states no other function that does both.
91    for marker in ["=a.split(\"\")", "=a.split('')"] {
92        let mut search = player;
93        let mut offset = 0;
94        while let Some(index) = search.find(marker) {
95            let start = offset + index + marker.len();
96            let tail = &player[start..];
97            if let Some(end) = tail.find("return a.join(") {
98                let body = tail.get(..end)?;
99                if !body.contains("function") {
100                    return Some(body.trim_start_matches(';'));
101                }
102            }
103            offset = start;
104            search = &player[offset..];
105        }
106    }
107    None
108}
109
110/// Names the helper object the decipher body applies.
111fn helper_name(body: &str) -> Option<String> {
112    let call = body
113        .split(';')
114        .map(str::trim)
115        .find(|call| call.split_once('.').is_some_and(|(head, tail)| !head.is_empty() && tail.contains('(')))?;
116    call.split('.').next().map(str::to_owned)
117}
118
119/// Observes the body of the helper object the player states under one name.
120fn helper_object(player: &str, name: &str) -> Option<String> {
121    // A player states the object as a `var` on its own or within a list, with or without spaces.
122    let start = [format!("var {name}={{"), format!("var {name} = {{"), format!("{name}={{")]
123        .into_iter()
124        .find_map(|marker| Some(player.find(&marker)? + marker.len()))?;
125    let tail = &player[start..];
126    let mut depth = 1_u32;
127    for (offset, character) in tail.char_indices() {
128        match character {
129            '{' => depth += 1,
130            '}' => {
131                depth -= 1;
132                if depth == 0 {
133                    return tail.get(..offset).map(str::to_owned);
134                }
135            }
136            _ => {}
137        }
138    }
139    None
140}
141
142/// Reads one `helper.name(a,argument)` call of the decipher body.
143fn helper_call<'a>(call: &'a str, helper: &str) -> Option<(&'a str, usize)> {
144    let call = call.trim();
145    let tail = call.strip_prefix(helper)?.strip_prefix('.')?;
146    let (name, arguments) = tail.split_once('(')?;
147    let argument = arguments.trim_end_matches(')').split(',').nth(1)?;
148    Some((name, argument.trim().parse().unwrap_or(0)))
149}
150
151/// Classifies one helper function by the transformation its body performs.
152fn step(object: &str, name: &str, argument: usize) -> Result<SignatureStep, SignatureProgramError> {
153    let marker = format!("{name}:function");
154    let start = object.find(&marker).ok_or_else(|| SignatureProgramError::UnknownTransformation(name.to_owned()))?;
155    let body = &object[start..];
156    let body = body.split_once('}').map_or(body, |(head, _)| head);
157    if body.contains("reverse") {
158        Ok(SignatureStep::Reverse)
159    } else if body.contains("splice") {
160        Ok(SignatureStep::Drop(argument))
161    } else if body.contains('%') {
162        Ok(SignatureStep::Swap(argument))
163    } else {
164        Err(SignatureProgramError::UnknownTransformation(name.to_owned()))
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    /// States one player script shaped like the one Youtube serves.
173    const PLAYER: &str = r#"
174var _yt_player={};
175(function(g){var window=this;var Kx={
176xW:function(a){a.reverse()},
177Jd:function(a,b){a.splice(0,b)},
178Ll:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c}};
179g.tj=function(a){a=a.split("");Kx.Ll(a,32);Kx.xW(a,58);Kx.Jd(a,2);Kx.Ll(a,7);
180return a.join("")};
181})(_yt_player);
182"#;
183
184    #[test]
185    fn the_recovered_program_is_the_sequence_the_player_states() {
186        let program = SignatureProgram::recover(PLAYER).expect("decipher program");
187
188        assert_eq!(
189            program.steps(),
190            [SignatureStep::Swap(32), SignatureStep::Reverse, SignatureStep::Drop(2), SignatureStep::Swap(7),]
191        );
192    }
193
194    #[test]
195    fn deciphering_applies_every_transformation_in_order() {
196        let program = SignatureProgram::recover(PLAYER).expect("decipher program");
197
198        // Applying the same four steps by hand to the same input states the expected answer.
199        let mut expected: Vec<char> = "abcdefghijklmnopqrstuvwxyz0123456789".chars().collect();
200        let position = 32 % expected.len();
201        expected.swap(0, position);
202        expected.reverse();
203        expected.drain(..2);
204        let position = 7 % expected.len();
205        expected.swap(0, position);
206
207        assert_eq!(program.decipher("abcdefghijklmnopqrstuvwxyz0123456789"), expected.into_iter().collect::<String>());
208    }
209
210    #[test]
211    fn deciphering_preserves_the_character_count_the_steps_denote() {
212        let program = SignatureProgram::recover(PLAYER).expect("decipher program");
213        let signature = "abcdefghijklmnopqrstuvwxyz0123456789";
214
215        assert_eq!(program.decipher(signature).chars().count(), signature.len() - 2);
216    }
217
218    #[test]
219    fn a_script_stating_no_decipher_function_states_no_program() {
220        assert_eq!(SignatureProgram::recover("var a = 1;"), Err(SignatureProgramError::MissingFunction));
221    }
222
223    #[test]
224    fn a_decipher_function_without_its_helper_object_states_no_program() {
225        let orphan = r#"g.tj=function(a){a=a.split("");Kx.Ll(a,32);return a.join("")};"#;
226
227        assert_eq!(SignatureProgram::recover(orphan), Err(SignatureProgramError::MissingHelper));
228    }
229
230    #[test]
231    fn the_empty_program_leaves_a_signature_unchanged() {
232        assert_eq!(SignatureProgram::default().decipher("posed"), "posed");
233    }
234}