Skip to main content

offline_protocol_core/
lib.rs

1//! Core types and data structures for the Offline Protocol SDK.
2//!
3//! This crate provides the fundamental types used throughout the SDK:
4//! - Message types and identifiers
5//! - Protocol types (UserId, AppId, TTL, etc.)
6//! - Error types
7//!
8//! All types in this crate are 100% safe Rust with no unsafe blocks.
9//!
10//! # Building without `std`
11//!
12//! This crate compiles for bare-metal targets with `--no-default-features`,
13//! which is how a constrained leaf node (a Cortex-M lock or sensor that speaks
14//! the protocol but cannot host the engine) links it. `alloc` is required in
15//! both configurations.
16//!
17//! What the `std` feature adds is everything that mints state from the
18//! platform, because a bare-metal target supplies none of it: a clock
19//! ([`Timestamp::now`], [`WallClockTimestamp::now`], [`LocalInstant`]), an
20//! entropy source ([`MessageId::new`], and so [`Message::new`] and
21//! [`Message::builder`] with it), and threads (the [`sync`] module's
22//! poison-recovering lock helpers). Everything that parses, validates,
23//! re-encodes or compares is present in both configurations, which is the half
24//! a leaf node actually needs: it receives a frame someone else minted.
25//!
26//! Construct messages from wire-supplied parts on the no_std path, via
27//! [`MessageId::from_bytes`], [`Timestamp::from_millis`] and the struct
28//! literal, rather than the `now()`/`new()` constructors.
29
30#![cfg_attr(not(feature = "std"), no_std)]
31#![deny(unsafe_code)]
32#![warn(missing_docs)]
33
34extern crate alloc;
35
36pub mod address;
37pub mod error;
38pub mod message;
39pub mod service;
40#[cfg(feature = "std")]
41pub mod sync;
42pub mod types;
43pub mod username;
44pub mod wire;
45
46pub use address::{Address, AddressError};
47pub use error::{Error, Result};
48pub use message::{
49    ContentType, ForwardInfo, MediaMetadata, Message, MessageId, MessagePriority, ReplyContext,
50    WireCodec,
51};
52pub use service::{ServiceDescriptor, ServiceId};
53#[cfg(feature = "std")]
54pub use sync::{MutexExt, RwLockExt};
55#[cfg(feature = "std")]
56pub use types::LocalInstant;
57pub use types::{
58    validate_id_chars, AppId, HopCount, IdValidationError, LamportClock, MetadataMap, Timestamp,
59    UserId, WallClockTimestamp, MAX_ID_LEN, TTL,
60};
61pub use username::{contains_control_or_format, Username, UsernameError};
62pub use wire::{WIRE_V1_MAGIC, WIRE_VERSION_V1};
63
64#[cfg(all(test, feature = "std"))]
65mod manifest_guard_tests {
66    /// Dependencies this crate declares locally instead of inheriting from the
67    /// workspace table, and which must therefore be kept in lockstep by hand.
68    /// See the comment above `[dependencies]` in this crate's `Cargo.toml` for
69    /// why inheritance is not an option here.
70    const LOCAL_DEPS: &[&str] = &[
71        "serde",
72        "serde_json",
73        "base64",
74        "unicode-normalization",
75        "thiserror",
76        "uuid",
77        "tracing",
78    ];
79
80    /// Extracts the version requirement a manifest states for `name`.
81    ///
82    /// Handles both spellings cargo accepts: `name = "1.0"` and
83    /// `name = { version = "1.0", ... }`.
84    fn version_req(manifest: &str, name: &str) -> Option<String> {
85        let line = manifest
86            .lines()
87            .find(|l| l.starts_with(&format!("{name} = ")))?;
88        let rest = match line.find("version = \"") {
89            Some(i) => &line[i + "version = \"".len()..],
90            None => {
91                let i = line.find('"')?;
92                &line[i + 1..]
93            }
94        };
95        rest.find('"').map(|end| rest[..end].to_string())
96    }
97
98    /// The local dependency entries must not drift from the workspace table.
99    ///
100    /// Dropping inheritance is what makes `default-features = false` take
101    /// effect (cargo ignores it on an inherited dependency), but it also means
102    /// a workspace-wide version bump silently stops applying to this crate.
103    /// This test is what turns that silence into a failure.
104    #[test]
105    fn local_dep_versions_match_the_workspace_table() {
106        let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
107        let root = dir.join("../../Cargo.toml");
108        // A published .crate archive contains no workspace root. Skip rather
109        // than panic: this guard protects the repo, and a packaged build that
110        // panics here fails the release for a condition it cannot observe.
111        let Ok(root_manifest) = std::fs::read_to_string(&root) else {
112            return;
113        };
114        let own_manifest =
115            std::fs::read_to_string(dir.join("Cargo.toml")).expect("own manifest is readable");
116
117        for dep in LOCAL_DEPS {
118            let workspace = version_req(&root_manifest, dep)
119                .unwrap_or_else(|| panic!("`{dep}` not found in the workspace dependency table"));
120            let own = version_req(&own_manifest, dep)
121                .unwrap_or_else(|| panic!("`{dep}` not found in this crate's manifest"));
122            assert_eq!(
123                workspace, own,
124                "`{dep}` version drifted: workspace says {workspace:?}, \
125                 offline-protocol-core says {own:?}. Update both together."
126            );
127        }
128    }
129}