feat: add multi-engine regex flavour support
This commit is contained in:
37
engines/ruby/README.md
Normal file
37
engines/ruby/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Ruby regular-expression engine
|
||||
|
||||
Regex Tools executes Ruby expressions in the actual CRuby 4.0.0 engine built
|
||||
for `wasm32-wasi` by ruby.wasm. The reviewed runtime is the minimal
|
||||
`ruby.wasm` from `@ruby/4.0-wasm-wasi@2.9.3-2.9.4`; the JavaScript host API is
|
||||
`@ruby/wasm-wasi@2.9.3-2.9.4`.
|
||||
|
||||
The fixed `bridge.rb` module is evaluated once during worker start. Pattern,
|
||||
flags, subject, limits and replacement travel through a bounded,
|
||||
length-prefixed file in the worker-local WASI memory filesystem. Bounded inert
|
||||
JSON returns metadata only; replacement output is written as UTF-8 to a
|
||||
separate bounded worker-local file. No user-controlled value is evaluated or
|
||||
interpolated as Ruby or JavaScript source, and the bridge does not use the
|
||||
ruby.wasm `JS.eval` conversion path.
|
||||
|
||||
CRuby performs compilation and matching and reports character offsets, which
|
||||
the adapter exposes as Unicode code-point native offsets and normalizes to
|
||||
browser UTF-16 ranges. Native `String#gsub`/`String#sub` block iteration drives
|
||||
replacement matching without materializing expanded output. A fixed streaming
|
||||
implementation of CRuby 4.0.0's `rb_reg_regsub` grammar handles `\1` through
|
||||
`\9`, `\k<name>`, `\0`, `\&`, ``\` ``, `\'`, `\+`, and `\\`; a load-time
|
||||
bounded fixture verifies parity with CRuby. The expander stops on a valid UTF-8
|
||||
boundary at the configured byte limit. Unpaired browser UTF-16 surrogates are
|
||||
rejected before request encoding. A workbench timeout, cancellation,
|
||||
supersession or crash terminates the dedicated worker and its complete
|
||||
WebAssembly heap.
|
||||
|
||||
The deterministic pack contains only:
|
||||
|
||||
- `ruby.wasm`;
|
||||
- `engine-metadata.json`;
|
||||
- `SHA256SUMS`;
|
||||
- ruby.wasm's exact `LICENSE.txt`;
|
||||
- ruby.wasm's complete `NOTICE.txt`, including CRuby and bundled third-party
|
||||
notices; and
|
||||
- the exact browser WASI shim and `tslib` license texts required by the
|
||||
JavaScript host bundle.
|
||||
605
engines/ruby/bridge.rb
Normal file
605
engines/ruby/bridge.rb
Normal file
@@ -0,0 +1,605 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Fixed bridge evaluated once inside the bundled CRuby WebAssembly runtime.
|
||||
#
|
||||
# JavaScript writes one bounded, length-prefixed request to the worker-local
|
||||
# WASI memory filesystem. User-controlled pattern, subject and replacement
|
||||
# strings are never interpolated into Ruby or JavaScript source. Replacement
|
||||
# matching is driven by CRuby String#gsub/String#sub block iteration, while a
|
||||
# fixed streaming implementation of CRuby 4.0.0's rb_reg_regsub grammar writes
|
||||
# only the requested UTF-8 prefix to a separate worker-local output file.
|
||||
module RegexToolsRubyBridge
|
||||
ABI_VERSION = 2
|
||||
MAXIMUM_CAPTURE_GROUPS = 1_000
|
||||
MAXIMUM_ERROR_CHARACTERS = 1_024
|
||||
MAXIMUM_REQUEST_BYTES = 18 * 1024 * 1024
|
||||
MAXIMUM_RESPONSE_BYTES = 16 * 1024 * 1024
|
||||
MAXIMUM_MATCHES = 10_000
|
||||
MAXIMUM_CAPTURE_ROWS = 100_000
|
||||
MAXIMUM_OUTPUT_BYTES = 64 * 1024 * 1024
|
||||
REQUEST_FIELD_COUNT = 8
|
||||
REQUEST_FILE = "/regex-tools-request.bin"
|
||||
OUTPUT_FILE = "/regex-tools-output.bin"
|
||||
REQUEST_MAGIC = "RGXRUBY1".b
|
||||
REPLACEMENT_STOP = :regex_tools_replacement_stop
|
||||
HEX_DIGITS = "0123456789abcdef"
|
||||
|
||||
class ReplacementFailure < StandardError
|
||||
attr_reader :original
|
||||
|
||||
def initialize(original)
|
||||
@original = original
|
||||
super(original.message)
|
||||
set_backtrace(original.backtrace)
|
||||
end
|
||||
end
|
||||
|
||||
class BoundedUtf8Output
|
||||
attr_reader :bytes
|
||||
|
||||
def initialize(maximum_bytes)
|
||||
@maximum_bytes = maximum_bytes
|
||||
@value = String.new(capacity: [maximum_bytes, 64 * 1024].min)
|
||||
@value.force_encoding(Encoding::UTF_8)
|
||||
@bytes = 0
|
||||
@truncated = false
|
||||
end
|
||||
|
||||
def append(value, byte_start = 0, byte_length = nil)
|
||||
byte_length ||= value.bytesize - byte_start
|
||||
return true if byte_length <= 0
|
||||
return false if @truncated
|
||||
|
||||
remaining = @maximum_bytes - @bytes
|
||||
if byte_length <= remaining
|
||||
@value << value.byteslice(byte_start, byte_length)
|
||||
@bytes += byte_length
|
||||
return true
|
||||
end
|
||||
|
||||
prefix = value.byteslice(byte_start, remaining)
|
||||
prefix = prefix.byteslice(0, prefix.bytesize - 1) until prefix.valid_encoding?
|
||||
@value << prefix
|
||||
@bytes += prefix.bytesize
|
||||
@truncated = true
|
||||
false
|
||||
end
|
||||
|
||||
def truncated?
|
||||
@truncated
|
||||
end
|
||||
|
||||
def value
|
||||
@value
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
def run
|
||||
reset_output
|
||||
encode_json(run_record(*read_request))
|
||||
rescue StandardError => error
|
||||
begin
|
||||
reset_output
|
||||
rescue StandardError
|
||||
# The original bridge error is more useful than a secondary cleanup
|
||||
# failure. The adapter will still reject a stale output byte count.
|
||||
end
|
||||
encode_json(error_record("bridge", "bridge-error", error))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reset_output
|
||||
File.binwrite(OUTPUT_FILE, "".b)
|
||||
end
|
||||
|
||||
def read_request
|
||||
data = File.binread(REQUEST_FILE)
|
||||
unless data.bytesize <= MAXIMUM_REQUEST_BYTES &&
|
||||
data.byteslice(0, REQUEST_MAGIC.bytesize) == REQUEST_MAGIC
|
||||
raise ArgumentError, "Invalid Ruby bridge request."
|
||||
end
|
||||
|
||||
offset = REQUEST_MAGIC.bytesize
|
||||
fields = REQUEST_FIELD_COUNT.times.map do
|
||||
raise ArgumentError, "Truncated Ruby bridge request." if offset + 4 > data.bytesize
|
||||
|
||||
length = data.getbyte(offset) << 24 |
|
||||
data.getbyte(offset + 1) << 16 |
|
||||
data.getbyte(offset + 2) << 8 |
|
||||
data.getbyte(offset + 3)
|
||||
offset += 4
|
||||
raise ArgumentError, "Truncated Ruby bridge field." if offset + length > data.bytesize
|
||||
|
||||
field = data.byteslice(offset, length)
|
||||
offset += length
|
||||
field.force_encoding(Encoding::UTF_8)
|
||||
raise ArgumentError, "Invalid UTF-8 Ruby bridge field." unless field.valid_encoding?
|
||||
|
||||
field
|
||||
end
|
||||
raise ArgumentError, "Trailing Ruby bridge request data." unless offset == data.bytesize
|
||||
|
||||
fields
|
||||
end
|
||||
|
||||
def bounded_integer(value, label, maximum)
|
||||
text = value.to_s
|
||||
unless /\A[1-9][0-9]*\z/.match?(text)
|
||||
raise ArgumentError, "#{label} must be a positive decimal integer."
|
||||
end
|
||||
number = text.to_i
|
||||
unless number <= maximum
|
||||
raise ArgumentError, "#{label} exceeds the Ruby bridge limit."
|
||||
end
|
||||
number
|
||||
end
|
||||
|
||||
def run_record(
|
||||
operation_value,
|
||||
pattern_value,
|
||||
flags_value,
|
||||
subject_value,
|
||||
maximum_matches_value,
|
||||
maximum_capture_rows_value,
|
||||
replacement_value,
|
||||
maximum_output_bytes_value
|
||||
)
|
||||
operation = operation_value.to_s
|
||||
return identity if operation == "identity"
|
||||
|
||||
unless operation == "execute" || operation == "replace"
|
||||
return error_record(
|
||||
"bridge",
|
||||
"invalid-operation",
|
||||
"Unsupported Ruby bridge operation."
|
||||
)
|
||||
end
|
||||
|
||||
pattern = pattern_value.to_s
|
||||
flags = flags_value.to_s
|
||||
subject = subject_value.to_s
|
||||
replacement = replacement_value.to_s
|
||||
maximum_matches = bounded_integer(
|
||||
maximum_matches_value,
|
||||
"maximumMatches",
|
||||
MAXIMUM_MATCHES
|
||||
)
|
||||
maximum_capture_rows = bounded_integer(
|
||||
maximum_capture_rows_value,
|
||||
"maximumCaptureRows",
|
||||
MAXIMUM_CAPTURE_ROWS
|
||||
)
|
||||
maximum_output_bytes = bounded_integer(
|
||||
maximum_output_bytes_value,
|
||||
"maximumOutputBytes",
|
||||
MAXIMUM_OUTPUT_BYTES
|
||||
)
|
||||
unless /\A[gimx]*\z/.match?(flags) && flags.each_char.uniq.length == flags.length
|
||||
return error_record(
|
||||
"bridge",
|
||||
"invalid-flags",
|
||||
"The Ruby bridge received unsupported or duplicate flags."
|
||||
)
|
||||
end
|
||||
|
||||
regexp = compile(pattern, flags)
|
||||
return regexp if regexp.is_a?(Hash)
|
||||
|
||||
group_count = capture_group_count(regexp)
|
||||
if group_count > MAXIMUM_CAPTURE_GROUPS
|
||||
return error_record(
|
||||
"compile",
|
||||
"capture-group-limit",
|
||||
"The pattern declares #{group_count} capture groups; " \
|
||||
"the bridge limit is #{MAXIMUM_CAPTURE_GROUPS}."
|
||||
)
|
||||
end
|
||||
|
||||
begin
|
||||
collected = collect_matches(
|
||||
regexp,
|
||||
subject,
|
||||
flags.include?("g"),
|
||||
group_count,
|
||||
maximum_matches,
|
||||
maximum_capture_rows,
|
||||
operation == "replace" ? replacement : nil,
|
||||
maximum_output_bytes
|
||||
)
|
||||
rescue ReplacementFailure => error
|
||||
return error_record(
|
||||
"replacement",
|
||||
"replacement-error",
|
||||
error.original
|
||||
)
|
||||
rescue StandardError => error
|
||||
return error_record("match", "match-error", error)
|
||||
end
|
||||
|
||||
result = {
|
||||
"abi" => ABI_VERSION,
|
||||
"ok" => true,
|
||||
"groupCount" => group_count,
|
||||
"groupNames" => capture_group_names(regexp),
|
||||
"matches" => collected["matches"],
|
||||
"resultsTruncated" => collected["resultsTruncated"]
|
||||
}
|
||||
return result if operation == "execute"
|
||||
|
||||
File.binwrite(OUTPUT_FILE, collected["output"])
|
||||
result.merge(
|
||||
"outputBytes" => collected["outputBytes"],
|
||||
"outputTruncated" => collected["outputTruncated"]
|
||||
)
|
||||
rescue StandardError => error
|
||||
error_record("bridge", "bridge-error", error)
|
||||
end
|
||||
|
||||
# RbValue#toJS delegates Hash/Array conversion to the ruby.wasm `js` gem,
|
||||
# whose generic conversion path constructs objects through JS.eval. The
|
||||
# Toolbox CSP deliberately disallows that capability. Return one inert,
|
||||
# metadata-only JSON string, assembled incrementally without per-value
|
||||
# arrays or the potentially 6x JSON escaping amplification of output text.
|
||||
def encode_json(value)
|
||||
output = String.new(capacity: 4 * 1024)
|
||||
output.force_encoding(Encoding::UTF_8)
|
||||
append_json(output, value)
|
||||
if output.bytesize > MAXIMUM_RESPONSE_BYTES
|
||||
raise RangeError, "Ruby bridge metadata exceeds its response limit."
|
||||
end
|
||||
output
|
||||
end
|
||||
|
||||
def append_json(output, value)
|
||||
case value
|
||||
when Hash
|
||||
output << "{"
|
||||
first = true
|
||||
value.each do |key, item|
|
||||
output << "," unless first
|
||||
first = false
|
||||
append_json_string(output, key.to_s)
|
||||
output << ":"
|
||||
append_json(output, item)
|
||||
end
|
||||
output << "}"
|
||||
when Array
|
||||
output << "["
|
||||
value.each_with_index do |item, index|
|
||||
output << "," unless index.zero?
|
||||
append_json(output, item)
|
||||
end
|
||||
output << "]"
|
||||
when String
|
||||
append_json_string(output, value)
|
||||
when Integer
|
||||
output << value.to_s
|
||||
when true
|
||||
output << "true"
|
||||
when false
|
||||
output << "false"
|
||||
when nil
|
||||
output << "null"
|
||||
else
|
||||
append_json_string(output, value.to_s)
|
||||
end
|
||||
if output.bytesize > MAXIMUM_RESPONSE_BYTES
|
||||
raise RangeError, "Ruby bridge metadata exceeds its response limit."
|
||||
end
|
||||
end
|
||||
|
||||
def append_json_string(output, value)
|
||||
text = value.valid_encoding? \
|
||||
? value \
|
||||
: value.encode(
|
||||
Encoding::UTF_8,
|
||||
invalid: :replace,
|
||||
undef: :replace,
|
||||
replace: "\uFFFD"
|
||||
)
|
||||
output << "\""
|
||||
text.each_codepoint do |codepoint|
|
||||
case codepoint
|
||||
when 0x08 then output << "\\b"
|
||||
when 0x09 then output << "\\t"
|
||||
when 0x0A then output << "\\n"
|
||||
when 0x0C then output << "\\f"
|
||||
when 0x0D then output << "\\r"
|
||||
when 0x22 then output << "\\\""
|
||||
when 0x5C then output << "\\\\"
|
||||
when 0x00..0x1F
|
||||
output << "\\u00"
|
||||
output << HEX_DIGITS.getbyte(codepoint >> 4)
|
||||
output << HEX_DIGITS.getbyte(codepoint & 0x0F)
|
||||
else
|
||||
output << codepoint
|
||||
end
|
||||
end
|
||||
output << "\""
|
||||
end
|
||||
|
||||
def identity
|
||||
verify_replacement_parity
|
||||
{
|
||||
"abi" => ABI_VERSION,
|
||||
"ok" => true,
|
||||
"identity" => {
|
||||
"implementation" => RUBY_ENGINE,
|
||||
"rubyVersion" => RUBY_VERSION,
|
||||
"platform" => RUBY_PLATFORM,
|
||||
"defaultExternalEncoding" => Encoding.default_external.name
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def compile(pattern, flags)
|
||||
options = 0
|
||||
options |= Regexp::IGNORECASE if flags.include?("i")
|
||||
options |= Regexp::MULTILINE if flags.include?("m")
|
||||
options |= Regexp::EXTENDED if flags.include?("x")
|
||||
Regexp.new(pattern, options)
|
||||
rescue RegexpError => error
|
||||
error_record("compile", "compile-error", error)
|
||||
rescue StandardError => error
|
||||
error_record("compile", "compile-error", error)
|
||||
end
|
||||
|
||||
def capture_group_count(regexp)
|
||||
# Alternating with an empty non-capturing branch makes a match available
|
||||
# without changing the original capture numbering.
|
||||
Regexp.union(regexp, /(?:)/).match("").length - 1
|
||||
end
|
||||
|
||||
def capture_group_names(regexp)
|
||||
regexp.named_captures.flat_map do |name, numbers|
|
||||
numbers.map { |number| [number, name] }
|
||||
end.sort_by { |number, name| [number, name] }
|
||||
end
|
||||
|
||||
def match_record(match, group_count)
|
||||
captures = (1..group_count).map do |group_number|
|
||||
start_offset = match.begin(group_number)
|
||||
start_offset.nil? \
|
||||
? nil \
|
||||
: [start_offset, match.end(group_number)]
|
||||
end
|
||||
{
|
||||
"span" => [match.begin(0), match.end(0)],
|
||||
"captures" => captures
|
||||
}
|
||||
end
|
||||
|
||||
def collect_matches(
|
||||
regexp,
|
||||
subject,
|
||||
global,
|
||||
group_count,
|
||||
maximum_matches,
|
||||
maximum_capture_rows,
|
||||
replacement = nil,
|
||||
maximum_output_bytes = 1
|
||||
)
|
||||
records = []
|
||||
capture_rows = 0
|
||||
results_truncated = false
|
||||
output = replacement.nil? \
|
||||
? nil \
|
||||
: BoundedUtf8Output.new(maximum_output_bytes)
|
||||
last_replacement_end = 0
|
||||
|
||||
callback = proc do
|
||||
match = Regexp.last_match
|
||||
rows_for_match = [1, group_count].max
|
||||
if records.length >= maximum_matches ||
|
||||
capture_rows + rows_for_match > maximum_capture_rows
|
||||
results_truncated = true
|
||||
throw REPLACEMENT_STOP
|
||||
end
|
||||
|
||||
records << match_record(match, group_count)
|
||||
capture_rows += rows_for_match
|
||||
unless output.nil? || output.truncated?
|
||||
match_start = match.bytebegin(0)
|
||||
output.append(
|
||||
subject,
|
||||
last_replacement_end,
|
||||
match_start - last_replacement_end
|
||||
)
|
||||
unless output.truncated?
|
||||
begin
|
||||
append_replacement(
|
||||
output,
|
||||
replacement,
|
||||
subject,
|
||||
match,
|
||||
regexp.names.empty?
|
||||
)
|
||||
rescue StandardError => error
|
||||
raise ReplacementFailure, error
|
||||
end
|
||||
end
|
||||
end
|
||||
last_replacement_end = match.byteend(0)
|
||||
""
|
||||
end
|
||||
|
||||
catch(REPLACEMENT_STOP) do
|
||||
global ? subject.gsub(regexp, &callback) : subject.sub(regexp, &callback)
|
||||
end
|
||||
|
||||
unless output.nil? || output.truncated?
|
||||
output.append(
|
||||
subject,
|
||||
last_replacement_end,
|
||||
subject.bytesize - last_replacement_end
|
||||
)
|
||||
end
|
||||
|
||||
result = {
|
||||
"matches" => records,
|
||||
"resultsTruncated" => results_truncated
|
||||
}
|
||||
unless output.nil?
|
||||
result["output"] = output.value
|
||||
result["outputBytes"] = output.bytes
|
||||
result["outputTruncated"] = output.truncated?
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
# Mirrors CRuby 4.0.0 rb_reg_regsub: \1..\9, \k<name>, \0, \&, \`,
|
||||
# \', \+, \\ and literal preservation for every other escaped character.
|
||||
# Match selection and byte offsets remain native MatchData results.
|
||||
def append_replacement(
|
||||
output,
|
||||
replacement,
|
||||
subject,
|
||||
match,
|
||||
numeric_backrefs_active
|
||||
)
|
||||
bytes = replacement.b
|
||||
cursor = 0
|
||||
while cursor < bytes.bytesize && !output.truncated?
|
||||
slash = bytes.index("\\", cursor)
|
||||
if slash.nil?
|
||||
output.append(replacement, cursor, bytes.bytesize - cursor)
|
||||
break
|
||||
end
|
||||
output.append(replacement, cursor, slash - cursor) if slash > cursor
|
||||
break if output.truncated?
|
||||
|
||||
if slash + 1 >= bytes.bytesize
|
||||
output.append(replacement, slash, 1)
|
||||
break
|
||||
end
|
||||
|
||||
marker = bytes.getbyte(slash + 1)
|
||||
cursor = slash + 2
|
||||
case marker
|
||||
when 0x31..0x39
|
||||
append_capture(
|
||||
output,
|
||||
subject,
|
||||
match,
|
||||
marker - 0x30
|
||||
) if numeric_backrefs_active
|
||||
when 0x6B # k
|
||||
if cursor < bytes.bytesize && bytes.getbyte(cursor) == 0x3C # <
|
||||
name_start = cursor + 1
|
||||
name_end = bytes.index(">", name_start)
|
||||
if name_end.nil?
|
||||
raise RuntimeError, "invalid group name reference format"
|
||||
end
|
||||
name = replacement.byteslice(name_start, name_end - name_start)
|
||||
append_capture(output, subject, match, name)
|
||||
cursor = name_end + 1
|
||||
else
|
||||
output.append(replacement, slash, 2)
|
||||
end
|
||||
when 0x30, 0x26 # 0, &
|
||||
append_capture(output, subject, match, 0)
|
||||
when 0x60 # `
|
||||
output.append(subject, 0, match.bytebegin(0))
|
||||
when 0x27 # '
|
||||
match_end = match.byteend(0)
|
||||
output.append(subject, match_end, subject.bytesize - match_end)
|
||||
when 0x2B # +
|
||||
group_number = match.length - 1
|
||||
group_number -= 1 while group_number > 0 && match.bytebegin(group_number).nil?
|
||||
append_capture(output, subject, match, group_number) if group_number > 0
|
||||
when 0x5C # \
|
||||
output.append(replacement, slash + 1, 1)
|
||||
else
|
||||
if marker >= 0x80
|
||||
cursor += 1 while cursor < bytes.bytesize &&
|
||||
(bytes.getbyte(cursor) & 0xC0) == 0x80
|
||||
end
|
||||
output.append(replacement, slash, cursor - slash)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def append_capture(output, subject, match, group)
|
||||
if group.is_a?(Integer) && group >= match.length
|
||||
return
|
||||
end
|
||||
byte_start, byte_end = match.byteoffset(group)
|
||||
return if byte_start.nil?
|
||||
|
||||
output.append(subject, byte_start, byte_end - byte_start)
|
||||
end
|
||||
|
||||
def verify_replacement_parity
|
||||
return if @replacement_parity_verified
|
||||
|
||||
slash = 92.chr
|
||||
fixtures = [
|
||||
[
|
||||
/(a)(b)?/,
|
||||
"ab a",
|
||||
[
|
||||
"<",
|
||||
"#{slash}0",
|
||||
"|#{slash}&",
|
||||
"|#{slash}1",
|
||||
"|#{slash}2",
|
||||
"|#{slash}9",
|
||||
"|#{slash}+",
|
||||
"|#{slash}#{slash}",
|
||||
"|#{slash}q",
|
||||
"|#{slash}`",
|
||||
"|#{slash}'",
|
||||
">"
|
||||
].join,
|
||||
true
|
||||
],
|
||||
[
|
||||
/(?<left>a)(?<right>b)?/,
|
||||
"ab a",
|
||||
"<#{slash}k<left>|#{slash}k<right>|#{slash}1>",
|
||||
true
|
||||
],
|
||||
[/(é)/, "xéy", "#{slash}é/#{slash}1", false]
|
||||
]
|
||||
fixtures.each do |regexp, subject, replacement, global|
|
||||
native = global \
|
||||
? subject.gsub(regexp, replacement) \
|
||||
: subject.sub(regexp, replacement)
|
||||
bounded = collect_matches(
|
||||
regexp,
|
||||
subject,
|
||||
global,
|
||||
capture_group_count(regexp),
|
||||
MAXIMUM_MATCHES,
|
||||
MAXIMUM_CAPTURE_ROWS,
|
||||
replacement,
|
||||
1024 * 1024
|
||||
)
|
||||
unless !bounded["resultsTruncated"] &&
|
||||
!bounded["outputTruncated"] &&
|
||||
bounded["output"] == native
|
||||
raise "Bounded Ruby replacement semantics failed the CRuby parity test."
|
||||
end
|
||||
end
|
||||
@replacement_parity_verified = true
|
||||
end
|
||||
|
||||
def error_record(phase, code, error)
|
||||
message = error.is_a?(Exception) ? error.message : error.to_s
|
||||
if message.length > MAXIMUM_ERROR_CHARACTERS
|
||||
message = "#{message[0, MAXIMUM_ERROR_CHARACTERS - 1]}…"
|
||||
end
|
||||
{
|
||||
"abi" => ABI_VERSION,
|
||||
"ok" => false,
|
||||
"phase" => phase,
|
||||
"code" => code,
|
||||
"message" => message
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RegexToolsRubyBridge
|
||||
Reference in New Issue
Block a user