Javascript で Base64 encode/decode

趣旨

泣く子も黙る RFC2045 Base64 encode/decode を Javascript で実装してみた。「Base64は、データを64種類の印字可能な英数字のみを用いて、それ以外の文字を扱うことの出来ない通信環境にてマルチバイト文字やバイナリデータを扱うためのエンコード方式である」(Wikipedia)

情報ソース

Base64Javascript による実装は、ググれば数多くある。その中でも次の実装は秀逸だった。

Base64 Encode / Decode

今回自分で書いたのは、この実装のライセンス条項がちょっと厳しかったからだ。(商用利用不可) ただし、このコードのほうが私のものより洗練されているので、ライセンス条件が合致するならこっちを使ったほうがいいかも。

使い方

入力文字列の文字コードは何でもよいが、かならず UTF-8 としてエンコードされる。(内部的に encodeURIComponent を使っているため) デコードされた結果も UTF-8 文字列になる。

基本的に、文字列をエンコードするときには、

base64.encodeStringAsUTF8("あ%愛")

とし、デコードするときには、

base64.decodeStringAsUTF8("44GCJeaEmw==")

のようにする。

詳しくはテストケースを見てほしい。(説明手抜き(笑))

ソースコードのライセンスは public domain とする(著作権放棄)。自己責任で使ってほしい。バグはないとは思うのだが、もし発見したらご一報ください。

ソースコード

/**
 * Base64 encoding
 * http://www.ietf.org/rfc/rfc2045.txt
 */

var Base64 = function() {
	this.initialize();
};

Base64.prototype.initialize = function() {
	this.symbols = [];
	var startChar = "A".charCodeAt(0);
	for(var i = 0; i < 26; i++) {
		this.symbols.push(String.fromCharCode(startChar + i));
	}
	var startChar = "a".charCodeAt(0);
	for(var i = 0; i < 26; i++) {
		this.symbols.push(String.fromCharCode(startChar + i));
	}
	var startChar = "0".charCodeAt(0);
	for(var i = 0; i < 10; i++) {
		this.symbols.push(String.fromCharCode(startChar + i));
	}
	this.symbols.push("+", "/");
	
	this.encodeMap = [];
	for(var i = 0; i < this.symbols.length; i++) {
		this.encodeMap[i] = this.symbols[i];
	}
	
	this.decodeMap = [];
	for(var i = 0; i < this.symbols.length; i++) {
		this.decodeMap[this.symbols[i]] = i;
	}
	this.decodeMap["="] = null;
};

Base64.prototype.encode = function(octets) {
	var i;
	var map = this.encodeMap;
	var encoded = [];
	for (i = 0, len = Math.floor(octets.length / 3) * 3;
		i < len; i += 3) {
		var b0 = octets[i];
		var b1 = octets[i + 1];
		var b2 = octets[i + 2];
		var qs = map[(b0 >> 2) & 0x3f] 
	  				+ map[((b0 << 4) + (b1 >> 4)) & 0x3f]
	  				+ map[((b1 << 2) + (b2 >> 6)) & 0x3f]
	  				+ map[b2 & 0x3f];
		encoded.push(qs);
	}

	switch(octets.length % 3) {
		case 1:
			var b0 = octets[i];
			var qs = map[(b0 >> 2) & 0x3f] 
			       + map[(b0 << 4) & 0x3f]
						 + "=="; 
			encoded.push(qs);
			break;
		case 2:
			var b0 = octets[i];
		  var b1 = octets[i + 1];			
			var qs = map[(b0 >> 2) & 0x3f] 
						 + map[((b0 << 4) + (b1 >> 4)) & 0x3f] 
						 + map[(b1 << 2) & 0x3f] 
						 + "=";
			encoded.push(qs);
		  break;
	}
	
	return encoded.join("");
};

Base64.prototype.decode = function(encoded) {
	if(encoded.length % 4 != 0) {
		throw "encoded.length must be a multiple of 4.";
	}
	
	var decoded = [];
	var map = this.decodeMap;
	for (var i = 0, len = encoded.length; i < len; i += 4) {
		var b0 = map[encoded[i]];
		var b1 = map[encoded[i + 1]];
		var b2 = map[encoded[i + 2]];
		var b3 = map[encoded[i + 3]];
		
		var d0 = ((b0 << 2) + (b1 >> 4)) & 0xff;
		decoded.push(d0);
		
		if(b2 == null) break; // encoded[i + 1] == "="
		
		var d1 = ((b1 << 4) + (b2 >> 2)) & 0xff;
		decoded.push(d1);
		
		if(b3 == null) break; // encoded[i + 2] == "="
		
	  var d2 = ((b2 << 6) + b3) & 0xff;
		decoded.push(d2);
		
	}
	
	return decoded;
};

Base64.prototype.uriEncodedToOctets = function(uriEncoded) {
	var octets = [];
	for(var i = 0, len = uriEncoded.length; i < len; i++) {
		// Note that IE6 doesn't allow an expression like "uriEncoded[i]";
		var c = uriEncoded.charAt(i);
		var b;
		if (c == "%") {
			var hex = uriEncoded.charAt(++i) + uriEncoded.charAt(++i);
			b = parseInt(hex, 16);
		} else {
			b = c.charCodeAt(0);
		}
		octets.push(b);
  }
	return octets;
};

Base64.prototype.encodeStringAsUTF8 = function(utf8str) {
	var uriEncoded = encodeURIComponent(utf8str);
	var octets = this.uriEncodedToOctets(uriEncoded);
	return this.encode(octets);
};

Base64.prototype.octetsToUriEncoded = function(octets) {
	var uriEncoded = [];
	
	for(var i = 0, len = octets.length; i < len; i++) {
		var hex = octets[i].toString(16);
		hex = ("0" + hex).substr(hex.length - 1, 2);
		uriEncoded.push("%" + hex);
  }
	return uriEncoded.join("");
};

Base64.prototype.decodeStringAsUTF8 = function(encoded) {
	var octets = this.decode(encoded);
	var uriEncoded = this.octetsToUriEncoded(octets);
	return 	decodeURIComponent(uriEncoded);
};

var base64 = new Base64();

テストケース(jsunit)

<html>
 <head>
  <title>Test Page</title>
  <script language="javascript" src="../jsunit/app/jsUnitCore.js"></script>
  <script language="javascript" src="base64.js"></script>
 
 </head>
 <body>
  <script language="javascript">
  var _assertEquals = assertEquals;
  assertEquals = function(expected, actual) {
		if(expected.constructor == Array) {
			_assertEquals(expected.toString(), actual.toString());
		} else {
			_assertEquals(expected, actual);
		}
	};
	
	function setUp() {

	}
	
	function test_initialize() 
	{
		//var base64 = new Base64();

		assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", base64.symbols.join(""));
		assertEquals("/", base64.encodeMap[63]);
		assertEquals(62, base64.decodeMap["+"]);
	}

	function test_encode() 
	{
		//var base64 = new Base64();
		assertEquals("QQ==", base64.encode([65])); // A
		assertEquals("QUI=", base64.encode([65,66])); // AB
		assertEquals("QUJD", base64.encode([65,66,67])); // ABC		
		assertEquals("QUJDRA==", base64.encode([65,66,67,68]));	// ABCD	
		assertEquals("QUJDREU=", base64.encode([65,66,67,68,69])); // ABCDE
	}

	function test_decode() 
	{
		//var base64 = new Base64();
		assertEquals([65], base64.decode("QQ==")); // A
		assertEquals([65,66], base64.decode("QUI=")); // AB
		assertEquals([65,66,67], base64.decode("QUJD")); // ABC		
		assertEquals([65,66,67,68], base64.decode("QUJDRA=="));	// ABCD	
		assertEquals([65,66,67,68,69], base64.decode("QUJDREU=")); // ABCDE
	}

	function test_uriEncodedToOctets() 
	{
		//var base64 = new Base64();
		assertEquals([65, 0x25, 0x20, 66], base64.uriEncodedToOctets("A%25%20B")); 
	}

	function test_encodeStringAsUTF8() 
	{
		//var base64 = new Base64();
		assertEquals("44GC", base64.encodeStringAsUTF8("あ")); 
		assertEquals("44GCJeaEmw==", base64.encodeStringAsUTF8("あ%愛")); 

	}
	
	function test_octetsToUriEncoded() 
	{
		//var base64 = new Base64();
		assertEquals("%09%11", base64.octetsToUriEncoded([0x9, 0x11])); 
	}

	function test_decodeStringAsUTF8() 
	{
		//var base64 = new Base64();
		assertEquals("あ%愛", base64.decodeStringAsUTF8("44GCJeaEmw==")); 
	}

  </script>
 </body>
</html>

更新履歴

2008/02/28

  • uriEncodedToOctets() を修正 (IE6 で 文字列[n] のような表現がエラーなっていた問題を解決)
  • decode(), initialize() を修正( if(!n) のような箇所があり、意図としては n が undefined のとき、というつもりだったのだが、実際には n = 0 のときにも偽になってしまうため修正。Ruby にひきづられて、いつまでたってもこの手の間違いが抜けない。本当は isNaN() とか使ってもいいのかもしれない)