lib/plum/frame.rb
# frozen-string-literal: true
using Plum::BinaryString
module Plum
class Frame
FRAME_TYPES = {
data: 0x00,
headers: 0x01,
priority: 0x02,
rst_stream: 0x03,
settings: 0x04,
push_promise: 0x05,
ping: 0x06,
goaway: 0x07,
window_update: 0x08,
continuation: 0x09
}.freeze
# @!visibility private
FRAME_TYPES_INVERSE = FRAME_TYPES.invert.freeze
FRAME_FLAGS = {
data: {
end_stream: 0x01,
padded: 0x08
}.freeze,
headers: {
end_stream: 0x01,
end_headers: 0x04,
padded: 0x08,
priority: 0x20
}.freeze,
priority: {}.freeze,
rst_stream: {}.freeze,
settings: {
ack: 0x01
}.freeze,
push_promise: {
end_headers: 0x04,
padded: 0x08
}.freeze,
ping: {
ack: 0x01
}.freeze,
goaway: {}.freeze,
window_update: {}.freeze,
continuation: {
end_headers: 0x04
}.freeze
}.freeze
# @!visibility private
FRAME_FLAGS_MAP = FRAME_FLAGS.values.inject(:merge).freeze
# type_value = 0x00 - 0x09 are known, but these classes aren't defined yet...
# Frame.register_subclass adds them.
SUB_CLASSES = []
private_constant :SUB_CLASSES
# RFC7540: 4.1 Frame format
# +-----------------------------------------------+
# | Length (24) |
# +---------------+---------------+---------------+
# | Type (8) | Flags (8) |
# +-+-------------+---------------+-------------------------------+
# |R| Stream Identifier (31) |
# +=+=============================================================+
# | Frame Payload (0...) ...
# +---------------------------------------------------------------+
# [Integer] Frame type. 8-bit
attr_accessor :type_value
# [Integer] Flags. 8-bit
attr_accessor :flags_value
# [Integer] Stream Identifier. Unsigned 31-bit integer
attr_reader :stream_id
# [String] The payload. Value is frozen.
attr_reader :payload
# @private
protected def initialize_base(type: nil, type_value: nil, flags: nil, flags_value: nil, stream_id: nil, payload: nil)
@payload = payload || ""
@length = @payload.bytesize
@type_value = type_value || FRAME_TYPES[type] or raise ArgumentError.new("unknown frame type: #{type}")
@flags_value = flags_value or self.flags = flags
@stream_id = stream_id or raise ArgumentError.new("stream_id is necessary")
self
end
# @private
def initialize(*, **)
raise ArgumentError, "can't instantiate abstract class"
end
# Creates a new instance of Frame or an subclass of Frame.
# @private
def self.craft(type: nil, type_value: nil, **args)
type_value ||= type && FRAME_TYPES[type] or (raise ArgumentError, "unknown frame type")
klass = SUB_CLASSES[type_value] || Frame::Unknown
instance = klass.allocate
instance.send(:initialize_base, type_value: type_value, **args)
instance
end
# Returns the length of payload.
# @return [Integer] The length.
def length
@length
end
# Returns the type of the frame in Symbol.
# @return [Symbol] The type.
def type
FRAME_TYPES_INVERSE[@type_value] || ("unknown_%02x" % @type_value).to_sym
end
# Returns the set flags on the frame.
# @return [Array<Symbol>] The flags.
def flags
fs = FRAME_FLAGS[type]
[0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80]
.select { |v| @flags_value & v > 0 }
.map { |val| fs && fs.key(val) || ("unknown_%02x" % val).to_sym }
end
# Sets the frame flags.
# @param values [Array<Symbol>] The flags.
def flags=(values)
val = 0
FRAME_FLAGS_MAP.values_at(*values).each { |c|
val |= c if c
}
@flags_value = val
end
# Frame#flag_name?() == Frame#flags().include?(:flag_name)
FRAME_FLAGS_MAP.each { |name, value|
class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{name}?
@flags_value & #{value} > 0
end
EOS
}
# Assembles the frame into binary representation.
# @return [String] Binary representation of this frame.
def assemble
[length / 0x100, length % 0x100,
@type_value,
@flags_value,
@stream_id].pack("nCCCN") << @payload
end
# @private
def inspect
"#<%s:0x%04x length=%d, flags=%p, stream_id=0x%04x, payload=%p>" % [self.class, __id__, length, flags, stream_id, payload]
end
class << self
# Parses a frame from given buffer. It changes given buffer.
# @param buffer [String] The buffer stored the data received from peer. Encoding must be Encoding::BINARY.
# @return [Frame, nil] The parsed frame or nil if the buffer is imcomplete.
def parse!(buffer)
return nil if buffer.bytesize < 9 # header: 9 bytes
length = buffer.uint24
return nil if buffer.bytesize < 9 + length
cur = buffer.byteshift(9 + length)
type_value, flags_value, r_sid = cur.byteslice(3, 6).unpack("CCN")
# r = r_sid >> 31 # currently not used
stream_id = r_sid # & ~(1 << 31)
frame = (SUB_CLASSES[type_value] || Frame::Unknown).allocate
frame.send(:initialize_base,
type_value: type_value,
flags_value: flags_value,
stream_id: stream_id,
payload: cur.byteslice(9, length))
frame.freeze
end
# Sub classes must call this.
protected def register_subclass(type_value)
SUB_CLASSES[type_value] = self
end
end
end
end