# encoding: utf-8 # frozen_string_literal: true # # Ascii85 is an implementation of Adobe's binary-to-text encoding of the # same name in pure Ruby. # # See http://www.adobe.com/products/postscript/pdfs/PLRM.pdf page 131 # and http://en.wikipedia.org/wiki/Ascii85 for more information about # the format. # # Author:: Johannes Holzfuß (johannes@holzfuss.name) # License:: Distributed under the MIT License (see LICENSE file) # module Ascii85 # # Encodes the bytes of the given String as Ascii85. # # If +wrap_lines+ evaluates to +false+, the output will be returned as # a single long line. Otherwise #encode formats the output into lines # of length +wrap_lines+ (minimum is 2). # # Ascii85.encode("Ruby") # => <~;KZGo~> # # Ascii85.encode("Supercalifragilisticexpialidocious", 15) # => <~;g!%jEarNoBkD # BoB5)0rF*),+AU& # 0.@;KXgDe!L"F`R # ~> # # Ascii85.encode("Supercalifragilisticexpialidocious", false) # => <~;g!%jEarNoBkDBoB5)0rF*),+AU&0.@;KXgDe!L"F`R~> # # def self.encode(str, wrap_lines = 80) to_encode = str.to_s return '' if to_encode.empty? # Deal with multi-byte encodings if to_encode.respond_to?(:bytesize) input_size = to_encode.bytesize else input_size = to_encode.size end # Compute number of \0s to pad the message with (0..3) padding_length = (-input_size) % 4 # Extract big-endian integers tuples = (to_encode + ("\0" * padding_length)).unpack('N*') # Encode tuples.map! do |tuple| if tuple == 0 'z' else tmp = String.new 5.times do tmp << ((tuple % 85) + 33).chr tuple /= 85 end tmp.reverse end end # We can't use the z-abbreviation if we're going to cut off padding if (padding_length > 0) and (tuples.last == 'z') tuples[-1] = '!!!!!' end # Cut off the padding tuples[-1] = tuples[-1][0..(4 - padding_length)] # If we don't need to wrap the lines, add delimiters and return if (!wrap_lines) return '<~' + tuples.join + '~>' end # Otherwise we wrap the lines line_length = [2, wrap_lines.to_i].max wrapped = [] to_wrap = '<~' + tuples.join 0.step(to_wrap.length, line_length) do |index| wrapped << to_wrap.slice(index, line_length) end # Add end-marker – on a new line if necessary if (wrapped.last.length + 2) > line_length wrapped << '~>' else wrapped[-1] << '~>' end return wrapped.join("\n") end # # Searches through +str+ and decodes the _first_ Ascii85-String found. # # #decode expects an Ascii85-encoded String enclosed in <~ and ~> — it will # ignore all characters outside these markers. The returned strings are always # encoded as ASCII-8BIT. # # Ascii85.decode("<~;KZGo~>") # => "Ruby" # # Ascii85.decode("Foo<~;KZGo~>Bar<~;KZGo~>Baz") # => "Ruby" # # Ascii85.decode("No markers") # => "" # # #decode will raise Ascii85::DecodingError when malformed input is # encountered. # def self.decode(str) input = str.to_s opening_delim = '<~' closing_delim = '~>' # Make sure the delimiter strings have the correct encoding. # # Although I don't think it likely, this may raise encoding # errors if an especially exotic input encoding is introduced. # As of Ruby 1.9.2 all non-dummy encodings work fine though. # if opening_delim.respond_to?(:encode) opening_delim = opening_delim.encode(input.encoding) closing_delim = closing_delim.encode(input.encoding) end # Get the positions of the opening/closing delimiters. If there is # no pair of opening/closing delimiters, return the empty string. (start_pos = input.index(opening_delim)) or return '' (end_pos = input.index(closing_delim, start_pos + 2)) or return '' # Get the string inside the delimiter-pair input = input[(start_pos + 2)...end_pos] # Decode word = 0 count = 0 result = [] input.each_byte do |c| case c.chr when " ", "\t", "\r", "\n", "\f", "\0" # Ignore whitespace next when 'z' if count == 0 # Expand z to 0-word result << 0 else raise(Ascii85::DecodingError, "Found 'z' inside Ascii85 5-tuple") end when '!'..'u' # Decode 5 characters into a 4-byte word word += (c - 33) * 85**(4 - count) count += 1 if count == 5 if word > 0xffffffff raise(Ascii85::DecodingError, "Invalid Ascii85 5-tuple (#{word} >= 2**32)") end result << word word = 0 count = 0 end else raise(Ascii85::DecodingError, "Illegal character inside Ascii85: #{c.chr.dump}") end end # Convert result into a String result = result.pack('N*') if count > 0 # Finish last, partially decoded 32-bit-word if count == 1 raise(Ascii85::DecodingError, "Last 5-tuple consists of single character") end count -= 1 word += 85**(4 - count) result << ((word >> 24) & 255).chr if count >= 1 result << ((word >> 16) & 255).chr if count >= 2 result << ((word >> 8) & 255).chr if count == 3 end return result end # # This error is raised when Ascii85.decode encounters one of the following # problems in the input: # # * An invalid character. Valid characters are '!'..'u' and 'z'. # * A 'z' character inside a 5-tuple. 'z's are only valid on their own. # * An invalid 5-tuple that decodes to >= 2**32 # * The last tuple consisting of a single character. Valid tuples always have # at least two characters. # class DecodingError < StandardError; end end