yara-ffi 4.0.0 → 4.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.
@@ -0,0 +1,161 @@
1
+ module Yara
2
+ # Public: Wrapper around the YARA-X YRX_COMPILER API.
3
+ #
4
+ # This class allows adding multiple sources, defining globals before
5
+ # compilation, and building a YRX_RULES object that can be used by
6
+ # {Yara::Scanner}.
7
+ class Compiler
8
+ class CompileError < StandardError; end
9
+
10
+ def initialize(flags = 0)
11
+ @compiler_ptr_holder = ::FFI::MemoryPointer.new(:pointer)
12
+ result = Yara::FFI.yrx_compiler_create(flags, @compiler_ptr_holder)
13
+ if result != Yara::FFI.yrx_last_error && result != Yara::FFI::YRX_SUCCESS
14
+ # Defensive: use yrx_last_error for message but prefer success check
15
+ end
16
+
17
+ if result != Yara::FFI::YRX_SUCCESS
18
+ raise CompileError, "Failed to create compiler: #{Yara::FFI.yrx_last_error}"
19
+ end
20
+ @compiler = @compiler_ptr_holder.get_pointer(0)
21
+ end
22
+
23
+ def add_source(src, origin = nil)
24
+ if origin
25
+ result = Yara::FFI.yrx_compiler_add_source_with_origin(@compiler, src, origin)
26
+ else
27
+ result = Yara::FFI.yrx_compiler_add_source(@compiler, src)
28
+ end
29
+
30
+ if result != Yara::FFI::YRX_SUCCESS
31
+ raise CompileError, "Failed to add source: #{Yara::FFI.yrx_last_error}"
32
+ end
33
+ nil
34
+ end
35
+
36
+ def define_global_str(ident, value)
37
+ result = Yara::FFI.yrx_compiler_define_global_str(@compiler, ident, value)
38
+ if result != Yara::FFI::YRX_SUCCESS
39
+ raise CompileError, "Failed to define global str #{ident}: #{Yara::FFI.yrx_last_error}"
40
+ end
41
+ nil
42
+ end
43
+
44
+ def define_global_bool(ident, value)
45
+ result = Yara::FFI.yrx_compiler_define_global_bool(@compiler, ident, !!value)
46
+ if result != Yara::FFI::YRX_SUCCESS
47
+ raise CompileError, "Failed to define global bool #{ident}: #{Yara::FFI.yrx_last_error}"
48
+ end
49
+ nil
50
+ end
51
+
52
+ def define_global_int(ident, value)
53
+ result = Yara::FFI.yrx_compiler_define_global_int(@compiler, ident, value)
54
+ if result != Yara::FFI::YRX_SUCCESS
55
+ raise CompileError, "Failed to define global int #{ident}: #{Yara::FFI.yrx_last_error}"
56
+ end
57
+ nil
58
+ end
59
+
60
+ def define_global_float(ident, value)
61
+ result = Yara::FFI.yrx_compiler_define_global_float(@compiler, ident, value)
62
+ if result != Yara::FFI::YRX_SUCCESS
63
+ raise CompileError, "Failed to define global float #{ident}: #{Yara::FFI.yrx_last_error}"
64
+ end
65
+ nil
66
+ end
67
+
68
+ # Build and return a pointer to YRX_RULES. The caller is responsible for
69
+ # calling yrx_rules_destroy on the returned pointer when finished.
70
+ def build
71
+ rules_ptr = Yara::FFI.yrx_compiler_build(@compiler)
72
+ if rules_ptr.nil? || rules_ptr.null?
73
+ raise CompileError, "Failed to build rules: #{Yara::FFI.yrx_last_error}"
74
+ end
75
+ rules_ptr
76
+ end
77
+
78
+ # Public: Build and return a serialized representation of compiled rules.
79
+ #
80
+ # Compiles the currently added sources (via add_source) into a YRX_RULES
81
+ # object and serializes it into a binary blob suitable for persistence or
82
+ # transport. The returned String contains the serialized rules and can be
83
+ # deserialized later with Yara::Scanner.from_serialized or the underlying
84
+ # yrx_rules_deserialize FFI call.
85
+ #
86
+ # Returns a String containing binary serialized rules (raw bytes).
87
+ # Raises CompileError if compilation or serialization fails.
88
+ def build_serialized
89
+ rules_ptr = build
90
+
91
+ buf_ptr_holder = ::FFI::MemoryPointer.new(:pointer)
92
+ result = Yara::FFI.yrx_rules_serialize(rules_ptr, buf_ptr_holder)
93
+
94
+ # Destroy the rules pointer after serialization (we own it from build)
95
+ Yara::FFI.yrx_rules_destroy(rules_ptr)
96
+
97
+ if result != Yara::FFI::YRX_SUCCESS
98
+ raise CompileError, "Failed to serialize rules: #{Yara::FFI.yrx_last_error}"
99
+ end
100
+
101
+ buf_ptr = buf_ptr_holder.get_pointer(0)
102
+ buffer = Yara::FFI::YRX_BUFFER.new(buf_ptr)
103
+ begin
104
+ data_ptr = buffer[:data]
105
+ length = buffer[:length]
106
+ str = data_ptr.get_bytes(0, length)
107
+ return str
108
+ ensure
109
+ Yara::FFI.yrx_buffer_destroy(buf_ptr)
110
+ end
111
+ end
112
+
113
+ # Return compilation errors as a parsed JSON object (Array of error objects).
114
+ # This uses yrx_compiler_errors_json which returns a YRX_BUFFER containing
115
+ # the JSON serialization. The buffer is destroyed after being converted.
116
+ def errors_json
117
+ buf_ptr_holder = ::FFI::MemoryPointer.new(:pointer)
118
+ result = Yara::FFI.yrx_compiler_errors_json(@compiler, buf_ptr_holder)
119
+ if result != Yara::FFI::YRX_SUCCESS
120
+ raise CompileError, "Failed to get errors JSON: #{Yara::FFI.yrx_last_error}"
121
+ end
122
+
123
+ buf_ptr = buf_ptr_holder.get_pointer(0)
124
+ buffer = Yara::FFI::YRX_BUFFER.new(buf_ptr)
125
+ data_ptr = buffer[:data]
126
+ length = buffer[:length]
127
+ json_str = data_ptr.read_string_length(length)
128
+ Yara::FFI.yrx_buffer_destroy(buf_ptr)
129
+
130
+ JSON.parse(json_str)
131
+ end
132
+
133
+ # Return compilation warnings as parsed JSON (Array of warning objects).
134
+ def warnings_json
135
+ buf_ptr_holder = ::FFI::MemoryPointer.new(:pointer)
136
+ result = Yara::FFI.yrx_compiler_warnings_json(@compiler, buf_ptr_holder)
137
+ if result != Yara::FFI::YRX_SUCCESS
138
+ raise CompileError, "Failed to get warnings JSON: #{Yara::FFI.yrx_last_error}"
139
+ end
140
+
141
+ buf_ptr = buf_ptr_holder.get_pointer(0)
142
+ buffer = Yara::FFI::YRX_BUFFER.new(buf_ptr)
143
+ data_ptr = buffer[:data]
144
+ length = buffer[:length]
145
+ json_str = data_ptr.read_string_length(length)
146
+ Yara::FFI.yrx_buffer_destroy(buf_ptr)
147
+
148
+ JSON.parse(json_str)
149
+ end
150
+
151
+ def destroy
152
+ Yara::FFI.yrx_compiler_destroy(@compiler) if @compiler
153
+ @compiler = nil
154
+ end
155
+
156
+ # Ensure resources are cleaned up.
157
+ def finalize
158
+ destroy
159
+ end
160
+ end
161
+ end
data/lib/yara/ffi.rb CHANGED
@@ -178,6 +178,99 @@ module Yara
178
178
  # C Signature: enum YRX_RESULT yrx_scanner_scan(struct YRX_SCANNER *scanner, const uint8_t *data, size_t len)
179
179
  attach_function :yrx_scanner_scan, [:pointer, :pointer, :size_t], :int
180
180
 
181
+ # Public: Set timeout (in milliseconds) for a scanner.
182
+ #
183
+ # scanner - A Pointer to the scanner object
184
+ # timeout - A uint64_t value representing timeout in milliseconds
185
+ #
186
+ # Returns an Integer result code (YRX_SUCCESS on success).
187
+ # C Signature: enum YRX_RESULT yrx_scanner_set_timeout(struct YRX_SCANNER *scanner, uint64_t timeout)
188
+ attach_function :yrx_scanner_set_timeout, [:pointer, :ulong_long], :int
189
+
190
+ # Public: Set a global string variable for the scanner.
191
+ # C Signature: enum YRX_RESULT yrx_scanner_set_global_str(struct YRX_SCANNER *scanner, const char *ident, const char *value)
192
+ attach_function :yrx_scanner_set_global_str, [:pointer, :string, :string], :int
193
+
194
+ # Public: Set a global boolean variable for the scanner.
195
+ # C Signature: enum YRX_RESULT yrx_scanner_set_global_bool(struct YRX_SCANNER *scanner, const char *ident, bool value)
196
+ attach_function :yrx_scanner_set_global_bool, [:pointer, :string, :bool], :int
197
+
198
+ # Public: Set a global integer variable for the scanner.
199
+ # C Signature: enum YRX_RESULT yrx_scanner_set_global_int(struct YRX_SCANNER *scanner, const char *ident, int64_t value)
200
+ attach_function :yrx_scanner_set_global_int, [:pointer, :string, :long_long], :int
201
+
202
+ # Public: Set a global float variable for the scanner.
203
+ # C Signature: enum YRX_RESULT yrx_scanner_set_global_float(struct YRX_SCANNER *scanner, const char *ident, double value)
204
+ attach_function :yrx_scanner_set_global_float, [:pointer, :string, :double], :int
205
+
206
+ # YRX_COMPILER APIs
207
+ # Create a compiler: enum YRX_RESULT yrx_compiler_create(uint32_t flags, struct YRX_COMPILER **compiler)
208
+ attach_function :yrx_compiler_create, [:uint32, :pointer], :int
209
+
210
+ # Destroy a compiler: void yrx_compiler_destroy(struct YRX_COMPILER *compiler)
211
+ attach_function :yrx_compiler_destroy, [:pointer], :void
212
+
213
+ # Add source: enum YRX_RESULT yrx_compiler_add_source(struct YRX_COMPILER *compiler, const char *src)
214
+ attach_function :yrx_compiler_add_source, [:pointer, :string], :int
215
+
216
+ # Add source with origin: enum YRX_RESULT yrx_compiler_add_source_with_origin(struct YRX_COMPILER *compiler, const char *src, const char *origin)
217
+ attach_function :yrx_compiler_add_source_with_origin, [:pointer, :string, :string], :int
218
+
219
+ # Define globals on compiler
220
+ attach_function :yrx_compiler_define_global_str, [:pointer, :string, :string], :int
221
+ attach_function :yrx_compiler_define_global_bool, [:pointer, :string, :bool], :int
222
+ attach_function :yrx_compiler_define_global_int, [:pointer, :string, :long_long], :int
223
+ attach_function :yrx_compiler_define_global_float, [:pointer, :string, :double], :int
224
+
225
+ # Build compiler into rules: struct YRX_RULES *yrx_compiler_build(struct YRX_COMPILER *compiler)
226
+ attach_function :yrx_compiler_build, [:pointer], :pointer
227
+
228
+ # YRX_BUFFER utilities
229
+ # C Signature: void yrx_buffer_destroy(struct YRX_BUFFER *buf)
230
+ attach_function :yrx_buffer_destroy, [:pointer], :void
231
+
232
+ # Compiler diagnostics as JSON
233
+ # C Signature: enum YRX_RESULT yrx_compiler_errors_json(struct YRX_COMPILER *compiler, struct YRX_BUFFER **buf)
234
+ attach_function :yrx_compiler_errors_json, [:pointer, :pointer], :int
235
+
236
+ # C Signature: enum YRX_RESULT yrx_compiler_warnings_json(struct YRX_COMPILER *compiler, struct YRX_BUFFER **buf)
237
+ attach_function :yrx_compiler_warnings_json, [:pointer, :pointer], :int
238
+
239
+ # Serialize rules into a YRX_BUFFER
240
+ # C Signature: enum YRX_RESULT yrx_rules_serialize(const struct YRX_RULES *rules, struct YRX_BUFFER **buf)
241
+ attach_function :yrx_rules_serialize, [:pointer, :pointer], :int
242
+
243
+ # Deserialize rules from bytes
244
+ # C Signature: enum YRX_RESULT yrx_rules_deserialize(const uint8_t *data, size_t len, struct YRX_RULES **rules)
245
+ attach_function :yrx_rules_deserialize, [:pointer, :size_t, :pointer], :int
246
+
247
+ # Struct mapping for YRX_BUFFER
248
+ class YRX_BUFFER < ::FFI::Struct
249
+ layout :data, :pointer,
250
+ :length, :size_t
251
+ end
252
+
253
+ # Struct mapping for YRX_MATCH
254
+ class YRX_MATCH < ::FFI::Struct
255
+ layout :offset, :size_t,
256
+ :length, :size_t
257
+ end
258
+
259
+ # Struct mapping for YRX_METADATA
260
+ # Note: The value field is a union. We'll use a simple approach
261
+ # and access the value as a pointer, then interpret based on type.
262
+ class YRX_METADATA < ::FFI::Struct
263
+ layout :identifier, :pointer,
264
+ :value_type, :int,
265
+ :value, [:char, 16] # Union represented as byte array (largest possible union member)
266
+ end
267
+
268
+ # Struct mapping for YRX_METADATA_BYTES
269
+ class YRX_METADATA_BYTES < ::FFI::Struct
270
+ layout :length, :size_t,
271
+ :data, :pointer
272
+ end
273
+
181
274
  # Public: Extract the identifier (name) from a rule object.
182
275
  #
183
276
  # This function retrieves the rule name from a YRX_RULE pointer, typically
@@ -278,6 +371,85 @@ module Yara
278
371
  # C Signature: enum YRX_RESULT yrx_pattern_identifier(const struct YRX_PATTERN *pattern, const uint8_t **ident, size_t *len)
279
372
  attach_function :yrx_pattern_identifier, [:pointer, :pointer, :pointer], :int
280
373
 
374
+ # Internal: Callback function type for match iteration.
375
+ #
376
+ # This callback is invoked for each match found during pattern match
377
+ # iteration. The callback receives pointers to the match and user data.
378
+ #
379
+ # match - A Pointer to the YRX_MATCH structure
380
+ # user_data - A Pointer to optional user-provided data
381
+ #
382
+ # C Signature: typedef void (*YRX_MATCH_CALLBACK)(const struct YRX_MATCH *match, void *user_data)
383
+ callback :match_callback, [:pointer, :pointer], :void
384
+
385
+ # Internal: Callback function type for tag iteration.
386
+ #
387
+ # This callback is invoked for each tag during rule tag iteration.
388
+ # The callback receives a pointer to the tag string and user data.
389
+ #
390
+ # tag - A Pointer to null-terminated string representing the tag
391
+ # user_data - A Pointer to optional user-provided data
392
+ #
393
+ # C Signature: typedef void (*YRX_TAG_CALLBACK)(const char *tag, void *user_data)
394
+ callback :tag_callback, [:pointer, :pointer], :void
395
+
396
+ # Public: Iterate through all matches for a specific pattern.
397
+ #
398
+ # This function calls the provided callback for each match found for the
399
+ # given pattern during scanning. Each match provides the offset and length
400
+ # of where the pattern matched in the scanned data.
401
+ #
402
+ # pattern - A Pointer to the YRX_PATTERN structure
403
+ # callback - A Proc matching the match_callback signature
404
+ # user_data - A Pointer to optional data passed to callback (can be nil)
405
+ #
406
+ # Examples
407
+ #
408
+ # callback = proc { |match_ptr, user_data| puts "Found match" }
409
+ # result = Yara::FFI.yrx_pattern_iter_matches(pattern_ptr, callback, nil)
410
+ #
411
+ # Returns an Integer result code (YRX_SUCCESS on success).
412
+ # C Signature: enum YRX_RESULT yrx_pattern_iter_matches(const struct YRX_PATTERN *pattern, YRX_MATCH_CALLBACK callback, void *user_data)
413
+ attach_function :yrx_pattern_iter_matches, [:pointer, :match_callback, :pointer], :int
414
+
415
+ # Public: Extract the namespace from a rule object.
416
+ #
417
+ # This function retrieves the rule namespace from a YRX_RULE pointer.
418
+ # The namespace is returned as a pointer and length rather than a
419
+ # null-terminated string.
420
+ #
421
+ # rule - A Pointer to the YRX_RULE structure
422
+ # ns - A FFI::MemoryPointer that will receive the namespace pointer
423
+ # len - A FFI::MemoryPointer that will receive the namespace length
424
+ #
425
+ # Examples
426
+ #
427
+ # ns_ptr = FFI::MemoryPointer.new(:pointer)
428
+ # len_ptr = FFI::MemoryPointer.new(:size_t)
429
+ # result = Yara::FFI.yrx_rule_namespace(rule_ptr, ns_ptr, len_ptr)
430
+ #
431
+ # Returns an Integer result code (YRX_SUCCESS on success).
432
+ # C Signature: enum YRX_RESULT yrx_rule_namespace(const struct YRX_RULE *rule, const uint8_t **ns, size_t *len)
433
+ attach_function :yrx_rule_namespace, [:pointer, :pointer, :pointer], :int
434
+
435
+ # Public: Iterate through all tags in a rule.
436
+ #
437
+ # This function calls the provided callback for each tag defined
438
+ # in the rule. Tags are used for categorizing and organizing rules.
439
+ #
440
+ # rule - A Pointer to the YRX_RULE structure
441
+ # callback - A Proc matching the tag_callback signature
442
+ # user_data - A Pointer to optional data passed to callback (can be nil)
443
+ #
444
+ # Examples
445
+ #
446
+ # callback = proc { |tag_ptr, user_data| puts "Found tag: #{tag_ptr.read_string}" }
447
+ # result = Yara::FFI.yrx_rule_iter_tags(rule_ptr, callback, nil)
448
+ #
449
+ # Returns an Integer result code (YRX_SUCCESS on success).
450
+ # C Signature: enum YRX_RESULT yrx_rule_iter_tags(const struct YRX_RULE *rule, YRX_TAG_CALLBACK callback, void *user_data)
451
+ attach_function :yrx_rule_iter_tags, [:pointer, :tag_callback, :pointer], :int
452
+
281
453
  # Public: YARA-X result codes for operation status.
282
454
  #
283
455
  # These constants represent the possible return values from YARA-X functions.
@@ -301,5 +473,33 @@ module Yara
301
473
 
302
474
  # Public: Invalid argument passed to function.
303
475
  YRX_INVALID_ARGUMENT = 5
476
+
477
+ # Public: Metadata type constants for YRX_METADATA_TYPE enum.
478
+ #
479
+ # These constants represent the possible types of metadata values in YARA-X.
480
+ # They correspond to the YRX_METADATA_TYPE enum values in the C API.
481
+
482
+ # Public: 64-bit signed integer metadata value.
483
+ YRX_METADATA_TYPE_I64 = 0
484
+
485
+ # Public: 64-bit floating point metadata value.
486
+ YRX_METADATA_TYPE_F64 = 1
487
+
488
+ # Public: Boolean metadata value.
489
+ YRX_METADATA_TYPE_BOOLEAN = 2
490
+
491
+ # Public: String metadata value.
492
+ YRX_METADATA_TYPE_STRING = 3
493
+
494
+ # Public: Bytes metadata value.
495
+ YRX_METADATA_TYPE_BYTES = 4
496
+
497
+ # Public: Alternative naming following YARA-X C API documentation.
498
+ # Maps to the same values as above for compatibility.
499
+ YRX_I64 = 0
500
+ YRX_F64 = 1
501
+ YRX_BOOLEAN = 2
502
+ YRX_STRING = 3
503
+ YRX_BYTES = 4
304
504
  end
305
505
  end
@@ -0,0 +1,178 @@
1
+ module Yara
2
+ # Public: Represents a single pattern match found during YARA scanning.
3
+ #
4
+ # A PatternMatch contains detailed information about where and how a specific
5
+ # YARA pattern matched within the scanned data. This includes the exact offset
6
+ # and length of the match, allowing for precise forensic analysis and data
7
+ # extraction.
8
+ #
9
+ # PatternMatch instances are typically created internally during the scanning
10
+ # process and accessed through ScanResult methods like pattern_matches.
11
+ #
12
+ # Examples
13
+ #
14
+ # # Access pattern matches through scan results
15
+ # results.each do |result|
16
+ # result.pattern_matches.each do |pattern_name, matches|
17
+ # matches.each do |match|
18
+ # puts "Pattern #{pattern_name} matched at offset #{match.offset}"
19
+ # puts "Matched text: '#{match.matched_data(scanned_data)}'"
20
+ # end
21
+ # end
22
+ # end
23
+ class PatternMatch
24
+ # Public: The byte offset where this pattern match begins in the scanned data.
25
+ attr_reader :offset
26
+
27
+ # Public: The length in bytes of this pattern match.
28
+ attr_reader :length
29
+
30
+ # Public: Initialize a new PatternMatch.
31
+ #
32
+ # This constructor is typically called internally when processing YARA-X
33
+ # match results. It captures the precise location and size of a pattern
34
+ # match within scanned data.
35
+ #
36
+ # offset - An Integer byte offset where the match begins
37
+ # length - An Integer length in bytes of the match
38
+ #
39
+ # Examples
40
+ #
41
+ # # Typically created internally during scanning
42
+ # match = PatternMatch.new(42, 10)
43
+ # match.offset # => 42
44
+ # match.length # => 10
45
+ def initialize(offset, length)
46
+ @offset = offset
47
+ @length = length
48
+ end
49
+
50
+ # Public: Extract the actual matched data from the scanned content.
51
+ #
52
+ # This method returns the exact bytes that matched the pattern by extracting
53
+ # the appropriate slice from the original scanned data. This is useful for
54
+ # forensic analysis, debugging rules, and understanding what triggered a match.
55
+ #
56
+ # data - A String containing the original data that was scanned
57
+ #
58
+ # Examples
59
+ #
60
+ # # Extract what actually matched
61
+ # scan_data = "hello world test data"
62
+ # match = PatternMatch.new(6, 5) # matches "world"
63
+ # match.matched_data(scan_data) # => "world"
64
+ #
65
+ # Returns a String containing the matched bytes.
66
+ # Returns empty String if offset/length are outside data bounds.
67
+ def matched_data(data)
68
+ return "" if offset < 0 || offset >= data.bytesize
69
+ return "" if length <= 0 || offset + length > data.bytesize
70
+
71
+ data.byteslice(offset, length)
72
+ end
73
+
74
+ # Public: Get the end offset of this match (exclusive).
75
+ #
76
+ # This convenience method calculates the byte position immediately after
77
+ # the last byte of this match, which is useful for range operations and
78
+ # avoiding overlapping matches.
79
+ #
80
+ # Examples
81
+ #
82
+ # match = PatternMatch.new(10, 5)
83
+ # match.end_offset # => 15
84
+ #
85
+ # Returns an Integer representing the end offset (exclusive).
86
+ def end_offset
87
+ offset + length
88
+ end
89
+
90
+ # Public: Check if this match overlaps with another match.
91
+ #
92
+ # This method determines whether two pattern matches have any overlapping
93
+ # bytes. This is useful for analyzing complex rules with multiple patterns
94
+ # or detecting redundant matches.
95
+ #
96
+ # other - Another PatternMatch instance to compare against
97
+ #
98
+ # Examples
99
+ #
100
+ # match1 = PatternMatch.new(10, 5) # bytes 10-14
101
+ # match2 = PatternMatch.new(12, 5) # bytes 12-16
102
+ # match1.overlaps?(match2) # => true
103
+ #
104
+ # match3 = PatternMatch.new(20, 5) # bytes 20-24
105
+ # match1.overlaps?(match3) # => false
106
+ #
107
+ # Returns a Boolean indicating whether the matches overlap.
108
+ def overlaps?(other)
109
+ offset < other.end_offset && end_offset > other.offset
110
+ end
111
+
112
+ # Public: Get a human-readable string representation of this match.
113
+ #
114
+ # This method provides a concise string representation showing the key
115
+ # details of the match, useful for debugging and logging purposes.
116
+ #
117
+ # Examples
118
+ #
119
+ # match = PatternMatch.new(42, 10)
120
+ # match.to_s # => "PatternMatch(offset: 42, length: 10)"
121
+ #
122
+ # Returns a String representation of this match.
123
+ def to_s
124
+ "PatternMatch(offset: #{offset}, length: #{length})"
125
+ end
126
+
127
+ # Public: Detailed inspection string with all attributes.
128
+ #
129
+ # Provides a complete string representation including all match attributes,
130
+ # useful for debugging and development purposes.
131
+ #
132
+ # Examples
133
+ #
134
+ # match = PatternMatch.new(42, 10)
135
+ # match.inspect # => "#<Yara::PatternMatch:0x... @offset=42, @length=10>"
136
+ #
137
+ # Returns a String with detailed object information.
138
+ def inspect
139
+ "#<#{self.class}:0x#{object_id.to_s(16)} @offset=#{@offset}, @length=#{@length}>"
140
+ end
141
+
142
+ # Public: Compare two PatternMatch objects for equality.
143
+ #
144
+ # Two matches are considered equal if they have the same offset and length.
145
+ # This is useful for deduplicating matches or comparing results.
146
+ #
147
+ # other - Another PatternMatch instance to compare against
148
+ #
149
+ # Examples
150
+ #
151
+ # match1 = PatternMatch.new(10, 5)
152
+ # match2 = PatternMatch.new(10, 5)
153
+ # match1 == match2 # => true
154
+ #
155
+ # Returns a Boolean indicating equality.
156
+ def ==(other)
157
+ other.is_a?(PatternMatch) && offset == other.offset && length == other.length
158
+ end
159
+
160
+ # Public: Generate hash code for this match.
161
+ #
162
+ # Uses offset and length to generate a hash code, enabling PatternMatch
163
+ # instances to be used as hash keys or in sets.
164
+ #
165
+ # Examples
166
+ #
167
+ # match = PatternMatch.new(42, 10)
168
+ # {match => "info"} # Can be used as hash key
169
+ #
170
+ # Returns an Integer hash code.
171
+ def hash
172
+ [offset, length].hash
173
+ end
174
+
175
+ # Public: Enable hash equality based on hash code.
176
+ alias_method :eql?, :==
177
+ end
178
+ end