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}