rbgl 0.1.0 → 1.0.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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +20 -2
  4. data/README.md +130 -74
  5. data/Rakefile +7 -0
  6. data/bench/renderer_bench.rb +283 -0
  7. data/docs/.nojekyll +1 -0
  8. data/docs/index.html +38 -0
  9. data/examples/array_test.rb +1 -1
  10. data/examples/color_test.rb +1 -1
  11. data/examples/cube_spinning.rb +1 -1
  12. data/examples/gradient.rb +1 -1
  13. data/examples/multi_return_test.rb +1 -1
  14. data/examples/plasma.rb +1 -1
  15. data/examples/sphere_raymarch.rb +1 -1
  16. data/examples/triangle_window.rb +1 -1
  17. data/exe/rbgl +42 -0
  18. data/lib/rbgl/cli/doctor.rb +155 -0
  19. data/lib/rbgl/engine/buffer.rb +150 -40
  20. data/lib/rbgl/engine/clip_space_clipper.rb +163 -0
  21. data/lib/rbgl/engine/context.rb +133 -63
  22. data/lib/rbgl/engine/framebuffer.rb +153 -48
  23. data/lib/rbgl/engine/immutable_color.rb +32 -0
  24. data/lib/rbgl/engine/pipeline.rb +38 -7
  25. data/lib/rbgl/engine/rasterizer/attribute_interpolator.rb +195 -0
  26. data/lib/rbgl/engine/rasterizer/line_renderer.rb +88 -0
  27. data/lib/rbgl/engine/rasterizer/point_renderer.rb +35 -0
  28. data/lib/rbgl/engine/rasterizer/triangle_renderer.rb +136 -0
  29. data/lib/rbgl/engine/rasterizer.rb +65 -171
  30. data/lib/rbgl/engine/shader/base_shader.rb +51 -0
  31. data/lib/rbgl/engine/shader/builtins.rb +257 -0
  32. data/lib/rbgl/engine/shader/dynamic_data.rb +73 -0
  33. data/lib/rbgl/engine/shader.rb +5 -318
  34. data/lib/rbgl/engine/texture.rb +264 -51
  35. data/lib/rbgl/engine.rb +4 -1
  36. data/lib/rbgl/gui/backend.rb +21 -34
  37. data/lib/rbgl/gui/backend_factory.rb +89 -0
  38. data/lib/rbgl/gui/cocoa/backend.rb +59 -46
  39. data/lib/rbgl/gui/cocoa/key_mapper.rb +48 -0
  40. data/lib/rbgl/gui/event.rb +4 -2
  41. data/lib/rbgl/gui/file_backend/bmp_writer.rb +63 -0
  42. data/lib/rbgl/gui/file_backend/frame_writer.rb +35 -0
  43. data/lib/rbgl/gui/file_backend/png_writer.rb +18 -0
  44. data/lib/rbgl/gui/file_backend/ppm_writer.rb +26 -0
  45. data/lib/rbgl/gui/file_backend.rb +50 -55
  46. data/lib/rbgl/gui/wayland/backend.rb +214 -60
  47. data/lib/rbgl/gui/wayland/codec.rb +50 -0
  48. data/lib/rbgl/gui/wayland/connection.rb +188 -231
  49. data/lib/rbgl/gui/wayland/event_dispatcher.rb +165 -0
  50. data/lib/rbgl/gui/wayland/global_binder.rb +46 -0
  51. data/lib/rbgl/gui/wayland/input_mapper.rb +61 -0
  52. data/lib/rbgl/gui/wayland/protocol_objects/arguments.rb +63 -0
  53. data/lib/rbgl/gui/wayland/protocol_objects/display.rb +54 -0
  54. data/lib/rbgl/gui/wayland/protocol_objects/seat.rb +115 -0
  55. data/lib/rbgl/gui/wayland/protocol_objects/shm.rb +157 -0
  56. data/lib/rbgl/gui/wayland/protocol_objects/surface.rb +33 -0
  57. data/lib/rbgl/gui/wayland/protocol_objects/xdg_shell.rb +60 -0
  58. data/lib/rbgl/gui/wayland/protocol_objects.rb +8 -0
  59. data/lib/rbgl/gui/window/event_dispatcher.rb +30 -0
  60. data/lib/rbgl/gui/window/render_loop.rb +111 -0
  61. data/lib/rbgl/gui/window.rb +74 -84
  62. data/lib/rbgl/gui/x11/atom_cache.rb +29 -0
  63. data/lib/rbgl/gui/x11/backend.rb +73 -49
  64. data/lib/rbgl/gui/x11/connection.rb +284 -219
  65. data/lib/rbgl/gui/x11/event_parser.rb +71 -0
  66. data/lib/rbgl/gui/x11/key_mapper.rb +48 -0
  67. data/lib/rbgl/gui/x11/pixel_encoder.rb +164 -0
  68. data/lib/rbgl/gui/x11/request_encoder.rb +155 -0
  69. data/lib/rbgl/gui/x11/setup_parser.rb +138 -0
  70. data/lib/rbgl/gui/x11/transport.rb +81 -0
  71. data/lib/rbgl/gui/x11/x_authority.rb +107 -0
  72. data/lib/rbgl/gui.rb +3 -5
  73. data/lib/rbgl/version.rb +1 -1
  74. metadata +63 -24
  75. data/examples/chemical_heartbeat.rb +0 -166
  76. data/examples/dark_transit.rb +0 -166
  77. data/examples/fractured_orb.rb +0 -428
  78. data/examples/fractured_orb_rb.rb +0 -598
  79. data/examples/hexagonal_flow.rb +0 -333
  80. data/examples/teapot.rb +0 -362
  81. data/examples/teapot_mcu.rb +0 -344
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ module X11
6
+ module KeyMapper
7
+ SPECIAL_KEYSYMS = {
8
+ 0xFF08 => :backspace,
9
+ 0xFF09 => :tab,
10
+ 0xFF0D => :enter,
11
+ 0xFF1B => :escape,
12
+ 0xFF50 => :home,
13
+ 0xFF51 => :left,
14
+ 0xFF52 => :up,
15
+ 0xFF53 => :right,
16
+ 0xFF54 => :down,
17
+ 0xFF55 => :page_up,
18
+ 0xFF56 => :page_down,
19
+ 0xFF57 => :end,
20
+ 0xFF63 => :insert,
21
+ 0xFFFF => :delete,
22
+ 0xFFE1 => :left_shift,
23
+ 0xFFE2 => :right_shift,
24
+ 0xFFE3 => :left_control,
25
+ 0xFFE4 => :right_control,
26
+ 0xFFE7 => :left_meta,
27
+ 0xFFE8 => :right_meta,
28
+ 0xFFE9 => :left_alt,
29
+ 0xFFEA => :right_alt
30
+ }.merge((0xFFBE..0xFFC9).zip((1..12).map { |number| :"f#{number}" }).to_h).freeze
31
+
32
+ DIGIT_NAMES = %i[zero one two three four five six seven eight nine].freeze
33
+
34
+ module_function
35
+
36
+ def key(keysym, fallback:)
37
+ return fallback if keysym.nil? || keysym.zero?
38
+ return SPECIAL_KEYSYMS[keysym] if SPECIAL_KEYSYMS.key?(keysym)
39
+ return :space if keysym == 0x20
40
+ return DIGIT_NAMES[keysym - 0x30] if keysym.between?(0x30, 0x39)
41
+ return keysym.chr.downcase.to_sym if keysym.between?(0x41, 0x5A) || keysym.between?(0x61, 0x7A)
42
+
43
+ fallback
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ module X11
6
+ class PixelEncoder
7
+ TRUE_COLOR_CLASSES = [4, 5].freeze
8
+ IMAGE_BYTE_ORDERS = %i[little big].freeze
9
+
10
+ attr_reader :bytes_per_pixel
11
+
12
+ def initialize(bits_per_pixel:, scanline_pad:, visual_class:, red_mask:, green_mask:, blue_mask:,
13
+ image_byte_order: :little)
14
+ unless TRUE_COLOR_CLASSES.include?(visual_class)
15
+ raise GUI::BackendUnavailable, "X11 root visual must be TrueColor or DirectColor"
16
+ end
17
+ unless [16, 24, 32].include?(bits_per_pixel)
18
+ raise GUI::BackendUnavailable, "Unsupported X11 pixel width: #{bits_per_pixel}"
19
+ end
20
+ unless IMAGE_BYTE_ORDERS.include?(image_byte_order)
21
+ raise GUI::BackendUnavailable, "Unsupported X11 image byte order: #{image_byte_order}"
22
+ end
23
+
24
+ @bytes_per_pixel = bits_per_pixel / 8
25
+ @scanline_alignment = scanline_pad / 8
26
+ @image_byte_order = image_byte_order
27
+ @red_lut = channel_lut(red_mask)
28
+ @green_lut = channel_lut(green_mask)
29
+ @blue_lut = channel_lut(blue_mask)
30
+ @standard_bgra = bits_per_pixel == 32 && image_byte_order == :little &&
31
+ red_mask == 0x00FF0000 && green_mask == 0x0000FF00 && blue_mask == 0x000000FF
32
+ end
33
+
34
+ def bytes_per_line(width)
35
+ aligned_length(width * @bytes_per_pixel)
36
+ end
37
+
38
+ def encode(framebuffer)
39
+ stride = bytes_per_line(framebuffer.width)
40
+ if @standard_bgra
41
+ return pad_rows(framebuffer.to_bgra_bytes, framebuffer.width * 4, stride, framebuffer.height)
42
+ end
43
+
44
+ output = String.new(capacity: stride * framebuffer.height, encoding: Encoding::BINARY)
45
+ row_pixels = 0
46
+
47
+ framebuffer.each_packed_pixel do |color|
48
+ append_pixel(
49
+ output,
50
+ @red_lut[(color >> 16) & 0xFF] |
51
+ @green_lut[(color >> 8) & 0xFF] |
52
+ @blue_lut[color & 0xFF]
53
+ )
54
+ row_pixels += 1
55
+ next unless row_pixels == framebuffer.width
56
+
57
+ append_padding(output, stride - (framebuffer.width * @bytes_per_pixel))
58
+ row_pixels = 0
59
+ end
60
+
61
+ output
62
+ end
63
+
64
+ def encode_rgba(buffer, width, height)
65
+ if @standard_bgra
66
+ output = String.new(capacity: bytes_per_line(width) * height, encoding: Encoding::BINARY)
67
+ encode_standard_rgba(output, buffer, width, height)
68
+ return output
69
+ end
70
+
71
+ encode_arbitrary_rgba(buffer, width, height)
72
+ end
73
+
74
+ private
75
+
76
+ def encode_standard_rgba(output, buffer, width, height)
77
+ stride = bytes_per_line(width)
78
+ row_bytes = width * 4
79
+ byte_index = 0
80
+
81
+ height.times do
82
+ width.times do
83
+ output << buffer.getbyte(byte_index + 2) << buffer.getbyte(byte_index + 1) <<
84
+ buffer.getbyte(byte_index) << buffer.getbyte(byte_index + 3)
85
+ byte_index += 4
86
+ end
87
+ append_padding(output, stride - row_bytes)
88
+ end
89
+ end
90
+
91
+ def encode_arbitrary_rgba(buffer, width, height)
92
+ stride = bytes_per_line(width)
93
+ output = String.new(capacity: stride * height, encoding: Encoding::BINARY)
94
+ byte_index = 0
95
+
96
+ height.times do
97
+ width.times do
98
+ append_pixel(
99
+ output,
100
+ @red_lut[buffer.getbyte(byte_index)] |
101
+ @green_lut[buffer.getbyte(byte_index + 1)] |
102
+ @blue_lut[buffer.getbyte(byte_index + 2)]
103
+ )
104
+ byte_index += 4
105
+ end
106
+ append_padding(output, stride - (width * @bytes_per_pixel))
107
+ end
108
+
109
+ output
110
+ end
111
+
112
+ def append_pixel(output, pixel)
113
+ if @image_byte_order == :little
114
+ index = 0
115
+ while index < @bytes_per_pixel
116
+ output << ((pixel >> (index * 8)) & 0xFF)
117
+ index += 1
118
+ end
119
+ else
120
+ index = @bytes_per_pixel - 1
121
+ while index >= 0
122
+ output << ((pixel >> (index * 8)) & 0xFF)
123
+ index -= 1
124
+ end
125
+ end
126
+ end
127
+
128
+ def append_padding(output, length)
129
+ output << ("\x00" * length) if length.positive?
130
+ end
131
+
132
+ def channel_lut(mask)
133
+ raise GUI::BackendUnavailable, "X11 visual has an empty color mask" if mask.zero?
134
+
135
+ shift = 0
136
+ shifted = mask
137
+ while shifted & 1 == 0
138
+ shifted >>= 1
139
+ shift += 1
140
+ end
141
+ maximum = shifted
142
+ Array.new(256) { |value| ((value * maximum / 255.0).round << shift) }.freeze
143
+ end
144
+
145
+ def pad_rows(bytes, row_bytes, stride, height)
146
+ return bytes if row_bytes == stride
147
+
148
+ output = String.new(capacity: stride * height, encoding: Encoding::BINARY)
149
+ height.times do |row|
150
+ output << bytes.byteslice(row * row_bytes, row_bytes)
151
+ append_padding(output, stride - row_bytes)
152
+ end
153
+ output
154
+ end
155
+
156
+ def aligned_length(length)
157
+ return length if @scanline_alignment <= 1
158
+
159
+ ((length + @scanline_alignment - 1) / @scanline_alignment) * @scanline_alignment
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ module X11
6
+ class RequestEncoder
7
+ EVENT_MASKS = {
8
+ exposure: 0x8000,
9
+ key_press: 0x0001,
10
+ key_release: 0x0002,
11
+ button_press: 0x0004,
12
+ button_release: 0x0008,
13
+ pointer_motion: 0x0040,
14
+ structure_notify: 0x020000
15
+ }.freeze
16
+
17
+ WINDOW_CLASSES = {
18
+ input_output: 1,
19
+ input_only: 2
20
+ }.freeze
21
+
22
+ PROPERTY_MODES = {
23
+ replace: 0,
24
+ prepend: 1,
25
+ append: 2
26
+ }.freeze
27
+
28
+ IMAGE_FORMATS = {
29
+ bitmap: 0,
30
+ xy_pixmap: 1,
31
+ z_pixmap: 2
32
+ }.freeze
33
+
34
+ def create_window_data(depth:, wid:, parent:, x:, y:, width:, height:,
35
+ border_width:, window_class:, visual:, value_mask:, values:)
36
+ mask = 0
37
+ value_list = []
38
+
39
+ if value_mask.include?(:back_pixel)
40
+ mask |= 0x0002
41
+ value_list << values[:back_pixel]
42
+ end
43
+
44
+ if value_mask.include?(:event_mask)
45
+ mask |= 0x0800
46
+ value_list << encode_event_mask(values[:event_mask])
47
+ end
48
+
49
+ data = [
50
+ wid,
51
+ parent,
52
+ x, y,
53
+ width, height,
54
+ border_width,
55
+ WINDOW_CLASSES.fetch(window_class),
56
+ visual,
57
+ mask
58
+ ].pack("VVs<s<vvvvVV") + value_list.pack("V*")
59
+
60
+ [depth, data]
61
+ end
62
+
63
+ def create_gc_data(gc_id, drawable, values = {})
64
+ mask = 0
65
+ value_list = []
66
+
67
+ if values[:foreground]
68
+ mask |= 0x0004
69
+ value_list << values[:foreground]
70
+ end
71
+
72
+ if values[:background]
73
+ mask |= 0x0008
74
+ value_list << values[:background]
75
+ end
76
+
77
+ [gc_id, drawable, mask].pack("VVV") + value_list.pack("V*")
78
+ end
79
+
80
+ def put_image_data(format:, drawable:, gc:, width:, height:, dst_x:, dst_y:, depth:, data:)
81
+ format_byte = IMAGE_FORMATS.fetch(format)
82
+ header = [
83
+ drawable,
84
+ gc,
85
+ width, height,
86
+ dst_x, dst_y,
87
+ 0,
88
+ depth
89
+ ].pack("VVvvs<s<CC") + "\x00\x00"
90
+
91
+ [format_byte, header, data]
92
+ end
93
+
94
+ def change_property_data(window, property_atom, type_atom, data, mode: :replace, format: 8)
95
+ data_bytes, value_count = pack_property_data(data, format)
96
+ request = [window, property_atom, type_atom, format].pack("VVVC") +
97
+ "\x00\x00\x00" +
98
+ [value_count].pack("V") +
99
+ pad_to_4(data_bytes)
100
+
101
+ [PROPERTY_MODES.fetch(mode), request]
102
+ end
103
+
104
+ def intern_atom_data(name)
105
+ [name.bytesize, 0].pack("vv") + pad_to_4(name)
106
+ end
107
+
108
+ def request_packet(opcode, data, extra = 0)
109
+ encode_packet(opcode, extra, data)
110
+ end
111
+
112
+ def request_packet_with_data(opcode, extra, header_data, bulk_data)
113
+ encode_packet(opcode, extra, header_data + bulk_data)
114
+ end
115
+
116
+ def pack_property_data(data, format)
117
+ case format
118
+ when 8
119
+ bytes = data.to_s.b
120
+ [bytes, bytes.bytesize]
121
+ when 16
122
+ values = Array(data)
123
+ [values.pack("v*"), values.size]
124
+ when 32
125
+ values = Array(data)
126
+ [values.pack("V*"), values.size]
127
+ else
128
+ raise ArgumentError, "Unsupported property format: #{format}"
129
+ end
130
+ end
131
+
132
+ def pad_to_4(str)
133
+ bytes = str.b
134
+ padding = (4 - (bytes.bytesize % 4)) % 4
135
+ bytes + ("\x00" * padding)
136
+ end
137
+
138
+ private
139
+
140
+ def encode_packet(opcode, extra, data)
141
+ length = (4 + data.bytesize + 3) / 4
142
+ raise ArgumentError, "X11 request exceeds the 16-bit core protocol limit" if length > 65_535
143
+
144
+ header = [opcode, extra, length].pack("CCv")
145
+ padding = "\x00" * ((length * 4) - 4 - data.bytesize)
146
+ header + data + padding
147
+ end
148
+
149
+ def encode_event_mask(events)
150
+ Array(events).sum { |event| EVENT_MASKS.fetch(event) }
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBGL
4
+ module GUI
5
+ module X11
6
+ class SetupParser
7
+ Setup = Struct.new(
8
+ :resource_id_base,
9
+ :resource_id_mask,
10
+ :maximum_request_length,
11
+ :image_byte_order,
12
+ :minimum_keycode,
13
+ :maximum_keycode,
14
+ :pixmap_formats,
15
+ :screens,
16
+ keyword_init: true
17
+ )
18
+ PixmapFormat = Struct.new(:depth, :bits_per_pixel, :scanline_pad, keyword_init: true)
19
+ Depth = Struct.new(:depth, :visuals, keyword_init: true)
20
+ Screen = Struct.new(
21
+ :root,
22
+ :white_pixel,
23
+ :black_pixel,
24
+ :root_visual,
25
+ :root_depth,
26
+ :depths,
27
+ keyword_init: true
28
+ ) do
29
+ def visuals
30
+ depths.flat_map(&:visuals)
31
+ end
32
+ end
33
+ Visual = Struct.new(:id, :visual_class, :red_mask, :green_mask, :blue_mask, keyword_init: true)
34
+
35
+ def parse(data)
36
+ raise ArgumentError, "Truncated X11 setup response" if data.bytesize < 32
37
+
38
+ vendor_length = data.byteslice(16, 2).unpack1("v")
39
+ screen_count = data.getbyte(20)
40
+ format_count = data.getbyte(21)
41
+ format_offset = 32 + padded_length(vendor_length)
42
+ formats = parse_formats(data, format_offset, format_count)
43
+ screens = parse_screens(data, format_offset + (format_count * 8), screen_count)
44
+
45
+ Setup.new(
46
+ resource_id_base: data.byteslice(4, 4).unpack1("V"),
47
+ resource_id_mask: data.byteslice(8, 4).unpack1("V"),
48
+ maximum_request_length: data.byteslice(18, 2).unpack1("v"),
49
+ image_byte_order: parse_image_byte_order(data.getbyte(22)),
50
+ minimum_keycode: data.getbyte(26),
51
+ maximum_keycode: data.getbyte(27),
52
+ pixmap_formats: formats,
53
+ screens: screens
54
+ )
55
+ end
56
+
57
+ private
58
+
59
+ def parse_formats(data, offset, count)
60
+ count.times.map do |index|
61
+ bytes = required_slice(data, offset + (index * 8), 8)
62
+ PixmapFormat.new(
63
+ depth: bytes.getbyte(0),
64
+ bits_per_pixel: bytes.getbyte(1),
65
+ scanline_pad: bytes.getbyte(2)
66
+ )
67
+ end
68
+ end
69
+
70
+ def parse_screens(data, offset, count)
71
+ screens = []
72
+ count.times do
73
+ screen, offset = parse_screen(data, offset)
74
+ screens << screen
75
+ end
76
+ screens
77
+ end
78
+
79
+ def parse_screen(data, offset)
80
+ header = required_slice(data, offset, 40)
81
+ depth_count = header.getbyte(39)
82
+ cursor = offset + 40
83
+ depths = []
84
+
85
+ depth_count.times do
86
+ depth_header = required_slice(data, cursor, 8)
87
+ depth_value = depth_header.getbyte(0)
88
+ visual_count = depth_header.byteslice(2, 2).unpack1("v")
89
+ cursor += 8
90
+ visuals = []
91
+ visual_count.times do
92
+ visuals << parse_visual(required_slice(data, cursor, 24))
93
+ cursor += 24
94
+ end
95
+ depths << Depth.new(depth: depth_value, visuals: visuals)
96
+ end
97
+
98
+ screen = Screen.new(
99
+ root: header.byteslice(0, 4).unpack1("V"),
100
+ white_pixel: header.byteslice(8, 4).unpack1("V"),
101
+ black_pixel: header.byteslice(12, 4).unpack1("V"),
102
+ root_visual: header.byteslice(32, 4).unpack1("V"),
103
+ root_depth: header.getbyte(38),
104
+ depths: depths
105
+ )
106
+ [screen, cursor]
107
+ end
108
+
109
+ def parse_visual(bytes)
110
+ Visual.new(
111
+ id: bytes.byteslice(0, 4).unpack1("V"),
112
+ visual_class: bytes.getbyte(4),
113
+ red_mask: bytes.byteslice(8, 4).unpack1("V"),
114
+ green_mask: bytes.byteslice(12, 4).unpack1("V"),
115
+ blue_mask: bytes.byteslice(16, 4).unpack1("V")
116
+ )
117
+ end
118
+
119
+ def required_slice(data, offset, length)
120
+ bytes = data.byteslice(offset, length)
121
+ raise ArgumentError, "Truncated X11 setup response" unless bytes&.bytesize == length
122
+
123
+ bytes
124
+ end
125
+
126
+ def padded_length(length)
127
+ (length + 3) & ~3
128
+ end
129
+
130
+ def parse_image_byte_order(value)
131
+ { 0 => :little, 1 => :big }.fetch(value) do
132
+ raise ArgumentError, "Unsupported X11 image byte order: #{value}"
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "io/wait"
4
+
5
+ module RBGL
6
+ module GUI
7
+ module X11
8
+ class Transport
9
+ READ_CHUNK_SIZE = 16_384
10
+
11
+ attr_reader :socket
12
+
13
+ def initialize(socket)
14
+ @socket = socket
15
+ @read_buffer = String.new(encoding: Encoding::BINARY)
16
+ end
17
+
18
+ def write(data)
19
+ @socket.write(data)
20
+ end
21
+
22
+ def read(length)
23
+ read_exact(length)
24
+ end
25
+
26
+ def read_exact(length, timeout: nil)
27
+ fill_buffer(length, timeout: timeout)
28
+ @read_buffer.slice!(0, length)
29
+ end
30
+
31
+ def flush
32
+ @socket.flush
33
+ end
34
+
35
+ def pending
36
+ read_available
37
+ return 0 if @read_buffer.bytesize < 32
38
+
39
+ packet_size = 32
40
+ if @read_buffer.getbyte(0) == 1
41
+ packet_size += @read_buffer.byteslice(4, 4).unpack1("V") * 4
42
+ end
43
+ @read_buffer.bytesize >= packet_size ? 1 : 0
44
+ end
45
+
46
+ def close
47
+ @socket.close unless @socket.closed?
48
+ end
49
+
50
+ private
51
+
52
+ def fill_buffer(length, timeout:)
53
+ deadline = timeout && (monotonic_time + timeout)
54
+
55
+ while @read_buffer.bytesize < length
56
+ wait = deadline && [deadline - monotonic_time, 0].max
57
+ raise IO::EAGAINWaitReadable unless @socket.wait_readable(wait)
58
+
59
+ read_available
60
+ end
61
+ end
62
+
63
+ def read_available
64
+ return unless @socket.wait_readable(0)
65
+
66
+ loop do
67
+ chunk = @socket.read_nonblock(READ_CHUNK_SIZE, exception: false)
68
+ break if chunk == :wait_readable
69
+ raise EOFError, "X11 server closed the connection" if chunk.nil?
70
+
71
+ @read_buffer << chunk
72
+ end
73
+ end
74
+
75
+ def monotonic_time
76
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "ipaddr"
5
+
6
+ module RBGL
7
+ module GUI
8
+ module X11
9
+ class XAuthority
10
+ FAMILY_INTERNET = 0
11
+ FAMILY_INTERNET6 = 6
12
+ FAMILY_LOCAL = 256
13
+ FAMILY_WILD = 65_535
14
+ AUTH_NAME = "MIT-MAGIC-COOKIE-1"
15
+
16
+ Entry = Struct.new(:family, :address, :display_number, :name, :data, keyword_init: true)
17
+
18
+ def self.cookie_for(host:, display_number:, env: ENV)
19
+ path = env["XAUTHORITY"] || default_path(env)
20
+ return nil unless path && File.file?(path)
21
+
22
+ new(File.binread(path)).cookie_for(host: host, display_number: display_number)
23
+ rescue ArgumentError, EOFError
24
+ nil
25
+ end
26
+
27
+ def self.default_path(env)
28
+ home = env["HOME"] || Dir.home
29
+ File.join(home, ".Xauthority")
30
+ rescue ArgumentError
31
+ nil
32
+ end
33
+
34
+ def initialize(data)
35
+ @entries = parse(data)
36
+ end
37
+
38
+ def cookie_for(host:, display_number:)
39
+ candidates = @entries.select do |entry|
40
+ (entry.display_number.empty? || entry.display_number == display_number.to_s) && entry.name == AUTH_NAME
41
+ end
42
+ candidates.find { |entry| address_matches?(entry, host) }&.data
43
+ end
44
+
45
+ private
46
+
47
+ def parse(data)
48
+ offset = 0
49
+ entries = []
50
+
51
+ while offset < data.bytesize
52
+ family, offset = read_u16(data, offset)
53
+ address, offset = read_field(data, offset)
54
+ display_number, offset = read_field(data, offset)
55
+ name, offset = read_field(data, offset)
56
+ cookie, offset = read_field(data, offset)
57
+ entries << Entry.new(
58
+ family: family,
59
+ address: address,
60
+ display_number: display_number,
61
+ name: name,
62
+ data: cookie
63
+ )
64
+ end
65
+
66
+ entries
67
+ end
68
+
69
+ def read_u16(data, offset)
70
+ bytes = data.byteslice(offset, 2)
71
+ raise EOFError, "Truncated Xauthority entry" unless bytes&.bytesize == 2
72
+
73
+ [bytes.unpack1("n"), offset + 2]
74
+ end
75
+
76
+ def read_field(data, offset)
77
+ length, offset = read_u16(data, offset)
78
+ value = data.byteslice(offset, length)
79
+ raise EOFError, "Truncated Xauthority field" unless value&.bytesize == length
80
+
81
+ [value, offset + length]
82
+ end
83
+
84
+ def address_matches?(entry, host)
85
+ return true if entry.family == FAMILY_WILD
86
+ return local_address_matches?(entry.address) if host.nil? && entry.family == FAMILY_LOCAL
87
+ return false unless [FAMILY_INTERNET, FAMILY_INTERNET6].include?(entry.family)
88
+
89
+ family = entry.family == FAMILY_INTERNET ? :INET : :INET6
90
+ Addrinfo.getaddrinfo(host, nil, family).any? do |address|
91
+ IPAddr.new(address.ip_address).hton == entry.address
92
+ end
93
+ rescue SocketError
94
+ false
95
+ end
96
+
97
+ def local_address_matches?(address)
98
+ return true if address.empty?
99
+
100
+ expected = address.downcase
101
+ actual = Socket.gethostname.downcase
102
+ expected == actual || expected.split(".", 2).first == actual.split(".", 2).first
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end