eui-ruby 0.1.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.
@@ -0,0 +1,385 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'limits'
4
+ require_relative 'style'
5
+
6
+ module EUI
7
+ module Proto
8
+ # The closed set of primitive node kinds (`spec/02-wire-format.md` §4.1).
9
+ #
10
+ # Everything a person would call a widget — button, dialog, table, date
11
+ # picker — is composed from these on the server, which is why the
12
+ # catalogue grows without shipping a new client.
13
+ module NodeKind
14
+ BY_NAME = {
15
+ 'box' => 0x01, 'text' => 0x02, 'image' => 0x03, 'icon' => 0x04,
16
+ 'input' => 0x05, 'textarea' => 0x06, 'scroll' => 0x07, 'list' => 0x08,
17
+ 'canvas' => 0x09, 'spacer' => 0x0A, 'divider' => 0x0B, 'overlay' => 0x0C,
18
+ 'slot' => 0x0D, 'sizer' => 0x0E, 'audio' => 0x0F, 'video' => 0x10,
19
+ 'scene' => 0x11
20
+ }.freeze
21
+ BY_CODE = BY_NAME.invert.freeze
22
+
23
+ LEAF = %w[text icon spacer divider audio video scene].map { |n| BY_NAME[n] }.freeze
24
+ INERT = %w[spacer divider].map { |n| BY_NAME[n] }.freeze
25
+
26
+ def self.code(name)
27
+ BY_NAME.fetch(name.to_s) { raise ViewError, "unknown node kind '#{name}'" }
28
+ end
29
+
30
+ def self.name(code)
31
+ BY_CODE.fetch(code) { raise DecodeError, "unknown node kind #{code}" }
32
+ end
33
+
34
+ def self.leaf?(code) = LEAF.include?(code)
35
+ def self.inert?(code) = INERT.include?(code)
36
+ end
37
+
38
+ # Input and lifecycle events (`spec/06-events.md`).
39
+ module EventKind
40
+ BY_NAME = {
41
+ 'click' => 0x01, 'double_click' => 0x02, 'pointer_down' => 0x03, 'pointer_up' => 0x04,
42
+ 'pointer_move' => 0x05, 'pointer_enter' => 0x06, 'pointer_leave' => 0x07,
43
+ 'key_down' => 0x08, 'key_up' => 0x09, 'text_input' => 0x0A, 'focus' => 0x0B,
44
+ 'blur' => 0x0C, 'change' => 0x0D, 'submit' => 0x0E, 'scroll' => 0x0F,
45
+ 'resize' => 0x10, 'context_menu' => 0x11, 'drag_start' => 0x12, 'drag_over' => 0x13,
46
+ 'drop' => 0x14, 'long_press' => 0x15, 'window' => 0x16, 'ended' => 0x17,
47
+ 'time_update' => 0x18, 'wake' => 0x19, 'file_pick' => 0x1A, 'file_save' => 0x1B,
48
+ 'location' => 0x1C, 'nfc_tag' => 0x1D, 'file_drag' => 0x1E, 'back' => 0x1F,
49
+ 'level' => 0x20
50
+ }.freeze
51
+ BY_CODE = BY_NAME.invert.freeze
52
+
53
+ # The protocol version an event arrived in: an event a client cannot
54
+ # decode is left out at encode time rather than sent, so the view still
55
+ # renders and the widget simply never hears from it.
56
+ SINCE = { 0x20 => 3 }.freeze
57
+
58
+ def self.code(name)
59
+ BY_NAME.fetch(name.to_s) { raise ViewError, "unknown event '#{name}'" }
60
+ end
61
+
62
+ def self.name(code)
63
+ BY_CODE.fetch(code) { raise DecodeError, "unknown event kind #{code}" }
64
+ end
65
+
66
+ def self.since(code) = SINCE.fetch(code, 1)
67
+ end
68
+
69
+ # A string: interned in the session's atom table, or carried inline.
70
+ TextRef = Struct.new(:atom, :inline) do
71
+ def self.atom_ref(id) = new(Integer(id), nil)
72
+ def self.inline_ref(s) = new(nil, s.to_s)
73
+
74
+ def self.decode(reader)
75
+ case (tag = reader.u8)
76
+ when 0x00 then atom_ref(reader.varint32)
77
+ when 0x01 then inline_ref(reader.str(Limits::MAX_INLINE_STR, 'inline string'))
78
+ else raise DecodeError, "unknown TextRef tag #{tag}"
79
+ end
80
+ end
81
+
82
+ def encode(writer)
83
+ if atom
84
+ writer.u8(0x00).varint(atom)
85
+ else
86
+ writer.u8(0x01).str(inline)
87
+ end
88
+ end
89
+ end
90
+
91
+ # A property value (`spec/02-wire-format.md` §4.4).
92
+ #
93
+ # Tagged rather than inferred from the Ruby object: a string may be an
94
+ # atom or an inline run, and 32 bytes may be an asset hash or a label.
95
+ class Value
96
+ NULL = 0x00
97
+ BOOL = 0x01
98
+ INT = 0x02
99
+ FLOAT = 0x03
100
+ ATOM = 0x04
101
+ STR = 0x05
102
+ ASSET = 0x06
103
+ COLOR = 0x07
104
+ LIST = 0x08
105
+
106
+ attr_reader :tag, :value
107
+
108
+ def initialize(tag, value = nil)
109
+ @tag = tag
110
+ @value = value
111
+ end
112
+
113
+ class << self
114
+ def null = new(NULL)
115
+ def bool(b) = new(BOOL, b ? true : false)
116
+ def int(n) = new(INT, Integer(n))
117
+ def float(f)
118
+ f = Float(f)
119
+ raise ViewError, 'a float on the wire must be finite' unless f.finite?
120
+
121
+ new(FLOAT, f)
122
+ end
123
+ def atom(id) = new(ATOM, Integer(id))
124
+ def str(s) = new(STR, s.to_s)
125
+ def asset(hash)
126
+ raise ViewError, 'an asset is 32 bytes' unless hash.bytesize == Limits::HASH_BYTES
127
+
128
+ new(ASSET, hash.b)
129
+ end
130
+ def color(ref) = new(COLOR, ref)
131
+ def list(items) = new(LIST, items)
132
+
133
+ # A plain Ruby value as the wire would carry it. Strings go inline:
134
+ # interning is the encoder's decision, not this one's.
135
+ def from(ruby)
136
+ case ruby
137
+ when nil then null
138
+ when true, false then bool(ruby)
139
+ when Integer then int(ruby)
140
+ when Float then float(ruby)
141
+ when String, Symbol then str(ruby.to_s)
142
+ when ColorRef then color(ruby)
143
+ when Array then list(ruby.map { |i| from(i) })
144
+ when Value then ruby
145
+ else raise ViewError, "a prop cannot carry #{ruby.class}"
146
+ end
147
+ end
148
+ end
149
+
150
+ def self.decode(reader, depth = 1)
151
+ raise DecodeError, 'value nesting' if depth > Limits::MAX_VALUE_DEPTH
152
+
153
+ case (tag = reader.u8)
154
+ when NULL then null
155
+ when BOOL
156
+ case reader.u8
157
+ when 0 then bool(false)
158
+ when 1 then bool(true)
159
+ else raise DecodeError, 'bool must be 0 or 1'
160
+ end
161
+ when INT then int(reader.svarint)
162
+ when FLOAT then float(reader.f64)
163
+ when ATOM then atom(reader.varint32)
164
+ when STR then str(reader.str(Limits::MAX_INLINE_STR, 'inline string'))
165
+ when ASSET then asset(reader.take(Limits::HASH_BYTES))
166
+ when COLOR then color(ColorRef.new(reader.u16))
167
+ when LIST
168
+ count = reader.varint32_max(Limits::MAX_VALUE_LIST, 'value list length')
169
+ list(Array.new(count) { decode(reader, depth + 1) })
170
+ else raise DecodeError, "unknown Value tag #{tag}"
171
+ end
172
+ end
173
+
174
+ def encode(writer)
175
+ case @tag
176
+ when NULL then writer.u8(NULL)
177
+ when BOOL then writer.u8(BOOL).u8(@value ? 1 : 0)
178
+ when INT then writer.u8(INT).svarint(@value)
179
+ when FLOAT then writer.u8(FLOAT).f64(@value)
180
+ when ATOM then writer.u8(ATOM).varint(@value)
181
+ when STR then writer.u8(STR).str(@value)
182
+ when ASSET then writer.u8(ASSET).raw(@value)
183
+ when COLOR then writer.u8(COLOR).u16(@value.bits)
184
+ when LIST
185
+ writer.u8(LIST).varint(@value.length)
186
+ @value.each { |item| item.encode(writer) }
187
+ end
188
+ writer
189
+ end
190
+
191
+ # What a handler sees: plain Ruby, with atoms left as their ids for
192
+ # the session to resolve against its own table.
193
+ def to_ruby
194
+ case @tag
195
+ when NULL then nil
196
+ when LIST then @value.map(&:to_ruby)
197
+ when COLOR then @value
198
+ else @value
199
+ end
200
+ end
201
+
202
+ def ==(other)
203
+ other.is_a?(Value) && other.tag == @tag && other.value == @value
204
+ end
205
+ alias eql? ==
206
+
207
+ def hash = [@tag, @value].hash
208
+ end
209
+
210
+ # What an event does when it happens.
211
+ Handler = Struct.new(:kind, :chunk, :name) do
212
+ SERVER = 0x00
213
+ LOCAL = 0x01
214
+ BOTH = 0x02
215
+
216
+ # Round trip: the client emits an `Event` frame naming this atom.
217
+ def self.server(atom) = new(SERVER, nil, Integer(atom))
218
+ # Runs entirely on the client, in the metered VM. No network traffic.
219
+ def self.local(chunk) = new(LOCAL, Integer(chunk), nil)
220
+ # Runs locally for the immediate feedback, then tells the server.
221
+ def self.local_then_server(chunk, atom) = new(BOTH, Integer(chunk), Integer(atom))
222
+
223
+ def self.decode(reader)
224
+ case (tag = reader.u8)
225
+ when SERVER then server(reader.varint32)
226
+ when LOCAL then local(reader.varint32)
227
+ when BOTH then local_then_server(reader.varint32, reader.varint32)
228
+ else raise DecodeError, "unknown Handler tag #{tag}"
229
+ end
230
+ end
231
+
232
+ def encode(writer)
233
+ case kind
234
+ when SERVER then writer.u8(SERVER).varint(name)
235
+ when LOCAL then writer.u8(LOCAL).varint(chunk)
236
+ when BOTH then writer.u8(BOTH).varint(chunk).varint(name)
237
+ end
238
+ end
239
+ end
240
+
241
+ # One node of a subtree, with its props and handlers held as ranges into
242
+ # the owning [Subtree]'s side arrays.
243
+ FlatNode = Struct.new(:kind, :id, :style, :key, :text, :props, :handlers, :child_count, keyword_init: true)
244
+
245
+ # A subtree, stored pre-order.
246
+ #
247
+ # Reconstructing the shape needs nothing but `child_count`: a node's
248
+ # first child is the next entry, and its next sibling is found by
249
+ # skipping that child's own descendants. Decoding is iterative, so a
250
+ # hostile 10 000-deep tree costs a bounds check rather than the stack.
251
+ class Subtree
252
+ attr_reader :nodes, :props, :handlers
253
+
254
+ def initialize
255
+ @nodes = []
256
+ @props = []
257
+ @handlers = []
258
+ end
259
+
260
+ def root = @nodes.first
261
+
262
+ def props_of(node)
263
+ start, len = node.props
264
+ @props[start, len] || []
265
+ end
266
+
267
+ def handlers_of(node)
268
+ start, len = node.handlers
269
+ @handlers[start, len] || []
270
+ end
271
+
272
+ def push(kind:, id:, style:, key: 0, text: nil, props: [], handlers: [], child_count: 0)
273
+ prop_start = @props.length
274
+ @props.concat(props)
275
+ handler_start = @handlers.length
276
+ @handlers.concat(handlers)
277
+ @nodes << FlatNode.new(
278
+ kind: kind, id: id, style: style, key: key, text: text,
279
+ props: [prop_start, props.length], handlers: [handler_start, handlers.length],
280
+ child_count: child_count
281
+ )
282
+ self
283
+ end
284
+
285
+ def self.decode(reader)
286
+ out = new
287
+ pending = []
288
+ loop do
289
+ raise DecodeError, 'node count' if out.nodes.length >= Limits::MAX_NODES
290
+
291
+ child_count = out.send(:decode_one, reader)
292
+ if child_count.positive?
293
+ raise DecodeError, 'tree depth' if pending.length >= Limits::MAX_TREE_DEPTH
294
+
295
+ pending << child_count
296
+ else
297
+ while (top = pending.last)
298
+ pending[-1] = top - 1
299
+ break unless pending[-1].zero?
300
+
301
+ pending.pop
302
+ end
303
+ end
304
+ return out if pending.empty?
305
+ end
306
+ end
307
+
308
+ def encode(writer)
309
+ @nodes.each do |node|
310
+ props = props_of(node)
311
+ handlers = handlers_of(node)
312
+
313
+ flags = 0
314
+ flags |= 0x01 unless node.key.zero?
315
+ flags |= 0x02 if node.text
316
+ flags |= 0x04 unless props.empty?
317
+ flags |= 0x08 unless handlers.empty?
318
+
319
+ writer.u8(node.kind).u8(flags).varint(node.id).varint(node.style)
320
+ writer.varint(node.key) unless node.key.zero?
321
+ node.text&.encode(writer)
322
+ unless props.empty?
323
+ writer.varint(props.length)
324
+ props.each do |(atom, value)|
325
+ writer.varint(atom)
326
+ value.encode(writer)
327
+ end
328
+ end
329
+ unless handlers.empty?
330
+ writer.varint(handlers.length)
331
+ handlers.each do |(event, handler)|
332
+ writer.u8(event)
333
+ handler.encode(writer)
334
+ end
335
+ end
336
+ writer.varint(node.child_count)
337
+ end
338
+ writer
339
+ end
340
+
341
+ private
342
+
343
+ def decode_one(reader)
344
+ kind = reader.u8
345
+ NodeKind.name(kind)
346
+ flags = reader.u8
347
+ raise DecodeError, 'reserved node flags set' if (flags & 0xF0) != 0
348
+
349
+ id = reader.varint32
350
+ raise DecodeError, 'node id must be non-zero' if id.zero?
351
+
352
+ style = reader.varint32
353
+ key = (flags & 0x01).zero? ? 0 : reader.varint32
354
+ text = (flags & 0x02).zero? ? nil : TextRef.decode(reader)
355
+
356
+ props = []
357
+ unless (flags & 0x04).zero?
358
+ count = reader.varint32_max(Limits::MAX_PROPS, 'props per node')
359
+ count.times { props << [reader.varint32, Value.decode(reader)] }
360
+ end
361
+
362
+ handlers = []
363
+ unless (flags & 0x08).zero?
364
+ count = reader.varint32_max(Limits::MAX_HANDLERS, 'handlers per node')
365
+ count.times do
366
+ event = reader.u8
367
+ EventKind.name(event) # refuse one this revision does not define
368
+ handlers << [event, Handler.decode(reader)]
369
+ end
370
+ end
371
+
372
+ if NodeKind.inert?(kind) && (text || !props.empty? || !handlers.empty?)
373
+ raise DecodeError, 'inert node kind carries content'
374
+ end
375
+
376
+ child_count = reader.varint32_max(Limits::MAX_CHILDREN, 'children per node')
377
+ raise DecodeError, 'a leaf kind carries children' if NodeKind.leaf?(kind) && child_count.positive?
378
+
379
+ push(kind: kind, id: id, style: style, key: key, text: text, props: props,
380
+ handlers: handlers, child_count: child_count)
381
+ child_count
382
+ end
383
+ end
384
+ end
385
+ end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'node'
4
+ require_relative 'style'
5
+
6
+ module EUI
7
+ module Proto
8
+ # One operation in a batch (`spec/02-wire-format.md` §5).
9
+ #
10
+ # Structural only. Whether an op is *coherent* — that the node it names
11
+ # exists, that the atom it references was defined — is session state and
12
+ # belongs to the session, not here.
13
+ class Op
14
+ DEF_ATOM = 0x10
15
+ DEF_STYLE = 0x11
16
+ DEF_COLOR = 0x12
17
+ DEF_CHUNK = 0x13
18
+ DEF_CHUNK_BYTES = 0x14
19
+ DEF_FONT = 0x15
20
+ MOUNT = 0x20
21
+ REPLACE = 0x21
22
+ SET_STYLE = 0x22
23
+ SET_TEXT = 0x23
24
+ SET_PROP = 0x24
25
+ INSERT_CHILD = 0x25
26
+ REMOVE_CHILD = 0x26
27
+ MOVE_CHILD = 0x27
28
+ SET_HANDLER = 0x28
29
+ CLEAR_HANDLER = 0x29
30
+ FOCUS = 0x2A
31
+ SCROLL_TO = 0x2B
32
+ NOTIFY = 0x2C
33
+
34
+ attr_reader :opcode, :fields
35
+
36
+ def initialize(opcode, fields = {})
37
+ @opcode = opcode
38
+ @fields = fields
39
+ end
40
+
41
+ def [](name) = @fields[name]
42
+
43
+ class << self
44
+ def def_atom(id, value) = new(DEF_ATOM, id: id, value: value)
45
+ def def_style(id, record) = new(DEF_STYLE, id: id, record: record)
46
+ def def_color(id, rgba) = new(DEF_COLOR, id: id, rgba: rgba)
47
+ def def_chunk(id, hash) = new(DEF_CHUNK, id: id, hash: hash)
48
+ def def_chunk_bytes(id, bytes) = new(DEF_CHUNK_BYTES, id: id, bytes: bytes)
49
+ def def_font(role, faces) = new(DEF_FONT, role: role, faces: faces)
50
+ def mount(subtree) = new(MOUNT, subtree: subtree)
51
+ def replace(node, subtree) = new(REPLACE, node: node, subtree: subtree)
52
+ def set_style(node, style) = new(SET_STYLE, node: node, style: style)
53
+ def set_text(node, text) = new(SET_TEXT, node: node, text: text)
54
+ def set_prop(node, prop, value) = new(SET_PROP, node: node, prop: prop, value: value)
55
+ def insert_child(parent, index, subtree) = new(INSERT_CHILD, parent: parent, index: index, subtree: subtree)
56
+ def remove_child(parent, index, count) = new(REMOVE_CHILD, parent: parent, index: index, count: count)
57
+ def move_child(parent, from, to) = new(MOVE_CHILD, parent: parent, from: from, to: to)
58
+ def set_handler(node, event, handler) = new(SET_HANDLER, node: node, event: event, handler: handler)
59
+ def clear_handler(node, event) = new(CLEAR_HANDLER, node: node, event: event)
60
+ def focus(node) = new(FOCUS, node: node)
61
+ def scroll_to(node, x, y) = new(SCROLL_TO, node: node, x: x, y: y)
62
+ def notify(title, body = '', tag = '') = new(NOTIFY, title: title, body: body, tag: tag)
63
+ end
64
+
65
+ def self.decode(reader)
66
+ case (opcode = reader.u8)
67
+ when DEF_ATOM
68
+ def_atom(nonzero(reader.varint32, 'atom id'), reader.str(Limits::MAX_ATOM_BYTES, 'atom value'))
69
+ when DEF_STYLE
70
+ def_style(nonzero(reader.varint32, 'style id'), StyleRecord.decode(reader))
71
+ when DEF_COLOR
72
+ def_color(nonzero(reader.varint32, 'color id'), reader.u32)
73
+ when DEF_CHUNK
74
+ def_chunk(nonzero(reader.varint32, 'chunk id'), reader.take(Limits::HASH_BYTES))
75
+ when DEF_CHUNK_BYTES
76
+ def_chunk_bytes(nonzero(reader.varint32, 'chunk id'), reader.bytes(Limits::MAX_CHUNK_BYTES, 'chunk bytes'))
77
+ when DEF_FONT
78
+ role = reader.u8
79
+ raise DecodeError, 'font role' if role > Limits::MAX_FONT_ROLE
80
+
81
+ count = reader.varint32
82
+ raise DecodeError, 'a font role with no face' if count.zero?
83
+ raise DecodeError, 'font faces' if count > Limits::MAX_FACES_PER_ROLE
84
+
85
+ def_font(role, Array.new(count) { reader.take(Limits::HASH_BYTES) })
86
+ when MOUNT then mount(Subtree.decode(reader))
87
+ when REPLACE then replace(nonzero(reader.varint32, 'node id'), Subtree.decode(reader))
88
+ when SET_STYLE then set_style(nonzero(reader.varint32, 'node id'), reader.varint32)
89
+ when SET_TEXT then set_text(nonzero(reader.varint32, 'node id'), TextRef.decode(reader))
90
+ when SET_PROP
91
+ set_prop(nonzero(reader.varint32, 'node id'), reader.varint32, Value.decode(reader))
92
+ when INSERT_CHILD
93
+ insert_child(nonzero(reader.varint32, 'node id'), reader.varint32, Subtree.decode(reader))
94
+ when REMOVE_CHILD
95
+ remove_child(nonzero(reader.varint32, 'node id'), reader.varint32, reader.varint32)
96
+ when MOVE_CHILD
97
+ move_child(nonzero(reader.varint32, 'node id'), reader.varint32, reader.varint32)
98
+ when SET_HANDLER
99
+ node = nonzero(reader.varint32, 'node id')
100
+ event = reader.u8
101
+ EventKind.name(event)
102
+ set_handler(node, event, Handler.decode(reader))
103
+ when CLEAR_HANDLER
104
+ node = nonzero(reader.varint32, 'node id')
105
+ event = reader.u8
106
+ EventKind.name(event)
107
+ clear_handler(node, event)
108
+ when FOCUS then focus(nonzero(reader.varint32, 'node id'))
109
+ when SCROLL_TO then scroll_to(nonzero(reader.varint32, 'node id'), reader.svarint, reader.svarint)
110
+ when NOTIFY
111
+ notify(reader.str(Limits::MAX_NOTIFY_TITLE, 'notification title'),
112
+ reader.str(Limits::MAX_NOTIFY_BODY, 'notification body'),
113
+ reader.str(Limits::MAX_NOTIFY_TAG, 'notification tag'))
114
+ else raise DecodeError, "unknown opcode #{opcode}"
115
+ end
116
+ end
117
+
118
+ def self.nonzero(value, what)
119
+ raise DecodeError, "#{what} must be non-zero" if value.zero?
120
+
121
+ value
122
+ end
123
+
124
+ def encode(writer)
125
+ f = @fields
126
+ writer.u8(@opcode)
127
+ case @opcode
128
+ when DEF_ATOM then writer.varint(f[:id]).str(f[:value])
129
+ when DEF_STYLE
130
+ writer.varint(f[:id])
131
+ f[:record].encode(writer)
132
+ when DEF_COLOR then writer.varint(f[:id]).u32(f[:rgba])
133
+ when DEF_CHUNK then writer.varint(f[:id]).raw(f[:hash])
134
+ when DEF_CHUNK_BYTES then writer.varint(f[:id]).bytes(f[:bytes])
135
+ when DEF_FONT
136
+ writer.u8(f[:role]).varint(f[:faces].length)
137
+ f[:faces].each { |face| writer.raw(face) }
138
+ when MOUNT then f[:subtree].encode(writer)
139
+ when REPLACE
140
+ writer.varint(f[:node])
141
+ f[:subtree].encode(writer)
142
+ when SET_STYLE then writer.varint(f[:node]).varint(f[:style])
143
+ when SET_TEXT
144
+ writer.varint(f[:node])
145
+ f[:text].encode(writer)
146
+ when SET_PROP
147
+ writer.varint(f[:node]).varint(f[:prop])
148
+ f[:value].encode(writer)
149
+ when INSERT_CHILD
150
+ writer.varint(f[:parent]).varint(f[:index])
151
+ f[:subtree].encode(writer)
152
+ when REMOVE_CHILD then writer.varint(f[:parent]).varint(f[:index]).varint(f[:count])
153
+ when MOVE_CHILD then writer.varint(f[:parent]).varint(f[:from]).varint(f[:to])
154
+ when SET_HANDLER
155
+ writer.varint(f[:node]).u8(f[:event])
156
+ f[:handler].encode(writer)
157
+ when CLEAR_HANDLER then writer.varint(f[:node]).u8(f[:event])
158
+ when FOCUS then writer.varint(f[:node])
159
+ when SCROLL_TO then writer.varint(f[:node]).svarint(f[:x]).svarint(f[:y])
160
+ when NOTIFY then writer.str(f[:title]).str(f[:body]).str(f[:tag])
161
+ else raise Error, "cannot encode opcode #{@opcode}"
162
+ end
163
+ writer
164
+ end
165
+
166
+ def ==(other)
167
+ other.is_a?(Op) && other.opcode == @opcode && other.fields == @fields
168
+ end
169
+ end
170
+
171
+ # An ordered run of ops carrying a sequence number. The client acks the
172
+ # last one it applied, and applies a batch all or nothing.
173
+ Batch = Struct.new(:seq, :ops) do
174
+ def self.decode(reader)
175
+ seq = reader.varint
176
+ count = reader.varint32_max(Limits::MAX_OPS_PER_BATCH, 'ops per batch')
177
+ notifications = 0
178
+ ops = Array.new(count) do
179
+ op = Op.decode(reader)
180
+ if op.opcode == Op::NOTIFY
181
+ notifications += 1
182
+ raise DecodeError, 'notifications per batch' if notifications > Limits::MAX_NOTIFY_PER_BATCH
183
+ end
184
+ op
185
+ end
186
+ new(seq, ops)
187
+ end
188
+
189
+ def encode(writer)
190
+ writer.varint(seq).varint(ops.length)
191
+ ops.each { |op| op.encode(writer) }
192
+ writer
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'limits'
4
+ require_relative '../errors'
5
+
6
+ module EUI
7
+ module Proto
8
+ # A cursor over bytes that refuses anything it was not promised.
9
+ #
10
+ # Two rules do most of the work: a varint must be minimally encoded, and
11
+ # a frame must account for every byte it declared. Both are the classic
12
+ # route to a parser disagreeing with itself.
13
+ class Reader
14
+ def initialize(bytes)
15
+ @bytes = bytes.b
16
+ @pos = 0
17
+ end
18
+
19
+ def remaining
20
+ @bytes.bytesize - @pos
21
+ end
22
+
23
+ def eof?
24
+ remaining.zero?
25
+ end
26
+
27
+ def take(count)
28
+ raise DecodeError, "short read: wanted #{count}, #{remaining} left" if count > remaining
29
+
30
+ slice = @bytes.byteslice(@pos, count)
31
+ @pos += count
32
+ slice
33
+ end
34
+
35
+ def u8
36
+ take(1).unpack1('C')
37
+ end
38
+
39
+ def u16
40
+ take(2).unpack1('v')
41
+ end
42
+
43
+ def u32
44
+ take(4).unpack1('V')
45
+ end
46
+
47
+ def u64
48
+ take(8).unpack1('Q<')
49
+ end
50
+
51
+ def f64
52
+ value = take(8).unpack1('E')
53
+ raise DecodeError, 'float must be finite' unless value.finite?
54
+
55
+ value
56
+ end
57
+
58
+ def array(count)
59
+ take(count)
60
+ end
61
+
62
+ # LEB128, and only the minimal spelling of it: a multi-byte encoding
63
+ # whose last byte is `0x00` is an error rather than a normalisation.
64
+ def varint(max_bytes = 10)
65
+ value = 0
66
+ shift = 0
67
+ count = 0
68
+ loop do
69
+ byte = u8
70
+ count += 1
71
+ raise DecodeError, 'varint too long' if count > max_bytes
72
+
73
+ value |= (byte & 0x7F) << shift
74
+ break if (byte & 0x80).zero?
75
+ raise DecodeError, 'non-minimal varint' if count == max_bytes
76
+
77
+ shift += 7
78
+ end
79
+ raise DecodeError, 'non-minimal varint' if count > 1 && value < (1 << (7 * (count - 1)))
80
+
81
+ value
82
+ end
83
+
84
+ def varint32
85
+ value = varint(5)
86
+ raise DecodeError, 'varint overflows u32' if value > 0xFFFF_FFFF
87
+
88
+ value
89
+ end
90
+
91
+ def varint32_max(max, what)
92
+ value = varint32
93
+ raise DecodeError, "#{what} above #{max}" if value > max
94
+
95
+ value
96
+ end
97
+
98
+ def svarint
99
+ raw = varint(10)
100
+ (raw >> 1) ^ -(raw & 1)
101
+ end
102
+
103
+ def bytes(max, what)
104
+ len = varint32
105
+ raise DecodeError, "#{what} of #{len} bytes, at most #{max}" if len > max
106
+
107
+ take(len)
108
+ end
109
+
110
+ def str(max, what)
111
+ value = bytes(max, what).force_encoding(Encoding::UTF_8)
112
+ raise DecodeError, "#{what} is not valid UTF-8" unless value.valid_encoding?
113
+
114
+ value
115
+ end
116
+
117
+ # Trailing bytes are an error, not padding.
118
+ def finish!
119
+ raise DecodeError, "#{remaining} trailing bytes" unless eof?
120
+
121
+ self
122
+ end
123
+ end
124
+ end
125
+ end