#!/usr/bin/env ruby # # A one-long-line Base-64 Encoder in 273 characters # Original and improved by Aaron D. Gifford - http://www.aarongifford.com/ # Super-short Ruby 1.9 version by Stefan (aka apeiros) # # See the Ruby-Talk email archive thread beginning at: # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/358586 # ORIGINAL: This one works in 1.8 and 1.9 -- 276 characters: # def e(s);c=('A'..'Z').to_a.join+('a'..'z').to_a.join+'0123456789+/';(t=s.unpack('C*').inject([0,'',0]){|a,v|a[0]==0?[2,a[1]+c[v>>2,1],v*16&48]:a[0]==2?[4,a[1]+c[v>>4|a[2],1],v*4&60]:[0,a[1]+c[v>>6|a[2],1]+c[v&63,1],0]})[1]+(t[0]==0?'':t[0]==2?c[t[2],1]+'==':c[t[2],1]+'=');end # IMPROVED: Also works in 1.8 and 1.9 -- eliminate one .join by adding () # and replacing + with | for a net reduction of 3 characters -- 273 characters: # def e(s);c=(('A'..'Z').to_a|('a'..'z').to_a).join+'0123456789+/';(t=s.unpack('C*').inject([0,'',0]){|a,v|a[0]==0?[2,a[1]+c[v>>2,1],v*16&48]:a[0]==2?[4,a[1]+c[v>>4|a[2],1],v*4&60]:[0,a[1]+c[v>>6|a[2],1]+c[v&63,1],0]})[1]+(t[0]==0?'':t[0]==2?c[t[2],1]+'==':c[t[2],1]+'=');end # FURTHER IMPROVEMENT: Using Stefan's splat and join techniques # in a 1.8 and 1.9 compatible manner saving 9 more characters # -- 264 characters: # def e(s);c=[*'A'..'Z']*''+[*'a'..'z']*''+'0123456789+/';(t=s.unpack('C*').inject([0,'',0]){|a,v|a[0]==0?[2,a[1]+c[v>>2,1],v*16&48]:a[0]==2?[4,a[1]+c[v>>4|a[2],1],v*4&60]:[0,a[1]+c[v>>6|a[2],1]+c[v&63,1],0]})[1]+(t[0]==0?'':t[0]==2?c[t[2],1]+'==':c[t[2],1]+'=');end # IMPROVED FOR RUBY 1.9 ONLY: Eliminate unneeded ,1 string slices, # saving 12 more characters -- 261 characters: # def e(s);c=(('A'..'Z').to_a|('a'..'z').to_a).join+'0123456789+/';(t=s.unpack('C*').inject([0,'',0]){|a,v|a[0]==0?[2,a[1]+c[v>>2],v*16&48]:a[0]==2?[4,a[1]+c[v>>4|a[2]],v*4&60]:[0,a[1]+c[v>>6|a[2]]+c[v&63],0]})[1]+(t[0]==0?'':t[0]==2?c[t[2]]+'==':c[t[2]]+'=');end # FURTHER IMPROVEMENT FOR RUBY 1.9 ONLY: Switch to using apeiros's # splat and join techniques and ? for character literals, shaving # off 20 more characters (Wow, apeiros!) -- 241 characters: # def e(s);c=[*?A..?Z,*?a..?z,*?0..?9,'+/']*'';(t=s.unpack('C*').inject([0,'',0]){|a,v|a[0]==0?[2,a[1]+c[v>>2],v*16&48]:a[0]==2?[4,a[1]+c[v>>4|a[2]],v*4&60]:[0,a[1]+c[v>>6|a[2]]+c[v&63],0]})[1]+(t[0]==0?'':t[0]==2?c[t[2]]+'==':c[t[2]]+'=');end # Stefan's VASTLY improved Ruby 1.9 version: # Build the character string using ?A for character literals instead # of 'A', saving 4 additional characters. Instead of using building # ranges inside parenthesis and using .to_a, use the splat * pseudo- # operator inside the Array [] method call to expand ranges to fill # the array. Instead of using .join use *'' to perform the join on # array members. THUS instead of: # c=(('A'..'Z').to_a|('a'..'z').to_a).join+'0123456789+/' # Stefan has: # c=[*?A..?Z,*?a..?z,*?0..?9]*''+'+/' # THAT alone saves 20 characters over the IMPROVED version, or a # savings of 23 characters over the ORIGINAL. Additionally, Stefan # unpacks the source data string into a string of binary digits, # using unpack('B*')[0], then pads the binary string as necessary # to make sure its length is evenly divisible by six, then uses # String#scan and a regular expression to divide the binary string # into an array of six-bit strings. Using Array#map, Stefan uses # String#to_i(2) to convert each six-digit binary string to the # Fixnum value it represents, and use that to String#[] slice the # appropriate Base-64 encoding character the number represents. # Using *'' to join the resulting encoded character array to a # string, then padding with '=' characters as needed, Stefan # shaves off a stunning 104 additional characters from the ORIGINAL. # APEIROS VASTLY SHORTER VERSION: -- 149 characters # def e(s);c=[*?A..?Z,*?a..?z,*?0..?9]*''+'+/';(s.unpack("B*")[0]+'0'*((6-s.size*8%6)%6)).scan(/.{6}/).map{|e|c[e.to_i(2)]}*''+'='*((3-s.size%3)%3);end # TINY IMPROVEMENT ON APEIROS VERSION: I notice Stefan could have # shaved two more characters using the same ? character literal # in two more places Stefan must have missed -- 147 characters: def e(s);c=[*?A..?Z,*?a..?z,*?0..?9]*''+'+/';(s.unpack('B*')[0]+?0*((6-s.size*8%6)%6)).scan(/.{6}/).map{|e|c[e.to_i(2)]}*''+?=*((3-s.size%3)%3);end # NOTE to Stefan (aka apeiros): # ----------------------------- # I've been schooled in the art of code golf in Ruby (Ruby Golf) by # a pro, my amateur status exposed. Thank you for the golf lesson, # Stefan, and for reminding me about ? character literals and # teaching me how to use the splat/explode/asterisk pseudo-operator # to expand Ranges. -- Aaron # --- # WARNING: Under 1.9, unless strings are 8-bit binary encoded, the # encoder may not always work. # ========================================== # NOW FOR A SUPER-SIMPLISTIC COMPARISON TEST # AGAINST RUBY'S BASE64 ENCODER/DECODER: # ========================================== ## In Ruby 1.8.7 and 1.9, [data].pack('m*') is used as the baseline ## Base-64 encoder instead of Base64::encode64(data) from 'base64' def check_it(test_input, show_data) test_encode = e(test_input) # Compare results against Base64: good_encode = [test_input].pack('m*').gsub(/[\r\n]+/,'') test_decode = test_encode.unpack('m')[0] raise "ENCODE FAILED: test encode(#{test_encode.size}) did not match " + "known good encode (#{good_encode.size})." if test_encode != good_encode raise "Decode failed" if test_input != test_decode puts "SUCCESS!" puts "TEST INPUT (#{test_input.length})" + (show_data ? ": '#{test_input}'" : '') if (test_encode.size > 64) puts "TEST ENCODE (#{test_encode.length}): '#{test_encode[0,48]}...#{test_encode[-16,16]}'" puts "BASE64 ENCODE (#{good_encode.length}): '#{good_encode[0,48]}...#{good_encode[-16,16]}'" else puts "TEST ENCODE (#{test_encode.length}): '#{test_encode}'" puts "BASE64 ENCODE (#{good_encode.length}): '#{good_encode}'" end puts "BASE64 DECODE (#{test_decode.length})" + (show_data ? ": '#{test_decode}'" : '') puts "TEST PASSED!" end # Test different lengths of input strings: check_it "1", true check_it "12", true check_it "123", true check_it "1234", true # And test some longer ones: check_it "This is the test string that should be encoded using the above code...", true check_it \ "Four score and seven years ago our fathers brought forth upon this\n" + "continent a new nation conceived in liberty and dedicated to the\n" + "proposition that all men are created equal.\n", true # On Unix hosts with /dev/urandom random generators, do a binary test: if File.exists?('/dev/urandom') TESTSIZE = 4723 f=File.open('/dev/urandom','r') s='' while(s.length