1571 lines
		
	
	
		
			59 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			1571 lines
		
	
	
		
			59 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
--[[
 | 
						||
 | 
						||
Here is a MQTT v5.0 protocol implementation
 | 
						||
 | 
						||
MQTT v5.0 documentation (DOC):
 | 
						||
	http://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html
 | 
						||
 | 
						||
]]
 | 
						||
 | 
						||
-- module table
 | 
						||
local protocol5 = {}
 | 
						||
 | 
						||
-- load required stuff
 | 
						||
local type = type
 | 
						||
local error = error
 | 
						||
local assert = assert
 | 
						||
local ipairs = ipairs
 | 
						||
local require = require
 | 
						||
local tostring = tostring
 | 
						||
local setmetatable = setmetatable
 | 
						||
 | 
						||
local table = require("table")
 | 
						||
local unpack = table.unpack or unpack
 | 
						||
local tbl_sort = table.sort
 | 
						||
 | 
						||
local string = require("string")
 | 
						||
local str_char = string.char
 | 
						||
local fmt = string.format
 | 
						||
 | 
						||
local const = require("mqtt.const")
 | 
						||
local const_v50 = const.v50
 | 
						||
 | 
						||
local tools = require("mqtt.tools")
 | 
						||
local sortedpairs = tools.sortedpairs
 | 
						||
 | 
						||
local bit = require("mqtt.bitwrap")
 | 
						||
local bor = bit.bor
 | 
						||
local band = bit.band
 | 
						||
local lshift = bit.lshift
 | 
						||
local rshift = bit.rshift
 | 
						||
 | 
						||
local protocol = require("mqtt.protocol")
 | 
						||
local make_uint8 = protocol.make_uint8
 | 
						||
local make_uint16 = protocol.make_uint16
 | 
						||
local make_uint32 = protocol.make_uint32
 | 
						||
local make_string = protocol.make_string
 | 
						||
local make_var_length = protocol.make_var_length
 | 
						||
local parse_var_length = protocol.parse_var_length
 | 
						||
local make_uint8_0_or_1 = protocol.make_uint8_0_or_1
 | 
						||
local make_uint16_nonzero = protocol.make_uint16_nonzero
 | 
						||
local make_var_length_nonzero = protocol.make_var_length_nonzero
 | 
						||
local parse_string = protocol.parse_string
 | 
						||
local parse_uint8 = protocol.parse_uint8
 | 
						||
local parse_uint8_0_or_1 = protocol.parse_uint8_0_or_1
 | 
						||
local parse_uint16 = protocol.parse_uint16
 | 
						||
local parse_uint16_nonzero = protocol.parse_uint16_nonzero
 | 
						||
local parse_uint32 = protocol.parse_uint32
 | 
						||
local parse_var_length_nonzero = protocol.parse_var_length_nonzero
 | 
						||
local make_header = protocol.make_header
 | 
						||
local check_qos = protocol.check_qos
 | 
						||
local check_packet_id = protocol.check_packet_id
 | 
						||
local combine = protocol.combine
 | 
						||
local packet_type = protocol.packet_type
 | 
						||
local packet_mt = protocol.packet_mt
 | 
						||
local connack_packet_mt = protocol.connack_packet_mt
 | 
						||
local start_parse_packet = protocol.start_parse_packet
 | 
						||
local parse_packet_connect_input = protocol.parse_packet_connect_input
 | 
						||
 | 
						||
-- Returns true if given value is a valid Retain Handling option, DOC: 3.8.3.1 Subscription Options
 | 
						||
local function check_retain_handling(val)
 | 
						||
	return (val == 0) or (val == 1) or (val == 2)
 | 
						||
end
 | 
						||
 | 
						||
-- Create Connect Flags data, DOC: 3.1.2.3 Connect Flags
 | 
						||
local function make_connect_flags(args)
 | 
						||
	local byte = 0 -- bit 0 should be zero
 | 
						||
	-- DOC: 3.1.2.4 Clean Start
 | 
						||
	if args.clean ~= nil then
 | 
						||
		assert(type(args.clean) == "boolean", "expecting .clean to be a boolean")
 | 
						||
		if args.clean then
 | 
						||
			byte = bor(byte, lshift(1, 1))
 | 
						||
		end
 | 
						||
	end
 | 
						||
	-- DOC: 3.1.2.5 Will Flag
 | 
						||
	if args.will ~= nil then
 | 
						||
		-- check required args are presented
 | 
						||
		assert(type(args.will) == "table", "expecting .will to be a table")
 | 
						||
		assert(type(args.will.payload) == "string", "expecting .will.payload to be a string")
 | 
						||
		assert(type(args.will.topic) == "string", "expecting .will.topic to be a string")
 | 
						||
		assert(type(args.will.qos) == "number", "expecting .will.qos to be a number")
 | 
						||
		assert(check_qos(args.will.qos), "expecting .will.qos to be a valid QoS value")
 | 
						||
		assert(type(args.will.retain) == "boolean", "expecting .will.retain to be a boolean")
 | 
						||
		if args.will.properties ~= nil then
 | 
						||
			assert(type(args.will.properties) == "table", "expecting .will.properties to be a table")
 | 
						||
		end
 | 
						||
		if args.will.user_properties ~= nil then
 | 
						||
			assert(type(args.will.user_properties) == "table", "expecting .will.user_properties to be a table")
 | 
						||
		end
 | 
						||
		-- will flag should be set to 1
 | 
						||
		byte = bor(byte, lshift(1, 2))
 | 
						||
		-- DOC: 3.1.2.6 Will QoS
 | 
						||
		byte = bor(byte, lshift(args.will.qos, 3))
 | 
						||
		-- DOC: 3.1.2.7 Will Retain
 | 
						||
		if args.will.retain then
 | 
						||
			byte = bor(byte, lshift(1, 5))
 | 
						||
		end
 | 
						||
	end
 | 
						||
	-- DOC: 3.1.2.8 User Name Flag
 | 
						||
	if args.username ~= nil then
 | 
						||
		assert(type(args.username) == "string", "expecting .username to be a string")
 | 
						||
		byte = bor(byte, lshift(1, 7))
 | 
						||
	end
 | 
						||
	-- DOC: 3.1.2.9 Password Flag
 | 
						||
	if args.password ~= nil then
 | 
						||
		assert(type(args.password) == "string", "expecting .password to be a string")
 | 
						||
		assert(args.username, "the .username is required to set .password")
 | 
						||
		byte = bor(byte, lshift(1, 6))
 | 
						||
	end
 | 
						||
	return make_uint8(byte)
 | 
						||
end
 | 
						||
 | 
						||
-- Known property names and its identifiers, DOC: 2.2.2.2 Property
 | 
						||
local property_pairs = {
 | 
						||
	{ 0x01, "payload_format_indicator",
 | 
						||
		make = make_uint8_0_or_1,
 | 
						||
		parse = parse_uint8_0_or_1, },
 | 
						||
	{ 0x02, "message_expiry_interval",
 | 
						||
		make = make_uint32,
 | 
						||
		parse = parse_uint32, },
 | 
						||
	{ 0x03, "content_type",
 | 
						||
		make = make_string,
 | 
						||
		parse = parse_string, },
 | 
						||
	{ 0x08, "response_topic",
 | 
						||
		make = make_string,
 | 
						||
		parse = parse_string, },
 | 
						||
	{ 0x09, "correlation_data",
 | 
						||
		make = make_string,
 | 
						||
		parse = parse_string, },
 | 
						||
	{ 0x0B, "subscription_identifiers",
 | 
						||
		make = function(value) return str_char(make_var_length_nonzero(value)) end,
 | 
						||
		parse = parse_var_length_nonzero,
 | 
						||
		multiple = true, },
 | 
						||
	{ 0x11, "session_expiry_interval",
 | 
						||
		make = make_uint32,
 | 
						||
		parse = parse_uint32, },
 | 
						||
	{ 0x12, "assigned_client_identifier",
 | 
						||
		make = make_string,
 | 
						||
		parse = parse_string, },
 | 
						||
	{ 0x13, "server_keep_alive",
 | 
						||
		make = make_uint16,
 | 
						||
		parse = parse_uint16, },
 | 
						||
	{ 0x15, "authentication_method",
 | 
						||
		make = make_string,
 | 
						||
		parse = parse_string, },
 | 
						||
	{ 0x16, "authentication_data",
 | 
						||
		make = make_string,
 | 
						||
		parse = parse_string, },
 | 
						||
	{ 0x17, "request_problem_information",
 | 
						||
		make = make_uint8_0_or_1,
 | 
						||
		parse = parse_uint8_0_or_1, },
 | 
						||
	{ 0x18, "will_delay_interval",
 | 
						||
		make = make_uint32,
 | 
						||
		parse = parse_uint32, },
 | 
						||
	{ 0x19, "request_response_information",
 | 
						||
		make = make_uint8_0_or_1,
 | 
						||
		parse = parse_uint8_0_or_1, },
 | 
						||
	{ 0x1A, "response_information",
 | 
						||
		make = make_string,
 | 
						||
		parse = parse_string, },
 | 
						||
	{ 0x1C, "server_reference",
 | 
						||
		make = make_string,
 | 
						||
		parse = parse_string, },
 | 
						||
	{ 0x1F, "reason_string",
 | 
						||
		make = make_string,
 | 
						||
		parse = parse_string, },
 | 
						||
	{ 0x21, "receive_maximum",
 | 
						||
		make = make_uint16,
 | 
						||
		parse = parse_uint16, },
 | 
						||
	{ 0x22, "topic_alias_maximum",
 | 
						||
		make = make_uint16,
 | 
						||
		parse = parse_uint16, },
 | 
						||
	{ 0x23, "topic_alias",
 | 
						||
		make = make_uint16_nonzero,
 | 
						||
		parse = parse_uint16_nonzero, },
 | 
						||
	{ 0x24, "maximum_qos",
 | 
						||
		make = make_uint8_0_or_1,
 | 
						||
		parse = parse_uint8_0_or_1, },
 | 
						||
	{ 0x25, "retain_available",
 | 
						||
		make = make_uint8_0_or_1,
 | 
						||
		parse = parse_uint8_0_or_1, },
 | 
						||
	{ 0x26, "user_property", -- NOTE: not implemented intentionally
 | 
						||
		make = function(value_) error("not implemented") end, -- luacheck: ignore
 | 
						||
		parse = function(read_func_) error("not implemented") end, }, -- luacheck: ignore
 | 
						||
	{ 0x27, "maximum_packet_size",
 | 
						||
		make = make_uint32,
 | 
						||
		parse = parse_uint32, },
 | 
						||
	{ 0x28, "wildcard_subscription_available",
 | 
						||
		make = make_uint8_0_or_1,
 | 
						||
		parse = parse_uint8_0_or_1, },
 | 
						||
	{ 0x29, "subscription_identifiers_available",
 | 
						||
		make = make_uint8_0_or_1,
 | 
						||
		parse = parse_uint8_0_or_1, },
 | 
						||
	{ 0x2A, "shared_subscription_available",
 | 
						||
		make = make_uint8_0_or_1,
 | 
						||
		parse = parse_uint8_0_or_1, },
 | 
						||
}
 | 
						||
 | 
						||
-- properties table with keys in two directions: from name to identifier and back
 | 
						||
local properties = {}
 | 
						||
-- table with property value make functions
 | 
						||
local property_make = {}
 | 
						||
-- table with property value parse function
 | 
						||
local property_parse = {}
 | 
						||
-- table with property multiple flag
 | 
						||
local property_multiple = {}
 | 
						||
-- fill the properties and property_make tables
 | 
						||
for _, prop in ipairs(property_pairs) do
 | 
						||
	properties[prop[2]] = prop[1]				-- name ==> identifier
 | 
						||
	properties[prop[1]] = prop[2]				-- identifier ==> name
 | 
						||
	property_make[prop[1]] = prop.make			-- identifier ==> make function
 | 
						||
	property_parse[prop[1]] = prop.parse		-- identifier ==> make function
 | 
						||
	property_multiple[prop[1]] = prop.multiple	-- identifier ==> multiple flag
 | 
						||
end
 | 
						||
 | 
						||
-- Allowed properties per packet type
 | 
						||
local allowed_properties = {
 | 
						||
	[packet_type.CONNECT] = {
 | 
						||
		[0x11] = true, -- DOC: 3.1.2.11.2 Session Expiry Interval
 | 
						||
		[0x21] = true, -- DOC: 3.1.2.11.3 Receive Maximum
 | 
						||
		[0x27] = true, -- DOC: 3.1.2.11.4 Maximum Packet Size
 | 
						||
		[0x22] = true, -- DOC: 3.1.2.11.5 Topic Alias Maximum
 | 
						||
		[0x19] = true, -- DOC: 3.1.2.11.6 Request Response Information
 | 
						||
		[0x17] = true, -- DOC: 3.1.2.11.7 Request Problem Information
 | 
						||
		[0x26] = true, -- DOC: 3.1.2.11.8 User Property
 | 
						||
		[0x15] = true, -- DOC: 3.1.2.11.9 Authentication Method
 | 
						||
		[0x16] = true, -- DOC: 3.1.2.11.10 Authentication Data
 | 
						||
	},
 | 
						||
	[packet_type.CONNACK] = {
 | 
						||
		[0x11] = true, -- DOC: 3.2.2.3.2 Session Expiry Interval
 | 
						||
		[0x21] = true, -- DOC: 3.2.2.3.3 Receive Maximum
 | 
						||
		[0x24] = true, -- DOC: 3.2.2.3.4 Maximum QoS
 | 
						||
		[0x25] = true, -- DOC: 3.2.2.3.5 Retain Available
 | 
						||
		[0x27] = true, -- DOC: 3.2.2.3.6 Maximum Packet Size
 | 
						||
		[0x12] = true, -- DOC: 3.2.2.3.7 Assigned Client Identifier
 | 
						||
		[0x22] = true, -- DOC: 3.2.2.3.8 Topic Alias Maximum
 | 
						||
		[0x1F] = true, -- DOC: 3.2.2.3.9 Reason String
 | 
						||
		[0x26] = true, -- DOC: 3.2.2.3.10 User Property
 | 
						||
		[0x28] = true, -- DOC: 3.2.2.3.11 Wildcard Subscription Available
 | 
						||
		[0x29] = true, -- DOC: 3.2.2.3.12 Subscription Identifiers Available
 | 
						||
		[0x2A] = true, -- DOC: 3.2.2.3.13 Shared Subscription Available
 | 
						||
		[0x13] = true, -- DOC: 3.2.2.3.14 Server Keep Alive
 | 
						||
		[0x1A] = true, -- DOC: 3.2.2.3.15 Response Information
 | 
						||
		[0x1C] = true, -- DOC: 3.2.2.3.16 Server Reference
 | 
						||
		[0x15] = true, -- DOC: 3.2.2.3.17 Authentication Method
 | 
						||
		[0x16] = true, -- DOC: 3.2.2.3.18 Authentication Data
 | 
						||
	},
 | 
						||
	[packet_type.PUBLISH] = {
 | 
						||
		[0x01] = true, -- DOC: 3.3.2.3.2 Payload Format Indicator
 | 
						||
		[0x02] = true, -- DOC: 3.3.2.3.3 Message Expiry Interval
 | 
						||
		[0x23] = true, -- DOC: 3.3.2.3.4 Topic Alias
 | 
						||
		[0x08] = true, -- DOC: 3.3.2.3.5 Response Topic
 | 
						||
		[0x09] = true, -- DOC: 3.3.2.3.6 Correlation Data
 | 
						||
		[0x26] = true, -- DOC: 3.3.2.3.7 User Property
 | 
						||
		[0x0B] = true, -- DOC: 3.3.2.3.8 Subscription Identifier
 | 
						||
		[0x03] = true, -- DOC: 3.3.2.3.9 Content Type
 | 
						||
	},
 | 
						||
	will = {
 | 
						||
		[0x18] = true, -- DOC: 3.1.3.2.2 Will Delay Interval
 | 
						||
		[0x01] = true, -- DOC: 3.1.3.2.3 Payload Format Indicator
 | 
						||
		[0x02] = true, -- DOC: 3.1.3.2.4 Message Expiry Interval
 | 
						||
		[0x03] = true, -- DOC: 3.1.3.2.5 Content Type
 | 
						||
		[0x08] = true, -- DOC: 3.1.3.2.6 Response Topic
 | 
						||
		[0x09] = true, -- DOC: 3.1.3.2.7 Correlation Data
 | 
						||
		[0x26] = true, -- DOC: 3.1.3.2.8 User Property
 | 
						||
	},
 | 
						||
	[packet_type.PUBACK] = {
 | 
						||
		[0x1F] = true, -- DOC: 3.4.2.2.2 Reason String
 | 
						||
		[0x26] = true, -- DOC: 3.4.2.2.3 User Property
 | 
						||
	},
 | 
						||
	[packet_type.PUBREC] = {
 | 
						||
		[0x1F] = true, -- DOC: 3.5.2.2.2 Reason String
 | 
						||
		[0x26] = true, -- DOC: 3.5.2.2.3 User Property
 | 
						||
	},
 | 
						||
	[packet_type.PUBREL] = {
 | 
						||
		[0x1F] = true, -- DOC: 3.6.2.2.2 Reason String
 | 
						||
		[0x26] = true, -- DOC: 3.6.2.2.3 User Property
 | 
						||
	},
 | 
						||
	[packet_type.PUBCOMP] = {
 | 
						||
		[0x1F] = true, -- DOC: 3.7.2.2.2 Reason String
 | 
						||
		[0x26] = true, -- DOC: 3.7.2.2.3 User Property
 | 
						||
	},
 | 
						||
	[packet_type.SUBSCRIBE] = {
 | 
						||
		[0x0B] = { multiple=false }, -- DOC: 3.8.2.1.2 Subscription Identifier -- DOC: It is a Protocol Error to include the Subscription Identifier more than once.
 | 
						||
		[0x26] = true, -- DOC: 3.8.2.1.3 User Property
 | 
						||
	},
 | 
						||
	[packet_type.SUBACK] = {
 | 
						||
		[0x1F] = true, -- DOC: 3.9.2.1.2 Reason String
 | 
						||
		[0x26] = true, -- DOC: 3.9.2.1.3 User Property
 | 
						||
	},
 | 
						||
	[packet_type.UNSUBSCRIBE] = {
 | 
						||
		[0x26] = true, -- DOC: 3.10.2.1.2 User Property
 | 
						||
	},
 | 
						||
	[packet_type.UNSUBACK] = {
 | 
						||
		[0x1F] = true, -- DOC: 3.11.2.1.2 Reason String
 | 
						||
		[0x26] = true, -- DOC: 3.11.2.1.3 User Property
 | 
						||
	},
 | 
						||
	-- NOTE: PINGREQ (3.12), PINGRESP (3.13) has no properties
 | 
						||
	[packet_type.DISCONNECT] = {
 | 
						||
		[0x11] = true, -- DOC: 3.14.2.2.2 Session Expiry Interval
 | 
						||
		[0x1F] = true, -- DOC: 3.14.2.2.3 Reason String
 | 
						||
		[0x26] = true, -- DOC: 3.14.2.2.4 User Property
 | 
						||
		[0x1C] = true, -- DOC: 3.14.2.2.5 Server Reference
 | 
						||
	},
 | 
						||
	[packet_type.AUTH] = {
 | 
						||
		[0x15] = true, -- DOC: 3.15.2.2.2 Authentication Method
 | 
						||
		[0x16] = true, -- DOC: 3.15.2.2.3 Authentication Data
 | 
						||
		[0x1F] = true, -- DOC: 3.15.2.2.4 Reason String
 | 
						||
		[0x26] = true, -- DOC: 3.15.2.2.5 User Property
 | 
						||
	},
 | 
						||
}
 | 
						||
 | 
						||
-- Create properties field for various control packets, DOC: 2.2.2 Properties
 | 
						||
local function make_properties(ptype, args)
 | 
						||
	local allowed = assert(allowed_properties[ptype], "invalid packet type to detect allowed properties")
 | 
						||
	local props = ""
 | 
						||
	local uprop_id = properties.user_property
 | 
						||
	-- writing known properties
 | 
						||
	if args.properties ~= nil then
 | 
						||
		assert(type(args.properties) == "table", "expecting .properties to be a table")
 | 
						||
		-- validate all properties and append them to order list
 | 
						||
		local order = {}
 | 
						||
		for name, value in sortedpairs(args.properties) do
 | 
						||
			assert(type(name) == "string", "expecting property name to be a string: "..tostring(name))
 | 
						||
			-- detect property identifier and check it's allowed for that packet type
 | 
						||
			local prop_id = assert(properties[name], "unknown property: "..tostring(name))
 | 
						||
			assert(prop_id ~= uprop_id, "user properties should be passed in .user_properties table")
 | 
						||
			if not allowed[prop_id] then
 | 
						||
				error("property "..name.." is not allowed for packet type "..packet_type[ptype])
 | 
						||
			end
 | 
						||
			order[#order + 1] = { prop_id, name, value }
 | 
						||
		end
 | 
						||
		-- sort props in the identifier ascending order
 | 
						||
		tbl_sort(order, function(a, b) return a[1] < b[1] end)
 | 
						||
		for _, item in ipairs(order) do
 | 
						||
			local prop_id, name,  value = unpack(item)
 | 
						||
			if property_multiple[prop_id] then
 | 
						||
				assert(type(value) == "table", "expecting list-table for property with multiple value")
 | 
						||
				assert(#value == 1, "only one value for multiple-property supported")
 | 
						||
				value = value[1]
 | 
						||
			end
 | 
						||
			-- make property data
 | 
						||
			local ok, val = pcall(property_make[prop_id], value)
 | 
						||
			if not ok then
 | 
						||
				error("invalid property value: "..name.." = "..tostring(value)..": "..tostring(val))
 | 
						||
			end
 | 
						||
			local prop = combine(
 | 
						||
				str_char(make_var_length(prop_id)),
 | 
						||
				val
 | 
						||
			)
 | 
						||
			-- and append it to props
 | 
						||
			if type(props) == "string" then
 | 
						||
				props = combine(prop)
 | 
						||
			else
 | 
						||
				props:append(prop)
 | 
						||
			end
 | 
						||
		end
 | 
						||
	end
 | 
						||
	-- writing userproperties
 | 
						||
	if args.user_properties ~= nil then
 | 
						||
		assert(type(args.user_properties) == "table", "expecting .user_properties to be a table")
 | 
						||
		assert(allowed[uprop_id], "user_property is not allowed for packet type "..ptype)
 | 
						||
		local order = {}
 | 
						||
		local dups = {}
 | 
						||
		if args.user_properties[1] then
 | 
						||
			-- at first use array items as they given as {name, value} pairs with stable order
 | 
						||
			for i, pair in ipairs(args.user_properties) do
 | 
						||
				-- validate types for name and value
 | 
						||
				if type(pair) ~= "table" then
 | 
						||
					error(fmt("user property at position %d should be {name, value} table", i))
 | 
						||
				end
 | 
						||
				if type(pair[1]) ~= "string" then
 | 
						||
					error(fmt("user property name at position %d should be a string", i))
 | 
						||
				end
 | 
						||
				if type(pair[2]) ~= "string" then
 | 
						||
					error(fmt("user property '%s' value at position %d should be a string", pair[1], i))
 | 
						||
				end
 | 
						||
				order[i] = pair
 | 
						||
				dups[pair[1]] = pair[2]
 | 
						||
			end
 | 
						||
		end
 | 
						||
		-- now add the rest of user properties given as string table keys
 | 
						||
		for name, val in sortedpairs(args.user_properties) do
 | 
						||
			if type(name) ~= "number" then -- skipping number keys as they already added above
 | 
						||
				-- validate types for name and value
 | 
						||
				if type(name) ~= "string" then
 | 
						||
					error(fmt("user property name '%s' should be a string", name))
 | 
						||
				end
 | 
						||
				if type(val) ~= "string" then
 | 
						||
					error(fmt("user property '%s' value '%s' should be a string", name, val))
 | 
						||
				end
 | 
						||
				-- check that name+value key already added
 | 
						||
				if dups[name] ~= val then
 | 
						||
					order[#order + 1] = {name, val}
 | 
						||
				end
 | 
						||
			end
 | 
						||
		end
 | 
						||
		for _, pair in ipairs(order) do
 | 
						||
			local name = pair[1]
 | 
						||
			local value = pair[2]
 | 
						||
			-- make user property data
 | 
						||
			local prop = combine(
 | 
						||
				str_char(make_var_length(uprop_id)),
 | 
						||
				make_string(name),
 | 
						||
				make_string(value)
 | 
						||
			)
 | 
						||
			-- and append it to props
 | 
						||
			if type(props) == "string" then
 | 
						||
				props = combine(prop)
 | 
						||
			else
 | 
						||
				props:append(prop)
 | 
						||
			end
 | 
						||
		end
 | 
						||
	end
 | 
						||
	-- and combine properties with its length field
 | 
						||
	return combine(
 | 
						||
		str_char(make_var_length(props:len())),		-- DOC: 2.2.2.1 Property Length
 | 
						||
		props										-- DOC: 2.2.2.2 Property
 | 
						||
	)
 | 
						||
end
 | 
						||
 | 
						||
-- Create CONNECT packet, DOC: 3.1 CONNECT – Connection Request
 | 
						||
local function make_packet_connect(args)
 | 
						||
	-- check args
 | 
						||
	assert(type(args.id) == "string", "expecting .id to be a string with MQTT client id")
 | 
						||
	-- DOC: 3.1.2.10 Keep Alive
 | 
						||
	local keep_alive_ival = 0
 | 
						||
	if args.keep_alive then
 | 
						||
		assert(type(args.keep_alive) == "number")
 | 
						||
		keep_alive_ival = args.keep_alive
 | 
						||
	end
 | 
						||
	-- DOC: 3.1.2.11 CONNECT Properties
 | 
						||
	local props = make_properties(packet_type.CONNECT, args)
 | 
						||
	-- DOC: 3.1.2 CONNECT Variable Header
 | 
						||
	local variable_header = combine(
 | 
						||
		make_string("MQTT"), 				-- DOC: 3.1.2.1 Protocol Name
 | 
						||
		make_uint8(5), 						-- DOC: 3.1.2.2 Protocol Version (5 is for MQTT v5.0)
 | 
						||
		make_connect_flags(args), 			-- DOC: 3.1.2.3 Connect Flags
 | 
						||
		make_uint16(keep_alive_ival), 		-- DOC: 3.1.2.10 Keep Alive
 | 
						||
		props								-- DOC: 3.1.2.11 CONNECT Properties
 | 
						||
	)
 | 
						||
	-- DOC: 3.1.3 CONNECT Payload
 | 
						||
	-- DOC: 3.1.3.1 Client Identifier (ClientID)
 | 
						||
	local payload = combine(
 | 
						||
		make_string(args.id)
 | 
						||
	)
 | 
						||
	if args.will then
 | 
						||
		-- DOC: 3.1.3.2 Will Properties
 | 
						||
		payload:append(make_properties("will", args.will))
 | 
						||
		-- DOC: 3.1.3.3 Will Topic
 | 
						||
		assert(type(args.will.topic) == "string", "expecting will.topic to be a string")
 | 
						||
		payload:append(make_string(args.will.topic))
 | 
						||
		-- DOC: 3.1.3.4 Will Payload
 | 
						||
		assert(args.will.payload == nil or type(args.will.payload) == "string", "expecting will.payload to be a string or nil")
 | 
						||
		payload:append(make_string(args.will.payload))
 | 
						||
	end
 | 
						||
	if args.username then
 | 
						||
		-- DOC: 3.1.3.5 User Name
 | 
						||
		payload:append(make_string(args.username))
 | 
						||
		if args.password then
 | 
						||
			-- DOC: 3.1.3.6 Password
 | 
						||
			payload:append(make_string(args.password))
 | 
						||
		end
 | 
						||
	end
 | 
						||
	-- DOC: 3.1.1 Fixed header
 | 
						||
	local header = make_header(packet_type.CONNECT, 0, variable_header:len() + payload:len())
 | 
						||
	return combine(header, variable_header, payload)
 | 
						||
end
 | 
						||
 | 
						||
-- Create CONNACK packet, DOC: 3.2 CONNACK – Connect acknowledgement
 | 
						||
local function make_packet_connack(args)
 | 
						||
	-- check args
 | 
						||
	assert(type(args.sp) == "boolean", "expecting .sp to be a boolean with Session Present flag")
 | 
						||
	assert(type(args.rc) == "number", "expecting .rc to be a number with Connect Reason Code")
 | 
						||
	-- DOC: 3.2.2 CONNACK Variable Header
 | 
						||
	local props = make_properties(packet_type.CONNACK, args)
 | 
						||
	local variable_header = combine(
 | 
						||
		make_uint8(args.sp and 1 or 0),		-- DOC: 3.2.2.1.1 Session Present
 | 
						||
		make_uint8(args.rc),				-- DOC: 3.2.2.2 Connect Reason Code
 | 
						||
		props								-- DOC: 3.2.2.3 CONNACK Properties
 | 
						||
	)
 | 
						||
	-- DOC: 3.2.3 CONNACK Payload
 | 
						||
	-- DOC: The CONNACK packet has no Payload.
 | 
						||
	-- DOC: 3.2.1 CONNACK Fixed Header
 | 
						||
	local header = make_header(packet_type.CONNACK, 0, variable_header:len())
 | 
						||
	return combine(header, variable_header)
 | 
						||
end
 | 
						||
 | 
						||
-- Create PUBLISH packet, DOC: 3.3 PUBLISH – Publish message
 | 
						||
local function make_packet_publish(args)
 | 
						||
	-- check args
 | 
						||
	assert(type(args.topic) == "string", "expecting .topic to be a string")
 | 
						||
	if args.payload ~= nil then
 | 
						||
		assert(type(args.payload) == "string", "expecting .payload to be a string")
 | 
						||
	end
 | 
						||
	if args.qos ~= nil then
 | 
						||
		assert(type(args.qos) == "number", "expecting .qos to be a number")
 | 
						||
		assert(check_qos(args.qos), "expecting .qos to be a valid QoS value")
 | 
						||
	end
 | 
						||
	if args.retain ~= nil then
 | 
						||
		assert(type(args.retain) == "boolean", "expecting .retain to be a boolean")
 | 
						||
	end
 | 
						||
	if args.dup ~= nil then
 | 
						||
		assert(type(args.dup) == "boolean", "expecting .dup to be a boolean")
 | 
						||
	end
 | 
						||
 | 
						||
	-- DOC: 3.3.1 PUBLISH Fixed Header
 | 
						||
	local flags = 0
 | 
						||
	-- 3.3.1.3 RETAIN
 | 
						||
	if args.retain then
 | 
						||
		flags = bor(flags, 0x1)
 | 
						||
	end
 | 
						||
	-- DOC: 3.3.1.2 QoS
 | 
						||
	flags = bor(flags, lshift(args.qos or 0, 1))
 | 
						||
	-- DOC: 3.3.1.1 DUP
 | 
						||
	if args.dup then
 | 
						||
		flags = bor(flags, lshift(1, 3))
 | 
						||
	end
 | 
						||
	-- DOC: 3.3.2 PUBLISH Variable Header
 | 
						||
	local variable_header = combine(
 | 
						||
		make_string(args.topic)
 | 
						||
	)
 | 
						||
	-- DOC: 3.3.2.2 Packet Identifier
 | 
						||
	if args.qos and args.qos > 0 then
 | 
						||
		assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
 | 
						||
		assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
 | 
						||
		variable_header:append(make_uint16(args.packet_id))
 | 
						||
	end
 | 
						||
	-- DOC: 3.3.2.3 PUBLISH Properties
 | 
						||
	variable_header:append(make_properties(packet_type.PUBLISH, args))
 | 
						||
	-- DOC: 3.3.3 PUBLISH Payload
 | 
						||
	local payload
 | 
						||
	if args.payload then
 | 
						||
		payload = args.payload
 | 
						||
	else
 | 
						||
		payload = ""
 | 
						||
	end
 | 
						||
	-- DOC: 3.3.1 Fixed header
 | 
						||
	local header = make_header(packet_type.PUBLISH, flags, variable_header:len() + payload:len())
 | 
						||
	return combine(header, variable_header, payload)
 | 
						||
end
 | 
						||
 | 
						||
-- Create PUBACK packet, DOC: 3.4 PUBACK – Publish acknowledgement
 | 
						||
local function make_packet_puback(args)
 | 
						||
	-- check args
 | 
						||
	assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
 | 
						||
	assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
 | 
						||
	assert(type(args.rc) == "number", "expecting .rc to be a number")
 | 
						||
	-- DOC: 3.4.2 PUBACK Variable Header
 | 
						||
	local variable_header = combine(make_uint16(args.packet_id))
 | 
						||
	local props = make_properties(packet_type.PUBACK, args)		-- DOC: 3.4.2.2 PUBACK Properties
 | 
						||
	-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties. In this case the PUBACK has a Remaining Length of 2.
 | 
						||
	if props:len() > 1 or args.rc ~= 0 then
 | 
						||
		variable_header:append(make_uint8(args.rc))				-- DOC: 3.4.2.1 PUBACK Reason Code
 | 
						||
		variable_header:append(props)							-- DOC: 3.4.2.2 PUBACK Properties
 | 
						||
	end
 | 
						||
	-- DOC: 3.4.1 PUBACK Fixed Header
 | 
						||
	local header = make_header(packet_type.PUBACK, 0, variable_header:len())
 | 
						||
	return combine(header, variable_header)
 | 
						||
end
 | 
						||
 | 
						||
-- Create PUBREC packet, DOC: 3.5 PUBREC – Publish received (QoS 2 delivery part 1)
 | 
						||
local function make_packet_pubrec(args)
 | 
						||
	-- check args
 | 
						||
	assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
 | 
						||
	assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
 | 
						||
	assert(type(args.rc) == "number", "expecting .rc to be a number")
 | 
						||
	-- DOC: 3.5.2 PUBREC Variable Header
 | 
						||
	local variable_header = combine(make_uint16(args.packet_id))
 | 
						||
	local props = make_properties(packet_type.PUBREC, args)		-- DOC: 3.5.2.2 PUBREC Properties
 | 
						||
	-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties. In this case the PUBREC has a Remaining Length of 2.
 | 
						||
	if props:len() > 1 or args.rc ~= 0 then
 | 
						||
		variable_header:append(make_uint8(args.rc))				-- DOC: 3.5.2.1 PUBREC Reason Code
 | 
						||
		variable_header:append(props)							-- DOC: 3.5.2.2 PUBREC Properties
 | 
						||
	end
 | 
						||
	-- DOC: 3.5.1 PUBREC Fixed Header
 | 
						||
	local header = make_header(packet_type.PUBREC, 0, variable_header:len())
 | 
						||
	return combine(header, variable_header)
 | 
						||
end
 | 
						||
 | 
						||
-- Create PUBREL packet, DOC: 3.6 PUBREL – Publish release (QoS 2 delivery part 2)
 | 
						||
local function make_packet_pubrel(args)
 | 
						||
	-- check args
 | 
						||
	assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
 | 
						||
	assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
 | 
						||
	assert(type(args.rc) == "number", "expecting .rc to be a number")
 | 
						||
	-- DOC: 3.6.2 PUBREL Variable Header
 | 
						||
	local variable_header = combine(make_uint16(args.packet_id))
 | 
						||
	local props = make_properties(packet_type.PUBREL, args)		-- DOC: 3.6.2.2 PUBREL Properties
 | 
						||
	-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties. In this case the PUBREL has a Remaining Length of 2.
 | 
						||
	if props:len() > 1 or args.rc ~= 0 then
 | 
						||
		variable_header:append(make_uint8(args.rc))				-- DOC: 3.6.2.1 PUBREL Reason Code
 | 
						||
		variable_header:append(props)							-- DOC: 3.6.2.2 PUBREL Properties
 | 
						||
	end
 | 
						||
	-- DOC: 3.6.1 PUBREL Fixed Header
 | 
						||
	local header = make_header(packet_type.PUBREL, 2, variable_header:len()) -- flags: fixed 0010 bits, DOC: Figure 3‑14 – PUBREL packet Fixed Header
 | 
						||
	return combine(header, variable_header)
 | 
						||
end
 | 
						||
 | 
						||
-- Create PUBCOMP packet, DOC: 3.7 PUBCOMP – Publish complete (QoS 2 delivery part 3)
 | 
						||
local function make_packet_pubcomp(args)
 | 
						||
	-- check args
 | 
						||
	assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
 | 
						||
	assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
 | 
						||
	assert(type(args.rc) == "number", "expecting .rc to be a number")
 | 
						||
	-- DOC: 3.7.2 PUBCOMP Variable Header
 | 
						||
	local variable_header = combine(make_uint16(args.packet_id))
 | 
						||
	local props = make_properties(packet_type.PUBCOMP, args)	-- DOC: 3.7.2.2 PUBCOMP Properties
 | 
						||
	-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties. In this case the PUBCOMP has a Remaining Length of 2.
 | 
						||
	if props:len() > 1 or args.rc ~= 0 then
 | 
						||
		variable_header:append(make_uint8(args.rc))				-- DOC: 3.7.2.1 PUBCOMP Reason Code
 | 
						||
		variable_header:append(props)							-- DOC: 3.7.2.2 PUBCOMP Properties
 | 
						||
	end
 | 
						||
	-- DOC: 3.7.1 PUBCOMP Fixed Header
 | 
						||
	local header = make_header(packet_type.PUBCOMP, 0, variable_header:len())
 | 
						||
	return combine(header, variable_header)
 | 
						||
end
 | 
						||
 | 
						||
-- Create SUBSCRIBE packet, DOC: 3.8 SUBSCRIBE - Subscribe request
 | 
						||
local function make_packet_subscribe(args)
 | 
						||
	-- check args
 | 
						||
	assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
 | 
						||
	assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
 | 
						||
	assert(type(args.subscriptions) == "table", "expecting .subscriptions to be a table")
 | 
						||
	assert(#args.subscriptions > 0, "expecting .subscriptions to be a non-empty array")
 | 
						||
	-- DOC: 3.8.2 SUBSCRIBE Variable Header
 | 
						||
	local variable_header = combine(
 | 
						||
		make_uint16(args.packet_id),
 | 
						||
		make_properties(packet_type.SUBSCRIBE, args)	-- DOC: 3.8.2.1 SUBSCRIBE Properties
 | 
						||
	)
 | 
						||
	-- DOC: 3.8.3 SUBSCRIBE Payload
 | 
						||
	local payload = combine()
 | 
						||
	for i, subscription in ipairs(args.subscriptions) do
 | 
						||
		assert(type(subscription) == "table", "expecting .subscriptions["..i.."] to be a table")
 | 
						||
		assert(type(subscription.topic) == "string", "expecting .subscriptions["..i.."].topic to be a string")
 | 
						||
		if subscription.qos ~= nil then
 | 
						||
			assert(type(subscription.qos) == "number", "expecting .subscriptions["..i.."].qos to be a number")
 | 
						||
			assert(check_qos(subscription.qos), "expecting .subscriptions["..i.."].qos to be a valid QoS value")
 | 
						||
		end
 | 
						||
		if subscription.retain_as_published ~= nil then
 | 
						||
			assert(type(subscription.retain_as_published) == "boolean", "expecting .subscriptions["..i.."].retain_as_published to be a boolean")
 | 
						||
		end
 | 
						||
		if subscription.retain_handling ~= nil then
 | 
						||
			assert(type(subscription.retain_handling) == "number", "expecting .subscriptions["..i.."].retain_handling to be a number")
 | 
						||
			assert(check_retain_handling(subscription.retain_handling), "expecting .subscriptions["..i.."].retain_handling to be a valid Retain Handling option")
 | 
						||
		end
 | 
						||
		-- DOC: 3.8.3.1 Subscription Options
 | 
						||
		local so = subscription.qos or 0
 | 
						||
		if subscription.no_local then
 | 
						||
			so = bor(so, 4) -- set Bit 2
 | 
						||
		end
 | 
						||
		if subscription.retain_as_published then
 | 
						||
			so = bor(so, 8) -- set Bit 3
 | 
						||
		end
 | 
						||
		if subscription.retain_handling then
 | 
						||
			so = bor(so, lshift(subscription.retain_handling, 4)) -- set Bit 4 and 5
 | 
						||
		end
 | 
						||
		payload:append(make_string(subscription.topic))
 | 
						||
		payload:append(make_uint8(so))
 | 
						||
	end
 | 
						||
	-- DOC: 3.8.1 SUBSCRIBE Fixed Header
 | 
						||
	local header = make_header(packet_type.SUBSCRIBE, 2, variable_header:len() + payload:len()) -- flags: fixed 0010 bits, DOC: Figure 3‑18 SUBSCRIBE packet Fixed Header
 | 
						||
	return combine(header, variable_header, payload)
 | 
						||
end
 | 
						||
 | 
						||
-- Create SUBACK packet, DOC: 3.9 SUBACK – Subscribe acknowledgement
 | 
						||
local function make_packet_suback(args)
 | 
						||
	-- check args
 | 
						||
	assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
 | 
						||
	assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
 | 
						||
	assert(type(args.rc) == "table", "expecting .rc to be a table")
 | 
						||
	assert(#args.rc > 0, "expecting .rc to be a non-empty array")
 | 
						||
	-- DOC: 3.9.2 SUBACK Variable Header
 | 
						||
	local variable_header = combine(
 | 
						||
		make_uint16(args.packet_id),
 | 
						||
		make_properties(packet_type.SUBACK, args)	-- DOC: 3.9.2.1 SUBACK Properties
 | 
						||
	)
 | 
						||
	-- DOC: 3.9.3 SUBACK Payload
 | 
						||
	local payload = combine()
 | 
						||
	for i, rc in ipairs(args.rc) do
 | 
						||
		assert(type(rc) == "number", "expecting .rc["..i.."] to be a number")
 | 
						||
		assert(rc >= 0 and rc <= 255, "expecting .rc["..i.."] to be in range [0, 255]")
 | 
						||
		payload:append(make_uint8(rc))
 | 
						||
	end
 | 
						||
	-- DOC: 3.9.1 SUBACK Fixed Header
 | 
						||
	local header = make_header(packet_type.SUBACK, 0, variable_header:len() + payload:len()) -- NOTE: fixed flags value 0x0
 | 
						||
	return combine(header, variable_header, payload)
 | 
						||
end
 | 
						||
 | 
						||
-- Create UNSUBSCRIBE packet, DOC: 3.10 UNSUBSCRIBE – Unsubscribe request
 | 
						||
local function make_packet_unsubscribe(args)
 | 
						||
	-- check args
 | 
						||
	assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
 | 
						||
	assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
 | 
						||
	assert(type(args.subscriptions) == "table", "expecting .subscriptions to be a table")
 | 
						||
	assert(#args.subscriptions > 0, "expecting .subscriptions to be a non-empty array")
 | 
						||
	-- DOC: 3.10.2 UNSUBSCRIBE Variable Header
 | 
						||
	local variable_header = combine(
 | 
						||
		make_uint16(args.packet_id),
 | 
						||
		make_properties(packet_type.UNSUBSCRIBE, args)	-- DOC: 3.10.2.1 UNSUBSCRIBE Properties
 | 
						||
	)
 | 
						||
	-- DOC: 3.10.3 UNSUBSCRIBE Payload
 | 
						||
	local payload = combine()
 | 
						||
	for i, subscription in ipairs(args.subscriptions) do
 | 
						||
		assert(type(subscription) == "string", "expecting .subscriptions["..i.."] to be a string")
 | 
						||
		payload:append(make_string(subscription))
 | 
						||
	end
 | 
						||
	-- DOC: 3.10.1 UNSUBSCRIBE Fixed Header
 | 
						||
	local header = make_header(packet_type.UNSUBSCRIBE, 2, variable_header:len() + payload:len()) -- flags: fixed 0010 bits, DOC: Figure 3.28 – UNSUBSCRIBE packet Fixed Header
 | 
						||
	return combine(header, variable_header, payload)
 | 
						||
end
 | 
						||
 | 
						||
-- Create UNSUBACK packet, DOC: 3.11 UNSUBACK – Unsubscribe acknowledgement
 | 
						||
local function make_packet_unsuback(args)
 | 
						||
	-- check args
 | 
						||
	assert(type(args.packet_id) == "number", "expecting .packet_id to be a number")
 | 
						||
	assert(check_packet_id(args.packet_id), "expecting .packet_id to be a valid Packet Identifier")
 | 
						||
	assert(type(args.rc) == "table", "expecting .rc to be a table")
 | 
						||
	assert(#args.rc > 0, "expecting .rc to be a non-empty array")
 | 
						||
	-- DOC: 3.11.2 UNSUBACK Variable Header
 | 
						||
	local variable_header = combine(
 | 
						||
		make_uint16(args.packet_id),
 | 
						||
		make_properties(packet_type.UNSUBACK, args)	-- DOC: 3.11.2.1 UNSUBACK Properties
 | 
						||
	)
 | 
						||
	-- DOC: 3.11.3 UNSUBACK Payload
 | 
						||
	local payload = combine()
 | 
						||
	for i, rc in ipairs(args.rc) do
 | 
						||
		assert(type(rc) == "number", "expecting .rc["..i.."] to be a number")
 | 
						||
		assert(rc >= 0 and rc <= 255, "expecting .rc["..i.."] to be in range [0, 255]")
 | 
						||
		payload:append(make_uint8(rc))
 | 
						||
	end
 | 
						||
	-- DOC: 3.11.1 UNSUBACK Fixed Header
 | 
						||
	local header = make_header(packet_type.UNSUBACK, 0, variable_header:len() + payload:len()) -- NOTE: fixed flags value 0x0
 | 
						||
	return combine(header, variable_header, payload)
 | 
						||
end
 | 
						||
 | 
						||
-- Create DISCONNECT packet, DOC: 3.14 DISCONNECT – Disconnect notification
 | 
						||
local function make_packet_disconnect(args)
 | 
						||
	-- check args
 | 
						||
	assert(type(args.rc) == "number", "expecting .rc to be a number")
 | 
						||
	-- DOC: 3.14.2 DISCONNECT Variable Header
 | 
						||
	local variable_header = combine()
 | 
						||
	local props = make_properties(packet_type.DISCONNECT, args)	-- DOC: 3.14.2.2 DISCONNECT Properties
 | 
						||
	-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Normal disconnecton) and there are no Properties. In this case the DISCONNECT has a Remaining Length of 0.
 | 
						||
	if props:len() > 1 or args.rc ~= 0 then
 | 
						||
		variable_header:append(make_uint8(args.rc))				-- DOC: 3.14.2.1 Disconnect Reason Code
 | 
						||
		variable_header:append(props)							-- DOC: 3.14.2.2 DISCONNECT Properties
 | 
						||
	end
 | 
						||
	-- DOC: 3.14.1 DISCONNECT Fixed Header
 | 
						||
	local header = make_header(packet_type.DISCONNECT, 0, variable_header:len()) -- flags: 0
 | 
						||
	return combine(header, variable_header)
 | 
						||
end
 | 
						||
 | 
						||
-- Create AUTH packet, DOC: 3.15 AUTH – Authentication exchange
 | 
						||
local function make_packet_auth(args)
 | 
						||
	-- check args
 | 
						||
	assert(type(args.rc) == "number", "expecting .rc to be a number")
 | 
						||
	-- DOC: 3.15.2 AUTH Variable Header
 | 
						||
	local variable_header = combine()
 | 
						||
	-- DOC: The Reason Code and Property Length can be omitted if the Reason Code is 0x00 (Success) and there are no Properties. In this case the AUTH has a Remaining Length of 0.
 | 
						||
	local props = make_properties(packet_type.AUTH, args)		-- DOC: 3.15.2.2 AUTH Properties
 | 
						||
	if props:len() > 1 or args.rc ~= 0 then
 | 
						||
		variable_header:append(make_uint8(args.rc))				-- DOC: 3.15.2.1 Authenticate Reason Code
 | 
						||
		variable_header:append(props)							-- DOC: 3.15.2.2 AUTH Properties
 | 
						||
	end
 | 
						||
	-- DOC: 3.15.1 AUTH Fixed Header
 | 
						||
	local header = make_header(packet_type.AUTH, 0, variable_header:len())
 | 
						||
	return combine(header, variable_header)
 | 
						||
end
 | 
						||
 | 
						||
-- Create packet of given {type: number} in args
 | 
						||
function protocol5.make_packet(args)
 | 
						||
	assert(type(args) == "table", "expecting args to be a table")
 | 
						||
	assert(type(args.type) == "number", "expecting .type number in args")
 | 
						||
	local ptype = args.type
 | 
						||
	if ptype == packet_type.CONNECT then			-- 1
 | 
						||
		return make_packet_connect(args)
 | 
						||
	elseif ptype == packet_type.CONNACK then		-- 3
 | 
						||
		return make_packet_connack(args)
 | 
						||
	elseif ptype == packet_type.PUBLISH then		-- 3
 | 
						||
		return make_packet_publish(args)
 | 
						||
	elseif ptype == packet_type.PUBACK then			-- 4
 | 
						||
		return make_packet_puback(args)
 | 
						||
	elseif ptype == packet_type.PUBREC then			-- 5
 | 
						||
		return make_packet_pubrec(args)
 | 
						||
	elseif ptype == packet_type.PUBREL then			-- 6
 | 
						||
		return make_packet_pubrel(args)
 | 
						||
	elseif ptype == packet_type.PUBCOMP then		-- 7
 | 
						||
		return make_packet_pubcomp(args)
 | 
						||
	elseif ptype == packet_type.SUBSCRIBE then		-- 8
 | 
						||
		return make_packet_subscribe(args)
 | 
						||
	elseif ptype == packet_type.SUBACK then			-- 9
 | 
						||
		return make_packet_suback(args)
 | 
						||
	elseif ptype == packet_type.UNSUBSCRIBE then	-- 10
 | 
						||
		return make_packet_unsubscribe(args)
 | 
						||
	elseif ptype == packet_type.UNSUBACK then		-- 11
 | 
						||
		return make_packet_unsuback(args)
 | 
						||
	elseif ptype == packet_type.PINGREQ then		-- 12
 | 
						||
		-- DOC: 3.12 PINGREQ – PING request
 | 
						||
		return combine("\192\000") -- 192 == 0xC0, type == 12, flags == 0
 | 
						||
	elseif ptype == packet_type.PINGRESP then		-- 12
 | 
						||
		-- DOC: 3.13 PINGRESP – PING response
 | 
						||
		return combine("\208\000") -- 208 == 0xD0, type == 13, flags == 0
 | 
						||
	elseif ptype == packet_type.DISCONNECT then		-- 14
 | 
						||
		return make_packet_disconnect(args)
 | 
						||
	elseif ptype == packet_type.AUTH then			-- 15
 | 
						||
		return make_packet_auth(args)
 | 
						||
	else
 | 
						||
		error("unexpected packet type to make: "..ptype)
 | 
						||
	end
 | 
						||
end
 | 
						||
 | 
						||
-- Parse properties using given read_data function for specified packet type
 | 
						||
-- Result will be stored in packet.properties and packet.user_properties
 | 
						||
-- Returns false plus string with error message on failure
 | 
						||
-- Returns true on success
 | 
						||
local function parse_properties(ptype, read_data, input, packet)
 | 
						||
	assert(type(read_data) == "function", "expecting read_data to be a function")
 | 
						||
	-- DOC: 2.2.2 Properties
 | 
						||
	-- parse Property Length
 | 
						||
	-- create read_func for parse_var_length and other parse functions, reading from data string instead of network connector
 | 
						||
	local len, err = parse_var_length(read_data)
 | 
						||
	if not len then
 | 
						||
		return false, "failed to parse properties length: "..err
 | 
						||
	end
 | 
						||
	-- check data contains enough bytes for reading properties
 | 
						||
	if input.available < len then
 | 
						||
		return false, "not enough data to parse properties of length "..len
 | 
						||
	end
 | 
						||
	-- ensure properties and user_properties are presented in packet
 | 
						||
	if not packet.properties then
 | 
						||
		packet.properties = {}
 | 
						||
	end
 | 
						||
	if not packet.user_properties then
 | 
						||
		packet.user_properties = {}
 | 
						||
	end
 | 
						||
	local uprops = packet.user_properties
 | 
						||
	-- parse allowed properties
 | 
						||
	local uprop_id = properties.user_property
 | 
						||
	local allowed = assert(allowed_properties[ptype], "no allowed properties for specified packet type: "..tostring(ptype))
 | 
						||
	local props_end = input[1] + len
 | 
						||
	while input[1] < props_end do
 | 
						||
		-- property id, DOC: 2.2.2.2 Property
 | 
						||
		local prop_id
 | 
						||
		prop_id, err = parse_var_length(read_data)
 | 
						||
		if not prop_id then
 | 
						||
			return false, "failed to parse property length: "..err
 | 
						||
		end
 | 
						||
		if not allowed[prop_id] then
 | 
						||
			if not allowed[prop_id] then
 | 
						||
				return false, "property "..tostring(properties[prop_id]).." ("..prop_id..") is not allowed for that packet type"
 | 
						||
			end
 | 
						||
		end
 | 
						||
		if prop_id == uprop_id then
 | 
						||
			-- parse name=value string pair
 | 
						||
			local name, value
 | 
						||
			name, err = parse_string(read_data)
 | 
						||
			if not name then
 | 
						||
				return false, "failed to parse user property name: "..err
 | 
						||
			end
 | 
						||
			value, err = parse_string(read_data)
 | 
						||
			if not value then
 | 
						||
				return false, "failed to parse user property value: "..err
 | 
						||
			end
 | 
						||
			local old_val = uprops[name]
 | 
						||
			if old_val ~= nil then
 | 
						||
				-- ensure uprops contains pairs with name = <old value>
 | 
						||
				local found = false
 | 
						||
				for _, pair in ipairs(uprops) do
 | 
						||
					if pair[1] == name and pair[2] == old_val then
 | 
						||
						found = true
 | 
						||
						break
 | 
						||
					end
 | 
						||
				end
 | 
						||
				if not found then
 | 
						||
					uprops[#uprops + 1] = {name, old_val}
 | 
						||
				end
 | 
						||
				uprops[#uprops + 1] = {name, value}
 | 
						||
			end
 | 
						||
			uprops[name] = value
 | 
						||
		else
 | 
						||
			-- parse property value according its identifier
 | 
						||
			local value
 | 
						||
			value, err = property_parse[prop_id](read_data)
 | 
						||
			if err then
 | 
						||
				return false, "failed to parse property "..prop_id.." value: "..err
 | 
						||
			end
 | 
						||
			if allowed[prop_id] ~= true then
 | 
						||
				if packet.properties[properties[prop_id]] ~= nil then
 | 
						||
					return false, "it is a Protocol Error to include the "..properties[prop_id].." ("..prop_id..") property more than once"
 | 
						||
				end
 | 
						||
			end
 | 
						||
			-- make an array of property values, if it's allowed to send multiple such properties
 | 
						||
			if property_multiple[prop_id] then
 | 
						||
				local curr = packet.properties[properties[prop_id]] or {}
 | 
						||
				curr[#curr + 1] = value
 | 
						||
				packet.properties[properties[prop_id]] = curr
 | 
						||
			else
 | 
						||
				packet.properties[properties[prop_id]] = value
 | 
						||
			end
 | 
						||
		end
 | 
						||
	end
 | 
						||
	return true
 | 
						||
end
 | 
						||
 | 
						||
-- Parse CONNACK packet, DOC: 3.2 CONNACK – Connect acknowledgement
 | 
						||
local function parse_packet_connack(ptype, flags, input)
 | 
						||
	-- DOC: 3.2.1 CONNACK Fixed Header
 | 
						||
	if flags ~= 0 then -- Reserved
 | 
						||
		return false, packet_type[ptype]..": unexpected flags value: "..flags
 | 
						||
	end
 | 
						||
	if input.available < 3 then
 | 
						||
		return false, packet_type[ptype]..": expecting data of length 3 bytes or more"
 | 
						||
	end
 | 
						||
	local read_data = input.read_func
 | 
						||
	-- DOC: 3.2.2 CONNACK Variable Header
 | 
						||
	-- DOC: 3.2.2.1.1 Session Present
 | 
						||
	-- DOC: 3.2.2.2 Connect Reason Code
 | 
						||
	local byte1, byte2 = parse_uint8(read_data), parse_uint8(read_data)
 | 
						||
	local sp = (band(byte1, 0x1) ~= 0)
 | 
						||
	local packet = setmetatable({type=ptype, sp=sp, rc=byte2}, connack_packet_mt)
 | 
						||
	-- DOC: 3.2.2.3 CONNACK Properties
 | 
						||
	local ok, err = parse_properties(ptype, read_data, input, packet)
 | 
						||
	if not ok then
 | 
						||
		return false, packet_type[ptype]..": failed to parse packet properties: "..err
 | 
						||
	end
 | 
						||
	return packet
 | 
						||
end
 | 
						||
 | 
						||
-- Parse PUBLISH packet, DOC: 3.3 PUBLISH – Publish message
 | 
						||
local function parse_packet_publish(ptype, flags, input)
 | 
						||
	-- DOC: 3.3.1 PUBLISH Fixed Header
 | 
						||
	-- DOC: 3.3.1.1 DUP
 | 
						||
	local dup = (band(flags, 0x8) ~= 0)
 | 
						||
	-- DOC: 3.3.1.2 QoS
 | 
						||
	local qos = band(rshift(flags, 1), 0x3)
 | 
						||
	-- DOC: 3.3.1.3 RETAIN
 | 
						||
	local retain = (band(flags, 0x1) ~= 0)
 | 
						||
	-- DOC: 3.3.2 PUBLISH Variable Header
 | 
						||
	-- DOC: 3.3.2.1 Topic Name
 | 
						||
	local read_data = input.read_func
 | 
						||
	local topic, err = parse_string(read_data)
 | 
						||
	if not topic then
 | 
						||
		return false, packet_type[ptype]..": failed to parse topic: "..err
 | 
						||
	end
 | 
						||
	-- DOC: 3.3.2.2 Packet Identifier
 | 
						||
	local packet_id, ok
 | 
						||
	if qos > 0 then
 | 
						||
		packet_id, err = parse_uint16(read_data)
 | 
						||
		if not packet_id then
 | 
						||
			return false, packet_type[ptype]..": failed to parse packet_id: "..err
 | 
						||
		end
 | 
						||
	end
 | 
						||
	-- DOC: 3.3.2.3 PUBLISH Properties
 | 
						||
	local packet = setmetatable({type=ptype, dup=dup, qos=qos, retain=retain, packet_id=packet_id, topic=topic}, packet_mt)
 | 
						||
	ok, err = parse_properties(ptype, read_data, input, packet)
 | 
						||
	if not ok then
 | 
						||
		return false, packet_type[ptype]..": failed to parse packet properties: "..err
 | 
						||
	end
 | 
						||
	if input.available > 0 then
 | 
						||
		-- DOC: 3.3.3 PUBLISH Payload
 | 
						||
		packet.payload = read_data(input.available)
 | 
						||
	end
 | 
						||
	return packet
 | 
						||
end
 | 
						||
 | 
						||
-- Parse PUBACK packet, DOC: 3.4 PUBACK – Publish acknowledgement
 | 
						||
local function parse_packet_puback(ptype, flags, input)
 | 
						||
	-- DOC: 3.4.1 PUBACK Fixed Header
 | 
						||
	if flags ~= 0 then -- Reserved
 | 
						||
		return false, packet_type[ptype]..": unexpected flags value: "..flags
 | 
						||
	end
 | 
						||
	local read_data = input.read_func
 | 
						||
	-- DOC: 3.4.2 PUBACK Variable Header
 | 
						||
	local packet_id, err = parse_uint16(read_data)
 | 
						||
	if not packet_id then
 | 
						||
		return false, packet_type[ptype]..": failed to parse packet_id: "..err
 | 
						||
	end
 | 
						||
	local packet = setmetatable({type=ptype, packet_id=packet_id, rc=0, properties={}, user_properties={}}, packet_mt)
 | 
						||
	if input.available > 0 then
 | 
						||
		-- DOC: 3.4.2.1 PUBACK Reason Code
 | 
						||
		local rc, ok
 | 
						||
		rc, err = parse_uint8(read_data)
 | 
						||
		if not rc then
 | 
						||
			return false, packet_type[ptype]..": failed to parse rc: "..err
 | 
						||
		end
 | 
						||
		packet.rc = rc
 | 
						||
		-- DOC: 3.4.2.2 PUBACK Properties
 | 
						||
		ok, err = parse_properties(ptype, read_data, input, packet)
 | 
						||
		if not ok then
 | 
						||
			return false, packet_type[ptype]..": failed to parse packet properties: "..err
 | 
						||
		end
 | 
						||
	end
 | 
						||
	return packet
 | 
						||
end
 | 
						||
 | 
						||
-- Parse PUBREC packet, DOC: 3.5 PUBREC – Publish received (QoS 2 delivery part 1)
 | 
						||
local function parse_packet_pubrec(ptype, flags, input)
 | 
						||
	-- DOC: 3.5.1 PUBREC Fixed Header
 | 
						||
	if flags ~= 0 then -- Reserved
 | 
						||
		return false, packet_type[ptype]..": unexpected flags value: "..flags
 | 
						||
	end
 | 
						||
	local read_data = input.read_func
 | 
						||
	-- DOC: 3.5.2 PUBREC Variable Header
 | 
						||
	local packet_id, err = parse_uint16(read_data)
 | 
						||
	if not packet_id then
 | 
						||
		return false, packet_type[ptype]..": failed to parse packet_id: "..err
 | 
						||
	end
 | 
						||
	local packet = setmetatable({type=ptype, packet_id=packet_id, rc=0, properties={}, user_properties={}}, packet_mt)
 | 
						||
	if input.available > 0 then
 | 
						||
		-- DOC: 3.5.2.1 PUBREC Reason Code
 | 
						||
		local rc
 | 
						||
		rc, err = parse_uint8(read_data)
 | 
						||
		if not rc then
 | 
						||
			return false, packet_type[ptype]..": failed to parse rc: "..err
 | 
						||
		end
 | 
						||
		packet.rc = rc
 | 
						||
		-- DOC: 3.5.2.2 PUBREC Properties
 | 
						||
		local ok
 | 
						||
		ok, err = parse_properties(ptype, read_data, input, packet)
 | 
						||
		if not ok then
 | 
						||
			return false, packet_type[ptype]..": failed to parse packet properties: "..err
 | 
						||
		end
 | 
						||
	end
 | 
						||
	return packet
 | 
						||
end
 | 
						||
 | 
						||
-- Parse PUBREL packet, DOC: 3.6 PUBREL – Publish release (QoS 2 delivery part 2)
 | 
						||
local function parse_packet_pubrel(ptype, flags, input)
 | 
						||
	-- DOC: 3.6.1 PUBREL Fixed Header
 | 
						||
	if flags ~= 2 then -- Reserved
 | 
						||
		return false, packet_type[ptype]..": unexpected flags value: "..flags
 | 
						||
	end
 | 
						||
	local read_data = input.read_func
 | 
						||
	-- DOC: 3.6.2 PUBREL Variable Header
 | 
						||
	local packet_id, err = parse_uint16(read_data)
 | 
						||
	if not packet_id then
 | 
						||
		return false, packet_type[ptype]..": failed to parse packet_id: "..err
 | 
						||
	end
 | 
						||
	local packet = setmetatable({type=ptype, packet_id=packet_id, rc=0, properties={}, user_properties={}}, packet_mt)
 | 
						||
	if input.available > 0 then
 | 
						||
		-- DOC: 3.6.2.1 PUBREL Reason Code
 | 
						||
		local rc
 | 
						||
		rc, err = parse_uint8(read_data)
 | 
						||
		if not rc then
 | 
						||
			return false, packet_type[ptype]..": failed to parse rc: "..err
 | 
						||
		end
 | 
						||
		packet.rc = rc
 | 
						||
		-- DOC: 3.6.2.2 PUBREL Properties
 | 
						||
		local ok
 | 
						||
		ok, err = parse_properties(ptype, read_data, input, packet)
 | 
						||
		if not ok then
 | 
						||
			return false, packet_type[ptype]..": failed to parse packet properties: "..err
 | 
						||
		end
 | 
						||
	end
 | 
						||
	return packet
 | 
						||
end
 | 
						||
 | 
						||
-- Parse PUBCOMP packet, DOC: 3.7 PUBCOMP – Publish complete (QoS 2 delivery part 3)
 | 
						||
local function parse_packet_pubcomp(ptype, flags, input)
 | 
						||
	-- DOC: 3.7.1 PUBCOMP Fixed Header
 | 
						||
	if flags ~= 0 then -- Reserved
 | 
						||
		return false, packet_type[ptype]..": unexpected flags value: "..flags
 | 
						||
	end
 | 
						||
	local read_data = input.read_func
 | 
						||
	-- DOC: 3.7.2 PUBCOMP Variable Header
 | 
						||
	local packet_id, err = parse_uint16(read_data)
 | 
						||
	if not packet_id then
 | 
						||
		return false, packet_type[ptype]..": failed to parse packet_id: "..err
 | 
						||
	end
 | 
						||
	local packet = setmetatable({type=ptype, packet_id=packet_id, rc=0, properties={}, user_properties={}}, packet_mt)
 | 
						||
	if input.available > 0 then
 | 
						||
		-- DOC: 3.7.2.1 PUBCOMP Reason Code
 | 
						||
		local rc
 | 
						||
		rc, err = parse_uint8(read_data)
 | 
						||
		if not rc then
 | 
						||
			return false, packet_type[ptype]..": failed to parse rc: "..err
 | 
						||
		end
 | 
						||
		packet.rc = rc
 | 
						||
		-- DOC: 3.7.2.2 PUBCOMP Properties
 | 
						||
		local ok
 | 
						||
		ok, err = parse_properties(ptype, read_data, input, packet)
 | 
						||
		if not ok then
 | 
						||
			return false, packet_type[ptype]..": failed to parse packet properties: "..err
 | 
						||
		end
 | 
						||
	end
 | 
						||
	return packet
 | 
						||
end
 | 
						||
 | 
						||
-- Parse SUBSCRIBE packet, DOC: 3.8 SUBSCRIBE - Subscribe request
 | 
						||
local function parse_packet_subscribe(ptype, flags, input)
 | 
						||
	-- DOC: 3.8.1 SUBSCRIBE Fixed Header
 | 
						||
	if flags ~= 2 then -- Reserved
 | 
						||
		return false, packet_type[ptype]..": unexpected flags value: "..flags
 | 
						||
	end
 | 
						||
	local read_data = input.read_func
 | 
						||
	-- DOC: 3.8.2 SUBSCRIBE Variable Header
 | 
						||
	local packet_id, err = parse_uint16(read_data)
 | 
						||
	if not packet_id then
 | 
						||
		return false, packet_type[ptype]..": failed to parse packet_id: "..err
 | 
						||
	end
 | 
						||
	local packet = setmetatable({type=ptype, packet_id=packet_id, properties={}, user_properties={}}, packet_mt)
 | 
						||
	-- DOC: 3.8.2.1 SUBSCRIBE Properties
 | 
						||
	local ok
 | 
						||
	ok, err = parse_properties(ptype, read_data, input, packet)
 | 
						||
	if not ok then
 | 
						||
		return false, packet_type[ptype]..": failed to parse packet properties: "..err
 | 
						||
	end
 | 
						||
	-- DOC: 3.8.3 SUBSCRIBE Payload
 | 
						||
	if input.available == 0 then
 | 
						||
		-- DOC: A SUBSCRIBE packet with no Payload is a Protocol Error.
 | 
						||
		return false, packet_type[ptype]..": empty subscriptions list"
 | 
						||
	end
 | 
						||
	local subscriptions = {}
 | 
						||
	while input.available > 0 do
 | 
						||
		local topic_filter
 | 
						||
		topic_filter, err = parse_string(input.read_func)
 | 
						||
		if not topic_filter then
 | 
						||
			return false, packet_type[ptype]..": failed to parse SUBSCRIBE topic filter: "..err
 | 
						||
		end
 | 
						||
		-- DOC: 3.8.3.1 Subscription Options
 | 
						||
		local subscription_options
 | 
						||
		subscription_options, err = parse_uint8(input.read_func)
 | 
						||
		if not subscription_options then
 | 
						||
			return false, packet_type[ptype]..": failed to parse subscription_options: "..err
 | 
						||
		end
 | 
						||
		subscriptions[#subscriptions + 1] = {
 | 
						||
			topic = topic_filter,
 | 
						||
			qos = band(subscription_options, 0x3),
 | 
						||
			no_local = band(subscription_options, 0x4) ~= 0,
 | 
						||
			retain_as_published = band(subscription_options, 0x8) ~= 0, -- Retain As Published
 | 
						||
			retain_handling = band(rshift(subscription_options, 4), 0x3), -- Retain Handling
 | 
						||
		}
 | 
						||
	end
 | 
						||
	packet.subscriptions = subscriptions
 | 
						||
	return packet
 | 
						||
end
 | 
						||
 | 
						||
-- SUBACK return codes/reason code strings
 | 
						||
-- DOC: Table 3‑8 - Subscribe Reason Codes
 | 
						||
local suback_rc = {
 | 
						||
	[0x00] = "Granted QoS 0",
 | 
						||
	[0x01] = "Granted QoS 1",
 | 
						||
	[0x02] = "Granted QoS 2",
 | 
						||
	[0x80] = "Unspecified error",
 | 
						||
	[0x83] = "Implementation specific error",
 | 
						||
	[0x87] = "Not authorized",
 | 
						||
	[0x8F] = "Topic Filter invalid",
 | 
						||
	[0x91] = "Packet Identifier in use",
 | 
						||
	[0x97] = "Quota exceeded",
 | 
						||
	[0x9E] = "Shared Subscriptions not supported",
 | 
						||
	[0xA1] = "Subscription Identifiers not supported",
 | 
						||
	[0xA2] = "Wildcard Subscriptions not supported",
 | 
						||
}
 | 
						||
protocol5.suback_rc = suback_rc
 | 
						||
 | 
						||
--- Parsed SUBACK packet metatable
 | 
						||
local suback_packet_mt = {
 | 
						||
	__tostring = protocol.packet_tostring, -- packet-to-human-readable-string conversion metamethod using protocol.packet_tostring()
 | 
						||
	reason_strings = function(self) -- Returns reason strings for the SUBACK packet according to its rc field
 | 
						||
		local human_readable = {}
 | 
						||
		for i, rc in ipairs(self.rc) do
 | 
						||
			local reason_string = suback_rc[rc]
 | 
						||
			if reason_string then
 | 
						||
				human_readable[i] = reason_string
 | 
						||
			else
 | 
						||
				human_readable[i] = "Unknown: "..tostring(rc)
 | 
						||
			end
 | 
						||
		end
 | 
						||
		return human_readable
 | 
						||
	end,
 | 
						||
}
 | 
						||
suback_packet_mt.__index = suback_packet_mt
 | 
						||
protocol5.suback_packet_mt = suback_packet_mt
 | 
						||
 | 
						||
-- Parse SUBACK packet, DOC: 3.9 SUBACK – Subscribe acknowledgement
 | 
						||
local function parse_packet_suback(ptype, flags, input)
 | 
						||
	-- DOC: 3.9.1 SUBACK Fixed Header
 | 
						||
	if flags ~= 0 then -- Reserved
 | 
						||
		return false, packet_type[ptype]..": unexpected flags value: "..flags
 | 
						||
	end
 | 
						||
	local read_data = input.read_func
 | 
						||
	-- DOC: 3.9.2 SUBACK Variable Header
 | 
						||
	local packet_id, err = parse_uint16(read_data)
 | 
						||
	if not packet_id then
 | 
						||
		return false, packet_type[ptype]..": failed to parse packet_id: "..err
 | 
						||
	end
 | 
						||
	-- DOC: 3.9.2.1 SUBACK Properties
 | 
						||
	local packet = setmetatable({type=ptype, packet_id=packet_id}, suback_packet_mt)
 | 
						||
	local ok
 | 
						||
	ok, err = parse_properties(ptype, read_data, input, packet)
 | 
						||
	if not ok then
 | 
						||
		return false, packet_type[ptype]..": failed to parse packet properties: "..err
 | 
						||
	end
 | 
						||
	-- DOC: 3.9.3 SUBACK Payload
 | 
						||
	local rcs = {}
 | 
						||
	while input.available > 0 do
 | 
						||
		local rc
 | 
						||
		rc, err = parse_uint8(read_data)
 | 
						||
		if not rc then
 | 
						||
			return false, packet_type[ptype]..": failed to parse reason code: "..err
 | 
						||
		end
 | 
						||
		rcs[#rcs + 1] = rc
 | 
						||
	end
 | 
						||
	if not next(rcs) then
 | 
						||
		return false, packet_type[ptype]..": expecting at least one reason code"
 | 
						||
	end
 | 
						||
	packet.rc = rcs
 | 
						||
	return packet
 | 
						||
end
 | 
						||
 | 
						||
-- Parse UNSUBSCRIBE packet, DOC: 3.10 UNSUBSCRIBE – Unsubscribe request
 | 
						||
local function parse_packet_unsubscribe(ptype, flags, input)
 | 
						||
	-- DOC: 3.10.1 UNSUBSCRIBE Fixed Header
 | 
						||
	if flags ~= 2 then -- Reserved
 | 
						||
		return false, packet_type[ptype]..": unexpected flags value: "..flags
 | 
						||
	end
 | 
						||
	local read_data = input.read_func
 | 
						||
	-- DOC: 3.10.2 UNSUBSCRIBE Variable Header
 | 
						||
	local packet_id, err = parse_uint16(read_data)
 | 
						||
	if not packet_id then
 | 
						||
		return false, packet_type[ptype]..": failed to parse packet_id: "..err
 | 
						||
	end
 | 
						||
	local packet = setmetatable({type=ptype, packet_id=packet_id, properties={}, user_properties={}}, packet_mt)
 | 
						||
	-- DOC: 3.10.2.1 UNSUBSCRIBE Properties
 | 
						||
	local ok
 | 
						||
	ok, err = parse_properties(ptype, read_data, input, packet)
 | 
						||
	if not ok then
 | 
						||
		return false, packet_type[ptype]..": failed to parse packet properties: "..err
 | 
						||
	end
 | 
						||
	-- 3.10.3 UNSUBSCRIBE Payload
 | 
						||
	-- DOC: An UNSUBSCRIBE packet with no Payload is a Protocol Error.
 | 
						||
	if input.available == 0 then
 | 
						||
		return false, packet_type[ptype]..": empty subscriptions list"
 | 
						||
	end
 | 
						||
	local subscriptions = {}
 | 
						||
	while input.available > 0 do
 | 
						||
		local topic_filter
 | 
						||
		topic_filter, err = parse_string(input.read_func)
 | 
						||
		if not topic_filter then
 | 
						||
			return false, packet_type[ptype]..": failed to parse topic filter: "..err
 | 
						||
		end
 | 
						||
		subscriptions[#subscriptions + 1] = topic_filter
 | 
						||
	end
 | 
						||
	packet.subscriptions = subscriptions
 | 
						||
	return packet
 | 
						||
end
 | 
						||
 | 
						||
-- UNSUBACK Reason Codes
 | 
						||
-- DOC[2]: Table 3‑9 - Unsubscribe Reason Codes
 | 
						||
local unsuback_rc = {
 | 
						||
	[0x00] = "Success",
 | 
						||
	[0x11] = "No subscription existed",
 | 
						||
	[0x80] = "Unspecified error",
 | 
						||
	[0x83] = "Implementation specific error",
 | 
						||
	[0x87] = "Not authorized",
 | 
						||
	[0x8F] = "Topic Filter invalid",
 | 
						||
	[0x91] = "Packet Identifier in use",
 | 
						||
}
 | 
						||
protocol5.unsuback_rc = unsuback_rc
 | 
						||
 | 
						||
--- Parsed UNSUBACK packet metatable
 | 
						||
local unsuback_packet_mt = {
 | 
						||
	__tostring = protocol.packet_tostring, -- packet-to-human-readable-string conversion metamethod using protocol.packet_tostring()
 | 
						||
	reason_strings = function(self) -- Returns reason strings for the UNSUBACK packet according to its rc field
 | 
						||
		local human_readable = {}
 | 
						||
		for i, rc in ipairs(self.rc) do
 | 
						||
			local reason_string = unsuback_rc[rc]
 | 
						||
			if reason_string then
 | 
						||
				human_readable[i] = reason_string
 | 
						||
			else
 | 
						||
				human_readable[i] = "Unknown: "..tostring(rc)
 | 
						||
			end
 | 
						||
		end
 | 
						||
		return human_readable
 | 
						||
	end,
 | 
						||
}
 | 
						||
unsuback_packet_mt.__index = unsuback_packet_mt
 | 
						||
protocol5.unsuback_packet_mt = unsuback_packet_mt
 | 
						||
 | 
						||
-- Parse UNSUBACK packet, DOC: 3.11 UNSUBACK – Unsubscribe acknowledgement
 | 
						||
local function parse_packet_unsuback(ptype, flags, input)
 | 
						||
	-- DOC: 3.11.1 UNSUBACK Fixed Header
 | 
						||
	if flags ~= 0 then -- Reserved
 | 
						||
		return false, packet_type[ptype]..": unexpected flags value: "..flags
 | 
						||
	end
 | 
						||
	local read_data = input.read_func
 | 
						||
	-- DOC: 3.11.2 UNSUBACK Variable Header
 | 
						||
	local packet_id, err = parse_uint16(read_data)
 | 
						||
	if not packet_id then
 | 
						||
		return false, packet_type[ptype]..": failed to parse packet_id: "..err
 | 
						||
	end
 | 
						||
	-- 3.11.2.1 UNSUBACK Properties
 | 
						||
	local packet = setmetatable({type=ptype, packet_id=packet_id}, unsuback_packet_mt)
 | 
						||
	local ok
 | 
						||
	ok, err = parse_properties(ptype, read_data, input, packet)
 | 
						||
	if not ok then
 | 
						||
		return false, packet_type[ptype]..": failed to parse packet properties: "..err
 | 
						||
	end
 | 
						||
	-- 3.11.3 UNSUBACK Payload
 | 
						||
	local rcs = {}
 | 
						||
	while input.available > 0 do
 | 
						||
		local rc
 | 
						||
		rc, err = parse_uint8(read_data)
 | 
						||
		if not rc then
 | 
						||
			return false, packet_type[ptype]..": failed to parse reason code: "..err
 | 
						||
		end
 | 
						||
		rcs[#rcs + 1] = rc
 | 
						||
	end
 | 
						||
	if not next(rcs) then
 | 
						||
		return false, packet_type[ptype]..": expecting at least one reason code in"
 | 
						||
	end
 | 
						||
	packet.rc = rcs
 | 
						||
	return packet
 | 
						||
end
 | 
						||
 | 
						||
-- Parse PINGREQ packet, DOC: 3.12 PINGREQ – PING request
 | 
						||
local function parse_packet_pingreq(ptype, flags, _)
 | 
						||
	-- DOC: 3.12.1 PINGREQ Fixed Header
 | 
						||
	if flags ~= 0 then -- Reserved
 | 
						||
		return false, packet_type[ptype]..": unexpected flags value: "..flags
 | 
						||
	end
 | 
						||
	return setmetatable({type=ptype, properties={}, user_properties={}}, packet_mt)
 | 
						||
end
 | 
						||
 | 
						||
-- Parse PINGRESP packet, DOC: 3.13 PINGRESP – PING response
 | 
						||
local function parse_packet_pingresp(ptype, flags, _)
 | 
						||
	-- DOC: 3.13.1 PINGRESP Fixed Header
 | 
						||
	if flags ~= 0 then -- Reserved
 | 
						||
		return false, packet_type[ptype]..": unexpected flags value: "..flags
 | 
						||
	end
 | 
						||
	return setmetatable({type=ptype, properties={}, user_properties={}}, packet_mt)
 | 
						||
end
 | 
						||
 | 
						||
-- DISCONNECT reason codes
 | 
						||
-- DOC: Table 3‑10 – Disconnect Reason Code values
 | 
						||
local disconnect_rc = {
 | 
						||
	[0x00] = "Normal disconnection",
 | 
						||
	[0x04] = "Disconnect with Will Message",
 | 
						||
	[0x80] = "Unspecified error",
 | 
						||
	[0x81] = "Malformed Packet",
 | 
						||
	[0x82] = "Protocol Error",
 | 
						||
	[0x83] = "Implementation specific error",
 | 
						||
	[0x87] = "Not authorized",
 | 
						||
	[0x89] = "Server busy",
 | 
						||
	[0x8B] = "Server shutting down",
 | 
						||
	[0x8D] = "Keep Alive timeout",
 | 
						||
	[0x8E] = "Session taken over",
 | 
						||
	[0x8F] = "Topic Filter invalid",
 | 
						||
	[0x90] = "Topic Name invalid",
 | 
						||
	[0x93] = "Receive Maximum exceeded",
 | 
						||
	[0x94] = "Topic Alias invalid",
 | 
						||
	[0x95] = "Packet too large",
 | 
						||
	[0x96] = "Message rate too high",
 | 
						||
	[0x97] = "Quota exceeded",
 | 
						||
	[0x98] = "Administrative action",
 | 
						||
	[0x99] = "Payload format invalid",
 | 
						||
	[0x9A] = "Retain not supported",
 | 
						||
	[0x9B] = "QoS not supported",
 | 
						||
	[0x9C] = "Use another server",
 | 
						||
	[0x9D] = "Server moved",
 | 
						||
	[0x9E] = "Shared Subscriptions not supported",
 | 
						||
	[0x9F] = "Connection rate exceeded",
 | 
						||
	[0xA0] = "Maximum connect time",
 | 
						||
	[0xA1] = "Subscription Identifiers not supported",
 | 
						||
	[0xA2] = "Wildcard Subscriptions not supported",
 | 
						||
}
 | 
						||
protocol5.disconnect_rc = disconnect_rc
 | 
						||
 | 
						||
--- Parsed DISCONNECT packet metatable
 | 
						||
local disconnect_packet_mt = {
 | 
						||
	__tostring = protocol.packet_tostring, -- packet-to-human-readable-string conversion metamethod using protocol.packet_tostring()
 | 
						||
	reason_string = function(self) -- Returns reason string for the DISCONNECT packet according to its rc field
 | 
						||
		local reason_string = disconnect_rc[self.rc]
 | 
						||
		if not reason_string then
 | 
						||
			reason_string = "Unknown: "..self.rc
 | 
						||
		end
 | 
						||
		return reason_string
 | 
						||
	end,
 | 
						||
}
 | 
						||
disconnect_packet_mt.__index = disconnect_packet_mt
 | 
						||
protocol5.disconnect_packet_mt = disconnect_packet_mt
 | 
						||
 | 
						||
-- Parse DISCONNECT packet, DOC: 3.14 DISCONNECT – Disconnect notification
 | 
						||
local function parse_packet_disconnect(ptype, flags, input)
 | 
						||
	-- DOC: 3.14.1 DISCONNECT Fixed Header
 | 
						||
	if flags ~= 0 then -- Reserved
 | 
						||
		return false, packet_type[ptype]..": unexpected flags value: "..flags
 | 
						||
	end
 | 
						||
	local read_data = input.read_func
 | 
						||
	local packet = setmetatable({type=ptype, rc=0, properties={}, user_properties={}}, disconnect_packet_mt)
 | 
						||
	if input.available > 0 then
 | 
						||
		-- DOC: 3.14.2 DISCONNECT Variable Header
 | 
						||
		-- DOC: 3.14.2.1 Disconnect Reason Code
 | 
						||
		local rc, err = parse_uint8(read_data)
 | 
						||
		if not rc then
 | 
						||
			return false, packet_type[ptype]..": failed to parse rc: "..err
 | 
						||
		end
 | 
						||
		packet.rc = rc
 | 
						||
		-- DOC: 3.14.2.2 DISCONNECT Properties
 | 
						||
		local ok
 | 
						||
		ok, err = parse_properties(ptype, read_data, input, packet)
 | 
						||
		if not ok then
 | 
						||
			return false, packet_type[ptype]..": failed to parse packet properties: "..err
 | 
						||
		end
 | 
						||
	end
 | 
						||
	return packet
 | 
						||
end
 | 
						||
 | 
						||
-- Parse AUTH packet, DOC: 3.15 AUTH – Authentication exchange
 | 
						||
local function parse_packet_auth(ptype, flags, input)
 | 
						||
	-- DOC: 3.15.1 AUTH Fixed Header
 | 
						||
	if flags ~= 0 then -- Reserved
 | 
						||
		return false, packet_type[ptype]..": unexpected flags value: "..flags
 | 
						||
	end
 | 
						||
	local read_data = input.read_func
 | 
						||
	-- DOC: 3.15.2.1 Authenticate Reason Code
 | 
						||
	local packet = setmetatable({type=ptype, rc=0, properties={}, user_properties={}}, packet_mt)
 | 
						||
	if input.available > 1 then
 | 
						||
		-- DOC: 3.15.2 AUTH Variable Header
 | 
						||
		local rc, err = parse_uint8(read_data)
 | 
						||
		if not rc then
 | 
						||
			return false, packet_type[ptype]..": failed to parse Authenticate Reason Code: "..err
 | 
						||
		end
 | 
						||
		packet.rc = rc
 | 
						||
		-- DOC: 3.15.2.2 AUTH Properties
 | 
						||
		local ok
 | 
						||
		ok, err = parse_properties(ptype, read_data, input, packet)
 | 
						||
		if not ok then
 | 
						||
			return false, packet_type[ptype]..": failed to parse packet properties: "..err
 | 
						||
		end
 | 
						||
	end
 | 
						||
	return packet
 | 
						||
end
 | 
						||
 | 
						||
-- Parse packet using given read_func
 | 
						||
-- Returns packet on success or false and error message on failure
 | 
						||
function protocol5.parse_packet(read_func)
 | 
						||
	local ptype, flags, input = start_parse_packet(read_func)
 | 
						||
	if not ptype then
 | 
						||
		return false, flags
 | 
						||
	end
 | 
						||
	local packet, err
 | 
						||
 | 
						||
	-- parse read data according type in fixed header
 | 
						||
	if ptype == packet_type.CONNECT then			-- 1
 | 
						||
		packet, err = parse_packet_connect_input(input, const_v50)
 | 
						||
	elseif ptype == packet_type.CONNACK then		-- 2
 | 
						||
		packet, err = parse_packet_connack(ptype, flags, input)
 | 
						||
	elseif ptype == packet_type.PUBLISH then		-- 3
 | 
						||
		packet, err = parse_packet_publish(ptype, flags, input)
 | 
						||
	elseif ptype == packet_type.PUBACK then			-- 4
 | 
						||
		packet, err = parse_packet_puback(ptype, flags, input)
 | 
						||
	elseif ptype == packet_type.PUBREC then			-- 5
 | 
						||
		packet, err = parse_packet_pubrec(ptype, flags, input)
 | 
						||
	elseif ptype == packet_type.PUBREL then			-- 6
 | 
						||
		packet, err = parse_packet_pubrel(ptype, flags, input)
 | 
						||
	elseif ptype == packet_type.PUBCOMP then		-- 7
 | 
						||
		packet, err = parse_packet_pubcomp(ptype, flags, input)
 | 
						||
	elseif ptype == packet_type.SUBSCRIBE then		-- 8
 | 
						||
		packet, err = parse_packet_subscribe(ptype, flags, input)
 | 
						||
	elseif ptype == packet_type.SUBACK then			-- 9
 | 
						||
		packet, err = parse_packet_suback(ptype, flags, input)
 | 
						||
	elseif ptype == packet_type.UNSUBSCRIBE then	-- 10
 | 
						||
		packet, err = parse_packet_unsubscribe(ptype, flags, input)
 | 
						||
	elseif ptype == packet_type.UNSUBACK then		-- 11
 | 
						||
		packet, err = parse_packet_unsuback(ptype, flags, input)
 | 
						||
	elseif ptype == packet_type.PINGREQ then		-- 12
 | 
						||
		packet, err = parse_packet_pingreq(ptype, flags, input)
 | 
						||
	elseif ptype == packet_type.PINGRESP then		-- 13
 | 
						||
		packet, err = parse_packet_pingresp(ptype, flags, input)
 | 
						||
	elseif ptype == packet_type.DISCONNECT then		-- 14
 | 
						||
		packet, err = parse_packet_disconnect(ptype, flags, input)
 | 
						||
	elseif ptype == packet_type.AUTH then			-- 15
 | 
						||
		packet, err = parse_packet_auth(ptype, flags, input)
 | 
						||
	else
 | 
						||
		return false, "unexpected packet type received: "..tostring(ptype)
 | 
						||
	end
 | 
						||
	if packet and input.available > 0 then
 | 
						||
		return false, packet_type[ptype]..": extra data in remaining length left after packet parsing"
 | 
						||
	end
 | 
						||
	return packet, err
 | 
						||
end
 | 
						||
 | 
						||
-- Continue parsing of the MQTT v5.0 CONNECT packet
 | 
						||
-- Internally called from the protocol.parse_packet_connect_input() function
 | 
						||
-- Returns packet on success or false and error message on failure
 | 
						||
function protocol5._parse_packet_connect_continue(input, packet)
 | 
						||
	local read_func = input.read_func
 | 
						||
	local ok, client_id, err
 | 
						||
 | 
						||
	-- DOC: 3.1.2.11 CONNECT Properties
 | 
						||
	ok, err = parse_properties(packet_type.CONNECT, read_func, input, packet)
 | 
						||
	if not ok then
 | 
						||
		return false, "CONNECT: failed to parse packet properties: "..err
 | 
						||
	end
 | 
						||
 | 
						||
	-- DOC: 3.1.3 CONNECT Payload
 | 
						||
 | 
						||
	-- DOC: 3.1.3.1 Client Identifier (ClientID)
 | 
						||
	client_id, err = parse_string(read_func)
 | 
						||
	if not client_id then
 | 
						||
		return false, "CONNECT: failed to parse client_id: "..err
 | 
						||
	end
 | 
						||
	packet.id = client_id
 | 
						||
 | 
						||
	local will = packet.will
 | 
						||
	if will then
 | 
						||
		-- DOC: 3.1.3.2 Will Properties
 | 
						||
		ok, err = parse_properties("will", read_func, input, will)
 | 
						||
		if not ok then
 | 
						||
			return false, "CONNECT: failed to parse will message properties: "..err
 | 
						||
		end
 | 
						||
 | 
						||
		-- DOC: 3.1.3.3 Will Topic
 | 
						||
		local will_topic, will_payload
 | 
						||
		will_topic, err = parse_string(read_func)
 | 
						||
		if not will_topic then
 | 
						||
			return false, "CONNECT: failed to parse will_topic: "..err
 | 
						||
		end
 | 
						||
		will.topic = will_topic
 | 
						||
 | 
						||
		-- DOC: 3.1.3.4 Will Payload
 | 
						||
		will_payload, err = parse_string(read_func)
 | 
						||
		if not will_payload then
 | 
						||
			return false, "CONNECT: failed to parse will_payload: "..err
 | 
						||
		end
 | 
						||
		will.payload = will_payload
 | 
						||
	end
 | 
						||
 | 
						||
	-- DOC: 3.1.3.5 User Name
 | 
						||
	if packet.username then
 | 
						||
		local username
 | 
						||
		username, err = parse_string(read_func)
 | 
						||
		if not username then
 | 
						||
			return false, "CONNECT: failed to parse username: "..err
 | 
						||
		end
 | 
						||
		packet.username = username
 | 
						||
	else
 | 
						||
		packet.username = nil
 | 
						||
	end
 | 
						||
 | 
						||
	-- DOC: 3.1.3.6 Password
 | 
						||
	if packet.password then
 | 
						||
		local password
 | 
						||
		password, err = parse_string(read_func)
 | 
						||
		if not password then
 | 
						||
			return false, "CONNECT: failed to parse password: "..err
 | 
						||
		end
 | 
						||
		packet.password = password
 | 
						||
	else
 | 
						||
		packet.password = nil
 | 
						||
	end
 | 
						||
 | 
						||
	return setmetatable(packet, packet_mt)
 | 
						||
end
 | 
						||
 | 
						||
-- export module table
 | 
						||
return protocol5
 | 
						||
 | 
						||
-- vim: ts=4 sts=4 sw=4 noet ft=lua
 |