![]() |
base64-Codierung mittels JavaScript |
|
| |
base64 ist ein textbasiertes Format, welches der Übertragung von Binärdaten dient. Es ist in der RFC 2045 beschrieben und gehört zu den MIME-Richtlinien. Deshalb wird es manchmal auch als MIME-Codierung bezeichnet. Die Übertragung von binären Dateien mittels E-Mail zum Beispiel macht eine solche Konvertierung notwendig, da E-Mail ein textbasiertes Übertragungsformat ist. Durch die Verwendung verschiedener Zeichensätze auf den verschiedenen Servern, über welche diese Mail geleitet wird, könnten die angehängten Daten zerstört werden. Um das zu verhindern, gibt es base64. base64 benutzt einen sehr eingeschränkten Zeichensatz von 64 Zeichen, wie der Name schon vermuten lässt. Dieser Zeichensatz besteht aus folgenden Zeichen:
| "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" |
Dazu kommt ein "=" für das sogenannte Padding. Diese Auswahl ist so getroffen, weil diese Zeichen in nahezu jedem Zeichensatz der Computerwelt vorkommen. Die Auswahl der Zeichen macht base64 robuster als beispielsweise "uuencode"(welches einen anderen Zeichensatz verwendet).
Sehen wir uns den Zeichensatz näher an. Er enthält keine Steuerzeichen und beginnt mit "A". Das bedeutet der Charcode dieses Zeichens ist also 0. Die Anzahl von 64 Zeichen ermöglicht es dem Zeichensatz, 6 Bit pro Zeichen zu repräsentieren (der Logarithmus aus 64 zur Basis 2). Das bedeutet andersherum betrachtet, dass ein Zeichen eines base64-codierten Datensroms nur 6 Bit repräsentiert und nicht 8. Somit ist eine base64-codierte Datei immer rund 1/3 größer als das Original, denn der Speicherbedarf eines Zeichens ist nach wie vor 8 Bit, auch wenn es nur 6 Bit repräsentiert. Dafür hat man jedoch die Sicherheit, dass kein Bit verlorengeht. Und Sicherheit hat immer ihren Preis.
Kommen wir zur Umrechnung. base64 bearbeitet die Daten immer auf binärer Ebene. Ebenso ist es dem Verfahren egal, ob die Original-Datei mit 8, 10 oder 16 Bit großen Stücken arbeitet. Bei der Codierung wird der Bitstrom der Datei analysiert, immer nach 6 Bit abgeschnitten und in ein Zeichen des base64-Zeichensatzes umgewandelt. Sieht man sich das folgende Schaubild an, wird das Verfahren sehr schnell klar.
![]() |
Nun könnte man denken, der base64 Strom liest sich wie ein einziges langes Wort ohne Punkt und Komma. Dem ist auch (fast) so. Die
RFC 2045: MIME (Multipurpose Internet Mail Extensions) Teil 1 schreibt vor, dass der Strom in maximal 76 Zeichen lange Einheiten (Zeilen) unterteilt werden muss. Da base64 ein auf binärer Ebene arbeitendes Format ist, müssen die im Originaltext enthaltenen Zeilentrenner in eine Form gebracht werden. Das Problem ist, dass die Zeilenumbruchformate von Windows, Unix und Macintosh basierten Systemen unterschiedlich sind. Es besteht sogar ein Unterschied auf gleichen Systemen. Der Internet Explorer zum Beispiel erzeugt in Textareas den windows-typischen "\r\n"-Umbruch. Browser basierend auf der Gecko-Engine (Netscape 6, Mozilla) erzeugen dagegen nur ein Unix-typisches "\n". Schon alleine deshalb sollten alle Zeilenumbrüche auf ein einheitliches Maß gebracht werden. In unserem Fall werden die Zeilenumbrüche bereites in der UTF-8 Codierung normalisiert, denn nur diese Funktion arbeitet stringbasiert und ermöglicht den Einsatz regulärer Audrücke. Die entsprechende Zeile ist dort eingefügt.
Darüber hinaus gibt es jedoch noch eine Kleinigkeit zu bedenken. Was ist, wenn der Datenstrom endet, und kein Vielfaches von 6 ist erreicht? Hierfür gibt es das Padding, welches an geeigneter Stelle weiter unten erklärt wird.
Der erfahrene JavaScript-Programmierer hebt spätestens hier die Hand für einen Einwand. JavaScript kann nicht binär auf Daten zugreifen. Der Zugriff auf die (in diesem Fall Strings) kann nur byteweise erfolgen. Allerdings eröffnet uns die Mathematik ein kleines Hintertürchen. Das kleinste gemeinseame Vielfache - kurz kgV. Für alle, deren Matheunterricht etwas länger her ist: das kgV ist die Zahl, welche durch beide Operanden restlos teilbar ist, also im schlimmsten Falle das Produkt.
In unserem Falle muss das zu bearbeitende Stück des Bitstreams sowohl durch 8 - für den byteweisen Zugriff auf die Strings, als auch durch 6 - für die Eingliederung in den base64-Stream, teilbar sein. Im schlechtesten Falle wäre diese Zahl das Produkt aus 8 und 6, also 48. Aber Adam Ries meint es gut und wir müssen nur auf einem 24-Bit langen Stück arbeiten. Hier ein Beispiel. Die Bytes sind der Übersicht halber eingefärbt
| Der Bitstream im Byteformat | 11010110 10110111 00110001 |
| Der Bitstream im 6-Bit Format | 110101 101011 011100 110001 |
Aus dieser Erkenntnis ergibt sich die Vorgehensweise. Wir marschieren im Dreierhopp durch das Array, teilen diese 3 Byte in 4 Stücke à 6 Bit Länge und wandeln das Ergebnis in Zeichen aus dem base64-Zeichensatz. Somit ist base64-Codierung zumindest für Texte in JavaScript möglich. Das Isolieren und Zuweisen der Teilstücke passiert wieder durch Bitverschiebung mit den Operatoren & und |.
Es ist leichter, den Überblick zu behalten, wenn man sich den base64-Zeichensatz in Form eines Arrays zurechtlegt. Dies passiert mit Hilfe einer Funktion, welche onLoad im document-body aufgerufen wird und 2 globale Variablen initialisiert.
00: function b64arrays() {
01: var b64s='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
02: b64 = [];f64 =[];
03: for (var i=0; i<b64s.length ;i++) {
04: b64[i] = b64s.charAt(i);
05: f64[b64s.charAt(i)] = i;
06: }
07: }
In Zeile 2 werden zwei Arrays initialisiert (b64 und f64). Beachten Sie den weggelassenen var-Initiator. Würde dieser voran stehen, wären die Arrays nur innerhalb der Funktion gültig. Einer der beliebtesten JavaScript-Fehler überhaupt. Danach werden die Arrays gefüllt. b64 wird einfach mit dem Zeichensatz gefüllt, so dass an der Stelle des Arrays, welcher den Charcode repräsentiert, das jeweilge Zeichen steht. f64 ist ein sogenanntes "assoziatives" Array. Es wird mit dem Charcode des Zeichens gefüllt. Zugegriffen wird über das Zeichen als String. Kurz beschrieben ergibt f64['D'] = 3. Dieses Array ist besonders bei der Decodierung sehr hilfreich.
Bevor wir uns dem Code widmen, kommen hier die versprochen Erläuterungen zum Padding. Was ist das? Ganz einfach: Stellen wir uns vor, wir sollen nur ein oder zwei Zeichen nach base64 codieren. Dann wird der Bitsrom in der Mitte eines base64-Zeichens abgerissen. Deshalb wird er aufgefüllt. Sehen wir uns die Tabelle von vorhin nochmal an, nur mit anderen Maßgaben: (die Unterstriche repräsentieren ein fehlendes Zeichen).
| Der Bitstream im Byteformat | 11010110 10110111 ________ |
| Der Bitstream im 6-Bit Format | 110101 101011 0111__ ______ |
In der base64 Spezifikation hat man sich entschlossen, den Rest mit = aufzufüllen. Somit bedeutet ein = am Ende des base64-Streams, dass der letzte Teil nur 2 Byte beinhaltet. Die beiden fehlenden Bits beim vorletzten Zeichen werden als Nullen angenommen.
| Der Bitstream im Byteformat | 11010110 10110111 ________ |
| Der Bitstream im 6-Bit Format | 110101 101011 011100 = |
Analog arbeitet das Verfahren wenn der Stream 2 Byte "zu kurz" ist:
| Der Bitstream im Byteformat | 11010110 ________ ________ |
| Der Bitstream im 6-Bit Format | 110101 100000 = = |
Wir machen uns bei unserem Encoder zu nutze, dass ein paar angehängte Nullen nicht stören, solange wir uns merken, wieviele Nullen angehängt werden. Diese mit Nullen aufgefüllten Teile des Datenstroms werden nach der Konvertierung durch = ersetzt. Das Auffüllen mit den Nullen vor Beginn der Konvertierung vermeidet ungültige Rechenoperationen wie die Bitverschiebung in einem Arrayelemnt, welches gar nicht existiert.
Zur Ermittlung kann man die modulo-Division verwenden (Division mit Rest).
Hier ist der Quellcode des Encoders.
00: function encode_base64(d) {
01: var r=[]; var i=0; var dl=d.length;
02: // Padding vorbereiten
03: if ((dl%3) == 1) {
04: d[d.length] = 0; d[d.length] = 0;}
05: if ((dl%3) == 2)
06: d[d.length] = 0;
07: // Konvertieren
08: while (i<d.length)
09: {
10: r[r.length] = b64[d[i]>>2];
11: r[r.length] = b64[((d[i]&3)<<4) | (d[i+1]>>4)];
12: r[r.length] = b64[((d[i+1]&15)<<2) | (d[i+2]>>6)];
13: r[r.length] = b64[d[i+2]&63];
14: if ((i%57)==54)
15: r[r.length] = "\n";
16: i+=3;
17: }
18: // padding abschließen
19: if ((dl%3) == 1)
20: r[r.length-1] = r[r.length-2] = "=";
21: if ((dl%3) == 2)
22: r[r.length-1] = "=";
23: // Array in text zusammenführen
24: var t=r.join("");
25: return t;
26: }
In Zeile 1 wird das Array für dem codierten Text initialisiert, ein Iterator i gesetzt, und dl merkt sich die Originallänge, bevor wir eventuell benötigte Nullen anfügen. Die Vorbereitungen für das Padding passieren in Zeile 3 bis 7.
Nun zur eigentliche Konvertierung. Jede Zeile bearbeitet einen base64-Wert. Zeile 10 zieht sich die ersten 6 Bit aus dem ersten Zeichen durch Bitverschiebung. Und hier macht sich dann die Erstellung der Zeichensatz-Arrays bezahlt. Es wird einfach der Wert an der ermittelten Stelle abgefragt. Zeile 11 zieht die letzten beiden Bit aus dem ersten Zeichen und die ersten 4 Bit aus dem 2. Zeichen heraus und fügt sie zusammen. Dazu wird die Bitverschiebetechnik ähnlich dem UTF-8-Codierer verwendet. Die letzten beiden Zeichen werden auf eine äquivalente Art und Weise ermittelt.
Nun zu Zeile 14 und 15. Da base64 die schlechte Angewohnheit hat, Daten aufzublasen, sollte man den geforderten Zeilenumbruch wirklich erst bei der maximal möglichen Zahl von 76 machen. Dies wird hier sicher gestellt. Wer Lust hat, kann es nachrechnen. Die Abfrage berücksichtigt folgendes: Iterator i arbeitet auf dem Original, nicht auf dem base64-codierten Text. Deshalb muss 76 = 57 +(57/3) sein, und der Modulo-Wert darf nicht 0 erreichen, da die Zeile sonst 80 statt 76 Zeichen hat. Nehmen Sie es als gegeben - es stimmt (und ziehen Sie keine voreiligen Schlüsse über mein Demokratieverständnis ;-)..... ).
Die Zeilen 19 bis 22 schließen das Padding, falls notwendig.
Steigen wir wieder mit dem Quellcode ein.
00: function b64t2d(t) {
01: var d=[]; var i=0;
02: // zur decodierung die Umbrueche killen
03: t=t.replace(/\n|\r/g,""); t=t.replace(/=/g,"");
04: while (i<t.length)
05: {
06: d[d.length] = (f64[t.charAt(i)]<<2) | (f64[t.charAt(i+1)]>>4);
07: d[d.length] = (((f64[t.charAt(i+1)]&15)<<4) | (f64[t.charAt(i+2)]>>2));
08: d[d.length] = (((f64[t.charAt(i+2)]&3)<<6) | (f64[t.charAt(i+3)]));
09: i+=4;
10: }
11: if (t.length%4 == 2)
12: d = d.slice(0, d.length-2);
13: if (t.length%4 == 3)
14: d = d.slice(0, d.length-1);
15: return d;
16: }
Das Verfahren arbeitet ähnlich der Encodierung und ist dort auch erklärt. Erwähnt werden sollte nur Zeile 3, in der die CRLF-Sequenzen entfernt werden und auch gleich die Padding-Zeichen, da diese für den Decoder nicht relevant sind. Wir ermitteln die Paddinglänge in Zeile 11 bzw. 13 mit der bewährten Modulo-Division, was zuverlässiger ist.
© 2007
Impressum
© 2000-2005
tobias@justdreams.de für den Text auf dieser Seite