[SOLUTION] Whiteout (#34)

This was a fun one. If I would consider anything my Ruby forte, text
processing would be it. So this was right up my alley. I learned a good
bit too. For example, Fixnum#to_s can take a radix representing the base
you want the number converted to in the String. String#to_i does the
same thing, just in the opposite direction.

I first wrote a simple binary conversion that was inspired by what I
could figure out from the original Perl ACME::Bleach (which wasn't too
much since I'm not a Perl hacker and it was somewhat obfuscated.) Then I
  thought I could probably one-up that by making a ternary conversion. I
considered trying higher radixes, but found at least on my editor (Vim)
that only spaces, tabs and newlines were truly "invisible." So ternary
it was, as shown in the code below.

Since I had written two conversions, I decided to make things
interesting and randomly choose which one I used when creating the
files. That should thoroughly confuse people who try and decode any
files that have been "whited out" without knowing the code :slight_smile:

Anyhow, here is the code (if this weren't a Ruby Quiz I would make this
code much more compact and obfuscated):

# Ruby Quiz: Whiteout (#34)
# Solution by Ryan Leavengood

···

#
# There are two ways of "whiting out", one that uses a binary
# encoding of spaces and tabs on each line (preserving the
# original newlines), and a ternary encoding that makes newlines
# part of the code and encodes any of the original newlines. The
# method of encoding is chosen at random. In theory other
# non-printable characters could be added to increase the radix
# used for encoding, but I think the best cross-platform "whiting
# out" can be had using spaces, tabs and newlines.

REQUIRE_LINE = "require 'whiteout'"

class WhiteoutBinary
   attr_reader :id

   WHITESPACE = " \t"
   DIGITS = '01'

   def initialize
     @id = " \t\t"
   end

   def paint_on(paper)
     paper.map do |line|
       line.chomp.unpack('b*')[0].tr(DIGITS, WHITESPACE)
     end.join("\n")
   end

   def rub_off(paper)
     paper.map do |line|
       [line.chomp.tr(WHITESPACE, DIGITS)].pack('b*')
     end.join("\n")
   end
end

class WhiteoutTernary
   attr_reader :id

   WHITESPACE = " \t\n"
   DIGITS = '012'
   # This allows up to 22222 ternary, which is 242 decimal, enough
   # for most of ASCII
   DIGIT_LENGTH = 5
   RADIX = 3

   def initialize
     @id = " \t\t\t"
   end

   def paint_on(paper)
     paper.join.gsub(/./m) do |c|
       c[0].to_s(RADIX).rjust(DIGIT_LENGTH,'0')
     end.tr(DIGITS, WHITESPACE)
   end

   def rub_off(paper)
     paper.join.tr(WHITESPACE, DIGITS).gsub(/.{#{DIGIT_LENGTH}}/) do |d|
       d.to_i(RADIX).chr
     end
   end
end

bottle_holder = [WhiteoutBinary.new, WhiteoutTernary.new]

if $0 == __FILE__
   ARGV.each do |filename|
     wo_name = "#{filename}.wo"
     File.open(wo_name, 'w') do |file|
       whiteout = bottle_holder[rand(2)]
       paper = IO.readlines(filename)
       if paper[0] =~ /^\s*#!/
         file.print paper.shift
       end
       file.puts REQUIRE_LINE
       file.puts whiteout.id
       file.print whiteout.paint_on(paper)
     end
     File.rename(filename, filename+'.bak')
     File.rename(wo_name, filename)
   end
else
   paper = IO.readlines($0)
   paper.shift if paper[0] =~ /^\s*#!/
   paper.shift if paper[0] =~ /^#{REQUIRE_LINE}/
   id = paper.shift.chomp
   whiteout = bottle_holder.find {|bottle| bottle.id == id}
   if whiteout
     eval whiteout::rub_off(paper)
   else
     puts "Error: This does not appear to be a valid whiteout file!"
     exit(1)
   end
end
__END__

Ryan Leavengood

Ryan Leavengood <mrcode@netrox.net> writes:

Anyhow, here is the code (if this weren't a Ruby Quiz I would make this
code much more compact and obfuscated):

And this is my solution, with no time spend on robustness or error
handling. It is more space efficient, though, as I use base 4:

unless caller.empty?
  eval File.read($0). # or extract from caller...
       gsub(/\A.*\0/m, '').
       tr(" \n\t\v", "0123").
       scan(/\d{4}/m).map { |s| s.to_i(4) }.
       pack("c*")
else
  require 'fileutils'
  ARGV.each { |file|
    code = File.read file
    FileUtils.copy file, file + ".dirty"
    File.open(file, "w") { |out|
      code.gsub!(/\A#!.*/) { |shebang|
        out.puts shebang
        ''
      }
      out.puts 'require "whiteout"'
      out.print "\0"
      code.each_byte { |b|
        out.print b.to_s(4).rjust(4).tr("0123", " \n\t\v")
      }
    }
  }
end

···

--
Christian Neukirchen <chneukirchen@gmail.com> http://chneukirchen.org

Here is my solution. It is quite similar to all the others already posted.

I use Zlib::Deflate to compress the source file, then the bytes are converted to base 3 and represented by spaces, tabs and newlines.
This way the result is approx. 3 times bigger than the source.

Dominik

The code:

require "zlib"

def encode_to_ws(str)
     str=Zlib::Deflate.deflate(str, 9)
     res=""
     str.each_byte { |b| res << b.to_s(3).rjust(6,"0") }
     res.tr("012", " \t\n")
end

def decode_from_ws(str)
     raise "wrong length" unless str.length%6 == 0
     str.tr!(" \t\n", "012")
     res=""
     for i in 0...(str.length/6)
         res << str[i*6, 6].to_i(3).chr
     end
     Zlib::Inflate.inflate(res)
end

if $0 == __FILE__
     if File.file?(f=ARGV[0])
         str=IO.read(f)
         File.open(f, "wb") { |out|
             if str =~ /\A#!.*/
                 out.puts $&
             end
             out.puts 'require "whiteout"'
             out.print encode_to_ws(str)
         }
     else
         puts "usage #$0 file.rb"
     end
else
     if File.file?($0)
         str=File.read($0)
         str.sub!(/\A(#!.*)?require "whiteout".*?\n/m, "")
         eval('$0=__FILE__')
         eval(decode_from_ws(str))
     else
         raise "required whiteout from non-file"
     end
end