rufio 0.33.0 → 0.34.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
- data/CHANGELOG.md +50 -8
- data/lib/rufio/async_scanner_fiber.rb +154 -0
- data/lib/rufio/async_scanner_promise.rb +66 -0
- data/lib/rufio/command_logger.rb +3 -0
- data/lib/rufio/native/rufio_zig.bundle +0 -0
- data/lib/rufio/native_scanner.rb +252 -233
- data/lib/rufio/native_scanner_zig.rb +215 -82
- data/lib/rufio/parallel_scanner.rb +173 -0
- data/lib/rufio/version.rb +1 -1
- data/lib/rufio.rb +3 -1
- data/lib_zig/rufio_native/Makefile +2 -1
- data/lib_zig/rufio_native/src/main.zig +328 -117
- data/lib_zig/rufio_native/src/main.zig.sync +205 -0
- metadata +7 -10
- data/lib/rufio/native/rufio_native.bundle +0 -0
- data/lib/rufio/native_scanner_magnus.rb +0 -194
- data/lib_rust/rufio_native/.cargo/config.toml +0 -2
- data/lib_rust/rufio_native/Cargo.lock +0 -346
- data/lib_rust/rufio_native/Cargo.toml +0 -18
- data/lib_rust/rufio_native/build.rs +0 -46
- data/lib_rust/rufio_native/src/lib.rs +0 -197
- /data/docs/{CHANGELOG_v0.33.0.md → CHANGELOG_v0.34.0.md} +0 -0
data/lib/rufio/native_scanner.rb
CHANGED
|
@@ -1,260 +1,309 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'ffi'
|
|
4
|
-
require 'json'
|
|
5
|
-
|
|
6
3
|
module Rufio
|
|
7
|
-
#
|
|
8
|
-
class
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
# 非同期スキャナークラス(Pure Ruby実装、ポーリングベース)
|
|
5
|
+
class NativeScannerRubyCore
|
|
6
|
+
POLL_INTERVAL = 0.01 # 10ms
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@thread = nil
|
|
10
|
+
@state = :idle
|
|
11
|
+
@results = []
|
|
12
|
+
@error = nil
|
|
13
|
+
@current_progress = 0
|
|
14
|
+
@total_progress = 0
|
|
15
|
+
@mutex = Mutex.new
|
|
16
|
+
end
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
# 非同期スキャン開始
|
|
19
|
+
def scan_async(path)
|
|
20
|
+
raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
|
|
21
|
+
raise StandardError, "Scanner is already running" unless @state == :idle
|
|
22
|
+
|
|
23
|
+
@mutex.synchronize do
|
|
24
|
+
@state = :scanning
|
|
25
|
+
@results = []
|
|
26
|
+
@error = nil
|
|
27
|
+
@current_progress = 0
|
|
28
|
+
@total_progress = 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@thread = Thread.new do
|
|
32
|
+
begin
|
|
33
|
+
# ディレクトリをスキャン
|
|
34
|
+
entries = []
|
|
35
|
+
Dir.foreach(path) do |entry|
|
|
36
|
+
next if entry == '.' || entry == '..'
|
|
37
|
+
|
|
38
|
+
# キャンセルチェック
|
|
39
|
+
break if @state == :cancelled
|
|
40
|
+
|
|
41
|
+
full_path = File.join(path, entry)
|
|
42
|
+
stat = File.lstat(full_path)
|
|
43
|
+
|
|
44
|
+
entries << {
|
|
45
|
+
name: entry,
|
|
46
|
+
type: file_type(stat),
|
|
47
|
+
size: stat.size,
|
|
48
|
+
mtime: stat.mtime.to_i,
|
|
49
|
+
mode: stat.mode,
|
|
50
|
+
executable: stat.executable?,
|
|
51
|
+
hidden: entry.start_with?('.')
|
|
52
|
+
}
|
|
16
53
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
54
|
+
# 進捗を更新
|
|
55
|
+
@mutex.synchronize do
|
|
56
|
+
@current_progress += 1
|
|
57
|
+
@total_progress = entries.length + 1
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# 結果を保存
|
|
62
|
+
@mutex.synchronize do
|
|
63
|
+
if @state == :cancelled
|
|
64
|
+
@state = :cancelled
|
|
65
|
+
else
|
|
66
|
+
@results = entries
|
|
67
|
+
@state = :done
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
@mutex.synchronize do
|
|
72
|
+
@error = e
|
|
73
|
+
@state = :failed
|
|
74
|
+
end
|
|
75
|
+
end
|
|
29
76
|
end
|
|
30
77
|
|
|
31
|
-
|
|
32
|
-
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# 高速スキャン(エントリ数制限付き)
|
|
82
|
+
def scan_fast_async(path, max_entries)
|
|
83
|
+
raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
|
|
84
|
+
raise StandardError, "Scanner is already running" unless @state == :idle
|
|
85
|
+
|
|
86
|
+
@mutex.synchronize do
|
|
87
|
+
@state = :scanning
|
|
88
|
+
@results = []
|
|
89
|
+
@error = nil
|
|
90
|
+
@current_progress = 0
|
|
91
|
+
@total_progress = max_entries
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
@thread = Thread.new do
|
|
95
|
+
begin
|
|
96
|
+
entries = []
|
|
97
|
+
count = 0
|
|
98
|
+
|
|
99
|
+
Dir.foreach(path) do |entry|
|
|
100
|
+
next if entry == '.' || entry == '..'
|
|
101
|
+
break if count >= max_entries
|
|
102
|
+
|
|
103
|
+
# キャンセルチェック
|
|
104
|
+
break if @state == :cancelled
|
|
105
|
+
|
|
106
|
+
full_path = File.join(path, entry)
|
|
107
|
+
stat = File.lstat(full_path)
|
|
108
|
+
|
|
109
|
+
entries << {
|
|
110
|
+
name: entry,
|
|
111
|
+
type: file_type(stat),
|
|
112
|
+
size: stat.size,
|
|
113
|
+
mtime: stat.mtime.to_i,
|
|
114
|
+
mode: stat.mode,
|
|
115
|
+
executable: stat.executable?,
|
|
116
|
+
hidden: entry.start_with?('.')
|
|
117
|
+
}
|
|
118
|
+
count += 1
|
|
119
|
+
|
|
120
|
+
# 進捗を更新
|
|
121
|
+
@mutex.synchronize do
|
|
122
|
+
@current_progress = count
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# 結果を保存
|
|
127
|
+
@mutex.synchronize do
|
|
128
|
+
if @state == :cancelled
|
|
129
|
+
@state = :cancelled
|
|
130
|
+
else
|
|
131
|
+
@results = entries
|
|
132
|
+
@state = :done
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
rescue StandardError => e
|
|
136
|
+
@mutex.synchronize do
|
|
137
|
+
@error = e
|
|
138
|
+
@state = :failed
|
|
139
|
+
end
|
|
140
|
+
end
|
|
33
141
|
end
|
|
142
|
+
|
|
143
|
+
self
|
|
34
144
|
end
|
|
35
145
|
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
146
|
+
# ポーリングして完了待ち
|
|
147
|
+
def wait(timeout: nil)
|
|
148
|
+
start_time = Time.now
|
|
149
|
+
loop do
|
|
150
|
+
state = get_state
|
|
151
|
+
|
|
152
|
+
case state
|
|
153
|
+
when :done
|
|
154
|
+
return get_results
|
|
155
|
+
when :failed
|
|
156
|
+
raise @error || StandardError.new("Scan failed")
|
|
157
|
+
when :cancelled
|
|
158
|
+
raise StandardError, "Scan cancelled"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if timeout && (Time.now - start_time) > timeout
|
|
162
|
+
raise StandardError, "Timeout"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
sleep POLL_INTERVAL
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# 進捗報告付きで完了待ち
|
|
170
|
+
def wait_with_progress(&block)
|
|
171
|
+
loop do
|
|
172
|
+
state = get_state
|
|
173
|
+
progress = get_progress
|
|
174
|
+
|
|
175
|
+
yield(progress[:current], progress[:total]) if block_given?
|
|
176
|
+
|
|
177
|
+
case state
|
|
178
|
+
when :done
|
|
179
|
+
return get_results
|
|
180
|
+
when :failed
|
|
181
|
+
raise @error || StandardError.new("Scan failed")
|
|
182
|
+
when :cancelled
|
|
183
|
+
raise StandardError, "Scan cancelled"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
sleep POLL_INTERVAL
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# 状態確認
|
|
191
|
+
def get_state
|
|
192
|
+
@mutex.synchronize { @state }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# 進捗取得
|
|
196
|
+
def get_progress
|
|
197
|
+
@mutex.synchronize do
|
|
198
|
+
{
|
|
199
|
+
current: @current_progress,
|
|
200
|
+
total: @total_progress
|
|
201
|
+
}
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# キャンセル
|
|
206
|
+
def cancel
|
|
207
|
+
@mutex.synchronize do
|
|
208
|
+
@state = :cancelled if @state == :scanning
|
|
49
209
|
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# 結果取得(完了後)
|
|
213
|
+
def get_results
|
|
214
|
+
@mutex.synchronize { @results.dup }
|
|
215
|
+
end
|
|
50
216
|
|
|
51
|
-
|
|
52
|
-
|
|
217
|
+
# スキャナーを明示的に破棄
|
|
218
|
+
def close
|
|
219
|
+
@thread&.join if @thread&.alive?
|
|
220
|
+
@thread = nil
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
private
|
|
224
|
+
|
|
225
|
+
# ファイルタイプを判定
|
|
226
|
+
def file_type(stat)
|
|
227
|
+
if stat.directory?
|
|
228
|
+
'directory'
|
|
229
|
+
elsif stat.symlink?
|
|
230
|
+
'symlink'
|
|
231
|
+
elsif stat.file?
|
|
232
|
+
'file'
|
|
233
|
+
else
|
|
234
|
+
'other'
|
|
53
235
|
end
|
|
54
236
|
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# NativeScanner - Rubyベースのディレクトリスキャナー(ネイティブライブラリは削除済み)
|
|
240
|
+
class NativeScanner
|
|
241
|
+
@mode = 'ruby'
|
|
242
|
+
@current_library = nil
|
|
55
243
|
|
|
56
244
|
class << self
|
|
57
|
-
#
|
|
245
|
+
# モード設定(常にRubyモードを使用)
|
|
58
246
|
def mode=(value)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if RustLib.available?
|
|
62
|
-
@mode = 'rust'
|
|
63
|
-
@current_library = RustLib
|
|
64
|
-
else
|
|
65
|
-
@mode = 'ruby'
|
|
66
|
-
@current_library = nil
|
|
67
|
-
end
|
|
68
|
-
when 'go'
|
|
69
|
-
if GoLib.available?
|
|
70
|
-
@mode = 'go'
|
|
71
|
-
@current_library = GoLib
|
|
72
|
-
else
|
|
73
|
-
@mode = 'ruby'
|
|
74
|
-
@current_library = nil
|
|
75
|
-
end
|
|
76
|
-
when 'auto'
|
|
77
|
-
# 優先順位: Rust > Go > Ruby
|
|
78
|
-
if RustLib.available?
|
|
79
|
-
@mode = 'rust'
|
|
80
|
-
@current_library = RustLib
|
|
81
|
-
elsif GoLib.available?
|
|
82
|
-
@mode = 'go'
|
|
83
|
-
@current_library = GoLib
|
|
84
|
-
else
|
|
85
|
-
@mode = 'ruby'
|
|
86
|
-
@current_library = nil
|
|
87
|
-
end
|
|
88
|
-
when 'ruby'
|
|
89
|
-
@mode = 'ruby'
|
|
90
|
-
@current_library = nil
|
|
91
|
-
else
|
|
92
|
-
# 無効なモードはrubyにフォールバック
|
|
93
|
-
@mode = 'ruby'
|
|
94
|
-
@current_library = nil
|
|
95
|
-
end
|
|
247
|
+
@mode = 'ruby'
|
|
248
|
+
@current_library = nil
|
|
96
249
|
end
|
|
97
250
|
|
|
98
251
|
# 現在のモード取得
|
|
99
252
|
def mode
|
|
100
|
-
|
|
101
|
-
self.mode = 'auto' if @mode.nil?
|
|
102
|
-
@mode
|
|
253
|
+
@mode ||= 'ruby'
|
|
103
254
|
end
|
|
104
255
|
|
|
105
|
-
#
|
|
256
|
+
# 利用可能なライブラリをチェック(Rubyのみ)
|
|
106
257
|
def available_libraries
|
|
107
258
|
{
|
|
108
|
-
|
|
109
|
-
go: GoLib.available?
|
|
259
|
+
ruby: true
|
|
110
260
|
}
|
|
111
261
|
end
|
|
112
262
|
|
|
113
263
|
# ディレクトリをスキャン
|
|
114
264
|
def scan_directory(path)
|
|
115
|
-
|
|
116
|
-
mode if @mode.nil?
|
|
117
|
-
|
|
118
|
-
case @mode
|
|
119
|
-
when 'rust'
|
|
120
|
-
scan_with_rust(path)
|
|
121
|
-
when 'go'
|
|
122
|
-
scan_with_go(path)
|
|
123
|
-
else
|
|
124
|
-
scan_with_ruby(path)
|
|
125
|
-
end
|
|
265
|
+
scan_with_ruby(path)
|
|
126
266
|
end
|
|
127
267
|
|
|
128
268
|
# 高速スキャン(エントリ数制限付き)
|
|
129
269
|
def scan_directory_fast(path, max_entries = 1000)
|
|
130
|
-
|
|
131
|
-
mode if @mode.nil?
|
|
132
|
-
|
|
133
|
-
case @mode
|
|
134
|
-
when 'rust'
|
|
135
|
-
scan_fast_with_rust(path, max_entries)
|
|
136
|
-
when 'go'
|
|
137
|
-
scan_fast_with_go(path, max_entries)
|
|
138
|
-
else
|
|
139
|
-
scan_fast_with_ruby(path, max_entries)
|
|
140
|
-
end
|
|
270
|
+
scan_fast_with_ruby(path, max_entries)
|
|
141
271
|
end
|
|
142
272
|
|
|
143
273
|
# バージョン情報取得
|
|
144
274
|
def version
|
|
145
|
-
#
|
|
146
|
-
mode if @mode.nil?
|
|
147
|
-
|
|
148
|
-
case @mode
|
|
149
|
-
when 'rust'
|
|
150
|
-
ptr = RustLib.get_version
|
|
151
|
-
ptr.read_string
|
|
152
|
-
when 'go'
|
|
153
|
-
ptr = GoLib.GetVersion
|
|
154
|
-
result = ptr.read_string
|
|
155
|
-
GoLib.FreeCString(ptr)
|
|
156
|
-
result
|
|
157
|
-
else
|
|
158
|
-
"Ruby #{RUBY_VERSION}"
|
|
159
|
-
end
|
|
275
|
+
"Ruby #{RUBY_VERSION}"
|
|
160
276
|
end
|
|
161
277
|
|
|
162
278
|
private
|
|
163
279
|
|
|
164
|
-
#
|
|
165
|
-
def scan_with_rust(path)
|
|
166
|
-
raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
|
|
167
|
-
|
|
168
|
-
ptr = RustLib.scan_directory(path)
|
|
169
|
-
json_str = ptr.read_string
|
|
170
|
-
parse_scan_result(json_str)
|
|
171
|
-
rescue StandardError => e
|
|
172
|
-
raise StandardError, "Rust scan failed: #{e.message}"
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
# Rustライブラリで高速スキャン
|
|
176
|
-
def scan_fast_with_rust(path, max_entries)
|
|
177
|
-
raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
|
|
178
|
-
|
|
179
|
-
ptr = RustLib.scan_directory_fast(path, max_entries)
|
|
180
|
-
json_str = ptr.read_string
|
|
181
|
-
parse_scan_result(json_str)
|
|
182
|
-
rescue StandardError => e
|
|
183
|
-
raise StandardError, "Rust fast scan failed: #{e.message}"
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
# Goライブラリでスキャン
|
|
187
|
-
def scan_with_go(path)
|
|
188
|
-
raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
|
|
189
|
-
|
|
190
|
-
ptr = GoLib.ScanDirectory(path)
|
|
191
|
-
json_str = ptr.read_string
|
|
192
|
-
GoLib.FreeCString(ptr)
|
|
193
|
-
parse_scan_result(json_str)
|
|
194
|
-
rescue StandardError => e
|
|
195
|
-
raise StandardError, "Go scan failed: #{e.message}"
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# Goライブラリで高速スキャン
|
|
199
|
-
def scan_fast_with_go(path, max_entries)
|
|
200
|
-
raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
|
|
201
|
-
|
|
202
|
-
ptr = GoLib.ScanDirectoryFast(path, max_entries)
|
|
203
|
-
json_str = ptr.read_string
|
|
204
|
-
GoLib.FreeCString(ptr)
|
|
205
|
-
parse_scan_result(json_str)
|
|
206
|
-
rescue StandardError => e
|
|
207
|
-
raise StandardError, "Go fast scan failed: #{e.message}"
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
# Rubyでスキャン(フォールバック実装)
|
|
280
|
+
# Rubyでスキャン(ポーリング方式)
|
|
211
281
|
def scan_with_ruby(path)
|
|
212
282
|
raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
|
|
213
283
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
entries << {
|
|
222
|
-
name: entry,
|
|
223
|
-
type: file_type(stat),
|
|
224
|
-
size: stat.size,
|
|
225
|
-
mtime: stat.mtime.to_i,
|
|
226
|
-
mode: stat.mode
|
|
227
|
-
}
|
|
284
|
+
# 非同期スキャナーを作成してスキャン
|
|
285
|
+
scanner = NativeScannerRubyCore.new
|
|
286
|
+
begin
|
|
287
|
+
scanner.scan_async(path)
|
|
288
|
+
scanner.wait(timeout: 60)
|
|
289
|
+
ensure
|
|
290
|
+
scanner.close
|
|
228
291
|
end
|
|
229
|
-
entries
|
|
230
292
|
rescue StandardError => e
|
|
231
293
|
raise StandardError, "Ruby scan failed: #{e.message}"
|
|
232
294
|
end
|
|
233
295
|
|
|
234
|
-
# Ruby
|
|
296
|
+
# Ruby高速スキャン(エントリ数制限付き、ポーリング方式)
|
|
235
297
|
def scan_fast_with_ruby(path, max_entries)
|
|
236
298
|
raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
|
|
237
299
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
full_path = File.join(path, entry)
|
|
246
|
-
stat = File.lstat(full_path)
|
|
247
|
-
|
|
248
|
-
entries << {
|
|
249
|
-
name: entry,
|
|
250
|
-
type: file_type(stat),
|
|
251
|
-
size: stat.size,
|
|
252
|
-
mtime: stat.mtime.to_i,
|
|
253
|
-
mode: stat.mode
|
|
254
|
-
}
|
|
255
|
-
count += 1
|
|
300
|
+
scanner = NativeScannerRubyCore.new
|
|
301
|
+
begin
|
|
302
|
+
scanner.scan_fast_async(path, max_entries)
|
|
303
|
+
scanner.wait(timeout: 60)
|
|
304
|
+
ensure
|
|
305
|
+
scanner.close
|
|
256
306
|
end
|
|
257
|
-
entries
|
|
258
307
|
rescue StandardError => e
|
|
259
308
|
raise StandardError, "Ruby fast scan failed: #{e.message}"
|
|
260
309
|
end
|
|
@@ -271,36 +320,6 @@ module Rufio
|
|
|
271
320
|
'other'
|
|
272
321
|
end
|
|
273
322
|
end
|
|
274
|
-
|
|
275
|
-
# JSONレスポンスをパース
|
|
276
|
-
def parse_scan_result(json_str)
|
|
277
|
-
entries = JSON.parse(json_str, symbolize_names: true)
|
|
278
|
-
|
|
279
|
-
# エラーチェック(配列ではなくハッシュが返された場合)
|
|
280
|
-
if entries.is_a?(Hash) && entries[:error]
|
|
281
|
-
raise StandardError, entries[:error]
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
# 配列が返された場合は各エントリを変換
|
|
285
|
-
if entries.is_a?(Array)
|
|
286
|
-
return entries.map do |entry|
|
|
287
|
-
{
|
|
288
|
-
name: entry[:name],
|
|
289
|
-
type: entry[:is_dir] ? 'directory' : 'file',
|
|
290
|
-
size: entry[:size],
|
|
291
|
-
mtime: entry[:mtime],
|
|
292
|
-
mode: 0, # Rustライブラリはmodeを返さない
|
|
293
|
-
executable: entry[:executable],
|
|
294
|
-
hidden: entry[:hidden]
|
|
295
|
-
}
|
|
296
|
-
end
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
# それ以外の場合は空配列
|
|
300
|
-
[]
|
|
301
|
-
rescue JSON::ParserError => e
|
|
302
|
-
raise StandardError, "Failed to parse scan result: #{e.message}"
|
|
303
|
-
end
|
|
304
323
|
end
|
|
305
324
|
end
|
|
306
325
|
end
|