rufio 1.0.2 → 1.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6a5acef1da176c0bfb0c96f4dacc64e6ab519095b9e282a0c2558ca2594cd34
4
- data.tar.gz: 5be4e0fe22abcd6d866842ab61436605e940d38d5c3b43f477fc3afea0323413
3
+ metadata.gz: 247130c9fd76d05ed1ca70b859ea587e0b42f82ca7491fdc8b1ebea80d0257d4
4
+ data.tar.gz: 0122c547241cbd60c870798324b9cddaef6a3767a3153f52ca3b6b6cee3efa07
5
5
  SHA512:
6
- metadata.gz: 40f7a8c8dc30211dd09ec1c047c2ca52f4f9854020e41e6a56d56d531f84e87dbf920c7f01df213d9d67c4085edf634e7e6b2c924b0b3a8fcb2da3ab56765d54
7
- data.tar.gz: bc5b4458b71cdd85287f141ea8c8fff2cc66a6ba6a972cca558f1bf6bbd81c6a74a0e46fd1acaf413f2771e4170752fab4bb10838c45791d40c61e76c43c0f89
6
+ metadata.gz: 6ef019b2c59bc7bea03293c837ca7ff1f40cb4015e585056c9d12628dd552e33533a6ab3e6d012473110ebac0456978d1bb5c762b6192f13d7d8a269fd0826a9
7
+ data.tar.gz: 75138dbdeb740c137045ef17ba6ede44a8a4b2aa85450618e457b3e8d04ca0d5d6157622aadccb9abb934e4c60b4c5a830923406d9854d02128c5f65e4c19e53
data/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ All notable changes to rufio will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.3] - 2026-10-23
9
+
10
+ ### Security
11
+ - **Fix shell command injection via file names**: `FileOpener` interpolated file paths into shell strings and only quoted them when they contained spaces or quotes, so opening a file named like `a;touch pwned` or containing `$(...)` / backticks could execute arbitrary commands. Commands are now executed in array form without a shell (`system(*argv)`), paths starting with `-` are prefixed with `./`, and the Windows default-app launch uses `rundll32 url.dll,FileProtocolHandler` instead of `cmd /c start` (avoids `%VAR%` / `&` expansion). Removed `quote_shell_argument`
12
+ - **rga option injection**: A `--` separator is now passed before the search query so queries such as `--pre=cmd` are not interpreted as rga options
13
+ - **Command logs are now written with `0600` permissions**, since command output may contain secrets
14
+
15
+ ### Removed
16
+ - `TestUISnapshot` and its `test/snapshots/` fixtures (tests were failing independent of code changes)
17
+
18
+ ### Fixed
19
+ - **Windows: Sluggish key handling**: The main loop processed only one input byte per frame and then slept ~33ms, so key repeat could not keep up. Now up to 32 queued inputs are processed per frame with a single redraw, and the Windows frame interval is ~8ms
20
+ - **Windows: Input queue wait**: Uses `Queue#pop(timeout:)` on Ruby 3.2+ instead of 1ms `sleep` polling (inaccurate under the ~15ms Windows timer resolution)
21
+ - **Windows: Arrow keys misread as ESC**: The wait for escape sequence follow-up bytes was extended from 5ms to 25ms to tolerate ConPTY byte delays
22
+
8
23
  ## [1.0.0] - 2026-04-28
9
24
 
10
25
  ### Added
@@ -30,7 +30,8 @@ module Rufio
30
30
  # ディレクトリが存在しない場合は作成(バックグラウンドスレッドでの実行時の競合を防ぐ)
31
31
  FileUtils.mkdir_p(@log_dir) unless Dir.exist?(@log_dir)
32
32
 
33
- File.write(filepath, content)
33
+ # コマンド出力に機密情報が含まれ得るため、所有者のみ読み書き可能にする
34
+ File.write(filepath, content, perm: 0o600)
34
35
  end
35
36
 
36
37
  # ログファイル一覧を取得(新しい順)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'shellwords'
4
+
3
5
  module Rufio
4
6
  class FileOpener
5
7
  def initialize
@@ -43,26 +45,26 @@ module Rufio
43
45
  end
44
46
 
45
47
  def execute_command(application, file_path)
46
- quoted_path = quote_shell_argument(file_path)
48
+ path = safe_path(file_path)
47
49
 
48
50
  case RbConfig::CONFIG['host_os']
49
51
  when /mswin|mingw|cygwin/
50
- # Windows
51
- system("start \"\" \"#{file_path}\"")
52
+ # Windows: cmd.exe を経由せず(%VAR% や & の展開を避ける)既定アプリで開く
53
+ run_command(['rundll32', 'url.dll,FileProtocolHandler', path])
52
54
  when /darwin/
53
55
  # macOS
54
56
  if application == 'open'
55
- system("open #{quoted_path}")
57
+ run_command(['open', path])
56
58
  else
57
59
  # VSCodeなど特定のアプリケーション
58
- system("#{application} #{quoted_path}")
60
+ run_command([*application_argv(application), path])
59
61
  end
60
62
  else
61
63
  # Linux/Unix
62
64
  if application == 'open'
63
- system("xdg-open #{quoted_path}")
65
+ run_command(['xdg-open', path])
64
66
  else
65
- system("#{application} #{quoted_path}")
67
+ run_command([*application_argv(application), path])
66
68
  end
67
69
  end
68
70
  rescue StandardError => e
@@ -71,37 +73,39 @@ module Rufio
71
73
  end
72
74
 
73
75
  def execute_command_with_line(application, file_path, line_number)
74
- quoted_path = quote_shell_argument(file_path)
76
+ path = safe_path(file_path)
77
+ line = line_number.to_i
78
+ app = application_argv(application)
75
79
 
76
80
  case RbConfig::CONFIG['host_os']
77
81
  when /mswin|mingw|cygwin/
78
82
  # Windows
79
83
  if application.include?('code')
80
- system("#{application} --goto #{quoted_path}:#{line_number}")
84
+ run_command([*app, '--goto', "#{path}:#{line}"])
81
85
  else
82
- system("start \"\" \"#{file_path}\"")
86
+ run_command(['rundll32', 'url.dll,FileProtocolHandler', path])
83
87
  end
84
88
  when /darwin/
85
89
  # macOS
86
90
  if application == 'open'
87
- system("open #{quoted_path}")
91
+ run_command(['open', path])
88
92
  elsif application.include?('code')
89
- system("#{application} --goto #{quoted_path}:#{line_number}")
93
+ run_command([*app, '--goto', "#{path}:#{line}"])
90
94
  elsif application.include?('vim') || application.include?('nvim')
91
- system("#{application} +#{line_number} #{quoted_path}")
95
+ run_command([*app, "+#{line}", path])
92
96
  else
93
- system("#{application} #{quoted_path}")
97
+ run_command([*app, path])
94
98
  end
95
99
  else
96
100
  # Linux/Unix
97
101
  if application == 'open'
98
- system("xdg-open #{quoted_path}")
102
+ run_command(['xdg-open', path])
99
103
  elsif application.include?('code')
100
- system("#{application} --goto #{quoted_path}:#{line_number}")
104
+ run_command([*app, '--goto', "#{path}:#{line}"])
101
105
  elsif application.include?('vim') || application.include?('nvim')
102
- system("#{application} +#{line_number} #{quoted_path}")
106
+ run_command([*app, "+#{line}", path])
103
107
  else
104
- system("#{application} #{quoted_path}")
108
+ run_command([*app, path])
105
109
  end
106
110
  end
107
111
  rescue StandardError => e
@@ -109,27 +113,39 @@ module Rufio
109
113
  false
110
114
  end
111
115
 
112
- def quote_shell_argument(argument)
113
- if argument.include?(' ') || argument.include?("'") || argument.include?('"')
114
- '"' + argument.gsub('"', '\"') + '"'
116
+ # シェルを介さず配列形式で実行する(ファイル名経由のコマンドインジェクション防止)
117
+ def run_command(argv)
118
+ system(*argv)
119
+ end
120
+
121
+ # 設定のアプリケーション指定(例: "code -r")を引数配列に分解する
122
+ def application_argv(application)
123
+ if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
124
+ # Windows のパスはバックスラッシュを含むため POSIX 分割は使わない
125
+ File.exist?(application) ? [application] : application.split
115
126
  else
116
- argument
127
+ Shellwords.split(application)
117
128
  end
118
129
  end
119
130
 
131
+ # '-' で始まるパスがオプションとして解釈されないようにする
132
+ def safe_path(path)
133
+ path.start_with?('-') ? File.join('.', path) : path
134
+ end
135
+
120
136
  def execute_explorer_command(directory_path)
121
- quoted_path = quote_shell_argument(directory_path)
137
+ path = safe_path(directory_path)
122
138
 
123
139
  case RbConfig::CONFIG['host_os']
124
140
  when /mswin|mingw|cygwin/
125
141
  # Windows
126
- system("explorer #{quoted_path}")
142
+ run_command(['explorer', path])
127
143
  when /darwin/
128
144
  # macOS
129
- system("open #{quoted_path}")
145
+ run_command(['open', path])
130
146
  else
131
147
  # Linux/Unix
132
- system("xdg-open #{quoted_path}")
148
+ run_command(['xdg-open', path])
133
149
  end
134
150
  rescue StandardError => e
135
151
  warn "Failed to open directory: #{e.message}"
@@ -57,7 +57,8 @@ module Rufio
57
57
  search_results = nil
58
58
  Dir.chdir(current_path) do
59
59
  escaped_query = Shellwords.escape(search_query)
60
- search_results = `rga --line-number --with-filename #{escaped_query} . 2>/dev/null`
60
+ # "--" で検索語がオプション(--pre=コマンド 等)として解釈されるのを防ぐ
61
+ search_results = `rga --line-number --with-filename -- #{escaped_query} . 2>/dev/null`
61
62
  end
62
63
 
63
64
  if search_results.empty?
@@ -237,7 +237,8 @@ module Rufio
237
237
  # Windows: IO.select を使わずキューから取得(5ms タイムアウト)。
238
238
  def read_next_input_byte
239
239
  if windows?
240
- b = windows_queue_pop_with_timeout(0.005)
240
+ # ConPTY ではバイト到着が遅れることがあるため 5ms では ESC 単体と誤判定する
241
+ b = windows_queue_pop_with_timeout(0.025)
241
242
  b.nil? ? nil : b.chr(Encoding::BINARY)
242
243
  else
243
244
  STDIN.read_nonblock(1) rescue nil
@@ -280,7 +281,15 @@ module Rufio
280
281
  end
281
282
 
282
283
  # Windows 専用: @input_queue からタイムアウト付きで Integer バイトを取り出す。
284
+ # Ruby 3.2+ は Queue#pop(timeout:) で正確に待機する(sleep ポーリングは
285
+ # Windows のタイマー分解能 約15ms により遅延する)。それ以前はポーリング。
286
+ QUEUE_POP_TIMEOUT_SUPPORTED = Queue.instance_method(:pop).parameters.any? { |_, n| n == :timeout }
287
+
283
288
  def windows_queue_pop_with_timeout(timeout_sec)
289
+ if QUEUE_POP_TIMEOUT_SUPPORTED
290
+ return @input_queue.pop(timeout: timeout_sec)
291
+ end
292
+
284
293
  deadline = Time.now + timeout_sec
285
294
  loop do
286
295
  begin
@@ -344,10 +353,13 @@ module Rufio
344
353
  # ゲームループパターンのmain_loop(CPU最適化版:フレームスキップ対応)
345
354
  # UPDATE → DRAW → RENDER → SLEEP のサイクル
346
355
  # 変更がない場合は描画をスキップしてCPU使用率を削減
356
+ MAX_INPUTS_PER_FRAME = 32
357
+
347
358
  def main_loop
348
359
  # CPU最適化: 固定FPSをやめて、イベントドリブンに変更
349
360
  # 最小スリープ時間(入力チェック間隔)
350
- min_sleep_interval = 0.0333 # 30FPS(約33.33ms/フレーム)
361
+ # Windows は入力をキュー経由で受けるため、入力遅延を抑えるよう短めにする
362
+ min_sleep_interval = windows? ? 0.008 : 0.0333 # Windows: 約8ms / Unix: 30FPS
351
363
  check_interval = 0.1 # バックグラウンドタスクのチェック間隔
352
364
 
353
365
  # Phase 3: Screen/Rendererを初期化
@@ -402,7 +414,14 @@ module Rufio
402
414
 
403
415
  # UPDATE phase - ノンブロッキング入力処理
404
416
  # 入力があった場合は再描画が必要
405
- had_input = handle_input_nonblocking
417
+ # 1フレームで溜まった入力をまとめて処理し、再描画は1回だけにする
418
+ # (1フレーム1キーだとキーリピートに追いつけず操作がもたつく)
419
+ had_input = false
420
+ MAX_INPUTS_PER_FRAME.times do
421
+ break unless handle_input_nonblocking
422
+ had_input = true
423
+ break unless @running
424
+ end
406
425
  needs_redraw = true if had_input
407
426
 
408
427
  # バックグラウンドコマンドの完了チェック(0.1秒ごと)
data/lib/rufio/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rufio
4
- VERSION = '1.0.2'
4
+ VERSION = '1.0.3'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rufio
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - masisz
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-12 00:00:00.000000000 Z
11
+ date: 2026-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: io-console