/* base64.pk - base64 ASCII encoding for GNU poke.  */

/* This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/* Copyright (C) 2023, 2024 Jose E. Marchesi.
 *
 * Poke version of the Internet Software Consortium C implementation
 * of base64, which is:
 *
 * Copyright (c) 1996-1999 by Internet Software Consortium.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS
 * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE
 * CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
 * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
 * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
 * SOFTWARE. */

/* The base64 encoding is specified by
   https://datatracker.ietf.org/doc/html/rfc4648 */

var base64_chars
  = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

fun base64_encode = (uint<8>[] src) string:
{
  var srclength = src'length;
  var targsize = (src'length /^ 3) * 4;
  var target = uint<8>[targsize]();
  var input = uint<8>[3]();
  var output = uint<8>[4]();
  var s = 0UL;
  var datalength = 0UL;

  while (srclength > 2)
    {
      input[0] = src[s++];
      input[1] = src[s++];
      input[2] = src[s++];
      srclength -= 3;

      output[0] = input[0] .>> 2;
      output[1] = ((input[0] & 0x03) <<. 4) + (input[1] .>> 4);
      output[2] = ((input[1] & 0x0f) <<. 2) + (input[2] .>> 6);
      output[3] = input[2] & 0x3f;
      assert (output[0] < 64);
      assert (output[1] < 64);
      assert (output[2] < 64);
      assert (output[3] < 64);

      target[datalength++] = base64_chars[output[0]];
      target[datalength++] = base64_chars[output[1]];
      target[datalength++] = base64_chars[output[2]];
      target[datalength++] = base64_chars[output[3]];
    }

  /* Do now the padding.  */
  if (srclength != 0)
    {
      input[2] = '\0';
      input[1] = '\0';
      input[0] = '\0';

      for (var i = 0; i < srclength; i++)
        input[i] = src[s++];

      output[0] = input[0] .>> 2;
      output[1] = ((input[0] & 0x03) <<. 4) + (input[1] .>> 4);
      output[2] = ((input[1] & 0x0f) <<. 2) + (input[2] .>> 6);
      assert (output[0] < 64);
      assert (output[1] < 64);
      assert (output[2] < 64);

      target[datalength++] = base64_chars[output[0]];
      target[datalength++] = base64_chars[output[1]];
      if (srclength == 1)
        target[datalength++] = '=';
      else
        target[datalength++] = base64_chars[output[2]];
        target[datalength++] = '=';
    }

  return catos (target);
}

fun base64_decode = (string src) uint<8>[]:
{
  fun invalid_input = (string cause, int<32> position) void:
  {
    raise Exception { code = EC_inval,
                             name = "invalid argument",
                             msg = cause + format (" at position %i32d", position) };
  }

  var srclength = src'length;
  var targsize = (rtrim (src, "=")'length /^ 4) * 3;
  var target = uint<8>[targsize+1]();

  var state = 0;
  var tarindex = 0;
  var ch = 0UB;
  var s = 0UL;

  while (s < srclength)
    {
      ch = src[s++];

      if (ch == ' ')
        continue;

      if (ch == '=')
        {
          s--;
          break;
        }

      var pos = strchr (base64_chars, ch);

      if (pos == base64_chars'length) /* A non-base64 character.  */
        invalid_input ("non-base64 character passed to base_64_decode", s - 1);

      if (state == 0)
        {
          assert (tarindex < targsize);
          target[tarindex] = pos <<. 2;
          state = 1;
        }
      else if (state == 1)
        {
          assert (tarindex + 1 < targsize);
          target[tarindex] |= pos .>> 4;
          target[tarindex + 1] = (pos & 0x0f) <<. 4;
          tarindex += 1;
          state = 2;
        }
      else if (state == 2)
        {
          assert (tarindex + 1 < targsize);
          target[tarindex] |= pos .>> 2;
          target[tarindex + 1] = (pos & 0x03) <<. 6;
          tarindex += 1;
          state = 3;
        }
      else if (state == 3)
        {
          assert (tarindex < targsize);
          target[tarindex] |= pos;
          tarindex++;
          state = 0;
        }
      else
        /* Unreachable.  */
        assert (0);
    }

  /* We are done with the base-64 chars.  Let's see if we ended on a
     byte boundary, and/or with erroneous trailing characters.  */
  if (ch == '=')
    {
      ch = src[s++];
      if (state == 0 || state == 1)
        {
          invalid_input ("invalid `=' in string passed to base64_decode",
                         s - 1);
        }
      else if (state == 2 || state == 3)
        {
          if (state == 2)
            {
              /* Skip any number of spaces.  */
              while (s < srclength)
                {
                  ch = src[s++];
                  if (ch != ' ')
                    break;
                }
              /* Make sure there is another trailing = sign.  */
              if (ch != '=')
                invalid_input ("expected `=' in string passed to base64_decode",
                               s - 1);
            }

              /* State 2 and 3.  */
            while (s < srclength)
              {
                ch = src[s++];
                if (ch != ' ')
                  invalid_input ("expected whitespace in string passed to base64_decode",
                                 s - 1);
              }

          if (target[tarindex] != 0)
            raise E_inval;
        }
    }

  return target[:tarindex];
}
