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;
42#[cfg(feature = "std")]
43pub mod time;
44pub mod types;
45pub mod username;
46pub mod wire;
47
48pub use address::{Address, AddressError};
49pub use error::{Error, Result};
50pub use message::{
51    ContentType, ForwardInfo, MediaMetadata, Message, MessageId, MessagePriority, ReplyContext,
52    WireCodec,
53};
54pub use service::{ServiceDescriptor, ServiceId};
55#[cfg(feature = "std")]
56pub use sync::{MutexExt, RwLockExt};
57#[cfg(feature = "std")]
58pub use types::LocalInstant;
59pub use types::{
60    validate_id_chars, AppId, HopCount, IdValidationError, LamportClock, MetadataMap, Timestamp,
61    UserId, WallClockTimestamp, MAX_ID_LEN, TTL,
62};
63pub use username::{contains_control_or_format, Username, UsernameError};
64pub use wire::{WIRE_V1_MAGIC, WIRE_VERSION_V1};
65
66#[cfg(all(test, feature = "std"))]
67mod manifest_guard_tests {
68    /// Dependencies this crate declares locally instead of inheriting from the
69    /// workspace table, and which must therefore be kept in lockstep by hand.
70    /// See the comment above `[dependencies]` in this crate's `Cargo.toml` for
71    /// why inheritance is not an option here.
72    const LOCAL_DEPS: &[&str] = &[
73        "serde",
74        "serde_json",
75        "base64",
76        "unicode-normalization",
77        "thiserror",
78        "uuid",
79        "tracing",
80        "web-time",
81    ];
82
83    /// Extracts the version requirement a manifest states for `name`.
84    ///
85    /// Handles both spellings cargo accepts: `name = "1.0"` and
86    /// `name = { version = "1.0", ... }`.
87    fn version_req(manifest: &str, name: &str) -> Option<String> {
88        let line = manifest
89            .lines()
90            .find(|l| l.starts_with(&format!("{name} = ")))?;
91        let rest = match line.find("version = \"") {
92            Some(i) => &line[i + "version = \"".len()..],
93            None => {
94                let i = line.find('"')?;
95                &line[i + 1..]
96            }
97        };
98        rest.find('"').map(|end| rest[..end].to_string())
99    }
100
101    /// The local dependency entries must not drift from the workspace table.
102    ///
103    /// Dropping inheritance is what makes `default-features = false` take
104    /// effect (cargo ignores it on an inherited dependency), but it also means
105    /// a workspace-wide version bump silently stops applying to this crate.
106    /// This test is what turns that silence into a failure.
107    #[test]
108    fn local_dep_versions_match_the_workspace_table() {
109        let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
110        let root = dir.join("../../Cargo.toml");
111        // A published .crate archive contains no workspace root. Skip rather
112        // than panic: this guard protects the repo, and a packaged build that
113        // panics here fails the release for a condition it cannot observe.
114        let Ok(root_manifest) = std::fs::read_to_string(&root) else {
115            return;
116        };
117        let own_manifest =
118            std::fs::read_to_string(dir.join("Cargo.toml")).expect("own manifest is readable");
119
120        for dep in LOCAL_DEPS {
121            let workspace = version_req(&root_manifest, dep)
122                .unwrap_or_else(|| panic!("`{dep}` not found in the workspace dependency table"));
123            let own = version_req(&own_manifest, dep)
124                .unwrap_or_else(|| panic!("`{dep}` not found in this crate's manifest"));
125            assert_eq!(
126                workspace, own,
127                "`{dep}` version drifted: workspace says {workspace:?}, \
128                 offline-protocol-core says {own:?}. Update both together."
129            );
130        }
131    }
132
133    /// The `version` of the `[workspace.package]` table.
134    fn workspace_version(root_manifest: &str) -> Option<String> {
135        let table = root_manifest.split("[workspace.package]").nth(1)?;
136        let table = table.split("\n[").next()?;
137        let line = table.lines().find(|l| l.starts_with("version = \""))?;
138        let rest = &line["version = \"".len()..];
139        rest.find('"').map(|end| rest[..end].to_string())
140    }
141
142    /// A crate that cannot inherit an internal dependency (the dual std/no_std
143    /// crates, and the browser crate, all for the `default-features` reason
144    /// above) names the sibling's version by hand. A release cut bumps the
145    /// workspace version, and a missed hand-written pin fails the cut at its
146    /// first cargo command, after the tag is planned. This turns that into a
147    /// test failure on the bump commit itself.
148    #[test]
149    fn local_internal_pins_name_the_workspace_version() {
150        let crates = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
151        let Ok(root_manifest) = std::fs::read_to_string(crates.join("../Cargo.toml")) else {
152            return;
153        };
154        // Normalised so a Windows checkout's CRLF cannot hide a line.
155        let root_manifest = root_manifest.replace("\r\n", "\n");
156        let version = workspace_version(&root_manifest).expect("[workspace.package] version");
157        let pinned = format!("version = \"{version}\"");
158
159        let mut checked = 0;
160        let mut drifted = Vec::new();
161        for entry in std::fs::read_dir(&crates)
162            .expect("crates directory")
163            .flatten()
164        {
165            let manifest = entry.path().join("Cargo.toml");
166            let Ok(text) = std::fs::read_to_string(&manifest) else {
167                continue;
168            };
169            for line in text.lines() {
170                let internal = line.starts_with("offline-protocol")
171                    && line.contains("path = \"../")
172                    && line.contains("version = \"");
173                if !internal {
174                    continue;
175                }
176                checked += 1;
177                if !line.contains(&pinned) {
178                    drifted.push(format!("{}: {}", manifest.display(), line.trim()));
179                }
180            }
181        }
182        assert!(
183            checked > 0,
184            "found no hand-pinned internal dependency to check"
185        );
186        assert!(
187            drifted.is_empty(),
188            "these internal dependencies must name the workspace version {version}:\n{}",
189            drifted.join("\n")
190        );
191    }
192}