BASE64 Zeichencodes
« | 06 Mar 2019 | »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:
1static char const * const base64_chars = 2 "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 3 "abcdefghijklmnopqrstuvwxyz" 4 "0123456789+/"; 5static char const base64_nullchar = '='; 6 7void gate_base64_enc3(char const* inputTriple, char* outputQuad) 8{ 9 outputQuad[0] = base64_chars[((gate_uint8_t)inputTriple[0] >> 2) & 0x3f]; 10 outputQuad[1] = base64_chars[(((gate_uint8_t)inputTriple[0] & 0x03) << 4) 11 | (((gate_uint8_t)inputTriple[1] >> 4) & 0x0f)]; 12 outputQuad[2] = base64_chars[(((gate_uint8_t)inputTriple[1] & 0x0f) << 2) 13 | (((gate_uint8_t)inputTriple[2] >> 6) & 0x03)]; 14 outputQuad[3] = base64_chars[(gate_uint8_t)inputTriple[2] & 0x3f]; 15}
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.
1gate_size_t gate_base64_dec(char const* inputQuad, char* outputTriple) 2{ 3 gate_size_t pos1 = gate_str_char_pos(base64_chars, 64, inputQuad[0], 0); 4 gate_size_t pos2 = gate_str_char_pos(base64_chars, 64, inputQuad[1], 0); 5 gate_size_t pos3 = gate_str_char_pos(base64_chars, 64, inputQuad[2], 0); 6 gate_size_t pos4 = gate_str_char_pos(base64_chars, 64, inputQuad[3], 0); 7 gate_size_t ret = 0; 8 gate_uint32_t quad = 0; 9 10 do 11 { 12 if(pos1 == GATE_STR_NPOS) 13 { 14 break; 15 } 16 quad |= (gate_uint32_t)pos1 << 18; 17 18 if(pos2 == GATE_STR_NPOS) 19 { 20 break; 21 } 22 quad |= (gate_uint32_t)pos2 << 12; 23 outputTriple[0] = (char)(gate_uint8_t)((quad >> 16) & 0xff); 24 ++ret; 25 26 if(pos3 == GATE_STR_NPOS) 27 { 28 break; 29 } 30 quad |= (gate_uint32_t)pos3 << 6; 31 outputTriple[1] = (char)(gate_uint8_t)((quad >> 8) & 0xff); 32 ++ret; 33 34 if(pos4 == GATE_STR_NPOS) 35 { 36 break; 37 } 38 quad |= (gate_uint32_t)pos4; 39 outputTriple[2] = (char)(gate_uint8_t)(quad & 0xff); 40 ++ret; 41 42 } while(0); 43 44 return ret; 45}
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.