├── src ├── v3 │ ├── mod.rs │ └── packet │ │ └── mod.rs ├── v5 │ ├── mod.rs │ ├── packet │ │ ├── pings │ │ │ ├── types.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── subacks │ │ │ └── types.rs │ │ ├── pubacks │ │ │ └── types.rs │ │ ├── unsubscribe.rs │ │ └── subscribe.rs │ └── property │ │ ├── mod.rs │ │ ├── types.rs │ │ └── values.rs ├── io │ ├── mod.rs │ ├── net.rs │ ├── write.rs │ └── err.rs ├── test │ ├── mod.rs │ ├── tx.rs │ ├── rx.rs │ ├── write.rs │ └── read.rs ├── client │ ├── info │ │ ├── mod.rs │ │ └── connect.rs │ ├── options │ │ ├── mod.rs │ │ ├── disconnect.rs │ │ ├── publish.rs │ │ ├── subscribe.rs │ │ └── connect.rs │ ├── raw │ │ ├── err.rs │ │ └── net.rs │ ├── event.rs │ └── err.rs ├── packet │ ├── mod.rs │ ├── tx.rs │ └── rx.rs ├── config │ ├── shared.rs │ ├── client.rs │ ├── server.rs │ └── mod.rs ├── types │ ├── mod.rs │ ├── qos.rs │ ├── binary.rs │ ├── will.rs │ ├── int.rs │ ├── string.rs │ ├── topic.rs │ └── reason_code.rs ├── lib.rs ├── session │ ├── flight.rs │ └── mod.rs ├── header.rs ├── fmt.rs ├── bytes.rs └── buffer.rs ├── .ci ├── mqtt_pass_plain.txt ├── mosquitto.conf ├── hive_extension_config.xml └── hive_cred.xml ├── tests ├── integration │ ├── mod.rs │ ├── creds.rs │ └── sub_options.rs ├── mod.rs ├── common │ ├── fmt.rs │ ├── mod.rs │ ├── assert.rs │ └── utils.rs └── load │ └── mod.rs ├── .gitignore ├── rustfmt.toml ├── rust-toolchain.toml ├── .github └── workflows │ ├── fmt.yaml │ ├── msrv.yaml │ ├── unit_tests.yaml │ ├── clippy.yaml │ └── integration_tests.yaml ├── CHANGELOG.md ├── LICENSE ├── Cargo.toml ├── README.md └── examples └── demo.rs /src/v3/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/v3/packet/mod.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ci/mqtt_pass_plain.txt: -------------------------------------------------------------------------------- 1 | test:testPass 2 | -------------------------------------------------------------------------------- /src/v5/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod packet; 2 | pub mod property; 3 | -------------------------------------------------------------------------------- /tests/integration/mod.rs: -------------------------------------------------------------------------------- 1 | mod creds; 2 | mod pub_sub; 3 | mod sub_options; 4 | -------------------------------------------------------------------------------- /src/io/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod err; 2 | pub mod net; 3 | pub mod read; 4 | pub mod write; 5 | -------------------------------------------------------------------------------- /src/test/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod read; 2 | pub mod write; 3 | 4 | pub mod rx; 5 | pub mod tx; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | 4 | Cargo.lock 5 | 6 | /target 7 | 8 | .ci/mqtt_pass_hashed.txt 9 | -------------------------------------------------------------------------------- /.ci/mosquitto.conf: -------------------------------------------------------------------------------- 1 | allow_anonymous false 2 | listener 1883 0.0.0.0 3 | password_file .ci/mqtt_pass_hashed.txt 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2024" 2 | max_width = 100 3 | use_small_heuristics = "Default" 4 | newline_style = "Unix" 5 | reorder_imports = true 6 | -------------------------------------------------------------------------------- /tests/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "alloc")] 2 | mod common; 3 | #[cfg(feature = "alloc")] 4 | mod integration; 5 | #[cfg(feature = "alloc")] 6 | mod load; 7 | -------------------------------------------------------------------------------- /src/client/info/mod.rs: -------------------------------------------------------------------------------- 1 | //! Contains information types returned after certain client actions. 2 | 3 | mod connect; 4 | 5 | pub use connect::Info as ConnectInfo; 6 | -------------------------------------------------------------------------------- /src/packet/mod.rs: -------------------------------------------------------------------------------- 1 | mod rx; 2 | mod tx; 3 | 4 | pub use rx::*; 5 | pub use tx::*; 6 | 7 | use crate::header::PacketType; 8 | 9 | pub trait Packet { 10 | const PACKET_TYPE: PacketType; 11 | } 12 | -------------------------------------------------------------------------------- /src/io/net.rs: -------------------------------------------------------------------------------- 1 | use crate::eio::{Read, Write}; 2 | 3 | /// Underlying transport of MQTT. Must provide an ordered, lossless, stream of bytes from Client to Server and Server to Client. 4 | pub trait Transport: Read + Write {} 5 | 6 | impl Transport for T where T: Read + Write {} 7 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.92" 3 | profile = "default" 4 | targets = [ 5 | "thumbv7em-none-eabi", 6 | "thumbv7m-none-eabi", 7 | "thumbv6m-none-eabi", 8 | "thumbv7em-none-eabihf", 9 | "thumbv8m.main-none-eabihf", 10 | "wasm32-unknown-unknown", 11 | ] 12 | -------------------------------------------------------------------------------- /.github/workflows/fmt.yaml: -------------------------------------------------------------------------------- 1 | on: [pull_request, workflow_dispatch] 2 | name: Format 3 | 4 | jobs: 5 | rustfmt_check: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout repository 9 | uses: actions/checkout@v5 10 | 11 | - name: Install toolchain 12 | run: rustup show 13 | 14 | - name: Check formatting 15 | run: cargo fmt --all --check 16 | -------------------------------------------------------------------------------- /src/client/options/mod.rs: -------------------------------------------------------------------------------- 1 | //! Contains user-facing option types for configuring client actions. 2 | 3 | mod connect; 4 | mod disconnect; 5 | mod publish; 6 | mod subscribe; 7 | 8 | pub use connect::{Options as ConnectOptions, WillOptions}; 9 | pub use disconnect::Options as DisconnectOptions; 10 | pub use publish::Options as PublicationOptions; 11 | pub use subscribe::{Options as SubscriptionOptions, RetainHandling}; 12 | -------------------------------------------------------------------------------- /src/v5/packet/pings/types.rs: -------------------------------------------------------------------------------- 1 | use crate::header::PacketType; 2 | 3 | pub struct Req; 4 | pub struct Resp; 5 | 6 | pub trait PingPacketType { 7 | const PACKET_TYPE: PacketType; 8 | const FLAGS: u8; 9 | } 10 | 11 | impl PingPacketType for Req { 12 | const PACKET_TYPE: PacketType = PacketType::Pingreq; 13 | const FLAGS: u8 = 0x00; 14 | } 15 | impl PingPacketType for Resp { 16 | const PACKET_TYPE: PacketType = PacketType::Pingresp; 17 | const FLAGS: u8 = 0x00; 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/msrv.yaml: -------------------------------------------------------------------------------- 1 | on: [pull_request, workflow_dispatch] 2 | name: MSRV 3 | 4 | jobs: 5 | msrv_check: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout repository 9 | uses: actions/checkout@v5 10 | 11 | - name: Install toolchain 12 | run: rustup show 13 | 14 | - name: Install cargo-msrv 15 | uses: taiki-e/install-action@v2 16 | with: 17 | tool: cargo-msrv 18 | 19 | - name: Run MSRV check 20 | run: cargo msrv verify 21 | -------------------------------------------------------------------------------- /src/client/options/disconnect.rs: -------------------------------------------------------------------------------- 1 | use crate::config::SessionExpiryInterval; 2 | 3 | /// Options for a disconnection to the server with a DISCONNECT packet. 4 | pub struct Options { 5 | /// If set to true, the server publishes the will message. 6 | pub publish_will: bool, 7 | 8 | /// The session expiry interval property. Not allowed to be set to a non-zero value 9 | /// if the session expiry interval property in the CONNECT packet has been 0. 10 | pub session_expiry_interval: Option, 11 | } 12 | -------------------------------------------------------------------------------- /tests/common/fmt.rs: -------------------------------------------------------------------------------- 1 | macro_rules! warn_inspect { 2 | ($e:expr, $t:literal) => {{ 3 | use std::result::Result::*; 4 | let r = $e; 5 | match r { 6 | Ok(_) => {} 7 | Err(ref e) => ::log::warn!("{}: {:?}", $t, e), 8 | } 9 | 10 | r 11 | }}; 12 | } 13 | 14 | macro_rules! error_panic { 15 | ($t:literal, $e:expr) => {{ 16 | ::log::error!("{}: {:?}", $t, $e); 17 | panic!("{}: {:?}", $t, $e); 18 | }}; 19 | } 20 | 21 | pub(crate) use error_panic; 22 | pub(crate) use warn_inspect; 23 | -------------------------------------------------------------------------------- /src/config/shared.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{KeepAlive, SessionExpiryInterval}; 2 | 3 | /// Negotiated configuration valid for the duration of a connection. 4 | #[derive(Debug, Clone, Copy, Default)] 5 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 6 | pub struct Config { 7 | /// The maximum interval in seconds allowed to expire between sending two packets without the connection being closed. 8 | pub keep_alive: KeepAlive, 9 | 10 | /// The negotiated session expiry interval after the connection has been closed. 11 | pub session_expiry_interval: SessionExpiryInterval, 12 | } 13 | -------------------------------------------------------------------------------- /src/v5/packet/mod.rs: -------------------------------------------------------------------------------- 1 | // mod auth; 2 | mod connack; 3 | mod connect; 4 | mod disconnect; 5 | mod pings; 6 | mod pubacks; 7 | mod publish; 8 | mod subacks; 9 | mod subscribe; 10 | mod unsubscribe; 11 | 12 | // pub use auth::AuthPacket; 13 | pub use connack::ConnackPacket; 14 | pub use connect::ConnectPacket; 15 | pub use disconnect::DisconnectPacket; 16 | pub use pings::{PingreqPacket, PingrespPacket}; 17 | pub use pubacks::*; 18 | pub use publish::PublishPacket; 19 | pub use subacks::{SubackPacket, UnsubackPacket}; 20 | pub use subscribe::SubscribePacket; 21 | pub use unsubscribe::UnsubscribePacket; 22 | -------------------------------------------------------------------------------- /.github/workflows/unit_tests.yaml: -------------------------------------------------------------------------------- 1 | on: [pull_request, workflow_dispatch] 2 | name: Unit Tests 3 | 4 | jobs: 5 | unit_tests: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout repository 9 | uses: actions/checkout@v5 10 | 11 | - name: Install toolchain 12 | run: rustup show 13 | 14 | - name: Run unit tests with alloc feature 15 | run: RUST_LOG=trace cargo test unit --features "log" -- --show-output 16 | 17 | - name: Run unit tests with bump feature 18 | run: RUST_LOG=trace cargo test unit --no-default-features --features "v5 bump log" -- --show-output 19 | -------------------------------------------------------------------------------- /.github/workflows/clippy.yaml: -------------------------------------------------------------------------------- 1 | on: [pull_request, workflow_dispatch] 2 | name: Clippy 3 | 4 | env: 5 | RUSTFLAGS: "-Dwarnings" 6 | 7 | jobs: 8 | clippy_check: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v5 13 | 14 | - name: Install toolchain 15 | run: rustup show 16 | 17 | - name: Run clippy with alloc & log features 18 | run: cargo clippy --all-targets --features "log" 19 | 20 | - name: Run clippy with bump & defmt features 21 | run: cargo clippy --all-targets --no-default-features --features "v5 bump defmt" 22 | -------------------------------------------------------------------------------- /.ci/hive_extension_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 60 6 | 7 | 12 | 13 | 14 | PLAIN 15 | 16 | -------------------------------------------------------------------------------- /.ci/hive_cred.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | test 6 | testPass 7 | 8 | superuser 9 | 10 | 11 | 12 | 13 | 14 | superuser 15 | 16 | 17 | 18 | # 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/test/tx.rs: -------------------------------------------------------------------------------- 1 | use tokio_test::assert_ok; 2 | 3 | use crate::{packet::TxPacket, test::write::SliceWriter}; 4 | 5 | macro_rules! encode { 6 | ($p:expr, $e:expr) => { 7 | crate::test::tx::assert_encoded($p, $e).await 8 | }; 9 | } 10 | 11 | pub async fn assert_encoded(packet: impl TxPacket, encoded: [u8; N]) { 12 | let mut buffer = [0; N]; 13 | let written = { 14 | let mut writer = SliceWriter::new(&mut buffer); 15 | assert_ok!(packet.send(&mut writer).await); 16 | writer.written() 17 | }; 18 | assert_eq!(N, written); 19 | 20 | let written = &buffer[..written]; 21 | 22 | assert_eq!(encoded, written); 23 | } 24 | 25 | pub(crate) use encode; 26 | -------------------------------------------------------------------------------- /src/config/client.rs: -------------------------------------------------------------------------------- 1 | use crate::config::SessionExpiryInterval; 2 | 3 | /// Configuration of the client which must be upheld by the server. 4 | #[derive(Debug, Clone, Copy, Default)] 5 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 6 | pub struct Config { 7 | /// The session expiry interval requested by the client. 8 | /// This field is used to determine whether a non-zero session expiry interval 9 | /// can be used when disconnecting. 10 | pub session_expiry_interval: SessionExpiryInterval, 11 | // receive_maximum 12 | // maximum_packet_size 13 | // topic_alias_maximum 14 | // request_response_information this requests a response_information property in the CONNACK 15 | // request_problem_information indicates to the server that it may include reason strings and user properties 16 | } 17 | -------------------------------------------------------------------------------- /src/client/info/connect.rs: -------------------------------------------------------------------------------- 1 | use crate::types::MqttString; 2 | 3 | /// Information taken from a connection handshake the client does not have to store 4 | /// for correct operational behaviour and does not store for optimization purposes. 5 | /// 6 | /// Does not include the reason code as it is always `Success` (0x00) if this is returned. 7 | #[derive(Debug, Clone)] 8 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 9 | pub struct Info<'i> { 10 | /// If set to true, a previous session is continued by the server for this connection. 11 | pub session_present: bool, 12 | 13 | /// The server can assign a different client identifier than the one in the CONNECT packet 14 | /// or must assign a client identifier if none was included in the CONNECT packet. 15 | /// This is the final client identifier value used for this session. 16 | pub client_identifier: MqttString<'i>, 17 | } 18 | -------------------------------------------------------------------------------- /src/types/mod.rs: -------------------------------------------------------------------------------- 1 | //! Contains types used throughout the MQTT specification. 2 | 3 | mod binary; 4 | mod int; 5 | mod qos; 6 | mod reason_code; 7 | mod string; 8 | mod topic; 9 | mod will; 10 | 11 | pub(crate) use topic::SubscriptionFilter; 12 | pub(crate) use will::Will; 13 | 14 | pub use binary::MqttBinary; 15 | pub use int::VarByteInt; 16 | pub use qos::{IdentifiedQoS, QoS}; 17 | pub use reason_code::ReasonCode; 18 | pub use string::MqttString; 19 | pub use topic::{TopicFilter, TopicName}; 20 | 21 | /// Variable byte integer: If `VarByteInt::MAX_ENCODABLE` is exceeded, an error of this type is returned. 22 | /// MQTT string: If `MqttString::MAX_LENGTH` is exceeded, an error of this type is returned. 23 | /// MQTT binary: If `MqttBinary::MAX_LENGTH` is exceeded, an error of this type is returned. 24 | #[derive(Debug)] 25 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 26 | pub struct TooLargeToEncode; 27 | -------------------------------------------------------------------------------- /src/client/options/publish.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{QoS, TopicName}; 2 | 3 | /// Options for a publication. 4 | #[derive(Debug, Clone)] 5 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 6 | pub struct Options<'p> { 7 | /// Depicts the value of the retain flag in the PUBLISH packet. 8 | /// If set to 1, the server should retain the message on this topic. 9 | /// Retained messages with quality of service 0 can be discarded 10 | /// at any time by the server. 11 | pub retain: bool, 12 | 13 | /// The topic that the message is published on. 14 | pub topic: TopicName<'p>, 15 | 16 | /// The quality of service that the message is published with to the server. 17 | /// The quality of service level used by the server to send this publication 18 | /// to subscribed clients is the minimum of this value and the quality of service 19 | /// value of the receiving client's subscription. 20 | pub qos: QoS, 21 | } 22 | -------------------------------------------------------------------------------- /src/packet/tx.rs: -------------------------------------------------------------------------------- 1 | use crate::{eio::Write, io::err::WriteError, packet::Packet, types::TooLargeToEncode}; 2 | 3 | pub trait TxPacket: Packet { 4 | async fn send(&self, write: &mut W) -> Result<(), TxError>; 5 | } 6 | 7 | #[derive(Debug)] 8 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 9 | pub enum TxError { 10 | Write(E), 11 | WriteZero, 12 | 13 | /// The remaining length of a packet is too large to be encoded by a variable byte integer 14 | RemainingLenExceeded, 15 | } 16 | 17 | impl From> for TxError { 18 | fn from(e: WriteError) -> Self { 19 | match e { 20 | WriteError::Write(e) => Self::Write(e), 21 | WriteError::WriteZero => Self::WriteZero, 22 | } 23 | } 24 | } 25 | 26 | impl From for TxError { 27 | fn from(_: TooLargeToEncode) -> Self { 28 | Self::RemainingLenExceeded 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![doc = include_str!("../README.md")] 3 | #![warn(missing_docs)] 4 | #![warn(clippy::missing_safety_doc)] 5 | #![deny(clippy::unnecessary_safety_doc)] 6 | #![deny(clippy::unnecessary_safety_comment)] 7 | 8 | #[cfg(test)] 9 | extern crate std; 10 | 11 | #[cfg(feature = "alloc")] 12 | extern crate alloc; 13 | 14 | use embedded_io_async as eio; 15 | 16 | #[cfg(all(feature = "bump", feature = "alloc"))] 17 | compile_error!("You may not enable both `bump` and `alloc` features."); 18 | 19 | #[cfg(all(feature = "log", feature = "defmt"))] 20 | compile_error!("You may not enable both `log` and `defmt` features."); 21 | 22 | #[cfg(all(test, not(any(feature = "bump", feature = "alloc"))))] 23 | compile_error!("Enable either one of `bump` or `alloc` features for testing."); 24 | 25 | mod bytes; 26 | mod fmt; 27 | mod header; 28 | mod io; 29 | mod packet; 30 | 31 | pub mod buffer; 32 | pub mod client; 33 | pub mod config; 34 | pub mod session; 35 | pub mod types; 36 | 37 | pub use bytes::Bytes; 38 | 39 | #[cfg(test)] 40 | mod test; 41 | 42 | #[cfg(feature = "v3")] 43 | mod v3; 44 | #[cfg(feature = "v5")] 45 | mod v5; 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## Unreleased 9 | 10 | - Extensive rewrite 11 | - Trait based buffer provision in deserialization 12 | - More robust packet deserialization regarding protocol errors and malformed packets 13 | - Newtype abstraction of types in the mqtt protocol 14 | - Improved error handling with non-recoverable and recoverable errors 15 | - Quality of service 2 16 | - More lightweight mqtt features such as flags in packets 17 | - Remove support for user properties 18 | - Upgrade to Rust 1.92 19 | 20 | ## 0.3.1 - 2025-12-11 21 | 22 | - Add support for the embedded_io_async::ReadReady trait 23 | - Don't treat a reason code of NoMatchingSubscribers from server as error 24 | - Bugfixes in networking, IO and deserializing of disconnect packet 25 | - Bump dependencies and Rust version to 1.91 26 | 27 | ## 0.3.0 - 2024-01-30 28 | 29 | - Bump dependencies 30 | - Switch to stable Rust 31 | 32 | ## 0.2.0 - 2023-12-03 33 | 34 | - Bump dependencies and fix warnings 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2022] [Ondrej Babec ] 4 | Copyright (c) [2025] [Julian Graf ] 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-mqtt" 3 | version = "0.4.0" 4 | authors = [ 5 | "Ondrej Babec ", 6 | "Julian Graf ", 7 | ] 8 | edition = "2024" 9 | rust-version = "1.87" 10 | resolver = "2" 11 | description = "MQTT client for embedded and non-embedded environments" 12 | readme = "README.md" 13 | license = "MIT" 14 | repository = "https://github.com/obabec/rust-mqtt" 15 | keywords = ["embedded", "async", "mqtt", "no_std"] 16 | categories = ["embedded", "network-programming", "no-std"] 17 | exclude = [".github", ".ci"] 18 | 19 | [dependencies] 20 | embedded-io-async = { version = "0.7.0" } 21 | 22 | heapless = "0.9.2" 23 | 24 | defmt = { version = "1.0", optional = true } 25 | log = { version = "0.4", optional = true } 26 | 27 | 28 | [dev-dependencies] 29 | embedded-io-adapters = { version = "0.7.0", features = ["tokio-1"] } 30 | tokio = { version = "1.48.0", features = [ 31 | "net", 32 | "time", 33 | "macros", 34 | "rt-multi-thread", 35 | ] } 36 | tokio-test = "0.4.4" 37 | test-log = "0.2.19" 38 | log = "0.4.28" 39 | env_logger = "0.11.8" 40 | 41 | 42 | [features] 43 | default = ["v5", "alloc"] 44 | 45 | log = ["dep:log"] 46 | defmt = ["dep:defmt", "embedded-io-async/defmt"] 47 | 48 | bump = [] 49 | alloc = [] 50 | 51 | v3 = [] 52 | v5 = [] 53 | 54 | 55 | [[example]] 56 | name = "demo" 57 | -------------------------------------------------------------------------------- /src/session/flight.rs: -------------------------------------------------------------------------------- 1 | /// An incomplete QoS 1 or 2 publication. 2 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 3 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 4 | pub struct InFlightPublish { 5 | /// The packet identifier of the publication process. 6 | pub packet_identifier: u16, 7 | /// The state of the publication process. 8 | pub state: S, 9 | } 10 | 11 | /// The state of an incomplete QoS 1 or 2 publication by the client. 12 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 13 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 14 | pub enum CPublishFlightState { 15 | /// A QoS 1 PUBLISH packet has been sent. 16 | /// The next step in the handshake is the server sending a PUBACK packet. 17 | AwaitingPuback, 18 | /// A QoS 2 PUBLISH packet has been sent. 19 | /// The next step in the handshake is the server sending a PUBREC packet. 20 | AwaitingPubrec, 21 | /// A PUBREC packet has been received and responded to with a PUBREL packet. 22 | /// The last step in the handshake is the server sending a PUBCOMP packet. 23 | AwaitingPubcomp, 24 | } 25 | 26 | /// The state of an incomplete QoS 2 publication by the server. 27 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 28 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 29 | pub enum SPublishFlightState { 30 | /// A QoS 2 packet has been received and responded to with a PUBREC packet. 31 | /// The next step in the handshake is the server sending a PUBREL packet. 32 | AwaitingPubrel, 33 | } 34 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; 2 | 3 | use embedded_io_adapters::tokio_1::FromTokio; 4 | use rust_mqtt::{ 5 | buffer::AllocBuffer, 6 | client::{ 7 | Client, 8 | options::{ConnectOptions, DisconnectOptions, RetainHandling, SubscriptionOptions}, 9 | }, 10 | config::{KeepAlive, SessionExpiryInterval}, 11 | types::{MqttBinary, MqttString, QoS}, 12 | }; 13 | use tokio::net::TcpStream; 14 | 15 | pub mod assert; 16 | pub mod fmt; 17 | pub mod utils; 18 | 19 | pub type Tcp = FromTokio; 20 | pub type TestClient<'a> = Client<'a, Tcp, AllocBuffer, 1, 1, 1>; 21 | 22 | pub const BROKER_ADDRESS: SocketAddr = 23 | SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 1883)); 24 | 25 | pub const USERNAME: MqttString<'static> = unsafe { MqttString::from_slice_unchecked("test") }; 26 | pub const PASSWORD: MqttBinary<'static> = 27 | unsafe { MqttBinary::from_slice_unchecked("testPass".as_bytes()) }; 28 | 29 | pub const NO_SESSION_CONNECT_OPTIONS: &ConnectOptions<'static> = &ConnectOptions { 30 | clean_start: true, 31 | keep_alive: KeepAlive::Infinite, 32 | session_expiry_interval: SessionExpiryInterval::EndOnDisconnect, 33 | user_name: Some(USERNAME), 34 | password: Some(PASSWORD), 35 | will: None, 36 | }; 37 | 38 | pub const DEFAULT_QOS0_SUB_OPTIONS: SubscriptionOptions = SubscriptionOptions { 39 | retain_handling: RetainHandling::AlwaysSend, 40 | retain_as_published: false, 41 | no_local: false, 42 | qos: QoS::AtMostOnce, 43 | }; 44 | 45 | pub const DEFAULT_DC_OPTIONS: &DisconnectOptions = &DisconnectOptions { 46 | publish_will: true, 47 | session_expiry_interval: None, 48 | }; 49 | -------------------------------------------------------------------------------- /src/v5/property/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | eio::Read, 3 | io::{err::ReadError, read::Readable}, 4 | }; 5 | 6 | mod types; 7 | mod values; 8 | 9 | pub use types::PropertyType; 10 | pub use values::*; 11 | 12 | /// This implementation serves as both an enabler trait for a generic Readable and Writable implementation for the Property as well as a marker trait for the following qualities of the Readable and Writable impls: 13 | /// 14 | /// * Writable writes both its property type's identifier and its content 15 | /// * Readable reads only the property's content 16 | pub trait Property { 17 | const TYPE: PropertyType; 18 | type Inner; 19 | 20 | fn into_inner(self) -> Self::Inner; 21 | } 22 | 23 | /// Helper trait to read optional, but at max once properties into a packet 24 | pub trait AtMostOnceProperty { 25 | async fn try_set( 26 | &mut self, 27 | read: &mut R, 28 | ) -> Result<(), AtMostOncePropertyError>>; 29 | } 30 | pub enum AtMostOncePropertyError { 31 | Read(E), 32 | AlreadySet, 33 | } 34 | impl From> for AtMostOncePropertyError> { 35 | fn from(e: ReadError) -> Self { 36 | Self::Read(e) 37 | } 38 | } 39 | impl> AtMostOnceProperty for Option { 40 | async fn try_set( 41 | &mut self, 42 | read: &mut R, 43 | ) -> Result<(), AtMostOncePropertyError>> { 44 | if self.is_some() { 45 | Err(AtMostOncePropertyError::AlreadySet) 46 | } else { 47 | let value = T::read(read).await.map_err(AtMostOncePropertyError::Read)?; 48 | 49 | self.replace(value); 50 | Ok(()) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/config/server.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::{MaximumPacketSize, ReceiveMaximum}, 3 | types::QoS, 4 | }; 5 | 6 | /// Configuration of the server which must be upheld by the client. 7 | #[derive(Debug, Clone, Copy)] 8 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 9 | pub struct Config { 10 | /// Maximum concurrent QoS 1 & 2 publications that the server is willing to accept. 11 | pub receive_maximum: ReceiveMaximum, 12 | 13 | /// Maximum supported QoS by the server. 14 | pub maximum_qos: QoS, 15 | 16 | /// Retained messages are supported by the server. 17 | pub retain_supported: bool, 18 | 19 | /// Maximum packet size that the server is willing to accept. 20 | pub maximum_packet_size: MaximumPacketSize, 21 | 22 | /// Highest value of a topic alias the server is willing to accept. 23 | /// Equal to the number of distinct topic aliases the server supports. 24 | pub topic_alias_maximum: u16, 25 | 26 | /// Serverside support for wildcard subscriptions. 27 | pub wildcard_subscription_supported: bool, 28 | /// Serverside support for subscription identifiers. 29 | pub subscription_identifiers_supported: bool, 30 | /// Serverside support for shared subscriptions. 31 | pub shared_subscription_supported: bool, 32 | } 33 | 34 | impl Default for Config { 35 | fn default() -> Self { 36 | Self { 37 | receive_maximum: ReceiveMaximum(u16::MAX), 38 | maximum_qos: QoS::ExactlyOnce, 39 | retain_supported: true, 40 | maximum_packet_size: Default::default(), 41 | topic_alias_maximum: 0, 42 | wildcard_subscription_supported: true, 43 | subscription_identifiers_supported: true, 44 | shared_subscription_supported: true, 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/client/options/subscribe.rs: -------------------------------------------------------------------------------- 1 | use crate::types::QoS; 2 | 3 | /// Options for subscription included for every topic. 4 | #[derive(Debug, Clone, Copy)] 5 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 6 | pub struct Options { 7 | /// Serverside retain handling configuration for this subscription. 8 | pub retain_handling: RetainHandling, 9 | 10 | /// If set to true, the server sets the retain flag of a PUBLISH packet matching 11 | /// this subscription to the retain flag value of the original publication. 12 | /// If set to false, the server sets the retain flag of a PUBLISH packet matching 13 | /// this subscription to false. This does not apply for retained messages sent 14 | /// directly after subscribing - these messages always have the retain flag set to 1. 15 | pub retain_as_published: bool, 16 | 17 | /// If set to true, the server does not forward any publications matching 18 | /// this subscription to connections with client identifiers the same as 19 | /// the client identifier of this connection. 20 | /// If set to true on a shared subscription, a protocol error is triggered. 21 | pub no_local: bool, 22 | 23 | /// The maximum quality of server that the server can forward publications 24 | /// matching this subscription with to the client. A quality of service level 25 | /// lower than this can be granted by the server. 26 | pub qos: QoS, 27 | } 28 | 29 | /// Serverside retain handling configuration for a subscription. 30 | #[derive(Debug, Clone, Copy, Default)] 31 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 32 | pub enum RetainHandling { 33 | /// Retained messages are always sent at the time of the subscribe. 34 | #[default] 35 | AlwaysSend, 36 | /// Retained messages are only sent if the subscription did not exist before. 37 | SendIfNotSubscribedBefore, 38 | /// Retained messages are never sent. 39 | NeverSend, 40 | } 41 | -------------------------------------------------------------------------------- /src/v5/packet/subacks/types.rs: -------------------------------------------------------------------------------- 1 | use crate::{header::PacketType, types::ReasonCode}; 2 | 3 | #[derive(Debug)] 4 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 5 | pub struct Suback; 6 | #[derive(Debug)] 7 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 8 | pub struct Unsuback; 9 | 10 | pub trait SubackPacketType { 11 | const PACKET_TYPE: PacketType; 12 | 13 | fn reason_code_allowed(reason_code: ReasonCode) -> bool; 14 | } 15 | 16 | impl SubackPacketType for Suback { 17 | const PACKET_TYPE: PacketType = PacketType::Suback; 18 | 19 | fn reason_code_allowed(reason_code: ReasonCode) -> bool { 20 | matches!( 21 | reason_code, 22 | ReasonCode::Success 23 | | ReasonCode::GrantedQoS1 24 | | ReasonCode::GrantedQoS2 25 | | ReasonCode::UnspecifiedError 26 | | ReasonCode::ImplementationSpecificError 27 | | ReasonCode::NotAuthorized 28 | | ReasonCode::TopicFilterInvalid 29 | | ReasonCode::PacketIdentifierInUse 30 | | ReasonCode::QuotaExceeded 31 | | ReasonCode::SharedSubscriptionsNotSupported 32 | | ReasonCode::SubscriptionIdentifiersNotSupported 33 | | ReasonCode::WildcardSubscriptionsNotSupported 34 | ) 35 | } 36 | } 37 | 38 | impl SubackPacketType for Unsuback { 39 | const PACKET_TYPE: PacketType = PacketType::Unsuback; 40 | 41 | fn reason_code_allowed(reason_code: ReasonCode) -> bool { 42 | matches!( 43 | reason_code, 44 | ReasonCode::Success 45 | | ReasonCode::NoSubscriptionExisted 46 | | ReasonCode::UnspecifiedError 47 | | ReasonCode::ImplementationSpecificError 48 | | ReasonCode::NotAuthorized 49 | | ReasonCode::TopicFilterInvalid 50 | | ReasonCode::PacketIdentifierInUse 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/integration_tests.yaml: -------------------------------------------------------------------------------- 1 | on: [pull_request, workflow_dispatch] 2 | name: Integration Tests 3 | 4 | jobs: 5 | integration_tests_mosquitto: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout repository 9 | uses: actions/checkout@v5 10 | 11 | - name: Install toolchain 12 | run: rustup show 13 | 14 | - name: Install Mosquitto 15 | run: | 16 | sudo apt-get update 17 | sudo apt-get install mosquitto 18 | sudo service mosquitto stop 19 | 20 | - name: Configure & Start Mosquitto 21 | run: | 22 | cp .ci/mqtt_pass_plain.txt .ci/mqtt_pass_hashed.txt 23 | chmod 700 .ci/mqtt_pass_hashed.txt 24 | mosquitto_passwd -U .ci/mqtt_pass_hashed.txt 25 | mosquitto -c .ci/mosquitto.conf -d 26 | 27 | - name: Run integration tests 28 | run: RUST_LOG=trace cargo test integration --features "log" -- --show-output 29 | 30 | integration_tests_hive: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v5 35 | 36 | - name: Install toolchain 37 | run: rustup show 38 | 39 | - name: Start HiveMQ 40 | run: | 41 | curl -LO https://github.com/hivemq/hivemq-community-edition/releases/download/2025.5/hivemq-ce-2025.5.zip 42 | unzip hivemq-ce-2025.5.zip 43 | curl -LO https://github.com/hivemq/hivemq-file-rbac-extension/releases/download/4.6.8/hivemq-file-rbac-extension-4.6.8.zip 44 | unzip hivemq-file-rbac-extension-4.6.8.zip 45 | mv hivemq-file-rbac-extension hivemq-ce-2025.5/extensions 46 | cp .ci/hive_cred.xml hivemq-ce-2025.5/extensions/hivemq-file-rbac-extension/credentials.xml 47 | cp .ci/hive_extension_config.xml hivemq-ce-2025.5/extensions/hivemq-file-rbac-extension/conf/config.xml 48 | hivemq-ce-2025.5/bin/run.sh & 49 | 50 | - name: Run integration tests 51 | run: RUST_LOG=trace cargo test integration --features "log" -- --show-output 52 | -------------------------------------------------------------------------------- /tests/integration/creds.rs: -------------------------------------------------------------------------------- 1 | use rust_mqtt::{ 2 | Bytes, 3 | client::{MqttError, options::ConnectOptions}, 4 | config::{KeepAlive, SessionExpiryInterval}, 5 | types::MqttBinary, 6 | }; 7 | 8 | use crate::common::{BROKER_ADDRESS, USERNAME, utils::connected_client}; 9 | 10 | const NO_CREDS_CONNECT_OPTIONS: ConnectOptions = ConnectOptions { 11 | clean_start: true, 12 | keep_alive: KeepAlive::Infinite, 13 | session_expiry_interval: SessionExpiryInterval::EndOnDisconnect, 14 | user_name: None, 15 | password: None, 16 | will: None, 17 | }; 18 | 19 | #[tokio::test] 20 | #[test_log::test] 21 | async fn connect_no_creds() { 22 | let r = connected_client(BROKER_ADDRESS, &NO_CREDS_CONNECT_OPTIONS, None).await; 23 | 24 | assert!(r.is_err()); 25 | let e = unsafe { r.unwrap_err_unchecked() }; 26 | 27 | assert!(matches!( 28 | e, 29 | MqttError::Disconnect { 30 | reason: _, 31 | reason_string: _ 32 | } 33 | )); 34 | } 35 | 36 | #[tokio::test] 37 | #[test_log::test] 38 | async fn connect_no_password() { 39 | let mut options = NO_CREDS_CONNECT_OPTIONS; 40 | options.user_name = Some(USERNAME); 41 | 42 | let r = connected_client(BROKER_ADDRESS, &options, None).await; 43 | 44 | assert!(r.is_err()); 45 | let e = unsafe { r.unwrap_err_unchecked() }; 46 | 47 | assert!(matches!( 48 | e, 49 | MqttError::Disconnect { 50 | reason: _, 51 | reason_string: _ 52 | } 53 | )); 54 | } 55 | 56 | #[tokio::test] 57 | #[test_log::test] 58 | async fn connect_wrong_password() { 59 | let mut options = NO_CREDS_CONNECT_OPTIONS; 60 | options.user_name = Some(USERNAME); 61 | options.password = Some(MqttBinary::new(Bytes::Borrowed(b"wrong password")).unwrap()); 62 | 63 | let r = connected_client(BROKER_ADDRESS, &options, None).await; 64 | 65 | assert!(r.is_err()); 66 | let e = unsafe { r.unwrap_err_unchecked() }; 67 | 68 | assert!(matches!( 69 | e, 70 | MqttError::Disconnect { 71 | reason: _, 72 | reason_string: _ 73 | } 74 | )); 75 | } 76 | -------------------------------------------------------------------------------- /tests/common/assert.rs: -------------------------------------------------------------------------------- 1 | macro_rules! assert_ok { 2 | ($e:expr) => {{ 3 | use std::result::Result::*; 4 | match $e { 5 | Ok(v) => v, 6 | Err(e) => panic!("assertion failed: Err({:?})", e), 7 | } 8 | }}; 9 | ($e:expr, $t:literal) => {{ 10 | use std::result::Result::*; 11 | match $e { 12 | Ok(v) => v, 13 | Err(e) => crate::common::fmt::error_panic!($t, e), 14 | } 15 | }}; 16 | } 17 | 18 | macro_rules! assert_subscribe { 19 | ($client:expr, $options:expr, $topic:expr) => { 20 | let qos = $options.qos; 21 | let granted_qos = assert_ok!( 22 | crate::common::utils::subscribe(&mut $client, $options, $topic).await, 23 | "Failed to subscribe" 24 | ); 25 | assert_eq!( 26 | qos, granted_qos, 27 | "Subscribed with different quality of service than expected" 28 | ); 29 | }; 30 | } 31 | 32 | macro_rules! assert_unsubscribe { 33 | ($client:expr, $topic:expr) => { 34 | assert_ok!( 35 | crate::common::utils::unsubscribe(&mut $client, $topic).await, 36 | "Failed to unsubscribe" 37 | ); 38 | }; 39 | } 40 | 41 | macro_rules! assert_recv { 42 | ($client:expr) => {{ assert_ok!(crate::common::utils::receive_and_complete(&mut $client).await) }}; 43 | } 44 | 45 | macro_rules! assert_recv_excl { 46 | ($client:expr, $topic:expr) => {{ 47 | let p = assert_ok!(crate::common::utils::receive_and_complete(&mut $client).await); 48 | assert_eq!( 49 | $topic.as_ref(), 50 | &p.topic, 51 | "expected message (left) != received message (right)" 52 | ); 53 | 54 | p 55 | }}; 56 | } 57 | 58 | macro_rules! assert_published { 59 | ($client:expr, $options:expr, $msg:expr) => { 60 | assert_ok!(crate::common::utils::publish_and_complete(&mut $client, &$options, $msg).await) 61 | }; 62 | } 63 | 64 | pub(crate) use assert_ok; 65 | pub(crate) use assert_published; 66 | pub(crate) use assert_recv; 67 | pub(crate) use assert_recv_excl; 68 | pub(crate) use assert_subscribe; 69 | pub(crate) use assert_unsubscribe; 70 | -------------------------------------------------------------------------------- /src/client/raw/err.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | client::raw::NetStateError, 3 | eio::{self, ErrorKind}, 4 | packet::{RxError, TxError}, 5 | types::ReasonCode, 6 | }; 7 | 8 | /// The main error returned by `Raw`. 9 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 10 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 11 | pub enum Error { 12 | /// A packet was too long to encode its length with the variable byte integer 13 | PacketTooLong, 14 | 15 | /// The underlying Read/Write method returned an error. 16 | Network(ErrorKind), 17 | 18 | /// The network is in a faulty state. 19 | Disconnected, 20 | 21 | /// A buffer provision by the `BufferProvider` failed. 22 | Alloc(B), 23 | 24 | /// A generic constant such as `MAX_PROPERTIES` is too small. 25 | ConstSpace, 26 | 27 | /// Malformed packet or Protocol Error. 28 | Server, 29 | } 30 | 31 | impl From> for Error { 32 | fn from(e: TxError) -> Self { 33 | match e { 34 | TxError::WriteZero => Self::Network(ErrorKind::WriteZero), 35 | TxError::Write(e) => Self::Network(e.kind()), 36 | TxError::RemainingLenExceeded => Self::PacketTooLong, 37 | } 38 | } 39 | } 40 | 41 | impl From> for (Error, Option) { 42 | fn from(e: RxError) -> Self { 43 | match e { 44 | RxError::Read(e) => (Error::Network(e.kind()), None), 45 | RxError::Buffer(b) => ( 46 | Error::Alloc(b), 47 | Some(ReasonCode::ImplementationSpecificError), 48 | ), 49 | RxError::InsufficientConstSpace => ( 50 | Error::ConstSpace, 51 | Some(ReasonCode::ImplementationSpecificError), 52 | ), 53 | RxError::UnexpectedEOF => (Error::Network(ErrorKind::NotConnected), None), 54 | RxError::MalformedPacket => (Error::Server, Some(ReasonCode::MalformedPacket)), 55 | RxError::ProtocolError => (Error::Server, Some(ReasonCode::ProtocolError)), 56 | } 57 | } 58 | } 59 | 60 | impl From for Error { 61 | fn from(_: NetStateError) -> Self { 62 | Self::Disconnected 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | //! Contains configuration primitives and accumulations for server and client configuration. 2 | 3 | mod client; 4 | mod server; 5 | mod shared; 6 | 7 | pub use client::Config as ClientConfig; 8 | pub use server::Config as ServerConfig; 9 | pub use shared::Config as SharedConfig; 10 | 11 | /// Keep alive mechanism within a connection. 12 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 13 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 14 | pub enum KeepAlive { 15 | /// There is no keep alive mechanism. Any amount of time can pass between 2 MQTT packets 16 | #[default] 17 | Infinite, 18 | 19 | /// The maximum time interval in seconds allowed to pass between 2 MQTT packets. 20 | /// 21 | /// Must be greater than 0. 22 | Seconds(u16), 23 | } 24 | 25 | impl KeepAlive { 26 | pub(crate) fn as_u16(&self) -> u16 { 27 | match self { 28 | KeepAlive::Infinite => u16::MAX, 29 | KeepAlive::Seconds(s) => *s, 30 | } 31 | } 32 | } 33 | 34 | /// The handling of a session after a disconnection. 35 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 36 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 37 | pub enum SessionExpiryInterval { 38 | /// The session ends the moment a DISCONNECT packet is sent or the network connection closes. 39 | #[default] 40 | EndOnDisconnect, 41 | /// The session is not ended under any circumstances. 42 | NeverEnd, 43 | /// The session ends after this many seconds have passed after a DISCONNECT packet is sent or the network connection closes. 44 | Seconds(u32), 45 | } 46 | 47 | /// Maximum packet size. Exceeding this is a protocol error. 48 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 49 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 50 | pub enum MaximumPacketSize { 51 | /// There is no imposed limit on how large packets can be. 52 | /// The technical limit is `VarByteInt::MAX_ENCODABLE` + 5 (size of fixed header). 53 | #[default] 54 | Unlimited, 55 | 56 | /// There is a limit on how large packets can be. 57 | Limit(u32), 58 | } 59 | 60 | /// Maximum concurrent publications with a Quality of Service > 0. 61 | /// 62 | /// Default is 65536 / `u16::MAX` and is used when no receive maximum is present. Can't be zero. 63 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 64 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 65 | pub struct ReceiveMaximum(pub(crate) u16); 66 | -------------------------------------------------------------------------------- /src/client/raw/net.rs: -------------------------------------------------------------------------------- 1 | use core::mem; 2 | 3 | use crate::{io::net::Transport, types::ReasonCode}; 4 | 5 | /// Represents a network connection with different variants for handling failures in the connection gracefully. 6 | #[derive(Debug, Default)] 7 | pub enum NetState { 8 | /// The connection and dialogue with the server is ok 9 | Ok(N), 10 | 11 | /// The connection is ok but some protocol specific error (e.g. MalformedPacket or ProtocolError occured) 12 | /// 13 | /// Sending messages can be considered, but the session should ultimately be closed according to spec 14 | Faulted(N, ReasonCode), 15 | 16 | /// The connection is closed 17 | #[default] 18 | Terminated, 19 | } 20 | 21 | /// A network connection state related error. 22 | pub enum Error { 23 | /// An error has occurred in the application MQTT communication. 24 | /// The underlying network is still open and should be used for sending a DISCONNECT packet. 25 | Faulted, 26 | 27 | /// An error has occurred in the application MQTT communication. 28 | /// The underlying network has already been closed because either a DISCONNECT packet has already been sent or shouldn't be sent at all. 29 | Terminated, 30 | } 31 | 32 | impl NetState { 33 | pub fn is_ok(&self) -> bool { 34 | matches!(self, Self::Ok(_)) 35 | } 36 | pub fn replace(&mut self, net: N) { 37 | *self = Self::Ok(net); 38 | } 39 | pub fn get(&mut self) -> Result<&mut N, Error> { 40 | match self { 41 | Self::Ok(n) => Ok(n), 42 | Self::Faulted(_, _) => Err(Error::Faulted), 43 | Self::Terminated => Err(Error::Terminated), 44 | } 45 | } 46 | pub fn get_reason_code(&mut self) -> Option { 47 | match self { 48 | Self::Ok(_) => None, 49 | Self::Faulted(_, r) => Some(*r), 50 | Self::Terminated => None, 51 | } 52 | } 53 | 54 | pub fn fail(&mut self, reason_code: ReasonCode) { 55 | *self = match mem::take(self) { 56 | Self::Ok(n) | Self::Faulted(n, _) => Self::Faulted(n, reason_code), 57 | Self::Terminated => Self::Terminated, 58 | } 59 | } 60 | 61 | pub fn terminate(&mut self) -> Option { 62 | match mem::take(self) { 63 | Self::Ok(n) => Some(n), 64 | Self::Faulted(n, _) => Some(n), 65 | Self::Terminated => None, 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/v5/packet/pubacks/types.rs: -------------------------------------------------------------------------------- 1 | use crate::{header::PacketType, types::ReasonCode}; 2 | 3 | #[derive(Debug)] 4 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 5 | pub struct Ack; 6 | #[derive(Debug)] 7 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 8 | pub struct Rec; 9 | #[derive(Debug)] 10 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 11 | pub struct Rel; 12 | #[derive(Debug)] 13 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 14 | pub struct Comp; 15 | 16 | pub trait PubackPacketType { 17 | const PACKET_TYPE: PacketType; 18 | const FLAGS: u8; 19 | 20 | fn reason_code_allowed(reason_code: ReasonCode) -> bool; 21 | } 22 | 23 | impl PubackPacketType for Ack { 24 | const PACKET_TYPE: PacketType = PacketType::Puback; 25 | const FLAGS: u8 = 0x00; 26 | 27 | fn reason_code_allowed(reason_code: ReasonCode) -> bool { 28 | is_ack_rec_reason_code(reason_code) 29 | } 30 | } 31 | 32 | impl PubackPacketType for Rec { 33 | const PACKET_TYPE: PacketType = PacketType::Pubrec; 34 | const FLAGS: u8 = 0x00; 35 | 36 | fn reason_code_allowed(reason_code: ReasonCode) -> bool { 37 | is_ack_rec_reason_code(reason_code) 38 | } 39 | } 40 | 41 | impl PubackPacketType for Rel { 42 | const PACKET_TYPE: PacketType = PacketType::Pubrel; 43 | const FLAGS: u8 = 0x02; 44 | 45 | fn reason_code_allowed(reason_code: ReasonCode) -> bool { 46 | is_rel_comp_reason_code(reason_code) 47 | } 48 | } 49 | 50 | impl PubackPacketType for Comp { 51 | const PACKET_TYPE: PacketType = PacketType::Pubcomp; 52 | const FLAGS: u8 = 0x00; 53 | 54 | fn reason_code_allowed(reason_code: ReasonCode) -> bool { 55 | is_rel_comp_reason_code(reason_code) 56 | } 57 | } 58 | 59 | fn is_ack_rec_reason_code(reason_code: ReasonCode) -> bool { 60 | matches!( 61 | reason_code, 62 | ReasonCode::Success 63 | | ReasonCode::NoMatchingSubscribers 64 | | ReasonCode::UnspecifiedError 65 | | ReasonCode::ImplementationSpecificError 66 | | ReasonCode::NotAuthorized 67 | | ReasonCode::TopicNameInvalid 68 | | ReasonCode::PacketIdentifierInUse 69 | | ReasonCode::QuotaExceeded 70 | | ReasonCode::PayloadFormatInvalid 71 | ) 72 | } 73 | 74 | fn is_rel_comp_reason_code(reason_code: ReasonCode) -> bool { 75 | matches!( 76 | reason_code, 77 | ReasonCode::Success | ReasonCode::PacketIdentifierNotFound 78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /src/types/qos.rs: -------------------------------------------------------------------------------- 1 | /// MQTT's Quality of Service 2 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)] 3 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 4 | pub enum QoS { 5 | /// Quality of Service Level 0. Publications with this level are only sent once. 6 | AtMostOnce = 0, 7 | /// Quality of Service Level 1. Publications with this level are sent until a PUBACK indicates that it was received. 8 | AtLeastOnce = 1, 9 | /// Quality of Service Level 2. Publications with this level are followed by a handshake assuring it is received once. 10 | ExactlyOnce = 2, 11 | } 12 | impl From for QoS { 13 | fn from(value: IdentifiedQoS) -> Self { 14 | match value { 15 | IdentifiedQoS::AtMostOnce => Self::AtMostOnce, 16 | IdentifiedQoS::AtLeastOnce(_) => Self::AtLeastOnce, 17 | IdentifiedQoS::ExactlyOnce(_) => Self::ExactlyOnce, 18 | } 19 | } 20 | } 21 | 22 | impl QoS { 23 | pub(crate) fn into_bits(self, left_shift: u8) -> u8 { 24 | let bits = match self { 25 | Self::AtMostOnce => 0x00, 26 | Self::AtLeastOnce => 0x01, 27 | Self::ExactlyOnce => 0x02, 28 | }; 29 | 30 | bits << left_shift 31 | } 32 | 33 | pub(crate) fn try_from_bits(bits: u8) -> Result { 34 | match bits { 35 | 0x00 => Ok(Self::AtMostOnce), 36 | 0x01 => Ok(Self::AtLeastOnce), 37 | 0x02 => Ok(Self::ExactlyOnce), 38 | _ => Err(()), 39 | } 40 | } 41 | } 42 | 43 | /// MQTT's Quality of Service with special reference to the PUBLISH packet which contains 44 | /// a packet identifier if its Quality of Service is greater than 0. 45 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 46 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 47 | pub enum IdentifiedQoS { 48 | /// Quality of Service Level 0. PUBLISH packets do not contain a packet identifier. 49 | AtMostOnce, 50 | /// Quality of Service Level 1. PUBLISH packets contain the included packet identifier. 51 | AtLeastOnce(u16), 52 | /// Quality of Service Level 2. PUBLISH packets contain the included packet identifier. 53 | ExactlyOnce(u16), 54 | } 55 | 56 | impl IdentifiedQoS { 57 | /// Returns `Some(packet_identifer)` if IdentifiedQoS > 0 and therefore has to be 58 | /// identified and `None` otherwise. 59 | #[inline] 60 | pub fn packet_identifier(&self) -> Option { 61 | match self { 62 | Self::AtMostOnce => None, 63 | Self::AtLeastOnce(pid) => Some(*pid), 64 | Self::ExactlyOnce(pid) => Some(*pid), 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/packet/rx.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | 3 | use crate::{ 4 | buffer::BufferProvider, 5 | eio::Read, 6 | header::FixedHeader, 7 | io::{ 8 | err::{BodyReadError, ReadError}, 9 | read::BodyReader, 10 | }, 11 | packet::Packet, 12 | v5::property::AtMostOncePropertyError, 13 | }; 14 | 15 | pub trait RxPacket<'p>: Packet + Sized { 16 | /// Receives a packet. Must check the fixed header for correctness. 17 | async fn receive>( 18 | header: &FixedHeader, 19 | reader: BodyReader<'_, 'p, R, B>, 20 | ) -> Result>; 21 | } 22 | 23 | #[derive(Debug)] 24 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 25 | pub enum RxError { 26 | Read(E), 27 | Buffer(B), 28 | 29 | /// Constant space somewhere is not enough, e.g. `Vec` in SUBACK 30 | InsufficientConstSpace, 31 | 32 | UnexpectedEOF, 33 | 34 | MalformedPacket, 35 | ProtocolError, 36 | } 37 | 38 | impl From> for RxError { 39 | fn from(e: BodyReadError) -> Self { 40 | match e { 41 | BodyReadError::Read(e) => Self::Read(e), 42 | BodyReadError::Buffer(b) => Self::Buffer(b), 43 | BodyReadError::UnexpectedEOF => Self::UnexpectedEOF, 44 | BodyReadError::InsufficientRemainingLen => Self::MalformedPacket, 45 | BodyReadError::MalformedPacket => Self::MalformedPacket, 46 | BodyReadError::ProtocolError => Self::ProtocolError, 47 | } 48 | } 49 | } 50 | impl From>> for RxError { 51 | fn from(e: ReadError>) -> Self { 52 | match e { 53 | ReadError::Read(e) => e.into(), 54 | ReadError::UnexpectedEOF => Self::UnexpectedEOF, 55 | ReadError::MalformedPacket => Self::MalformedPacket, 56 | ReadError::ProtocolError => Self::ProtocolError, 57 | } 58 | } 59 | } 60 | impl From> for RxError { 61 | fn from(e: ReadError) -> Self { 62 | match e { 63 | ReadError::Read(e) => Self::Read(e), 64 | ReadError::UnexpectedEOF => Self::UnexpectedEOF, 65 | ReadError::MalformedPacket => Self::MalformedPacket, 66 | ReadError::ProtocolError => Self::ProtocolError, 67 | } 68 | } 69 | } 70 | impl From>>> 71 | for RxError 72 | { 73 | fn from(e: AtMostOncePropertyError>>) -> Self { 74 | match e { 75 | AtMostOncePropertyError::Read(e) => e.into(), 76 | AtMostOncePropertyError::AlreadySet => Self::ProtocolError, 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/rx.rs: -------------------------------------------------------------------------------- 1 | use crate::{eio::Read, io::err::ReadError, types::VarByteInt}; 2 | use tokio_test::assert_ok; 3 | 4 | #[cfg(feature = "alloc")] 5 | use crate::buffer::AllocBuffer; 6 | #[cfg(feature = "bump")] 7 | use crate::buffer::BumpBuffer; 8 | 9 | use crate::{ 10 | buffer::BufferProvider, 11 | header::FixedHeader, 12 | io::read::{BodyReader, Readable}, 13 | packet::RxPacket, 14 | test::read::SliceReader, 15 | }; 16 | 17 | macro_rules! decode { 18 | ($t:ty, $remaining_len:literal, $bytes:expr) => {{ 19 | const LEN: usize = ($bytes).len(); 20 | const REMAINING_LEN: usize = ($remaining_len); 21 | 22 | let buffer: &mut [u8; LEN] = std::boxed::Box::leak(std::boxed::Box::new([0u8; LEN])); 23 | 24 | crate::test::rx::decode_packet::<$t, _, REMAINING_LEN>(($bytes), buffer).await 25 | }}; 26 | } 27 | 28 | impl Readable for FixedHeader { 29 | async fn read(net: &mut R) -> Result> { 30 | let type_and_flags = u8::read(net).await?; 31 | let remaining_len = VarByteInt::read(net).await?; 32 | Ok(Self { 33 | type_and_flags, 34 | remaining_len, 35 | }) 36 | } 37 | } 38 | 39 | pub async fn decode_packet<'a, T: RxPacket<'a>, const N: usize, const REMAINING_LEN: usize>( 40 | bytes: [u8; N], 41 | buffer: &'a mut [u8], 42 | ) -> T { 43 | let mut reader = SliceReader::new(&bytes); 44 | 45 | let mut buffer = create_buffer(buffer); 46 | 47 | let result = FixedHeader::read(&mut reader).await; 48 | let header = assert_ok!(result); 49 | 50 | let packet_type = assert_ok!(header.packet_type()); 51 | assert_eq!(packet_type, T::PACKET_TYPE, "Packet type not matching"); 52 | 53 | assert_eq!( 54 | header.remaining_len.size(), 55 | REMAINING_LEN, 56 | "Remaining length not matching" 57 | ); 58 | 59 | let reader = BodyReader::new(&mut reader, &mut buffer, REMAINING_LEN); 60 | let result = T::receive(&header, reader).await; 61 | let packet = assert_ok!(result); 62 | 63 | packet 64 | } 65 | 66 | #[allow(unused_variables)] 67 | fn create_buffer<'a>(buffer: &'a mut [u8]) -> impl BufferProvider<'a> { 68 | #[cfg(feature = "bump")] 69 | { 70 | BumpBuffer::new(buffer) 71 | } 72 | #[cfg(feature = "alloc")] 73 | { 74 | AllocBuffer 75 | } 76 | 77 | #[cfg(not(any(feature = "bump", feature = "alloc")))] 78 | { 79 | buffer::FailingBuffer 80 | } 81 | } 82 | 83 | #[cfg(not(any(feature = "bump", feature = "alloc")))] 84 | mod buffer { 85 | use crate::buffer::BufferProvider; 86 | 87 | pub struct FailingBuffer; 88 | impl<'a> BufferProvider<'a> for FailingBuffer { 89 | type Buffer = &'a mut [u8]; 90 | type ProvisionError = (); 91 | 92 | fn provide_buffer(&mut self, _: usize) -> Result { 93 | Err(()) 94 | } 95 | } 96 | } 97 | 98 | pub(crate) use decode; 99 | -------------------------------------------------------------------------------- /src/io/write.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | eio::Write, 3 | io::err::WriteError, 4 | types::{MqttBinary, MqttString}, 5 | }; 6 | 7 | pub trait Writable { 8 | fn written_len(&self) -> usize; 9 | async fn write(&self, write: &mut W) -> Result<(), WriteError>; 10 | } 11 | 12 | macro_rules! wlen { 13 | ( $W:ty ) => { 14 | <$W>::default().written_len() 15 | }; 16 | } 17 | 18 | impl Writable for Option { 19 | fn written_len(&self) -> usize { 20 | self.as_ref().map(|t| t.written_len()).unwrap_or_default() 21 | } 22 | 23 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 24 | match self { 25 | Some(t) => t.write(write).await, 26 | None => Ok(()), 27 | } 28 | } 29 | } 30 | 31 | impl Writable for [u8] { 32 | fn written_len(&self) -> usize { 33 | self.len() 34 | } 35 | 36 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 37 | let mut slice = self; 38 | while !slice.is_empty() { 39 | match write.write(slice).await? { 40 | 0 => return Err(WriteError::WriteZero), 41 | n => slice = &slice[n..], 42 | } 43 | } 44 | Ok(()) 45 | } 46 | } 47 | impl Writable for u8 { 48 | fn written_len(&self) -> usize { 49 | 1 50 | } 51 | 52 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 53 | self.to_be_bytes().write(write).await 54 | } 55 | } 56 | impl Writable for u16 { 57 | fn written_len(&self) -> usize { 58 | 2 59 | } 60 | 61 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 62 | self.to_be_bytes().write(write).await 63 | } 64 | } 65 | impl Writable for u32 { 66 | fn written_len(&self) -> usize { 67 | 4 68 | } 69 | 70 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 71 | self.to_be_bytes().write(write).await 72 | } 73 | } 74 | impl Writable for bool { 75 | fn written_len(&self) -> usize { 76 | wlen!(u8) 77 | } 78 | 79 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 80 | >::into(*self).write(write).await 81 | } 82 | } 83 | impl<'b> Writable for MqttBinary<'b> { 84 | fn written_len(&self) -> usize { 85 | self.0.len() + wlen!(u16) 86 | } 87 | 88 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 89 | let len = self.len(); 90 | len.write(write).await?; 91 | 92 | self.0.write(write).await?; 93 | 94 | Ok(()) 95 | } 96 | } 97 | impl<'b> Writable for MqttString<'b> { 98 | fn written_len(&self) -> usize { 99 | self.0.written_len() 100 | } 101 | 102 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 103 | self.0.write(write).await 104 | } 105 | } 106 | 107 | pub(crate) use wlen; 108 | -------------------------------------------------------------------------------- /src/header.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | eio::Write, 3 | fmt::unreachable, 4 | io::{ 5 | err::WriteError, 6 | write::{Writable, wlen}, 7 | }, 8 | types::VarByteInt, 9 | }; 10 | 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 12 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 13 | pub struct FixedHeader { 14 | pub(crate) type_and_flags: u8, 15 | pub(crate) remaining_len: VarByteInt, 16 | } 17 | 18 | impl Writable for FixedHeader { 19 | fn written_len(&self) -> usize { 20 | wlen!(u8) + self.remaining_len.written_len() 21 | } 22 | 23 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 24 | self.type_and_flags.write(write).await?; 25 | self.remaining_len.write(write).await?; 26 | 27 | Ok(()) 28 | } 29 | } 30 | 31 | impl FixedHeader { 32 | pub(crate) fn new(packet_type: PacketType, flags: u8, remaining_len: VarByteInt) -> Self { 33 | let packet_type = (packet_type as u8) << 4; 34 | Self { 35 | type_and_flags: packet_type | flags, 36 | remaining_len, 37 | } 38 | } 39 | 40 | pub fn flags(&self) -> u8 { 41 | self.type_and_flags & 0x0F 42 | } 43 | 44 | pub fn packet_type(&self) -> Result { 45 | PacketType::from_type_and_flags(self.type_and_flags) 46 | } 47 | } 48 | 49 | /// Returned if packet type is reserved 50 | #[derive(Debug)] 51 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 52 | pub struct Reserved; 53 | 54 | #[derive(Debug, PartialEq, Eq)] 55 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 56 | pub enum PacketType { 57 | Connect = 1, 58 | Connack = 2, 59 | Publish = 3, 60 | Puback = 4, 61 | Pubrec = 5, 62 | Pubrel = 6, 63 | Pubcomp = 7, 64 | Subscribe = 8, 65 | Suback = 9, 66 | Unsubscribe = 10, 67 | Unsuback = 11, 68 | Pingreq = 12, 69 | Pingresp = 13, 70 | Disconnect = 14, 71 | 72 | #[cfg(feature = "v5")] 73 | Auth = 15, 74 | } 75 | 76 | impl PacketType { 77 | pub fn from_type_and_flags(type_and_flags: u8) -> Result { 78 | match type_and_flags >> 4 { 79 | 0 => Err(Reserved), 80 | 1 => Ok(PacketType::Connect), 81 | 2 => Ok(PacketType::Connack), 82 | 3 => Ok(PacketType::Publish), 83 | 4 => Ok(PacketType::Puback), 84 | 5 => Ok(PacketType::Pubrec), 85 | 6 => Ok(PacketType::Pubrel), 86 | 7 => Ok(PacketType::Pubcomp), 87 | 8 => Ok(PacketType::Subscribe), 88 | 9 => Ok(PacketType::Suback), 89 | 10 => Ok(PacketType::Unsubscribe), 90 | 11 => Ok(PacketType::Unsuback), 91 | 12 => Ok(PacketType::Pingreq), 92 | 13 => Ok(PacketType::Pingresp), 93 | 14 => Ok(PacketType::Disconnect), 94 | 95 | #[cfg(feature = "v3")] 96 | 15 => Err(PacketTypeError::Reserved), 97 | 98 | #[cfg(feature = "v5")] 99 | 15 => Ok(PacketType::Auth), 100 | 101 | _ => unreachable!(), 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/write.rs: -------------------------------------------------------------------------------- 1 | use core::{cmp::min, fmt}; 2 | 3 | use crate::eio::{self, ErrorType, Write}; 4 | 5 | pub struct SliceWriter<'a> { 6 | slice: &'a mut [u8], 7 | index: usize, 8 | } 9 | 10 | #[derive(Debug)] 11 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 12 | pub struct SliceWriterError; 13 | impl fmt::Display for SliceWriterError { 14 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 15 | write!(f, "{:?}", self) 16 | } 17 | } 18 | impl core::error::Error for SliceWriterError { 19 | fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { 20 | None 21 | } 22 | } 23 | impl eio::Error for SliceWriterError { 24 | fn kind(&self) -> eio::ErrorKind { 25 | eio::ErrorKind::Other 26 | } 27 | } 28 | impl<'a> ErrorType for SliceWriter<'a> { 29 | type Error = SliceWriterError; 30 | } 31 | impl<'a> Write for SliceWriter<'a> { 32 | async fn write(&mut self, buf: &[u8]) -> Result { 33 | let left = self.slice.len().saturating_sub(self.index); 34 | let writing = min(left, buf.len()); 35 | let end = self.index + writing; 36 | if writing == 0 { 37 | return Ok(0); 38 | } 39 | 40 | let dest = &mut self.slice[self.index..end]; 41 | let src = &buf[..writing]; 42 | 43 | dest.clone_from_slice(src); 44 | self.index = end; 45 | Ok(writing) 46 | } 47 | 48 | async fn flush(&mut self) -> Result<(), Self::Error> { 49 | Ok(()) 50 | } 51 | } 52 | 53 | impl<'a> SliceWriter<'a> { 54 | pub fn new(slice: &'a mut [u8]) -> Self { 55 | Self { slice, index: 0 } 56 | } 57 | 58 | pub fn written(&self) -> usize { 59 | self.index 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod unit { 65 | use crate::{eio::Write, test::write::SliceWriter}; 66 | use tokio_test::assert_ok; 67 | 68 | #[tokio::test] 69 | #[test_log::test] 70 | async fn writes_in_one() { 71 | let mut buf = [0u8; 5]; 72 | { 73 | let mut writer = SliceWriter::new(&mut buf); 74 | 75 | let n = assert_ok!(writer.write(b"hello").await); 76 | assert_eq!(n, 5); 77 | 78 | let n = assert_ok!(writer.write(b"again").await); 79 | assert_eq!(n, 0); 80 | } 81 | 82 | assert_eq!(&buf, b"hello"); 83 | } 84 | 85 | #[tokio::test] 86 | #[test_log::test] 87 | async fn writes_in_chunks() { 88 | let mut buf = [0u8; 2]; 89 | { 90 | let mut writer = SliceWriter::new(&mut buf); 91 | 92 | let n1 = assert_ok!(writer.write(b"a").await); 93 | assert_eq!(n1, 1); 94 | 95 | let n2 = assert_ok!(writer.write(b"b").await); 96 | assert_eq!(n2, 1); 97 | 98 | let n3 = assert_ok!(writer.write(b"c").await); 99 | assert_eq!(n3, 0); 100 | } 101 | 102 | assert_eq!(&buf, b"ab"); 103 | } 104 | 105 | #[tokio::test] 106 | #[test_log::test] 107 | async fn partial_write_when_not_enough_space() { 108 | let mut buf = [0u8; 3]; 109 | { 110 | let mut writer = SliceWriter::new(&mut buf); 111 | 112 | let n1 = assert_ok!(writer.write(b"ab").await); 113 | assert_eq!(n1, 2); 114 | 115 | let n2 = assert_ok!(writer.write(b"xyz").await); 116 | assert_eq!(n2, 1); 117 | } 118 | 119 | assert_eq!(&buf, b"abx"); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/v5/packet/pings/mod.rs: -------------------------------------------------------------------------------- 1 | use core::marker::PhantomData; 2 | 3 | use crate::{ 4 | buffer::BufferProvider, 5 | eio::{Read, Write}, 6 | fmt::{error, trace}, 7 | header::{FixedHeader, PacketType}, 8 | io::{read::BodyReader, write::Writable}, 9 | packet::{Packet, RxError, RxPacket, TxError, TxPacket}, 10 | types::VarByteInt, 11 | v5::packet::pings::types::{PingPacketType, Req, Resp}, 12 | }; 13 | 14 | mod types; 15 | 16 | #[derive(Debug, Clone)] 17 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 18 | pub struct GenericPingPacket { 19 | phantom_data: PhantomData, 20 | } 21 | 22 | pub type PingreqPacket = GenericPingPacket; 23 | pub type PingrespPacket = GenericPingPacket; 24 | 25 | impl Packet for GenericPingPacket { 26 | const PACKET_TYPE: PacketType = T::PACKET_TYPE; 27 | } 28 | impl<'p, T: PingPacketType> RxPacket<'p> for GenericPingPacket { 29 | async fn receive>( 30 | header: &FixedHeader, 31 | _: BodyReader<'_, 'p, R, B>, 32 | ) -> Result> { 33 | trace!("decoding"); 34 | 35 | if header.flags() != T::FLAGS { 36 | error!("flags are not 0"); 37 | Err(RxError::MalformedPacket) 38 | } else { 39 | Ok(Self { 40 | phantom_data: PhantomData, 41 | }) 42 | } 43 | } 44 | } 45 | impl TxPacket for GenericPingPacket { 46 | async fn send(&self, write: &mut W) -> Result<(), TxError> { 47 | // Safety: 0 < VarByteInt::MAX_ENCODABLE 48 | let remaining_len = unsafe { VarByteInt::new_unchecked(0) }; 49 | 50 | FixedHeader::new(Self::PACKET_TYPE, T::FLAGS, remaining_len) 51 | .write(write) 52 | .await?; 53 | 54 | Ok(()) 55 | } 56 | } 57 | 58 | impl GenericPingPacket { 59 | pub fn new() -> Self { 60 | Self { 61 | phantom_data: PhantomData, 62 | } 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod unit { 68 | mod req { 69 | use crate::{ 70 | test::{rx::decode, tx::encode}, 71 | v5::packet::PingreqPacket, 72 | }; 73 | 74 | #[tokio::test] 75 | #[test_log::test] 76 | async fn encode() { 77 | #[rustfmt::skip] 78 | encode!( 79 | PingreqPacket::new(), 80 | [ 81 | 0xC0, // 82 | 0x00, // remaining length 83 | ] 84 | ); 85 | } 86 | 87 | #[tokio::test] 88 | #[test_log::test] 89 | async fn decode() { 90 | decode!(PingreqPacket, 0, [0xC0, 0x00]); 91 | } 92 | } 93 | 94 | mod resp { 95 | use crate::{ 96 | test::{rx::decode, tx::encode}, 97 | v5::packet::PingrespPacket, 98 | }; 99 | 100 | #[tokio::test] 101 | #[test_log::test] 102 | async fn encode() { 103 | #[rustfmt::skip] 104 | encode!( 105 | PingrespPacket::new(), 106 | [ 107 | 0xD0, // 108 | 0x00, // remaining length 109 | ] 110 | ); 111 | } 112 | 113 | #[tokio::test] 114 | #[test_log::test] 115 | async fn decode() { 116 | decode!(PingrespPacket, 0, [0xD0, 0x00]); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/types/binary.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | 3 | use crate::{ 4 | bytes::Bytes, 5 | types::{MqttString, TooLargeToEncode}, 6 | }; 7 | 8 | /// Arbitrary binary data with a length less than or equal to `Self::MAX_LENGTH` 9 | #[derive(Default, Clone, PartialEq, Eq)] 10 | pub struct MqttBinary<'b>(pub(crate) Bytes<'b>); 11 | 12 | impl<'b> fmt::Debug for MqttBinary<'b> { 13 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 14 | f.debug_tuple("MqttBinary").field(&self.as_ref()).finish() 15 | } 16 | } 17 | 18 | #[cfg(feature = "defmt")] 19 | impl<'a> defmt::Format for MqttBinary<'a> { 20 | fn format(&self, fmt: defmt::Formatter) { 21 | defmt::write!(fmt, "MqttBinary({:?}", self.as_ref()); 22 | } 23 | } 24 | 25 | impl<'b> TryFrom<&'b [u8]> for MqttBinary<'b> { 26 | type Error = TooLargeToEncode; 27 | 28 | fn try_from(value: &'b [u8]) -> Result { 29 | Self::from_slice(value) 30 | } 31 | } 32 | impl<'b> TryFrom<&'b str> for MqttBinary<'b> { 33 | type Error = TooLargeToEncode; 34 | 35 | fn try_from(value: &'b str) -> Result { 36 | Self::from_slice(value.as_bytes()) 37 | } 38 | } 39 | impl<'b> From> for MqttBinary<'b> { 40 | fn from(value: MqttString<'b>) -> Self { 41 | Self(value.0.0) 42 | } 43 | } 44 | 45 | impl<'b> AsRef<[u8]> for MqttBinary<'b> { 46 | fn as_ref(&self) -> &[u8] { 47 | &self.0 48 | } 49 | } 50 | 51 | impl<'b> MqttBinary<'b> { 52 | /// The maximum length of binary data so that it can be encoded. This value is limited by the 2-byte length field. 53 | pub const MAX_LENGTH: usize = u16::MAX as usize; 54 | 55 | /// Creates MQTT binary data and checks for the max length in bytes of `Self::MAX_LENGTH`. 56 | pub fn new(bytes: Bytes<'b>) -> Result { 57 | match bytes.len() { 58 | ..=Self::MAX_LENGTH => Ok(Self(bytes)), 59 | _ => Err(TooLargeToEncode), 60 | } 61 | } 62 | 63 | /// Creates MQTT binary data and checks for the max length in bytes of `Self::MAX_LENGTH`. 64 | pub const fn from_slice(slice: &'b [u8]) -> Result { 65 | match slice.len() { 66 | ..=Self::MAX_LENGTH => Ok(Self(Bytes::Borrowed(slice))), 67 | _ => Err(TooLargeToEncode), 68 | } 69 | } 70 | 71 | /// Creates MQTT binary data without checking for the max length in bytes of `Self::MAX_LENGTH`. 72 | /// 73 | /// # Safety 74 | /// The length of the slice parameter in bytes is less than or equal to `Self::MAX_LENGTH`. 75 | pub const unsafe fn new_unchecked(bytes: Bytes<'b>) -> Self { 76 | Self(bytes) 77 | } 78 | 79 | /// Creates MQTT binary data without checking for the max length in bytes of `Self::MAX_LENGTH`. 80 | /// 81 | /// # Safety 82 | /// The length of the slice parameter in bytes is less than or equal to `Self::MAX_LENGTH`. 83 | pub const unsafe fn from_slice_unchecked(slice: &'b [u8]) -> Self { 84 | Self(Bytes::Borrowed(slice)) 85 | } 86 | 87 | /// Returns the length of the underlying data. 88 | #[inline] 89 | pub const fn len(&self) -> u16 { 90 | self.0.len() as u16 91 | } 92 | 93 | /// Returns whether the underlying data is empty. 94 | #[inline] 95 | pub const fn is_empty(&self) -> bool { 96 | self.0.is_empty() 97 | } 98 | 99 | /// Delegates to `Bytes::as_borrowed()`. 100 | #[inline] 101 | pub fn as_borrowed(&'b self) -> Self { 102 | Self(self.0.as_borrowed()) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/read.rs: -------------------------------------------------------------------------------- 1 | use core::{cmp::min, fmt}; 2 | 3 | use crate::eio::{self, ErrorType, Read}; 4 | 5 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 6 | pub struct SliceReader<'a> { 7 | slice: &'a [u8], 8 | index: usize, 9 | } 10 | 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 12 | pub struct SliceReaderError; 13 | impl fmt::Display for SliceReaderError { 14 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 15 | write!(f, "{:?}", self) 16 | } 17 | } 18 | impl core::error::Error for SliceReaderError { 19 | fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { 20 | None 21 | } 22 | } 23 | impl eio::Error for SliceReaderError { 24 | fn kind(&self) -> eio::ErrorKind { 25 | eio::ErrorKind::Other 26 | } 27 | } 28 | impl<'a> ErrorType for SliceReader<'a> { 29 | type Error = SliceReaderError; 30 | } 31 | impl<'a> Read for SliceReader<'a> { 32 | async fn read(&mut self, buf: &mut [u8]) -> Result { 33 | let left = self.slice.len() - self.index; 34 | let reading = min(left, buf.len()); 35 | let end = self.index + reading; 36 | let src = &self.slice[self.index..end]; 37 | let dest = &mut buf[..reading]; 38 | 39 | dest.clone_from_slice(src); 40 | self.index = end; 41 | Ok(reading) 42 | } 43 | } 44 | 45 | impl<'a> SliceReader<'a> { 46 | pub fn new(slice: &'a [u8]) -> Self { 47 | Self { slice, index: 0 } 48 | } 49 | } 50 | 51 | #[cfg(test)] 52 | mod unit { 53 | use tokio_test::assert_ok; 54 | 55 | use crate::{eio::Read, test::read::SliceReader}; 56 | 57 | #[tokio::test] 58 | #[test_log::test] 59 | async fn read_full() { 60 | let mut reader = SliceReader::new(b"hello there"); 61 | let mut buf = [0u8; 5]; 62 | 63 | let n = assert_ok!(reader.read(&mut buf).await); 64 | assert_eq!(n, 5); 65 | assert_eq!(&buf, b"hello"); 66 | } 67 | 68 | #[tokio::test] 69 | #[test_log::test] 70 | async fn read_partial() { 71 | let mut reader = SliceReader::new(b"hello"); 72 | let mut buf = [0u8; 10]; 73 | 74 | let n = assert_ok!(reader.read(&mut buf).await); 75 | assert_eq!(n, 5); 76 | assert_eq!(&buf[..n], b"hello"); 77 | assert_eq!(&buf[n..], [0, 0, 0, 0, 0]); 78 | } 79 | 80 | #[tokio::test] 81 | #[test_log::test] 82 | async fn read_chunks() { 83 | let mut reader = SliceReader::new(b"ab"); 84 | let mut buf = [0u8; 1]; 85 | 86 | let n = assert_ok!(reader.read(&mut buf).await); 87 | assert_eq!(n, 1); 88 | assert_eq!(&buf, b"a"); 89 | 90 | let n = assert_ok!(reader.read(&mut buf).await); 91 | assert_eq!(n, 1); 92 | assert_eq!(&buf, b"b"); 93 | } 94 | 95 | #[tokio::test] 96 | #[test_log::test] 97 | async fn read_empty_eof() { 98 | let mut reader = SliceReader::new(b""); 99 | let mut buf = [0u8; 1]; 100 | 101 | let n = assert_ok!(reader.read(&mut buf).await); 102 | assert_eq!(n, 0); 103 | assert_eq!(&buf[..], [0]); 104 | } 105 | 106 | #[tokio::test] 107 | #[test_log::test] 108 | async fn read_some_eof() { 109 | let mut reader = SliceReader::new(b"hi"); 110 | let mut buf = [0u8; 2]; 111 | 112 | let n = assert_ok!(reader.read(&mut buf).await); 113 | assert_eq!(n, 2); 114 | assert_eq!(&buf[..], b"hi"); 115 | 116 | let n = assert_ok!(reader.read(&mut buf).await); 117 | assert_eq!(n, 0); 118 | assert_eq!(&buf[..], b"hi"); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/types/will.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | client::options::WillOptions, 3 | eio::Write, 4 | io::{err::WriteError, write::Writable}, 5 | types::{MqttBinary, MqttString, VarByteInt}, 6 | v5::property::{ 7 | ContentType, CorrelationData, MessageExpiryInterval, PayloadFormatIndicator, ResponseTopic, 8 | WillDelayInterval, 9 | }, 10 | }; 11 | 12 | #[derive(Debug, Clone)] 13 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 14 | pub struct Will<'w> { 15 | pub will_topic: MqttString<'w>, 16 | 17 | // Will properties 18 | pub will_delay_interval: Option, 19 | pub payload_format_indicator: Option, 20 | pub message_expiry_interval: Option, 21 | pub content_type: Option>, 22 | pub response_topic: Option>, 23 | pub correlation_data: Option>, 24 | 25 | pub will_payload: MqttBinary<'w>, 26 | } 27 | 28 | impl<'w> From> for Will<'w> { 29 | fn from(options: WillOptions<'w>) -> Self { 30 | Self { 31 | will_topic: options.will_topic, 32 | will_delay_interval: match options.will_delay_interval { 33 | 0 => None, 34 | i => Some(WillDelayInterval(i)), 35 | }, 36 | payload_format_indicator: match options.is_payload_utf8 { 37 | false => None, 38 | true => Some(PayloadFormatIndicator(true)), 39 | }, 40 | message_expiry_interval: options.message_expiry_interval.map(Into::into), 41 | content_type: options.content_type.map(Into::into), 42 | response_topic: options.response_topic.map(Into::into), 43 | correlation_data: options.correlation_data.map(Into::into), 44 | will_payload: options.will_payload, 45 | } 46 | } 47 | } 48 | 49 | impl<'p> Writable for Will<'p> { 50 | fn written_len(&self) -> usize { 51 | let will_properties_length = self.will_properties_length(); 52 | 53 | will_properties_length.written_len() 54 | + will_properties_length.size() 55 | + self.will_topic.written_len() 56 | + self.will_payload.written_len() 57 | } 58 | 59 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 60 | let will_properties_length = self.will_properties_length(); 61 | 62 | will_properties_length.write(write).await?; 63 | 64 | self.will_delay_interval.write(write).await?; 65 | self.payload_format_indicator.write(write).await?; 66 | self.message_expiry_interval.write(write).await?; 67 | self.content_type.write(write).await?; 68 | self.response_topic.write(write).await?; 69 | self.correlation_data.write(write).await?; 70 | 71 | self.will_topic.write(write).await?; 72 | self.will_payload.write(write).await?; 73 | 74 | Ok(()) 75 | } 76 | } 77 | 78 | impl<'p> Will<'p> { 79 | pub fn will_properties_length(&self) -> VarByteInt { 80 | let will_properties_length = self.will_delay_interval.written_len() 81 | + self.payload_format_indicator.written_len() 82 | + self.message_expiry_interval.written_len() 83 | + self.content_type.written_len() 84 | + self.response_topic.written_len() 85 | + self.correlation_data.written_len(); 86 | 87 | // Safety: 196626 < VarByteInt::MAX_ENCODABLE 88 | // will delay interval: 5 89 | // payload format indicator: 2 90 | // message expiry interval: 5 91 | // content type: 65538 92 | // response topic: 65538 93 | // correlation data: 65538 94 | unsafe { VarByteInt::new_unchecked(will_properties_length as u32) } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/io/err.rs: -------------------------------------------------------------------------------- 1 | use core::{ 2 | error::Error, 3 | fmt::{self, Display}, 4 | }; 5 | 6 | use crate::eio::{self, ErrorKind, ReadExactError}; 7 | 8 | #[derive(Debug, PartialEq, Eq)] 9 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 10 | pub enum ReadError { 11 | Read(E), 12 | UnexpectedEOF, 13 | MalformedPacket, 14 | ProtocolError, 15 | } 16 | 17 | impl From> for ReadError> { 18 | fn from(e: BodyReadError) -> Self { 19 | match e { 20 | e @ BodyReadError::InsufficientRemainingLen => ReadError::Read(e), 21 | e @ BodyReadError::Read(_) => ReadError::Read(e), 22 | e @ BodyReadError::Buffer(_) => ReadError::Read(e), 23 | BodyReadError::UnexpectedEOF => ReadError::UnexpectedEOF, 24 | BodyReadError::MalformedPacket => ReadError::MalformedPacket, 25 | BodyReadError::ProtocolError => ReadError::ProtocolError, 26 | } 27 | } 28 | } 29 | 30 | #[derive(Debug, PartialEq, Eq)] 31 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 32 | pub enum BodyReadError { 33 | Read(E), 34 | 35 | /// A buffer provision failed. 36 | Buffer(B), 37 | 38 | /// EOF has been returned by a Read method. 39 | UnexpectedEOF, 40 | 41 | /// There is not enough `remaining length` to read a packet field 42 | /// 43 | /// The difference to UnexpectedEOF is that this can be a boundary set by the programm. 44 | /// UnexpectedEOF is caused by the underlying Read 45 | InsufficientRemainingLen, 46 | 47 | MalformedPacket, 48 | ProtocolError, 49 | } 50 | impl Display for BodyReadError { 51 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 52 | write!(f, "{self:?}") 53 | } 54 | } 55 | impl Error for BodyReadError { 56 | fn source(&self) -> Option<&(dyn Error + 'static)> { 57 | match self { 58 | Self::Read(e) => e.source(), 59 | Self::Buffer(_) => None, 60 | Self::UnexpectedEOF => None, 61 | Self::InsufficientRemainingLen => None, 62 | Self::MalformedPacket => None, 63 | Self::ProtocolError => None, 64 | } 65 | } 66 | } 67 | impl eio::Error for BodyReadError { 68 | fn kind(&self) -> ErrorKind { 69 | match self { 70 | Self::Read(e) => e.kind(), 71 | Self::Buffer(_) => ErrorKind::OutOfMemory, 72 | Self::UnexpectedEOF => ErrorKind::Other, 73 | Self::InsufficientRemainingLen => ErrorKind::InvalidData, 74 | Self::MalformedPacket => ErrorKind::InvalidData, 75 | Self::ProtocolError => ErrorKind::InvalidData, 76 | } 77 | } 78 | } 79 | 80 | impl From for BodyReadError { 81 | fn from(e: E) -> Self { 82 | Self::Read(e) 83 | } 84 | } 85 | impl From> for BodyReadError { 86 | fn from(e: ReadExactError) -> Self { 87 | match e { 88 | ReadExactError::UnexpectedEof => Self::UnexpectedEOF, 89 | ReadExactError::Other(e) => Self::Read(e), 90 | } 91 | } 92 | } 93 | impl From> for BodyReadError { 94 | fn from(e: ReadError) -> Self { 95 | match e { 96 | ReadError::Read(e) => Self::Read(e), 97 | ReadError::UnexpectedEOF => Self::UnexpectedEOF, 98 | ReadError::MalformedPacket => Self::MalformedPacket, 99 | ReadError::ProtocolError => Self::ProtocolError, 100 | } 101 | } 102 | } 103 | 104 | #[derive(Debug, PartialEq, Eq)] 105 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 106 | pub enum WriteError { 107 | WriteZero, 108 | Write(E), 109 | } 110 | 111 | impl From for WriteError { 112 | fn from(e: E) -> Self { 113 | Self::Write(e) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-mqtt 2 | 3 | rust-mqtt is an MQTT client primarily for no_std environments. The library provides an async API depending on `embedded_io_async`'s traits. As of now, only MQTT version 5.0 is supported. 4 | 5 | ## Library state 6 | 7 | ### Supported MQTT features 8 | 9 | - Will 10 | - Bidirectional publications with Quality of Service 0, 1 and 2 11 | - Flow control 12 | - Configuration & session tracking 13 | - Session recovery 14 | 15 | ### Currently unsupported MQTT features & limitations 16 | 17 | - AUTH packet 18 | - User properties 19 | - Serverside maximum packet size 20 | - Request/Response 21 | - Subscription identifiers 22 | - Topic alias 23 | - Subscribing to multiple topics in a single packet 24 | - Message expiry interval 25 | 26 | ### Extension plans (more or less by priority) 27 | 28 | - Receive the 'remaining length' (variable header & payload) of an mqtt packet using a buffer instead of calling `Read::read` very frequently. 29 | - Refrain from sending packets exceeding server's maximum packet size. 30 | - MQTT version 3 31 | - Sync implementation. 32 | 33 | ### Feature flags 34 | 35 | - `log`: Enables logging via the `log` crate 36 | - `defmt`: Implements `defmt::Format` for crate items & enables logging via the `defmt` crate (version 1) 37 | - `bump`: Adds a simple bump allocator `BufferProvider` implementation 38 | - `alloc`: Adds a heap-allocation based `BufferProvider` implementation using the `alloc` crate 39 | - `v3`: Unused 40 | - `v5`: Enables MQTT version 5.0 41 | 42 | ## Usage 43 | 44 | ### Examples 45 | 46 | The example 'demo' contains most of rust-mqtt's features. Note that the example is very specific and showcases the client API and is not a good way to use the client. 47 | Out of the box, it connects to a broker on localhost:1883 with basic authentication. The easiest way to set this up is by installing, configuring and running Mosquitto using the CI configuration: 48 | 49 | ```bash 50 | cp .ci/mqtt_pass_plain.txt .ci/mqtt_pass_hashed.txt 51 | chmod 700 .ci/mqtt_pass_hashed.txt 52 | mosquitto_passwd -U .ci/mqtt_pass_hashed.txt 53 | mosquitto -c .ci/mosquitto.conf -v 54 | ``` 55 | 56 | Then you can run the example with different logging configs and the bump/alloc features: 57 | 58 | ```bash 59 | RUST_LOG=debug cargo run --example demo 60 | RUST_LOG=trace cargo run --example demo --no-default-features --features "v5 log bump" 61 | ``` 62 | 63 | ### Tests 64 | 65 | Unit tests should be ran using both the 'alloc' and 'bump' features. 66 | 67 | ```bash 68 | cargo test unit 69 | cargo test unit --no-default-features --features "v5 bump" 70 | ``` 71 | 72 | For integration tests, you can set up the mosquitto broker as used in the CI pipeline. 73 | You should restart the broker after every run of the integration test suite as it 74 | carries non-idempotent state that will impact the tests. 75 | 76 | ```bash 77 | cp .ci/mqtt_pass_plain.txt .ci/mqtt_pass_hashed.txt 78 | chmod 700 .ci/mqtt_pass_hashed.txt 79 | mosquitto_passwd -U .ci/mqtt_pass_hashed.txt 80 | mosquitto -c .ci/mosquitto.conf [-d] 81 | ``` 82 | 83 | Then you can run integration tests with the alloc feature. 84 | 85 | ```bash 86 | cargo test integration 87 | ``` 88 | 89 | It can be helpful to see logging output when running tests. 90 | 91 | ```bash 92 | RUST_LOG=trace cargo test unit --no-default-features --features "v5 bump" -- --show-output 93 | RUST_LOG=warn cargo test -- --show-output 94 | RUST_LOG=info cargo test integration -- --show-output 95 | ``` 96 | 97 | The full test suite can run with the alloc feature, just make sure a fresh broker is up and running. 98 | 99 | ```bash 100 | cargo test 101 | ``` 102 | 103 | ## Acknowledgment 104 | 105 | This project could not be in state in which currently is without Ulf Lilleengen and the rest of the community 106 | from [Drogue IoT](https://github.com/drogue-iot). 107 | 108 | ## Contact 109 | 110 | For any information, open an issue if your matter could be helpful or interesting for others or should be documented. Otherwise contact us on email , . 111 | -------------------------------------------------------------------------------- /src/types/int.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | eio::{Read, Write}, 3 | fmt::unreachable, 4 | io::{ 5 | err::{ReadError, WriteError}, 6 | read::Readable, 7 | write::Writable, 8 | }, 9 | types::TooLargeToEncode, 10 | }; 11 | 12 | /// MQTT's variable byte integer encoding. Mainly used for packet length, but also throughout some properties. 13 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 14 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 15 | pub struct VarByteInt(u32); 16 | 17 | impl Readable for VarByteInt { 18 | async fn read(net: &mut R) -> Result> { 19 | let mut multiplier = 1; 20 | let mut value = 0; 21 | 22 | loop { 23 | let byte = u8::read(net).await?; 24 | 25 | value += (byte & 0x7F) as u32 * multiplier; 26 | if multiplier > 128 * 128 * 128 { 27 | return Err(ReadError::MalformedPacket); 28 | } 29 | multiplier *= 128; 30 | if byte & 128 == 0 { 31 | break; 32 | } 33 | } 34 | 35 | Ok(Self(value)) 36 | } 37 | } 38 | 39 | impl Writable for VarByteInt { 40 | fn written_len(&self) -> usize { 41 | match self.0 { 42 | 0..=127 => 1, 43 | 128..=16_383 => 2, 44 | 16_384..=2_097_151 => 3, 45 | 2_097_152..=Self::MAX_ENCODABLE => 4, 46 | _ => unreachable!( 47 | "Invariant, never occurs if VarByteInts are generated using From and TryFrom" 48 | ), 49 | } 50 | } 51 | 52 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 53 | let mut x = self.0; 54 | let mut encoded_byte: u8; 55 | 56 | loop { 57 | encoded_byte = (x % 128) as u8; 58 | x /= 128; 59 | 60 | if x > 0 { 61 | encoded_byte |= 128; 62 | } 63 | encoded_byte.write(write).await?; 64 | 65 | if x == 0 { 66 | return Ok(()); 67 | } 68 | } 69 | } 70 | } 71 | 72 | impl VarByteInt { 73 | /// The maximum encodable value using the variable byte integer encoding according to . 74 | pub const MAX_ENCODABLE: u32 = 268_435_455; 75 | 76 | /// Creates a variable byte integer without checking for the `VarByteInt::MAX_ENCODABLE` invariant. 77 | /// 78 | /// # Safety 79 | /// The value parameter is less than or equal to `VarByteInt::MAX_ENCODABLE` 80 | pub const unsafe fn new_unchecked(value: u32) -> Self { 81 | Self(value) 82 | } 83 | 84 | /// Returns the inner value. 85 | pub fn value(&self) -> u32 { 86 | self.0 87 | } 88 | 89 | /// Returns `Self::value() as usize` 90 | pub fn size(&self) -> usize { 91 | self.0 as usize 92 | } 93 | 94 | /// Decodes a variable byte integer from a slice. 95 | /// 96 | /// # Safety 97 | /// The slice contains a validly encoded variable byte integer and is not longer than that encoding. 98 | pub unsafe fn from_slice_unchecked(slice: &[u8]) -> Self { 99 | let mut multiplier = 1; 100 | let mut value = 0; 101 | 102 | for b in slice { 103 | value += (b & 0x7F) as u32 * multiplier; 104 | multiplier *= 128; 105 | if b & 128 == 0 { 106 | break; 107 | } 108 | } 109 | 110 | Self(value) 111 | } 112 | } 113 | 114 | impl TryFrom for VarByteInt { 115 | type Error = TooLargeToEncode; 116 | 117 | fn try_from(value: u32) -> Result { 118 | if value > Self::MAX_ENCODABLE { 119 | Err(TooLargeToEncode) 120 | } else { 121 | Ok(Self(value)) 122 | } 123 | } 124 | } 125 | impl From for VarByteInt { 126 | fn from(value: u16) -> Self { 127 | Self(value as u32) 128 | } 129 | } 130 | impl From for VarByteInt { 131 | fn from(value: u8) -> Self { 132 | Self(value as u32) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/types/string.rs: -------------------------------------------------------------------------------- 1 | use core::{ 2 | fmt, 3 | str::{Utf8Error, from_utf8, from_utf8_unchecked}, 4 | }; 5 | 6 | use crate::{ 7 | Bytes, 8 | types::{MqttBinary, TooLargeToEncode}, 9 | }; 10 | 11 | /// Arbitrary UTF-8 encoded string with a length in bytes less than or equal to `Self::MAX_LENGTH` 12 | #[derive(Default, Clone, PartialEq, Eq)] 13 | pub struct MqttString<'s>(pub(crate) MqttBinary<'s>); 14 | 15 | impl<'s> fmt::Debug for MqttString<'s> { 16 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 17 | f.debug_tuple("MqttString").field(&self.as_ref()).finish() 18 | } 19 | } 20 | 21 | #[cfg(feature = "defmt")] 22 | impl<'a> defmt::Format for MqttString<'a> { 23 | fn format(&self, fmt: defmt::Formatter) { 24 | defmt::write!(fmt, "MqttString({:?}", self.as_ref()); 25 | } 26 | } 27 | 28 | impl<'s> TryFrom> for MqttString<'s> { 29 | type Error = Utf8Error; 30 | 31 | fn try_from(value: MqttBinary<'s>) -> Result { 32 | from_utf8(value.0.as_ref())?; 33 | Ok(Self(value)) 34 | } 35 | } 36 | impl<'s> TryFrom<&'s str> for MqttString<'s> { 37 | type Error = TooLargeToEncode; 38 | 39 | fn try_from(value: &'s str) -> Result { 40 | let binary = MqttBinary::try_from(value.as_bytes())?; 41 | 42 | Ok(Self(binary)) 43 | } 44 | } 45 | 46 | impl<'s> AsRef for MqttString<'s> { 47 | fn as_ref(&self) -> &str { 48 | // Safety: MqttString contains valid UTF-8 49 | unsafe { from_utf8_unchecked(self.0.as_ref()) } 50 | } 51 | } 52 | 53 | impl<'s> MqttString<'s> { 54 | /// The maximum length of a string in bytes so that it can be encoded. This value is limited by the 2-byte length field. 55 | pub const MAX_LENGTH: usize = MqttBinary::MAX_LENGTH; 56 | 57 | /// Creates an MQTT string and checks for the max length in bytes of `Self::MAX_LENGTH`. 58 | /// 59 | /// # Important 60 | /// Does not check that the data is valid UTF-8! 61 | pub fn new(bytes: Bytes<'s>) -> Result { 62 | Ok(Self(MqttBinary::new(bytes)?)) 63 | } 64 | 65 | /// Creates an MQTT string and checks for the max length in bytes of `Self::MAX_LENGTH`. 66 | pub const fn from_slice(slice: &'s str) -> Result { 67 | match slice.len() { 68 | // Safety: The length of the slice parameter in bytes is less than or equal to `Self::MAX_LENGTH`. 69 | ..=Self::MAX_LENGTH => Ok(Self(unsafe { 70 | MqttBinary::from_slice_unchecked(slice.as_bytes()) 71 | })), 72 | _ => Err(TooLargeToEncode), 73 | } 74 | } 75 | 76 | /// Creates an MQTT string without checking for the max length in bytes of `Self::MAX_LENGTH`. 77 | /// 78 | /// # Safety 79 | /// The length of the slice parameter in bytes is less than or equal to `Self::MAX_LENGTH`. 80 | pub unsafe fn new_unchecked(bytes: Bytes<'s>) -> Self { 81 | // Safety: The length of the slice parameter in bytes is less than or equal to `Self::MAX_LENGTH`. 82 | Self(unsafe { MqttBinary::new_unchecked(bytes) }) 83 | } 84 | 85 | /// Creates an MQTT string without checking for the max length in bytes of `Self::MAX_LENGTH`. 86 | /// 87 | /// # Safety 88 | /// The length of the slice parameter in bytes is less than or equal to `Self::MAX_LENGTH`. 89 | pub const unsafe fn from_slice_unchecked(slice: &'s str) -> Self { 90 | // Safety: The length of the slice parameter in bytes is less than or equal to `Self::MAX_LENGTH`. 91 | Self(unsafe { MqttBinary::from_slice_unchecked(slice.as_bytes()) }) 92 | } 93 | 94 | /// Returns the length of the underlying data in bytes. 95 | #[inline] 96 | pub fn len(&self) -> u16 { 97 | self.0.len() 98 | } 99 | 100 | /// Returns whether the underlying data is empty. 101 | #[inline] 102 | pub const fn is_empty(&self) -> bool { 103 | self.0.is_empty() 104 | } 105 | 106 | /// Delegates to `Bytes::as_borrowed()`. 107 | #[inline] 108 | pub fn as_borrowed(&'s self) -> Self { 109 | Self(self.0.as_borrowed()) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/fmt.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | #[clippy::format_args] 4 | macro_rules! debug_assert_ { 5 | ($($x:tt)*) => { 6 | { 7 | #[cfg(not(feature = "defmt"))] 8 | ::core::debug_assert!($($x)*); 9 | #[cfg(feature = "defmt")] 10 | ::defmt::debug_assert!($($x)*); 11 | } 12 | }; 13 | } 14 | 15 | #[clippy::format_args] 16 | macro_rules! debug_assert_eq_ { 17 | ($($x:tt)*) => { 18 | { 19 | #[cfg(not(feature = "defmt"))] 20 | ::core::debug_assert_eq!($($x)*); 21 | #[cfg(feature = "defmt")] 22 | ::defmt::debug_assert_eq!($($x)*); 23 | } 24 | }; 25 | } 26 | 27 | #[clippy::format_args] 28 | macro_rules! debug_assert_ne_ { 29 | ($($x:tt)*) => { 30 | { 31 | #[cfg(not(feature = "defmt"))] 32 | ::core::debug_assert_ne!($($x)*); 33 | #[cfg(feature = "defmt")] 34 | ::defmt::debug_assert_ne!($($x)*); 35 | } 36 | }; 37 | } 38 | 39 | #[clippy::format_args] 40 | macro_rules! unreachable_ { 41 | ($($x:tt)*) => { 42 | { 43 | #[cfg(not(feature = "defmt"))] 44 | ::core::unreachable!($($x)*); 45 | #[cfg(feature = "defmt")] 46 | ::defmt::unreachable!($($x)*); 47 | } 48 | }; 49 | } 50 | 51 | #[clippy::format_args] 52 | macro_rules! panic_ { 53 | ($($x:tt)*) => { 54 | { 55 | #[cfg(not(feature = "defmt"))] 56 | ::core::panic!($($x)*); 57 | #[cfg(feature = "defmt")] 58 | ::defmt::panic!($($x)*); 59 | } 60 | }; 61 | } 62 | 63 | #[clippy::format_args] 64 | macro_rules! trace { 65 | ($s:literal $(, $x:expr)* $(,)?) => { 66 | { 67 | #[cfg(feature = "log")] 68 | ::log::trace!($s $(, $x)*); 69 | #[cfg(feature = "defmt")] 70 | ::defmt::trace!($s $(, $x)*); 71 | #[cfg(not(any(feature = "log", feature="defmt")))] 72 | let _ = ($( & $x ),*); 73 | } 74 | }; 75 | } 76 | 77 | #[clippy::format_args] 78 | macro_rules! debug { 79 | ($s:literal $(, $x:expr)* $(,)?) => { 80 | { 81 | #[cfg(feature = "log")] 82 | ::log::debug!($s $(, $x)*); 83 | #[cfg(feature = "defmt")] 84 | ::defmt::debug!($s $(, $x)*); 85 | #[cfg(not(any(feature = "log", feature="defmt")))] 86 | let _ = ($( & $x ),*); 87 | } 88 | }; 89 | } 90 | 91 | #[clippy::format_args] 92 | macro_rules! info { 93 | ($s:literal $(, $x:expr)* $(,)?) => { 94 | { 95 | #[cfg(feature = "defmt")] 96 | ::defmt::info!($s $(, $x)*); 97 | #[cfg(feature = "log")] 98 | ::log::info!($s $(, $x)*); 99 | #[cfg(not(any(feature = "log", feature="defmt")))] 100 | let _ = ($( & $x ),*); 101 | } 102 | }; 103 | } 104 | 105 | #[clippy::format_args] 106 | macro_rules! warn_ { 107 | ($s:literal $(, $x:expr)* $(,)?) => { 108 | { 109 | #[cfg(feature = "log")] 110 | ::log::warn!($s $(, $x)*); 111 | #[cfg(feature = "defmt")] 112 | ::defmt::warn!($s $(, $x)*); 113 | #[cfg(not(any(feature = "log", feature="defmt")))] 114 | let _ = ($( & $x ),*); 115 | } 116 | }; 117 | } 118 | 119 | #[clippy::format_args] 120 | macro_rules! error { 121 | ($s:literal $(, $x:expr)* $(,)?) => { 122 | { 123 | #[cfg(feature = "log")] 124 | ::log::error!($s $(, $x)*); 125 | #[cfg(feature = "defmt")] 126 | ::defmt::error!($s $(, $x)*); 127 | #[cfg(not(any(feature = "log", feature="defmt")))] 128 | let _ = ($( & $x ),*); 129 | } 130 | }; 131 | } 132 | 133 | pub(crate) use debug; 134 | pub(crate) use error; 135 | pub(crate) use info; 136 | pub(crate) use trace; 137 | pub(crate) use warn_ as warn; 138 | 139 | pub(crate) use debug_assert_ as debug_assert; 140 | pub(crate) use debug_assert_eq_ as debug_assert_eq; 141 | pub(crate) use debug_assert_ne_ as debug_assert_ne; 142 | 143 | pub(crate) use panic_ as panic; 144 | pub(crate) use unreachable_ as unreachable; 145 | -------------------------------------------------------------------------------- /src/v5/packet/unsubscribe.rs: -------------------------------------------------------------------------------- 1 | use heapless::Vec; 2 | 3 | use crate::{ 4 | eio::Write, 5 | header::{FixedHeader, PacketType}, 6 | io::write::Writable, 7 | packet::{Packet, TxError, TxPacket}, 8 | types::{MqttString, TooLargeToEncode, TopicFilter, VarByteInt}, 9 | }; 10 | 11 | #[derive(Debug, Clone)] 12 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 13 | pub struct UnsubscribePacket<'p, const MAX_TOPIC_FILTERS: usize> { 14 | packet_identifier: u16, 15 | topic_filters: Vec, MAX_TOPIC_FILTERS>, 16 | } 17 | 18 | impl<'p, const MAX_TOPIC_FILTERS: usize> Packet for UnsubscribePacket<'p, MAX_TOPIC_FILTERS> { 19 | const PACKET_TYPE: PacketType = PacketType::Unsubscribe; 20 | } 21 | impl<'p, const MAX_TOPIC_FILTERS: usize> TxPacket for UnsubscribePacket<'p, MAX_TOPIC_FILTERS> { 22 | /// If MAX_TOPIC_FILTERS is to less than or equal to 4095, it is guaranteed that TxError::RemainingLenExceeded is never returned. 23 | async fn send(&self, write: &mut W) -> Result<(), TxError> { 24 | FixedHeader::new(Self::PACKET_TYPE, 0x02, self.remaining_length()?) 25 | .write(write) 26 | .await?; 27 | 28 | self.packet_identifier.write(write).await?; 29 | self.properties_length().write(write).await?; 30 | 31 | for t in &self.topic_filters { 32 | t.as_ref().write(write).await?; 33 | } 34 | 35 | Ok(()) 36 | } 37 | } 38 | 39 | impl<'p, const MAX_TOPIC_FILTERS: usize> UnsubscribePacket<'p, MAX_TOPIC_FILTERS> { 40 | fn remaining_length(&self) -> Result { 41 | let variable_header_length = self.packet_identifier.written_len(); 42 | 43 | let properties_length = self.properties_length(); 44 | let total_properties_length = properties_length.size() + properties_length.written_len(); 45 | 46 | let body_length: usize = self 47 | .topic_filters 48 | .iter() 49 | .map(TopicFilter::as_ref) 50 | .map(MqttString::written_len) 51 | .sum(); 52 | 53 | let total_length = variable_header_length + total_properties_length + body_length; 54 | 55 | // MAX_TOPIC_FILTERS has to be less than or equal to 4095 to guarantee: 56 | // Max length = 3 + MAX_TOPIC_FILTERS * 65537 <= VarByteInt::MAX_ENCODABLE 57 | // packet identifier: 2 58 | // properties length: 1 59 | // properties: 0 60 | // topic filters: MAX_TOPIC_FILTERS * 65537 61 | VarByteInt::try_from(total_length as u32) 62 | } 63 | 64 | pub fn properties_length(&self) -> VarByteInt { 65 | // Safety: Max length = 0 < VarByteInt::MAX_ENCODABLE 66 | unsafe { VarByteInt::new_unchecked(0) } 67 | } 68 | } 69 | 70 | impl<'p, const MAX_TOPIC_FILTERS: usize> UnsubscribePacket<'p, MAX_TOPIC_FILTERS> { 71 | pub fn new( 72 | packet_identifier: u16, 73 | topic_filters: Vec, MAX_TOPIC_FILTERS>, 74 | ) -> Self { 75 | Self { 76 | packet_identifier, 77 | topic_filters, 78 | } 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod unit { 84 | use heapless::Vec; 85 | 86 | use crate::{ 87 | test::tx::encode, 88 | types::{MqttString, TopicFilter}, 89 | v5::packet::UnsubscribePacket, 90 | }; 91 | 92 | #[tokio::test] 93 | #[test_log::test] 94 | async fn encode_payload() { 95 | let mut topics = Vec::new(); 96 | 97 | topics 98 | .push(unsafe { 99 | TopicFilter::new_unchecked(MqttString::try_from("test/+/topic").unwrap()) 100 | }) 101 | .unwrap(); 102 | topics 103 | .push(unsafe { TopicFilter::new_unchecked(MqttString::try_from("test/#").unwrap()) }) 104 | .unwrap(); 105 | 106 | let packet: UnsubscribePacket<'_, 2> = UnsubscribePacket::new(9874, topics); 107 | 108 | #[rustfmt::skip] 109 | encode!(packet, [ 110 | 0xA2, 111 | 0x19, 112 | 0x26, // Packet identifier MSB 113 | 0x92, // Packet identifier LSB 114 | 0x00, // Property length 115 | // Payload 116 | 0x00, 0x0C, b't', b'e', b's', b't', b'/', b'+', b'/', b't', b'o', b'p', b'i', b'c', 117 | // Payload 118 | 0x00, 0x06, b't', b'e', b's', b't', b'/', b'#', 119 | ] 120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/bytes.rs: -------------------------------------------------------------------------------- 1 | use core::{borrow::Borrow, fmt, ops::Deref}; 2 | 3 | #[cfg(feature = "alloc")] 4 | use alloc::boxed::Box; 5 | 6 | /// Contiguous bytes in memory. Is either a `u8` slice or (with crate feature "alloc") an owned `Box`. 7 | /// 8 | /// It is recommended to almost always use owned `Bytes<'a>` instead of a reference `&Bytes<'a>`, 9 | /// as it makes this type compatible for code designed for both owned and borrowed variants. 10 | /// 11 | /// Important: Cloning this will clone the underlying Box if it is an owned variant. 12 | /// You can however borrow another owned `Bytes<'a>` by calling `as_borrowed()`. 13 | /// The `as_borrowed()` method is passed on through wrapper types, for example `MqttString`. 14 | pub enum Bytes<'a> { 15 | /// Owned variant, only available with the `std` or `alloc` feature enabled. 16 | #[cfg(feature = "alloc")] 17 | Owned(Box<[u8]>), 18 | 19 | /// Borrowed variant. 20 | Borrowed(&'a [u8]), 21 | } 22 | 23 | impl<'a> Bytes<'a> { 24 | /// Borrows `self` with its full lifetime to create another owned `Self` instance. 25 | #[inline] 26 | pub const fn as_borrowed(&'a self) -> Self { 27 | match self { 28 | #[cfg(feature = "alloc")] 29 | Self::Owned(b) => Self::Borrowed(b), 30 | Self::Borrowed(s) => Self::Borrowed(s), 31 | } 32 | } 33 | 34 | /// Returns the number of bytes. 35 | #[inline] 36 | pub const fn len(&self) -> usize { 37 | match self { 38 | #[cfg(feature = "alloc")] 39 | Self::Owned(b) => b.len(), 40 | Self::Borrowed(s) => s.len(), 41 | } 42 | } 43 | 44 | /// Returns whether the underlying data has a length of 0. 45 | #[inline] 46 | pub const fn is_empty(&self) -> bool { 47 | self.len() == 0 48 | } 49 | } 50 | 51 | impl<'a> Clone for Bytes<'a> { 52 | fn clone(&self) -> Self { 53 | match self { 54 | #[cfg(feature = "alloc")] 55 | Self::Owned(b) => Self::Owned(b.clone()), 56 | Self::Borrowed(s) => Self::Borrowed(s), 57 | } 58 | } 59 | } 60 | 61 | #[cfg(feature = "alloc")] 62 | impl<'a> From> for Box<[u8]> { 63 | fn from(bytes: Bytes<'a>) -> Self { 64 | match bytes { 65 | Bytes::Owned(b) => b, 66 | Bytes::Borrowed(s) => s.into(), 67 | } 68 | } 69 | } 70 | 71 | impl<'a> Default for Bytes<'a> { 72 | fn default() -> Self { 73 | Self::Borrowed(&[]) 74 | } 75 | } 76 | 77 | impl<'a> fmt::Debug for Bytes<'a> { 78 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 79 | match self { 80 | Self::Borrowed(x) => f.debug_tuple("Borrowed").field(x).finish(), 81 | #[cfg(feature = "alloc")] 82 | Self::Owned(x) => f.debug_tuple("Owned").field(x).finish(), 83 | } 84 | } 85 | } 86 | 87 | #[cfg(feature = "defmt")] 88 | impl<'a> defmt::Format for Bytes<'a> { 89 | fn format(&self, fmt: defmt::Formatter) { 90 | match self { 91 | Self::Borrowed(x) => defmt::write!(fmt, "Borrowed({:?})", *x), 92 | #[cfg(feature = "alloc")] 93 | Self::Owned(x) => defmt::write!(fmt, "Owned({:?})", x.as_ref()), 94 | } 95 | } 96 | } 97 | 98 | impl<'a> PartialEq for Bytes<'a> { 99 | fn eq(&self, other: &Self) -> bool { 100 | let one: &[u8] = self; 101 | let other: &[u8] = other; 102 | 103 | one == other 104 | } 105 | } 106 | impl<'a> Eq for Bytes<'a> {} 107 | 108 | impl<'a> From<&'a mut [u8]> for Bytes<'a> { 109 | fn from(value: &'a mut [u8]) -> Self { 110 | Self::Borrowed(value) 111 | } 112 | } 113 | impl<'a> From<&'a [u8]> for Bytes<'a> { 114 | fn from(value: &'a [u8]) -> Self { 115 | Self::Borrowed(value) 116 | } 117 | } 118 | impl<'a> From<&'a mut str> for Bytes<'a> { 119 | fn from(value: &'a mut str) -> Self { 120 | Self::Borrowed(value.as_bytes()) 121 | } 122 | } 123 | impl<'a> From<&'a str> for Bytes<'a> { 124 | fn from(value: &'a str) -> Self { 125 | Self::Borrowed(value.as_bytes()) 126 | } 127 | } 128 | 129 | #[cfg(feature = "alloc")] 130 | impl<'a> From> for Bytes<'a> { 131 | fn from(value: Box<[u8]>) -> Self { 132 | Self::Owned(value) 133 | } 134 | } 135 | 136 | impl<'a> Deref for Bytes<'a> { 137 | type Target = [u8]; 138 | 139 | fn deref(&self) -> &Self::Target { 140 | match self { 141 | Self::Borrowed(value) => value, 142 | #[cfg(feature = "alloc")] 143 | Self::Owned(value) => value, 144 | } 145 | } 146 | } 147 | 148 | impl<'a> Borrow<[u8]> for Bytes<'a> { 149 | fn borrow(&self) -> &[u8] { 150 | self 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/types/topic.rs: -------------------------------------------------------------------------------- 1 | use heapless::Vec; 2 | 3 | use crate::{ 4 | client::options::{RetainHandling, SubscriptionOptions}, 5 | eio::Write, 6 | io::{ 7 | err::WriteError, 8 | write::{Writable, wlen}, 9 | }, 10 | types::MqttString, 11 | }; 12 | 13 | /// A topic name string for that messages can be published on according to . 14 | /// Cannot contain wildcard characters. 15 | /// 16 | /// Examples: 17 | /// - "sport/tennis/player1" 18 | /// - "sport/tennis/player1/ranking" 19 | /// - "sport/tennis/player1/score/wimbledon" 20 | #[derive(Debug, Clone, PartialEq, Eq)] 21 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 22 | pub struct TopicName<'t>(MqttString<'t>); 23 | 24 | impl<'t> TopicName<'t> { 25 | /// Creates a new topic name without checking for correct syntax of the topic name string. 26 | /// 27 | /// # Safety 28 | /// The syntax of the topic name is valid. 29 | pub unsafe fn new_unchecked(topic: MqttString<'t>) -> Self { 30 | Self(topic) 31 | } 32 | 33 | /// Delegates to `Bytes::as_borrowed()`. 34 | #[inline] 35 | pub fn as_borrowed(&'t self) -> Self { 36 | Self(self.as_ref().as_borrowed()) 37 | } 38 | } 39 | 40 | impl<'t> AsRef> for TopicName<'t> { 41 | fn as_ref(&self) -> &MqttString<'t> { 42 | &self.0 43 | } 44 | } 45 | impl<'t> From> for MqttString<'t> { 46 | fn from(value: TopicName<'t>) -> Self { 47 | value.0 48 | } 49 | } 50 | 51 | /// A topic filter string for subscribing to certain topics according to . 52 | /// Can contain wildcard characters. 53 | /// 54 | /// Examples: 55 | /// - "sport/tennis/#" 56 | /// - "sport/+/player1" 57 | #[derive(Debug, Clone, PartialEq, Eq)] 58 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 59 | pub struct TopicFilter<'t>(MqttString<'t>); 60 | 61 | impl<'t> TopicFilter<'t> { 62 | /// Creates a new topic filter without checking for correct syntax of the topic filter string. 63 | /// 64 | /// # Safety 65 | /// The syntax of the topic filter is valid. 66 | pub unsafe fn new_unchecked(topic: MqttString<'t>) -> Self { 67 | Self(topic) 68 | } 69 | 70 | /// Delegates to `Bytes::as_borrowed()`. 71 | #[inline] 72 | pub fn as_borrowed(&'t self) -> Self { 73 | Self(self.as_ref().as_borrowed()) 74 | } 75 | } 76 | 77 | impl<'t> AsRef> for TopicFilter<'t> { 78 | fn as_ref(&self) -> &MqttString<'t> { 79 | &self.0 80 | } 81 | } 82 | impl<'t> From> for MqttString<'t> { 83 | fn from(value: TopicFilter<'t>) -> Self { 84 | value.0 85 | } 86 | } 87 | impl<'t> From> for TopicFilter<'t> { 88 | fn from(value: TopicName<'t>) -> Self { 89 | Self(value.0) 90 | } 91 | } 92 | 93 | #[derive(Debug, Clone, PartialEq, Eq)] 94 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 95 | pub struct SubscriptionFilter<'t> { 96 | topic: TopicFilter<'t>, 97 | subscription_options: u8, 98 | } 99 | 100 | impl<'p, const MAX_TOPIC_FILTERS: usize> Writable 101 | for Vec, MAX_TOPIC_FILTERS> 102 | { 103 | fn written_len(&self) -> usize { 104 | self.iter() 105 | .map(|t| &t.topic) 106 | .map(|t| t.as_ref().written_len() + wlen!(u8)) 107 | .sum() 108 | } 109 | 110 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 111 | for t in self { 112 | t.topic.as_ref().write(write).await?; 113 | t.subscription_options.write(write).await?; 114 | } 115 | 116 | Ok(()) 117 | } 118 | } 119 | 120 | impl<'t> SubscriptionFilter<'t> { 121 | pub fn new(topic: TopicFilter<'t>, options: &SubscriptionOptions) -> Self { 122 | let retain_handling_bits = match options.retain_handling { 123 | RetainHandling::AlwaysSend => 0x00, 124 | RetainHandling::SendIfNotSubscribedBefore => 0x10, 125 | RetainHandling::NeverSend => 0x20, 126 | }; 127 | 128 | let retain_as_published_bit = match options.retain_as_published { 129 | true => 0x08, 130 | false => 0x00, 131 | }; 132 | 133 | let no_local_bit = match options.no_local { 134 | true => 0x04, 135 | false => 0x00, 136 | }; 137 | 138 | let qos_bits = options.qos.into_bits(0); 139 | 140 | let subscribe_options_bits = 141 | retain_handling_bits | retain_as_published_bit | no_local_bit | qos_bits; 142 | 143 | Self { 144 | topic, 145 | subscription_options: subscribe_options_bits, 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/client/options/connect.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::{KeepAlive, SessionExpiryInterval}, 3 | types::{MqttBinary, MqttString, QoS, Will}, 4 | v5::property::{PayloadFormatIndicator, WillDelayInterval}, 5 | }; 6 | 7 | /// Options for a connection. 8 | #[derive(Debug, Clone)] 9 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 10 | pub struct Options<'c> { 11 | /// If set to true, a new session is started. If set to false, an existing session is continued. 12 | pub clean_start: bool, 13 | 14 | /// The setting of the keep alive the client wishes. Can be set to a different value by the server. 15 | pub keep_alive: KeepAlive, 16 | 17 | /// The setting of the session expiry interval the server wishes. Can be set to a different value by the server. 18 | pub session_expiry_interval: SessionExpiryInterval, 19 | 20 | /// The user name the client wishes to authenticate with. 21 | pub user_name: Option>, 22 | /// The user name the client wishes to perform basic authentication with. 23 | pub password: Option>, 24 | 25 | /// The will configuration for the session of the connection. 26 | pub will: Option>, 27 | } 28 | 29 | /// Options for configuring the client's will or last will in a session. 30 | /// The server can publish a single PUBLISH packet in place of the client. 31 | /// This process behaves as if the will message was published by the client. 32 | /// The will is published at the earlier of the following scenarios: 33 | /// - The session of the client ends. 34 | /// - The will delay interval passes. 35 | #[derive(Debug, Clone)] 36 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 37 | pub struct WillOptions<'c> { 38 | /// The quality of service that the server publishes the will message with in place of the client. 39 | pub will_qos: QoS, 40 | 41 | /// The value of the retain flag of the will message published by the server in place of the client. 42 | pub will_retain: bool, 43 | 44 | /// The topic of the will publication. 45 | pub will_topic: MqttString<'c>, 46 | 47 | /// The payload of the will publication. 48 | pub will_payload: MqttBinary<'c>, 49 | 50 | // Will properties starting here 51 | /// The interval in seconds that passes after a disconnection before the server publishes the will. 52 | /// The session of the client does not necessarily have to end for this scenario to happen. 53 | /// The client can reconnect before this interval has passed to prevent the will publication. 54 | /// If the value of the will delay interval is 0, the property is omitted on the network. 55 | pub will_delay_interval: u32, 56 | 57 | /// The payload format indicator property in the will publication. If set to false, the property 58 | /// is omitted on the network. 59 | pub is_payload_utf8: bool, 60 | 61 | /// The message expiry interval in seconds of the will publication. If set to `None`, the message 62 | /// does not expire and the message expiry interval property is omitted on the network. 63 | pub message_expiry_interval: Option, 64 | 65 | /// The content type property in the will publication. If set to `None`, the property is omitted 66 | /// on the network. 67 | pub content_type: Option>, 68 | 69 | /// The response topic property in the will publication. If set to `None`, the property is omitted 70 | /// on the network. 71 | pub response_topic: Option>, 72 | 73 | /// The correlation data property in the will publication. If set to `None`, the property is omitted 74 | /// on the network. 75 | pub correlation_data: Option>, 76 | } 77 | 78 | impl<'c> WillOptions<'c> { 79 | pub(crate) fn as_will(&'c self) -> Will<'c> { 80 | Will { 81 | will_topic: self.will_topic.as_borrowed(), 82 | will_delay_interval: match self.will_delay_interval { 83 | 0 => None, 84 | i => Some(WillDelayInterval(i)), 85 | }, 86 | payload_format_indicator: match self.is_payload_utf8 { 87 | false => None, 88 | true => Some(PayloadFormatIndicator(true)), 89 | }, 90 | message_expiry_interval: self.message_expiry_interval.map(Into::into), 91 | content_type: self 92 | .content_type 93 | .as_ref() 94 | .map(MqttString::as_borrowed) 95 | .map(Into::into), 96 | response_topic: self 97 | .response_topic 98 | .as_ref() 99 | .map(MqttString::as_borrowed) 100 | .map(Into::into), 101 | correlation_data: self 102 | .correlation_data 103 | .as_ref() 104 | .map(MqttBinary::as_borrowed) 105 | .map(Into::into), 106 | will_payload: self.will_payload.as_borrowed(), 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/client/event.rs: -------------------------------------------------------------------------------- 1 | //! Contains the main `Event` and content types the client can emit. 2 | 3 | use crate::{ 4 | bytes::Bytes, 5 | types::{IdentifiedQoS, MqttString, ReasonCode}, 6 | }; 7 | 8 | /// Events emitted by the client when receiving an MQTT packet. 9 | #[derive(Debug)] 10 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 11 | pub enum Event<'e> { 12 | /// The server sent a PINGRESP packet. 13 | Pingresp, 14 | 15 | /// The server sent a PUBLISH packet. 16 | /// 17 | /// The client has acted as follows: 18 | /// - QoS 0: No action 19 | /// - QoS 1: A PUBACK packet has been sent to the server. 20 | /// - QoS 2: A PUBREC packet has been sent to the server and the packet identifier is tracked as in flight 21 | Publish(Publish<'e>), 22 | 23 | /// The server sent a SUBACK packet matching a SUBSCRIBE packet. 24 | /// 25 | /// The subscription process is complete and was successful if the reason code indicates success. 26 | /// The SUBSCRIBE packet won't have to be resent. 27 | Suback(Suback), 28 | 29 | /// The server sent an UNSUBACK packet matching an UNSUBSCRIBE packet. 30 | /// 31 | /// The unsubscription process is complete and was successful if the reason code indicates success. 32 | /// The UNSUBSCRIBE packet won't have to be resent. 33 | Unsuback(Suback), 34 | 35 | /// The server sent a PUBACK or PUBREC with an erroneous reason code, 36 | /// therefore rejecting the publication. 37 | /// 38 | /// The publication process is aborted. 39 | PublishRejected(Pubrej), 40 | 41 | /// The server sent a PUBACK packet matching a QoS 1 PUBLISH packet 42 | /// confirming that the PUBLISH has been received. 43 | /// 44 | /// The QoS 1 publication process is complete, 45 | /// the PUBLISH packet won't have to be resent. 46 | PublishAcknowledged(Puback), 47 | 48 | /// The server sent a PUBREC packet matching a QoS 2 PUBLISH packet 49 | /// confirming that the PUBLISH has been received. 50 | /// 51 | /// The client has responded with a PUBREL packet. 52 | /// 53 | /// The first handshake of the QoS 2 publication process is complete, 54 | /// the PUBLISH packet won't have to be resent. 55 | PublishReceived(Puback), 56 | 57 | /// The server sent a PUBREL packet matching a QoS 2 PUBREC packet 58 | /// confirming that the PUBREC has been received. 59 | /// 60 | /// The client has responded with a PUBCOMP packet. 61 | /// 62 | /// The QoS 2 publication process is complete, 63 | /// the PUBREC packet won't have to be resent. 64 | PublishReleased(Puback), 65 | 66 | /// The server sent a PUBCOMP packet matching a QoS 2 PUBREL packet 67 | /// confirming that the PUBREL has been received. 68 | /// 69 | /// The QoS 2 publication process is complete, 70 | /// the PUBREL packet won't have to be resent. 71 | PublishComplete(Puback), 72 | 73 | /// The server sent a SUBACK, PUBACK, PUBREC, PUBREL or PUBCOMP 74 | /// packet with a packet identifier that is not in flight (anymore). 75 | /// 76 | /// The client has not responded to the server. 77 | Ignored, 78 | } 79 | 80 | /// Content of `Event::Suback`. 81 | #[derive(Debug)] 82 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 83 | pub struct Suback { 84 | /// Packet identifier of the acknowledged SUBSCRIBE packet. 85 | pub packet_identifier: u16, 86 | /// Reason code returned for the subscription. 87 | pub reason_code: ReasonCode, 88 | } 89 | 90 | /// Content of `Event::Publish`. 91 | #[derive(Debug)] 92 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 93 | pub struct Publish<'p> { 94 | /// The quality of service the server determined to use for this publication. It is the minimum of 95 | /// the matching subscription with the highest quality of service level and the quality of service of 96 | /// the publishing client's publication. 97 | /// 98 | /// If the quality of service is greater than 0, this includes the non-zero packet identifier of the 99 | /// PUBLISH packet. 100 | pub identified_qos: IdentifiedQoS, 101 | 102 | /// The DUP flag in the PUBLISH packet. If set to false, it indicates that this is the first occasion 103 | /// the server has attempted to send this publication. 104 | pub dup: bool, 105 | 106 | /// The retain flag in the PUBLISH packet. If set to true, it indicates that the publication is the 107 | /// result of a retained message. If set to false, this publication having been retained depends on 108 | /// the retain as published flag of the matching subscription. 109 | pub retain: bool, 110 | 111 | /// The exact topic of this publication. 112 | pub topic: MqttString<'p>, 113 | 114 | /// The application message of this publication. 115 | pub message: Bytes<'p>, 116 | } 117 | 118 | /// Content of `Event::PublishAcknowledged`, `Event::PublishReceived`, `Event::PublishReleased` and 119 | /// `Event::PublishComplete`. 120 | #[derive(Debug)] 121 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 122 | pub struct Puback { 123 | /// Packet identifier of the acknowledged PUBLISH packet. 124 | pub packet_identifier: u16, 125 | /// Reason code of this state in the publication process 126 | pub reason_code: ReasonCode, 127 | } 128 | 129 | /// Content of `Event::PublishRejecetd`. 130 | #[derive(Debug)] 131 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 132 | pub struct Pubrej { 133 | /// Packet identifier of the rejected PUBLISH packet. 134 | pub packet_identifier: u16, 135 | /// Reason code of the rejection. 136 | pub reason_code: ReasonCode, 137 | } 138 | -------------------------------------------------------------------------------- /src/v5/property/types.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | eio::{Read, Write}, 3 | io::{ 4 | err::{ReadError, WriteError}, 5 | read::Readable, 6 | write::Writable, 7 | }, 8 | }; 9 | 10 | #[derive(Debug, Clone, Copy, PartialEq)] 11 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 12 | pub enum PropertyType { 13 | /// PUBLISH, Will Properties 14 | PayloadFormatIndicator, 15 | 16 | /// PUBLISH, Will Properties 17 | MessageExpiryInterval, 18 | 19 | /// PUBLISH, Will Properties 20 | ContentType, 21 | 22 | /// PUBLISH, Will Properties 23 | ResponseTopic, 24 | 25 | /// PUBLISH, Will Properties 26 | CorrelationData, 27 | 28 | /// PUBLISH, SUBSCRIBE 29 | SubscriptionIdentifier, 30 | 31 | /// CONNECT, CONNACK, DISCONNECT 32 | SessionExpiryInterval, 33 | 34 | /// CONNACK 35 | AssignedClientIdentifier, 36 | 37 | /// CONNACK 38 | ServerKeepAlive, 39 | 40 | /// CONNECT, CONNACK, AUTH 41 | AuthenticationMethod, 42 | 43 | /// CONNET, CONNACK, AUTH 44 | AuthenticationData, 45 | 46 | /// CONNECT 47 | RequestProblemInformation, 48 | 49 | /// Will Properties 50 | WillDelayInterval, 51 | 52 | /// CONNECT 53 | RequestResponseInformation, 54 | 55 | /// CONNACK 56 | ResponseInformation, 57 | 58 | /// CONNACK, DISCONNECT 59 | ServerReference, 60 | 61 | /// CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBACK, UNSUBACK, DISCONNECT, AUTH 62 | ReasonString, 63 | 64 | /// CONNECT, CONNACK 65 | ReceiveMaximum, 66 | 67 | /// CONNECT, CONNACK 68 | TopicAliasMaximum, 69 | 70 | /// PUBLISH 71 | TopicAlias, 72 | 73 | /// CONNACK 74 | MaximumQoS, 75 | 76 | /// CONNACK 77 | RetainAvailable, 78 | 79 | /// CONNECT, CONNACK, PUBLISH, Will Properties, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, DISCONNECT, AUTH 80 | UserProperty, 81 | 82 | /// CONNECT, CONNACK 83 | MaximumPacketSize, 84 | 85 | /// CONNACK 86 | WildcardSubscriptionAvailable, 87 | 88 | /// CONNACK 89 | SubscriptionIdentifierAvailable, 90 | 91 | /// CONNACK 92 | SharedSubscriptionAvailable, 93 | } 94 | 95 | impl PropertyType { 96 | pub const fn from_identifier(identifier: u8) -> Result { 97 | Ok(match identifier { 98 | 0x01 => Self::PayloadFormatIndicator, 99 | 0x02 => Self::MessageExpiryInterval, 100 | 0x03 => Self::ContentType, 101 | 0x08 => Self::ResponseTopic, 102 | 0x09 => Self::CorrelationData, 103 | 0x0B => Self::SubscriptionIdentifier, 104 | 0x11 => Self::SessionExpiryInterval, 105 | 0x12 => Self::AssignedClientIdentifier, 106 | 0x13 => Self::ServerKeepAlive, 107 | 0x15 => Self::AuthenticationMethod, 108 | 0x16 => Self::AuthenticationData, 109 | 0x17 => Self::RequestProblemInformation, 110 | 0x18 => Self::WillDelayInterval, 111 | 0x19 => Self::RequestResponseInformation, 112 | 0x1A => Self::ResponseInformation, 113 | 0x1C => Self::ServerReference, 114 | 0x1F => Self::ReasonString, 115 | 0x21 => Self::ReceiveMaximum, 116 | 0x22 => Self::TopicAliasMaximum, 117 | 0x23 => Self::TopicAlias, 118 | 0x24 => Self::MaximumQoS, 119 | 0x25 => Self::RetainAvailable, 120 | 0x26 => Self::UserProperty, 121 | 0x27 => Self::MaximumPacketSize, 122 | 0x28 => Self::WildcardSubscriptionAvailable, 123 | 0x29 => Self::SubscriptionIdentifierAvailable, 124 | 0x2A => Self::SharedSubscriptionAvailable, 125 | _ => return Err(()), 126 | }) 127 | } 128 | pub const fn identifier(&self) -> u8 { 129 | match self { 130 | Self::PayloadFormatIndicator => 0x01, 131 | Self::MessageExpiryInterval => 0x02, 132 | Self::ContentType => 0x03, 133 | Self::ResponseTopic => 0x08, 134 | Self::CorrelationData => 0x09, 135 | Self::SubscriptionIdentifier => 0x0B, 136 | Self::SessionExpiryInterval => 0x11, 137 | Self::AssignedClientIdentifier => 0x12, 138 | Self::ServerKeepAlive => 0x13, 139 | Self::AuthenticationMethod => 0x15, 140 | Self::AuthenticationData => 0x16, 141 | Self::RequestProblemInformation => 0x17, 142 | Self::WillDelayInterval => 0x18, 143 | Self::RequestResponseInformation => 0x19, 144 | Self::ResponseInformation => 0x1A, 145 | Self::ServerReference => 0x1C, 146 | Self::ReasonString => 0x1F, 147 | Self::ReceiveMaximum => 0x21, 148 | Self::TopicAliasMaximum => 0x22, 149 | Self::TopicAlias => 0x23, 150 | Self::MaximumQoS => 0x24, 151 | Self::RetainAvailable => 0x25, 152 | Self::UserProperty => 0x26, 153 | Self::MaximumPacketSize => 0x27, 154 | Self::WildcardSubscriptionAvailable => 0x28, 155 | Self::SubscriptionIdentifierAvailable => 0x29, 156 | Self::SharedSubscriptionAvailable => 0x2A, 157 | } 158 | } 159 | } 160 | 161 | impl Readable for PropertyType { 162 | async fn read(net: &mut R) -> Result> { 163 | let identifier = u8::read(net).await?; 164 | 165 | Self::from_identifier(identifier).map_err(|_| ReadError::MalformedPacket) 166 | } 167 | } 168 | 169 | impl Writable for PropertyType { 170 | fn written_len(&self) -> usize { 171 | 1 172 | } 173 | 174 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 175 | let identifier: u8 = self.identifier(); 176 | identifier.write(write).await 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/client/err.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | client::raw::RawError, 3 | eio::ErrorKind, 4 | header::Reserved, 5 | types::{MqttString, ReasonCode, TooLargeToEncode}, 6 | }; 7 | 8 | /// The main error returned by `Client`. 9 | /// 10 | /// Distincts between unrecoverable and recoverable errors. 11 | /// Recoverability in this context refers to whether the current network connection can 12 | /// be used for further communication after the error has occured. 13 | /// 14 | /// # Recovery 15 | /// - For unrecoverable errors, `Client::abort` can be called to send an optional DISCONNECT packet if allowed by specification. 16 | /// You can recover the session by calling `Client::recover` 17 | /// - For recoverable errors, follow the error-specific behaviour. 18 | #[derive(Debug, Clone, PartialEq, Eq)] 19 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 20 | pub enum Error<'e> { 21 | /// An underlying Read/Write method returned an error. 22 | /// 23 | /// Unrecoverable error. `Client::abort` should be called. 24 | Network(ErrorKind), 25 | 26 | /// The remote server did something the client does not understand / does not match the specification. 27 | /// 28 | /// Unrecoverable error. `Client::abort` should be called. 29 | Server, 30 | 31 | /// A buffer or generic constant provided by the user was too small to correctly receive a packet. 32 | /// 33 | /// Unrecoverable error. `Client::abort` should be called. 34 | ReceiveBuffer, 35 | 36 | /// A buffer provision by the `BufferProvider` failed. Therefore a packet could not be received correctly. 37 | /// 38 | /// Unrecoverable error. `Client::abort` should be called. 39 | Alloc, 40 | 41 | /// An AUTH packet header has been received by the client. AUTH packets are not supported by the client. 42 | /// The client has scheduled a DISCONNECT packet with `ReasonCode::ImplementationSpecificError`. 43 | /// The packet body has not been decoded. 44 | /// 45 | /// Unrecoverable error. `Client::abort` should be called. 46 | AuthPacketReceived, 47 | 48 | /// The client could not connect to the broker or the broker has sent a DISCONNECT packet. 49 | /// 50 | /// Unrecoverable error. `Client::abort` should be called. 51 | Disconnect { 52 | /// The reason code of the causing CONNACK or DISCONNECT packet. If the disconnection is caused 53 | /// by a CONNACK packet, the reason code ss always erroneous. 54 | reason: ReasonCode, 55 | /// The reason string property of the causing CONNACK or DISCONNECT packet if the server included 56 | /// a reason string. 57 | reason_string: Option>, 58 | }, 59 | 60 | /// A method tried to send a packet with a packet identifier that is not tracked as in flight. 61 | /// 62 | /// Recoverable error. No action has been taken by the client. 63 | PacketIdentifierNotInFlight, 64 | 65 | /// A packet was too long to encode its length with the variable byte integer. 66 | /// 67 | /// This can currently only be returned from `Client::publish` or `Client::republish` 68 | /// 69 | /// Recoverable error. No action has been taken by the client. 70 | PacketMaxLengthExceeded, 71 | 72 | /// An action was rejected because an internal buffer used for tracking session state is full. 73 | /// 74 | /// Recoverable error. Try again after an `Event` has been emitted that indicates that buffer might be free again. 75 | /// 76 | /// Example: 77 | /// `Client::subscribe` returns this error. Wait until an `Event::Suback` is received. 78 | /// This clears a spot in the subscribe packet identifiers. 79 | SessionBuffer, 80 | 81 | /// A publish now would exceed the server's receive maximum and ultimately cause a protocol error. 82 | /// 83 | /// Recoverable error. Try again after either an `Event::PublishAcknowledged` or `Event::PublishComplete` has 84 | /// been emitted that indicates that buffer might be free again. 85 | SendQuotaExceeded, 86 | 87 | /// A publish now with the given session expiry interval would cause a protocol error. 88 | /// 89 | /// A disconnection was attempted with a session expiry interval change where the session expiry interval in the 90 | /// CONNECT packet was zero (`SessionExpiryInterval::EndOnDisconnect`) and was greater than zero 91 | /// (`SessionExpiryInterval::NeverEnd | SessionExpiryInterval::Seconds(_`) in the DISCONNECT packet. 92 | /// 93 | /// Recoverable error. Try disconnecting again without an session expiry interval or with a session expiry interval 94 | /// of zero (`SessionExpiryInterval::EndOnDisconnect`). 95 | IllegalDisconnectSessionExpiryInterval, 96 | 97 | /// Another unrecoverable error has been returned earlier. The underlying connection is in a state, 98 | /// in which it refuses/is not able to perform regular communication. 99 | /// 100 | /// Unrecoverable error. `Client::abort` should be called. 101 | RecoveryRequired, 102 | } 103 | 104 | impl<'e> Error<'e> { 105 | /// Returns whether the client can recover from this error without closing the network connection. 106 | pub fn is_recoverable(&self) -> bool { 107 | matches!(self, Self::PacketIdentifierNotInFlight) 108 | } 109 | } 110 | 111 | impl<'e> From for Error<'e> { 112 | fn from(_: Reserved) -> Self { 113 | Self::Server 114 | } 115 | } 116 | 117 | impl<'e, B> From> for Error<'e> { 118 | fn from(e: RawError) -> Self { 119 | match e { 120 | RawError::PacketTooLong => Self::PacketMaxLengthExceeded, 121 | RawError::Disconnected => Self::RecoveryRequired, 122 | RawError::Network(e) => Self::Network(e), 123 | RawError::Alloc(_) => Self::Alloc, 124 | RawError::ConstSpace => Self::ReceiveBuffer, 125 | RawError::Server => Self::Server, 126 | } 127 | } 128 | } 129 | 130 | impl<'e> From for Error<'e> { 131 | fn from(_: TooLargeToEncode) -> Self { 132 | Self::PacketMaxLengthExceeded 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/session/mod.rs: -------------------------------------------------------------------------------- 1 | //! Contains utilities for session management. 2 | 3 | use heapless::Vec; 4 | 5 | mod flight; 6 | 7 | pub use flight::{CPublishFlightState, InFlightPublish, SPublishFlightState}; 8 | 9 | /// Session-associated information 10 | /// 11 | /// Client identifier is not stored here as it would lead to inconsistencies with the underyling allocation system. 12 | #[derive(Debug, Default)] 13 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 14 | pub struct Session { 15 | /// The currently in-flight outgoing publications. 16 | pub pending_client_publishes: Vec, RECEIVE_MAXIMUM>, 17 | /// The currently in-flight incoming publications. 18 | pub pending_server_publishes: Vec, SEND_MAXIMUM>, 19 | } 20 | 21 | impl 22 | Session 23 | { 24 | /// Returns whether the packet identifier is currently in-flight in a client->server publication process. 25 | pub fn is_used_cpublish_packet_identifier(&self, packet_identifier: u16) -> bool { 26 | self.cpublish_flight_state(packet_identifier).is_some() 27 | } 28 | /// Returns whether the packet identifier is currently in-flight in a server->client publication process. 29 | pub fn is_used_spublish_packet_identifier(&self, packet_identifier: u16) -> bool { 30 | self.spublish_flight_state(packet_identifier).is_some() 31 | } 32 | 33 | /// Returns the state of the publication of the packet identifier if the packet identifier is in-flight in an outgoing publication. 34 | pub fn cpublish_flight_state(&self, packet_identifier: u16) -> Option { 35 | self.pending_client_publishes 36 | .iter() 37 | .find(|f| f.packet_identifier == packet_identifier) 38 | .map(|f| f.state) 39 | } 40 | /// Returns the state of the publication of the packet identifier if the packet identifier is in-flight in an incoming publication. 41 | pub fn spublish_flight_state(&self, packet_identifier: u16) -> Option { 42 | self.pending_server_publishes 43 | .iter() 44 | .find(|f| f.packet_identifier == packet_identifier) 45 | .map(|f| f.state) 46 | } 47 | 48 | /// Returns the amount of currently in-flight outgoing publications. 49 | pub fn in_flight_cpublishes(&self) -> u16 { 50 | self.pending_client_publishes.len() as u16 51 | } 52 | /// Returns the amount of currently in-flight incoming publications. 53 | pub fn in_flight_spublishes(&self) -> u16 { 54 | self.pending_server_publishes.len() as u16 55 | } 56 | /// Returns the amount of slots for outgoing publications. 57 | pub fn cpublish_remaining_capacity(&self) -> u16 { 58 | (self.pending_client_publishes.capacity() - self.pending_client_publishes.len()) as u16 59 | } 60 | /// Returns the amount of slots for incoming publications. 61 | pub fn spublish_remaining_capacity(&self) -> u16 { 62 | (self.pending_server_publishes.capacity() - self.pending_server_publishes.len()) as u16 63 | } 64 | 65 | /// Adds an entry to await a PUBACK packet. Assumes the packet identifier has no entry currently. 66 | /// 67 | /// # Safety 68 | /// `self.pending_client_publishes` has free capacity. 69 | pub(crate) unsafe fn await_puback(&mut self, packet_identifier: u16) { 70 | // Safety: self.pending_client_publishes has free capacity. 71 | unsafe { 72 | self.pending_client_publishes 73 | .push(InFlightPublish { 74 | packet_identifier, 75 | state: CPublishFlightState::AwaitingPuback, 76 | }) 77 | .unwrap_unchecked() 78 | } 79 | } 80 | /// Adds an entry to await a PUBREC packet. Assumes the packet identifier has no entry currently. 81 | /// 82 | /// # Safety 83 | /// `self.pending_client_publishes` has free capacity. 84 | pub(crate) unsafe fn await_pubrec(&mut self, packet_identifier: u16) { 85 | // Safety: self.pending_client_publishes has free capacity. 86 | unsafe { 87 | self.pending_client_publishes 88 | .push(InFlightPublish { 89 | packet_identifier, 90 | state: CPublishFlightState::AwaitingPubrec, 91 | }) 92 | .unwrap_unchecked() 93 | } 94 | } 95 | /// Adds an entry to await a PUBREL packet. Assumes the packet identifier has no entry currently. 96 | /// 97 | /// # Safety 98 | /// `self.pending_server_publishes` has free capacity. 99 | pub(crate) unsafe fn await_pubrel(&mut self, packet_identifier: u16) { 100 | // Safety: self.pending_server_publishes has free capacity. 101 | unsafe { 102 | self.pending_server_publishes 103 | .push(InFlightPublish { 104 | packet_identifier, 105 | state: SPublishFlightState::AwaitingPubrel, 106 | }) 107 | .unwrap_unchecked() 108 | } 109 | } 110 | /// Adds an entry to await a PUBCOMP packet. Assumes the packet identifier has no entry currently. 111 | /// 112 | /// # Safety 113 | /// `self.pending_client_publishes` has free capacity. 114 | pub(crate) unsafe fn await_pubcomp(&mut self, packet_identifier: u16) { 115 | // Safety: self.pending_client_publishes has free capacity. 116 | unsafe { 117 | self.pending_client_publishes 118 | .push(InFlightPublish { 119 | packet_identifier, 120 | state: CPublishFlightState::AwaitingPubcomp, 121 | }) 122 | .unwrap_unchecked() 123 | } 124 | } 125 | 126 | pub(crate) fn remove_cpublish( 127 | &mut self, 128 | packet_identifier: u16, 129 | ) -> Option { 130 | self.pending_client_publishes 131 | .iter() 132 | .position(|s| s.packet_identifier == packet_identifier) 133 | .map(|i| { 134 | // Safety: `.iter().position()` confirms the index is within bounds. 135 | unsafe { self.pending_client_publishes.swap_remove_unchecked(i) }.state 136 | }) 137 | } 138 | pub(crate) fn remove_spublish( 139 | &mut self, 140 | packet_identifier: u16, 141 | ) -> Option { 142 | self.pending_server_publishes 143 | .iter() 144 | .position(|s| s.packet_identifier == packet_identifier) 145 | .map(|i| { 146 | // Safety: `.iter().position()` confirms the index is within bounds. 147 | unsafe { self.pending_server_publishes.swap_remove_unchecked(i) }.state 148 | }) 149 | } 150 | 151 | pub(crate) fn clear(&mut self) { 152 | self.pending_client_publishes.clear(); 153 | self.pending_server_publishes.clear(); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/buffer.rs: -------------------------------------------------------------------------------- 1 | //! Contains the trait the client uses to store slices of memory and basic implementations. 2 | 3 | use crate::bytes::Bytes; 4 | 5 | #[cfg(feature = "alloc")] 6 | pub use alloc::AllocBuffer; 7 | #[cfg(feature = "bump")] 8 | pub use bump::BumpBuffer; 9 | 10 | /// A trait to describe anything that can allocate memory. 11 | /// 12 | /// Returned memory can be borrowed or owned. Either way, it is bound by the `'a` 13 | /// lifetime - usually just the lifetime of the underlying buffer. 14 | /// 15 | /// The client does not store any references to memory returned by this provider. 16 | pub trait BufferProvider<'a> { 17 | /// The type returned from a successful buffer provision. 18 | /// Must implement `AsMut<[u8]>` so that it can be borrowed mutably right after allocation for initialization 19 | /// and `Into>` for storing. 20 | type Buffer: AsMut<[u8]> + Into>; 21 | 22 | /// The error type returned from a failed buffer provision. 23 | type ProvisionError: core::fmt::Debug; 24 | 25 | /// If successful, returns contiguous memory with a size in bytes of the `len` argument. 26 | fn provide_buffer(&mut self, len: usize) -> Result; 27 | } 28 | 29 | #[cfg(feature = "bump")] 30 | mod bump { 31 | use core::slice; 32 | 33 | use crate::buffer::BufferProvider; 34 | 35 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 36 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 37 | pub struct InsufficientSpace; 38 | 39 | /// Allocates memory from an underlying buffer by bumping up a pointer by the requested length. 40 | /// 41 | /// Can be resetted when no references to buffer contents exist. 42 | #[derive(Debug)] 43 | pub struct BumpBuffer<'a> { 44 | slice: &'a mut [u8], 45 | index: usize, 46 | } 47 | 48 | impl<'a> BufferProvider<'a> for BumpBuffer<'a> { 49 | type Buffer = &'a mut [u8]; 50 | type ProvisionError = InsufficientSpace; 51 | 52 | /// Return the next `len` bytes from the buffer, advancing the internal 53 | /// pointer. Returns `InsufficientSpace` if there isn't enough room. 54 | fn provide_buffer(&mut self, len: usize) -> Result { 55 | if self.remaining_len() < len { 56 | Err(InsufficientSpace) 57 | } else { 58 | let start = self.index; 59 | // Safety: we checked bounds above, and the pointer originates from 60 | // the backing slice owned by this struct with the same lifetime. 61 | let ptr = unsafe { self.slice.as_mut_ptr().add(start) }; 62 | // Advance index after computing start so callers don't observe a 63 | // partially-advanced state if we ever change ordering. 64 | self.index += len; 65 | 66 | // Safety: the slice starts at the self.index which is not part of any other reservations. 67 | // self.index has been skipped ahead the slice's full length 68 | let slice = unsafe { slice::from_raw_parts_mut(ptr, len) }; 69 | 70 | Ok(slice) 71 | } 72 | } 73 | } 74 | 75 | impl<'a> BumpBuffer<'a> { 76 | /// Creates a new `BumpBuffer` with the provided slice as underlying buffer. 77 | pub fn new(slice: &'a mut [u8]) -> Self { 78 | Self { slice, index: 0 } 79 | } 80 | 81 | /// Returns the remaining amount of unallocated bytes in the underlying buffer. 82 | #[inline] 83 | pub fn remaining_len(&self) -> usize { 84 | self.slice.len() - self.index 85 | } 86 | 87 | /// Invalidates all previous allocations by resetting the `BumpBuffer`'s index, 88 | /// allowing the underlying buffer to be reallocated. 89 | /// 90 | /// # Safety 91 | /// No more references exist to previously allocated slices / underlying buffer content. 92 | /// In the context of the client, this is the case when no server publication content 93 | /// (topic & message) and no reason strings still held. 94 | #[inline] 95 | pub unsafe fn reset(&mut self) { 96 | self.index = 0; 97 | } 98 | } 99 | 100 | #[cfg(test)] 101 | mod unit { 102 | use tokio_test::{assert_err, assert_ok}; 103 | 104 | use super::*; 105 | 106 | #[test] 107 | fn provide_buffer_and_remaining_len() { 108 | let mut backing = [0; 10]; 109 | 110 | { 111 | let mut buf = BumpBuffer::new(&mut backing); 112 | 113 | assert_eq!(buf.remaining_len(), 10); 114 | 115 | let s1 = assert_ok!(buf.provide_buffer(4)); 116 | assert_eq!(s1.len(), 4); 117 | 118 | s1.copy_from_slice(&[1, 2, 3, 4]); 119 | assert_eq!(buf.remaining_len(), 6); 120 | 121 | // take remaining 6 bytes 122 | let s2 = assert_ok!(buf.provide_buffer(6)); 123 | assert_eq!(s2.len(), 6); 124 | 125 | s2.copy_from_slice(&[5, 6, 7, 8, 9, 10]); 126 | assert_eq!(buf.remaining_len(), 0); 127 | 128 | assert_eq!(s1, [1, 2, 3, 4]); 129 | assert_eq!(s2, [5, 6, 7, 8, 9, 10]); 130 | 131 | let err = assert_err!(buf.provide_buffer(1)); 132 | assert_eq!(err, InsufficientSpace); 133 | } 134 | 135 | assert_eq!(backing, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); 136 | } 137 | 138 | #[test] 139 | fn reset_allows_reuse() { 140 | let mut backing = [0; 6]; 141 | 142 | { 143 | let mut buf = BumpBuffer::new(&mut backing); 144 | 145 | let s1 = assert_ok!(buf.provide_buffer(3)); 146 | s1.copy_from_slice(&[11, 12, 13]); 147 | 148 | // reset and take again from start 149 | unsafe { buf.reset() } 150 | let s2 = assert_ok!(buf.provide_buffer(3)); 151 | assert_eq!(s1, s2); 152 | } 153 | 154 | assert_eq!(backing, [11, 12, 13, 0, 0, 0]); 155 | } 156 | } 157 | } 158 | 159 | #[cfg(feature = "alloc")] 160 | mod alloc { 161 | use core::convert::Infallible; 162 | 163 | use alloc::boxed::Box; 164 | use alloc::vec; 165 | 166 | use crate::buffer::BufferProvider; 167 | 168 | /// Allocates memory using the global allocator. 169 | #[derive(Debug)] 170 | pub struct AllocBuffer; 171 | 172 | impl<'a> BufferProvider<'a> for AllocBuffer { 173 | type Buffer = Box<[u8]>; 174 | type ProvisionError = Infallible; 175 | 176 | /// Allocates `len` bytes on the heap 177 | fn provide_buffer(&mut self, len: usize) -> Result { 178 | let buffer = vec![0; len].into_boxed_slice(); 179 | 180 | Ok(buffer) 181 | } 182 | } 183 | 184 | #[cfg(test)] 185 | mod unit { 186 | use crate::buffer::{BufferProvider, alloc::AllocBuffer}; 187 | use tokio_test::assert_ok; 188 | 189 | #[test] 190 | fn provide_buffer() { 191 | let mut alloc = AllocBuffer; 192 | 193 | let buffer = alloc.provide_buffer(10); 194 | let buffer = assert_ok!(buffer); 195 | assert_eq!(10, buffer.len()); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/v5/packet/subscribe.rs: -------------------------------------------------------------------------------- 1 | use heapless::Vec; 2 | 3 | use crate::{ 4 | eio::Write, 5 | header::{FixedHeader, PacketType}, 6 | io::write::Writable, 7 | packet::{Packet, TxError, TxPacket}, 8 | types::{SubscriptionFilter, TooLargeToEncode, VarByteInt}, 9 | v5::property::SubscriptionIdentifier, 10 | }; 11 | 12 | #[derive(Debug, Clone)] 13 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 14 | pub struct SubscribePacket<'p, const MAX_TOPIC_FILTERS: usize> { 15 | packet_identifier: u16, 16 | 17 | subscription_identifier: Option, 18 | subscribe_filters: Vec, MAX_TOPIC_FILTERS>, 19 | } 20 | 21 | impl<'p, const MAX_TOPIC_FILTERS: usize> Packet for SubscribePacket<'p, MAX_TOPIC_FILTERS> { 22 | const PACKET_TYPE: PacketType = PacketType::Subscribe; 23 | } 24 | impl<'p, const MAX_TOPIC_FILTERS: usize> TxPacket for SubscribePacket<'p, MAX_TOPIC_FILTERS> { 25 | /// If MAX_TOPIC_FILTERS is to less than or equal to 4095, it is guaranteed that TxError::RemainingLenExceeded is never returned. 26 | async fn send(&self, write: &mut W) -> Result<(), TxError> { 27 | FixedHeader::new(Self::PACKET_TYPE, 0x02, self.remaining_length()?) 28 | .write(write) 29 | .await?; 30 | 31 | self.packet_identifier.write(write).await?; 32 | self.properties_length().write(write).await?; 33 | self.subscription_identifier.write(write).await?; 34 | self.subscribe_filters.write(write).await?; 35 | 36 | Ok(()) 37 | } 38 | } 39 | 40 | impl<'p, const MAX_TOPIC_FILTERS: usize> SubscribePacket<'p, MAX_TOPIC_FILTERS> { 41 | /// Creates a new packet with no subscription identifier 42 | /// 43 | /// It is up to the caller to verify that the topic filters are short enough 44 | /// so that the packets complete length doesn't exceed `VarByteInt::MAX_ENCODABLE`. 45 | pub fn new( 46 | packet_identifier: u16, 47 | subscribe_filters: Vec, MAX_TOPIC_FILTERS>, 48 | ) -> Self { 49 | Self { 50 | packet_identifier, 51 | subscription_identifier: None, 52 | subscribe_filters, 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | pub fn add_subscription_identifier(&mut self, subscription_identifier: VarByteInt) { 58 | self.subscription_identifier = Some(subscription_identifier.into()); 59 | } 60 | 61 | fn remaining_length(&self) -> Result { 62 | let variable_header_length = self.packet_identifier.written_len(); 63 | 64 | let properties_length = self.properties_length(); 65 | let total_properties_length = properties_length.size() + properties_length.written_len(); 66 | 67 | let body_length = self.subscribe_filters.written_len(); 68 | 69 | let total_length = variable_header_length + total_properties_length + body_length; 70 | 71 | // MAX_TOPIC_FILTERS has to be less than or equal to 4095 to guarantee: 72 | // Max length = 11 + MAX_TOPIC_FILTERS * 65538 <= VarByteInt::MAX_ENCODABLE 73 | // packet identifier: 2 74 | // properties length: 4 75 | // properties: 5 76 | // topic filters: MAX_TOPIC_FILTERS * 65538 77 | VarByteInt::try_from(total_length as u32) 78 | } 79 | 80 | pub fn properties_length(&self) -> VarByteInt { 81 | let len = self.subscription_identifier.written_len(); 82 | 83 | // Safety: Max length = 5 < VarByteInt::MAX_ENCODABLE 84 | // subscription identifier: 5 85 | unsafe { VarByteInt::new_unchecked(len as u32) } 86 | } 87 | } 88 | 89 | #[cfg(test)] 90 | mod unit { 91 | use heapless::Vec; 92 | 93 | use crate::{ 94 | client::options::{RetainHandling, SubscriptionOptions}, 95 | test::tx::encode, 96 | types::{MqttString, QoS, SubscriptionFilter, TopicFilter, VarByteInt}, 97 | v5::packet::SubscribePacket, 98 | }; 99 | 100 | #[tokio::test] 101 | #[test_log::test] 102 | async fn encode_payload() { 103 | let mut topics = Vec::new(); 104 | 105 | topics 106 | .push(SubscriptionFilter::new( 107 | unsafe { TopicFilter::new_unchecked(MqttString::try_from("test/hello").unwrap()) }, 108 | &SubscriptionOptions { 109 | retain_handling: RetainHandling::AlwaysSend, 110 | retain_as_published: false, 111 | no_local: true, 112 | qos: QoS::AtMostOnce, 113 | }, 114 | )) 115 | .unwrap(); 116 | 117 | topics 118 | .push(SubscriptionFilter::new( 119 | unsafe { TopicFilter::new_unchecked(MqttString::try_from("asdfjklo/#").unwrap()) }, 120 | &SubscriptionOptions { 121 | retain_handling: RetainHandling::NeverSend, 122 | retain_as_published: true, 123 | no_local: false, 124 | qos: QoS::ExactlyOnce, 125 | }, 126 | )) 127 | .unwrap(); 128 | let packet: SubscribePacket<'_, 2> = SubscribePacket::new(23197, topics); 129 | 130 | #[rustfmt::skip] 131 | encode!(packet, [ 132 | 0x82, // 133 | 0x1D, // remaining length 134 | 0x5A, // Packet identifier MSB 135 | 0x9D, // Packet identifier LSB 136 | 0x00, // Property length 137 | 0x00, // Payload - Topic Filter 138 | 0x0A, // | 139 | b't', // | 140 | b'e', // | 141 | b's', // | 142 | b't', // | 143 | b'/', // | 144 | b'h', // | 145 | b'e', // | 146 | b'l', // | 147 | b'l', // | 148 | b'o', // Payload - Topic Filter 149 | 0x04, // Payload - Subscription Options 150 | 0x00, // Payload - Topic Filter 151 | 0x0A, // | 152 | b'a', // | 153 | b's', // | 154 | b'd', // | 155 | b'f', // | 156 | b'j', // | 157 | b'k', // | 158 | b'l', // | 159 | b'o', // | 160 | b'/', // | 161 | b'#', // Payload - Topic Filter 162 | 0x2A, // Payload - Subscription Options 163 | ] 164 | ); 165 | } 166 | 167 | #[tokio::test] 168 | #[test_log::test] 169 | async fn encode_properties() { 170 | let mut topics = Vec::new(); 171 | topics 172 | .push(SubscriptionFilter::new( 173 | unsafe { TopicFilter::new_unchecked(MqttString::try_from("abc/+/y").unwrap()) }, 174 | &SubscriptionOptions { 175 | retain_handling: RetainHandling::SendIfNotSubscribedBefore, 176 | retain_as_published: true, 177 | no_local: false, 178 | qos: QoS::AtMostOnce, 179 | }, 180 | )) 181 | .unwrap(); 182 | 183 | let mut packet: SubscribePacket<'_, 10> = SubscribePacket::new(23197, topics); 184 | packet.add_subscription_identifier(VarByteInt::try_from(87986078u32).unwrap()); 185 | 186 | #[rustfmt::skip] 187 | encode!(packet, [ 188 | 0x82, // 189 | 0x12, // remaining length 190 | 0x5A, // Packet identifier MSB 191 | 0x9D, // Packet identifier LSB 192 | 0x05, // Property length 193 | // Property - Subscription Identifier 194 | 0x0B, 0x9E, 0x9F, 0xFA, 0x29, // Payload - Topic Filter 195 | 0x00, 0x07, b'a', b'b', b'c', b'/', b'+', b'/', b'y', 0x18, 196 | ] 197 | ); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /tests/load/mod.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use rust_mqtt::{ 3 | client::options::{PublicationOptions, SubscriptionOptions}, 4 | types::{IdentifiedQoS, QoS, TopicName}, 5 | }; 6 | use tokio::{ 7 | join, 8 | sync::oneshot::{Receiver, Sender, channel}, 9 | }; 10 | 11 | use crate::common::{ 12 | BROKER_ADDRESS, DEFAULT_DC_OPTIONS, NO_SESSION_CONNECT_OPTIONS, 13 | assert::{assert_ok, assert_published, assert_recv, assert_subscribe}, 14 | utils::{connected_client, disconnect, unique_topic}, 15 | }; 16 | 17 | const MSG: &str = "testMessage"; 18 | 19 | async fn publish_multiple( 20 | topic: TopicName<'_>, 21 | qos: QoS, 22 | count: u16, 23 | ready_rx: Receiver<()>, 24 | ) -> Result<(), ()> { 25 | let mut client = 26 | assert_ok!(connected_client(BROKER_ADDRESS, NO_SESSION_CONNECT_OPTIONS, None).await); 27 | 28 | info!("[Publisher] Waiting for receiver to be ready"); 29 | assert_ok!(ready_rx.await); 30 | info!("[Publisher] Receiver is ready, starting to publish"); 31 | 32 | let pub_options = PublicationOptions { 33 | retain: false, 34 | topic: topic.clone(), 35 | qos, 36 | }; 37 | 38 | let topic_name = topic.as_ref(); 39 | info!( 40 | "[Publisher] Sending {} messages to topic {:?}", 41 | count, topic_name 42 | ); 43 | for i in 0..count { 44 | assert_published!(client, pub_options, MSG.into()); 45 | if (i + 1) % 100 == 0 { 46 | info!("[Publisher] Sent {}/{} messages", i + 1, count); 47 | } 48 | } 49 | 50 | info!("[Publisher] Disconnecting after sending {} messages", count); 51 | disconnect(&mut client, DEFAULT_DC_OPTIONS).await; 52 | Ok(()) 53 | } 54 | 55 | async fn receive_multiple( 56 | topic_name: TopicName<'static>, 57 | qos: QoS, 58 | count: u16, 59 | ready_tx: Sender<()>, 60 | ) -> Result<(), ()> { 61 | let mut client = 62 | assert_ok!(connected_client(BROKER_ADDRESS, NO_SESSION_CONNECT_OPTIONS, None).await); 63 | 64 | let options = SubscriptionOptions { 65 | retain_handling: rust_mqtt::client::options::RetainHandling::AlwaysSend, 66 | retain_as_published: false, 67 | no_local: false, 68 | qos, 69 | }; 70 | 71 | info!("[Receiver] Subscribing to topic {:?}", topic_name.as_ref()); 72 | assert_subscribe!(client, options, topic_name.into()); 73 | 74 | info!("[Receiver] Subscription confirmed, signaling ready"); 75 | assert_ok!(ready_tx.send(())); 76 | 77 | info!("[Receiver] Waiting for {} messages", count); 78 | for i in 0..count { 79 | let publish = assert_recv!(client); 80 | assert_eq!(&*publish.message, MSG.as_bytes()); 81 | assert_eq!( 82 | >::into(publish.identified_qos), 83 | qos 84 | ); 85 | 86 | if (i + 1) % 100 == 0 { 87 | info!("[Receiver] Received {}/{} messages", i + 1, count); 88 | } 89 | } 90 | 91 | info!( 92 | "[Receiver] Disconnecting after receiving {} messages", 93 | count 94 | ); 95 | disconnect(&mut client, DEFAULT_DC_OPTIONS).await; 96 | Ok(()) 97 | } 98 | 99 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 100 | #[test_log::test] 101 | async fn load_test_ten_qos0() { 102 | let (topic_name, _) = unique_topic(); 103 | let (ready_tx, ready_rx) = channel(); 104 | 105 | let (r, p) = join!( 106 | receive_multiple(topic_name.clone(), QoS::AtMostOnce, 10, ready_tx), 107 | publish_multiple(topic_name, QoS::AtMostOnce, 10, ready_rx) 108 | ); 109 | assert_ok!(r); 110 | assert_ok!(p); 111 | } 112 | 113 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 114 | #[test_log::test] 115 | async fn load_test_ten_qos1() { 116 | let (topic_name, _) = unique_topic(); 117 | let (ready_tx, ready_rx) = channel(); 118 | 119 | let (r, p) = join!( 120 | receive_multiple(topic_name.clone(), QoS::AtLeastOnce, 10, ready_tx), 121 | publish_multiple(topic_name, QoS::AtLeastOnce, 10, ready_rx) 122 | ); 123 | assert_ok!(r); 124 | assert_ok!(p); 125 | } 126 | 127 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 128 | #[test_log::test] 129 | async fn load_test_ten_qos2() { 130 | let (topic_name, _) = unique_topic(); 131 | let (ready_tx, ready_rx) = channel(); 132 | 133 | let (r, p) = join!( 134 | receive_multiple(topic_name.clone(), QoS::ExactlyOnce, 10, ready_tx), 135 | publish_multiple(topic_name, QoS::ExactlyOnce, 10, ready_rx) 136 | ); 137 | assert_ok!(r); 138 | assert_ok!(p); 139 | } 140 | 141 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 142 | #[test_log::test] 143 | async fn load_test_fifty_qos0() { 144 | let (topic_name, _) = unique_topic(); 145 | let (ready_tx, ready_rx) = channel(); 146 | 147 | let (r, p) = join!( 148 | receive_multiple(topic_name.clone(), QoS::AtMostOnce, 50, ready_tx), 149 | publish_multiple(topic_name, QoS::AtMostOnce, 50, ready_rx) 150 | ); 151 | assert_ok!(r); 152 | assert_ok!(p); 153 | } 154 | 155 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 156 | #[test_log::test] 157 | async fn load_test_fifty_qos1() { 158 | let (topic_name, _) = unique_topic(); 159 | let (ready_tx, ready_rx) = channel(); 160 | 161 | let (r, p) = join!( 162 | receive_multiple(topic_name.clone(), QoS::AtLeastOnce, 50, ready_tx), 163 | publish_multiple(topic_name, QoS::AtLeastOnce, 50, ready_rx) 164 | ); 165 | assert_ok!(r); 166 | assert_ok!(p); 167 | } 168 | 169 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 170 | #[test_log::test] 171 | async fn load_test_fifty_qos2() { 172 | let (topic_name, _) = unique_topic(); 173 | let (ready_tx, ready_rx) = channel(); 174 | 175 | let (r, p) = join!( 176 | receive_multiple(topic_name.clone(), QoS::ExactlyOnce, 50, ready_tx), 177 | publish_multiple(topic_name, QoS::ExactlyOnce, 50, ready_rx) 178 | ); 179 | assert_ok!(r); 180 | assert_ok!(p); 181 | } 182 | 183 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 184 | #[test_log::test] 185 | async fn load_test_five_hundred_qos0() { 186 | let (topic_name, _) = unique_topic(); 187 | let (ready_tx, ready_rx) = channel(); 188 | 189 | let (r, p) = join!( 190 | receive_multiple(topic_name.clone(), QoS::AtMostOnce, 500, ready_tx), 191 | publish_multiple(topic_name, QoS::AtMostOnce, 500, ready_rx) 192 | ); 193 | assert_ok!(r); 194 | assert_ok!(p); 195 | } 196 | 197 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 198 | #[test_log::test] 199 | async fn load_test_five_hundred_qos1() { 200 | let (topic_name, _) = unique_topic(); 201 | let (ready_tx, ready_rx) = channel(); 202 | 203 | let (r, p) = join!( 204 | receive_multiple(topic_name.clone(), QoS::AtLeastOnce, 500, ready_tx), 205 | publish_multiple(topic_name, QoS::AtLeastOnce, 500, ready_rx) 206 | ); 207 | assert_ok!(r); 208 | assert_ok!(p); 209 | } 210 | 211 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 212 | #[test_log::test] 213 | async fn load_test_five_hundred_qos2() { 214 | let (topic_name, _) = unique_topic(); 215 | let (ready_tx, ready_rx) = channel(); 216 | 217 | let (r, p) = join!( 218 | receive_multiple(topic_name.clone(), QoS::ExactlyOnce, 500, ready_tx), 219 | publish_multiple(topic_name, QoS::ExactlyOnce, 500, ready_rx) 220 | ); 221 | assert_ok!(r); 222 | assert_ok!(p); 223 | } 224 | 225 | #[tokio::test(flavor = "multi_thread", worker_threads = 1)] 226 | #[test_log::test] 227 | async fn load_test_ten_thousand_qos0() { 228 | let (topic_name, _) = unique_topic(); 229 | let (ready_tx, ready_rx) = channel(); 230 | 231 | let (r, p) = join!( 232 | receive_multiple(topic_name.clone(), QoS::AtMostOnce, 10000, ready_tx), 233 | publish_multiple(topic_name, QoS::AtMostOnce, 10000, ready_rx) 234 | ); 235 | assert_ok!(r); 236 | assert_ok!(p); 237 | } 238 | -------------------------------------------------------------------------------- /examples/demo.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | net::{Ipv4Addr, SocketAddr}, 3 | time::Duration, 4 | }; 5 | 6 | use embedded_io_adapters::tokio_1::FromTokio; 7 | use log::{error, info}; 8 | 9 | #[cfg(feature = "alloc")] 10 | use rust_mqtt::buffer::AllocBuffer; 11 | #[cfg(feature = "bump")] 12 | use rust_mqtt::buffer::BumpBuffer; 13 | use rust_mqtt::{ 14 | Bytes, 15 | client::{ 16 | Client, 17 | event::{Event, Suback}, 18 | options::{ 19 | ConnectOptions, DisconnectOptions, PublicationOptions, RetainHandling, 20 | SubscriptionOptions, WillOptions, 21 | }, 22 | }, 23 | config::{KeepAlive, SessionExpiryInterval}, 24 | types::{MqttBinary, MqttString, QoS, TopicName}, 25 | }; 26 | use tokio::{net::TcpStream, select, time::sleep}; 27 | 28 | #[tokio::main] 29 | async fn main() { 30 | env_logger::init(); 31 | 32 | #[cfg(feature = "alloc")] 33 | let mut buffer = AllocBuffer; 34 | #[cfg(feature = "bump")] 35 | let mut buffer = [0; 1024]; 36 | #[cfg(feature = "bump")] 37 | let mut buffer = BumpBuffer::new(&mut buffer); 38 | 39 | let mut client = Client::<'_, _, _, 1, 1, 1>::new(&mut buffer); 40 | 41 | let addr = SocketAddr::new(Ipv4Addr::new(127, 0, 0, 1).into(), 1883); 42 | let connection = TcpStream::connect(addr).await.unwrap(); 43 | let connection = FromTokio::new(connection); 44 | 45 | match client 46 | .connect( 47 | connection, 48 | &ConnectOptions { 49 | session_expiry_interval: SessionExpiryInterval::Seconds(5), 50 | clean_start: false, 51 | keep_alive: KeepAlive::Seconds(3), 52 | will: Some(WillOptions { 53 | will_qos: QoS::ExactlyOnce, 54 | will_retain: true, 55 | will_topic: MqttString::try_from("dead").unwrap(), 56 | will_payload: MqttBinary::try_from("joe mama").unwrap(), 57 | will_delay_interval: 10, 58 | is_payload_utf8: true, 59 | message_expiry_interval: Some(20), 60 | content_type: Some(MqttString::try_from("txt").unwrap()), 61 | response_topic: None, 62 | correlation_data: None, 63 | }), 64 | user_name: Some(MqttString::try_from("test").unwrap()), 65 | password: Some(MqttBinary::try_from("testPass").unwrap()), 66 | }, 67 | Some(MqttString::try_from("rust-mqtt-demo-client").unwrap()), 68 | ) 69 | .await 70 | { 71 | Ok(c) => { 72 | info!("Connected to server {:?}", c); 73 | info!("{:?}", client.client_config()); 74 | info!("{:?}", client.server_config()); 75 | info!("{:?}", client.shared_config()); 76 | info!("{:?}", client.session()); 77 | } 78 | Err(e) => { 79 | error!("Failed to connect to server: {:?}", e); 80 | return; 81 | } 82 | }; 83 | 84 | #[cfg(feature = "bump")] 85 | unsafe { 86 | client.buffer().reset() 87 | }; 88 | 89 | let sub_options = SubscriptionOptions { 90 | retain_handling: RetainHandling::SendIfNotSubscribedBefore, 91 | retain_as_published: true, 92 | no_local: false, 93 | qos: QoS::ExactlyOnce, 94 | }; 95 | 96 | let topic = 97 | unsafe { TopicName::new_unchecked(MqttString::from_slice("rust-mqtt/is/great").unwrap()) }; 98 | 99 | match client.subscribe(topic.clone().into(), sub_options).await { 100 | Ok(_) => info!("Sent Subscribe"), 101 | Err(e) => { 102 | error!("Failed to subscribe: {:?}", e); 103 | return; 104 | } 105 | }; 106 | 107 | match client.poll().await { 108 | Ok(Event::Suback(Suback { 109 | packet_identifier: _, 110 | reason_code, 111 | })) => info!("Subscribed with reason code {:?}", reason_code), 112 | Ok(e) => { 113 | error!("Expected Suback but received event {:?}", e); 114 | return; 115 | } 116 | Err(e) => { 117 | error!("Failed to receive Suback {:?}", e); 118 | return; 119 | } 120 | } 121 | 122 | let pub_options = PublicationOptions { 123 | retain: false, 124 | topic: topic.clone(), 125 | qos: QoS::ExactlyOnce, 126 | }; 127 | 128 | let publish_packet_id = match client 129 | .publish(&pub_options, Bytes::from("anything".as_bytes())) 130 | .await 131 | { 132 | Ok(i) => { 133 | info!("Published message with packet identifier {}", i); 134 | i 135 | } 136 | Err(e) => { 137 | error!("Failed to send Publish {:?}", e); 138 | return; 139 | } 140 | }; 141 | 142 | let pub_options = PublicationOptions { 143 | retain: false, 144 | topic: topic.clone(), 145 | qos: QoS::ExactlyOnce, 146 | }; 147 | client 148 | .republish( 149 | publish_packet_id, 150 | &pub_options, 151 | Bytes::from("anything".as_bytes()), 152 | ) 153 | .await 154 | .unwrap(); 155 | 156 | loop { 157 | match client.poll().await { 158 | Ok(Event::PublishComplete(_)) => { 159 | info!("Publish complete"); 160 | break; 161 | } 162 | Ok(e) => info!("Received event {:?}", e), 163 | Err(e) => { 164 | error!("Failed to poll: {:?}", e); 165 | return; 166 | } 167 | } 168 | } 169 | 170 | let mut pings = 3; 171 | 172 | while pings > 0 { 173 | select! { 174 | _ = sleep(Duration::from_secs(4)) => { 175 | match client.ping().await { 176 | Ok(_) => { 177 | pings -= 1; 178 | info!("Pinged server"); 179 | }, 180 | Err(e) => { 181 | error!("Failed to ping: {:?}", e); 182 | return; 183 | } 184 | } 185 | }, 186 | header = client.poll_header() => { 187 | let h = match header { 188 | Ok(h) => h, 189 | Err(e) => { 190 | error!("Failed to poll header: {:?}", e); 191 | return; 192 | } 193 | }; 194 | info!("Received header {:?}", h.packet_type()); 195 | match client.poll_body(h).await { 196 | Ok(e) => info!("Received Event {:?}", e), 197 | Err(e) => { 198 | error!("Failed to poll body: {:?}", e); 199 | return; 200 | } 201 | } 202 | } 203 | }; 204 | } 205 | 206 | match client.unsubscribe(topic.into()).await { 207 | Ok(_) => info!("Sent Unsubscribe"), 208 | Err(e) => { 209 | error!("Failed to unsubscribe: {:?}", e); 210 | return; 211 | } 212 | }; 213 | 214 | match client.poll().await { 215 | Ok(e) => info!("Received Event {:?}", e), 216 | Err(e) => { 217 | error!("Failed to poll: {:?}", e); 218 | return; 219 | } 220 | } 221 | 222 | match client.poll().await { 223 | Ok(Event::Unsuback(Suback { 224 | packet_identifier: _, 225 | reason_code, 226 | })) => info!("Unsubscribed with reason code {:?}", reason_code), 227 | Ok(e) => { 228 | info!("Expected Unsuback but received event {:?}", e); 229 | return; 230 | } 231 | Err(e) => { 232 | error!("Failed to receive Unsuback {:?}", e); 233 | return; 234 | } 235 | } 236 | 237 | match client 238 | .disconnect(&DisconnectOptions { 239 | publish_will: false, 240 | session_expiry_interval: None, 241 | }) 242 | .await 243 | { 244 | Ok(_) => info!("Disconnected from server"), 245 | Err(e) => { 246 | error!("Failed to disconnect from server: {:?}", e); 247 | return; 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /tests/common/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{net::SocketAddr, sync::Mutex}; 2 | 3 | use log::{info, warn}; 4 | use rust_mqtt::{ 5 | Bytes, 6 | buffer::AllocBuffer, 7 | client::{ 8 | Client, MqttError, 9 | event::{Event, Publish, Suback}, 10 | options::{ConnectOptions, DisconnectOptions, PublicationOptions, SubscriptionOptions}, 11 | }, 12 | types::{IdentifiedQoS, MqttString, QoS, ReasonCode, TopicFilter, TopicName}, 13 | }; 14 | use tokio::net::TcpStream; 15 | 16 | use crate::common::{Tcp, TestClient, fmt::warn_inspect}; 17 | 18 | static UNIQUE_TOPIC_COUNTER: Mutex = Mutex::new(0); 19 | static ALLOC: StaticAlloc = StaticAlloc(AllocBuffer); 20 | 21 | fn unique_number() -> u64 { 22 | let mut counter = UNIQUE_TOPIC_COUNTER.lock().unwrap(); 23 | let number = *counter; 24 | *counter += 1; 25 | 26 | number 27 | } 28 | pub fn unique_topic() -> (TopicName<'static>, TopicFilter<'static>) { 29 | let s = format!("rust/mqtt/is-amazing/{}", unique_number()); 30 | let b = s.into_bytes().into_boxed_slice(); 31 | let s = MqttString::new(Bytes::Owned(b)).unwrap(); 32 | 33 | let n = unsafe { TopicName::new_unchecked(s) }; 34 | let f = n.clone().into(); 35 | 36 | (n, f) 37 | } 38 | 39 | struct StaticAlloc(AllocBuffer); 40 | impl StaticAlloc { 41 | fn get(&self) -> &'static mut AllocBuffer { 42 | let inner = &raw const self.0 as *mut AllocBuffer; 43 | unsafe { &mut *inner } 44 | } 45 | } 46 | 47 | pub async fn connected_client( 48 | broker: SocketAddr, 49 | options: &ConnectOptions<'static>, 50 | client_identifier: Option>, 51 | ) -> Result, MqttError<'static>> { 52 | let mut client = Client::new(ALLOC.get()); 53 | 54 | let tcp = warn_inspect!( 55 | TcpStream::connect(broker).await, 56 | "Error while connecting TCP session" 57 | ) 58 | .map(Tcp::new) 59 | .map_err(|_| MqttError::RecoveryRequired)?; 60 | 61 | warn_inspect!( 62 | client.connect(tcp, options, client_identifier).await, 63 | "Client::connect() failed" 64 | ) 65 | .map(|_| client) 66 | } 67 | 68 | pub async fn disconnect(client: &mut TestClient<'_>, options: &DisconnectOptions) { 69 | let _ = warn_inspect!( 70 | client.disconnect(options).await, 71 | "Client::disconnect() failed" 72 | ); 73 | } 74 | 75 | pub async fn subscribe<'c>( 76 | client: &mut TestClient<'c>, 77 | options: SubscriptionOptions, 78 | topic: TopicFilter<'_>, 79 | ) -> Result> { 80 | let pid = warn_inspect!( 81 | client.subscribe(topic, options).await, 82 | "Client::subscribe() failed" 83 | )?; 84 | 85 | loop { 86 | match warn_inspect!(client.poll().await, "Client::poll() failed")? { 87 | Event::Suback(Suback { 88 | packet_identifier, 89 | reason_code, 90 | }) if packet_identifier == pid => { 91 | info!("Subscribed with reason code {:?}", reason_code); 92 | break match reason_code { 93 | ReasonCode::Success => Ok(QoS::AtMostOnce), 94 | ReasonCode::GrantedQoS1 => Ok(QoS::AtLeastOnce), 95 | ReasonCode::GrantedQoS2 => Ok(QoS::ExactlyOnce), 96 | _ => unreachable!(), 97 | }; 98 | } 99 | Event::Suback(Suback { 100 | packet_identifier, 101 | reason_code: _, 102 | }) => warn!( 103 | "Expected SUBACK for packet identifier {}, but received SUBACK for packet identifier {}", 104 | pid, packet_identifier 105 | ), 106 | e => warn!("Expected Event::Suback, but received {:?}", e), 107 | } 108 | } 109 | } 110 | 111 | pub async fn unsubscribe<'c>( 112 | client: &mut TestClient<'c>, 113 | topic: TopicFilter<'_>, 114 | ) -> Result<(), MqttError<'c>> { 115 | let pid = warn_inspect!( 116 | client.unsubscribe(topic).await, 117 | "Client::unsubscribe() failed" 118 | )?; 119 | 120 | loop { 121 | match warn_inspect!(client.poll().await, "Client::poll() failed")? { 122 | Event::Unsuback(Suback { 123 | packet_identifier, 124 | reason_code, 125 | }) if packet_identifier == pid => { 126 | info!("Unsubscribed with reason code {:?}", reason_code); 127 | break Ok(()); 128 | } 129 | Event::Unsuback(Suback { 130 | packet_identifier, 131 | reason_code: _, 132 | }) => warn!( 133 | "Expected UNSUBACK for packet identifier {}, but received UNSUBACK for packet identifier {}", 134 | pid, packet_identifier 135 | ), 136 | e => warn!("Expected Event::Unsuback, but received {:?}", e), 137 | } 138 | } 139 | } 140 | 141 | pub async fn receive_publish<'c>( 142 | client: &mut TestClient<'c>, 143 | ) -> Result, MqttError<'c>> { 144 | loop { 145 | match warn_inspect!(client.poll().await, "Client::poll() failed")? { 146 | Event::Publish(p) => break Ok(p), 147 | e => warn!("Expected PUBLISH, but received {:?}", e), 148 | } 149 | } 150 | } 151 | 152 | pub async fn receive_and_complete<'c>( 153 | client: &mut TestClient<'c>, 154 | ) -> Result, MqttError<'c>> { 155 | let publish = receive_publish(client).await?; 156 | 157 | match publish.identified_qos { 158 | IdentifiedQoS::AtMostOnce => {} 159 | IdentifiedQoS::AtLeastOnce(_) => {} 160 | IdentifiedQoS::ExactlyOnce(pid) => loop { 161 | match warn_inspect!(client.poll().await, "Client::poll() failed")? { 162 | Event::PublishReleased(p) if p.packet_identifier == pid => { 163 | break; 164 | } 165 | Event::PublishReleased(p) => warn!( 166 | "Expected PUBREL with packet identifier {}, but received PUBREL with packet identifier {}", 167 | pid, p.packet_identifier 168 | ), 169 | e => warn!("Expected PUBLISH, but received {:?}", e), 170 | } 171 | }, 172 | } 173 | 174 | Ok(publish) 175 | } 176 | 177 | pub async fn publish_and_complete<'c>( 178 | client: &mut TestClient<'c>, 179 | options: &PublicationOptions<'_>, 180 | message: Bytes<'_>, 181 | ) -> Result> { 182 | let pid = warn_inspect!( 183 | client.publish(options, message).await, 184 | "Client::poll() failed" 185 | )?; 186 | 187 | match options.qos { 188 | QoS::AtMostOnce => {} 189 | QoS::AtLeastOnce => loop { 190 | match warn_inspect!(client.poll().await, "Client::poll() failed")? { 191 | Event::PublishAcknowledged(p) if p.packet_identifier == pid => break, 192 | Event::PublishAcknowledged(p) => warn!( 193 | "Expected PUBACK with packet identifier {}, but received PUBACK with packet identifier {}", 194 | pid, p.packet_identifier 195 | ), 196 | e => warn!("Expected PUBACK, but received {:?}", e), 197 | } 198 | }, 199 | QoS::ExactlyOnce => { 200 | loop { 201 | match warn_inspect!(client.poll().await, "Client::poll() failed")? { 202 | Event::PublishReceived(p) if p.packet_identifier == pid => break, 203 | Event::PublishReceived(p) => warn!( 204 | "Expected PUBREC with packet identifier {}, but received PUBREC with packet identifier {}", 205 | pid, p.packet_identifier 206 | ), 207 | e => warn!("Expected PUBREC, but received {:?}", e), 208 | } 209 | } 210 | loop { 211 | match warn_inspect!(client.poll().await, "Client::poll() failed")? { 212 | Event::PublishComplete(p) if p.packet_identifier == pid => break, 213 | Event::PublishComplete(p) => warn!( 214 | "Expected PUBCOMP with packet identifier {}, but received PUBCOMP with packet identifier {}", 215 | pid, p.packet_identifier 216 | ), 217 | e => warn!("Expected PUBCOMP, but received {:?}", e), 218 | } 219 | } 220 | } 221 | } 222 | 223 | Ok(pid) 224 | } 225 | -------------------------------------------------------------------------------- /src/types/reason_code.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | eio::{Read, Write}, 3 | io::{ 4 | err::{ReadError, WriteError}, 5 | read::Readable, 6 | write::Writable, 7 | }, 8 | }; 9 | 10 | /// A Reason Code is a variable byte integer encoded value that indicates the result of an operation. 11 | /// 12 | /// The CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, DISCONNECT and AUTH Control Packets have 13 | /// a single Reason Code as part of the Variable Header. The SUBACK and UNSUBACK packets 14 | /// contain a list of one or more Reason Codes in the Payload. 15 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 16 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 17 | pub enum ReasonCode { 18 | /// # aka GrantedQoS0, NormalDisconnection 19 | /// 20 | /// CONNACK, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBACK, UNSUBACK, AUTH, DISCONNECT 21 | Success, 22 | 23 | /// SUBACK 24 | GrantedQoS1, 25 | 26 | /// SUBACK 27 | GrantedQoS2, 28 | 29 | /// DISCONNECT 30 | DisconnectWithWillMessage, 31 | 32 | /// PUBACK, PUBREC 33 | NoMatchingSubscribers, 34 | 35 | /// UNSUBACK 36 | NoSubscriptionExisted, 37 | 38 | /// AUTH 39 | ContinueAuthentication, 40 | 41 | /// AUTH 42 | ReAuthenticate, 43 | 44 | #[default] 45 | /// CONNACK, PUBACK, PUBREC, SUBACK, UNSUBACK, DISCONNECT 46 | UnspecifiedError, 47 | 48 | /// CONNACK, DISCONNECT 49 | MalformedPacket, 50 | 51 | /// CONNACK, DISCONNECT 52 | ProtocolError, 53 | 54 | /// CONNACK, PUBACK, PUBREC, SUBACK, UNSUBACK, DISCONNECT 55 | ImplementationSpecificError, 56 | 57 | /// CONNACK 58 | UnsupportedProtocolVersion, 59 | 60 | /// CONNACK 61 | ClientIdentifierNotValid, 62 | 63 | /// CONNACK 64 | BadUserNameOrPassword, 65 | 66 | /// CONNACK, PUBACK, PUBREC, SUBACK, UNSUBACK, DISCONNECT 67 | NotAuthorized, 68 | 69 | /// CONNACK 70 | ServerUnavailable, 71 | 72 | /// CONNACK, DISCONNECT 73 | ServerBusy, 74 | 75 | /// CONNACK 76 | Banned, 77 | 78 | /// DISCONNECT 79 | ServerShuttingDown, 80 | 81 | /// CONNACK, DISCONNECT 82 | BadAuthenticationMethod, 83 | 84 | /// DISCONNECT 85 | KeepAliveTimeout, 86 | 87 | /// DISCONNECT 88 | SessionTakenOver, 89 | 90 | /// SUBACK, UNSUBACK, DISCONNECT 91 | TopicFilterInvalid, 92 | 93 | /// CONNACK, PUBACK, PUBREC, DISCONNECT 94 | TopicNameInvalid, 95 | 96 | /// PUBACK, PUBREC, SUBACK, UNSUBACK 97 | PacketIdentifierInUse, 98 | 99 | /// PUBREL, PUBCOMP 100 | PacketIdentifierNotFound, 101 | 102 | /// DISCONNECT 103 | ReceiveMaximumExceeded, 104 | 105 | /// DISCONNECT 106 | TopicAliasInvalid, 107 | 108 | /// CONNACK, DISCONNECT 109 | PacketTooLarge, 110 | 111 | /// DISCONNECT 112 | MessageRateTooHigh, 113 | 114 | /// CONNACK, PUBACK, PUBREC, SUBACK, DISCONNECT 115 | QuotaExceeded, 116 | 117 | /// DISCONNECT 118 | AdministrativeAction, 119 | 120 | /// CONNACK, PUBACK, PUBREC, DISCONNECT 121 | PayloadFormatInvalid, 122 | 123 | /// CONNACK, DISCONNECT 124 | RetainNotSupported, 125 | 126 | /// CONNACK, DISCONNECT 127 | QoSNotSupported, 128 | 129 | /// CONNACK, DISCONNECT 130 | UseAnotherServer, 131 | 132 | /// CONNACK, DISCONNECT 133 | ServerMoved, 134 | 135 | /// SUBACK, DISCONNECT 136 | SharedSubscriptionsNotSupported, 137 | 138 | /// CONNACK, DISCONNECT 139 | ConnectionRateExceeded, 140 | 141 | /// DISCONNECT 142 | MaximumConnectTime, 143 | 144 | /// SUBACK, DISCONNECT 145 | SubscriptionIdentifiersNotSupported, 146 | 147 | /// SUBACK, DISCONNECT 148 | WildcardSubscriptionsNotSupported, 149 | } 150 | 151 | impl ReasonCode { 152 | /// Returns the numeric value of the reason code. 153 | pub fn value(&self) -> u8 { 154 | match self { 155 | Self::Success => 0x00, 156 | Self::GrantedQoS1 => 0x01, 157 | Self::GrantedQoS2 => 0x02, 158 | Self::DisconnectWithWillMessage => 0x04, 159 | Self::NoMatchingSubscribers => 0x10, 160 | Self::NoSubscriptionExisted => 0x11, 161 | Self::ContinueAuthentication => 0x18, 162 | Self::ReAuthenticate => 0x19, 163 | Self::UnspecifiedError => 0x80, 164 | Self::MalformedPacket => 0x81, 165 | Self::ProtocolError => 0x82, 166 | Self::ImplementationSpecificError => 0x83, 167 | Self::UnsupportedProtocolVersion => 0x84, 168 | Self::ClientIdentifierNotValid => 0x85, 169 | Self::BadUserNameOrPassword => 0x86, 170 | Self::NotAuthorized => 0x87, 171 | Self::ServerUnavailable => 0x88, 172 | Self::ServerBusy => 0x89, 173 | Self::Banned => 0x8A, 174 | Self::ServerShuttingDown => 0x8B, 175 | Self::BadAuthenticationMethod => 0x8C, 176 | Self::KeepAliveTimeout => 0x8D, 177 | Self::SessionTakenOver => 0x8E, 178 | Self::TopicFilterInvalid => 0x8F, 179 | Self::TopicNameInvalid => 0x90, 180 | Self::PacketIdentifierInUse => 0x91, 181 | Self::PacketIdentifierNotFound => 0x92, 182 | Self::ReceiveMaximumExceeded => 0x93, 183 | Self::TopicAliasInvalid => 0x94, 184 | Self::PacketTooLarge => 0x95, 185 | Self::MessageRateTooHigh => 0x96, 186 | Self::QuotaExceeded => 0x97, 187 | Self::AdministrativeAction => 0x98, 188 | Self::PayloadFormatInvalid => 0x99, 189 | Self::RetainNotSupported => 0x9A, 190 | Self::QoSNotSupported => 0x9B, 191 | Self::UseAnotherServer => 0x9C, 192 | Self::ServerMoved => 0x9D, 193 | Self::SharedSubscriptionsNotSupported => 0x9E, 194 | Self::ConnectionRateExceeded => 0x9F, 195 | Self::MaximumConnectTime => 0xA0, 196 | Self::SubscriptionIdentifiersNotSupported => 0xA1, 197 | Self::WildcardSubscriptionsNotSupported => 0xA2, 198 | } 199 | } 200 | 201 | /// Returns whether the reason code is successful. 202 | /// This is the case if the reason code's numeric value is less than 0x80. 203 | pub fn is_success(&self) -> bool { 204 | self.value() < 0x80 205 | } 206 | 207 | /// Returns whether the reason code indicates an error. 208 | /// This is the case if the reason code's numeric value is greater than or equal to 0x80. 209 | pub fn is_erroneous(&self) -> bool { 210 | self.value() >= 0x80 211 | } 212 | } 213 | 214 | impl Readable for ReasonCode { 215 | async fn read(net: &mut R) -> Result> { 216 | let value = u8::read(net).await?; 217 | Ok(match value { 218 | 0x00 => Self::Success, // Note: This is ambiguous - context determines the specific variant 219 | 0x01 => Self::GrantedQoS1, 220 | 0x02 => Self::GrantedQoS2, 221 | 0x04 => Self::DisconnectWithWillMessage, 222 | 0x10 => Self::NoMatchingSubscribers, 223 | 0x11 => Self::NoSubscriptionExisted, 224 | 0x18 => Self::ContinueAuthentication, 225 | 0x19 => Self::ReAuthenticate, 226 | 0x80 => Self::UnspecifiedError, 227 | 0x81 => Self::MalformedPacket, 228 | 0x82 => Self::ProtocolError, 229 | 0x83 => Self::ImplementationSpecificError, 230 | 0x84 => Self::UnsupportedProtocolVersion, 231 | 0x85 => Self::ClientIdentifierNotValid, 232 | 0x86 => Self::BadUserNameOrPassword, 233 | 0x87 => Self::NotAuthorized, 234 | 0x88 => Self::ServerUnavailable, 235 | 0x89 => Self::ServerBusy, 236 | 0x8A => Self::Banned, 237 | 0x8B => Self::ServerShuttingDown, 238 | 0x8C => Self::BadAuthenticationMethod, 239 | 0x8D => Self::KeepAliveTimeout, 240 | 0x8E => Self::SessionTakenOver, 241 | 0x8F => Self::TopicFilterInvalid, 242 | 0x90 => Self::TopicNameInvalid, 243 | 0x91 => Self::PacketIdentifierInUse, 244 | 0x92 => Self::PacketIdentifierNotFound, 245 | 0x93 => Self::ReceiveMaximumExceeded, 246 | 0x94 => Self::TopicAliasInvalid, 247 | 0x95 => Self::PacketTooLarge, 248 | 0x96 => Self::MessageRateTooHigh, 249 | 0x97 => Self::QuotaExceeded, 250 | 0x98 => Self::AdministrativeAction, 251 | 0x99 => Self::PayloadFormatInvalid, 252 | 0x9A => Self::RetainNotSupported, 253 | 0x9B => Self::QoSNotSupported, 254 | 0x9C => Self::UseAnotherServer, 255 | 0x9D => Self::ServerMoved, 256 | 0x9E => Self::SharedSubscriptionsNotSupported, 257 | 0x9F => Self::ConnectionRateExceeded, 258 | 0xA0 => Self::MaximumConnectTime, 259 | 0xA1 => Self::SubscriptionIdentifiersNotSupported, 260 | 0xA2 => Self::WildcardSubscriptionsNotSupported, 261 | _ => return Err(ReadError::ProtocolError), 262 | }) 263 | } 264 | } 265 | 266 | impl Writable for ReasonCode { 267 | fn written_len(&self) -> usize { 268 | 1 269 | } 270 | 271 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 272 | self.value().write(write).await 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /tests/integration/sub_options.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use rust_mqtt::{ 4 | client::options::{PublicationOptions, RetainHandling}, 5 | types::{MqttString, QoS}, 6 | }; 7 | use tokio::{ 8 | net::TcpStream, 9 | time::{sleep, timeout}, 10 | }; 11 | use tokio_test::assert_err; 12 | 13 | use crate::common::{ 14 | BROKER_ADDRESS, DEFAULT_DC_OPTIONS, DEFAULT_QOS0_SUB_OPTIONS, NO_SESSION_CONNECT_OPTIONS, Tcp, 15 | assert::{assert_ok, assert_published, assert_recv, assert_subscribe}, 16 | utils::{connected_client, disconnect, unique_topic}, 17 | }; 18 | 19 | #[tokio::test] 20 | #[test_log::test] 21 | async fn publish_no_local() { 22 | let (topic_name, topic_filter) = unique_topic(); 23 | let msg = "Mosquitto bit me."; 24 | 25 | let mut c = 26 | assert_ok!(connected_client(BROKER_ADDRESS, NO_SESSION_CONNECT_OPTIONS, None).await); 27 | 28 | let mut options = DEFAULT_QOS0_SUB_OPTIONS; 29 | options.qos = QoS::ExactlyOnce; 30 | options.no_local = true; 31 | assert_subscribe!(c, options, topic_filter.clone()); 32 | 33 | let pub_options = PublicationOptions { 34 | retain: false, 35 | topic: topic_name.clone(), 36 | qos: QoS::ExactlyOnce, 37 | }; 38 | 39 | assert_published!(c, pub_options, msg.into()); 40 | 41 | assert_err!( 42 | timeout(Duration::from_secs(10), async { 43 | assert_recv!(c); 44 | }) 45 | .await, 46 | "Expected to receive nothing" 47 | ); 48 | 49 | disconnect(&mut c, DEFAULT_DC_OPTIONS).await; 50 | } 51 | 52 | #[tokio::test] 53 | #[test_log::test] 54 | async fn subscribe_retain_handling_default() { 55 | let (topic_name, topic_filter) = unique_topic(); 56 | let msg = "Retained message for AlwaysSend."; 57 | 58 | let mut tx = 59 | assert_ok!(connected_client(BROKER_ADDRESS, NO_SESSION_CONNECT_OPTIONS, None).await); 60 | 61 | // Receiver client with specific ID to test session behavior 62 | let rx_id = MqttString::new("RETAIN_HANDLING_DEFAULT_RECEIVER".into()).unwrap(); 63 | let mut rx = assert_ok!( 64 | connected_client( 65 | BROKER_ADDRESS, 66 | NO_SESSION_CONNECT_OPTIONS, 67 | Some(rx_id.clone()) 68 | ) 69 | .await 70 | ); 71 | 72 | let pub_options = PublicationOptions { 73 | retain: true, 74 | topic: topic_name.clone(), 75 | qos: QoS::AtLeastOnce, 76 | }; 77 | assert_published!(tx, pub_options, msg.into()); 78 | 79 | sleep(Duration::from_secs(1)).await; 80 | 81 | // Subscribe with RetainHandling::AlwaysSend (default) - should receive retained message 82 | let mut options = DEFAULT_QOS0_SUB_OPTIONS; 83 | options.qos = QoS::AtLeastOnce; 84 | options.retain_handling = RetainHandling::AlwaysSend; 85 | assert_subscribe!(rx, options, topic_filter.clone()); 86 | 87 | let publish = assert_recv!(rx); 88 | assert_eq!(&*publish.message, msg.as_bytes()); 89 | assert!(publish.retain); 90 | 91 | // Subscribe again - should receive retained message again with RetainHandling::AlwaysSend 92 | assert_subscribe!(rx, options, topic_filter.clone()); 93 | 94 | let publish = assert_recv!(rx); 95 | assert_eq!(&*publish.message, msg.as_bytes()); 96 | assert!(publish.retain); 97 | 98 | // Disconnect and reconnect - should receive retained message again 99 | disconnect(&mut rx, DEFAULT_DC_OPTIONS).await; 100 | sleep(Duration::from_secs(1)).await; 101 | assert_ok!( 102 | rx.connect( 103 | Tcp::new(assert_ok!(TcpStream::connect(BROKER_ADDRESS).await)), 104 | NO_SESSION_CONNECT_OPTIONS, 105 | Some(rx_id), 106 | ) 107 | .await 108 | ); 109 | assert_subscribe!(rx, options, topic_filter.clone()); 110 | 111 | let publish = assert_recv!(rx); 112 | assert_eq!(&*publish.message, msg.as_bytes()); 113 | assert!(publish.retain); 114 | 115 | disconnect(&mut rx, DEFAULT_DC_OPTIONS).await; 116 | disconnect(&mut tx, DEFAULT_DC_OPTIONS).await; 117 | } 118 | 119 | #[tokio::test] 120 | #[test_log::test] 121 | async fn subscribe_retain_handling_never() { 122 | let (topic_name, topic_filter) = unique_topic(); 123 | let msg = "Retained message for NeverSend."; 124 | 125 | let mut tx = 126 | assert_ok!(connected_client(BROKER_ADDRESS, NO_SESSION_CONNECT_OPTIONS, None).await); 127 | 128 | let rx_id = MqttString::new("RETAIN_HANDLING_NEVER_RECEIVER".into()).unwrap(); 129 | let mut rx = 130 | assert_ok!(connected_client(BROKER_ADDRESS, NO_SESSION_CONNECT_OPTIONS, Some(rx_id)).await); 131 | 132 | let pub_options = PublicationOptions { 133 | retain: true, 134 | topic: topic_name.clone(), 135 | qos: QoS::AtLeastOnce, 136 | }; 137 | assert_published!(tx, pub_options, msg.into()); 138 | 139 | sleep(Duration::from_secs(1)).await; 140 | 141 | // Subscribe with NeverSend - should NOT receive retained message 142 | let mut options = DEFAULT_QOS0_SUB_OPTIONS; 143 | options.qos = QoS::AtLeastOnce; 144 | options.retain_handling = RetainHandling::NeverSend; 145 | assert_subscribe!(rx, options, topic_filter.clone()); 146 | 147 | assert_err!( 148 | timeout(Duration::from_secs(5), async { 149 | assert_recv!(rx); 150 | }) 151 | .await, 152 | "Expected to receive nothing with NeverSend" 153 | ); 154 | 155 | // Subscribe again - should still NOT receive retained message 156 | assert_subscribe!(rx, options, topic_filter.clone()); 157 | 158 | assert_err!( 159 | timeout(Duration::from_secs(5), async { 160 | assert_recv!(rx); 161 | }) 162 | .await, 163 | "Expected to receive nothing on resubscribe with NeverSend" 164 | ); 165 | 166 | disconnect(&mut rx, DEFAULT_DC_OPTIONS).await; 167 | disconnect(&mut tx, DEFAULT_DC_OPTIONS).await; 168 | } 169 | 170 | #[tokio::test] 171 | #[test_log::test] 172 | async fn subscribe_retain_handling_clean_only() { 173 | let (topic_name, topic_filter) = unique_topic(); 174 | let msg = "Retained message for SendIfNotSubscribedBefore."; 175 | 176 | let mut tx = 177 | assert_ok!(connected_client(BROKER_ADDRESS, NO_SESSION_CONNECT_OPTIONS, None).await); 178 | 179 | let rx_id = MqttString::new("RETAIN_HANDLING_CLEAN_ONLY_RECEIVER".into()).unwrap(); 180 | let mut rx = 181 | assert_ok!(connected_client(BROKER_ADDRESS, NO_SESSION_CONNECT_OPTIONS, Some(rx_id)).await); 182 | 183 | let pub_options = PublicationOptions { 184 | retain: true, 185 | topic: topic_name.clone(), 186 | qos: QoS::AtLeastOnce, 187 | }; 188 | assert_published!(tx, pub_options, msg.into()); 189 | 190 | sleep(Duration::from_secs(1)).await; 191 | 192 | // Subscribe for the first time with RetainHandling::SendIfNotSubscribedBefore - should receive retained message 193 | let mut options = DEFAULT_QOS0_SUB_OPTIONS; 194 | options.qos = QoS::AtLeastOnce; 195 | options.retain_handling = RetainHandling::SendIfNotSubscribedBefore; 196 | assert_subscribe!(rx, options, topic_filter.clone()); 197 | 198 | let publish = assert_recv!(rx); 199 | assert_eq!(&*publish.message, msg.as_bytes()); 200 | assert!(publish.retain); 201 | 202 | // Subscribe again - should NOT receive retained message (already subscribed before) 203 | assert_subscribe!(rx, options, topic_filter.clone()); 204 | 205 | assert_err!( 206 | timeout(Duration::from_secs(5), async { 207 | assert_recv!(rx); 208 | }) 209 | .await, 210 | "Expected to receive nothing on resubscribe with RetainHandling::SendIfNotSubscribedBefore" 211 | ); 212 | 213 | disconnect(&mut rx, DEFAULT_DC_OPTIONS).await; 214 | disconnect(&mut tx, DEFAULT_DC_OPTIONS).await; 215 | } 216 | 217 | #[tokio::test] 218 | #[test_log::test] 219 | async fn subscribe_retain_as_published_false() { 220 | let (topic_name, topic_filter) = unique_topic(); 221 | let msg = "Retained message for SendIfNotSubscribedBefore."; 222 | 223 | let mut tx = 224 | assert_ok!(connected_client(BROKER_ADDRESS, NO_SESSION_CONNECT_OPTIONS, None).await); 225 | 226 | let mut rx = 227 | assert_ok!(connected_client(BROKER_ADDRESS, NO_SESSION_CONNECT_OPTIONS, None).await); 228 | 229 | let pub_options = PublicationOptions { 230 | retain: true, 231 | topic: topic_name.clone(), 232 | qos: QoS::AtLeastOnce, 233 | }; 234 | assert_published!(tx, pub_options.clone(), msg.into()); 235 | 236 | sleep(Duration::from_secs(1)).await; 237 | 238 | let options = DEFAULT_QOS0_SUB_OPTIONS; 239 | assert_subscribe!(rx, options, topic_filter.clone()); 240 | 241 | let publish = assert_recv!(rx); 242 | assert_eq!(&*publish.message, msg.as_bytes()); 243 | assert!( 244 | publish.retain, 245 | "Retain flag is always set to true if the message was retained and delivered because of new subscription" 246 | ); 247 | 248 | assert_published!(tx, pub_options, msg.into()); 249 | 250 | let publish = assert_recv!(rx); 251 | assert_eq!(&*publish.message, msg.as_bytes()); 252 | assert!(!publish.retain, "Retain flag should be set to false"); 253 | 254 | disconnect(&mut tx, DEFAULT_DC_OPTIONS).await; 255 | disconnect(&mut rx, DEFAULT_DC_OPTIONS).await; 256 | } 257 | 258 | #[tokio::test] 259 | #[test_log::test] 260 | async fn subscribe_retain_as_published_true() { 261 | let (topic_name, topic_filter) = unique_topic(); 262 | let msg = "Retained message for SendIfNotSubscribedBefore."; 263 | 264 | let mut tx = 265 | assert_ok!(connected_client(BROKER_ADDRESS, NO_SESSION_CONNECT_OPTIONS, None).await); 266 | 267 | let mut rx = 268 | assert_ok!(connected_client(BROKER_ADDRESS, NO_SESSION_CONNECT_OPTIONS, None).await); 269 | 270 | let pub_options = PublicationOptions { 271 | retain: true, 272 | topic: topic_name.clone(), 273 | qos: QoS::AtLeastOnce, 274 | }; 275 | assert_published!(tx, pub_options.clone(), msg.into()); 276 | 277 | sleep(Duration::from_secs(1)).await; 278 | 279 | let mut options = DEFAULT_QOS0_SUB_OPTIONS; 280 | options.retain_as_published = true; 281 | assert_subscribe!(rx, options, topic_filter.clone()); 282 | 283 | let publish = assert_recv!(rx); 284 | assert_eq!(&*publish.message, msg.as_bytes()); 285 | assert!( 286 | publish.retain, 287 | "Retain flag is always set to true if the message was retained and delivered because of new subscription" 288 | ); 289 | 290 | assert_published!(tx, pub_options, msg.into()); 291 | 292 | let publish = assert_recv!(rx); 293 | assert_eq!(&*publish.message, msg.as_bytes()); 294 | assert!(publish.retain, "Retain flag should be set to true"); 295 | 296 | disconnect(&mut tx, DEFAULT_DC_OPTIONS).await; 297 | disconnect(&mut rx, DEFAULT_DC_OPTIONS).await; 298 | } 299 | -------------------------------------------------------------------------------- /src/v5/property/values.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::{KeepAlive, MaximumPacketSize, ReceiveMaximum, SessionExpiryInterval}, 3 | eio::{Read, Write}, 4 | io::{ 5 | err::{ReadError, WriteError}, 6 | read::{Readable, Store}, 7 | write::{Writable, wlen}, 8 | }, 9 | types::{MqttBinary, MqttString, QoS, VarByteInt}, 10 | v5::property::{Property, PropertyType}, 11 | }; 12 | 13 | /// Implements a newtype with the given identifier and wrapped type. 14 | /// 15 | /// * Implements `Writable`: Identifier and content are written 16 | /// * Implements `Readable`: Only content is read. In the case of the newtype having a lifetime `'a`, the `Readable` implementation is trait bounded by `Store<'a>` 17 | macro_rules! property { 18 | ($name:ident, $ty:ty) => { 19 | #[derive(Debug, PartialEq, Clone, Copy)] 20 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 21 | pub struct $name(pub(crate) $ty); 22 | 23 | impl Property for $name { 24 | const TYPE: PropertyType = PropertyType::$name; 25 | type Inner = $ty; 26 | 27 | fn into_inner(self) -> Self::Inner { 28 | self.0 29 | } 30 | } 31 | 32 | impl Readable for $name { 33 | async fn read(read: &mut R) -> Result::Error>> { 34 | let content = <$ty as Readable>::read(read).await?; 35 | Ok(Self(content)) 36 | } 37 | } 38 | 39 | impl Writable for $name { 40 | fn written_len(&self) -> usize { 41 | Self::TYPE.written_len() + self.0.written_len() 42 | } 43 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 44 | Self::TYPE.write(write).await?; 45 | self.0.write(write).await?; 46 | 47 | Ok(()) 48 | } 49 | } 50 | 51 | impl From<$ty> for $name { 52 | fn from(value: $ty) -> Self { 53 | Self(value) 54 | } 55 | } 56 | }; 57 | ($name:ident < $lt:lifetime >, $ty:ty) => { 58 | #[derive(Debug, PartialEq, Clone)] 59 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 60 | pub struct $name<$lt>(pub(crate) $ty); 61 | 62 | impl<$lt> Property for $name<$lt> { 63 | const TYPE: PropertyType = PropertyType::$name; 64 | type Inner = $ty; 65 | 66 | fn into_inner(self) -> Self::Inner { 67 | self.0 68 | } 69 | } 70 | 71 | impl<$lt, R: Read + Store<$lt>> Readable for $name<$lt> { 72 | async fn read(read: &mut R) -> Result::Error>> { 73 | let content = <$ty as Readable>::read(read).await?; 74 | Ok(Self(content)) 75 | } 76 | } 77 | 78 | impl<$lt> Writable for $name<$lt> { 79 | fn written_len(&self) -> usize { 80 | Self::TYPE.written_len() + self.0.written_len() 81 | } 82 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 83 | Self::TYPE.write(write).await?; 84 | self.0.write(write).await?; 85 | 86 | Ok(()) 87 | } 88 | } 89 | 90 | impl<$lt> From<$ty> for $name<$lt> { 91 | fn from(value: $ty) -> Self { 92 | Self(value) 93 | } 94 | } 95 | }; 96 | } 97 | 98 | property!(PayloadFormatIndicator, bool); 99 | property!(MessageExpiryInterval, u32); 100 | property!(ContentType<'c>, MqttString<'c>); 101 | property!(ResponseTopic<'c>, MqttString<'c>); 102 | property!(CorrelationData<'c>, MqttBinary<'c>); 103 | property!(SubscriptionIdentifier, VarByteInt); 104 | property!(AssignedClientIdentifier<'c>, MqttString<'c>); 105 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 106 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 107 | pub struct ServerKeepAlive(pub(crate) KeepAlive); 108 | property!(AuthenticationMethod<'c>, MqttString<'c>); 109 | property!(AuthenticationData<'c>, MqttBinary<'c>); 110 | property!(RequestProblemInformation, bool); 111 | property!(WillDelayInterval, u32); 112 | property!(RequestResponseInformation, bool); 113 | property!(ResponseInformation<'c>, MqttString<'c>); 114 | property!(ServerReference<'c>, MqttString<'c>); 115 | property!(ReasonString<'c>, MqttString<'c>); 116 | property!(TopicAliasMaximum, u16); 117 | #[derive(Debug, Clone, Copy, PartialEq)] 118 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 119 | pub struct TopicAlias(pub(crate) u16); 120 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 121 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 122 | pub struct MaximumQoS(pub(crate) QoS); 123 | property!(RetainAvailable, bool); 124 | // Insert UserProperty here 125 | property!(WildcardSubscriptionAvailable, bool); 126 | property!(SubscriptionIdentifierAvailable, bool); 127 | property!(SharedSubscriptionAvailable, bool); 128 | 129 | impl Property for ServerKeepAlive { 130 | const TYPE: PropertyType = PropertyType::ServerKeepAlive; 131 | type Inner = KeepAlive; 132 | 133 | fn into_inner(self) -> Self::Inner { 134 | self.0 135 | } 136 | } 137 | 138 | impl Readable for ServerKeepAlive { 139 | async fn read(read: &mut R) -> Result::Error>> { 140 | let value = u16::read(read).await?; 141 | 142 | Ok(Self(match value { 143 | 0 => KeepAlive::Infinite, 144 | s => KeepAlive::Seconds(s), 145 | })) 146 | } 147 | } 148 | 149 | impl Writable for ServerKeepAlive { 150 | fn written_len(&self) -> usize { 151 | if matches!(self.0, KeepAlive::Infinite) { 152 | 0 153 | } else { 154 | Self::TYPE.written_len() + wlen!(u16) 155 | } 156 | } 157 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 158 | let value = match self.0 { 159 | KeepAlive::Infinite => 0, 160 | KeepAlive::Seconds(s) => s, 161 | }; 162 | 163 | if value != 0 { 164 | Self::TYPE.write(write).await?; 165 | value.write(write).await?; 166 | } 167 | Ok(()) 168 | } 169 | } 170 | 171 | impl Property for SessionExpiryInterval { 172 | const TYPE: PropertyType = PropertyType::SessionExpiryInterval; 173 | type Inner = Self; 174 | 175 | fn into_inner(self) -> Self::Inner { 176 | self 177 | } 178 | } 179 | 180 | impl Readable for SessionExpiryInterval { 181 | async fn read(read: &mut R) -> Result::Error>> { 182 | let value = u32::read(read).await?; 183 | 184 | Ok(match value { 185 | 0 => Self::EndOnDisconnect, 186 | u32::MAX => Self::NeverEnd, 187 | s => Self::Seconds(s), 188 | }) 189 | } 190 | } 191 | 192 | impl Writable for SessionExpiryInterval { 193 | fn written_len(&self) -> usize { 194 | if matches!(self, Self::EndOnDisconnect) { 195 | 0 196 | } else { 197 | Self::TYPE.written_len() + wlen!(u32) 198 | } 199 | } 200 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 201 | let value = match self { 202 | Self::EndOnDisconnect => 0, 203 | Self::NeverEnd => u32::MAX, 204 | Self::Seconds(s) => *s, 205 | }; 206 | 207 | if value != 0 { 208 | Self::TYPE.write(write).await?; 209 | value.write(write).await?; 210 | } 211 | Ok(()) 212 | } 213 | } 214 | 215 | impl Property for MaximumQoS { 216 | const TYPE: PropertyType = PropertyType::MaximumQoS; 217 | type Inner = QoS; 218 | 219 | fn into_inner(self) -> Self::Inner { 220 | self.0 221 | } 222 | } 223 | impl Readable for MaximumQoS { 224 | async fn read(read: &mut R) -> Result::Error>> { 225 | let byte = u8::read(read).await?; 226 | let qos = QoS::try_from_bits(byte).map_err(|_| ReadError::MalformedPacket)?; 227 | Ok(Self(qos)) 228 | } 229 | } 230 | 231 | impl Property for TopicAlias { 232 | const TYPE: PropertyType = PropertyType::TopicAlias; 233 | type Inner = u16; 234 | 235 | fn into_inner(self) -> Self::Inner { 236 | self.0 237 | } 238 | } 239 | impl Readable for TopicAlias { 240 | async fn read(read: &mut R) -> Result::Error>> { 241 | let topic_alias = u16::read(read).await?; 242 | if topic_alias == 0 { 243 | Err(ReadError::ProtocolError) 244 | } else { 245 | Ok(Self(topic_alias)) 246 | } 247 | } 248 | } 249 | impl Writable for TopicAlias { 250 | fn written_len(&self) -> usize { 251 | Self::TYPE.written_len() + wlen!(u16) 252 | } 253 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 254 | Self::TYPE.write(write).await?; 255 | self.0.write(write).await?; 256 | Ok(()) 257 | } 258 | } 259 | 260 | impl Property for MaximumPacketSize { 261 | const TYPE: PropertyType = PropertyType::MaximumPacketSize; 262 | type Inner = Self; 263 | 264 | fn into_inner(self) -> Self::Inner { 265 | self 266 | } 267 | } 268 | impl Readable for MaximumPacketSize { 269 | async fn read(read: &mut R) -> Result::Error>> { 270 | let max = u32::read(read).await?; 271 | 272 | if max > 0 { 273 | Ok(Self::Limit(max)) 274 | } else { 275 | Err(ReadError::ProtocolError) 276 | } 277 | } 278 | } 279 | impl Writable for MaximumPacketSize { 280 | fn written_len(&self) -> usize { 281 | match self { 282 | Self::Unlimited => 0, 283 | Self::Limit(_) => Self::TYPE.written_len() + wlen!(u32), 284 | } 285 | } 286 | 287 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 288 | if let Self::Limit(l) = self { 289 | Self::TYPE.write(write).await?; 290 | l.write(write).await?; 291 | } 292 | 293 | Ok(()) 294 | } 295 | } 296 | 297 | impl Property for ReceiveMaximum { 298 | const TYPE: PropertyType = PropertyType::ReceiveMaximum; 299 | type Inner = u16; 300 | 301 | fn into_inner(self) -> Self::Inner { 302 | self.0 303 | } 304 | } 305 | impl Readable for ReceiveMaximum { 306 | async fn read(read: &mut R) -> Result::Error>> { 307 | let max = u16::read(read).await?; 308 | 309 | if max > 0 { 310 | Ok(Self(max)) 311 | } else { 312 | Err(ReadError::ProtocolError) 313 | } 314 | } 315 | } 316 | impl Writable for ReceiveMaximum { 317 | fn written_len(&self) -> usize { 318 | match self.0 { 319 | u16::MAX => 0, 320 | _ => Self::TYPE.written_len() + wlen!(u16), 321 | } 322 | } 323 | 324 | async fn write(&self, write: &mut W) -> Result<(), WriteError> { 325 | if self.0 < u16::MAX { 326 | Self::TYPE.write(write).await?; 327 | self.0.write(write).await?; 328 | } 329 | 330 | Ok(()) 331 | } 332 | } 333 | --------------------------------------------------------------------------------