Class: Ibex::LSP::Transport

Inherits:
Object
  • Object
show all
Defined in:
lib/ibex/lsp/transport.rb,
sig/ibex/lsp/transport.rbs

Overview

Reads and writes bounded LSP JSON-RPC messages over Content-Length framing.

Constant Summary collapse

HEADER_SEPARATOR =

Returns:

  • (::String)
"\r\n\r\n"

Instance Method Summary collapse

Constructor Details

#initialize(input, output) ⇒ Transport

Returns a new instance of Transport.

RBS:

  • (untyped input, untyped output) -> void

Parameters:

  • input (Object)
  • output (Object)


10
11
12
13
14
# File 'lib/ibex/lsp/transport.rb', line 10

def initialize(input, output)
  @input = input
  @output = output
  @buffer = String.new(encoding: Encoding::BINARY)
end

Instance Method Details

#content_length(header) ⇒ Integer

RBS:

  • (String header) -> Integer

Parameters:

  • header (String)

Returns:

  • (Integer)


73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/ibex/lsp/transport.rb', line 73

def content_length(header)
  values = header.split("\r\n").filter_map do |line|
    name, value = line.split(":", 2)
    raise ProtocolError.new("malformed JSON-RPC header", code: -32_700, fatal: true) unless value

    value.strip if name&.casecmp?("Content-Length")
  end
  unless values.one? && values.fetch(0).match?(/\A\d+\z/)
    raise ProtocolError.new("exactly one numeric Content-Length header is required",
                            code: -32_700, fatal: true)
  end

  length = Integer(values.fetch(0), 10)
  if length > Limits::MAX_MESSAGE_BYTES
    raise ProtocolError.new("JSON-RPC message exceeds #{Limits::MAX_MESSAGE_BYTES} bytes",
                            code: -32_700, fatal: true)
  end
  length
end

#read_bytes(length) ⇒ String

RBS:

  • (Integer length) -> String

Parameters:

  • length (Integer)

Returns:

  • (String)


94
95
96
97
98
99
100
101
102
103
104
# File 'lib/ibex/lsp/transport.rb', line 94

def read_bytes(length)
  while @buffer.bytesize < length
    chunk = read_chunk([4096, length - @buffer.bytesize].min)
    raise ProtocolError.new("truncated JSON-RPC body", code: -32_700, fatal: true) unless chunk

    @buffer << chunk.b
  end
  body = @buffer.byteslice(0, length) || ""
  @buffer = @buffer.byteslice(length, @buffer.bytesize) || +""
  body.force_encoding(Encoding::UTF_8)
end

#read_chunk(length) ⇒ String?

IO#read(length) may wait for the entire length on a live pipe. readpartial lets the stdio server process the bytes that are already available.

RBS:

  • (Integer length) -> String?

Parameters:

  • length (Integer)

Returns:

  • (String, nil)


109
110
111
112
113
114
115
# File 'lib/ibex/lsp/transport.rb', line 109

def read_chunk(length)
  return @input.read(length) unless @input.respond_to?(:readpartial)

  @input.readpartial(length)
rescue EOFError
  nil
end

#read_headerString?

RBS:

  • () -> String?

Returns:

  • (String, nil)


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/ibex/lsp/transport.rb', line 46

def read_header
  loop do
    index = @buffer.index(HEADER_SEPARATOR)
    if index
      if index > Limits::MAX_HEADER_BYTES
        raise ProtocolError.new("JSON-RPC header exceeds #{Limits::MAX_HEADER_BYTES} bytes",
                                code: -32_700, fatal: true)
      end

      header = @buffer.byteslice(0, index) || ""
      @buffer = @buffer.byteslice(index + HEADER_SEPARATOR.bytesize, @buffer.bytesize) || +""
      return header
    end
    if @buffer.bytesize > Limits::MAX_HEADER_BYTES
      raise ProtocolError.new("JSON-RPC header exceeds #{Limits::MAX_HEADER_BYTES} bytes",
                              code: -32_700, fatal: true)
    end

    chunk = read_chunk(4096)
    return nil if chunk.nil? && @buffer.empty?
    raise ProtocolError.new("truncated JSON-RPC header", code: -32_700, fatal: true) unless chunk

    @buffer << chunk.b
  end
end

#read_messageHash[String, untyped]?

RBS:

  • () -> Hash[String, untyped]?

Returns:

  • (Hash[String, untyped], nil)


17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/ibex/lsp/transport.rb', line 17

def read_message
  header = read_header
  return unless header

  length = content_length(header)
  body = read_bytes(length)
  parsed = JSON.parse(body)
  raise ProtocolError.new("JSON-RPC message must be an object", code: -32_600) unless parsed.is_a?(Hash)

  parsed
rescue JSON::ParserError => e
  raise ProtocolError.new("malformed JSON: #{e.message}", code: -32_700)
end

#write_message(message) ⇒ void

This method returns an undefined value.

RBS:

  • (Hash[String, untyped] message) -> void

Parameters:

  • message (Hash[String, untyped])


32
33
34
35
36
37
38
39
40
41
# File 'lib/ibex/lsp/transport.rb', line 32

def write_message(message)
  body = JSON.generate(message)
  if body.bytesize > Limits::MAX_OUTPUT_BYTES
    raise ProtocolError.new("JSON-RPC output exceeds #{Limits::MAX_OUTPUT_BYTES} bytes", code: -32_603)
  end

  @output.write("Content-Length: #{body.bytesize}\r\n\r\n")
  @output.write(body)
  @output.flush if @output.respond_to?(:flush)
end