0

Converting ASCII strings to and from hex and base-64 is fairly simple in Javascript. However, dealing with binary and UTF-encoded strings throws a wrench into this.

Javascript's built-in atob() and btoa() functions do not work with UTF-encoded strings, which is a major problem for strings coming from elements (e.g. inputs) in an HTML document that declares a UTF-8 charset. Additionally, it seems that base-64 can only be directly encoded using strings that are already in ASCII-encoded hex, with no direct way provided to convert a binary string (either ASCII or UTF-8-encoded) to base-64.

To compound the issue further, it appears that nearly all of the questions posted on SO and elsewhere assume that "binary string" is equivalent to binary data represented in a hex-encoded string, as opposed to a string consisting of base-2 numbers.

Given a UTF-8 or ASCII-encoded string consisting of binary, hex, or base-64 characters, how would you convert between the three?

PaulC
  • 123
  • 1
  • 10

2 Answers2

0

This is the answer I have come up with so far. I have not yet looked into eliminating the intermediary steps for hex ⇌ base-64, so those functions involve a conversion to binary.

// Binary → Hex
binToHex = (value) => {
  let hexString = '';

  for (let i=0; i < (value.length)/4; i++) {
    let piece = value.substr(4*i, 4);
    hexString += parseInt(piece, 2).toString(16);
  }

  return hexString;
}

// Binary → Base-64
binToB64 = (value) => {
    let arrayBuffer = new ArrayBuffer(Math.ceil(value.length/8))
        ,decArray 
        ,uint8DecArray

    bitsToDecArray = (bits) => {
        let decArray = [];

        for (let i=0; i< Math.ceil(value.length/8); i++) {
            let length = 8*i+8 > value.length ? value.length - 8*i : 8
                ,bin = value.substr(8*i, length)

            decArray.push(parseInt(bin, 2).toString(10));
        }

        return decArray;
    }

    decArray = bitsToDecArray(value);
    uint8DecArray = new Uint8Array(decArray);

    // From http://stackoverflow.com/a/7372816/4111381
    base64ArrayBuffer = (arrayBuffer) => {
        let base64    = ''
            ,encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
            ,bytes         = new Uint8Array(arrayBuffer)
            ,byteLength    = bytes.byteLength
            ,byteRemainder = byteLength % 3
            ,mainLength    = byteLength - byteRemainder
            ,a, b, c, d
            ,chunk

        // Main loop deals with bytes in chunks of 3
        for (var i = 0; i < mainLength; i = i + 3) {
            // Combine the three bytes into a single integer
            chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]

            // Use bitmasks to extract 6-bit segments from the triplet
            a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18
            b = (chunk & 258048)   >> 12 // 258048   = (2^6 - 1) << 12
            c = (chunk & 4032)     >>  6 // 4032     = (2^6 - 1) << 6
            d = chunk & 63               // 63       = 2^6 - 1

            // Convert the raw binary segments to the appropriate ASCII encoding
            base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]
        }

        // Deal with the remaining bytes and padding
        if (byteRemainder == 1) {
            chunk = bytes[mainLength]

            a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2

            // Set the 4 least significant bits to zero
            b = (chunk & 3)   << 4 // 3   = 2^2 - 1

            base64 += encodings[a] + encodings[b] + '=='
        } else if (byteRemainder == 2) {
            chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]

            a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10
            b = (chunk & 1008)  >>  4 // 1008  = (2^6 - 1) << 4

            // Set the 2 least significant bits to zero
            c = (chunk & 15)    <<  2 // 15    = 2^4 - 1

            base64 += encodings[a] + encodings[b] + encodings[c] + '='
        }

        return base64
    }

    return base64ArrayBuffer(uint8DecArray);
}

// Hex → Bin
hexToBin = (value) => {
    let binString = '';

    for (let i=0; i < value.length; i++) {
        let bin = parseInt(value[i], 16).toString(2);
        binString += ('0000' + bin).slice(-4);
    }

    return binString;
}

// Hex → Base-64
hexToB64 = (value) => {
    return binToB64(hexToBin(value));
}

// Base-64 → Binary
b64ToBin = (value) => {
    let bitString = ''
        ,base64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

    for(let i = 0; i < value.length; i+=4) {
        let segment = '';

        for (let j = 0; j < 4; j++) {
            console.log(i+j)
            if (value[i+j] != '=') {
                let bin = base64chars.indexOf(value[i+j]).toString(2)
                segment += ('000000' + bin).slice(-6);
            }
            else segment += '000000';
        }
        bitString += segment;
    }

    // Strip ending null bytes
    while (bitString.endsWith('00000000')) {
        bitString = bitString.substr(0, bitString.length-8);
    }

    return bitString;
}

// Base-64 → Hex
b64ToHex = (value) => {
    return binToHex(b64ToBin(value));
}
PaulC
  • 123
  • 1
  • 10
0

You could try using the turbocommons library. Just download the minified js file (turbocommons-es5.zip) and write the following code:

<script src="../yourpathtothelibrary/turbocommons-es5.js"></script>

<script>
    var ConversionUtils = org_turbocommons.ConversionUtils;

    ConversionUtils.stringToBase64('your binary string here');
</script>

Or you can look at the code on how it is done by the library here:

https://github.com/edertone/TurboCommons/blob/master/TurboCommons-TS/src/main/ts/utils/ConversionUtils.ts

More info here:

https://turboframework.org/en/blog/2022-10-26/encode-decode-base64-strings-javascript-typescript-php

Or you can test it online here:

https://turboframework.org/en/app/stringutils/base64-encode

Jaume Mussons Abad
  • 706
  • 1
  • 6
  • 20