• encode-x.js

  • ¶
    (function(){
      var LAST_COMPUTED_ALPH = "";
  • ¶

    Precomputed bases to help out, specifically base16 for hex, base58 for bitcoin and base64 for blobs.

      const BASES = {
        BASE_2: "01",
        BASE_8: "01234567",
        BASE_11: "0123456789a",
        BASE_16: "0123456789abcdef",
        BASE_32: "0123456789ABCDEFGHJKMNPQRSTVWXYZ",
        BASE_36: "0123456789abcdefghijklmnopqrstuvwxyz",
        BASE_58: "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvqxyz",
        BASE_62: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
        BASE_64: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
        BASE_66: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~",
        BASE_95: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/~!@#$%^&*()_`<>,.?'\";:[{]}\\|=- ",
        FALL_BACK: function(max_i){
          let res = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/~!@#$%^&*()_`<>,.?'\";:[{]}\\|=- ";
          if(LAST_COMPUTED_ALPH.length >= max_i) return LAST_COMPUTED_APLH.slice(0, max_i);
          if(res.length >= max_i) return res.slice(0, max_i);
  • ¶

    If the precomputations didnt help pump them up by looping through unicode. Not getting any results over ~66000 so just going to hard code that for version 1.

          for(let i = 0; i < 67777 && res.length < max_i; ++i) {
            let char = String.fromCharCode(i);
            if(res.includes(char) // Throw away included chars.
              || /^[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]*$/.test(char) // throw away some unicode spaces.
              || !/^[\u0020-\u007e\u00a0-\uffff]*$/.test(char) // Throw away 'unprintables'
              || /^[\u0fd9-\u0fff]*$/.test(char) // Throw away more unicode white spaces.
              || /\s/.test(char) // Throw away whitespaces.
            ) continue;
            res += char
          }
          return res.slice(0, max_i)
        }
      };
  • ¶

    Retrieve the proper alphabet for use in conversiosn.

      const alphabet = a => {
        if(typeof a === 'string')
          return a;
        else if(BASES['BASE_' + a]) return BASES['BASE_' + a];
  • ¶

    hard coded for version 1, can be avoided via the API (custom alphabets).

        else if(a > 65411) throw new Error("You need to specify a custom alphabet via `setGlobalAlphabet` for a base over 65411.");
        else {
          return BASES.FALL_BACK(a);
        }
      };
  • ¶

    Our own add function to circumvent javascripts 64bit floating cap.

      const add = (num1, num2, base) => {
  • ¶

    Our core buffer.

        var res = new Uint32Array(num1.length + num2.length), carry = 0, i = 0;
        while(i < num1.length || i < num2.length || carry) {
          let total = carry + (num1[i]||0) + (num2[i]||0);
  • ¶

    Modulous devision to swap bases. newNum = remainder concatinated with the remainders remainder and so on.

          res[i++] = total % base;
          carry = (total - total % base) / base
  • ¶

    grow buffer if we are carrying for next remainder check.

          if(carry && i >  res.length) {
            let copy = new Uint32Array(num1.length + num2.length + res.length)
            copy.set(res);
            res = copy;
          }
        }
        return res.slice(0, i); // Give back only whats needed.
      };
  • ¶

    Extend the addition function to introduce multiplications.

      const multiply = (num, exponent, base) => {
        if(num <= 0) return num === 0 ? [] : null;
        var result = new Uint32Array();
        while(true) {
  • ¶

    Bit shit to the right and keep doubling the exponent

          num & 1 && (result = add(result, exponent, base)); // First iteration will be 1.
          num = num >> 1;
          if(num === 0) break;
          exponent = add(exponent, exponent, base) // double.
        }
        return result
      };
  • ¶

    Swap out to buffers to avoid memory limits.

      const stringToNumberArray = (str, baseAlphabet) => {
        var charValues = str.split('');
        var res = new Uint32Array(charValues.length);
        var j = 0;
        for(let i = charValues.length - 1; i >= 0; i--) {
          var n = baseAlphabet.indexOf(charValues[i]);
          if(n < 0) throw new Error("Your data is not found in your alphabet.");
          res[j++] = n;
        }
        return res
      };
  • ¶

    Conversion begins here.

      const convertBase = (str, fromAlphabet, toAlphabet) => {
        var fromBase = fromAlphabet.length,
        toBase = toAlphabet.length,
        charValues = stringToNumberArray(str.toString(), fromAlphabet); // coerce numbers to string
        if(charValues === null) return null;
        var resNumbers = new Uint32Array(), exp = [1];
        for(let i = 0; i < charValues.length; ++i) {
          resNumbers = add(resNumbers, multiply(charValues[i], exp, toBase), toBase);
          exp = multiply(fromBase, exp, toBase)
        }
  • ¶

    And ends here.

        var res = '';
        if(!resNumbers.length) {
          let i = str.length;
          while(i--)
  • ¶

    The stream being encoded was pure zeros.

            res += toAlphabet[0]
        } else {
          let i = resNumbers.length;
          while(i--)
            res += toAlphabet[resNumbers[i]]
        }
        return res
      };
      const check = a => {
          if(a.split("").sort().join("").match(/(.)\1/g)) // Check for duplicates.
            throw new Error("Your alphabet must contain all unique charachters.");
          return a
      };
      function converter(ia = null, oa = null) {
        const p = new Proxy({
  • ¶

    Our API.

          fromXXXToYYY: "Functions to encode and decode.",
          setGlobalAlphabet: "Set both from and to alphabet.",
          setFromAlphabet: "Sets the from alphabet.",
          setToAlphabet: "Set the to alphabet.",
          resetAlphabets: "Resets the alhabets to default.",
        }, {
          get(t, cmd) { // t is our target
            try { if(!(cmd += "")) return } catch(e) { return } // Hack to drop loading errors. And coerce to string.
  • ¶

    Regex parse for configuration requests.

            if(/^dumpAlphabets$/i.test(cmd)) return function() {
              require('fs').writeFileSync('encode-x_dumpAlphabets', 'Last Computed Alphabet\n'+LAST_COMPUTED_APLPH+'\n\nFrom Alphabet\n'+t.incAlphabet+'\n\nTo Alphabet\n'+t.outAlphabet);
              return this
            }
            if(/^resetAlphabets$/i.test(cmd)) return function() {
              t.incAlphabet = t.outAlphabet = null;
              return this
            }
            if(/^SetGlobalAlphabet$/i.test(cmd)) return function(ga) {
              t.incAlphabet = t.outAlphabet = check(ga)
              return this
            }
            if(/^SetFromAlphabet$/i.test(cmd)) return function(fa) {
              t.incAlphabet = alphabet(fa);
              return this
            }
            if(/^SeTtoAlphabet$/i.test(cmd)) return function(ta) {
              t.outAlphabet = alphabet(ta);
              return this
            }
  • ¶

    Flags

            var outIsData = srcIsData = false;
  • ¶

    Old regex was /^[x20-x7e]+$/ – ascii, Even older was /^[\x00-\xff]*$/ – extended ascii (x7f is ‘del’)

            var cmds = '(?:\\d+|hex(?:idecimal)?|data|utf8|text|oct(?:al)?|bin(?:ary)?|oth(?:er)?|btc|bitcoin|(?:(?:b(?:ase)?)?(?:\\d+))?blob)';
            var cmdRe = new RegExp('from(' + cmds + ')to(' + cmds + ')', 'i');
            var matches = cmd.match(cmdRe);
            if(!matches) return;
  • ¶

    Parse for request to encode/decode.

            else if(/data|utf8|oth|text/i.test(matches[1])) { matches[1] = 16; srcIsData = true }
            else if(/data|utf8|oth|text/i.test(matches[2])) { matches[2] = 16; srcIsData = true }
            var checker, possibles = ['hex', 16, 'btc', 58, 'bitcoin', 58, 'blob', 64, 'bin', 2, 'oct', 8];
            if(checker = possibles.indexOf(matches[1].toLowerCase()) > 0) matches[1] = possibles[checker+1];
            if(checker = possibles.indexOf(matches[2].toLowerCase()) > 0) matches[2] = possibles[checker+1];
  • ¶

    Our only ‘public’ facing function.

            return function(src) {
              if(+matches[1] === 64 && +matches[2] === 16) return Buffer.from(src, 'base64').toString('hex');
              if(+matches[2] === 16 && +matches[1] === 64) return Buffer.from(src, 'hex').toString('base64');
              var a1, a2;
  • ¶

    Begin alphabet configuration.

              if(srcIsData) {
  • ¶

    Our source is not a number, so represent it as one.

                src = Buffer.from(src).toString('hex');
                a1 = alphabet(16);
              } else if(outIsData) {
                a2 = alphabet(16)
              }
              !a1 && (a1 = t.incAlphabet ? t.incAlphabet.slice(0, matches[1]) : alphabet(+matches[1]));
              !a2 && (a2 = t.outAlphabet ? t.outAlphabet.slice(0, matches[2]) : alphabet(+matches[2]));
              if(!srcIsData && matches[1] > a1.length) {
                throw new Error("From alphabet not long enough, consider manually setting a larger one via `setFromAlphabet`.")
              }
              if(!outIsData && matches[2] > a2.length) {
                throw new Error("To alphabet not long enough, consider manually setting a larger one via `setToAlphabet`.")
              }
  • ¶

    If our output is not to be a number, assume its text/utf8..

              return outIsData ? Buffer.from(convertBase(src, a1, a2), 'hex').toString('utf8') : convertBase(src, a1, a2)
            }
          }
        })
        ia && (p.incAlphabet = ia);
        oa && (p.outAlphabet = oa);
        return p
      }
  • ¶

    Try to ensure compatibility accross browsers and node.

      try { module.exports = converter } catch(e) { window.convert = converter };
    })();