This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source 'http://rubygems.org'
|
||||
|
||||
# Specify your gem's dependencies in Ascii85.gemspec
|
||||
gemspec
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
Executable
+112
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user