expect-pty 0.10.0 → 0.10.1

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: eabbf4b5d408191298426e8bc078219eacd371b58fd0cb06bdfda6ca26d43f22
4
- data.tar.gz: 6735e5a170c9b82f8043909001004bcb705f6be5971bfdd0c96db495f2cef29c
3
+ metadata.gz: 63830f1908113e813431d18f8e42dec3fb82db31bba040518daa206a86511ca2
4
+ data.tar.gz: e34b92d97f8253c87293c440a7f38bb0897c7c44e93c56c0c9225fa88fefb7d5
5
5
  SHA512:
6
- metadata.gz: 16e185ae81ce6355aa215c2e91c42ad625c6082317f19f45f51e9f300bfe937318f75f3de81183cd405f9fafaf4ecd99709a54502efd089d10d7d24011c194eb
7
- data.tar.gz: ab419a9535eed4647d6b4c095caf6a0af84cd5522e5213fe2b186c4dd5ca67428300b40025843d56705a7a9e32b5c3d2a1f8ec29a803c1c3d794405850c3f178
6
+ metadata.gz: 88a1cd1a481521d93f47cb4144d8d72c3b37b6df7fc590024fbd20f6d3cb84c6f74b92944b0d1a4c61cf6db82354d1ea4257d7370adf2e4971e4140c2040f0ec
7
+ data.tar.gz: 8b2aafa1ec75aa2b9255b82cfa81a3fcc2931fcacdda7f273fa1f95beff57e9cf390f088f8ceec408d7d78c6dc6448df55043e0dd42c5fa1fc896142d506fb12
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.10.1 - 2026-10-30
6
+
7
+ - Normalize result, buffer and rule snapshots to plain String/Array values without freezing caller input or retaining attached behavior. Preserve raw bytes, encodings and borrowed object identity.
8
+ - Make Result pretty_print use the safe summary, and execute the same validated argument snapshot exposed by Session#command.
9
+ - Preserve to_s conversion and confirmed write progress while normalizing protocol and transcript bytes before callbacks.
10
+
5
11
  ## 0.10.0 - 2026-10-30
6
12
 
7
13
  - 最低 Ruby 升级至 4.0.6;Result 明确区分 matched、timeout、eof、error,编号统一为 match_number,error 只保存原始异常。构造和 with 拒绝矛盾状态,旧 number 字段移除。
data/docs/API.md CHANGED
@@ -67,7 +67,7 @@ nil 保留全缓冲扫描;指定时,字符串和正则只搜索最新的尾
67
67
  固定 UTF-8 正则跳过边界切开的合法首字符,等待合法未完成尾字符;窗口内非法字节及 EOF 未完成字符仍报错。
68
68
  二进制正则按原始字节匹配。
69
69
 
70
- Result 的 `inspect` 与 `to_s` 仅显示匹配序号和错误状态,不打印原始文本、异常消息或来源对象。
70
+ Result 的 `inspect`、`to_s` 与 `pretty_print`(包括 `PP.pp`)仅显示匹配序号和错误状态,不打印原始文本、异常消息或来源对象。
71
71
  需要原始结果时显式读取字段;`redact` 不改变这些字段。
72
72
  Result 字段按顺序为 `status`、`match_number`、`error`、`match`、`before`、`after`、`session`、`captures`。
73
73
  状态为 `:matched`、`:timeout`、`:eof` 或 `:error`;`error` 只保存原始异常或 nil,等待超时和 EOF 均不是异常。
@@ -75,6 +75,9 @@ Result 字段按顺序为 `status`、`match_number`、`error`、`match`、`befor
75
75
  其余状态没有编号、match、after 或捕获,只有 `:error` 状态携带异常。参数、编码与回调异常仍按原规则直接抛出。
76
76
 
77
77
  对象、文本及捕获数组均不可变,未参与的捕获为 nil;来源会话与异常不复制、不冻结。
78
+ 文本与捕获容器重建为普通 String/Array,不继承调用方子类、附加状态或单例行为,也不冻结调用方输入。
79
+ 手工构造结果保留原始字节与编码;协议缓冲、字面规则、回复、转义及脱敏配置使用普通二进制字符串副本。
80
+ 写入仍先执行一次 `to_s`,随后固定普通字节副本;启动参数在修改终端前校验,实际执行与 `command` 使用同一快照。
78
81
  使用 `to_h`、位置/键模式解构或 `with` 构造新结果;构造必须指定 status,new、[]、with 都拒绝矛盾状态与非字符串文本。
79
82
  没有字段 writer、旧 number 或 to_a。会话的 `last_result`、`before`、`after`、`match`、`match_number`、`captures`、`error`
80
83
  读取最近结果;最近等待状态使用 `last_result.status`,与输入 EOF、IO 关闭、进程状态分别查询。
data/docs/MIGRATION.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # 迁移说明
2
2
 
3
+ ## 未发布:普通值快照
4
+
5
+ 结果文本、捕获数组和字符串规则不再保留输入子类、实例变量或单例方法。依赖这些附加属性的调用方应在外部保存元数据。原始字节、结果编码、nil 捕获、字段与 Data 构造接口保持不变。`PP.pp(result)` 与 `inspect` 使用同一摘要;显式字段和 `to_h` 仍可读取原始值。
6
+
3
7
  ## 0.9.x → 0.10.0:Ruby 4 与明确的等待结果
4
8
 
5
9
  - 最低运行环境为 Ruby 4.0.6,保留 Linux/macOS 支持;本地开发使用 RVM。
@@ -30,7 +30,7 @@ module Expect
30
30
 
31
31
  # 只改变显示字节,不改写匹配输入;记住上一块的 CR,避免分块的 CRLF 被扩成 CRCRLF。
32
32
  def render(data)
33
- bytes = data.to_s.b
33
+ bytes = String.new(data.to_s).force_encoding(Encoding::BINARY)
34
34
  rendered = bytes.gsub(/(?<!\r)\n/n, "\r\n")
35
35
  rendered = rendered.byteslice(1..) if @previous_carriage_return && bytes.start_with?("\n")
36
36
  @previous_carriage_return = bytes.end_with?("\r")
@@ -49,7 +49,7 @@ module Expect
49
49
  def on_sequence(sequence, &block)
50
50
  key = case sequence
51
51
  when :eof, Regexp then sequence
52
- when String then sequence.b.freeze
52
+ when String then String.new(sequence).force_encoding(Encoding::BINARY).freeze
53
53
  else raise ArgumentError, "sequence must be a String, Regexp or :eof"
54
54
  end
55
55
  raise ArgumentError, "escape sequence must not be empty" if key == ""
@@ -28,11 +28,12 @@ module Expect
28
28
  # 秘密仅作用于本会话的接收记录和诊断,不改写匹配或 outputs 的协议字节。
29
29
  # 先校验全部值再发布;注册是追加操作,应在首次通信前完成,不能追溯已交付的日志。
30
30
  def redact(*secrets)
31
+ secrets.map! { |secret| secret.is_a?(String) ? String.new(secret).force_encoding(Encoding::BINARY).freeze : secret }
31
32
  unless secrets.any? && secrets.all? { |secret| secret.is_a?(String) && !secret.empty? }
32
33
  raise ArgumentError, "secrets must be nonempty Strings"
33
34
  end
34
35
 
35
- @secrets = ((@secrets || []) + secrets.map { |secret| secret.b.freeze }).uniq.freeze
36
+ @secrets = ((@secrets || []) + secrets).uniq.freeze
36
37
  @transcript_redactor.patterns = @secrets if @transcript_redactor
37
38
  @diagnostic_redactors.each_value { |redactor| redactor.patterns = @secrets }
38
39
  self
@@ -53,7 +54,11 @@ module Expect
53
54
  return unless transcript
54
55
 
55
56
  # 单对象沿用 Session#write 的转换路径,省去数组与 join;转换后仍重新取得目标。
56
- data = objects.size == 1 ? objects.first.to_s.b : objects.map { |object| object.to_s.b }.join("") # rubocop:disable Style/RedundantArgument -- 显式空分隔符隔离全局 $,。
57
+ data = if objects.size == 1
58
+ String.new(objects.first.to_s).force_encoding(Encoding::BINARY)
59
+ else
60
+ objects.map { |object| String.new(object.to_s).force_encoding(Encoding::BINARY) }.join("") # rubocop:disable Style/RedundantArgument -- 显式空分隔符隔离全局 $,。
61
+ end
57
62
  # to_s 可切换或禁用记录目标;整条记录与其过滤尾部必须属于转换完成后的同一目标。
58
63
  target = transcript
59
64
  return unless target
@@ -6,7 +6,8 @@ module Expect
6
6
  Pattern = Data.define(:match_number, :value, :callback) do
7
7
  # 复制字符串模式;Regexp 和 callable 只引用,不取得外部对象的生命周期。
8
8
  def initialize(value:, match_number: nil, callback: nil)
9
- super(value: value.is_a?(String) ? value.b.freeze : value, match_number:, callback:)
9
+ value = String.new(value).force_encoding(Encoding::BINARY).freeze if value.is_a?(String)
10
+ super
10
11
  end
11
12
 
12
13
  # 将匹配会话或超时会话数组交给回调;额外上下文由调用方闭包保存。
@@ -15,11 +15,12 @@ module Expect
15
15
 
16
16
  # 复制冻结替换文本与规则;pending 保存尚不能安全输出的原字节,hidden 用 0/1 标记遮盖。
17
17
  def initialize(patterns, replacement: "[FILTERED]")
18
+ replacement = String.new(replacement).force_encoding(Encoding::BINARY) if replacement.is_a?(String)
18
19
  unless replacement.is_a?(String) && !replacement.empty?
19
20
  raise ArgumentError, "replacement must be a nonempty String"
20
21
  end
21
22
 
22
- @replacement = replacement.b.freeze
23
+ @replacement = replacement.freeze
23
24
  self.patterns = patterns
24
25
  @pending = "".b
25
26
  @hidden = "".b
@@ -28,11 +29,16 @@ module Expect
28
29
 
29
30
  # 更新后续匹配规则并保留已有尾部与掩码;不能追溯修改已经交付给日志目标的内容。
30
31
  def patterns=(patterns)
32
+ if patterns.is_a?(Array)
33
+ patterns = Array.new(patterns).map! do |pattern|
34
+ pattern.is_a?(String) ? String.new(pattern).force_encoding(Encoding::BINARY).freeze : pattern
35
+ end
36
+ end
31
37
  unless patterns.is_a?(Array) && patterns.all? { |pattern| pattern.is_a?(String) && !pattern.empty? }
32
38
  raise ArgumentError, "patterns must be an Array of nonempty Strings"
33
39
  end
34
40
 
35
- @patterns = patterns.map { |pattern| pattern.b.freeze }.uniq.freeze
41
+ @patterns = patterns.uniq.freeze
36
42
  @lookbehind = [(@patterns.map(&:bytesize).max || 0) - 1, 0].max
37
43
  @scanned_bytes = 0
38
44
  end
@@ -74,11 +80,12 @@ module Expect
74
80
  def append_chunk(data, visible:)
75
81
  raise ArgumentError, "data must be a String" unless data.is_a?(String)
76
82
 
83
+ data = String.new(data).force_encoding(Encoding::BINARY)
77
84
  # 没有规则且旧尾部已排空时,原样交付独立字节副本,不构造等长掩码。
78
85
  # 非空明文会分隔隐藏区间;空分片不能改变以后重新启用规则时的边界。
79
86
  if @patterns.empty? && @pending.empty?
80
87
  @masking = false unless data.empty?
81
- return visible ? data.b : "".b
88
+ return visible ? data : "".b
82
89
  end
83
90
 
84
91
  unless visible || data.empty?
@@ -86,7 +93,7 @@ module Expect
86
93
  @suppressed_until = @pending.bytesize + data.bytesize
87
94
  end
88
95
  @suppressed << ((visible ? "\0" : "\1") * data.bytesize) if @suppressed
89
- @pending << data.b
96
+ @pending << data
90
97
  @hidden << ("\0" * data.bytesize)
91
98
  mark_secrets
92
99
  release([@pending.bytesize - @lookbehind, 0].max)
@@ -17,14 +17,16 @@ module Expect
17
17
  # limit 是本规则在一次 expect 中允许的应答次数,nil 不限;规则文本和静态回复复制冻结。
18
18
  def on(pattern, reply: nil, limit: nil, &block)
19
19
  raise FrozenError, "responder is frozen" if frozen?
20
+
21
+ pattern = String.new(pattern).force_encoding(Encoding::BINARY).freeze if pattern.is_a?(String)
22
+ reply = String.new(reply).force_encoding(Encoding::BINARY).freeze if reply.is_a?(String)
20
23
  unless (pattern.is_a?(String) && !pattern.empty?) || pattern.is_a?(Regexp)
21
24
  raise ArgumentError, "response pattern must be a nonempty String or Regexp"
22
25
  end
23
26
 
24
27
  validate_action!(reply, limit, block)
25
28
 
26
- pattern = pattern.b.freeze if pattern.is_a?(String)
27
- @rules << [pattern, reply&.b&.freeze, limit, block].freeze
29
+ @rules << [pattern, reply, limit, block].freeze
28
30
  self
29
31
  end
30
32
 
data/lib/expect/result.rb CHANGED
@@ -28,9 +28,10 @@ module Expect
28
28
  session: nil, captures: [])
29
29
  raise ArgumentError, "captures must be an Array" unless captures.is_a?(Array)
30
30
 
31
+ captures = Array.new(captures)
31
32
  validate_outcome!(status, match_number, error, match, after, captures)
32
33
  super(status:, match_number:, error:, session:, match: copy_text(match), before: copy_text(before),
33
- after: copy_text(after), captures: captures.map { |value| copy_text(value) }.freeze)
34
+ after: copy_text(after), captures: captures.map! { |value| copy_text(value) }.freeze)
34
35
  end
35
36
 
36
37
  # 诊断只显示匹配序号和错误状态,不检查原始文本、异常或来源对象。
@@ -41,6 +42,11 @@ module Expect
41
42
  # @return [String] 与 inspect 相同的结果摘要。
42
43
  def to_s = inspect
43
44
 
45
+ # PP 使用同一摘要,不展开正文、异常或借用的来源对象。
46
+ # @param printer [PP] Ruby 标准 pretty printer。
47
+ # @return [void]
48
+ def pretty_print(printer) = printer.text(inspect)
49
+
44
50
  # 是否命中文本;EOF 和超时没有模式序号。
45
51
  def matched? = status == :matched
46
52
 
@@ -61,7 +67,7 @@ module Expect
61
67
 
62
68
  raise ArgumentError, "result text must be a String or nil" unless value.is_a?(String)
63
69
 
64
- value.dup.freeze
70
+ String.new(value).freeze
65
71
  end
66
72
 
67
73
  # 状态是唯一判据;new、[] 和 with 都经过同一边界,不能产生相互矛盾的结果。
@@ -56,12 +56,14 @@ module Expect
56
56
 
57
57
  # 在新控制终端中执行命令并同步确认 exec 结果;同一会话只能启动一次。
58
58
  def spawn(*command, env: {}, chdir: nil, raw: false, shell: false)
59
+ # 参数快照先于终端修改;公开记录与子进程使用同一份普通字符串。
60
+ command = command.map { |part| part.is_a?(String) ? String.new(part).freeze : part }.freeze
59
61
  validate_spawn!(command)
60
62
  raise ArgumentError, "shell must be true or false" unless [true, false].include?(shell)
61
63
  raise ArgumentError, "shell requires one command string" if shell && command.size != 1
62
64
 
63
65
  @slave.raw! if raw
64
- @command = command.map { |part| part.dup.freeze }.freeze
66
+ @command = command
65
67
  Cleanup.on_failure(-> { hard_close }) do
66
68
  child = @child_process.spawn(command, env:, chdir:, shell:) do |from_child, to_parent|
67
69
  exec_child(command, env:, chdir:, from_child:, to_parent:, shell:)
@@ -147,7 +149,7 @@ module Expect
147
149
  def buffer=(value)
148
150
  raise ArgumentError, "buffer must be a String" unless value.is_a?(String)
149
151
 
150
- @buffer = value.b
152
+ @buffer = String.new(value).force_encoding(Encoding::BINARY)
151
153
  @buffer_generation += 1
152
154
  @relay_history.clear
153
155
  trim_buffer
@@ -174,7 +176,11 @@ module Expect
174
176
 
175
177
  begin
176
178
  # 单对象不需要中间数组与 join 副本;to_s、诊断和部分写入仍经过同一条路径。
177
- data = objects.size == 1 ? objects.first.to_s.b : objects.map { |object| object.to_s.b }.join("") # rubocop:disable Style/RedundantArgument -- 显式空分隔符隔离全局 $,。
179
+ data = if objects.size == 1
180
+ String.new(objects.first.to_s).force_encoding(Encoding::BINARY)
181
+ else
182
+ objects.map { |object| String.new(object.to_s).force_encoding(Encoding::BINARY) }.join("") # rubocop:disable Style/RedundantArgument -- 显式空分隔符隔离全局 $,。
183
+ end
178
184
  check_writable!
179
185
  trace_data(:sending, data)
180
186
  rescue WriteTimeout
@@ -375,7 +381,7 @@ module Expect
375
381
 
376
382
  return mark_eof if data.nil?
377
383
 
378
- data = data.b
384
+ data = String.new(data).force_encoding(Encoding::BINARY)
379
385
  buffer << data
380
386
  @received_bytes += data.bytesize
381
387
  trim_buffer if trim
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Expect
4
4
  # Gem 与库共用的版本号;独立文件使 gemspec 无需加载完整会话实现。
5
- VERSION = "0.10.0"
5
+ VERSION = "0.10.1"
6
6
  end
data/sig/expect.rbs CHANGED
@@ -253,6 +253,8 @@ module Expect
253
253
 
254
254
  def deconstruct: () -> [result_status, Integer?, result_error, String?, String?, String?, Session?, Array[String?]]
255
255
 
256
+ def pretty_print: (untyped printer) -> void
257
+
256
258
  def with: (
257
259
  ?status: result_status,
258
260
  ?match_number: Integer?,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: expect-pty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - expect-pty contributors