mqtt-v5 0.0.1.ci.release

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 (36) hide show
  1. checksums.yaml +7 -0
  2. data/lib/mqtt/v5/async/client.rb +27 -0
  3. data/lib/mqtt/v5/client/authenticator.rb +83 -0
  4. data/lib/mqtt/v5/client/connection.rb +192 -0
  5. data/lib/mqtt/v5/client/session.rb +45 -0
  6. data/lib/mqtt/v5/client.rb +61 -0
  7. data/lib/mqtt/v5/errors.rb +19 -0
  8. data/lib/mqtt/v5/packet/auth.rb +57 -0
  9. data/lib/mqtt/v5/packet/connack.rb +110 -0
  10. data/lib/mqtt/v5/packet/connect.rb +130 -0
  11. data/lib/mqtt/v5/packet/disconnect.rb +78 -0
  12. data/lib/mqtt/v5/packet/ping_req.rb +20 -0
  13. data/lib/mqtt/v5/packet/ping_resp.rb +20 -0
  14. data/lib/mqtt/v5/packet/pub_ack.rb +60 -0
  15. data/lib/mqtt/v5/packet/pub_comp.rb +53 -0
  16. data/lib/mqtt/v5/packet/pub_rec.rb +60 -0
  17. data/lib/mqtt/v5/packet/pub_rel.rb +53 -0
  18. data/lib/mqtt/v5/packet/publish.rb +122 -0
  19. data/lib/mqtt/v5/packet/reason_code.rb +112 -0
  20. data/lib/mqtt/v5/packet/sub_ack.rb +66 -0
  21. data/lib/mqtt/v5/packet/subscribe.rb +87 -0
  22. data/lib/mqtt/v5/packet/unsub_ack.rb +59 -0
  23. data/lib/mqtt/v5/packet/unsubscribe.rb +112 -0
  24. data/lib/mqtt/v5/packet.rb +147 -0
  25. data/lib/mqtt/v5/packets.rb +25 -0
  26. data/lib/mqtt/v5/topic_alias/cache.rb +87 -0
  27. data/lib/mqtt/v5/topic_alias/frequency_weighted_policy.rb +50 -0
  28. data/lib/mqtt/v5/topic_alias/length_weighted_policy.rb +20 -0
  29. data/lib/mqtt/v5/topic_alias/lru_policy.rb +36 -0
  30. data/lib/mqtt/v5/topic_alias/manager.rb +143 -0
  31. data/lib/mqtt/v5/topic_alias/policy.rb +34 -0
  32. data/lib/mqtt/v5/topic_alias/weighted_policy.rb +48 -0
  33. data/lib/mqtt/v5/topic_alias.rb +27 -0
  34. data/lib/mqtt/v5/version.rb +11 -0
  35. data/lib/mqtt/v5.rb +4 -0
  36. metadata +88 -0
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+
5
+ module MQTT
6
+ module V5
7
+ module Packet
8
+ ReasonCode = Data.define(:code, :name, :error)
9
+
10
+ # Encapsulates reason codes
11
+ class ReasonCode < Data
12
+ # @!attribute [r] code
13
+ # @return [Integer] the reason code value
14
+ # @!attribute [r] name
15
+ # @return [String] the reason code name
16
+ # @!attribute [r] error
17
+ # @return [Class,nil] the error class for failed reason codes
18
+
19
+ class << self
20
+ # rubocop:disable Style/OptionalBooleanParameter
21
+ def register(code, name, packet_types, retriable = false)
22
+ @reason_codes ||= Hash.new { |h, k| h[k] = {} }
23
+ rc = create(code, name, retriable)
24
+ packet_types.each { |packet_type| @reason_codes[packet_type][code] = rc }
25
+ end
26
+ # rubocop:enable Style/OptionalBooleanParameter
27
+
28
+ def find(packet_name, reason_code)
29
+ @reason_codes.dig(packet_name, reason_code) || unknown_reason_code(packet_name, reason_code)
30
+ end
31
+
32
+ def failed?(reason_code)
33
+ reason_code >= 0x80
34
+ # define readers
35
+ end
36
+
37
+ private
38
+
39
+ def create(code, name, retriable)
40
+ error = define_error_class(code, name, retriable) if code >= 0x80
41
+ new(code, name, error)
42
+ end
43
+
44
+ def unknown_reason_code(packet_name, reason_code)
45
+ new(reason_code, "Unknown ReasonCode for #{packet_name}", UnknownReasonCode)
46
+ end
47
+
48
+ def define_error_class(code, name, retriable)
49
+ klass = Class.new(ResponseError)
50
+ klass.include(Error::Retriable) if retriable
51
+ klass.instance_variable_set(:@code, code)
52
+ MQTT::V5.const_set(name.split(/\s+/).map(&:capitalize).join, klass)
53
+ end
54
+ end
55
+
56
+ def failed?
57
+ self.class.failed?(code)
58
+ end
59
+
60
+ def success!(reason_string)
61
+ return self unless failed?
62
+
63
+ raise error, reason_string || name
64
+ end
65
+
66
+ def to_s
67
+ format '%<name>s(0x%02<code>x)', name:, code:
68
+ end
69
+ end
70
+
71
+ # Success/Failed for packets with a single reason_code field
72
+ module ReasonCodeAck
73
+ def defaults
74
+ super.merge!({ reason_code: 0x00 })
75
+ end
76
+
77
+ def success?
78
+ !failed?
79
+ end
80
+
81
+ # @return [self]
82
+ # @raise [ResponseError]
83
+ def success!
84
+ reason_code_data.success!(reason_string) && self
85
+ end
86
+
87
+ def failed?
88
+ ReasonCode.failed?(reason_code)
89
+ end
90
+
91
+ def to_s
92
+ "#{super}: #{reason_code_data}"
93
+ end
94
+
95
+ def reason_code_data
96
+ ReasonCode.find(packet_name, reason_code)
97
+ end
98
+ end
99
+
100
+ # Module for packets with a :reason_codes field
101
+ module ReasonCodeListAck
102
+ def reason_codes_data
103
+ reason_codes.map { |rc| ReasonCode.find(packet_name, rc) }
104
+ end
105
+
106
+ def to_s
107
+ "#{super}: #{reason_codes_data.map { |rc| format('0x%02<code>x', code: rc.code) }.tally}"
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packet'
4
+
5
+ module MQTT
6
+ module V5
7
+ module Packet
8
+ # MQTT 5.0 SUBACK packet
9
+ #
10
+ # Subscription acknowledgement sent by broker in response to a SUBSCRIBE packet.
11
+ #
12
+ # @see Core::Client#subscribe
13
+ # @see https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901171 MQTT 5.0 Spec §3.9
14
+ class SubAck
15
+ include Packet
16
+
17
+ fixed(9)
18
+
19
+ # @!attribute [r] packet_identifier
20
+ # @return [Integer] packet identifier matching the SUBSCRIBE packet (receive only)
21
+
22
+ # @!group Properties
23
+
24
+ # @!attribute [r] reason_string
25
+ # @return [String<UTF8>] human-readable reason for the response
26
+ # @!attribute [r] user_properties
27
+ # @return [Array<String, String>] user-defined properties as key-value pairs
28
+
29
+ # @!endgroup
30
+
31
+ # @!parse include ReasonCodeListAck
32
+ # @!attribute [r] reason_codes
33
+ # acknowledgement status for each subscription
34
+ #
35
+ # ✅ Success:
36
+ #
37
+ # - `0x00` Granted QoS 0
38
+ # - `0x01` Granted QoS 1
39
+ # - `0x02` Granted QoS 2
40
+ #
41
+ # ❌ Error:
42
+ #
43
+ # - `0x80` Unspecified error
44
+ # - `0x83` Implementation specific error
45
+ # - `0x87` Not authorized
46
+ # - `0x8F` Topic Filter invalid
47
+ # - `0x91` Packet Identifier in use
48
+ # - `0x97` Quota exceeded
49
+ # - `0xA1` Subscription Identifiers not supported
50
+ # - `0xA2` Wildcard Subscriptions not supported
51
+ #
52
+ # @return [Array<ReasonCode>]
53
+
54
+ variable(
55
+ packet_identifier: :int16,
56
+ properties:
57
+ )
58
+ payload(
59
+ reason_codes: # automatically includes ReasonCodeListAck
60
+ )
61
+
62
+ alias return_codes reason_codes
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packet'
4
+ require 'mqtt/core/packet/subscribe'
5
+
6
+ module MQTT
7
+ module V5
8
+ module Packet
9
+ # MQTT 5.0 SUBSCRIBE packet
10
+ #
11
+ # Sent by client to subscribe to one or more topic filters.
12
+ #
13
+ # @see Core::Client#subscribe
14
+ # @see https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901161 MQTT 5.0 Spec §3.8
15
+ class Subscribe
16
+ include Packet
17
+
18
+ fixed(8, [:reserved, 4, 0b0010])
19
+
20
+ # @!attribute [r] packet_identifier
21
+ # @return [Integer] packet identifier for matching with SUBACK (managed automatically by session)
22
+
23
+ # @!group Properties
24
+
25
+ # @!attribute [r] subscription_identifier
26
+ # @return [Integer] identifier for this subscription
27
+ # @!attribute [r] user_properties
28
+ # @return [Array<String, String>] user-defined properties as key-value pairs
29
+
30
+ # @!endgroup
31
+
32
+ # @!parse
33
+ # class TopicFilter
34
+ # # @!attribute [r] topic_filter
35
+ # # @return [String<UTF8>] topic filter pattern
36
+ # # @!attribute [r] max_qos
37
+ # # @return [Integer] maximum QoS level accepted by the client: 0, 1, or 2
38
+ # # @!attribute [r] no_local
39
+ # # @return [Boolean] do not forward messages back to same client id
40
+ # # @!attribute [r] retain_as_published
41
+ # # @return [Boolean] retain flag from PUBLISH should be preserved
42
+ # # @!attribute [r] retain
43
+ # # @return [Integer]
44
+ # # retained message handling
45
+ # #
46
+ # # - `0` Send retained messages at the time of subscribe
47
+ # # - `1` Send retained messages only if subscription does not currently exist
48
+ # # - `2` Do not send retained messages at the time of subscribe
49
+ # end
50
+
51
+ # @!attribute [r] topic_filters
52
+ # @return [Array<TopicFilter>] list of topic filters to subscribe to
53
+
54
+ variable(
55
+ packet_identifier: :int16,
56
+ properties:
57
+ )
58
+ payload(
59
+ topic_filters: list(
60
+ :topic_filter,
61
+ topic_filter: :utf8string,
62
+ subscription_options: flags(
63
+ [:reserved, 2],
64
+ [:retain, 2],
65
+ :retain_as_published,
66
+ :no_local,
67
+ [:max_qos, 2]
68
+ )
69
+ ) do
70
+ alias_method :requested_qos, :max_qos
71
+ end
72
+ )
73
+
74
+ MAX_QOS_FIELD = :max_qos
75
+ include MQTT::Core::Packet::Subscribe
76
+
77
+ def match?(publish_packet)
78
+ if subscription_identifier&.positive?
79
+ publish_packet.match_subscription_identifier?(subscription_identifier)
80
+ else
81
+ super
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packet'
4
+ module MQTT
5
+ module V5
6
+ module Packet
7
+ # MQTT 5.0 UNSUBACK packet
8
+ #
9
+ # Unsubscribe acknowledgement sent by broker in response to an UNSUBSCRIBE packet.
10
+ #
11
+ # @see Core::Client::Subscription#unsubscribe
12
+ # @see https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901187 MQTT 5.0 Spec §3.11
13
+ class UnsubAck
14
+ include Packet
15
+
16
+ fixed(11)
17
+
18
+ # @!attribute [r] packet_identifier
19
+ # @return [Integer] packet identifier matching the UNSUBSCRIBE packet (receive only)
20
+
21
+ # @!group Properties
22
+
23
+ # @!attribute [r] reason_string
24
+ # @return [String<UTF8>] human-readable reason for the response
25
+ # @!attribute [r] user_properties
26
+ # @return [Array<String, String>] user-defined properties as key-value pairs
27
+
28
+ # @!endgroup
29
+
30
+ # @!parse include ReasonCodeListAck
31
+ # @!attribute [r] reason_codes
32
+ # acknowledgement status for each topic filter
33
+ #
34
+ # ✅ Success:
35
+ #
36
+ # - `0x00` Success
37
+ # - `0x11` No subscription existed
38
+ #
39
+ # ❌ Error:
40
+ #
41
+ # - `0x80` Unspecified error
42
+ # - `0x83` Implementation specific error
43
+ # - `0x87` Not authorized
44
+ # - `0x8F` Topic Filter invalid
45
+ # - `0x91` Packet Identifier in use
46
+ #
47
+ # @return [Array<ReasonCode>]
48
+
49
+ variable(
50
+ packet_identifier: :int16,
51
+ properties:
52
+ )
53
+ payload(
54
+ reason_codes: # automatically includes ReasonCodeListAck
55
+ )
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../packet'
4
+ require 'mqtt/core/packet/unsubscribe'
5
+
6
+ module MQTT
7
+ module V5
8
+ module Packet
9
+ # MQTT 5.0 UNSUBSCRIBE packet
10
+ #
11
+ # Sent by client to unsubscribe from one or more topic filters.
12
+ #
13
+ # @see Core::Client::Subscription#unsubscribe
14
+ # @see https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901179 MQTT 5.0 Spec §3.10
15
+ class Unsubscribe
16
+ include Packet
17
+ include Core::Packet::Unsubscribe
18
+
19
+ fixed(10, [:reserved, 4, 0b0010])
20
+
21
+ # @!attribute [r] packet_identifier
22
+ # @return [Integer] packet identifier for matching with UNSUBACK (managed automatically by session)
23
+
24
+ # @!group Properties
25
+
26
+ # @!attribute [r] user_properties
27
+ # @return [Array<String, String>] user-defined properties as key-value pairs
28
+
29
+ # @!endgroup
30
+
31
+ # @!attribute [r] topic_filters
32
+ # @return [Array<String<UTF8>>] list of topic filters to unsubscribe from
33
+
34
+ variable(
35
+ packet_identifier: :int16,
36
+ properties:
37
+ )
38
+ payload(
39
+ topic_filters: list(:utf8string)
40
+ )
41
+
42
+ def apply_data(data)
43
+ @ignore_failed = data.delete(:ignore_failed) { false }
44
+ @ignore_no_subscription = data.delete(:ignore_no_subscription) { true }
45
+ super
46
+ end
47
+
48
+ # @return [Hash<String,Symbol>]
49
+ # Map of topic_filter to ack status
50
+ # * :success - subscription existed and was unsubscribed from the server
51
+ # * :no_subscription - subscription did not exist on the server
52
+ # * :failed - subscription failed to unsubscribe
53
+ def filter_status(unsuback)
54
+ topic_filters.zip(unsuback.reason_codes).to_h { |tf, rc| [tf, classify(rc)] }
55
+ end
56
+
57
+ # @return [Array<String>]
58
+ def unsubscribed_topic_filters(unsuback = nil)
59
+ return topic_filters unless unsuback
60
+
61
+ topic_filters.zip(unsuback.reason_codes).filter_map { |tf, rc| tf unless failed?(rc) }
62
+ end
63
+
64
+ # Version dependent partition topic_filters into success and failures
65
+ #
66
+ # Attributes @ignore_no_subscription and @ignore_failed can control whether these statuses are considered
67
+ # successful or not.
68
+ # @return [Array<Hash<String,Symbol>>] pair of Maps as per #filter_status
69
+ def partition_success(unsuback)
70
+ filter_status(unsuback).partition { |(_tf, ack_status)| ack_success?(ack_status) }.map(&:to_h)
71
+ end
72
+
73
+ def success!(unsuback)
74
+ _, failed = partition_success(unsuback)
75
+
76
+ raise UnsubscribeError, failed unless failed.empty?
77
+
78
+ self
79
+ end
80
+
81
+ private
82
+
83
+ def failed?(reason_code)
84
+ ReasonCode.failed?(reason_code)
85
+ end
86
+
87
+ def classify(reason_code)
88
+ if failed?(reason_code)
89
+ :failed
90
+ elsif reason_code == 0x17
91
+ :no_subscription
92
+ else
93
+ :success
94
+ end
95
+ end
96
+
97
+ def ack_success?(ack_status)
98
+ case ack_status
99
+ when :success
100
+ true
101
+ when :no_subscription
102
+ @ignore_no_subscription
103
+ when :failed
104
+ @ignore_failed
105
+ else
106
+ false
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mqtt/core/packet'
4
+ require_relative 'version'
5
+ require_relative 'errors'
6
+ require_relative 'packet/reason_code'
7
+
8
+ module MQTT
9
+ module V5
10
+ # MQTT 5.0 base packet definition
11
+ #
12
+ # Inherited constants (referenced via self:: in {MQTT::Core::Packet}
13
+ module Packet
14
+ VALUE_TYPES = {
15
+ utf8string: MQTT::Core::Type::UTF8String,
16
+ binary: MQTT::Core::Type::Binary,
17
+ remaining: MQTT::Core::Type::Remaining,
18
+ utf8pair: MQTT::Core::Type::UTF8StringPair,
19
+ int8: MQTT::Core::Type::Int8,
20
+ int16: MQTT::Core::Type::Int16,
21
+ int32: MQTT::Core::Type::Int32,
22
+ intvar: MQTT::Core::Type::VarInt,
23
+ varint: MQTT::Core::Type::VarInt,
24
+ boolean: MQTT::Core::Type::BooleanByte
25
+ }.freeze
26
+
27
+ # Available property types for MQTT version 5.0
28
+ # @note :user_properties
29
+ # The spec uses the singular 'user_property' - but it is the only property that can occur more than once
30
+ # and it the key in the pair can itself appear more than once, so we represent it as Array<String,String>
31
+ # and give it the plural name
32
+
33
+ # Attributes stored as a list of properties
34
+ PROPERTY_TYPES = [
35
+ [0x01, :payload_format_indicator, :int8, %i[publish will]],
36
+ [0x02, :message_expiry_interval, :int32, %i[publish will]],
37
+ [0x03, :content_type, :utf8string, %i[publish will]],
38
+ [0x08, :response_topic, :utf8string, %i[publish will]],
39
+ [0x09, :correlation_data, :binary, %i[publish will]],
40
+ # @!attribute [r] subscription_identifier
41
+ # @return [Integer] (publish)
42
+ # @return [Array<Integer>] (subscribe)
43
+ [0x0B, :subscription_identifier, :varint, %i[subscribe]],
44
+ [0x08, :subscription_identifiers, [:varint], %i[publish]], # this is only sent by servers
45
+ [0x11, :session_expiry_interval, :int32, %i[connect connack disconnect]],
46
+ [0x12, :assigned_client_identifier, :utf8string, [:connack]],
47
+ [0x13, :server_keep_alive, :int16, [:connack]],
48
+ [0x15, :authentication_method, :utf8string, %i[connect connack auth]],
49
+ [0x16, :authentication_data, :binary, %i[connect connack auth]],
50
+ [0x17, :request_problem_information, :boolean, [:connect]],
51
+ [0x18, :will_delay_interval, :int32, [:will]],
52
+ [0x19, :request_response_information, :boolean, [:connect]],
53
+ [0x1A, :response_information, :utf8string, [:connack]],
54
+ [0x1C, :server_reference, :utf8string, %i[connack disconnect]],
55
+ [0x1F, :reason_string, :utf8string, %i[connack puback pubrec pubrel pubcomp suback unsuback disconnect auth]],
56
+ [0x21, :receive_maximum, :int16, %i[connect connack]],
57
+ [0x22, :topic_alias_maximum, :int16, %i[connect connack]],
58
+ [0x23, :topic_alias, :int16, [:publish]],
59
+ [0x24, :maximum_qos, :int8, [:connack]],
60
+ [0x25, :retain_available, :boolean, [:connack]],
61
+ # @!attribute [r] user_properties
62
+ # @return [Array<String,String>] list of key,value pairs, same key may appear more than once
63
+ [0x26, :user_properties, [:utf8pair], :all],
64
+ [0x27, :maximum_packet_size, :int32, %i[connect connack]],
65
+ [0x28, :wildcard_subscription_available, :boolean, [:connack]],
66
+ [0x29, :subscription_identifier_available, :boolean, [:connack]],
67
+ [0x2A, :shared_subscription_available, :boolean, [:connack]]
68
+ ].map { |data| MQTT::Core::Type::Properties::PropertyType.create(*data, types: VALUE_TYPES) }.freeze
69
+
70
+ # If the sender is compliant with this specification it will not send Malformed Packets or cause Protocol Errors
71
+
72
+ # Reason codes
73
+ REASON_CODES = [
74
+ [0x00, 'Success', %i[connack puback pubrec pubrel pubcomp unsuback auth]],
75
+ [0x00, 'Normal disconnection', %i[disconnect]],
76
+ [0x00, 'Granted QoS 0', %i[suback]],
77
+ [0x01, 'Granted QoS 1', %i[suback]],
78
+ [0x02, 'Granted QoS 2', %i[suback]],
79
+ [0x04, 'Disconnect with Will Message', %i[disconnect]],
80
+ [0x10, 'No matching subscribers', %i[puback pubrec]],
81
+ [0x11, 'No subscription existed', %i[unsuback]],
82
+ [0x18, 'Continue authentication', %i[auth]],
83
+ [0x19, 'Re-authenticate', %i[auth]],
84
+ [0x80, 'Unspecified error', %i[connack puback pubrec suback unsuback disconnect], true],
85
+ [0x81, 'Malformed Packet', %i[connack disconnect]],
86
+ [0x82, 'Protocol Error', %i[connack disconnect]],
87
+ [0x83, 'Implementation specific error', %i[connack puback pubrec suback unsuback disconnect], true],
88
+ [0x84, 'Unsupported Protocol Version', %i[connack]],
89
+ [0x85, 'Client Identifier not valid', %i[connack]],
90
+ [0x86, 'Bad User Name or Password', %i[connack]],
91
+ [0x87, 'Not authorized', %i[connack puback pubrec suback unsuback disconnect]],
92
+ [0x88, 'Server unavailable', %i[connack], true],
93
+ [0x89, 'Server busy', %i[connack disconnect], true],
94
+ [0x8A, 'Banned', %i[connack]],
95
+ [0x8B, 'Server shutting down', %i[disconnect], true],
96
+ [0x8C, 'Bad authentication method', %i[connack disconnect]],
97
+ [0x8D, 'Keep alive timeout', %i[disconnect], true],
98
+ [0x8E, 'Session taken over', %i[disconnect]],
99
+ [0x8F, 'Topic Filter invalid', %i[suback unsuback disconnect]],
100
+ [0x90, 'Topic Name invalid', %i[connack puback pubrec disconnect]],
101
+ [0x91, 'Packet Identifier in use', %i[puback pubrec suback unsuback]],
102
+ [0x92, 'Packet Identifier not found', %i[pubrel pubcomp]],
103
+ [0x93, 'Receive Maximum exceeded', %i[disconnect], true],
104
+ [0x94, 'Topic Alias invalid', %i[disconnect]],
105
+ [0x95, 'Packet too large', %i[connack disconnect]],
106
+ [0x96, 'Message rate too high', %i[disconnect], true],
107
+ [0x97, 'Quota exceeded', %i[connack puback pubrec suback disconnect]],
108
+ [0x98, 'Administrative action', %i[disconnect]],
109
+ [0x99, 'Payload format invalid', %i[connack puback pubrec disconnect]],
110
+ [0x9A, 'Retain not supported', %i[connack disconnect]],
111
+ [0x9B, 'QoS not supported', %i[connack disconnect]],
112
+ [0x9C, 'Use another server', %i[connack disconnect]],
113
+ [0x9E, 'Shared Subscriptions not supported', %i[suback disconnect]],
114
+ [0x9F, 'Connection rate exceeded', %i[connack disconnect], true],
115
+ [0xA0, 'Maximum connect time', %i[disconnect]],
116
+ [0xA1, 'Subscription Identifiers not supported', %i[suback disconnect]],
117
+ [0xA2, 'Wildcard Subscriptions not supported', %i[suback disconnect]]
118
+ ].freeze.tap { |codes| codes.each { |code| ReasonCode.register(*code) } }
119
+
120
+ # rubocop:disable Style/MutableConstant
121
+ PACKET_TYPES = {}
122
+ # rubocop:enable Style/MutableConstant
123
+
124
+ # Additional class methods for the packet
125
+ module Definition
126
+ def reason_code
127
+ include ReasonCodeAck
128
+
129
+ { type: :int8, default: 0x00 }
130
+ end
131
+
132
+ def reason_codes
133
+ include ReasonCodeListAck
134
+
135
+ list(:int8)
136
+ end
137
+ end
138
+
139
+ extend MQTT::Core::Packet::ModuleMethods
140
+
141
+ def self.included(mod)
142
+ mod.include(MQTT::Core::Packet)
143
+ mod.extend(Definition)
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # MQTT 5.0
4
+ module MQTT
5
+ # MQTT 5.0 protocol implementation
6
+ module V5
7
+ require_relative 'packet/connect'
8
+ require_relative 'packet/connack'
9
+ require_relative 'packet/disconnect'
10
+ require_relative 'packet/ping_req'
11
+ require_relative 'packet/ping_resp'
12
+ require_relative 'packet/publish'
13
+ require_relative 'packet/pub_ack'
14
+ require_relative 'packet/pub_rec'
15
+ require_relative 'packet/pub_rel'
16
+ require_relative 'packet/pub_comp'
17
+ require_relative 'packet/subscribe'
18
+ require_relative 'packet/sub_ack'
19
+ require_relative 'packet/unsubscribe'
20
+ require_relative 'packet/unsub_ack'
21
+ require_relative 'packet/auth'
22
+
23
+ MQTT::V5::Packet::PACKET_TYPES.freeze
24
+ end
25
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MQTT
4
+ module V5
5
+ module TopicAlias
6
+ # Bidirectional topic<->alias storage
7
+ # @!visibility private
8
+ class Cache
9
+ # @!visibility private
10
+ attr_reader :max_assigned
11
+
12
+ # @return [Integer] maximum number of topic aliases this cache can hold
13
+ attr_reader :max
14
+
15
+ # @return [Integer] the total size of topic_names held in this cache
16
+ attr_reader :cached_bytes
17
+
18
+ def self.create(max)
19
+ max&.positive? ? new(max: max) : nil
20
+ end
21
+
22
+ def initialize(max:)
23
+ @max = max
24
+ @aliases = {} # String keys: topic => alias, Integer keys: alias => topic
25
+ @max_assigned = 0
26
+ @cached_bytes = 0
27
+ @available = []
28
+ end
29
+
30
+ # Resolve topic to alias or alias to topic
31
+ # @param topic_or_alias [String, Integer]
32
+ # @return [Integer, String, nil]
33
+ def resolve(topic_or_alias)
34
+ @aliases[topic_or_alias]
35
+ end
36
+
37
+ # @!visibility private
38
+ def assign
39
+ return false if full?
40
+
41
+ @available.pop || (@max_assigned += 1)
42
+ end
43
+
44
+ # @return [Boolean] true if there are {#max} entries in the map
45
+ def full?
46
+ @available.empty? && max_assigned >= max
47
+ end
48
+
49
+ # @return [Integer] number of entries in the map
50
+ def size
51
+ @max_assigned - @available.size
52
+ end
53
+
54
+ # @!visibility private
55
+ # Add bidirectional mapping
56
+ # @param alias_id [Integer]
57
+ # @param topic [String]
58
+ def add(alias_id, topic)
59
+ raise ProtocolError, "AliasId (#{alias_id}) must be between 1 and #{max}" unless alias_id.between?(1, max)
60
+
61
+ @max_assigned = alias_id if alias_id > @max_assigned
62
+ @cached_bytes += topic.bytesize - (@aliases[alias_id]&.bytesize || 0)
63
+ @aliases[alias_id] = topic
64
+ @aliases[topic] = alias_id
65
+ end
66
+
67
+ # @!visibility private
68
+ # Explicitly Remove a topic (a previously aliased topic later explicitly set to use alias false)
69
+ # @param topic [String]
70
+ # rubocop:disable Naming/PredicateMethod
71
+ def remove(topic)
72
+ alias_id = @aliases.delete(topic)
73
+ return false unless alias_id
74
+
75
+ @available.push(alias_id)
76
+ @cached_bytes -= @aliases.delete(alias_id)&.bytesize || 0
77
+ true
78
+ end
79
+ # rubocop:enable Naming/PredicateMethod
80
+
81
+ def topics
82
+ @aliases.keys.select { |k| k.is_a?(String) }
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end