Class: Ibex::LSP::Workspace

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

Overview

Converts file URIs to canonical paths constrained to initialized workspace roots.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root_uris, loader) ⇒ Workspace

Returns a new instance of Workspace.

RBS:

  • (Array[String] root_uris, Frontend::SourceLoader loader) -> void

Parameters:



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

def initialize(root_uris, loader)
  raise ProtocolError.new("initialize requires a workspace root", code: -32_602) if root_uris.empty?

  @loader = loader
  @roots = root_uris.map { |uri| canonical_root(uri) }.uniq.sort.freeze
end

Instance Attribute Details

#rootsArray[String] (readonly)

Signature:

  • Array[String]

Returns:

  • (Array[String])


7
8
9
# File 'lib/ibex/lsp/workspace.rb', line 7

def roots
  @roots
end

Instance Method Details

#canonical_root(uri) ⇒ String

RBS:

  • (String uri) -> String

Parameters:

  • uri (String)

Returns:

  • (String)


78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/ibex/lsp/workspace.rb', line 78

def canonical_root(uri)
  parsed = parse_file_uri(uri)
  decoded = decode_path(parsed.path)
  canonical = File.realpath(decoded)
  unless File.directory?(canonical)
    raise ProtocolError.new("workspace root is not a directory: #{uri}", code: -32_602)
  end

  canonical
rescue URI::InvalidURIError, ArgumentError, SystemCallError => e
  raise ProtocolError.new("invalid workspace root #{uri.inspect}: #{e.message}", code: -32_602)
end

#decode_path(encoded) ⇒ String

RBS:

  • (String encoded) -> String

Parameters:

  • encoded (String)

Returns:

  • (String)


92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/ibex/lsp/workspace.rb', line 92

def decode_path(encoded)
  raise ArgumentError, "file URI contains an invalid percent escape" if
    encoded.match?(/%(?![0-9A-Fa-f]{2})/)
  if encoded.match?(/%(?:2f|5c|00)/i)
    raise ArgumentError, "file URI must not percent-encode a path separator or NUL"
  end

  parser = begin
    URI::RFC2396_PARSER
  rescue NameError
    URI::DEFAULT_PARSER
  end
  decoded = parser.unescape(encoded)
  raise ArgumentError, "file URI path must be valid UTF-8" unless decoded.valid_encoding?
  raise ArgumentError, "file URI path must not contain NUL" if decoded.include?("\0")
  raise ArgumentError, "file URI path must not contain parent traversal" if
    decoded.split(%r{[\\/]}).include?("..")

  decoded
end

#inside?(path, root) ⇒ Boolean

RBS:

  • (String path, String root) -> bool

Parameters:

  • path (String)
  • root (String)

Returns:

  • (Boolean)


114
115
116
117
118
119
120
121
122
123
124
# File 'lib/ibex/lsp/workspace.rb', line 114

def inside?(path, root)
  cursor = path
  loop do
    return true if cursor == root

    parent = File.dirname(cursor)
    return false if parent == cursor

    cursor = parent
  end
end

#parse_file_uri(uri) ⇒ Object

URI is a stdlib boundary whose implementation class varies by scheme.

RBS:

  • (String uri) -> untyped

Parameters:

  • uri (String)

Returns:

  • (Object)


52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/ibex/lsp/workspace.rb', line 52

def parse_file_uri(uri)
  validate_raw_authority!(uri)
  parsed = URI.parse(uri)
  path = parsed.path
  local_host = parsed.host.nil? || parsed.host.empty? || parsed.host.casecmp?("localhost")
  unless parsed.scheme == "file" && local_host &&
         parsed.query.nil? && parsed.fragment.nil? && path&.start_with?("/")
    raise ArgumentError, "only absolute local file URIs are supported"
  end

  parsed
end

#path(uri, allow_missing: true) ⇒ String

RBS:

  • (String uri, ?allow_missing: bool) -> String

Parameters:

  • uri (String)
  • allow_missing: (Boolean) (defaults to: true)

Returns:

  • (String)


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

def path(uri, allow_missing: true)
  parsed = parse_file_uri(uri)
  decoded = decode_path(parsed.path)

  canonical = @loader.canonical_path(decoded, allow_missing: allow_missing)
  return canonical if root_for(canonical)

  raise ProtocolError.new("document is outside initialized workspace roots: #{uri}", code: -32_602)
rescue URI::InvalidURIError, ArgumentError => e
  raise ProtocolError.new("invalid file URI #{uri.inspect}: #{e.message}", code: -32_602)
rescue SystemCallError => e
  raise ProtocolError.new("cannot resolve file URI #{uri.inspect}: #{e.message}", code: -32_602)
end

#root_for(path) ⇒ String?

RBS:

  • (String path) -> String?

Parameters:

  • path (String)

Returns:

  • (String, nil)


44
45
46
# File 'lib/ibex/lsp/workspace.rb', line 44

def root_for(path)
  roots.select { |root| inside?(path, root) }.max_by(&:length)
end

#uri(path) ⇒ String

RBS:

  • (String path) -> String

Parameters:

  • path (String)

Returns:

  • (String)


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

def uri(path)
  canonical = @loader.canonical_path(path, allow_missing: true)
  parser = begin
    URI::RFC2396_PARSER
  rescue NameError
    URI::DEFAULT_PARSER
  end
  "file://#{parser.escape(canonical)}"
end

#validate_raw_authority!(uri) ⇒ void

This method returns an undefined value.

Ruby's URI::File may discard userinfo and port, so validate the raw authority first.

RBS:

  • (String uri) -> void

Parameters:

  • uri (String)


67
68
69
70
71
72
73
74
75
# File 'lib/ibex/lsp/workspace.rb', line 67

def validate_raw_authority!(uri)
  match = uri.match(%r{\Afile://([^/]*)}i)
  return unless match

  authority = match[1] || ""
  return if authority.empty? || authority.casecmp?("localhost")

  raise ArgumentError, "only an empty authority or localhost is supported"
end