Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
+26
View File
@@ -0,0 +1,26 @@
# 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
+70
View File
@@ -0,0 +1,70 @@
# 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
+6
View File
@@ -0,0 +1,6 @@
# frozen_string_literal: true
source 'http://rubygems.org'
# Specify your gem's dependencies in Ascii85.gemspec
gemspec
+19
View File
@@ -0,0 +1,19 @@
Copyright (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.
+78
View File
@@ -0,0 +1,78 @@
**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.
+14
View File
@@ -0,0 +1,14 @@
# 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
+112
View File
@@ -0,0 +1,112 @@
#!/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
@@ -0,0 +1,5 @@
# frozen_string_literal: true
module Ascii85
VERSION = '2.0.1'
end
+469
View File
@@ -0,0 +1,469 @@
# 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
+211
View File
@@ -0,0 +1,211 @@
# 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
end
@@ -0,0 +1,263 @@
# 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