class Net::SSH::Buffer

class can be quite handy.
are ever implementing a protocol on top of SSH (e.g. SFTP), this buffer
with these buffer objects directly, but it could happen. Also, if you
As a consumer of the Net::SSH library, you will rarely come into contact
taking up where the last left off.
byte of the buffer and increments the read cursor, with subsequent reads
read cursor is. Reading, on the other hand, always begins at the first
Writing to a buffer always appends to the end, regardless of where the
for building binary packets given a signature.
reading data items from the buffer, as well as a useful helper method
data packets. It provides a stream-like interface for sequentially
Net::SSH::Buffer is a flexible class for building and parsing binary

def self.from(*args)

easier to write multiple values of the same type in a briefer manner.
Any of these, except for :raw, accepts an Array argument, to make it

* :key => write an SSH-encoded key value (#write_key)
* :bignum => write an SSH-encoded bignum (#write_bignum)
* :bool => write a single byte, interpreted as a boolean (#write_bool)
* :mstring => same as string, but caller cannot resuse the string, avoids potential duplication (#write_moved)
* :string => write a 4-byte length followed by character data (#write_string)
* :byte => write a single byte (#write_byte)
* :long => write a 4-byte integer (#write_long)
* :int64 => write an 8-byte integer (#write_int64)
* :raw => write the next value verbatim (#write)

The supported data types are:

#-> "\1\0\0\0\5hello\1\2\3\4"
b = Buffer.from(:byte, 1, :string, "hello", :raw, "\1\2\3\4")

to the hash.
data that follows. If the type is :raw, the value is written directly
first of each pair of arguments being a symbol naming the type of the
from a single command. The arguments must be even in length, with the
This is a convenience method for creating and populating a new buffer
def self.from(*args)
  raise ArgumentError, "odd number of arguments given" unless args.length % 2 == 0
  buffer = new
  0.step(args.length - 1, 2) do |index|
    type = args[index]
    value = args[index + 1]
    if type == :raw
      buffer.append(value.to_s)
    elsif Array === value
      buffer.send("write_#{type}", *value)
    else
      buffer.send("write_#{type}", value)
    end
  end
  buffer
end

def ==(buffer)

are identical in size and content.
Compares the contents of the two buffers, returning +true+ only if they
def ==(buffer)
  to_s == buffer.to_s
end

def append(text)

read position. Returns the buffer object itself.
Appends the given text to the end of the buffer. Does not alter the
def append(text)
  @content << text
  self
end

def available

remain between the current position and the end of the buffer).
Returns the number of bytes available to be read (e.g., how many bytes
def available
  length - position
end

def clear!

0.
Resets the buffer, making it empty. Also, resets the read position to
def clear!
  @content = String.new
  @position = 0
end

def consume!(n = position)

Returns the buffer object itself.

would otherwise tend to grow without bound.
to be appended. It helps to keep the size of buffers down when they
buffer that has previously been read, when you are expecting more data
unless otherwise specified. This is useful for removing data from the
Consumes n bytes from the buffer, where n is the current position
def consume!(n = position)
  if n >= length
    # optimize for a fairly common case
    clear!
  elsif n > 0
    @content = @content[n..-1] || String.new
    @position -= n
    @position = 0 if @position < 0
  end
  self
end

def empty?

Returns +true+ if the buffer contains no data (e.g., it is of zero length).
def empty?
  @content.empty?
end

def eof?

reads will return nil, in this case.
Returns true if the pointer is at the end of the buffer. Subsequent
def eof?
  @position >= length
end

def initialize(content = String.new)

is initialized to the beginning of the buffer.
Creates a new buffer, initialized to the given content. The position
def initialize(content = String.new)
  @content = content.to_s
  @position = 0
end

def length

Returns the length of the buffer's content.
def length
  @content.length
end

def read(count = nil)

text in the buffer. This method will increment the pointer.
the read position. If +count+ is +nil+, this will return all remaining
Reads and returns the next +count+ bytes from the buffer, starting from
def read(count = nil)
  count ||= length
  count = length - @position if @position + count > length
  @position += count
  @content[@position - count, count]
end

def read!(count = nil)

and then consumes (as #consume!) all data up to the new read position.
Reads (as #read) and returns the given number of bytes from the buffer,
def read!(count = nil)
  data = read(count)
  consume!
  data
end

def read_all(&block)

Calls block(self) until the buffer is empty, and returns all results.
def read_all(&block)
  Enumerator.new { |e| e << yield(self) until eof? }.to_a
end

def read_bignum

binary format.
essentially just a string, which is reinterpreted to be a bignum in
Read a bignum (OpenSSL::BN) from the buffer, in SSH2 format. It is
def read_bignum
  data = read_string
  return unless data
  OpenSSL::BN.new(data, 2)
end

def read_bool

(i.e., zero is false, non-zero is true).
Read a single byte and convert it into a boolean, using 'C' rules
def read_bool
  b = read_byte or return nil
  b != 0
end

def read_buffer

object that wraps it.
Reads the next string from the buffer, and returns a new Buffer
def read_buffer
  Buffer.new(read_string)
end

def read_byte

the end of the buffer.
Read and return the next byte in the buffer. Returns nil if called at
def read_byte
  b = read(1) or return nil
  b.getbyte(0)
end

def read_int64

buffer.
Returns nil if there are less than 8 bytes remaining to be read in the
Return the next 8 bytes as a 64-bit integer (in network byte order).
def read_int64
  hi = read_long or return nil
  lo = read_long or return nil
  return (hi << 32) + lo
end

def read_key

type that was read.
describing its type. The remainder of the key is defined by the
Read a key from the buffer. The key will start with a string
def read_key
  type = read_string
  return (type ? read_keyblob(type) : nil)
end

def read_keyblob(type)

a key. Only RSA, DSA, and ECDSA keys are supported.
Read a keyblob of the given type from the buffer, and return it as
def read_keyblob(type)
  case type
  when /^(.*)-cert-v01@openssh\.com$/
    key = Net::SSH::Authentication::Certificate.read_certblob(self, $1)
  when /^ssh-dss$/
    p = read_bignum
    q = read_bignum
    g = read_bignum
    pub_key = read_bignum
    asn1 = OpenSSL::ASN1::Sequence.new(
      [
        OpenSSL::ASN1::Sequence.new(
          [
            OpenSSL::ASN1::ObjectId.new('DSA'),
            OpenSSL::ASN1::Sequence.new(
              [
                OpenSSL::ASN1::Integer.new(p),
                OpenSSL::ASN1::Integer.new(q),
                OpenSSL::ASN1::Integer.new(g)
              ]
            )
          ]
        ),
        OpenSSL::ASN1::BitString.new(OpenSSL::ASN1::Integer.new(pub_key).to_der)
      ]
    )
    key = OpenSSL::PKey::DSA.new(asn1.to_der)
  when /^ssh-rsa$/
    e = read_bignum
    n = read_bignum
    asn1 = OpenSSL::ASN1::Sequence(
      [
        OpenSSL::ASN1::Integer(n),
        OpenSSL::ASN1::Integer(e)
      ]
    )
    key = OpenSSL::PKey::RSA.new(asn1.to_der)
  when /^ssh-ed25519$/
    Net::SSH::Authentication::ED25519Loader.raiseUnlessLoaded("unsupported key type `#{type}'")
    key = Net::SSH::Authentication::ED25519::PubKey.read_keyblob(self)
  when /^ecdsa\-sha2\-(\w*)$/
    key = OpenSSL::PKey::EC.read_keyblob($1, self)
  else
    raise NotImplementedError, "unsupported key type `#{type}'"
  end
  return key
end

def read_long

buffer.
Returns nil if there are less than 4 bytes remaining to be read in the
Return the next four bytes as a long integer (in network byte order).
def read_long
  b = read(4) or return nil
  b.unpack("N").first
end

def read_private_keyblob(type)

def read_private_keyblob(type)
  case type
  when /^ssh-rsa$/
    n = read_bignum
    e = read_bignum
    d = read_bignum
    iqmp = read_bignum
    p = read_bignum
    q = read_bignum
    _unkown1 = read_bignum
    _unkown2 = read_bignum
    dmp1 = d % (p - 1)
    dmq1 = d % (q - 1)
    # Public key
    data_sequence = OpenSSL::ASN1::Sequence([
                                              OpenSSL::ASN1::Integer(n),
                                              OpenSSL::ASN1::Integer(e)
                                            ])
    if d && p && q && dmp1 && dmq1 && iqmp
      data_sequence = OpenSSL::ASN1::Sequence([
                                                OpenSSL::ASN1::Integer(0),
                                                OpenSSL::ASN1::Integer(n),
                                                OpenSSL::ASN1::Integer(e),
                                                OpenSSL::ASN1::Integer(d),
                                                OpenSSL::ASN1::Integer(p),
                                                OpenSSL::ASN1::Integer(q),
                                                OpenSSL::ASN1::Integer(dmp1),
                                                OpenSSL::ASN1::Integer(dmq1),
                                                OpenSSL::ASN1::Integer(iqmp)
                                              ])
    end
    asn1 = OpenSSL::ASN1::Sequence(data_sequence)
    OpenSSL::PKey::RSA.new(asn1.to_der)
  when /^ecdsa\-sha2\-(\w*)$/
    OpenSSL::PKey::EC.read_keyblob($1, self)
  else
    raise Exception, "Cannot decode private key of type #{type}"
  end
end

def read_string

Returns nil if there are not enough bytes to satisfy the request.
integer that describes the number of bytes remaining in the string.
Read and return an SSH2-encoded string. The string starts with a long
def read_string
  length = read_long or return nil
  read(length)
end

def read_to(pattern)

and including the text that matched the pattern.
immediately after the pattern, if it does match. Returns all data up to
does. Returns nil if nothing matches. Increments the position to point
String, Fixnum, or Regexp and is interpreted exactly as String#index
Reads all data up to and including the given pattern, which may be a
def read_to(pattern)
  index = @content.index(pattern, @position) or return nil
  length = case pattern
           when String then pattern.length
           when Integer then 1
           when Regexp then $&.length
           end
  index && read(index + length)
end

def remainder_as_buffer

a new Net::SSH::Buffer object.
Returns all text from the current pointer to the end of the buffer as
def remainder_as_buffer
  Buffer.new(@content[@position..-1])
end

def reset!

begin at position 0.
Resets the pointer to the start of the buffer. Subsequent reads will
def reset!
  @position = 0
end

def to_s

Returns a copy of the buffer's content.
def to_s
  (@content || "").dup
end

def write(*data)

read position. Returns the buffer object.
Writes the given data literally into the string. Does not alter the
def write(*data)
  data.each { |datum| @content << datum.dup.force_encoding('BINARY') }
  self
end

def write_bignum(*n)

Does not alter the read position. Returns the buffer object.
checking is done to ensure that the arguments are, in fact, bignums.
Writes each argument to the buffer as a bignum (SSH2-style). No
def write_bignum(*n)
  @content << n.map { |b| b.to_ssh }.join
  self
end

def write_bool(*b)

Returns the buffer object.
meaning true, and 0 meaning false. Does not alter the read position.
Writes each argument to the buffer as a (C-style) boolean, with 1
def write_bool(*b)
  b.each { |v| @content << (v ? "\1" : "\0") }
  self
end

def write_byte(*n)

position. Returns the buffer object.
Writes each argument to the buffer as a byte. Does not alter the read
def write_byte(*n)
  n.each { |b| @content << b.chr }
  self
end

def write_int64(*n)

buffer object.
64-bit integer (8 bytes). Does not alter the read position. Returns the
Writes each argument to the buffer as a network-byte-order-encoded
def write_int64(*n)
  n.each do |i|
    hi = (i >> 32) & 0xFFFFFFFF
    lo = i & 0xFFFFFFFF
    @content << [hi, lo].pack("N2")
  end
  self
end

def write_key(*key)

alter the read position. Returns the buffer object.
Writes the given arguments to the buffer as SSH2-encoded keys. Does not
def write_key(*key)
  key.each { |k| append(k.to_blob) }
  self
end

def write_long(*n)

buffer object.
long (4-byte) integer. Does not alter the read position. Returns the
Writes each argument to the buffer as a network-byte-order-encoded
def write_long(*n)
  @content << n.pack("N*")
  self
end

def write_moved(string)

to the method. This way we can mutate the string.
Optimized version of write where the caller gives up ownership of string
def write_moved(string)
  @content <<
    if string.frozen?
      string.dup.force_encoding('BINARY')
    else
      string.force_encoding('BINARY')
    end
  self
end

def write_mstring(*text)

Might alter arguments see write_moved
Does not alter the read position. Returns the buffer object.
string is prefixed by its length, encoded as a 4-byte long integer.
Writes each argument to the buffer as an SSH2-encoded string. Each
def write_mstring(*text)
  text.each do |string|
    s = string.to_s
    write_long(s.bytesize)
    write_moved(s)
  end
  self
end

def write_string(*text)

Does not alter the read position. Returns the buffer object.
string is prefixed by its length, encoded as a 4-byte long integer.
Writes each argument to the buffer as an SSH2-encoded string. Each
def write_string(*text)
  text.each do |string|
    s = string.to_s
    write_long(s.bytesize)
    write(s)
  end
  self
end