tree_haver 5.0.5 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/LICENSE.md +13 -0
- data/README.md +299 -660
- data/lib/tree_haver/backend_api.rb +78 -35
- data/lib/tree_haver/backend_context.rb +28 -0
- data/lib/tree_haver/backend_registry.rb +119 -382
- data/lib/tree_haver/backends/citrus.rb +55 -53
- data/lib/tree_haver/backends/ffi.rb +105 -101
- data/lib/tree_haver/backends/java.rb +92 -76
- data/lib/tree_haver/backends/mri.rb +44 -39
- data/lib/tree_haver/backends/parslet.rb +53 -48
- data/lib/tree_haver/backends/prism.rb +140 -43
- data/lib/tree_haver/backends/psych.rb +30 -26
- data/lib/tree_haver/backends/rust.rb +21 -17
- data/lib/tree_haver/backends/tslp.rb +274 -0
- data/lib/tree_haver/base/comment.rb +320 -0
- data/lib/tree_haver/base/language.rb +2 -2
- data/lib/tree_haver/base/node.rb +39 -31
- data/lib/tree_haver/base/parser.rb +4 -0
- data/lib/tree_haver/base/point.rb +3 -3
- data/lib/tree_haver/base/tree.rb +1 -1
- data/lib/tree_haver/citrus_grammar_finder.rb +21 -26
- data/lib/tree_haver/contracts.rb +1025 -0
- data/lib/tree_haver/grammar_finder.rb +125 -70
- data/lib/tree_haver/kaitai_backend.rb +30 -0
- data/lib/tree_haver/language.rb +36 -37
- data/lib/tree_haver/language_pack.rb +41 -0
- data/lib/tree_haver/language_registry.rb +34 -3
- data/lib/tree_haver/library_path_utils.rb +7 -7
- data/lib/tree_haver/node.rb +40 -31
- data/lib/tree_haver/parser.rb +44 -37
- data/lib/tree_haver/parslet_grammar_finder.rb +19 -26
- data/lib/tree_haver/path_validator.rb +39 -36
- data/lib/tree_haver/peg_backends.rb +76 -0
- data/lib/tree_haver/rspec/dependency_tags.rb +23 -1363
- data/lib/tree_haver/rspec.rb +1 -31
- data/lib/tree_haver/tree.rb +16 -7
- data/lib/tree_haver/version.rb +5 -14
- data/lib/tree_haver.rb +536 -1240
- data/sig/tree_haver.rbs +1 -229
- data.tar.gz.sig +0 -0
- metadata +154 -70
- metadata.gz.sig +0 -0
- data/CHANGELOG.md +0 -1393
- data/CITATION.cff +0 -20
- data/CODE_OF_CONDUCT.md +0 -134
- data/CONTRIBUTING.md +0 -359
- data/FUNDING.md +0 -74
- data/LICENSE.txt +0 -21
- data/REEK +0 -0
- data/RUBOCOP.md +0 -71
- data/SECURITY.md +0 -21
- data/lib/tree_haver/base.rb +0 -12
- data/lib/tree_haver/compat.rb +0 -43
- data/lib/tree_haver/rspec/testable_node.rb +0 -217
- data/sig/tree_haver/backends.rbs +0 -352
- data/sig/tree_haver/grammar_finder.rbs +0 -29
- data/sig/tree_haver/path_validator.rbs +0 -32
|
@@ -35,17 +35,17 @@ module TreeHaver
|
|
|
35
35
|
# Default directories that are generally trusted for system libraries
|
|
36
36
|
# These are searched by the dynamic linker anyway
|
|
37
37
|
DEFAULT_TRUSTED_DIRECTORIES = [
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
'/usr/lib',
|
|
39
|
+
'/usr/lib64',
|
|
40
|
+
'/usr/lib/x86_64-linux-gnu',
|
|
41
|
+
'/usr/lib/aarch64-linux-gnu',
|
|
42
|
+
'/usr/local/lib',
|
|
43
|
+
'/opt/homebrew/lib',
|
|
44
|
+
'/opt/local/lib'
|
|
45
45
|
].freeze
|
|
46
46
|
|
|
47
47
|
# Environment variable for adding trusted directories (comma-separated)
|
|
48
|
-
TRUSTED_DIRS_ENV_VAR =
|
|
48
|
+
TRUSTED_DIRS_ENV_VAR = 'TREE_HAVER_TRUSTED_DIRS'
|
|
49
49
|
|
|
50
50
|
# Maximum reasonable path length (prevents DoS via extremely long paths)
|
|
51
51
|
MAX_PATH_LENGTH = 4096
|
|
@@ -60,30 +60,39 @@ module TreeHaver
|
|
|
60
60
|
# Pattern for valid symbol names (C identifier format)
|
|
61
61
|
VALID_SYMBOL_PATTERN = /\A[a-zA-Z_][a-zA-Z0-9_]*\z/
|
|
62
62
|
|
|
63
|
-
@custom_trusted_directories = []
|
|
63
|
+
@custom_trusted_directories = []
|
|
64
64
|
@mutex = Mutex.new
|
|
65
65
|
|
|
66
66
|
module_function
|
|
67
67
|
|
|
68
|
-
# Get all trusted directories (default + custom + from ENV)
|
|
68
|
+
# Get all trusted directories (default + user-local + custom + from ENV)
|
|
69
69
|
#
|
|
70
70
|
# @return [Array<String>] list of all trusted directory prefixes
|
|
71
71
|
def trusted_directories
|
|
72
72
|
dirs = DEFAULT_TRUSTED_DIRECTORIES.dup
|
|
73
73
|
|
|
74
|
+
# Add user-local XDG directories (computed at call time from HOME)
|
|
75
|
+
begin
|
|
76
|
+
home = Dir.home
|
|
77
|
+
dirs << File.join(home, '.local', 'lib', 'tree-sitter')
|
|
78
|
+
dirs << File.join(home, '.local', 'lib')
|
|
79
|
+
rescue ArgumentError
|
|
80
|
+
# HOME not set — skip user-local dirs
|
|
81
|
+
end
|
|
82
|
+
|
|
74
83
|
# Add custom registered directories
|
|
75
84
|
@mutex.synchronize { dirs.concat(@custom_trusted_directories) }
|
|
76
85
|
|
|
77
86
|
# Add directories from environment variable
|
|
78
|
-
ENV[TRUSTED_DIRS_ENV_VAR]&.split(
|
|
87
|
+
ENV[TRUSTED_DIRS_ENV_VAR]&.split(',')&.each do |dir|
|
|
79
88
|
expanded = File.expand_path(dir.strip)
|
|
80
|
-
# :
|
|
89
|
+
# simplecov:disable
|
|
81
90
|
# File.expand_path always returns absolute paths on Unix/macOS.
|
|
82
91
|
# This guard exists for defensive programming on exotic platforms
|
|
83
92
|
# where expand_path might behave differently, but cannot be tested
|
|
84
93
|
# in standard CI environments.
|
|
85
|
-
dirs << expanded if expanded.start_with?(
|
|
86
|
-
# :
|
|
94
|
+
dirs << expanded if expanded.start_with?('/')
|
|
95
|
+
# simplecov:enable
|
|
87
96
|
end
|
|
88
97
|
|
|
89
98
|
dirs.uniq
|
|
@@ -106,15 +115,15 @@ module TreeHaver
|
|
|
106
115
|
def add_trusted_directory(directory)
|
|
107
116
|
expanded = File.expand_path(directory)
|
|
108
117
|
|
|
109
|
-
# :
|
|
118
|
+
# simplecov:disable
|
|
110
119
|
# File.expand_path always returns absolute paths on Unix/macOS.
|
|
111
120
|
# This guard exists for defensive programming on exotic platforms
|
|
112
121
|
# where expand_path might behave differently, but cannot be tested
|
|
113
122
|
# in standard CI environments.
|
|
114
|
-
unless expanded.start_with?(
|
|
123
|
+
unless expanded.start_with?('/')
|
|
115
124
|
raise ArgumentError, "Trusted directory must be an absolute path: #{directory.inspect}"
|
|
116
125
|
end
|
|
117
|
-
# :
|
|
126
|
+
# simplecov:enable
|
|
118
127
|
|
|
119
128
|
@mutex.synchronize do
|
|
120
129
|
@custom_trusted_directories << expanded unless @custom_trusted_directories.include?(expanded)
|
|
@@ -176,11 +185,11 @@ module TreeHaver
|
|
|
176
185
|
return false if path.include?("\0") # Null byte injection
|
|
177
186
|
|
|
178
187
|
# Must be absolute path (prevents relative path traversal)
|
|
179
|
-
return false unless path.start_with?(
|
|
188
|
+
return false unless path.start_with?('/') || windows_absolute_path?(path)
|
|
180
189
|
|
|
181
190
|
# Check for path traversal attempts
|
|
182
|
-
return false if path.include?(
|
|
183
|
-
return false if path.include?(
|
|
191
|
+
return false if path.include?('/../') || path.end_with?('/..')
|
|
192
|
+
return false if path.include?('/./') || path.end_with?('/.')
|
|
184
193
|
|
|
185
194
|
# Validate extension
|
|
186
195
|
# Allow versioned .so files like .so.0, .so.14, etc. (common on Linux)
|
|
@@ -191,9 +200,7 @@ module TreeHaver
|
|
|
191
200
|
return false unless filename.match?(VALID_FILENAME_PATTERN)
|
|
192
201
|
|
|
193
202
|
# Optionally require the path to be in a trusted directory
|
|
194
|
-
if require_trusted_dir
|
|
195
|
-
return false unless in_trusted_directory?(path)
|
|
196
|
-
end
|
|
203
|
+
return false if require_trusted_dir && !in_trusted_directory?(path)
|
|
197
204
|
|
|
198
205
|
true
|
|
199
206
|
end
|
|
@@ -293,7 +300,7 @@ module TreeHaver
|
|
|
293
300
|
def sanitize_language_name(name)
|
|
294
301
|
return if name.nil?
|
|
295
302
|
|
|
296
|
-
sanitized = name.to_s.downcase.gsub(/[^a-z0-9_]/,
|
|
303
|
+
sanitized = name.to_s.downcase.gsub(/[^a-z0-9_]/, '')
|
|
297
304
|
return if sanitized.empty?
|
|
298
305
|
return unless sanitized.match?(/\A[a-z]/)
|
|
299
306
|
|
|
@@ -308,24 +315,20 @@ module TreeHaver
|
|
|
308
315
|
errors = []
|
|
309
316
|
|
|
310
317
|
if path.nil? || path.empty?
|
|
311
|
-
errors <<
|
|
318
|
+
errors << 'Path is nil or empty'
|
|
312
319
|
return errors
|
|
313
320
|
end
|
|
314
321
|
|
|
315
322
|
errors << "Path exceeds maximum length (#{MAX_PATH_LENGTH})" if path.length > MAX_PATH_LENGTH
|
|
316
|
-
errors <<
|
|
317
|
-
errors <<
|
|
318
|
-
errors <<
|
|
319
|
-
errors <<
|
|
323
|
+
errors << 'Path contains null byte' if path.include?("\0")
|
|
324
|
+
errors << 'Path is not absolute' unless path.start_with?('/') || windows_absolute_path?(path)
|
|
325
|
+
errors << 'Path contains traversal sequence (/../)' if path.include?('/../') || path.end_with?('/..')
|
|
326
|
+
errors << 'Path contains traversal sequence (/./)' if path.include?('/./') || path.end_with?('/.')
|
|
320
327
|
|
|
321
|
-
unless has_valid_extension?(path)
|
|
322
|
-
errors << "Path does not have allowed extension (.so, .so.X, .dylib, .dll)"
|
|
323
|
-
end
|
|
328
|
+
errors << 'Path does not have allowed extension (.so, .so.X, .dylib, .dll)' unless has_valid_extension?(path)
|
|
324
329
|
|
|
325
330
|
filename = File.basename(path)
|
|
326
|
-
unless filename.match?(VALID_FILENAME_PATTERN)
|
|
327
|
-
errors << "Filename contains invalid characters"
|
|
328
|
-
end
|
|
331
|
+
errors << 'Filename contains invalid characters' unless filename.match?(VALID_FILENAME_PATTERN)
|
|
329
332
|
|
|
330
333
|
errors
|
|
331
334
|
end
|
|
@@ -333,7 +336,7 @@ module TreeHaver
|
|
|
333
336
|
# @api private
|
|
334
337
|
def windows_absolute_path?(path)
|
|
335
338
|
# Match Windows absolute paths like C:\path or D:/path
|
|
336
|
-
path.match?(
|
|
339
|
+
path.match?(%r{\A[A-Za-z]:[\\/]})
|
|
337
340
|
end
|
|
338
341
|
|
|
339
342
|
# @api private
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TreeHaver
|
|
4
|
+
CITRUS_BACKEND = BackendReference.new(
|
|
5
|
+
id: 'citrus',
|
|
6
|
+
family: 'peg'
|
|
7
|
+
).freeze
|
|
8
|
+
|
|
9
|
+
PARSLET_BACKEND = BackendReference.new(
|
|
10
|
+
id: 'parslet',
|
|
11
|
+
family: 'peg'
|
|
12
|
+
).freeze
|
|
13
|
+
|
|
14
|
+
BackendRegistry.register(CITRUS_BACKEND)
|
|
15
|
+
BackendRegistry.register(PARSLET_BACKEND)
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def peg_adapter_info(backend_ref)
|
|
20
|
+
AdapterInfo.new(
|
|
21
|
+
backend: backend_ref.id,
|
|
22
|
+
backend_ref: backend_ref,
|
|
23
|
+
supports_dialects: false,
|
|
24
|
+
supported_policies: []
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def peg_feature_profile(backend_ref)
|
|
29
|
+
FeatureProfile.new(
|
|
30
|
+
backend: backend_ref.id,
|
|
31
|
+
backend_ref: backend_ref,
|
|
32
|
+
supports_dialects: false,
|
|
33
|
+
supported_policies: []
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parse_with_citrus(source, grammar_module:)
|
|
38
|
+
raw = grammar_module.parse(source)
|
|
39
|
+
if raw&.respond_to?(:captures)
|
|
40
|
+
{
|
|
41
|
+
ok: true,
|
|
42
|
+
backend_ref: CITRUS_BACKEND,
|
|
43
|
+
raw: raw,
|
|
44
|
+
diagnostics: []
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
{
|
|
48
|
+
ok: false,
|
|
49
|
+
backend_ref: CITRUS_BACKEND,
|
|
50
|
+
diagnostics: [{ severity: 'error', category: 'parse_error', message: 'Citrus parse failed.' }]
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
{
|
|
55
|
+
ok: false,
|
|
56
|
+
backend_ref: CITRUS_BACKEND,
|
|
57
|
+
diagnostics: [{ severity: 'error', category: 'parse_error', message: e.message }]
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def parse_with_parslet(source, grammar_class:)
|
|
62
|
+
raw = grammar_class.new.parse(source)
|
|
63
|
+
{
|
|
64
|
+
ok: true,
|
|
65
|
+
backend_ref: PARSLET_BACKEND,
|
|
66
|
+
raw: raw,
|
|
67
|
+
diagnostics: []
|
|
68
|
+
}
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
{
|
|
71
|
+
ok: false,
|
|
72
|
+
backend_ref: PARSLET_BACKEND,
|
|
73
|
+
diagnostics: [{ severity: 'error', category: 'parse_error', message: e.message }]
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
end
|