expect-pty 0.7.5 → 0.7.6

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: 472fea69b87e58d9b08004f799da6d584508c87832227da3385301953a20070f
4
- data.tar.gz: fc6720abad1ee0daff9943e9c0f19dc144e949bb543a67b590a32500a1088688
3
+ metadata.gz: 1052a03b9bb9ac46051468e651c168d714f6be11aabd4bd8e2171dd390e2065a
4
+ data.tar.gz: 7c178220cc65517015144b2711679a3a26739efe1156dd144a26cb64fdfd6b46
5
5
  SHA512:
6
- metadata.gz: 8074ccf919b4f5a8d73d55a6acf8c40c3069a253cc5cd4e4acacec17f5277b157a62235987430519c2204d783b736d134b92963d29994aea56d639ceab1d3dd3
7
- data.tar.gz: 9b8879d3c0291ceeedb77624d352c8fa2f13a04412f777d45b6d4b0ed2dea3e32cb56cef041c65e297bc4687515a8772e29d42ca8a45993ae1ba21d7a7289b21
6
+ metadata.gz: 31b82ff772bc59ec35a4e3318be96b2b9c4b240898395c1bd8c543ed67515461d9ba282da415a1e208fbd9fb8f03390445cec132725873a039b3d75181fc552d
7
+ data.tar.gz: 1a7d7adfd64345c4e0d405657edd252e841430f65c335dc638708f43cdf20c4c12b4b8da28fe70b235801f8af50905595a6f982035a5ff73e9baafd02c8453d3
data/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.6 - 2026-10-09
6
+
7
+ - 修复会话关闭时日志异常覆盖原始错误的问题,保留进程等待、线程中断或句柄关闭的失败原因,并继续尝试完成诊断与 transcript 收尾。
8
+ - 优化长秘密的流尾脱敏和长字面转义扫描,减少近似前缀输入下的耗时与临时对象,保持分片、重叠和部分秘密的过滤行为。
9
+
5
10
  ## 0.7.5 - 2026-10-08
6
11
 
7
12
  - 修复 PTY、启动错误管道和工厂初始化的异步中断交接窗口;线程中断或终止后立即关闭已取得的句柄,保持用户初始化协议可中断。
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "relay_writer"
4
4
  require_relative "relay"
5
+ require_relative "literal_prefix"
5
6
 
6
7
  # 为会话补充人工接管和多路 IO 转接;核心会话定义位于 session.rb。
7
8
  module Expect
@@ -179,59 +180,12 @@ module Expect
179
180
  sequences.each_key do |key|
180
181
  next if key.is_a?(Regexp)
181
182
 
182
- prefix_length = literal_prefix_suffix(buffer, key, prefix_tables)
183
+ prefix_length = LiteralPrefix.length(buffer, key, prefix_tables)
183
184
  held = prefix_length if prefix_length > held
184
185
  end
185
186
  held
186
187
  end
187
188
 
188
- # KMP 在缓冲尾部寻找规则前缀,长规则只扫描相关字节一次,避免逐短前缀反复复制和比较。
189
- # 短规则使用原生字符串操作,避免为小输入建立前缀表。
190
- def self.literal_prefix_suffix(buffer, key, prefix_tables = {})
191
- maximum = [key.bytesize - 1, buffer.bytesize].min
192
- return 0 unless maximum.positive?
193
- return short_literal_prefix_suffix(buffer, key, maximum) if maximum < 64
194
-
195
- failure = key.frozen? ? (prefix_tables[key] ||= []) : []
196
- literal_prefix_failure_table(key, failure, maximum)
197
- matched = 0
198
- index = buffer.bytesize - maximum
199
- while index < buffer.bytesize
200
- byte = buffer.getbyte(index)
201
- matched = failure[matched - 1] while matched.positive? && byte != key.getbyte(matched)
202
- matched += 1 if byte == key.getbyte(matched)
203
- index += 1
204
- end
205
- matched
206
- end
207
-
208
- def self.literal_prefix_failure_table(key, failure, length)
209
- failure << 0 if failure.empty?
210
- matched = failure.last
211
- index = failure.length
212
- while index < length
213
- byte = key.getbyte(index)
214
- if byte == key.getbyte(matched)
215
- matched += 1
216
- failure << matched
217
- index += 1
218
- elsif matched.positive?
219
- matched = failure[matched - 1]
220
- else
221
- failure << 0
222
- index += 1
223
- end
224
- end
225
- failure
226
- end
227
-
228
- def self.short_literal_prefix_suffix(buffer, key, maximum)
229
- maximum.downto(1) do |length|
230
- return length if buffer.end_with?(key.byteslice(0, length))
231
- end
232
- 0
233
- end
234
-
235
189
  # 已转发历史有界保留,固定 UTF-8 正则不能从续字节开始匹配。
236
190
  def self.trim_history(history, data, limit:, utf8:)
237
191
  history << data
@@ -243,9 +197,7 @@ module Expect
243
197
  history.slice!(0) while (byte = history.getbyte(0)) && (0x80..0xBF).cover?(byte)
244
198
  end
245
199
 
246
- private_class_method :expired?, :handle_escape, :scan_sequences, :hold_literal_prefix,
247
- :literal_prefix_suffix, :literal_prefix_failure_table,
248
- :short_literal_prefix_suffix, :trim_history
200
+ private_class_method :expired?, :handle_escape, :scan_sequences, :hold_literal_prefix, :trim_history
249
201
 
250
202
  # 为当前数据块冻结目标选择并各建一个发送游标;此后修改 outputs 只影响后续数据。
251
203
  # 调用方须先排空旧游标;显示转换也只做一次,短写重试时不能重复转换 CRLF。
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expect
4
+ # 计算字节窗口尾部与规则真前缀重合的最长长度;转义暂存和流尾脱敏使用同一边界判断。
5
+ # 不持有输入或修改缓冲,调用方可为冻结规则提供前缀表缓存;短规则不建立表。
6
+ # @api private
7
+ module LiteralPrefix
8
+ def self.length(buffer, pattern, prefix_tables = nil)
9
+ maximum = [pattern.bytesize - 1, buffer.bytesize].min
10
+ return 0 unless maximum.positive?
11
+
12
+ # 尾字节不在候选前缀内时无需扫描;最长候选本身命中时也不必构造 KMP 表。
13
+ ending = pattern.rindex(buffer.byteslice(-1, 1), maximum - 1)
14
+ return 0 unless ending
15
+
16
+ maximum = ending + 1
17
+ return maximum if buffer.end_with?(pattern.byteslice(0, maximum))
18
+ return short_length(buffer, pattern, maximum - 1) if maximum < 64
19
+
20
+ failure = prefix_tables && pattern.frozen? ? (prefix_tables[pattern] ||= []) : []
21
+ extend_failure_table(pattern, failure, maximum)
22
+ matched = 0
23
+ index = buffer.bytesize - maximum
24
+ while index < buffer.bytesize
25
+ byte = buffer.getbyte(index)
26
+ matched = failure[matched - 1] while matched.positive? && byte != pattern.getbyte(matched)
27
+ matched += 1 if byte == pattern.getbyte(matched)
28
+ index += 1
29
+ end
30
+ matched
31
+ end
32
+
33
+ def self.extend_failure_table(pattern, failure, length)
34
+ failure << 0 if failure.empty?
35
+ matched = failure.last
36
+ index = failure.length
37
+ while index < length
38
+ byte = pattern.getbyte(index)
39
+ if byte == pattern.getbyte(matched)
40
+ matched += 1
41
+ failure << matched
42
+ index += 1
43
+ elsif matched.positive?
44
+ matched = failure[matched - 1]
45
+ else
46
+ failure << 0
47
+ index += 1
48
+ end
49
+ end
50
+ end
51
+
52
+ def self.short_length(buffer, pattern, maximum)
53
+ maximum.downto(1) do |length|
54
+ return length if buffer.end_with?(pattern.byteslice(0, length))
55
+ end
56
+ 0
57
+ end
58
+
59
+ private_class_method :extend_failure_table, :short_length
60
+ end
61
+
62
+ private_constant :LiteralPrefix
63
+ end
@@ -87,6 +87,12 @@ module Expect
87
87
 
88
88
  private
89
89
 
90
+ # 关闭时两种记录都要尝试完成;后续 writer 的常规异常不能覆盖先发生的诊断异常。
91
+ # 不关闭借用目标,新的 Interrupt/SystemExit 仍由 Cleanup 原样传播。
92
+ def finish_logging
93
+ Cleanup.always(-> { self.transcript = nil }) { flush_diagnostics }
94
+ end
95
+
90
96
  # 向目标写入并在支持时立即 flush,使日志和终端输出及时可见。
91
97
  # 这里遵循同步写入协议,不受 Matcher 的 IO 等待期限中断;慢目标转接应使用 Relay。
92
98
  # 短写只推进已确认的字节数;目标抛错时不猜测它是否已经产生副作用。
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "literal_prefix"
4
+
3
5
  module Expect
4
6
  # 单个日志字节流的过滤器。保留最长秘密长度减一的尾部,跨 write/read 分片仍可识别。
5
7
  # 掩码与原字节一起留存;重叠命中的区间取并集,已经输出的掩码不重复生成。
@@ -78,20 +80,13 @@ module Expect
78
80
 
79
81
  # 流关闭时无法再等待后续字节,默认隐藏与秘密开头一致的未完成尾部。
80
82
  def mark_partial_secrets
81
- tail = @pending.byteslice(-1, 1)
82
- return unless tail
83
+ return if @pending.empty?
83
84
 
84
85
  @patterns.each do |pattern|
85
- length = [pattern.bytesize - 1, @pending.bytesize].min
86
- # 前缀必须以当前尾字节结束;只收紧最高候选长度,密集候选仍沿用简单倒序扫描。
87
- next unless length.positive? && (offset = pattern.rindex(tail, length - 1))
88
-
89
- (offset + 1).downto(1) do |candidate|
90
- next unless @pending.end_with?(pattern.byteslice(0, candidate))
86
+ length = LiteralPrefix.length(@pending, pattern)
87
+ next unless length.positive?
91
88
 
92
- @hidden[-candidate, candidate] = "\1" * candidate
93
- break
94
- end
89
+ @hidden[-length, length] = "\1" * length
95
90
  end
96
91
  end
97
92
 
@@ -503,30 +503,26 @@ module Expect
503
503
  failure ||= error
504
504
  nil
505
505
  end
506
- # IO 关闭与进程退出独立记录:软关闭可能已经 closed?,但仍保留活跃 PID。
507
- cleanup.call { @resources.close_handles }
508
- @closed = true
509
- @interact_inputs&.delete_if do |_io, input|
510
- cleanup.call do
511
- input.close(graceful: false)
512
- true
506
+ # 日志收尾是本作用域的清理:不能覆盖进程等待/中断或已记录的句柄错误。
507
+ Cleanup.always(-> { finish_logging }) do
508
+ # IO 关闭与进程退出独立记录:软关闭可能已经 closed?,但仍保留活跃 PID。
509
+ cleanup.call { @resources.close_handles }
510
+ @closed = true
511
+ @interact_inputs&.delete_if do |_io, input|
512
+ cleanup.call do
513
+ input.close(graceful: false)
514
+ true
515
+ end
513
516
  end
517
+ @interact_output = nil
518
+ @pending_writes&.clear
519
+ @relay_history&.clear
520
+ @relay_callback = nil
521
+ status = close_child(timeout:, term_timeout:, force:)
522
+ raise failure if failure
523
+
524
+ status
514
525
  end
515
- @interact_output = nil
516
- @pending_writes&.clear
517
- @relay_history&.clear
518
- @relay_callback = nil
519
- status = close_child(timeout:, term_timeout:, force:)
520
- completed = true
521
- status
522
- ensure
523
- begin
524
- cleanup.call { flush_diagnostics }
525
- ensure
526
- cleanup.call { self.transcript = nil }
527
- end
528
- # 用本次流程的完成状态判断异常传播,不能误把调用者 rescue 中的异常当成当前错误。
529
- raise failure if failure && completed
530
526
  end
531
527
 
532
528
  # 句柄清理失败不改变进程策略;未回收 PID 保留给重复关闭或终结器继续处理。
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Expect
4
4
  # Gem 与库共用的版本号;独立文件使 gemspec 无需加载完整会话实现。
5
- VERSION = "0.7.5"
5
+ VERSION = "0.7.6"
6
6
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: expect-pty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.5
4
+ version: 0.7.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - expect-pty contributors
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-10-08 00:00:00.000000000 Z
10
+ date: 2026-10-09 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: io-console
@@ -67,6 +67,7 @@ files:
67
67
  - lib/expect.rb
68
68
  - lib/expect/cleanup.rb
69
69
  - lib/expect/interaction.rb
70
+ - lib/expect/literal_prefix.rb
70
71
  - lib/expect/logging.rb
71
72
  - lib/expect/matcher.rb
72
73
  - lib/expect/pattern.rb