BASE64 Zeichencodes

Zu den ältesten und ersten Kodierungen, die ich lernen durfte, zählte BASE64. 3 Bytes zu je 8 bits (3 x 8 = 24 bits), werden auf 4 Bytes aufgeteilt, wobei nur 6 bit breite Wertebereiche genutzt werden, schließlich ist ja 4 x 6 auch gleich 24.

Und dieser 6-bit breite Wertebereich wird auf die ASCII-Zeichen A-Z, a-z, 0-9 und ‘+’ und ‘/’ aufgeteilt.

Und so können wir jeden erdenklichen binären Block als reine Buchstaben- und Zahlenkombinationen darstellen, womit keine Sonderzeichen oder ASCII Steuercodes benutzt werden müssen.

Es geht dabei um das älteste Problem bei Datenformaten: Wie kann ich zwischen Steuerkommandos (Anfang, Ende, Metadaten) und dem eigentlichen Inhalt, der ja genau so Steuerzeichen enthalten kann, möglichst gut unterscheiden?
Lösung: Man kodiert um.

Und parallel gab es früher noch ein zweites Problem, nämlich die Frage, wie man 8-bit Bytes über eine 7-bit Leitung quetschen kann.

Diese beiden Probleme löste BASE64 eben mit der Reduktion der benutzten Bits in einem Byte, womit allerdings die Länge der Datenpuffer um 33 % anwächst.

Viele Netzwerkprotokolle und Datenformate nutzen daher BASE64 für Contentdaten. In E-Mails können auf diese Weise binäre Attachments angefügt werden, weil die Mail-Protokolle SMTP und POP3 nur reine Text-Buchstaben unterstützen. (Man muss allerdings auch hinzufügen, dass auch andere Kodierungsformen dort gebräuchlich sind.)
Und uralte Modems bzw. Akkustikkoppler nutzten das 8. Bit zum Teil auch als Prüfsumme und brauchten deshalb ebenso Kodierungen um binäre Datenströme über 7 Datenbits leiten zu können.


Die GATE Implementierung sieht für so eine Binary-Tripple-to-B64-Quad Konvertierung wie folgt aus:

static char const * const base64_chars = 
  "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  "abcdefghijklmnopqrstuvwxyz"
  "0123456789+/";
static char const base64_nullchar = '=';

void gate_base64_enc3(char const* inputTriple, char* outputQuad)
{
  outputQuad[0] = base64_chars[((gate_uint8_t)inputTriple[0] >> 2) & 0x3f];
  outputQuad[1] = base64_chars[(((gate_uint8_t)inputTriple[0] & 0x03) << 4) | (((gate_uint8_t)inputTriple[1] >> 4) & 0x0f)];
  outputQuad[2] = base64_chars[(((gate_uint8_t)inputTriple[1] & 0x0f) << 2) | (((gate_uint8_t)inputTriple[2] >> 6) & 0x03)];
  outputQuad[3] = base64_chars[(gate_uint8_t)inputTriple[2] & 0x3f];
}

Hinzu kommen noch zwei ähnliche Funktionen, die das Ende eines binären Blocks abschließen, wenn dieser nicht aus 3 sondern aus weniger Bytes besteht. Dafür ist das Gleichheitszeichen = reserviert, das eine solche Sequenz beendet.

Bei der Dekodierung muss jedes Element eines Viererblocks in der BASE64 Zeichenliste gefunden werden. Natürlich können hier auch Kodierungsfehler auftreten, die man finden und dann Abbrechen muss. Die aktuelle GATE-Implementierung prüft hier nicht extra auf ein =, sondern sieht alles außerhalb der definierten 64 Zeichen als ein Ende bzw einen Abbruch an.

gate_size_t gate_base64_dec(char const* inputQuad, char* outputTriple)
{
  gate_size_t pos1 = gate_str_char_pos(base64_chars, 64, inputQuad[0], 0);
  gate_size_t pos2 = gate_str_char_pos(base64_chars, 64, inputQuad[1], 0);
  gate_size_t pos3 = gate_str_char_pos(base64_chars, 64, inputQuad[2], 0);
  gate_size_t pos4 = gate_str_char_pos(base64_chars, 64, inputQuad[3], 0);
  gate_size_t ret = 0;
  gate_uint32_t quad = 0;

  do
  {
    if(pos1 == GATE_STR_NPOS)
    {
      break;
    }
    quad |= (gate_uint32_t)pos1 << 18;

    if(pos2 == GATE_STR_NPOS)
    {
      break;
    }
    quad |= (gate_uint32_t)pos2 << 12;
    outputTriple[0] = (char)(gate_uint8_t)((quad >> 16) & 0xff);
    ++ret;

    if(pos3 == GATE_STR_NPOS)
    {
      break;
    }
    quad |= (gate_uint32_t)pos3 << 6;
    outputTriple[1] = (char)(gate_uint8_t)((quad >> 8) & 0xff);
    ++ret;

    if(pos4 == GATE_STR_NPOS)
    {
      break;
    }
    quad |= (gate_uint32_t)pos4;
    outputTriple[2] = (char)(gate_uint8_t)(quad & 0xff);
    ++ret;

  } while(0);

  return ret;
}

Ich habe BASE64 mehrere Male in C++, wie auch in VB6, VBScript und JavaScript implementiert. Vielleicht auch mal spaßhalber in C# oder Java, doch daran erinnere ich mich dann nicht mehr.

Der interessantere Punkt ist die Integration der Kodierung in Blockpuffer oder Streams, denn es wäre ja extrem ineffizient immer nur 3 oder 4 Bytes zu lesen um dann die jeweils andere Menge sofort in einen Outputstream zu schreiben.

Sinnvoller ist es, die Daten in vernünftig großen Puffern zu sammeln und dann in einem Schritt zu konvertieren.
Das ist zwar nicht besonders schwer, aber immer recht anfällig für Pointer-Rechenfehler und diverse Bugs.


Übrigens, korrekterweise muss man anfügen, dass die Implementierung laut C-Standard nicht portabel ist und auf unseren CPUs nur “zufällig” funktioniert.

Warum?
Na weil hier böse zwischen char und anderen unsigned Typen hin und her gecastet wird. Das wäre laut Standard nur für Werte zwischen 0 und 127 erlaubt, nicht aber für 128 bis 255 (bzw. -128 bis -1), weil char zumeist ein Vorzeichen trägt.

Doch auf unseren Zweierkomplement- Binärsystemen kommt zum Glück immer das richtige Ergebnis heraus und so lange es keine nativen Quantenprozessoren gibt, die in Q-Bits statt normalen Bits rechnen, ist diese Implementierungen (so wie zahlreiche andere) immer noch OK und zwischen (fast) allen CPUs nutzbar.


Wenn sich eine triviale Erkenntnis mit Dummheit in der Interpretation paart, dann gibt es in der Regel Kollateralschäden in der Anwendung.
frei zitiert nach A. Van der Bellen
... also dann paaren wir mal eine komplexe Erkenntnis mit Klugheit in der Interpretation!