CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/rubocop/cop/lint/detect_invalid_pack_directives.rb
Views: 1904
1
# frozen_string_literal: true
2
3
module RuboCop
4
module Cop
5
module Lint
6
# Looks for invalid pack/unpack directives with Ruby 3.3.0 some directives
7
# that used to not raise errors, now will - context: https://bugs.ruby-lang.org/issues/19150:
8
# * Array#pack now raises ArgumentError for unknown directives
9
# * String#unpack now raises ArgumentError for unknown directives
10
#
11
# @example
12
# # bad
13
# ```
14
# 3.3.0-preview1 :003 > [0x1].pack('<L')
15
# <internal:pack>:8:in `pack': unknown pack directive '<' in '<L' (ArgumentError)
16
# ```
17
#
18
# # good
19
# ```
20
# 3.3.0-preview1 :001 > [0x1].pack('L<')
21
# => "\x01\x00\x00\x00"
22
# ```
23
class DetectInvalidPackDirectives < RuboCop::Cop::Base
24
# https://github.com/ruby/ruby/blob/7cfabe1acc55b24fc2c479a87efa71cf74e9e8fc/pack.c#L38
25
MODIFIABLE_DIRECTIVES = %w[s S i I l L q Q j J]
26
27
# https://github.com/ruby/ruby/blob/7cfabe1acc55b24fc2c479a87efa71cf74e9e8fc/pack.c#L298
28
ACCEPTABLE_DIRECTIVES = %w[U m A B H a A Z b B h H c C s S i I l L q Q j J n N v V f F e E d D g G x X @ % U u m M P p w]
29
30
# @param [RuboCop::AST::SendNode] node Node for the ruby `send` method
31
# @return [[RuboCop::AST::Node], raise] offense when an invalid directive is found, or raise if unexpected error found
32
def on_send(node)
33
_callee, method_name = *node
34
35
return unless %i[pack unpack pack1 unpack1].include?(method_name)
36
37
args = node.arguments
38
return if args.empty?
39
40
args.each do |arg|
41
next unless string_arg?(arg)
42
43
# if multiline arguments are passed
44
if arg.type == :dstr
45
idx = []
46
47
pack_directive = arg.children.map do |child|
48
if begin_arg?(child)
49
next
50
else
51
idx << child.children.first.length
52
child.children.join
53
end
54
end.join
55
56
# elsif single line arguments are passed
57
elsif arg.type == :str
58
pack_directive = arg.children.first
59
end
60
61
error = validate_directive(pack_directive)
62
if error.nil?
63
next
64
else
65
offense_range = get_error_range(arg, error[:index], idx)
66
return if offense_range.nil?
67
68
add_offense(offense_range, message: error[:message])
69
end
70
end
71
end
72
73
private
74
75
# Check if the pack directives are valid. See link for pack docs https://apidock.com/ruby/Array/pack
76
#
77
# Code based on https://github.com/ruby/ruby/blob/6391132c03ac08da0483adb986ff9a54e41f9e14/pack.c#L196
78
# adapted into Ruby
79
#
80
# @param [String] pack_directive The ruby pack/unpack directive to validate
81
# @return [Hash,nil] A hash with the message and index that the invalidate directive was found at, or nil.
82
def validate_directive(pack_directive)
83
# current pointer value
84
p = 0
85
86
# end of pointer range
87
pend = pack_directive.length
88
89
while p < pend
90
explicit_endian = 0
91
type_index = p
92
93
# get data type
94
type = pack_directive[type_index]
95
p += 1
96
97
if type.blank?
98
next
99
end
100
101
if type == '#'
102
p += 1 while p < pend && pack_directive[p] != "\n"
103
next
104
end
105
106
# Modifiers
107
loop do
108
case pack_directive[p]
109
when '_', '!'
110
if MODIFIABLE_DIRECTIVES.include?(type)
111
p += 1
112
else
113
return { message: "'#{pack_directive[p]}' allowed only after types #{MODIFIABLE_DIRECTIVES.join}", index: p }
114
end
115
when '<', '>'
116
unless MODIFIABLE_DIRECTIVES.include?(type)
117
return { message: "'#{pack_directive[p]}' allowed only after types #{MODIFIABLE_DIRECTIVES.join}", index: p }
118
end
119
120
if explicit_endian != 0
121
return { message: "Can't use both '<' and '>'.", index: p }
122
end
123
124
explicit_endian = pack_directive[p]
125
p += 1
126
else
127
break
128
end
129
end
130
131
# Data length
132
if pack_directive[p] == '*'
133
p += 1
134
elsif pack_directive[p]&.match?(/\d/)
135
p += 1 while pack_directive[p]&.match?(/\d/)
136
end
137
138
# Check type
139
unless ACCEPTABLE_DIRECTIVES.include?(type)
140
return { message: "unknown pack directive '#{type}' in '#{pack_directive}'", index: type_index }
141
end
142
end
143
144
nil
145
end
146
147
# Checks if the current node is of type `:str` or `:dstr` - `dstr` being multiline
148
#
149
# @param [RuboCop::AST::SendNode] node Node for the ruby `send` method
150
# @return [TrueClass, FalseClass]
151
def string_arg?(node)
152
node&.type == :str || node&.type == :dstr
153
end
154
155
# Check if the node if of type `:begin`
156
#
157
# @param [RuboCop::AST::SendNode] node Node for the ruby `send` method
158
# @return [TrueClass, FalseClass]
159
def begin_arg?(node)
160
node.type == :begin
161
end
162
163
# Get the range of the offense to more accurately raise offenses against specific directives
164
#
165
# @param [RuboCop::AST::DstrNode, RuboCop::AST::StrNode ] arg The node that need its range calculated
166
# @param [Integer] p The current pointer value
167
# @param [Array] idx An array holding to number of indexes for the node
168
# @return [Parser::Source::Range] The range of the node value
169
def get_error_range(arg, p, idx)
170
# Logic for multiline strings
171
if arg.type == :dstr
172
total = 0
173
index = 0
174
175
idx.each_with_index do |idx_length, count|
176
if total < p
177
total += idx_length
178
index = count
179
end
180
end
181
adjusted_index = p - idx[0..(index - 1)].sum
182
183
indexed_arg = arg.children[index]
184
185
if begin_arg?(indexed_arg)
186
return nil
187
else
188
newline_adjustment = indexed_arg.children.first[0..adjusted_index].scan(/[\n\t]/).count
189
end
190
191
# If there's opening quotes present, i.e. "a", instead of heredoc which doesn't have preceding opening quotes:
192
if indexed_arg.loc.begin
193
range_start = indexed_arg.loc.begin.end_pos + (p - adjusted_index)
194
else
195
expression = processed_source.raw_source[indexed_arg.loc.expression.begin.begin_pos...indexed_arg.loc.expression.end.end_pos]
196
if expression[/^\s+/].nil?
197
leading_whitespace_size = 0
198
else
199
leading_whitespace_size = expression[/^\s+/].length
200
end
201
adjusted_index += leading_whitespace_size
202
range_start = indexed_arg.loc.expression.begin_pos + (adjusted_index + newline_adjustment)
203
end
204
# Logic for single line strings
205
else
206
newline_adjustment = arg.children.first[0..p].scan(/[\n\t]/).count
207
range_start = arg.loc.begin.end_pos + (p + newline_adjustment)
208
end
209
210
range_end = range_start + 1
211
212
Parser::Source::Range.new(arg.loc.expression.source_buffer, range_start, range_end)
213
end
214
end
215
end
216
end
217
end
218
219