module Sixword # The Sixword::CLI class implements all of the complex processing needed for # the sixword Command Line Interface. class CLI # Exception for certain input validation errors class CLIError < StandardError; end # @return [String] Input filename attr_reader :filename # @return [Hash] Options hash attr_reader :options # @return [File, IO] Stream opened from #filename attr_reader :stream # @return [:encode, :decode] attr_reader :mode # Create a Sixword CLI to operate on filename with options # # @param filename [String] Input file name (or '-' for stdin) # @param options [Hash] # # @option options [:encode, :decode] :mode (:encode) # @option options [Boolean] :pad (false) # @option options [String] :hex_style # def initialize(filename, options) @filename = filename @options = {mode: :encode, pad: false}.merge(options) if filename == '-' # dup stdin and put it into binary mode @stream = $stdin.dup @stream.binmode else # read in binary mode even on unix so ruby yields binary encoding @stream = File.open(filename, 'rb') end @mode = @options.fetch(:mode) unless [:encode, :decode].include?(mode) raise ArgumentError.new("Invalid mode: #{mode.inspect}") end end # Return the value of the :pad option. # @return [Boolean] def pad? options.fetch(:pad) end # Return true if we are in encoding mode, false otherwise (decoding). # @return [Boolean] def encoding? mode == :encode end # Return the value of the :hex_style option. # @return [String, nil] def hex_style options[:hex_style] end # Format data as hex in various styles. def print_hex(data, chunk_index, cols=80) case hex_style when 'lower', 'lowercase' # encode to lowercase hex with no newlines print Sixword::Hex.encode(data) when 'finger', 'fingerprint' # encode to GPG fingerprint like hex with newlines newlines_every = cols / 5 if chunk_index != 0 if chunk_index % newlines_every == 0 print "\n" else print ' ' end end print Sixword::Hex.encode_fingerprint(data) when 'colon', 'colons' # encode to SSL/SSH fingerprint like hex with colons print ':' unless chunk_index == 0 print Sixword::Hex.encode_colons(data) end end # Run the encoding/decoding operation, printing the result to stdout. def run! if encoding? do_encode! do |encoded| puts encoded end else chunk_index = 0 do_decode! do |decoded| if hex_style print_hex(decoded, chunk_index) chunk_index += 1 else print decoded end end # add trailing newline for hex output puts if hex_style end end private def do_decode! unless block_given? raise ArgumentError.new("block is required") end read_input_by_6_words do |arr| yield Sixword.decode(arr, padding_ok: pad?) end end def do_encode! process_encode_input do |chunk| Sixword.encode_iter(chunk, words_per_slice:6, pad:pad?) do |encoded| yield encoded end end end def process_encode_input unless block_given? raise ArgumentError.new("block is required") end if hex_style # yield data in chunks from accumulate_hex_input until EOF accumulate_hex_input do |hex| begin data = Sixword::Hex.decode(hex) rescue ArgumentError => err # expose hex decoding failures to user raise CLIError.new(err.message) else yield data end end else # yield data 8 bytes at a time until EOF while true buf = stream.read(8) if buf yield buf else # EOF break end end end end # Yield data 6 words at a time until EOF def read_input_by_6_words block_size = 2048 word_arr = [] while true buf = stream.read(block_size) if buf.nil? break # EOF end buf.scan(/\S+/) do |word| word_arr << word # return the array if we have accumulated 6 words if word_arr.length == 6 yield word_arr word_arr.clear end end end # yield whatever we have left, if anything if !word_arr.empty? yield word_arr end end def accumulate_hex_input unless block_given? raise ArgumentError.new("must pass block") end # these are actually treated the same at the moment case hex_style when 'lower', 'lowercase' when 'finger', 'fingerprint' else raise CLIError.new("unknown hex style: #{hex_style.inspect}") end while true buf = '' # try to accumulate 8 bytes (16 chars) before yielding the hex while buf.length < 16 char = stream.getc if char.nil? # EOF, so yield whatever we have if it's non-empty yield buf unless buf.empty? return end if Sixword::Hex.valid_hex?(char) buf << char next elsif Sixword::Hex.strip_char?(char) next end raise CLIError.new("invalid hex character: #{char.inspect}") end yield buf end end end end