protocol-htty 0.2.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba295c47e02345ba7101c3958786fc21fd7187294abe6fef0f39a6677baf9be3
4
- data.tar.gz: 2fcb179db305375b86b5196ed149c0ef1eed4fb52c7d06447b0a5e68d4dd16a4
3
+ metadata.gz: 957aa78a655ceb3136db0acd1614010a25d1cda14b6e0861e6ebfb371870b739
4
+ data.tar.gz: d70de3f0375aef142f90beca94c2bd50f9986bbc6bfaf2920ea64dae13185467
5
5
  SHA512:
6
- metadata.gz: 4132b7c507a0b285f772c91bca4ddbcb315d5faccfeafd28c6907df4553ad5a20190299e9d924c75a946819e656205b603bcae69b6bc61d1e20b08f5e437f60f
7
- data.tar.gz: 3149badd26c592217168140c69b654190fdad74d6884b9ef4e729ff9e8a1f4b342f284bec1b1759698d53a15c4c11858e666654d6b7de0cc47e07f44854d45bd
6
+ metadata.gz: cdc24469f4d8bad151261849af0a4bd85b5ca86d2f6f007404e131cf90ab18f6fd279a82634a46565f3ffa19337c698741103c64220b7c89a8c1261d44652eea
7
+ data.tar.gz: 750995dc4c72f1eb9026bbba25795c8b5489d734d48e93edae8cabad4af12b4bc6a69746550e044f10ceaac2eab3f15fc6e2dddb08a919eccbc953c12c833ef3
checksums.yaml.gz.sig CHANGED
Binary file
@@ -3,8 +3,6 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2026, by Samuel Williams.
5
5
 
6
- require "io/stream"
7
-
8
6
  module Protocol
9
7
  module HTTY
10
8
  # Transport an opaque byte stream after the HTTY bootstrap handshake.
@@ -15,8 +13,12 @@ module Protocol
15
13
  BOOTSTRAP_PREFIX = "+H"
16
14
  RAW_MODE = "raw"
17
15
 
18
- def self.open(stream, bootstrap: nil, mode: RAW_MODE)
19
- stream = self.new(::IO::Stream(stream))
16
+ def self.open(input, output, bootstrap: nil, mode: RAW_MODE)
17
+ stream = self.new(input, output)
18
+
19
+ # Disable buffering:
20
+ input.sync = true
21
+ output.sync = true
20
22
 
21
23
  case bootstrap
22
24
  when :write
@@ -32,23 +34,26 @@ module Protocol
32
34
  return stream
33
35
  end
34
36
 
35
- # Create a stream on top of a raw byte-preserving transport.
36
- # @parameter stream [IO::Stream] The duplex byte stream used after bootstrap.
37
- def initialize(stream)
38
- @stream = stream
37
+ # Create a stream on top of raw byte-preserving endpoints.
38
+ # @parameter input [IO] The readable endpoint.
39
+ # @parameter output [IO] The writable endpoint.
40
+ def initialize(input, output)
41
+ @input = input
42
+ @output = output
39
43
  @local_closed = false
40
44
  end
41
45
 
42
- attr :stream
46
+ attr :input
47
+ attr :output
43
48
 
44
- # Return the underlying duplex stream.
49
+ # Return the underlying output stream.
45
50
  def io
46
- @stream
51
+ @output
47
52
  end
48
53
 
49
54
  def write_bootstrap(mode = RAW_MODE)
50
- @stream.write("#{DCS}#{BOOTSTRAP_PREFIX}#{mode}#{ST}")
51
- @stream.flush
55
+ @output.write("#{DCS}#{BOOTSTRAP_PREFIX}#{mode}#{ST}")
56
+ @output.flush
52
57
  end
53
58
 
54
59
  def read_bootstrap
@@ -67,18 +72,19 @@ module Protocol
67
72
  end
68
73
 
69
74
  # Read application bytes from the HTTY transport.
75
+ # The HTTP/2 framer always requests exact byte counts (header size, then payload length), so we delegate directly to the underlying input.
70
76
  def read(length = nil)
71
- return +"".b if length == 0
72
- return @stream.read(length)
77
+ @input.read(length)
73
78
  end
74
79
 
75
80
  # Write application bytes after bootstrap.
76
81
  # @returns [self]
77
82
  # @raises [IOError] If the local side of the transport is closed.
78
- def write(data)
83
+ def write(data, flush: false)
79
84
  raise IOError, "HTTY stream is closed for writing!" if @local_closed
80
85
 
81
- @stream.write(data.to_s.b)
86
+ @output.write(data.to_s.b)
87
+ @output.flush if flush
82
88
 
83
89
  return self
84
90
  end
@@ -86,7 +92,7 @@ module Protocol
86
92
  # Flush any buffered output through the underlying stream.
87
93
  # @returns [void]
88
94
  def flush
89
- @stream.flush
95
+ @output.flush
90
96
  end
91
97
 
92
98
  # Close the local write side of this stream abstraction.
@@ -95,7 +101,7 @@ module Protocol
95
101
  def close_write(error = nil)
96
102
  unless @local_closed
97
103
  @local_closed = true
98
- @stream.flush
104
+ @output.flush
99
105
  end
100
106
  end
101
107
 
@@ -110,16 +116,22 @@ module Protocol
110
116
  # Check whether the remote side may still provide more data.
111
117
  # @returns [bool] True if the remote side has not sent or implied a close.
112
118
  def readable?
113
- !@stream.closed?
119
+ !(@input.respond_to?(:closed?) && @input.closed?)
114
120
  end
115
121
 
116
122
  private
117
123
 
124
+ def read_some(length)
125
+ @input.read(length)
126
+ rescue EOFError, Errno::EIO
127
+ return nil
128
+ end
129
+
118
130
  def read_payload
119
- while prefix = @stream.read(1)
131
+ while prefix = read_some(1)
120
132
  next unless prefix == ESC
121
133
 
122
- marker = @stream.read(1)
134
+ marker = read_some(1)
123
135
  return nil unless marker
124
136
  next unless marker == "P"
125
137
 
@@ -130,11 +142,11 @@ module Protocol
130
142
  end
131
143
 
132
144
  def consume_packet
133
- buffer = +""
145
+ buffer = String.new.b
134
146
 
135
- while chunk = @stream.read(1)
147
+ while chunk = read_some(1)
136
148
  if chunk == ESC
137
- terminator = @stream.read(1)
149
+ terminator = read_some(1)
138
150
  return buffer if terminator == "\\"
139
151
 
140
152
  buffer << chunk
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module HTTY
8
- VERSION = "0.2.0"
8
+ VERSION = "0.4.0"
9
9
  end
10
10
  end
data/readme.md CHANGED
@@ -36,6 +36,16 @@ Please see the [project documentation](https://socketry.github.io/protocol-htty/
36
36
 
37
37
  Please see the [project releases](https://socketry.github.io/protocol-htty/releases/index) for all releases.
38
38
 
39
+ ### v0.4.0
40
+
41
+ - **Breaking**: Drop the `io-stream` dependency. `Stream.new` and `Stream.open` no longer coerce or wrap their `input` and `output` arguments; raw IO objects are used as-is. Callers that relied on `stream.io` returning an `IO::Stream::Buffered` should wrap the IO themselves before passing it in.
42
+ - Simplify `Stream#read` to delegate directly to the underlying input, since the HTTP/2 framer always requests exact byte counts (header size, then payload length).
43
+
44
+ ### v0.3.0
45
+
46
+ - Change `Protocol::HTTY::Stream` to take explicit input and output endpoints using `Stream.new(input, output)` and `Stream.open(input, output, **options)`.
47
+ - Read HTTY input without buffering ahead, preserving HTTP/2 frame boundaries by reading only the announced frame payload length.
48
+
39
49
  ### v0.2.0
40
50
 
41
51
  - Add `Protocol::HTTY::Stream.open(stream, **options)` as the preferred constructor for bootstrapping HTTY over an existing stream.
data/releases.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Releases
2
2
 
3
+ ## v0.4.0
4
+
5
+ - **Breaking**: Drop the `io-stream` dependency. `Stream.new` and `Stream.open` no longer coerce or wrap their `input` and `output` arguments; raw IO objects are used as-is. Callers that relied on `stream.io` returning an `IO::Stream::Buffered` should wrap the IO themselves before passing it in.
6
+ - Simplify `Stream#read` to delegate directly to the underlying input, since the HTTP/2 framer always requests exact byte counts (header size, then payload length).
7
+
8
+ ## v0.3.0
9
+
10
+ - Change `Protocol::HTTY::Stream` to take explicit input and output endpoints using `Stream.new(input, output)` and `Stream.open(input, output, **options)`.
11
+ - Read HTTY input without buffering ahead, preserving HTTP/2 frame boundaries by reading only the announced frame payload length.
12
+
3
13
  ## v0.2.0
4
14
 
5
15
  - Add `Protocol::HTTY::Stream.open(stream, **options)` as the preferred constructor for bootstrapping HTTY over an existing stream.
@@ -26,8 +26,7 @@ end
26
26
  describe Protocol::HTTY::Stream do
27
27
  let(:input) {StringIO.new}
28
28
  let(:output) {StringIO.new}
29
- let(:io) {IO::Stream::Duplex(input, output)}
30
- let(:stream) {subject.new(io)}
29
+ let(:stream) {subject.new(input, output)}
31
30
 
32
31
  it "writes the HTTY raw bootstrap" do
33
32
  stream.write_bootstrap
@@ -45,7 +44,7 @@ describe Protocol::HTTY::Stream do
45
44
  end
46
45
 
47
46
  it "reads a bootstrap split across one-byte reads" do
48
- stream = subject.new(OneByteInput.new("hello\eP+Hraw\e\\"))
47
+ stream = subject.new(OneByteInput.new("hello\eP+Hraw\e\\"), StringIO.new)
49
48
 
50
49
  expect(stream.read_bootstrap).to be == "raw"
51
50
  end
@@ -56,7 +55,7 @@ describe Protocol::HTTY::Stream do
56
55
 
57
56
  stream.read_bootstrap
58
57
 
59
- expect(io.read(5)).to be == "world"
58
+ expect(stream.read(5)).to be == "world"
60
59
  end
61
60
 
62
61
  it "raises on unsupported bootstrap modes" do
@@ -20,6 +20,9 @@ class PTYStream
20
20
  @buffer = +"".b
21
21
  end
22
22
 
23
+ attr :input
24
+ attr :output
25
+
23
26
  def read(length)
24
27
  while @buffer.bytesize < length
25
28
  @buffer << @input.readpartial(4096).b
@@ -104,7 +107,7 @@ describe "HTTY over a real PTY" do
104
107
  it "ignores terminal noise before the bootstrap" do
105
108
  Timeout.timeout(5) do
106
109
  with_fixture("bootstrap") do |stream|
107
- framer = Protocol::HTTY::Stream.new(stream)
110
+ framer = Protocol::HTTY::Stream.new(stream.input, stream.output)
108
111
 
109
112
  expect(framer.read_bootstrap).to be == "raw"
110
113
  expect(stream.read(3)).to be == "RAW"
@@ -115,7 +118,7 @@ describe "HTTY over a real PTY" do
115
118
  it "delivers the HTTP/2 connection preface after raw takeover" do
116
119
  Timeout.timeout(5) do
117
120
  with_fixture("raw_preface") do |stream|
118
- framer = Protocol::HTTY::Stream.new(stream)
121
+ framer = Protocol::HTTY::Stream.new(stream.input, stream.output)
119
122
 
120
123
  expect(framer.read_bootstrap).to be == "raw"
121
124
 
@@ -130,7 +133,8 @@ describe "HTTY over a real PTY" do
130
133
  it "runs an HTTP/2 session until command-side GOAWAY" do
131
134
  Timeout.timeout(5) do
132
135
  with_fixture("http2_server") do |stream|
133
- Protocol::HTTY::Stream.new(stream).read_bootstrap
136
+ stream = Protocol::HTTY::Stream.new(stream.input, stream.output)
137
+ stream.read_bootstrap
134
138
 
135
139
  framer = Protocol::HTTP2::Framer.new(stream)
136
140
  client = Client.new(framer)
@@ -162,7 +166,8 @@ describe "HTTY over a real PTY" do
162
166
  it "treats command exit after bootstrap without GOAWAY as an abort" do
163
167
  Timeout.timeout(5) do
164
168
  with_fixture("abort_after_bootstrap") do |stream|
165
- expect(Protocol::HTTY::Stream.new(stream).read_bootstrap).to be == "raw"
169
+ stream = Protocol::HTTY::Stream.new(stream.input, stream.output)
170
+ expect(stream.read_bootstrap).to be == "raw"
166
171
 
167
172
  framer = Protocol::HTTP2::Framer.new(stream)
168
173
 
@@ -4,13 +4,12 @@
4
4
  # Copyright, 2026, by Samuel Williams.
5
5
 
6
6
  require "stringio"
7
- require "tempfile"
8
7
  require "protocol/http2/framer"
9
8
  require "protocol/htty"
10
9
 
11
10
  describe Protocol::HTTY::Stream do
12
11
  let(:writer) {StringIO.new}
13
- let(:stream) {subject.open(IO::Stream::Duplex(StringIO.new, writer))}
12
+ let(:stream) {subject.open(StringIO.new, writer)}
14
13
 
15
14
  it "writes raw bytes after bootstrap" do
16
15
  stream.write_bootstrap
@@ -24,7 +23,7 @@ describe Protocol::HTTY::Stream do
24
23
  writer.write("\eP+Hraw\e\\#{Protocol::HTTP2::CONNECTION_PREFACE}")
25
24
  writer.rewind
26
25
 
27
- reader = subject.open(IO::Stream::Duplex(writer, StringIO.new), bootstrap: :read)
26
+ reader = subject.open(writer, StringIO.new, bootstrap: :read)
28
27
 
29
28
  expect(reader.read(Protocol::HTTP2::CONNECTION_PREFACE.bytesize)).to be == Protocol::HTTP2::CONNECTION_PREFACE
30
29
  expect(reader.read).to be == ""
@@ -32,7 +31,7 @@ describe Protocol::HTTY::Stream do
32
31
 
33
32
  it "writes the bootstrap when opened in write mode" do
34
33
  writer = StringIO.new
35
- stream = subject.open(IO::Stream::Duplex(StringIO.new, writer), bootstrap: :write)
34
+ stream = subject.open(StringIO.new, writer, bootstrap: :write)
36
35
 
37
36
  expect(writer.string).to be == "\eP+Hraw\e\\"
38
37
  stream.close
@@ -42,14 +41,14 @@ describe Protocol::HTTY::Stream do
42
41
  writer.write("hello")
43
42
  writer.rewind
44
43
 
45
- reader = subject.open(IO::Stream::Duplex(writer, StringIO.new))
44
+ reader = subject.open(writer, StringIO.new)
46
45
 
47
46
  expect(reader.read).to be == "hello"
48
47
  expect(reader.read).to be == ""
49
48
  end
50
49
 
51
50
  it "exposes the underlying output stream" do
52
- expect(stream.io).to be(:is_a?, ::IO::Stream::Buffered)
51
+ expect(stream.io).to be(:equal?, writer)
53
52
  end
54
53
 
55
54
  it "flushes through the underlying stream" do
@@ -93,24 +92,4 @@ describe Protocol::HTTY::Stream do
93
92
  stream.write("hello")
94
93
  end.to raise_exception(IOError)
95
94
  end
96
-
97
- it "wraps raw IO handles using IO::Stream" do
98
- Tempfile.create("protocol-htty") do |file|
99
- io_stream = subject.open(file).io
100
-
101
- expect(io_stream).to be(:is_a?, ::IO::Stream::Buffered)
102
- io_stream.close
103
- end
104
- end
105
-
106
- it "does not close wrapped raw IO handles when closed" do
107
- Tempfile.create("protocol-htty") do |file|
108
- wrapped_stream = subject.open(file)
109
-
110
- wrapped_stream.close
111
-
112
- expect(file).not.to be(:closed?)
113
- file.close
114
- end
115
- end
116
95
  end
data.tar.gz.sig CHANGED
@@ -1,2 +1,3 @@
1
-
2
- �U�:� �k�آ�$���拡 ��x.��ٞ���TF�e#v�ev>���!3�~��TCF2m,�{���0�́jp}��ns�����0lOܢt��姸��84\�:P4[m���p/2~?�":���b�ҷD�7q��[m�\����N���-#��� ������T[|�鈟\��ɱQ�G��$��Dtfv|�1UO
1
+ �g;��M�o��(� Y� ���Fl��iHs��,E(B�#Ȱ�C���ju�Ύ�o�K-��C�w�CUU����5S��vZET�������#��`����5q�D���!7�­Z;G�֘�Y,#D����3P��r�0�D���J"В w
2
+ ��f�"����irʅNyBZҸ�@F] 9?���6_;�є5�"�&���W�D���xk�Oݭ�g��:(�C��-� B��غ]luc�Ӟ��xt����
3
+ -3G� ��R^xƹ/��*�ƥ��"���3���o�|��ܗL��(�\
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-htty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -52,20 +52,6 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: io-stream
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: 0.13.0
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: 0.13.0
69
55
  executables: []
70
56
  extensions: []
71
57
  extra_rdoc_files: []
metadata.gz.sig CHANGED
Binary file