expect-pty 0.9.3 → 0.9.5
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 +12 -0
- data/docs/API.md +1 -1
- data/lib/expect/interaction.rb +12 -5
- data/lib/expect/literal_scan.rb +48 -0
- data/lib/expect/matcher.rb +6 -21
- data/lib/expect/redactor.rb +7 -0
- data/lib/expect/relay.rb +14 -5
- data/lib/expect/session.rb +5 -1
- data/lib/expect/version.rb +1 -1
- data/sig/expect.rbs +2 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fc988a8e70b1d6d216d390fc7e951862a19c24a70160d0d7de942b1acc3b96f1
|
|
4
|
+
data.tar.gz: aee179d58cb4ee6905f9b5269967e0585db71e9e864d76ebed7ebfa93c5c1556
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 529cafb6a8548bb650689343fd4bf4f6b5e4d44361a6093f5c0df769eee49f6bf5ccbc1c2873ab57694fc8ee8bbaf71b4c0ec786f20c0ea0f286d3b42ffd2c3f
|
|
7
|
+
data.tar.gz: fe3036a9b6b015900ab6cf73f7029498a02140596b1dfee6e2def446405938de85210bb84f2c7e3193f5e32649ff465e1ddf4f14f0b6e0d04ad6e3744545754c
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.9.5 - 2026-10-21
|
|
6
|
+
|
|
7
|
+
- 无脱敏规则且旧尾部排空时直接交付独立字节副本,避免为普通日志分配等长掩码;保留规则切换、抑制输出与相邻隐藏区间的语义。
|
|
8
|
+
|
|
9
|
+
## 0.9.4 - 2026-10-19
|
|
10
|
+
|
|
11
|
+
- 将字面未命中进度归一为内部 LiteralScan,供匹配与转接复用;大缓冲分块转接不再反复搜索已排除的范围,保留跨块转义与回调修改规则的行为。
|
|
12
|
+
- 转接活跃来源改用按对象身份维护的有序索引,避免批量 EOF 继续处理时反复遍历剩余来源。
|
|
13
|
+
- 修复句柄关闭和进程回收同时失败时后一个系统错误覆盖首个错误的问题;后续致命中断仍原样传播,失败资源可重试清理。
|
|
14
|
+
|
|
15
|
+
- 新增公开的 `Session#buffer_bytesize`,以常数时间查询当前主缓冲字节数,避免容量检查创建正文快照;原 `buffer` 的防御性复制契约不变。
|
|
16
|
+
|
|
5
17
|
## 0.9.3 - 2026-10-18
|
|
6
18
|
|
|
7
19
|
- 结果的 inspect 与 to_s 仅输出匹配序号和错误状态,避免诊断、字符串插值泄露匹配文本、捕获内容或异常消息;原始结果字段保持不变。
|
data/docs/API.md
CHANGED
|
@@ -134,7 +134,7 @@ end
|
|
|
134
134
|
| outputs | [] | 复制可写目标数组,原样转发接收字节 |
|
|
135
135
|
|
|
136
136
|
操作参数 `raw`、`consume`、`reset_timeout_on_read`、`graceful` 不持久化为会话设置。
|
|
137
|
-
`buffer`
|
|
137
|
+
`buffer` 返回副本;只检查容量时使用 `buffer_bytesize`,以常数时间取得当前未消费主缓冲的字节数,不复制正文、不读取 IO,也不包含 Relay 待写数据或历史窗口。`buffer=` 复制并应用上限,`clear_buffer` 移交并清空缓冲;`buffer_discarded_bytes` 累计窗口裁剪量,匹配消费不计入。
|
|
138
138
|
显式替换/清空、实际裁剪和消费输入会清除正则转义历史;非消费匹配、未裁剪的等待与内部缓冲交接保持连续历史。
|
|
139
139
|
正则转接历史默认保留至多 65,536 字节,设置 `buffer_limit` 后采用该上限。存在固定 UTF-8 正则时,历史预算至少为 3 字节,以保留四字节字符尚未接收完整的前缀;这不扩大普通匹配缓冲的上限,也不延迟协议转发。
|
|
140
140
|
|
data/lib/expect/interaction.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "relay/writer"
|
|
4
4
|
require_relative "relay"
|
|
5
5
|
require_relative "literal_prefix"
|
|
6
|
+
require_relative "literal_scan"
|
|
6
7
|
|
|
7
8
|
# 为会话补充人工接管和多路 IO 转接;核心会话定义位于 session.rb。
|
|
8
9
|
module Expect
|
|
@@ -81,14 +82,17 @@ module Expect
|
|
|
81
82
|
# 将待处理输入排入唯一的转接发送队列;返回 :ready、:queued、:stopped 或 :timeout。
|
|
82
83
|
# 字面序列暂存潜在前缀,正则序列结合历史匹配;final 为真时不再等待后续字节。
|
|
83
84
|
# @api private
|
|
84
|
-
def self.queue_input(session, buffer, final: false, deadline: nil)
|
|
85
|
+
def self.queue_input(session, buffer, final: false, deadline: nil, literal_scan: nil)
|
|
85
86
|
loop do
|
|
86
87
|
sequences = session.sequences.except(:eof)
|
|
87
88
|
history = session.relay_history
|
|
88
|
-
found, regexp_scanned, utf8_regexp = scan_sequences(
|
|
89
|
+
found, regexp_scanned, utf8_regexp = scan_sequences(
|
|
90
|
+
buffer, sequences, history, final:, literal_scan:, generation: session.buffer_generation
|
|
91
|
+
)
|
|
89
92
|
return :timeout if expired?(deadline)
|
|
90
93
|
|
|
91
94
|
if found
|
|
95
|
+
literal_scan&.reset
|
|
92
96
|
result = handle_escape(session, buffer, history, found)
|
|
93
97
|
return result unless result == :ready
|
|
94
98
|
return :timeout if expired?(deadline)
|
|
@@ -108,7 +112,10 @@ module Expect
|
|
|
108
112
|
end
|
|
109
113
|
# 排队前缀与尾部可共享原字节;替换窗口而非删除前缀,避免逐块复制整个余量。
|
|
110
114
|
# 保留缓冲对象身份,嵌套 Matcher 仍把尾部交回 Relay 借用的同一个对象。
|
|
111
|
-
|
|
115
|
+
if count.positive?
|
|
116
|
+
buffer.replace(buffer.byteslice(count..))
|
|
117
|
+
literal_scan&.advance(count)
|
|
118
|
+
end
|
|
112
119
|
return count.positive? ? :queued : :ready
|
|
113
120
|
end
|
|
114
121
|
end
|
|
@@ -134,7 +141,7 @@ module Expect
|
|
|
134
141
|
end
|
|
135
142
|
|
|
136
143
|
# 单轮正则共用一个组合文本;回调返回后下一轮重新取得规则和历史。
|
|
137
|
-
def self.scan_sequences(buffer, sequences, history, final:)
|
|
144
|
+
def self.scan_sequences(buffer, sequences, history, final:, literal_scan: nil, generation: nil)
|
|
138
145
|
text = nil
|
|
139
146
|
utf8_regexp = false
|
|
140
147
|
earliest = nil
|
|
@@ -153,7 +160,7 @@ module Expect
|
|
|
153
160
|
# 历史前缀已实时转发,不能撤回;负偏移表示本次只需消费转义尚未转发的部分。
|
|
154
161
|
offset -= history.bytesize
|
|
155
162
|
else
|
|
156
|
-
offset = buffer.index(key)
|
|
163
|
+
offset = literal_scan ? literal_scan.index(buffer, key, generation:) : buffer.index(key)
|
|
157
164
|
next unless offset
|
|
158
165
|
|
|
159
166
|
length = key.bytesize
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expect
|
|
4
|
+
# 一个连续字节窗口的字面未命中进度;不持有输入、会话或 IO。
|
|
5
|
+
# Matcher 的窗口只追加,Relay 还会移出安全前缀,两者共用绝对字节坐标。
|
|
6
|
+
# @api private
|
|
7
|
+
class LiteralScan
|
|
8
|
+
def initialize
|
|
9
|
+
@misses = {}.compare_by_identity
|
|
10
|
+
@consumed = 0
|
|
11
|
+
@generation = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# 返回相对当前窗口的字节位置;新输入只重扫可能跨越旧尾部的重叠区。
|
|
15
|
+
# literal 须为冻结的二进制 String,generation 变化表示窗口被替换、裁剪或外部消费。
|
|
16
|
+
def index(buffer, literal, generation:)
|
|
17
|
+
if @generation != generation
|
|
18
|
+
reset
|
|
19
|
+
@generation = generation
|
|
20
|
+
end
|
|
21
|
+
ending = @consumed + buffer.bytesize
|
|
22
|
+
previous = @misses[literal]
|
|
23
|
+
return nil if previous == ending
|
|
24
|
+
|
|
25
|
+
offset = previous ? [previous - @consumed - literal.bytesize + 1, 0].max : 0
|
|
26
|
+
position = buffer.index(literal, offset)
|
|
27
|
+
if position
|
|
28
|
+
@misses.delete(literal)
|
|
29
|
+
else
|
|
30
|
+
@misses[literal] = ending
|
|
31
|
+
end
|
|
32
|
+
position
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Relay 仅移出已扫描的安全前缀;未命中范围仍有效,无须逐规则调整偏移。
|
|
36
|
+
def advance(count)
|
|
37
|
+
@consumed += count
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# 转义消费或外部修改建立新边界,后续扫描不能复用旧输入的结论。
|
|
41
|
+
def reset
|
|
42
|
+
@misses.clear
|
|
43
|
+
@consumed = 0
|
|
44
|
+
@generation = nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
private_constant :LiteralScan
|
|
48
|
+
end
|
data/lib/expect/matcher.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "literal_scan"
|
|
4
|
+
|
|
3
5
|
module Expect
|
|
4
6
|
# 驱动一次单会话或多会话匹配,管理模式优先级、EOF 和共享期限,不接管 IO 所有权。
|
|
5
7
|
# 调度只在扫描、回调和 IO 操作之间检查期限,不强行中断用户代码或单次正则计算。
|
|
@@ -124,27 +126,10 @@ module Expect
|
|
|
124
126
|
# 仅复用同一缓冲代次中的字面未命中。保留模式长度减一的重叠区,覆盖跨读取命中。
|
|
125
127
|
# 不保存整个文本,也不对正则推断扫描窗口;回调替换、消费或转接恢复会改变代次。
|
|
126
128
|
def locate_literal(session, pattern, buffer)
|
|
127
|
-
@
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
# 模式已冻结;缓冲代次未变意味着只有尾部追加。
|
|
132
|
-
offset = if previous && previous[0] == generation
|
|
133
|
-
return nil if previous[1] == buffer.bytesize
|
|
134
|
-
|
|
135
|
-
[previous[1] - pattern.value.bytesize + 1, 0].max
|
|
136
|
-
else
|
|
137
|
-
0
|
|
138
|
-
end
|
|
139
|
-
position = pattern.locate(buffer, offset:)
|
|
140
|
-
if position
|
|
141
|
-
misses.delete(pattern)
|
|
142
|
-
else
|
|
143
|
-
entry = (misses[pattern] ||= [])
|
|
144
|
-
entry[0] = generation
|
|
145
|
-
entry[1] = buffer.bytesize
|
|
146
|
-
end
|
|
147
|
-
position
|
|
129
|
+
@literal_scans ||= {}.compare_by_identity
|
|
130
|
+
scan = (@literal_scans[session] ||= LiteralScan.new)
|
|
131
|
+
offset = scan.index(buffer, pattern.value, generation: session.buffer_generation)
|
|
132
|
+
[offset, pattern.value.bytesize, []] if offset
|
|
148
133
|
end
|
|
149
134
|
|
|
150
135
|
# 先记录并消费匹配,再执行回调;回调可选择结束、重置期限或保留期限继续。
|
data/lib/expect/redactor.rb
CHANGED
|
@@ -74,6 +74,13 @@ module Expect
|
|
|
74
74
|
def append_chunk(data, visible:)
|
|
75
75
|
raise ArgumentError, "data must be a String" unless data.is_a?(String)
|
|
76
76
|
|
|
77
|
+
# 没有规则且旧尾部已排空时,原样交付独立字节副本,不构造等长掩码。
|
|
78
|
+
# 非空明文会分隔隐藏区间;空分片不能改变以后重新启用规则时的边界。
|
|
79
|
+
if @patterns.empty? && @pending.empty?
|
|
80
|
+
@masking = false unless data.empty?
|
|
81
|
+
return visible ? data.b : "".b
|
|
82
|
+
end
|
|
83
|
+
|
|
77
84
|
unless visible || data.empty?
|
|
78
85
|
@suppressed ||= "\0" * @pending.bytesize
|
|
79
86
|
@suppressed_until = @pending.bytesize + data.bytesize
|
data/lib/expect/relay.rb
CHANGED
|
@@ -12,7 +12,8 @@ module Expect
|
|
|
12
12
|
# 这里只校验参数;run 取得所有来源后才转移缓冲,不关闭或接管任何外部 IO。
|
|
13
13
|
def initialize(sessions, timeout)
|
|
14
14
|
@sessions = sessions.uniq(&:object_id)
|
|
15
|
-
|
|
15
|
+
# 按对象身份保留声明顺序;每个 EOF 仅删除自身,避免逐个重扫剩余来源。
|
|
16
|
+
@active = @sessions.each_with_object({}.compare_by_identity) { |session, active| active[session] = true }
|
|
16
17
|
period = Expect.duration(timeout)
|
|
17
18
|
@deadline = period && (Expect.monotonic + period)
|
|
18
19
|
# 零超时保留首轮缓冲及一次轮询;正预算在扫描和每次回调之间重新检查。
|
|
@@ -20,6 +21,7 @@ module Expect
|
|
|
20
21
|
@token = Object.new
|
|
21
22
|
@buffers = {}.compare_by_identity
|
|
22
23
|
@previous = {}.compare_by_identity
|
|
24
|
+
@literal_scans = {}.compare_by_identity
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
# 每轮先处理转义和已排队输出,再共同选择读写;停止返回来源会话,总期限到达返回 nil。
|
|
@@ -52,7 +54,7 @@ module Expect
|
|
|
52
54
|
def dispatch_sources
|
|
53
55
|
idle = []
|
|
54
56
|
queued = false
|
|
55
|
-
@active.
|
|
57
|
+
@active.each_key do |session|
|
|
56
58
|
return [:timeout, idle, queued] if dispatch_expired?
|
|
57
59
|
|
|
58
60
|
next if session.pending_output?
|
|
@@ -72,7 +74,7 @@ module Expect
|
|
|
72
74
|
callback = session.sequences[:eof]
|
|
73
75
|
return [session, idle, queued] unless callback&.call
|
|
74
76
|
|
|
75
|
-
@active.
|
|
77
|
+
@active.delete(session)
|
|
76
78
|
return [:timeout, idle, queued] if dispatch_expired?
|
|
77
79
|
end
|
|
78
80
|
[nil, idle, queued]
|
|
@@ -82,11 +84,18 @@ module Expect
|
|
|
82
84
|
def dispatch_input(session)
|
|
83
85
|
if (callback = session.relay_callback)
|
|
84
86
|
session.relay_callback = nil
|
|
87
|
+
@literal_scans[session]&.reset
|
|
85
88
|
return :stopped unless callback.call
|
|
86
89
|
return :timeout if dispatch_expired?
|
|
87
90
|
end
|
|
88
91
|
|
|
89
|
-
|
|
92
|
+
# 正则与空规则不分配字面缓存;回调可追加规则,每轮按当前声明选择。
|
|
93
|
+
literal_scan = @literal_scans[session]
|
|
94
|
+
if !literal_scan && session.sequences.any? { |key, _handler| key.is_a?(String) }
|
|
95
|
+
literal_scan = (@literal_scans[session] ||= LiteralScan.new)
|
|
96
|
+
end
|
|
97
|
+
Interaction.queue_input(session, @buffers.fetch(session), final: session.eof?, deadline: @dispatch_deadline,
|
|
98
|
+
literal_scan:)
|
|
90
99
|
end
|
|
91
100
|
|
|
92
101
|
# 共同等待来源与目标,每轮读每个就绪来源一次;EINTR 返回原期限循环。
|
|
@@ -198,7 +207,7 @@ module Expect
|
|
|
198
207
|
targets = pending.each_with_object({}.compare_by_identity) do |output, index|
|
|
199
208
|
index[output.target] = true if output.target.is_a?(Session)
|
|
200
209
|
end
|
|
201
|
-
sources = @active.reject do |session|
|
|
210
|
+
sources = @active.each_key.reject do |session|
|
|
202
211
|
session.pending_output? && !targets.key?(session)
|
|
203
212
|
end
|
|
204
213
|
(sources + targets.keys).uniq { |session| session.to_io.object_id }.reject(&:eof?)
|
data/lib/expect/session.rb
CHANGED
|
@@ -130,6 +130,10 @@ module Expect
|
|
|
130
130
|
# 返回缓冲副本,防止调用方原地修改绕过裁剪规则。
|
|
131
131
|
def buffer = @buffer.dup
|
|
132
132
|
|
|
133
|
+
# 读取当前未消费主缓冲的字节数,不复制正文、不读取 IO,也不包含转接中的待写数据。
|
|
134
|
+
# @return [Integer] 当前主缓冲占用的字节数。
|
|
135
|
+
def buffer_bytesize = @buffer.bytesize
|
|
136
|
+
|
|
133
137
|
# Matcher 仅在无回调的当前扫描借用原字节,避免副本使下一次追加复制整个窗口。
|
|
134
138
|
# 不得修改或跨读取、回调保留;公开 buffer 和继续回调的快照仍独立复制。
|
|
135
139
|
# @api private
|
|
@@ -508,7 +512,7 @@ module Expect
|
|
|
508
512
|
@pending_writes&.clear
|
|
509
513
|
@relay_history&.clear
|
|
510
514
|
@relay_callback = nil
|
|
511
|
-
status = close_child(timeout:, term_timeout:, force:)
|
|
515
|
+
status = cleanup.call { close_child(timeout:, term_timeout:, force:) }
|
|
512
516
|
raise failure if failure
|
|
513
517
|
|
|
514
518
|
status
|
data/lib/expect/version.rb
CHANGED
data/sig/expect.rbs
CHANGED
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.9.
|
|
4
|
+
version: 0.9.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- expect-pty contributors
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-21 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: io-console
|
|
@@ -82,6 +82,7 @@ files:
|
|
|
82
82
|
- lib/expect/cleanup.rb
|
|
83
83
|
- lib/expect/interaction.rb
|
|
84
84
|
- lib/expect/literal_prefix.rb
|
|
85
|
+
- lib/expect/literal_scan.rb
|
|
85
86
|
- lib/expect/logging.rb
|
|
86
87
|
- lib/expect/matcher.rb
|
|
87
88
|
- lib/expect/pattern.rb
|