Ascii85-2.0.1/0000755000004100000410000000000014716761566012772 5ustar www-datawww-dataAscii85-2.0.1/bin/0000755000004100000410000000000014716761566013542 5ustar www-datawww-dataAscii85-2.0.1/bin/ascii850000755000004100000410000000512314716761566014736 0ustar www-datawww-data#!/usr/bin/env ruby # frozen_string_literal: true # # A simple command-line tool to de- and encode Ascii85, modeled after `base64` # from the GNU Coreutils. # require 'optparse' require File.join(File.dirname(__FILE__), '..', 'lib', 'ascii85') require File.join(File.dirname(__FILE__), '..', 'lib', 'Ascii85', 'version') class CLI attr_reader :options def initialize(argv, stdin: $stdin, stdout: $stdout) @in = stdin @out = stdout @options = { wrap: 80, action: :encode } parse_options(argv) end def parse_options(argv) @parser = OptionParser.new do |opts| opts.banner = "Usage: #{File.basename($PROGRAM_NAME)} [OPTIONS] [FILE]\n" \ 'Encodes or decodes FILE or STDIN using Ascii85 and writes to STDOUT.' opts.on('-w', '--wrap COLUMN', Integer, 'Wrap lines at COLUMN. Default is 80, use 0 for no wrapping') do |opt| @options[:wrap] = opt.abs @options[:wrap] = false if opt.zero? end opts.on('-d', '--decode', 'Decode the input') do @options[:action] = :decode end opts.on('-h', '--help', 'Display this help and exit') do @options[:action] = :help end opts.on('-V', '--version', 'Output version information') do |_opt| @options[:action] = :version end end remaining_args = @parser.parse!(argv) case remaining_args.size when 0 @options[:file] = '-' when 1 @options[:file] = remaining_args.first else raise(OptionParser::ParseError, "Superfluous operand(s): \"#{remaining_args[1..].join('", "')}\"") end end def input fn = @options[:file] return @in.binmode if fn == '-' raise(StandardError, "File not found: \"#{fn}\"") unless File.exist?(fn) raise(StandardError, "File is not readable: \"#{fn}\"") unless File.readable_real?(fn) File.new(fn, 'rb') end def decode Ascii85.decode(input.read, out: @out) end def encode Ascii85.encode(input, @options[:wrap], out: @out) end def version "Ascii85 v#{Ascii85::VERSION},\nwritten by Johannes Holzfuß" end def help @parser end def call case @options[:action] when :help then @out.puts help when :version then @out.puts version when :encode then encode when :decode then decode end end end if File.basename($PROGRAM_NAME) == "ascii85" begin CLI.new(ARGV).call rescue OptionParser::ParseError => e abort e.message rescue Ascii85::DecodingError => e abort "Decoding Error: #{e.message}" rescue StandardError => e abort "Error: #{e.message}" end end Ascii85-2.0.1/Ascii85.gemspec0000644000004100000410000000175514716761566015554 0ustar www-datawww-data# frozen_string_literal: true require_relative 'lib/Ascii85/version' Gem::Specification.new do |s| s.name = 'Ascii85' s.version = Ascii85::VERSION s.platform = Gem::Platform::RUBY s.author = 'Johannes Holzfuß' s.email = 'johannes@holzfuss.name' s.license = 'MIT' s.homepage = 'https://github.com/DataWraith/ascii85gem/' s.summary = 'Ascii85 encoder/decoder' s.description = "Ascii85 provides methods to encode/decode Adobe's binary-to-text encoding of the same name." s.required_ruby_version = '>= 2.7.0' s.add_development_dependency 'minitest', '~> 5', '>= 5.12.0' s.add_development_dependency 'rake', '~> 13' s.files = `git ls-files`.split("\n") - ['.gitignore', '.github/workflows/ruby.yml'] s.test_files = `git ls-files -- spec/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } s.require_paths = ['lib'] s.extra_rdoc_files = ['README.md', 'LICENSE'] end Ascii85-2.0.1/lib/0000755000004100000410000000000014716761566013540 5ustar www-datawww-dataAscii85-2.0.1/lib/Ascii85/0000755000004100000410000000000014716761566014745 5ustar www-datawww-dataAscii85-2.0.1/lib/Ascii85/version.rb0000644000004100000410000000010614716761566016754 0ustar www-datawww-data# frozen_string_literal: true module Ascii85 VERSION = '2.0.1' end Ascii85-2.0.1/lib/ascii85.rb0000644000004100000410000003416114716761566015337 0ustar www-datawww-data# frozen_string_literal: true require 'stringio' # # Ascii85 is an implementation of Adobe's binary-to-text encoding of the # same name in pure Ruby. # # See 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 class << self EMPTY_STRING = ''.dup.force_encoding(Encoding::ASCII_8BIT) START_MARKER = '<~'.dup.force_encoding(Encoding::ASCII_8BIT) ENDING_MARKER = '~>'.dup.force_encoding(Encoding::ASCII_8BIT) LINE_BREAK = "\n".dup.force_encoding(Encoding::ASCII_8BIT) # # Encodes the bytes of the given String or IO-like object as Ascii85. # # @param str_or_io [String, IO] The input to encode # @param wrap_lines [Integer, false] The line length for wrapping, or +false+ for no wrapping # @param out [IO, nil] An optional IO-like object to write the output to # # @return [String, IO] The encoded String or the output IO object that was passed in # # @example Encoding a simple String # Ascii85.encode("Ruby") # # => <~;KZGo~> # # @example Encoding with line wrapping # Ascii85.encode("Supercalifragilisticexpialidocious", 15) # # => <~;g!%jEarNoBkD # # BoB5)0rF*),+AU& # # 0.@;KXgDe!L"F`R # # ~> # # @example Encoding without line wrapping # Ascii85.encode("Supercalifragilisticexpialidocious", false) # # => <~;g!%jEarNoBkDBoB5)0rF*),+AU&0.@;KXgDe!L"F`R~> # # @example Encoding from an IO-like object # input = StringIO.new("Ruby") # Ascii85.encode(input) # # => "<~;KZGo~>" # # @example Encoding to an IO object # output = StringIO.new # Ascii85.encode("Ruby", out: output) # # => output (with "<~;KZGo~>" written to it) # def encode(str_or_io, wrap_lines = 80, out: nil) reader = if io_like?(str_or_io) str_or_io else StringIO.new(str_or_io.to_s, 'rb') end return EMPTY_STRING.dup if reader.eof? # Setup buffered Reader and Writers bufreader = BufferedReader.new(reader, unencoded_chunk_size) bufwriter = BufferedWriter.new(out || StringIO.new(String.new, 'wb'), encoded_chunk_size) writer = wrap_lines ? Wrapper.new(bufwriter, wrap_lines) : DummyWrapper.new(bufwriter) padding = unfrozen_binary_copy("\0\0\0\0") tuplebuf = unfrozen_binary_copy('!!!!!') exclamations = unfrozen_binary_copy('!!!!!') z = unfrozen_binary_copy('z') bufreader.each_chunk do |chunk| chunk.unpack('N*').each do |word| # Encode each big-endian 32-bit word into a 5-character tuple (except # for 0, which encodes to 'z') if word.zero? writer.write(z) else word, b0 = word.divmod(85) word, b1 = word.divmod(85) word, b2 = word.divmod(85) word, b3 = word.divmod(85) b4 = word tuplebuf.setbyte(0, b4 + 33) tuplebuf.setbyte(1, b3 + 33) tuplebuf.setbyte(2, b2 + 33) tuplebuf.setbyte(3, b1 + 33) tuplebuf.setbyte(4, b0 + 33) writer.write(tuplebuf) end end next if (chunk.bytesize & 0b11).zero? # If we have leftover bytes, we need to zero-pad to a multiple of four # before converting to a 32-bit word. padding_length = (-chunk.bytesize) % 4 trailing = chunk[-(4 - padding_length)..] word = (trailing + padding[0...padding_length]).unpack1('N') # Encode the last word and cut off any padding if word.zero? writer.write(exclamations[0..(4 - padding_length)]) else word, b0 = word.divmod(85) word, b1 = word.divmod(85) word, b2 = word.divmod(85) word, b3 = word.divmod(85) b4 = word tuplebuf.setbyte(0, b4 + 33) tuplebuf.setbyte(1, b3 + 33) tuplebuf.setbyte(2, b2 + 33) tuplebuf.setbyte(3, b1 + 33) tuplebuf.setbyte(4, b0 + 33) writer.write(tuplebuf[0..(4 - padding_length)]) end end # If no output IO-object was provided, extract the encoded String from the # default StringIO writer. We force the encoding to 'ASCII-8BIT' to work # around a TruffleRuby bug. return writer.finish.io.string.force_encoding(Encoding::ASCII_8BIT) if out.nil? # Otherwise we make sure to flush the output writer, and then return it. writer.finish.io end # Searches through a String and extracts the first substring enclosed by '<~' and '~>'. # # @param str [String] The String to search through # # @return [String] The extracted substring, or an empty String if no valid delimiters are found # # @example Extracting Ascii85 content # Ascii85.extract("Foo<~;KZGo~>Bar<~z~>Baz") # # => ";KZGo" # # @example When no delimiters are found # Ascii85.extract("No delimiters") # # => "" # # @note This method only accepts a String, not an IO-like object, as the entire input # needs to be available to ensure validity. # def extract(str) input = str.to_s # Make sure the delimiter Strings have the correct encoding. opening_delim = '<~'.encode(input.encoding) closing_delim = '~>'.encode(input.encoding) # Get the positions of the opening/closing delimiters. If there is no pair # of opening/closing delimiters, return an unfrozen empty String. (start_pos = input.index(opening_delim)) or return EMPTY_STRING.dup (end_pos = input.index(closing_delim, start_pos + 2)) or return EMPTY_STRING.dup # Get the String inside the delimiter-pair input[(start_pos + 2)...end_pos] end # # Searches through a String and decodes the first substring enclosed by '<~' and '~>'. # # @param str [String] The String containing Ascii85-encoded content # @param out [IO, nil] An optional IO-like object to write the output to # # @return [String, IO] The decoded String (in ASCII-8BIT encoding) or the output IO object (if it was provided) # # @raise [Ascii85::DecodingError] When malformed input is encountered # # @example Decoding Ascii85 content # Ascii85.decode("<~;KZGo~>") # # => "Ruby" # # @example Decoding with multiple Ascii85 blocks present (ignores all but the first) # Ascii85.decode("Foo<~;KZGo~>Bar<~87cURDZ~>Baz") # # => "Ruby" # # @example When no delimiters are found # Ascii85.decode("No delimiters") # # => "" # # @example Decoding to an IO object # output = StringIO.new # Ascii85.decode("<~;KZGo~>", out: output) # # => output (with "Ruby" written to it) # # @note This method only accepts a String, not an IO-like object, as the entire input # needs to be available to ensure validity. # def decode(str, out: nil) decode_raw(extract(str), out: out) end # # Decodes the given raw Ascii85-encoded String or IO-like object. # # @param str_or_io [String, IO] The Ascii85-encoded input to decode # @param out [IO, nil] An optional IO-like object to write the output to # # @return [String, IO] The decoded String (in ASCII-8BIT encoding) or the output IO object (if it was provided) # # @raise [Ascii85::DecodingError] When malformed input is encountered # # @example Decoding a raw Ascii85 String # Ascii85.decode_raw(";KZGo") # # => "Ruby" # # @example Decoding from an IO-like object # input = StringIO.new(";KZGo") # Ascii85.decode_raw(input) # # => "Ruby" # # @example Decoding to an IO object # output = StringIO.new # Ascii85.decode_raw(";KZGo", out: output) # # => output (with "Ruby" written to it) # # @note The input must not be enclosed in '<~' and '~>' delimiters. # def decode_raw(str_or_io, out: nil) reader = if io_like?(str_or_io) str_or_io else StringIO.new(str_or_io.to_s, 'rb') end # Return an unfrozen String on empty input return EMPTY_STRING.dup if reader.eof? # Setup buffered Reader and Writers bufreader = BufferedReader.new(reader, encoded_chunk_size) bufwriter = BufferedWriter.new(out || StringIO.new(String.new, 'wb'), unencoded_chunk_size) # Populate the lookup table (caches the exponentiation) lut = (0..4).map { |count| 85**(4 - count) } # Decode word = 0 count = 0 zeroes = unfrozen_binary_copy("\0\0\0\0") wordbuf = zeroes.dup bufreader.each_chunk do |chunk| chunk.each_byte do |c| case c.chr when ' ', "\t", "\r", "\n", "\f", "\0" # Ignore whitespace next when 'z' raise(Ascii85::DecodingError, "Found 'z' inside Ascii85 5-tuple") unless count.zero? # Expand z to 0-word bufwriter.write(zeroes) when '!'..'u' # Decode 5 characters into a 4-byte word word += (c - 33) * lut[count] count += 1 if count == 5 && word > 0xffffffff raise(Ascii85::DecodingError, "Invalid Ascii85 5-tuple (#{word} >= 2**32)") elsif count == 5 b3 = word & 0xff; word >>= 8 b2 = word & 0xff; word >>= 8 b1 = word & 0xff; word >>= 8 b0 = word wordbuf.setbyte(0, b0) wordbuf.setbyte(1, b1) wordbuf.setbyte(2, b2) wordbuf.setbyte(3, b3) bufwriter.write(wordbuf) word = 0 count = 0 end else raise(Ascii85::DecodingError, "Illegal character inside Ascii85: #{c.chr.dump}") end end end # We're done if all 5-tuples have been consumed if count.zero? bufwriter.flush return out || bufwriter.io.string.force_encoding(Encoding::ASCII_8BIT) end raise(Ascii85::DecodingError, 'Last 5-tuple consists of single character') if count == 1 # Finish last, partially decoded 32-bit word count -= 1 word += lut[count] bufwriter.write((word >> 24).chr) if count >= 1 bufwriter.write(((word >> 16) & 0xff).chr) if count >= 2 bufwriter.write(((word >> 8) & 0xff).chr) if count == 3 bufwriter.flush out || bufwriter.io.string.force_encoding(Encoding::ASCII_8BIT) end private # Copies the given String and forces the encoding of the returned copy to # be Encoding::ASCII_8BIT. def unfrozen_binary_copy(str) str.dup.force_encoding(Encoding::ASCII_8BIT) end # Buffers an underlying IO object to increase efficiency. You do not need # to use this directly. # # @private # class BufferedReader def initialize(io, buffer_size) @io = io @buffer_size = buffer_size end def each_chunk return enum_for(:each_chunk) unless block_given? until @io.eof? chunk = @io.read(@buffer_size) yield chunk if chunk end end end # Buffers an underlying IO object to increase efficiency. You do not need # to use this directly. # # @private # class BufferedWriter attr_accessor :io def initialize(io, buffer_size) @io = io @buffer_size = buffer_size @buffer = String.new(capacity: buffer_size, encoding: Encoding::ASCII_8BIT) end def write(tuple) flush if @buffer.bytesize + tuple.bytesize > @buffer_size @buffer << tuple end def flush @io.write(@buffer) @buffer.clear end end # Wraps the input in '<~' and '~>' delimiters and passes it through # unmodified to the underlying IO object otherwise. You do not need to # use this directly. # # @private # class DummyWrapper def initialize(out) @out = out @out.write(START_MARKER) end def write(buffer) @out.write(buffer) end def finish @out.write(ENDING_MARKER) @out.flush @out end end # Wraps the input in '<~' and '~>' delimiters and ensures that no line is # longer than the specified length. You do not need to use this directly. # # @private # class Wrapper def initialize(out, wrap_lines) @line_length = [2, wrap_lines.to_i].max @out = out @out.write(START_MARKER) @cur_len = 2 end def write(buffer) loop do s = buffer.bytesize if @cur_len + s < @line_length @out.write(buffer) @cur_len += s return end remaining = @line_length - @cur_len @out.write(buffer[0...remaining]) @out.write(LINE_BREAK) @cur_len = 0 buffer = buffer[remaining..] return if buffer.empty? end end def finish # Add the closing delimiter (may need to be pushed to the next line) @out.write(LINE_BREAK) if @cur_len + 2 > @line_length @out.write(ENDING_MARKER) @out.flush @out end end # Check if an object is IO-like # # @private # def io_like?(obj) obj.respond_to?(:read) && obj.respond_to?(:eof?) end # @return [Integer] Buffer size for to-be-encoded input # def unencoded_chunk_size 4 * 2048 end # @return [Integer] Buffer size for encoded output # def encoded_chunk_size 5 * 2048 end end # # Error raised when Ascii85 encounters problems while decoding the input. # # This error is raised for the following issues: # * An invalid character (valid characters are '!'..'u' and 'z') # * A 'z' character inside a 5-tuple ('z' is only valid on its 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 Ascii85-2.0.1/spec/0000755000004100000410000000000014716761566013724 5ustar www-datawww-dataAscii85-2.0.1/spec/bin/0000755000004100000410000000000014716761566014474 5ustar www-datawww-dataAscii85-2.0.1/spec/bin/cli_spec.rb0000644000004100000410000001166714716761566016615 0ustar www-datawww-data# frozen_string_literal: true require 'stringio' require 'tempfile' require 'minitest/autorun' # We can't require the executable file because it doesn't # have the '.rb' extension, so we have to load it. unless defined?(CLI) load File.join(__dir__, '..','..', 'bin', 'ascii85') end describe 'CLI' do it 'should recognize the -h and --help options' do [%w[-h], %w[--help]].each do |args| cli = CLI.new(args) assert_equal :help, cli.options[:action] end end it 'should recognize the -V and --version options' do [%w[-V], %w[--version]].each do |args| cli = CLI.new(args) assert_equal :version, cli.options[:action] end end it 'should complain about superfluous arguments' do assert_raises(OptionParser::ParseError) do CLI.new(%w[foo bar]) end end describe 'wrap' do it 'should default to wrapping at 80 characters' do cli = CLI.new([]) assert_equal 80, cli.options[:wrap] end it 'should recognize the -w and --wrap options' do [%w[-w 17], %w[--wrap 17]].each do |args| cli = CLI.new(args) assert_equal 17, cli.options[:wrap] end end it 'should recognize the no-wrapping setting' do cli = CLI.new(%w[-w 0]) assert_equal false, cli.options[:wrap] end it 'should raise an error if the wrap option is not an integer' do assert_raises(OptionParser::ParseError) do CLI.new(%w[-w foo]) end end end describe 'encoding' do it 'should encode from STDIN' do stdin = StringIO.new('Ruby') stdout = StringIO.new CLI.new([], stdin: stdin, stdout: stdout).call assert_equal '<~;KZGo~>', stdout.string end it 'should accept "-" as a file name' do stdin = StringIO.new('Ruby') stdout = StringIO.new CLI.new(['-'], stdin: stdin, stdout: stdout).call assert_equal '<~;KZGo~>', stdout.string end it 'should encode a file' do begin f = Tempfile.create('ascii85_encode') f.write('Ruby') f.close stdout = StringIO.new CLI.new([f.path], stdout: stdout).call assert_equal '<~;KZGo~>', stdout.string ensure File.unlink(f.path) end end it 'should wrap lines' do begin f = Tempfile.create('ascii85_wrap') f.write('a' * 20) f.close stdout = StringIO.new CLI.new([f.path, '-w2'], stdout: stdout).call assert stdout.string.lines.all? { |l| l.chomp.length <= 2 } ensure File.unlink(f.path) end end it 'should fail when the input file is not found' do assert_raises(StandardError) do CLI.new(['./foo/bar/baz']).call end end it 'should fail when the input file is not readable' do begin f = Tempfile.create('ascii85_encode') f.chmod(0o000) assert_raises(StandardError) do CLI.new([f.path]).call end ensure File.unlink(f.path) end end end describe 'decoding' do it 'should decode from STDIN' do stdin = StringIO.new('<~;KZGo~>') stdout = StringIO.new CLI.new(['-d'], stdin: stdin, stdout: stdout).call assert_equal 'Ruby', stdout.string end it 'should accept "-" as a file name' do stdin = StringIO.new('<~;KZGo~>') stdout = StringIO.new CLI.new(['-d','-'], stdin: stdin, stdout: stdout).call assert_equal 'Ruby', stdout.string end it 'should decode a file' do begin f = Tempfile.create('ascii85_decode') f.write('<~;KZGo~>') f.close stdout = StringIO.new CLI.new(['-d', f.path], stdout: stdout).call assert_equal 'Ruby', stdout.string ensure File.unlink(f.path) end end it 'should fail when the input file is not found' do assert_raises(StandardError) do CLI.new(['-d', './foo/bar/baz']).call end end it 'should fail when the input file is not readable' do begin f = Tempfile.create('ascii85_decode') f.chmod(0o000) assert_raises(StandardError) do CLI.new(['-d', f.path]).call end ensure File.unlink(f.path) end end describe 'invalid input' do it 'should return the empty string when the input does not have delimiters' do stdin = StringIO.new('No delimiters') stdout = StringIO.new CLI.new(['-d'], stdin: stdin, stdout: stdout).call assert_equal '', stdout.string end ERROR_CASES = [ '<~!!y!!~>', '<~!!z!!~>', '<~s8W-#~>', '<~!~>', ] it 'should raise an error when invalid input is encountered' do ERROR_CASES.each do |input| stdin = StringIO.new(input) stdout = StringIO.new assert_raises(Ascii85::DecodingError) do CLI.new(['-d'], stdin: stdin, stdout: stdout).call end end end end end endAscii85-2.0.1/spec/lib/0000755000004100000410000000000014716761566014472 5ustar www-datawww-dataAscii85-2.0.1/spec/lib/ascii85_spec.rb0000644000004100000410000002112414716761566017276 0ustar www-datawww-data# frozen_string_literal: true require 'minitest/autorun' require 'stringio' # Require implementation require File.expand_path('../../lib/ascii85', __dir__) TEST_CASES = { '' => '', ' ' => '<~+9~>', "\0" * 1 => '<~!!~>', "\0" * 2 => '<~!!!~>', "\0" * 3 => '<~!!!!~>', "\0" * 4 => '<~z~>', "\0" * 5 => '<~z!!~>', "A\0\0\0\0" => '<~5l^lb!!~>', # No z-abbreviation! 'A' => '<~5l~>', 'AB' => '<~5sb~>', 'ABC' => '<~5sdp~>', 'ABCD' => '<~5sdq,~>', 'ABCDE' => '<~5sdq,70~>', 'ABCDEF' => '<~5sdq,77I~>', 'ABCDEFG' => '<~5sdq,77Kc~>', 'ABCDEFGH' => '<~5sdq,77Kd<~>', 'ABCDEFGHI' => '<~5sdq,77Kd<8H~>', 'Ascii85' => '<~6$$OMBfIs~>', 'Antidisestablishmentarianism' => '<~6#LdYA8-*rF*(i"Ch[s(D.RU,@<-\'jDJ=0/~>', # Dōmo arigatō, Mr. Roboto (according to Wikipedia) 'どうもありがとうミスターロボット' => '<~j+42iJVN3:K&_E6j+<0KJW/W?W8iG`j+EuaK"9on^Z0sZj+FJoK:LtSKB%T?~>', [Math::PI].pack('G') => '<~5RAV2<(&;T~>', [Math::E].pack('G') => '<~5R"n0M\\K6,~>', # Minified example from Github issue 8. # Note that OT and OU as the trailing characters are equivalent. "\x9B\xB6\xB9+\x91" => '<~S$ojXOT~>' }.freeze describe Ascii85 do it '#decode should be the inverse of #encode' do # Generate a test string that contains all possible bytes test_str = String.new (0..255).each do |c| test_str << c.chr end encoded = Ascii85.encode(test_str) decoded = Ascii85.decode(encoded) assert_equal test_str, decoded end describe '#encode' do it 'should encode all specified test-cases correctly' do TEST_CASES.each_pair do |input, encoded| assert_equal encoded, Ascii85.encode(input) end end it 'should always return unfrozen Strings' do TEST_CASES.each_pair do |input, encoded| assert_equal false, Ascii85.encode(input).frozen? end end it 'should encode Strings in different encodings correctly' do input_euc_jp = 'どうもありがとうミスターロボット'.encode('EUC-JP') input_binary = input_euc_jp.force_encoding('ASCII-8BIT') assert_equal Ascii85.encode(input_binary), Ascii85.encode(input_euc_jp) end it 'should produce output lines no longer than specified' do test_str = '0123456789' * 30 # # No wrap # assert_equal 0, Ascii85.encode(test_str, false).count("\n") # # x characters per line, except for the last one # (2..12).each do |x| encoded = Ascii85.encode(test_str, x) # Determine the length of all lines count_arr = [] encoded.each_line do |line| count_arr << line.chomp.length end # The last line is allowed to be shorter than x, so remove it count_arr.pop if count_arr.last <= x # If the end-marker is on a line of its own, the next-to-last line is # allowed to be shorter than specified by exactly one character count_arr.pop if (encoded[-3].chr =~ /[\r\n]/) && (count_arr.last == x - 1) # Remove all line-lengths that are of length x from count_arr count_arr.delete_if { |len| len == x } # Now count_arr should be empty assert_empty count_arr end end it 'should not split the end-marker to achieve correct line length' do assert_equal "<~z\n~>", Ascii85.encode("\0" * 4, 4) end it 'should encode to an IO object when provided' do output = StringIO.new result = Ascii85.encode('Ruby', out: output) assert_equal output, result assert_equal '<~;KZGo~>', output.string end it 'should encode from an IO object' do input = StringIO.new('Ruby') result = Ascii85.encode(input) assert_equal '<~;KZGo~>', result end end describe '#extract' do it 'should extract data within delimiters only' do assert_empty Ascii85.extract('<~~>') assert_empty Ascii85.extract("Doesn't contain delimiters") assert_empty Ascii85.extract('Mismatched ~> delimiters 1') assert_empty Ascii85.extract('Mismatched <~ delimiters 2') assert_empty Ascii85.extract('Mismatched ~><~ delimiters 3') assert_equal ';KZGo', Ascii85.extract('<~;KZGo~><~z~>') assert_equal 'z', Ascii85.extract('FooBar<~z~>BazQux') end end describe '#decode' do it 'should decode all specified test-cases correctly' do TEST_CASES.each_pair do |decoded, input| assert_equal decoded.dup.force_encoding('ASCII-8BIT'), Ascii85.decode(input) end end it 'should always return unfrozen Strings' do TEST_CASES.each_pair do |input, encoded| assert_equal false, Ascii85.decode(encoded).frozen? end end it 'should accept valid input in encodings other than the default' do input = 'Ragnarök τέχνη русский язык I ♥ Ruby' input_ascii85 = Ascii85.encode(input) # Try to encode input_ascii85 in all possible encodings and see if we # do the right thing in #decode. Encoding.list.each do |encoding| next if encoding.dummy? next unless encoding.ascii_compatible? # CP949 is a Microsoft Codepage for Korean, which apparently does not # include a backslash, even though #ascii_compatible? returns true. This # leads to an Ascii85::DecodingError, so we simply skip the encoding. next if encoding.name == 'CP949' begin to_test = input_ascii85.encode(encoding) assert_equal input, Ascii85.decode(to_test).force_encoding('UTF-8') rescue Encoding::ConverterNotFoundError # Ignore this encoding end end end it 'should only process data within delimiters' do assert_empty Ascii85.decode('<~~>') assert_empty Ascii85.decode("Doesn't contain delimiters") assert_empty Ascii85.decode('Mismatched ~> delimiters 1') assert_empty Ascii85.decode('Mismatched <~ delimiters 2') assert_empty Ascii85.decode('Mismatched ~><~ delimiters 3') assert_equal 'Ruby', Ascii85.decode('<~;KZGo~><~z~>') assert_equal "\0\0\0\0", Ascii85.decode('FooBar<~z~>BazQux') end it 'should ignore whitespace' do decoded = Ascii85.decode("<~6 #LdYA\r\08\n \n\n- *rF*(i\"Ch[s \t(D.RU,@ <-\'jDJ=0\f/~>") assert_equal 'Antidisestablishmentarianism', decoded end it 'should return ASCII-8BIT encoded strings' do assert_equal 'ASCII-8BIT', Ascii85.decode('<~;KZGo~>').encoding.name end it 'should decode to an IO object when provided' do output = StringIO.new result = Ascii85.decode('<~;KZGo~>', out: output) assert_equal output, result assert_equal 'Ruby', output.string end describe 'Error conditions' do it 'should raise DecodingError if it encounters a word >= 2**32' do assert_raises(Ascii85::DecodingError) { Ascii85.decode('<~s8W-#~>') } end it 'should raise DecodingError if it encounters an invalid character' do assert_raises(Ascii85::DecodingError) { Ascii85.decode('<~!!y!!~>') } end it 'should raise DecodingError if the last tuple consists of a single character' do assert_raises(Ascii85::DecodingError) { Ascii85.decode('<~!~>') } end it 'should raise DecodingError if a z is found inside a 5-tuple' do assert_raises(Ascii85::DecodingError) { Ascii85.decode('<~!!z!!~>') } end end end describe '#decode_raw' do it 'should decode raw Ascii85 without delimiters' do TEST_CASES.each_pair do |decoded, input| raw_input = input[2...-2] # Remove '<~' and '~>' assert_equal decoded.dup.force_encoding('ASCII-8BIT'), Ascii85.decode_raw(raw_input) end end it 'should always return unfrozen Strings' do TEST_CASES.each_pair do |decoded, input| raw_input = input[2...-2] # Remove '<~' and '~>' assert_equal false, Ascii85.decode_raw(raw_input).frozen? end end it 'should decode from an IO object' do input = StringIO.new(';KZGo') result = Ascii85.decode_raw(input) assert_equal 'Ruby', result end it 'should decode to an IO object when provided' do output = StringIO.new result = Ascii85.decode_raw(';KZGo', out: output) assert_equal output, result assert_equal 'Ruby', output.string end it 'should raise DecodingError for invalid input' do assert_raises(Ascii85::DecodingError) { Ascii85.decode_raw('s8W-#') } assert_raises(Ascii85::DecodingError) { Ascii85.decode_raw('!!y!!') } assert_raises(Ascii85::DecodingError) { Ascii85.decode_raw('!') } assert_raises(Ascii85::DecodingError) { Ascii85.decode_raw('!!z!!') } end end end Ascii85-2.0.1/Rakefile0000644000004100000410000000036214716761566014440 0ustar www-datawww-data# frozen_string_literal: true require 'bundler' Bundler::GemHelper.install_tasks require 'rake/testtask' Rake::TestTask.new do |t| t.test_files = FileList['spec/**/*_spec.rb'] end task specs: :test task tests: :test task default: :test Ascii85-2.0.1/Gemfile0000644000004100000410000000017214716761566014265 0ustar www-datawww-data# frozen_string_literal: true source 'http://rubygems.org' # Specify your gem's dependencies in Ascii85.gemspec gemspec Ascii85-2.0.1/LICENSE0000644000004100000410000000204514716761566014000 0ustar www-datawww-dataCopyright (c) 2009 Johannes Holzfuß Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Ascii85-2.0.1/README.md0000644000004100000410000000445314716761566014257 0ustar www-datawww-data**Status**: This project is feature-complete. With the exception of fixes to reported bugs, no further development will take place. # Ascii85 ## Description Ascii85 is a Ruby gem that provides methods for encoding/decoding Adobe's binary-to-text encoding of the same name. See the Adobe PostScript Language Reference ([archived version][PLRM]) page 131 and [Wikipedia](https://en.wikipedia.org/wiki/Ascii85) for more information about the format. [PLRM]: https://web.archive.org/web/20161222092741/https://www.adobe.com/products/postscript/pdfs/PLRM.pdf ## Installation `$ gem install Ascii85` > [!IMPORTANT] > Note that the gem name is capitalized. ## Usage ```ruby require 'ascii85' Ascii85.encode("Ruby") => "<~;KZGo~>" Ascii85.decode("<~;KZGo~>") => "Ruby" Ascii85.extract("Foo<~;KZGo~>Bar") => ";KZGo" Ascii85.decode_raw(";KZGo") => "Ruby" ``` In addition, `Ascii85.encode` can take a second parameter that specifies the length of the returned lines. The default is 80; use `false` for unlimited. `Ascii85.decode` expects the input to be enclosed in `<~` and `~>` — it ignores everything outside of these, while `Ascii85.decode_raw` assumes that the entire String passed in is encoded in Ascii85. If you need to, you can use `Ascii85.extract` to find and extract the first substring of the input that is enclosed by the `<~` and `~>` delimiters. The output of `Ascii85.decode` and `Ascii85.decode_raw` will be a String that has the `ASCII-8BIT` encoding, so you may have to use `String#force_encoding` to convert it to the desired encoding. For further options, see the [Documentation](https://www.rubydoc.info/gems/Ascii85/). ## Command-line utility This gem includes `ascii85`, a command-line utility modeled after `base64` from the GNU coreutils. It can be used to encode/decode Ascii85 directly from the command-line: ``` Usage: ascii85 [OPTIONS] [FILE] Encodes or decodes FILE or STDIN using Ascii85 and writes to STDOUT. -w, --wrap COLUMN Wrap lines at COLUMN. Default is 80, use 0 for no wrapping -d, --decode Decode the input -h, --help Display this help and exit -V, --version Output version information ``` ## License Ascii85 is distributed under the MIT License. See the accompanying LICENSE file for details. Ascii85-2.0.1/CHANGELOG.md0000644000004100000410000000311614716761566014604 0ustar www-datawww-data# Ascii85 Changelog ## [2.0.1] - 2024-09-15 ### Fixed - Decoding binary data could lead to Encoding errors (Issue #8) ## [2.0.0] - 2024-08-20 ### BREAKING CHANGES - The minimum required Ruby version has been raised to 2.7.0. ### Added - `Ascii85.decode_raw` method that doesn't expect the input to be wrapped in `<~` and `~>` delimiters. - `Ascii85.extract` method to extract encoded text from between `<~` and `~>` for feeding to `#decode_raw`. - Option to pass an IO-object as input to `#encode` and `#decode_raw` instead of a String. - Option to pass an IO-object to `#encode` and `#decode_raw` for output. Output is written to the object instead of being returned as a String. - Streaming capability for `#encode` and `#decode_raw` when both input and output are IO objects, using constant memory. ## [1.1.1] - 2024-05-09 ### Fixed - Make `bin/ascii85` Ruby 3.2-compatible (thanks @tylerwillingham) - Slightly improved error handling of `bin/ascii85` ## [1.1.0] - 2020-11-11 ### Added - Make use of frozen_string_literal (thanks @aliismayilov) ### Changed - Updated tests to use newer minitest syntax ## [1.0.3] - 2018-01-25 ### Changed - Updated the gem's metadata ## [1.0.2] - 2012-09-16 ### Changed - Changed test runner from RSpec to MiniSpec - Added support for rubygems-test - Minor changes to make packaging easier ## [1.0.1] - 2011-05-05 ### Changed - Removed `hoe` dependency in favor of `bundler` - Minor corrections in the documentation ## [1.0.0] - 2009-12-25 ### Added - Ruby 1.9 compatibility - Command-line en- and decoder ## [0.9.0] - 2009-02-17 - Initial release