DEV Community

CarlyRaeJepsenStan
CarlyRaeJepsenStan

Posted on

Decoding Binary - 3 Different Ways

Hey everyone! I recently saw this tweet:

Obviously as a programmer, this is what everyone thinks I do - talk about stuff in 1s and 0s. Sadly, I didn't know what this said, so it's time to fix that.

First, I tried figuring out how to convert binary to text in Rust. After a short stint of searching on the combined intelligence of the entire world, I discovered the following useful commands: u8::from_str_radix and String::from_utf8

Now using these commands isn't super intuitive - in the docs for u8::from_str_radix, this is true:

assert_eq!(u8::from_str_radix("A", 16), Ok(10));
Enter fullscreen mode Exit fullscreen mode

A quick trip through man ascii in my terminal revealed "A" to be 41 in base 16, 65 in base 10, and 101 in base 8. But no 10! Ignoring this oddity (maybe u8 tables aren't in man ascii?), you can then chain the output from from_str_radix into from::utf8 to get out a human-readable character.

So, after some more interrogation of Stack Overflow and the friendly and intelligent Rust community, I finally got this code:

    assert_eq!(
        String::from_utf8(vec![u8::from_str_radix(&"01000001", 2).unwrap()]),
        Ok(String::from("A"))
    )
Enter fullscreen mode Exit fullscreen mode

So what's important here? We first make sure the radix (base) of our input is 2, and then because String::from_<any byte type> only accepts vectors, we use the vector macro vec! to ensure our input is a vector, and then finally feed it into String::from to get out something readable.

And because from_str_radix puts out a result, and we're sure that our input is going to be valid, we can safely unwrap it to use the result as our byte.

Great! The hard part is done - all I need to do now is to loop through the tweet's content, feed the words into my script here, and then collect together the resulting bytes and join them together. I won't give a full explanation, but in short map performs a function on every element of an iterator and stores the result to be re-assembled into another array.

fn main() {
    let a = "01110010 01100101 01101101 01100101 01101101 01100010 01100101 01110010 00100000 01101100 01100101 01100001 01110010 01101110 01101001 01101110 01100111 00100000 01100010 01101001 01101110 01100001 01110010 01111001 00111111";
    let output = a
        .split_whitespace()
        .map(|x| binary_to_ascii(&x))
        .collect::<Vec<_>>();
    println!("{:?}", output.concat());
}

pub fn binary_to_ascii(input: &str) -> String {
    return String::from_utf8(vec![u8::from_str_radix(&input, 2).unwrap()]).unwrap();
}

Enter fullscreen mode Exit fullscreen mode

Output:

Standard Error
   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 1.24s
     Running `target/debug/playground`
Standard Output
"remember learning binary?"
Enter fullscreen mode Exit fullscreen mode

Pretty cool, huh? I never learned binary so...

In any case, now it's time to switch gears and try doing it in the terminal! Befitting a true hacker aesthetic, I decided I'll convert binary into text using only native shell commands - no Python or anything like that.

Since we don't have nice things like from_radix and so on, we'll have to convert our base 2 numbers into text like this:
Binary -> Hexadecimal
Hexadecimal -> Text

So, how do we change bases in the terminal? We can use the built-in command bc (basic calculator) and the corresponding commands obase (output base) and ibase (input base) like this:

me@my-UbuntuBook:~$ bc
bc 1.07.1
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006, 2008, 2012-2017 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'. 
obase=16;ibase=2;01110010
72 # HERE! 
Enter fullscreen mode Exit fullscreen mode

Now that we have 72, which maps to a corresponding character's hex code, we can convert it into a character using a reverse hexdump! While tools like od and hexdump can convert characters into hexadecimal codes, only xxd provides a way to reverse it via the -r flag. For example, if we have a file only with 72 inside, and then reverse xxd it:

me@my-UbuntuBook:~$ cat has_seventy_two_inside
72
me@my-MacBookUbuntuBook:~$ xxd -r -p has_seventy_two_inside
r
Enter fullscreen mode Exit fullscreen mode

The -p flag means "plain", and outputs the result without line numbers and all that. For some reason if I don't have that flag, the output is blank so I don't know why? If you have any ideas, drop a comment!

Cool huh? But - we can't get arbitrary input into a running bc, and it's going to be a huge pain to have to type everything in, and then make files to xxd -r on. So let me introduce you to piping!

Piping using the pipe character | lets us move output from one command into another, or have a command take input from a previous one. For example, we could do this:

me@my-UbuntuBook:~$ echo "1+2" | bc
3
Enter fullscreen mode Exit fullscreen mode

Cool! So we can chain all our aforementioned commands together like this:

echo "obase=16; ibase=2; $BYTES_HERE" | bc | xxd -r -p
Enter fullscreen mode Exit fullscreen mode

Elegant, no? And because bash automatically turns strings into iterators split by string, I can skip splitting the string and just go straight to looping:

a="01110010 01100101 01101101 01100101 01101101 01100010 01100101 01110010 00100000 01101100 01100101 01100001 01110010 01101110 01101001 01101110 01100111 00100000 01100010 01101001 01101110 01100001 01110010 01111001 00111111"

for i in $a; 
 do echo "obase=16; ibase=2; $i" | bc | xxd -r -p;
done
Enter fullscreen mode Exit fullscreen mode

(sorry for the bad variable names)

Yay! That took quite a while to solve, but gives a nice satisfactory result.

And finally, everyone's favorite language - JavaScript. Not to brag or anything, but I golfed (one-lined) this problem in 2 minutes:

a="01110010 01100101 01101101 01100101 01101101 01100010 01100101 01110010 00100000 01101100 01100101 01100001 01110010 01101110 01101001 01101110 01100111 00100000 01100010 01101001 01101110 01100001 01110010 01111001 00111111"

a.split(" ").map(x => String.fromCharCode(parseInt(x, 2))).join("")
Enter fullscreen mode Exit fullscreen mode

Easy peezy lemon squeezy.

So how does this work? The .split() method on a string divides the string into an array by chopping it up at each argument passed into split. In this case, I passed a single space so the string of bytes got split up into an array of bytes. Next, just like in the Rust solution, I mapped a function that consumes binary information, converts it into a character code of some sort, and then converts the character code into a human-readable letter. More specifically, parseInt accepts two argument: a string and then a radix (in that order), and converts it into base 10. String.fromCharCode is essentially a reverse decimal dump; it accepts base-10 numbers, and outputs their corresponding character. And finally, because we output an array of letters, to put all the letters back together into a sentence, we use .join on an array with no separator so everything just gets mashed together. And with that, we get the same result.

Hopefully this helped you get a role as a master hacker who can read and decode binary in 2022's Most Awesome Upcoming Hacker Action Movie or at least impress your non-programmer parents, so if you learned something, click all the reactions on the side, and even if you didn't, do it anyway!

Thanks for reading, and see you next time!

Top comments (0)