network-infra-utility 0.2.0 → 0.5.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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +21 -0
  3. data/CHANGELOG.md +31 -4
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +70 -0
  6. data/Rakefile +1 -1
  7. data/bin/dns-query +834 -0
  8. data/bin/geo-doc +135 -0
  9. data/bin/geo-get +1 -1
  10. data/document/ASNum/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +242 -0
  11. data/document/DNSQuery/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +248 -0
  12. data/document/Geo/345/221/275/344/273/244/345/267/245/345/205/267/344/275/277/347/224/250/346/226/271/346/263/225.md +441 -0
  13. data/document/IP/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +297 -0
  14. data/document/MAC/346/250/241/345/235/227/345/212/237/350/203/275/350/257/264/346/230/216.md +296 -0
  15. data/document/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/344/275/277/347/224/250/346/226/271/346/263/225.md +764 -0
  16. data/network-infra-utility.gemspec +4 -2
  17. data/network.rb +3 -1
  18. data/service/geodb/GeoAPI.md +1 -0
  19. data/service/geodb/geodb.rb +278 -1
  20. data/service/ssh/README.md +942 -0
  21. data/service/ssh/bin/ssh-client +198 -0
  22. data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/345/212/237/350/203/275/351/234/200/346/261/202/346/226/207/346/241/243.md +292 -0
  23. data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/350/257/246/347/273/206/350/256/276/350/256/241/346/226/207/346/241/243.md +1521 -0
  24. data/service/ssh/design/SSH/350/277/236/346/216/245/345/256/242/346/210/267/347/253/257/350/275/257/344/273/266/350/256/276/350/256/241/346/226/207/346/241/243.md +2493 -0
  25. data/service/ssh/ext/ssh_core/bin/ssh_core.cmd +28 -0
  26. data/service/ssh/ext/ssh_core/config/sys.config +0 -0
  27. data/service/ssh/ext/ssh_core/config/vm.args +0 -0
  28. data/service/ssh/ext/ssh_core/local_deps/jsx/CHECKSUM +1 -0
  29. data/service/ssh/ext/ssh_core/local_deps/jsx/LICENSE +21 -0
  30. data/service/ssh/ext/ssh_core/local_deps/jsx/README.md +696 -0
  31. data/service/ssh/ext/ssh_core/local_deps/jsx/VERSION +1 -0
  32. data/service/ssh/ext/ssh_core/local_deps/jsx/contents.tar.gz +0 -0
  33. data/service/ssh/ext/ssh_core/local_deps/jsx/metadata.config +15 -0
  34. data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.config +17 -0
  35. data/service/ssh/ext/ssh_core/local_deps/jsx/rebar.lock +1 -0
  36. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.app.src +10 -0
  37. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx.erl +506 -0
  38. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.erl +393 -0
  39. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_config.hrl +18 -0
  40. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_consult.erl +81 -0
  41. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_decoder.erl +1909 -0
  42. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_encoder.erl +116 -0
  43. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_parser.erl +1214 -0
  44. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_json.erl +408 -0
  45. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_to_term.erl +389 -0
  46. data/service/ssh/ext/ssh_core/local_deps/jsx/src/jsx_verify.erl +121 -0
  47. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx.erl +506 -0
  48. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.erl +393 -0
  49. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_config.hrl +18 -0
  50. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_consult.erl +81 -0
  51. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_decoder.erl +1909 -0
  52. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_encoder.erl +116 -0
  53. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_parser.erl +1214 -0
  54. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_json.erl +408 -0
  55. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_to_term.erl +389 -0
  56. data/service/ssh/ext/ssh_core/local_deps/jsx/src/src/jsx_verify.erl +121 -0
  57. data/service/ssh/ext/ssh_core/rebar.config +24 -0
  58. data/service/ssh/ext/ssh_core/rebar.lock +1 -0
  59. data/service/ssh/ext/ssh_core/src/ssh_auth_engine.erl +156 -0
  60. data/service/ssh/ext/ssh_core/src/ssh_channel_stm.erl +232 -0
  61. data/service/ssh/ext/ssh_core/src/ssh_codec.erl +83 -0
  62. data/service/ssh/ext/ssh_core/src/ssh_conn_sup.erl +48 -0
  63. data/service/ssh/ext/ssh_core/src/ssh_conn_worker.erl +535 -0
  64. data/service/ssh/ext/ssh_core/src/ssh_core.app.src +36 -0
  65. data/service/ssh/ext/ssh_core/src/ssh_core_app.erl +11 -0
  66. data/service/ssh/ext/ssh_core/src/ssh_core_sup.erl +46 -0
  67. data/service/ssh/ext/ssh_core/src/ssh_infra_sup.erl +104 -0
  68. data/service/ssh/ext/ssh_core/src/ssh_ipc.hrl +80 -0
  69. data/service/ssh/ext/ssh_core/src/ssh_ipc_coalesce.erl +94 -0
  70. data/service/ssh/ext/ssh_core/src/ssh_ipc_gateway.erl +467 -0
  71. data/service/ssh/ext/ssh_core/src/ssh_ipc_proto.erl +95 -0
  72. data/service/ssh/ext/ssh_core/src/ssh_jump_chain.erl +101 -0
  73. data/service/ssh/ext/ssh_core/src/ssh_keepalive_mgr.erl +222 -0
  74. data/service/ssh/ext/ssh_core/src/ssh_known_hosts_proxy.erl +67 -0
  75. data/service/ssh/ext/ssh_core/src/ssh_port_fwd.erl +225 -0
  76. data/service/ssh/ext/ssh_core/src/ssh_sftp_session.erl +250 -0
  77. data/service/ssh/ext/ssh_core/src/ssh_sftp_sup.erl +62 -0
  78. data/service/ssh/ext/ssh_core_rs/Cargo.lock +2345 -0
  79. data/service/ssh/ext/ssh_core_rs/Cargo.toml +30 -0
  80. data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs +34 -0
  81. data/service/ssh/ext/ssh_core_rs/bin/ssh_core_rs.cmd +40 -0
  82. data/service/ssh/ext/ssh_core_rs/src/channel.rs +296 -0
  83. data/service/ssh/ext/ssh_core_rs/src/coalesce.rs +143 -0
  84. data/service/ssh/ext/ssh_core_rs/src/codec.rs +71 -0
  85. data/service/ssh/ext/ssh_core_rs/src/conn.rs +628 -0
  86. data/service/ssh/ext/ssh_core_rs/src/gateway.rs +389 -0
  87. data/service/ssh/ext/ssh_core_rs/src/handler.rs +293 -0
  88. data/service/ssh/ext/ssh_core_rs/src/keepalive.rs +194 -0
  89. data/service/ssh/ext/ssh_core_rs/src/main.rs +351 -0
  90. data/service/ssh/ext/ssh_core_rs/src/portfwd.rs +378 -0
  91. data/service/ssh/ext/ssh_core_rs/src/proto.rs +198 -0
  92. data/service/ssh/ext/ssh_core_rs/src/sftp.rs +294 -0
  93. data/service/ssh/lib/network_infra_utility/ssh/automation/macro_engine.rb +213 -0
  94. data/service/ssh/lib/network_infra_utility/ssh/client.rb +257 -0
  95. data/service/ssh/lib/network_infra_utility/ssh/config/schema.rb +90 -0
  96. data/service/ssh/lib/network_infra_utility/ssh/config/settings.rb +103 -0
  97. data/service/ssh/lib/network_infra_utility/ssh/config/store.rb +90 -0
  98. data/service/ssh/lib/network_infra_utility/ssh/ipc/coalesce.rb +83 -0
  99. data/service/ssh/lib/network_infra_utility/ssh/ipc/errors.rb +36 -0
  100. data/service/ssh/lib/network_infra_utility/ssh/ipc/router.rb +212 -0
  101. data/service/ssh/lib/network_infra_utility/ssh/ipc/transport.rb +81 -0
  102. data/service/ssh/lib/network_infra_utility/ssh/security/host_key.rb +211 -0
  103. data/service/ssh/lib/network_infra_utility/ssh/security/vault.rb +211 -0
  104. data/service/ssh/lib/network_infra_utility/ssh/session/history.rb +56 -0
  105. data/service/ssh/lib/network_infra_utility/ssh/session/manager.rb +92 -0
  106. data/service/ssh/lib/network_infra_utility/ssh/session/session.rb +109 -0
  107. data/service/ssh/lib/network_infra_utility/ssh/session/tree.rb +95 -0
  108. data/service/ssh/lib/network_infra_utility/ssh/terminal/ansi_parser.rb +435 -0
  109. data/service/ssh/lib/network_infra_utility/ssh/terminal/buffer.rb +78 -0
  110. data/service/ssh/lib/network_infra_utility/ssh/terminal/emulator.rb +159 -0
  111. data/service/ssh/lib/network_infra_utility/ssh/terminal/logger.rb +195 -0
  112. data/service/ssh/lib/network_infra_utility/ssh/terminal/screen.rb +212 -0
  113. data/service/ssh/lib/network_infra_utility/ssh/terminal/theme.rb +127 -0
  114. data/service/ssh/lib/network_infra_utility/ssh/version.rb +7 -0
  115. data/service/ssh/lib/network_infra_utility/ssh.rb +44 -0
  116. data/support/basic/as_num.rb +221 -0
  117. data/support/basic/mac_address.rb +281 -0
  118. data/version.rb +1 -1
  119. metadata +138 -1
@@ -0,0 +1,435 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NetworkInfraUtility
4
+ module SSH
5
+ module Terminal
6
+ # ANSI/xterm 转义序列解析器。
7
+ # 纯解析器,无状态副作用,只调 Emulator 回调(LLD §7.6)。
8
+ #
9
+ # 构造签名(LLD 修正版):AnsiParser.new(emulator)
10
+ # 传入 Emulator 自身,AnsiParser 回调 Emulator 方法,
11
+ # Emulator 再委托给 Screen。
12
+ #
13
+ # 覆盖目标:≥ 95% ANSI(FR-TERM-001 ④),参考 vte/tty 转义表。
14
+ # DoD(LLD §11.2):ANSI 覆盖率 ≥ 95%(用 vttest 子集验证)
15
+ #
16
+ # 支持的转义类别:
17
+ # - CSI (Control Sequence Introducer) \e[...X
18
+ # - SGR (Select Graphic Rendition) \e[...m
19
+ # - OSC (Operating System Command) \e]...\x07
20
+ # - DCS (Device Control String) \eP...\x9c
21
+ # - 单字符控制序列 \eX
22
+ class AnsiParser
23
+ # 解析状态机
24
+ # :ground — 普通文本,逐字符输出
25
+ # :escape — 收到 ESC (0x1b),等待中间字节
26
+ # :csi_entry — 收到 ESC [,收集参数
27
+ # :csi_param — 收集 CSI 参数(数字和分号)
28
+ # :csi_intermediate — 收集 CSI 中间字节 (空格到/)
29
+ # :osc_string — OSC 字符串,直到 BEL (0x07) 或 ST
30
+ # :dcs_string — DCS 字符串,直到 ST
31
+ # :charset — 字符集指定(ESC ( B 等)
32
+
33
+ ESC = "\e".freeze
34
+ CSI = "[".freeze
35
+ BEL = "\x07".freeze
36
+ ST = "\x9c".freeze # String Terminator (7-bit: ESC \)
37
+ DEL = 0x7f
38
+
39
+ # SGR 前景色映射(标准 8 色)
40
+ SGR_FG = {
41
+ 0 => :reset, 30 => :black, 31 => :red, 32 => :green,
42
+ 33 => :yellow, 34 => :blue, 35 => :magenta, 36 => :cyan, 37 => :white
43
+ }.freeze
44
+
45
+ # SGR 背景色映射
46
+ SGR_BG = {
47
+ 40 => :black, 41 => :red, 42 => :green, 43 => :yellow,
48
+ 44 => :blue, 45 => :magenta, 46 => :cyan, 47 => :white
49
+ }.freeze
50
+
51
+ # SGR 扩展前景色(高亮 8 色)
52
+ SGR_FG_BRIGHT = {
53
+ 90 => :black, 91 => :red, 92 => :green, 93 => :yellow,
54
+ 94 => :blue, 95 => :magenta, 96 => :cyan, 97 => :white
55
+ }.freeze
56
+
57
+ attr_reader :state
58
+
59
+ # @param emulator [Terminal::Emulator] 回调目标
60
+ def initialize(emulator)
61
+ @emu = emulator
62
+ @state = :ground
63
+ @params = []
64
+ @current_param = +""
65
+ @intermediates = +""
66
+ @string_buffer = +""
67
+ @charset = nil
68
+ end
69
+
70
+ # 喂入原始字节流,驱动状态机
71
+ # @param data [String] 原始字节
72
+ def feed(data)
73
+ data.each_byte do |byte|
74
+ process_byte(byte)
75
+ end
76
+ end
77
+
78
+ # 重置解析器状态
79
+ def reset
80
+ @state = :ground
81
+ @params = []
82
+ @current_param = +""
83
+ @intermediates = +""
84
+ @string_buffer = +""
85
+ end
86
+
87
+ private
88
+
89
+ def process_byte(byte)
90
+ char = byte.chr(Encoding::UTF_8)
91
+ case @state
92
+ when :ground
93
+ process_ground(byte, char)
94
+ when :escape
95
+ process_escape(byte, char)
96
+ when :csi_entry, :csi_param
97
+ process_csi(byte, char)
98
+ when :csi_intermediate
99
+ process_csi_intermediate(byte, char)
100
+ when :osc_string
101
+ process_osc(byte, char)
102
+ when :dcs_string
103
+ process_dcs(byte, char)
104
+ when :charset
105
+ process_charset(byte, char)
106
+ end
107
+ end
108
+
109
+ # ---- :ground ----
110
+ def process_ground(byte, char)
111
+ case byte
112
+ when 0x1b # ESC
113
+ @state = :escape
114
+ when 0x0d # CR
115
+ @emu.cursor_to(0, @emu.cursor_y)
116
+ when 0x0a, 0x0b, 0x0c # LF, VT, FF
117
+ @emu.newline
118
+ when 0x08 # BS
119
+ @emu.backspace
120
+ when 0x09 # HT
121
+ @emu.tab
122
+ when 0x07 # BEL
123
+ @emu.bell
124
+ when 0x00 # NUL — 忽略
125
+ # noop
126
+ when DEL # DEL — 忽略
127
+ # noop
128
+ else
129
+ if byte >= 0x20 && byte < 0x7f
130
+ # 可打印 ASCII
131
+ @emu.put_char(char)
132
+ elsif byte >= 0x80
133
+ # UTF-8 / 高位字节 — 直接当作字符
134
+ @emu.put_char(char)
135
+ end
136
+ end
137
+ end
138
+
139
+ # ---- :escape ----
140
+ def process_escape(byte, char)
141
+ case char
142
+ when CSI # [
143
+ @params = []
144
+ @current_param = +""
145
+ @intermediates = +""
146
+ @state = :csi_entry
147
+ when "]" # OSC
148
+ @string_buffer = +""
149
+ @state = :osc_string
150
+ when "P" # DCS
151
+ @string_buffer = +""
152
+ @state = :dcs_string
153
+ when "(" # G0 字符集
154
+ @charset = :g0
155
+ @state = :charset
156
+ when ")" # G1 字符集
157
+ @charset = :g1
158
+ @state = :charset
159
+ when "M" # Reverse Index (RI)
160
+ @emu.scroll_down(1)
161
+ @state = :ground
162
+ when "D" # Index (IND)
163
+ @emu.newline
164
+ @state = :ground
165
+ when "E" # Next Line (NEL)
166
+ @emu.cursor_to(0, @emu.cursor_y + 1)
167
+ @state = :ground
168
+ when "7" # DECSC — 保存光标
169
+ @state = :ground
170
+ # V2.0: save_cursor
171
+ when "8" # DECRC — 恢复光标
172
+ @state = :ground
173
+ # V2.0: restore_cursor
174
+ when "c" # RIS — 全部重置
175
+ @emu.clear_screen(:full)
176
+ @state = :ground
177
+ when "=" # 应用键盘模式
178
+ @state = :ground
179
+ when ">" # 数字键盘模式
180
+ @state = :ground
181
+ else
182
+ # 未知转义,回到 ground
183
+ @state = :ground
184
+ end
185
+ end
186
+
187
+ # ---- :csi_entry / :csi_param ----
188
+ def process_csi(byte, char)
189
+ if byte >= 0x30 && byte <= 0x39 # 0-9
190
+ @current_param << char
191
+ @state = :csi_param
192
+ elsif byte == 0x3b # ;
193
+ @params << (@current_param.empty? ? 0 : @current_param.to_i)
194
+ @current_param = +""
195
+ @state = :csi_param
196
+ elsif byte == 0x3f # ? — 私有参数前缀
197
+ @intermediates << char
198
+ @state = :csi_param
199
+ elsif byte >= 0x3c && byte <= 0x3e # < = > — 私有参数标记
200
+ @intermediates << char
201
+ @state = :csi_param
202
+ elsif byte >= 0x20 && byte <= 0x2f # 空格到/ — 中间字节
203
+ @intermediates << char
204
+ @state = :csi_intermediate
205
+ elsif byte >= 0x40 && byte <= 0x7e # 终止字节
206
+ @params << (@current_param.empty? ? 0 : @current_param.to_i) unless @current_param.empty?
207
+ dispatch_csi(char)
208
+ @state = :ground
209
+ else
210
+ @state = :ground
211
+ end
212
+ end
213
+
214
+ # ---- :csi_intermediate ----
215
+ def process_csi_intermediate(byte, char)
216
+ if byte >= 0x20 && byte <= 0x2f
217
+ @intermediates << char
218
+ elsif byte >= 0x40 && byte <= 0x7e
219
+ dispatch_csi(char)
220
+ @state = :ground
221
+ else
222
+ @state = :ground
223
+ end
224
+ end
225
+
226
+ # ---- :osc_string ----
227
+ def process_osc(byte, char)
228
+ if byte == 0x07 # BEL — 终止
229
+ handle_osc(@string_buffer)
230
+ @state = :ground
231
+ elsif byte == 0x1b
232
+ # 可能是 ST (ESC \)
233
+ @string_buffer << char
234
+ # 简化:下一个字节如果是 \ 就结束
235
+ elsif byte == 0x5c && @string_buffer.end_with?("\e")
236
+ @string_buffer = @string_buffer[0..-2]
237
+ handle_osc(@string_buffer)
238
+ @state = :ground
239
+ else
240
+ @string_buffer << char
241
+ end
242
+ end
243
+
244
+ # ---- :dcs_string ----
245
+ def process_dcs(byte, char)
246
+ if byte == 0x9c # ST
247
+ handle_dcs(@string_buffer)
248
+ @state = :ground
249
+ elsif byte == 0x1b
250
+ @string_buffer << char
251
+ elsif byte == 0x5c && @string_buffer.end_with?("\e")
252
+ @string_buffer = @string_buffer[0..-2]
253
+ handle_dcs(@string_buffer)
254
+ @state = :ground
255
+ else
256
+ @string_buffer << char
257
+ end
258
+ end
259
+
260
+ # ---- :charset ----
261
+ def process_charset(byte, char)
262
+ # 字符集指定(如 B = US-ASCII),V1.0 忽略
263
+ @state = :ground
264
+ end
265
+
266
+ # ---- CSI 分发 ----
267
+ def dispatch_csi(final_char)
268
+ p = @params
269
+ case final_char
270
+ when "A" # CUU — 光标上移
271
+ @emu.cursor_to(@emu.cursor_x, @emu.cursor_y - (p[0] || 1))
272
+ when "B" # CUD — 光标下移
273
+ @emu.cursor_to(@emu.cursor_x, @emu.cursor_y + (p[0] || 1))
274
+ when "C" # CUF — 光标右移
275
+ @emu.cursor_to(@emu.cursor_x + (p[0] || 1), @emu.cursor_y)
276
+ when "D" # CUB — 光标左移
277
+ @emu.cursor_to(@emu.cursor_x - (p[0] || 1), @emu.cursor_y)
278
+ when "H", "f" # CUP / HVP — 光标定位
279
+ row = (p[0] || 1) - 1
280
+ col = (p[1] || 1) - 1
281
+ @emu.cursor_to(col, row)
282
+ when "J" # ED — 擦除显示
283
+ mode = p[0] || 0
284
+ @emu.clear_screen(mode.zero? ? :below : (mode == 1 ? :above : :full))
285
+ when "K" # EL — 擦除行
286
+ mode = p[0] || 0
287
+ @emu.clear_line(mode.zero? ? :right : (mode == 1 ? :left : :full))
288
+ when "m" # SGR — 图形属性设置
289
+ dispatch_sgr(p)
290
+ when "r" # DECSTBM — 设置滚动区
291
+ top = (p[0] || 1) - 1
292
+ bottom = (p[1] || 1) - 1
293
+ @emu.set_scroll_region(top, bottom)
294
+ when "h" # SM — 设置模式
295
+ handle_set_mode(p, true)
296
+ when "l" # RM — 重置模式
297
+ handle_set_mode(p, false)
298
+ when "n" # DSR — 设备状态报告
299
+ handle_dsr(p)
300
+ when "S" # SU — 向上滚动
301
+ @emu.scroll_up(p[0] || 1)
302
+ when "T" # SD — 向下滚动
303
+ @emu.scroll_down(p[0] || 1)
304
+ when "L" # IL — 插入行
305
+ handle_insert_lines(p[0] || 1)
306
+ when "M" # DL — 删除行
307
+ handle_delete_lines(p[0] || 1)
308
+ when "P" # DCH — 删除字符
309
+ handle_delete_chars(p[0] || 1)
310
+ when "@" # ICH — 插入字符
311
+ handle_insert_chars(p[0] || 1)
312
+ when "X" # ECH — 擦除字符
313
+ handle_erase_chars(p[0] || 1)
314
+ when "d" # VPA — 垂直位置
315
+ @emu.cursor_to(@emu.cursor_x, (p[0] || 1) - 1)
316
+ when "G" # CHA — 水平位置
317
+ @emu.cursor_to((p[0] || 1) - 1, @emu.cursor_y)
318
+ else
319
+ # 未覆盖的 CSI 序列,骨架阶段忽略
320
+ end
321
+ end
322
+
323
+ # ---- SGR 分发 ----
324
+ def dispatch_sgr(params)
325
+ params = [0] if params.empty?
326
+
327
+ params.each_with_index do |code, _i|
328
+ case code
329
+ when 0
330
+ @emu.reset_style
331
+ when 1
332
+ @emu.set_style(bold: true)
333
+ when 2
334
+ @emu.set_style(dim: true)
335
+ when 3
336
+ @emu.set_style(italic: true)
337
+ when 4
338
+ @emu.set_style(underline: true)
339
+ when 5, 6
340
+ @emu.set_style(blink: true)
341
+ when 7
342
+ @emu.set_style(reverse: true)
343
+ when 22
344
+ @emu.set_style(bold: false, dim: false)
345
+ when 23
346
+ @emu.set_style(italic: false)
347
+ when 24
348
+ @emu.set_style(underline: false)
349
+ when 27
350
+ @emu.set_style(reverse: false)
351
+ when 38
352
+ # 扩展前景色(38;5;n 或 38;2;r;g;b),骨架阶段占位
353
+ @emu.set_style(fg_ext: true)
354
+ when 48
355
+ # 扩展背景色,骨架阶段占位
356
+ @emu.set_style(bg_ext: true)
357
+ when 39
358
+ @emu.set_style(fg: nil)
359
+ when 49
360
+ @emu.set_style(bg: nil)
361
+ when *SGR_FG.keys
362
+ @emu.set_style(fg: SGR_FG[code])
363
+ when *SGR_BG.keys
364
+ @emu.set_style(bg: SGR_BG[code])
365
+ when *SGR_FG_BRIGHT.keys
366
+ @emu.set_style(fg: SGR_FG_BRIGHT[code], bright: true)
367
+ else
368
+ # 未覆盖的 SGR 码,骨架阶段忽略
369
+ end
370
+ end
371
+ end
372
+
373
+ # ---- 模式设置 (DECSET/DECRST) ----
374
+ def handle_set_mode(params, enable)
375
+ params.each do |code|
376
+ case code
377
+ when 1049, 1047 # Alt screen
378
+ enable ? @emu.enter_alt_screen : @emu.exit_alt_screen
379
+ when 1048 # 保存/恢复光标
380
+ # V2.0: save/restore cursor
381
+ else
382
+ # 未覆盖的模式,骨架阶段忽略
383
+ end
384
+ end
385
+ end
386
+
387
+ # ---- DSR 设备状态报告 ----
388
+ def handle_dsr(params)
389
+ case params[0]
390
+ when 5 # 状态报告
391
+ # V2.0: send "ESC[0n" (OK) back to terminal
392
+ when 6 # 光标位置报告
393
+ # V2.0: send "ESC[<row>;<col>R" back
394
+ end
395
+ end
396
+
397
+ # ---- OSC 处理 ----
398
+ def handle_osc(string)
399
+ # OSC 序列如 "0;title" (窗口标题) 或 "4;0;rgb:00/00/00" (调色板)
400
+ # V1.0 骨架阶段忽略 OSC
401
+ end
402
+
403
+ # ---- DCS 处理 ----
404
+ def handle_dcs(string)
405
+ # DCS 序列用于 DECRQSS 等查询
406
+ # V1.0 骨架阶段忽略 DCS
407
+ end
408
+
409
+ # ---- 行/字符操作占位 ----
410
+ # 以下操作在 Screen 上的对应方法待具体实现阶段补充
411
+
412
+ def handle_insert_lines(n)
413
+ # IL — 在光标位置插入 n 行
414
+ # 骨架阶段:简化为滚动处理
415
+ end
416
+
417
+ def handle_delete_lines(n)
418
+ # DL — 在光标位置删除 n 行
419
+ end
420
+
421
+ def handle_delete_chars(n)
422
+ # DCH — 删除 n 个字符
423
+ end
424
+
425
+ def handle_insert_chars(n)
426
+ # ICH — 插入 n 个空字符
427
+ end
428
+
429
+ def handle_erase_chars(n)
430
+ # ECH — 擦除 n 个字符(用空格替换,不移动)
431
+ end
432
+ end
433
+ end
434
+ end
435
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NetworkInfraUtility
4
+ module SSH
5
+ module Terminal
6
+ # 回滚区 + 当前屏幕快照,提供 search/export。
7
+ # 写入由 Screen 触发(滚动时旧行进入回滚区)。
8
+ # 对应需求 FR-TERM-007:默认 ≥ 10000 行,可配置 1000–100000。
9
+ class Buffer
10
+ attr_reader :scrollback_lines, :max_lines
11
+ attr_accessor :screen
12
+
13
+ def initialize(max_lines: 10_000)
14
+ @max_lines = max_lines
15
+ @scrollback_lines = [] # 已滚出屏幕的历史行
16
+ @screen = nil
17
+ end
18
+
19
+ # 绑定 Screen,接收滚动事件
20
+ def attach_screen(screen)
21
+ @screen = screen
22
+ end
23
+
24
+ # Screen 滚动时调用,将被推出的行加入回滚区
25
+ def push_scrollback(line_text)
26
+ @scrollback_lines << line_text
27
+ trim
28
+ end
29
+
30
+ # 全量行数(回滚 + 屏幕)
31
+ def total_lines
32
+ @scrollback_lines.size + (@screen&.rows || 0)
33
+ end
34
+
35
+ # 搜索全部内容(回滚 + 屏幕)
36
+ # @param pattern [String] 正则表达式
37
+ # @return [Array<Match>]
38
+ def search(pattern)
39
+ regex = Regexp.new(pattern, Regexp::IGNORECASE)
40
+ matches = []
41
+ all_lines.each_with_index do |line, idx|
42
+ line.scan(regex) do
43
+ m = Regexp.last_match
44
+ matches << Match.new(line: idx, start: m.begin(0), end_: m.end(0), text: m[0])
45
+ end
46
+ end
47
+ matches
48
+ end
49
+
50
+ # 导出纯文本
51
+ def to_text
52
+ all_lines.join("\n")
53
+ end
54
+
55
+ # 设置最大回滚行数
56
+ def max_lines=(n)
57
+ @max_lines = n
58
+ trim
59
+ end
60
+
61
+ private
62
+
63
+ def all_lines
64
+ @scrollback_lines + (@screen ? @screen.lines.map(&:to_s) : [])
65
+ end
66
+
67
+ def trim
68
+ return if @scrollback_lines.size <= @max_lines
69
+
70
+ @scrollback_lines = @scrollback_lines[-@max_lines..]
71
+ end
72
+ end
73
+
74
+ # 搜索匹配结果
75
+ Match = Struct.new(:line, :start, :end_, :text)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "forwardable"
5
+ require_relative "screen"
6
+ require_relative "buffer"
7
+ require_relative "theme"
8
+ require_relative "logger"
9
+ require_relative "ansi_parser"
10
+
11
+ module NetworkInfraUtility
12
+ module SSH
13
+ module Terminal
14
+ # ANSI/xterm 转义序列解析与终端状态维护。
15
+ # 四层分离(LLD §7.6):
16
+ # AnsiParser — 纯解析器,解析 ANSI 转义序列,回调 Emulator 方法
17
+ # Emulator — 终端状态协调器,接收 AnsiParser 回调,委托给 Screen
18
+ # Screen — 屏幕状态(行/光标/滚动区/alt buffer/字符属性)
19
+ # Buffer — 回滚区 + 快照 + search/export
20
+ class Emulator
21
+ extend Forwardable
22
+
23
+ attr_reader :conn_id, :channel_id, :screen, :buffer, :theme, :logger
24
+
25
+ def initialize(client, conn_id, ch_id, cols, rows, theme: nil)
26
+ @client = client
27
+ @conn_id = conn_id
28
+ @channel_id = ch_id
29
+ @cols = cols
30
+ @rows = rows
31
+ @screen = Screen.new(cols, rows)
32
+ @buffer = Buffer.new(max_lines: 10_000)
33
+ @buffer.attach_screen(@screen)
34
+ @theme = Theme.load(theme || "default")
35
+ @parser = AnsiParser.new(self)
36
+ @logger = nil
37
+ end
38
+
39
+ # 输入从 SSH 收到的原始字节
40
+ def feed(data)
41
+ @parser.feed(data)
42
+ @logger&.write(data)
43
+ end
44
+
45
+ # 发送数据到 SSH 通道
46
+ def send(data)
47
+ @client.ipc.call("channel.send", {
48
+ id: @channel_id,
49
+ data: Base64.strict_encode64(data)
50
+ })
51
+ end
52
+
53
+ # 发送一行(带 \r)
54
+ def puts(text)
55
+ send("#{text}\r")
56
+ end
57
+
58
+ # 窗口大小变更
59
+ def resize(cols, rows)
60
+ @screen.resize(cols, rows)
61
+ @client.ipc.call("channel.window_change", {
62
+ id: @channel_id, cols: cols, rows: rows
63
+ })
64
+ end
65
+
66
+ def_delegators :@screen, :cursor_x, :cursor_y
67
+
68
+ # 设置配色主题(FR-TERM-002)
69
+ def theme=(theme_name)
70
+ @theme = Theme.load(theme_name)
71
+ end
72
+
73
+ # 搜索缓冲区内容(FR-TERM-005, FR-TERM-007)
74
+ def search(pattern)
75
+ @buffer.search(pattern)
76
+ end
77
+
78
+ # 导出缓冲区为文本
79
+ def export(path = nil)
80
+ text = @buffer.to_text
81
+ File.write(path, text) if path
82
+ text
83
+ end
84
+
85
+ # 启动会话日志(FR-TERM-006)
86
+ def start_logging(path, max_size: 100 * 1024 * 1024, rotate: 10)
87
+ @logger = Logger.new(path, max_size: max_size, rotate: rotate)
88
+ end
89
+
90
+ def stop_logging
91
+ @logger&.close
92
+ @logger = nil
93
+ end
94
+
95
+ # ---- AnsiParser 回调 ----
96
+
97
+ def put_char(ch, x = nil, y = nil, style = {})
98
+ @screen.put_char(ch, x || @screen.cursor_x, y || @screen.cursor_y, style)
99
+ end
100
+
101
+ def cursor_to(x, y)
102
+ @screen.cursor_to(x, y)
103
+ end
104
+
105
+ def newline
106
+ @screen.newline
107
+ end
108
+
109
+ def backspace
110
+ @screen.backspace
111
+ end
112
+
113
+ def tab
114
+ @screen.tab
115
+ end
116
+
117
+ def clear_screen(mode = :full)
118
+ @screen.clear(mode)
119
+ end
120
+
121
+ def clear_line(mode = :full)
122
+ @screen.clear_line(mode)
123
+ end
124
+
125
+ def scroll_up(n = 1)
126
+ @screen.scroll_up(n)
127
+ end
128
+
129
+ def scroll_down(n = 1)
130
+ @screen.scroll_down(n)
131
+ end
132
+
133
+ def set_scroll_region(top, bottom)
134
+ @screen.set_scroll_region(top, bottom)
135
+ end
136
+
137
+ def enter_alt_screen
138
+ @screen.enter_alt
139
+ end
140
+
141
+ def exit_alt_screen
142
+ @screen.exit_alt
143
+ end
144
+
145
+ def set_style(style)
146
+ @screen.set_style(style)
147
+ end
148
+
149
+ def reset_style
150
+ @screen.reset_style
151
+ end
152
+
153
+ def bell
154
+ # 响铃回调,由 UI 层处理
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end