rufio 1.1.0 → 1.3.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 +34 -0
- data/README.md +74 -3
- data/README_ja.md +71 -2
- data/Rakefile +14 -1
- data/bin/rufio +31 -2
- data/docs/fast_zip_design_ja.md +167 -0
- data/info/help.md +2 -0
- data/info/keybindings.md +3 -1
- data/info/welcome.md +1 -1
- data/lib/rufio/application.rb +12 -1
- data/lib/rufio/archive_controller.rb +211 -0
- data/lib/rufio/background_command_executor.rb +9 -0
- data/lib/rufio/background_status.rb +49 -0
- data/lib/rufio/bookmark_controller.rb +38 -121
- data/lib/rufio/bookmark_manager.rb +77 -331
- data/lib/rufio/command_completion.rb +6 -1
- data/lib/rufio/command_history.rb +5 -2
- data/lib/rufio/command_mode.rb +40 -134
- data/lib/rufio/command_mode_ui.rb +4 -42
- data/lib/rufio/config.rb +88 -136
- data/lib/rufio/config_loader.rb +3 -11
- data/lib/rufio/dialog_renderer.rb +64 -152
- data/lib/rufio/dialog_support.rb +69 -0
- data/lib/rufio/document_text_extractor.rb +167 -0
- data/lib/rufio/file_operation_controller.rb +49 -222
- data/lib/rufio/file_preview.rb +46 -2
- data/lib/rufio/global_search.rb +121 -9
- data/lib/rufio/help_menu.rb +8 -2
- data/lib/rufio/interpreter_resolver.rb +6 -11
- data/lib/rufio/job_manager.rb +2 -5
- data/lib/rufio/key_input.rb +77 -0
- data/lib/rufio/keybind_handler.rb +81 -115
- data/lib/rufio/local_script_scanner.rb +6 -18
- data/lib/rufio/native/aarch64-linux-gnu/librufio_zig.so +0 -0
- data/lib/rufio/native/aarch64-linux-musl/librufio_zig.so +0 -0
- data/lib/rufio/native/aarch64-macos/librufio_zig.dylib +0 -0
- data/lib/rufio/native/aarch64-windows/rufio_zig.dll +0 -0
- data/lib/rufio/native/x86_64-linux-gnu/librufio_zig.so +0 -0
- data/lib/rufio/native/x86_64-linux-musl/librufio_zig.so +0 -0
- data/lib/rufio/native/x86_64-macos/librufio_zig.dylib +0 -0
- data/lib/rufio/native/x86_64-windows/rufio_zig.dll +0 -0
- data/lib/rufio/native_content_search.rb +153 -0
- data/lib/rufio/native_scanner.rb +13 -68
- data/lib/rufio/native_scanner_zig.rb +106 -56
- data/lib/rufio/native_zip.rb +121 -0
- data/lib/rufio/navigation_controller.rb +18 -20
- data/lib/rufio/screen.rb +2 -12
- data/lib/rufio/script_executor.rb +6 -73
- data/lib/rufio/script_file.rb +44 -0
- data/lib/rufio/script_path_manager.rb +9 -43
- data/lib/rufio/script_runner.rb +13 -50
- data/lib/rufio/search_controller.rb +1 -1
- data/lib/rufio/shell_command_completion.rb +1 -5
- data/lib/rufio/shell_init.rb +90 -0
- data/lib/rufio/task_status.rb +5 -1
- data/lib/rufio/terminal_ui.rb +55 -294
- data/lib/rufio/text_utils.rb +26 -3
- data/lib/rufio/ui_renderer.rb +63 -40
- data/lib/rufio/version.rb +1 -1
- data/lib/rufio/zip_archive.rb +314 -0
- data/lib/rufio/zip_extractor.rb +346 -0
- data/lib/rufio/zip_writer.rb +268 -0
- data/lib/rufio/zoxide_integration.rb +5 -43
- data/lib/rufio.rb +5 -0
- data/lib_zig/rufio_native/Makefile +54 -17
- data/lib_zig/rufio_native/bench.sh +36 -0
- data/lib_zig/rufio_native/build.zig +32 -35
- data/lib_zig/rufio_native/src/cli.zig +99 -0
- data/lib_zig/rufio_native/src/main.zig +187 -83
- data/lib_zig/rufio_native/src/os.zig +889 -0
- data/lib_zig/rufio_native/src/search.zig +919 -0
- data/lib_zig/rufio_native/src/search_test.zig +88 -0
- data/lib_zig/rufio_native/src/zip.zig +482 -0
- metadata +125 -3
- data/lib/rufio/native/rufio_zig.bundle +0 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fiddle'
|
|
4
|
+
require_relative 'native_scanner'
|
|
5
|
+
require_relative 'native_scanner_zig'
|
|
6
|
+
|
|
7
|
+
module Rufio
|
|
8
|
+
# Zig 実装のコンテンツ検索(G mode 用)。
|
|
9
|
+
# Ruby 実装(GlobalSearch#scan_content)と同じ結果を、並列走査と SIMD 検索で返す。
|
|
10
|
+
class NativeContentSearch
|
|
11
|
+
HEADER_SIZE = 25
|
|
12
|
+
HEADER_FORMAT = 'CL<Q<L<L<L<'
|
|
13
|
+
STATE_SCANNING = 1
|
|
14
|
+
KINDS = { 1 => :match, 2 => :document, 3 => :error }.freeze
|
|
15
|
+
MAX_QUERY_BYTES = 1024
|
|
16
|
+
POLL_INTERVAL = 0.001
|
|
17
|
+
|
|
18
|
+
# 検索をまたいでファイル内容を保持するキャッシュ(mtime/inode/サイズで毎回検証する)
|
|
19
|
+
class Cache
|
|
20
|
+
DEFAULT_MAX_BYTES = 256 * 1024 * 1024
|
|
21
|
+
|
|
22
|
+
attr_reader :handle
|
|
23
|
+
|
|
24
|
+
def self.finalizer(handle)
|
|
25
|
+
proc { NativeScannerZigFFI.core_cache_destroy(handle) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(max_bytes: DEFAULT_MAX_BYTES)
|
|
29
|
+
@handle = NativeScannerZigFFI.core_cache_create(max_bytes)
|
|
30
|
+
raise StandardError, 'Failed to create search cache' if @handle.zero?
|
|
31
|
+
|
|
32
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(@handle))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def cached_bytes
|
|
36
|
+
@handle.zero? ? 0 : NativeScannerZigFFI.core_cache_bytes(@handle)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# このキャッシュを使う検索をすべて close してから呼ぶこと
|
|
40
|
+
def close
|
|
41
|
+
return if @handle.zero?
|
|
42
|
+
|
|
43
|
+
ObjectSpace.undefine_finalizer(self)
|
|
44
|
+
NativeScannerZigFFI.core_cache_destroy(@handle)
|
|
45
|
+
@handle = 0
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.available?
|
|
50
|
+
NativeScannerZigFFI.search_available?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Zig 側は ASCII のみ大文字小文字を畳み込むため、大文字小文字を持つ非 ASCII 文字は Ruby 実装に任せる
|
|
54
|
+
def self.supports_query?(query)
|
|
55
|
+
return false if query.empty? || query.bytesize > MAX_QUERY_BYTES
|
|
56
|
+
|
|
57
|
+
query.each_char.all? { |char| char.ascii_only? || (char.upcase == char && char.downcase == char) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# マッチ周辺のバイト列から Ruby 実装と同じスニペット(30 文字前から 200 文字)を作る
|
|
61
|
+
def self.snippet(window, offset)
|
|
62
|
+
line = window.dup.force_encoding(Encoding::UTF_8).scrub
|
|
63
|
+
match_index = window.byteslice(0, offset).force_encoding(Encoding::UTF_8).scrub.length
|
|
64
|
+
line[[match_index - 30, 0].max, 200].to_s.strip
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def initialize(roots:, query:, excluded_extensions: [], document_extensions: [], cache: nil, threads: 0)
|
|
68
|
+
raise StandardError, 'Zig content search is not available' unless self.class.available?
|
|
69
|
+
|
|
70
|
+
@cache = cache
|
|
71
|
+
@path_encoding = Encoding.find('filesystem')
|
|
72
|
+
roots_buf = pack(roots)
|
|
73
|
+
query_buf = query.b
|
|
74
|
+
excluded_buf = pack(excluded_extensions)
|
|
75
|
+
documents_buf = pack(document_extensions)
|
|
76
|
+
@handle = NativeScannerZigFFI.core_search_create(
|
|
77
|
+
roots_buf, roots_buf.bytesize, query_buf, query_buf.bytesize,
|
|
78
|
+
excluded_buf, excluded_buf.bytesize, documents_buf, documents_buf.bytesize,
|
|
79
|
+
threads, cache ? cache.handle : 0
|
|
80
|
+
)
|
|
81
|
+
raise StandardError, 'Failed to start native search' if @handle.zero?
|
|
82
|
+
|
|
83
|
+
@length = Fiddle::Pointer.malloc(8, Fiddle::RUBY_FREE)
|
|
84
|
+
@skipped = 0
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# [:match, root_index, path, line_number, snippet] / [:document, root_index, path] /
|
|
88
|
+
# [:error, root_index, message] / [:skipped, count] を検索終了まで順に返す
|
|
89
|
+
def each_event(&block)
|
|
90
|
+
return enum_for(:each_event) unless block_given?
|
|
91
|
+
|
|
92
|
+
each_batch { |batch| batch.each(&block) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# 1 回の取り出しごとにイベント配列を返す(UI へまとめて渡すため)
|
|
96
|
+
def each_batch
|
|
97
|
+
return enum_for(:each_batch) unless block_given?
|
|
98
|
+
|
|
99
|
+
loop do
|
|
100
|
+
state = NativeScannerZigFFI.core_search_state(@handle)
|
|
101
|
+
data = drain
|
|
102
|
+
batch = []
|
|
103
|
+
each_record(data) { |event| batch << event }
|
|
104
|
+
skipped = NativeScannerZigFFI.core_search_skipped(@handle)
|
|
105
|
+
if skipped > @skipped
|
|
106
|
+
batch << [:skipped, skipped - @skipped]
|
|
107
|
+
@skipped = skipped
|
|
108
|
+
end
|
|
109
|
+
yield batch unless batch.empty?
|
|
110
|
+
break if data.empty? && state != STATE_SCANNING
|
|
111
|
+
|
|
112
|
+
sleep POLL_INTERVAL if data.empty?
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def close
|
|
117
|
+
return if @handle.nil? || @handle.zero?
|
|
118
|
+
|
|
119
|
+
NativeScannerZigFFI.core_search_destroy(@handle)
|
|
120
|
+
@handle = 0
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def pack(values)
|
|
126
|
+
values.map { |value| value.to_s.b }.join("\0")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def drain
|
|
130
|
+
pointer = NativeScannerZigFFI.core_search_drain(@handle, @length)
|
|
131
|
+
length = @length[0, 8].unpack1('Q')
|
|
132
|
+
length.zero? ? ''.b : pointer.to_str(length)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def each_record(data)
|
|
136
|
+
position = 0
|
|
137
|
+
while position + HEADER_SIZE <= data.bytesize
|
|
138
|
+
kind, root, line, offset, path_length, extra_length = data.byteslice(position, HEADER_SIZE).unpack(HEADER_FORMAT)
|
|
139
|
+
position += HEADER_SIZE
|
|
140
|
+
path = data.byteslice(position, path_length).force_encoding(@path_encoding)
|
|
141
|
+
position += path_length
|
|
142
|
+
extra = data.byteslice(position, extra_length)
|
|
143
|
+
position += extra_length
|
|
144
|
+
|
|
145
|
+
case KINDS[kind]
|
|
146
|
+
when :match then yield [:match, root, path, line, self.class.snippet(extra, offset)]
|
|
147
|
+
when :document then yield [:document, root, path]
|
|
148
|
+
when :error then yield [:error, root, "#{path}: #{extra.force_encoding(Encoding::UTF_8).scrub}"]
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
data/lib/rufio/native_scanner.rb
CHANGED
|
@@ -17,69 +17,17 @@ module Rufio
|
|
|
17
17
|
|
|
18
18
|
# 非同期スキャン開始
|
|
19
19
|
def scan_async(path)
|
|
20
|
-
|
|
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
|
-
}
|
|
53
|
-
|
|
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
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
self
|
|
20
|
+
start_scan_thread(path, nil)
|
|
79
21
|
end
|
|
80
22
|
|
|
81
23
|
# 高速スキャン(エントリ数制限付き)
|
|
82
24
|
def scan_fast_async(path, max_entries)
|
|
25
|
+
start_scan_thread(path, max_entries)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def start_scan_thread(path, max_entries)
|
|
83
31
|
raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
|
|
84
32
|
raise StandardError, "Scanner is already running" unless @state == :idle
|
|
85
33
|
|
|
@@ -88,7 +36,7 @@ module Rufio
|
|
|
88
36
|
@results = []
|
|
89
37
|
@error = nil
|
|
90
38
|
@current_progress = 0
|
|
91
|
-
@total_progress = max_entries
|
|
39
|
+
@total_progress = max_entries || 0
|
|
92
40
|
end
|
|
93
41
|
|
|
94
42
|
@thread = Thread.new do
|
|
@@ -98,9 +46,7 @@ module Rufio
|
|
|
98
46
|
|
|
99
47
|
Dir.foreach(path) do |entry|
|
|
100
48
|
next if entry == '.' || entry == '..'
|
|
101
|
-
break if count >= max_entries
|
|
102
|
-
|
|
103
|
-
# キャンセルチェック
|
|
49
|
+
break if max_entries && count >= max_entries
|
|
104
50
|
break if @state == :cancelled
|
|
105
51
|
|
|
106
52
|
full_path = File.join(path, entry)
|
|
@@ -117,17 +63,14 @@ module Rufio
|
|
|
117
63
|
}
|
|
118
64
|
count += 1
|
|
119
65
|
|
|
120
|
-
# 進捗を更新
|
|
121
66
|
@mutex.synchronize do
|
|
122
67
|
@current_progress = count
|
|
68
|
+
@total_progress = entries.length + 1 unless max_entries
|
|
123
69
|
end
|
|
124
70
|
end
|
|
125
71
|
|
|
126
|
-
# 結果を保存
|
|
127
72
|
@mutex.synchronize do
|
|
128
|
-
if @state
|
|
129
|
-
@state = :cancelled
|
|
130
|
-
else
|
|
73
|
+
if @state != :cancelled
|
|
131
74
|
@results = entries
|
|
132
75
|
@state = :done
|
|
133
76
|
end
|
|
@@ -143,6 +86,8 @@ module Rufio
|
|
|
143
86
|
self
|
|
144
87
|
end
|
|
145
88
|
|
|
89
|
+
public
|
|
90
|
+
|
|
146
91
|
# ポーリングして完了待ち
|
|
147
92
|
def wait(timeout: nil)
|
|
148
93
|
start_time = Time.now
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'fiddle'
|
|
4
4
|
require 'fiddle/import'
|
|
5
|
+
require 'rbconfig'
|
|
5
6
|
|
|
6
7
|
module Rufio
|
|
7
8
|
# Zig拡張の非同期FFI層
|
|
@@ -9,21 +10,58 @@ module Rufio
|
|
|
9
10
|
module NativeScannerZigFFI
|
|
10
11
|
extend Fiddle::Importer
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
NATIVE_DIR = File.expand_path('native', __dir__)
|
|
14
|
+
|
|
15
|
+
# Gem に同梱するプラットフォーム(lib/rufio/native/<platform>/<file>)
|
|
16
|
+
PLATFORMS = {
|
|
17
|
+
'aarch64-macos' => 'librufio_zig.dylib',
|
|
18
|
+
'x86_64-macos' => 'librufio_zig.dylib',
|
|
19
|
+
'x86_64-linux-gnu' => 'librufio_zig.so',
|
|
20
|
+
'aarch64-linux-gnu' => 'librufio_zig.so',
|
|
21
|
+
'x86_64-linux-musl' => 'librufio_zig.so',
|
|
22
|
+
'aarch64-linux-musl' => 'librufio_zig.so',
|
|
23
|
+
'x86_64-windows' => 'rufio_zig.dll',
|
|
24
|
+
'aarch64-windows' => 'rufio_zig.dll'
|
|
25
|
+
}.freeze
|
|
13
26
|
|
|
14
27
|
@loaded = false
|
|
15
28
|
@available = false
|
|
29
|
+
@loaded_path = nil
|
|
16
30
|
|
|
17
31
|
class << self
|
|
32
|
+
attr_reader :loaded_path
|
|
33
|
+
|
|
34
|
+
# RbConfig の host_os / host_cpu から同梱バイナリのディレクトリ名を決める(非対応なら nil)
|
|
35
|
+
def platform_dir(host_os = RbConfig::CONFIG['host_os'], host_cpu = RbConfig::CONFIG['host_cpu'])
|
|
36
|
+
cpu = case host_cpu.to_s
|
|
37
|
+
when /\A(arm64|aarch64)/i then 'aarch64'
|
|
38
|
+
when /\A(x86_64|x64|amd64)\z/i then 'x86_64'
|
|
39
|
+
end
|
|
40
|
+
return nil unless cpu
|
|
41
|
+
|
|
42
|
+
case host_os.to_s
|
|
43
|
+
when /darwin/ then "#{cpu}-macos"
|
|
44
|
+
when /linux/ then "#{cpu}-linux-#{host_os.to_s.include?('musl') ? 'musl' : 'gnu'}"
|
|
45
|
+
when /mingw|mswin/ then "#{cpu}-windows"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def library_path(host_os = RbConfig::CONFIG['host_os'], host_cpu = RbConfig::CONFIG['host_cpu'])
|
|
50
|
+
dir = platform_dir(host_os, host_cpu)
|
|
51
|
+
dir && File.join(NATIVE_DIR, dir, PLATFORMS.fetch(dir))
|
|
52
|
+
end
|
|
53
|
+
|
|
18
54
|
def load!
|
|
19
55
|
return @available if @loaded
|
|
20
56
|
|
|
21
57
|
@loaded = true
|
|
58
|
+
path = library_path
|
|
22
59
|
|
|
23
|
-
if File.exist?(
|
|
60
|
+
if path && File.exist?(path)
|
|
24
61
|
begin
|
|
25
62
|
# 動的ライブラリをロード
|
|
26
|
-
dlload
|
|
63
|
+
dlload path
|
|
64
|
+
@loaded_path = path
|
|
27
65
|
|
|
28
66
|
# ABI Boundary: Ruby ABI非依存のC関数(非同期版)
|
|
29
67
|
extern 'uint64_t core_async_create()'
|
|
@@ -39,6 +77,8 @@ module Rufio
|
|
|
39
77
|
extern 'char* core_async_version()'
|
|
40
78
|
|
|
41
79
|
@available = true
|
|
80
|
+
load_search_functions!
|
|
81
|
+
load_zip_functions!
|
|
42
82
|
rescue StandardError => e
|
|
43
83
|
warn "Failed to load zig extension: #{e.message}" if ENV['RUFIO_DEBUG']
|
|
44
84
|
@available = false
|
|
@@ -55,10 +95,55 @@ module Rufio
|
|
|
55
95
|
@available
|
|
56
96
|
end
|
|
57
97
|
|
|
98
|
+
# コンテンツ検索 API(古いライブラリには存在しない)
|
|
99
|
+
def search_available?
|
|
100
|
+
load! unless @loaded
|
|
101
|
+
@search_available == true
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# ZIP データ処理 API(古いライブラリには存在しない)
|
|
105
|
+
def zip_available?
|
|
106
|
+
load! unless @loaded
|
|
107
|
+
@zip_available == true
|
|
108
|
+
end
|
|
109
|
+
|
|
58
110
|
# バージョン取得
|
|
59
111
|
def version
|
|
60
112
|
core_async_version.to_s
|
|
61
113
|
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def load_search_functions!
|
|
118
|
+
extern 'uint64_t core_search_create(const char*, size_t, const char*, size_t, const char*, size_t, const char*, size_t, uint32_t, uint64_t)'
|
|
119
|
+
extern 'uint8_t core_search_state(uint64_t)'
|
|
120
|
+
extern 'void* core_search_drain(uint64_t, void*)'
|
|
121
|
+
extern 'uint64_t core_search_skipped(uint64_t)'
|
|
122
|
+
extern 'void core_search_cancel(uint64_t)'
|
|
123
|
+
extern 'void core_search_destroy(uint64_t)'
|
|
124
|
+
extern 'uint64_t core_cache_create(uint64_t)'
|
|
125
|
+
extern 'uint64_t core_cache_bytes(uint64_t)'
|
|
126
|
+
extern 'void core_cache_destroy(uint64_t)'
|
|
127
|
+
@search_available = true
|
|
128
|
+
rescue Fiddle::DLError => e
|
|
129
|
+
warn "Zig content search unavailable: #{e.message}" if ENV['RUFIO_DEBUG']
|
|
130
|
+
@search_available = false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def load_zip_functions!
|
|
134
|
+
extern 'uint64_t core_zip_extract_create(const char*, size_t, const char*, size_t, uint32_t)'
|
|
135
|
+
extern 'uint64_t core_zip_deflate_create(const char*, size_t, int32_t, uint32_t)'
|
|
136
|
+
extern 'uint8_t core_zip_state(uint64_t)'
|
|
137
|
+
extern 'void core_zip_progress(uint64_t, void*, void*)'
|
|
138
|
+
extern 'uint8_t core_zip_status(uint64_t, size_t)'
|
|
139
|
+
extern 'uint8_t core_zip_deflate_result(uint64_t, size_t, void*, void*, void*)'
|
|
140
|
+
extern 'void core_zip_cancel(uint64_t)'
|
|
141
|
+
extern 'void core_zip_destroy(uint64_t)'
|
|
142
|
+
@zip_available = true
|
|
143
|
+
rescue Fiddle::DLError => e
|
|
144
|
+
warn "Zig ZIP engine unavailable: #{e.message}" if ENV['RUFIO_DEBUG']
|
|
145
|
+
@zip_available = false
|
|
146
|
+
end
|
|
62
147
|
end
|
|
63
148
|
end
|
|
64
149
|
|
|
@@ -200,12 +285,12 @@ module Rufio
|
|
|
200
285
|
if NativeScannerZigFFI.load!
|
|
201
286
|
class NativeScanner
|
|
202
287
|
class << self
|
|
203
|
-
# zig
|
|
288
|
+
# zigモードを追加(autoモードの優先順位: Zig > Ruby)
|
|
204
289
|
alias_method :original_mode=, :mode= unless method_defined?(:original_mode=)
|
|
205
290
|
|
|
206
291
|
def mode=(value)
|
|
207
292
|
case value
|
|
208
|
-
when 'zig'
|
|
293
|
+
when 'auto', 'zig'
|
|
209
294
|
if NativeScannerZigFFI.available?
|
|
210
295
|
@mode = 'zig'
|
|
211
296
|
@current_library = nil
|
|
@@ -214,7 +299,7 @@ module Rufio
|
|
|
214
299
|
@current_library = nil
|
|
215
300
|
end
|
|
216
301
|
else
|
|
217
|
-
original_mode
|
|
302
|
+
send(:original_mode=, value)
|
|
218
303
|
end
|
|
219
304
|
end
|
|
220
305
|
|
|
@@ -222,24 +307,10 @@ module Rufio
|
|
|
222
307
|
def scan_directory_with_zig(path)
|
|
223
308
|
raise StandardError, "Directory does not exist: #{path}" unless Dir.exist?(path)
|
|
224
309
|
|
|
225
|
-
# 非同期スキャナーを作成してスキャン
|
|
226
310
|
scanner = NativeScannerZigCore.new
|
|
227
311
|
begin
|
|
228
312
|
scanner.scan_async(path)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
# 結果の形式を統一(type フィールドを追加)
|
|
232
|
-
entries.map do |entry|
|
|
233
|
-
{
|
|
234
|
-
name: entry[:name],
|
|
235
|
-
type: entry[:is_dir] ? 'directory' : 'file',
|
|
236
|
-
size: entry[:size],
|
|
237
|
-
mtime: entry[:mtime],
|
|
238
|
-
mode: 0,
|
|
239
|
-
executable: entry[:executable],
|
|
240
|
-
hidden: entry[:hidden]
|
|
241
|
-
}
|
|
242
|
-
end
|
|
313
|
+
normalize_zig_entries(scanner.wait(timeout: 60))
|
|
243
314
|
ensure
|
|
244
315
|
scanner.close
|
|
245
316
|
end
|
|
@@ -254,19 +325,7 @@ module Rufio
|
|
|
254
325
|
scanner = NativeScannerZigCore.new
|
|
255
326
|
begin
|
|
256
327
|
scanner.scan_fast_async(path, max_entries)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
entries.map do |entry|
|
|
260
|
-
{
|
|
261
|
-
name: entry[:name],
|
|
262
|
-
type: entry[:is_dir] ? 'directory' : 'file',
|
|
263
|
-
size: entry[:size],
|
|
264
|
-
mtime: entry[:mtime],
|
|
265
|
-
mode: 0,
|
|
266
|
-
executable: entry[:executable],
|
|
267
|
-
hidden: entry[:hidden]
|
|
268
|
-
}
|
|
269
|
-
end
|
|
328
|
+
normalize_zig_entries(scanner.wait(timeout: 60))
|
|
270
329
|
ensure
|
|
271
330
|
scanner.close
|
|
272
331
|
end
|
|
@@ -324,28 +383,19 @@ module Rufio
|
|
|
324
383
|
original.merge(zig: NativeScannerZigFFI.available?)
|
|
325
384
|
end
|
|
326
385
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if NativeScannerZigFFI.available?
|
|
341
|
-
@mode = 'zig'
|
|
342
|
-
@current_library = nil
|
|
343
|
-
else
|
|
344
|
-
@mode = 'ruby'
|
|
345
|
-
@current_library = nil
|
|
346
|
-
end
|
|
347
|
-
else
|
|
348
|
-
send(:original_mode=, value)
|
|
386
|
+
private
|
|
387
|
+
|
|
388
|
+
def normalize_zig_entries(entries)
|
|
389
|
+
entries.map do |entry|
|
|
390
|
+
{
|
|
391
|
+
name: entry[:name],
|
|
392
|
+
type: entry[:is_dir] ? 'directory' : 'file',
|
|
393
|
+
size: entry[:size],
|
|
394
|
+
mtime: entry[:mtime],
|
|
395
|
+
mode: 0,
|
|
396
|
+
executable: entry[:executable],
|
|
397
|
+
hidden: entry[:hidden]
|
|
398
|
+
}
|
|
349
399
|
end
|
|
350
400
|
end
|
|
351
401
|
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fiddle'
|
|
4
|
+
require_relative 'native_scanner'
|
|
5
|
+
require_relative 'native_scanner_zig'
|
|
6
|
+
|
|
7
|
+
module Rufio
|
|
8
|
+
# Zig 実装の ZIP データ処理(並列・GVL 外)。
|
|
9
|
+
#
|
|
10
|
+
# 安全性の検証・パスの決定・ディレクトリ作成・rename・権限と日時の設定は Ruby 側
|
|
11
|
+
# (ZipExtractor / ZipWriter)が担い、ここではエントリ単位のデータ処理だけを行う。
|
|
12
|
+
# - 展開: ZIP 内の指定範囲を読み、伸長し、宣言サイズと CRC-32 を検証して一時ファイルに書く
|
|
13
|
+
# - 圧縮: ファイルを raw DEFLATE で一時ファイルに書き、CRC-32 とサイズを返す
|
|
14
|
+
module NativeZip
|
|
15
|
+
STATE_RUNNING = 1
|
|
16
|
+
STATE_DONE = 2
|
|
17
|
+
STATE_CANCELLED = 3
|
|
18
|
+
POLL_INTERVAL = 0.005
|
|
19
|
+
|
|
20
|
+
# タスクごとの結果コード(Zig 側 zip.zig の Status と一致させる)
|
|
21
|
+
STATUS_OK = 0
|
|
22
|
+
STATUS_CRC_MISMATCH = 1
|
|
23
|
+
STATUS_SIZE_MISMATCH = 2
|
|
24
|
+
STATUS_CORRUPT = 3
|
|
25
|
+
STATUS_IO_ERROR = 4
|
|
26
|
+
STATUS_CANCELLED = 5
|
|
27
|
+
STATUS_TOO_LARGE = 6
|
|
28
|
+
STATUS_PENDING = 255
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
def available?
|
|
32
|
+
return false if ENV['RUFIO_NATIVE_ZIP'] == '0'
|
|
33
|
+
|
|
34
|
+
NativeScannerZigFFI.zip_available?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# 実行中のジョブ(展開・圧縮共通)
|
|
39
|
+
class Job
|
|
40
|
+
def initialize(handle, count)
|
|
41
|
+
raise StandardError, 'Failed to start native ZIP job' if handle.zero?
|
|
42
|
+
|
|
43
|
+
@handle = handle
|
|
44
|
+
@count = count
|
|
45
|
+
@done = Fiddle::Pointer.malloc(8, Fiddle::RUBY_FREE)
|
|
46
|
+
@total = Fiddle::Pointer.malloc(8, Fiddle::RUBY_FREE)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# 完了まで待つ。progress には (処理済みバイト数, 合計バイト数) を渡す。
|
|
50
|
+
# cancelled が true を返したらキャンセルを要求し、終了を待ってから戻る
|
|
51
|
+
def wait(progress: nil, cancelled: nil)
|
|
52
|
+
loop do
|
|
53
|
+
state = NativeScannerZigFFI.core_zip_state(@handle)
|
|
54
|
+
report(progress)
|
|
55
|
+
return state unless state == STATE_RUNNING
|
|
56
|
+
|
|
57
|
+
NativeScannerZigFFI.core_zip_cancel(@handle) if cancelled&.call
|
|
58
|
+
sleep POLL_INTERVAL
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def status(index)
|
|
63
|
+
NativeScannerZigFFI.core_zip_status(@handle, index)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def close
|
|
67
|
+
return if @handle.zero?
|
|
68
|
+
|
|
69
|
+
NativeScannerZigFFI.core_zip_destroy(@handle)
|
|
70
|
+
@handle = 0
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
protected
|
|
74
|
+
|
|
75
|
+
attr_reader :handle
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def report(progress)
|
|
80
|
+
return unless progress
|
|
81
|
+
|
|
82
|
+
NativeScannerZigFFI.core_zip_progress(@handle, @done, @total)
|
|
83
|
+
progress.call(@done[0, 8].unpack1('Q<'), @total[0, 8].unpack1('Q<'))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @param zip_path [String]
|
|
88
|
+
# @param tasks [Array<Hash>] data_offset:, compressed_size:, size:, crc32:, method:, output: (一時ファイルのパス)
|
|
89
|
+
def self.extract(zip_path, tasks, threads: 0)
|
|
90
|
+
buffer = tasks.map do |task|
|
|
91
|
+
output = task[:output].b
|
|
92
|
+
[task[:data_offset], task[:compressed_size], task[:size], task[:crc32], task[:method], output.bytesize]
|
|
93
|
+
.pack('Q<Q<Q<L<CL<') + output
|
|
94
|
+
end.join
|
|
95
|
+
path = zip_path.b
|
|
96
|
+
Job.new(NativeScannerZigFFI.core_zip_extract_create(path, path.bytesize, buffer, buffer.bytesize, threads), tasks.length)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# 圧縮ジョブ
|
|
100
|
+
class DeflateJob < Job
|
|
101
|
+
# @return [Array(Integer, Integer, Integer, Integer)] status, crc32, 圧縮後サイズ, 元サイズ
|
|
102
|
+
def result(index)
|
|
103
|
+
crc = Fiddle::Pointer.malloc(4, Fiddle::RUBY_FREE)
|
|
104
|
+
compressed = Fiddle::Pointer.malloc(8, Fiddle::RUBY_FREE)
|
|
105
|
+
size = Fiddle::Pointer.malloc(8, Fiddle::RUBY_FREE)
|
|
106
|
+
status = NativeScannerZigFFI.core_zip_deflate_result(handle, index, crc, compressed, size)
|
|
107
|
+
[status, crc[0, 4].unpack1('L<'), compressed[0, 8].unpack1('Q<'), size[0, 8].unpack1('Q<')]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @param tasks [Array<Hash>] source:, output: (一時ファイルのパス)
|
|
112
|
+
def self.deflate(tasks, level:, threads: 0)
|
|
113
|
+
buffer = tasks.map do |task|
|
|
114
|
+
source = task[:source].b
|
|
115
|
+
output = task[:output].b
|
|
116
|
+
[source.bytesize].pack('L<') + source + [output.bytesize].pack('L<') + output
|
|
117
|
+
end.join
|
|
118
|
+
DeflateJob.new(NativeScannerZigFFI.core_zip_deflate_create(buffer, buffer.bytesize, level, threads), tasks.length)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -67,31 +67,18 @@ module Rufio
|
|
|
67
67
|
# @param in_help_mode [Boolean] ヘルプモード中かどうか
|
|
68
68
|
# @param in_log_viewer_mode [Boolean] ログビューワモード中かどうか
|
|
69
69
|
def navigate_enter(entry, in_help_mode, in_log_viewer_mode)
|
|
70
|
-
return false unless entry
|
|
71
|
-
|
|
72
|
-
if entry[:type] == 'directory'
|
|
73
|
-
return false if in_help_mode || in_log_viewer_mode
|
|
70
|
+
return false unless entry && entry[:type] == 'directory'
|
|
71
|
+
return false if in_help_mode || in_log_viewer_mode
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
if result
|
|
77
|
-
@current_index = 0
|
|
78
|
-
clear_filter_mode
|
|
79
|
-
end
|
|
80
|
-
result
|
|
81
|
-
else
|
|
82
|
-
false
|
|
83
|
-
end
|
|
73
|
+
perform_navigation { @directory_listing.navigate_to(entry[:name]) }
|
|
84
74
|
end
|
|
85
75
|
|
|
86
76
|
def navigate_parent
|
|
87
|
-
|
|
77
|
+
perform_navigation { @directory_listing.navigate_to_parent }
|
|
78
|
+
end
|
|
88
79
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
@current_index = 0
|
|
92
|
-
clear_filter_mode
|
|
93
|
-
end
|
|
94
|
-
result
|
|
80
|
+
def navigate_to_directory(path)
|
|
81
|
+
perform_navigation { @directory_listing.navigate_to_path(path) }
|
|
95
82
|
end
|
|
96
83
|
|
|
97
84
|
def refresh
|
|
@@ -262,5 +249,16 @@ module Rufio
|
|
|
262
249
|
navigate_enter(entry, in_help_mode, in_log_viewer_mode)
|
|
263
250
|
end
|
|
264
251
|
end
|
|
252
|
+
|
|
253
|
+
private
|
|
254
|
+
|
|
255
|
+
# 移動に成功したときだけ、移動後の選択・フィルタ状態を更新する。
|
|
256
|
+
def perform_navigation
|
|
257
|
+
return false unless @directory_listing
|
|
258
|
+
|
|
259
|
+
result = yield
|
|
260
|
+
clear_filter_mode if result
|
|
261
|
+
result
|
|
262
|
+
end
|
|
265
263
|
end
|
|
266
264
|
end
|