Rename project to quicnprotochat
This commit is contained in:
15
crates/quicnprotochat-proto/Cargo.toml
Normal file
15
crates/quicnprotochat-proto/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "quicnprotochat-proto"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Cap'n Proto schemas, generated types, and serialisation helpers for quicnprotochat. No crypto, no I/O."
|
||||
license = "MIT"
|
||||
|
||||
# build.rs invokes capnpc to generate Rust source from .capnp schemas.
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
capnp = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
capnpc = { workspace = true }
|
||||
59
crates/quicnprotochat-proto/build.rs
Normal file
59
crates/quicnprotochat-proto/build.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
//! Build script for quicnprotochat-proto.
|
||||
//!
|
||||
//! Invokes the `capnp` compiler to generate Rust types from `.capnp` schemas
|
||||
//! located in the workspace-root `schemas/` directory.
|
||||
//!
|
||||
//! # Prerequisites
|
||||
//!
|
||||
//! The `capnp` CLI must be installed and on `PATH`.
|
||||
//!
|
||||
//! Debian/Ubuntu: apt-get install capnproto
|
||||
//! macOS: brew install capnp
|
||||
//! Docker: see docker/Dockerfile
|
||||
|
||||
use std::{env, path::PathBuf};
|
||||
|
||||
fn main() {
|
||||
let manifest_dir =
|
||||
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set by Cargo"));
|
||||
|
||||
// Workspace root is two levels above this crate (quicnprotochat/crates/quicnprotochat-proto).
|
||||
let workspace_root = manifest_dir
|
||||
.join("../..")
|
||||
.canonicalize()
|
||||
.expect("could not canonicalize workspace root path");
|
||||
|
||||
let schemas_dir = workspace_root.join("schemas");
|
||||
|
||||
// Re-run this build script whenever any schema file changes.
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
schemas_dir.join("envelope.capnp").display()
|
||||
);
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
schemas_dir.join("auth.capnp").display()
|
||||
);
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
schemas_dir.join("delivery.capnp").display()
|
||||
);
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
schemas_dir.join("node.capnp").display()
|
||||
);
|
||||
|
||||
capnpc::CompilerCommand::new()
|
||||
// Treat `schemas/` as the include root so that inter-schema imports
|
||||
// resolve correctly.
|
||||
.src_prefix(&schemas_dir)
|
||||
.file(schemas_dir.join("envelope.capnp"))
|
||||
.file(schemas_dir.join("auth.capnp"))
|
||||
.file(schemas_dir.join("delivery.capnp"))
|
||||
.file(schemas_dir.join("node.capnp"))
|
||||
.run()
|
||||
.expect(
|
||||
"Cap'n Proto schema compilation failed. \
|
||||
Is `capnp` installed? (apt-get install capnproto / brew install capnp)",
|
||||
);
|
||||
}
|
||||
218
crates/quicnprotochat-proto/src/lib.rs
Normal file
218
crates/quicnprotochat-proto/src/lib.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
//! Cap'n Proto schemas, generated types, and serialisation helpers for quicnprotochat.
|
||||
//!
|
||||
//! # Design constraints
|
||||
//!
|
||||
//! This crate is intentionally restricted:
|
||||
//! - **No crypto** — key material never enters this crate.
|
||||
//! - **No I/O** — callers own transport; this crate only converts bytes ↔ types.
|
||||
//! - **No async** — pure synchronous data-layer code.
|
||||
//!
|
||||
//! # Generated code
|
||||
//!
|
||||
//! `build.rs` invokes `capnpc` at compile time and writes generated Rust source
|
||||
//! into `$OUT_DIR`. The `include!` macros below splice that code in as a module.
|
||||
//!
|
||||
//! # Canonical serialisation (M2+)
|
||||
//!
|
||||
//! `build_envelope` uses standard Cap'n Proto wire format. Canonical serialisation
|
||||
//! (deterministic byte representation for cryptographic signing of KeyPackages and
|
||||
//! Commits) is added in M2 once the Authentication Service is introduced.
|
||||
|
||||
// ── Generated types ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Cap'n Proto generated types for `schemas/envelope.capnp`.
|
||||
///
|
||||
/// Do not edit this module by hand — it is entirely machine-generated.
|
||||
pub mod envelope_capnp {
|
||||
include!(concat!(env!("OUT_DIR"), "/envelope_capnp.rs"));
|
||||
}
|
||||
|
||||
/// Cap'n Proto generated types for `schemas/auth.capnp`.
|
||||
///
|
||||
/// Do not edit this module by hand — it is entirely machine-generated.
|
||||
pub mod auth_capnp {
|
||||
include!(concat!(env!("OUT_DIR"), "/auth_capnp.rs"));
|
||||
}
|
||||
|
||||
/// Cap'n Proto generated types for `schemas/delivery.capnp`.
|
||||
///
|
||||
/// Do not edit this module by hand — it is entirely machine-generated.
|
||||
pub mod delivery_capnp {
|
||||
include!(concat!(env!("OUT_DIR"), "/delivery_capnp.rs"));
|
||||
}
|
||||
|
||||
/// Cap'n Proto generated types for `schemas/node.capnp`.
|
||||
///
|
||||
/// Do not edit this module by hand — it is entirely machine-generated.
|
||||
pub mod node_capnp {
|
||||
include!(concat!(env!("OUT_DIR"), "/node_capnp.rs"));
|
||||
}
|
||||
|
||||
// ── Re-exports ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// The message-type discriminant from the `Envelope` schema.
|
||||
///
|
||||
/// Re-exported here so callers can `use quicnprotochat_proto::MsgType` without
|
||||
/// spelling out the full generated module path.
|
||||
pub use envelope_capnp::envelope::MsgType;
|
||||
|
||||
// ── Owned envelope type ───────────────────────────────────────────────────────
|
||||
|
||||
/// An owned, decoded `Envelope` with no Cap'n Proto reader lifetimes.
|
||||
///
|
||||
/// All byte fields are eagerly copied out of the Cap'n Proto reader so that
|
||||
/// this type is `Send + 'static` and can cross async task boundaries freely.
|
||||
///
|
||||
/// # Invariants
|
||||
///
|
||||
/// - `group_id` and `sender_id` are either empty (for control messages such as
|
||||
/// `Ping`/`Pong`) or exactly 32 bytes (SHA-256 digest).
|
||||
/// - `payload` is empty for `Ping` and `Pong`; non-empty for all MLS variants.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedEnvelope {
|
||||
pub msg_type: MsgType,
|
||||
/// SHA-256 of the group name, or empty for point-to-point control messages.
|
||||
pub group_id: Vec<u8>,
|
||||
/// SHA-256 of the sender's Ed25519 identity public key, or empty.
|
||||
pub sender_id: Vec<u8>,
|
||||
/// Opaque payload — interpretation is determined by `msg_type`.
|
||||
pub payload: Vec<u8>,
|
||||
/// Unix timestamp in milliseconds.
|
||||
pub timestamp_ms: u64,
|
||||
}
|
||||
|
||||
// ── Serialisation helpers ─────────────────────────────────────────────────────
|
||||
|
||||
/// Serialise a [`ParsedEnvelope`] to unpacked Cap'n Proto wire bytes.
|
||||
///
|
||||
/// The returned bytes include the Cap'n Proto segment table header followed by
|
||||
/// the message data. They are suitable for use as the body of a length-prefixed
|
||||
/// quicnprotochat frame (the frame codec in `quicnprotochat-core` prepends the 4-byte length).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`capnp::Error`] if the underlying allocator fails (out of memory).
|
||||
/// This is not expected under normal operation.
|
||||
pub fn build_envelope(env: &ParsedEnvelope) -> Result<Vec<u8>, capnp::Error> {
|
||||
use capnp::message;
|
||||
|
||||
let mut message = message::Builder::new_default();
|
||||
{
|
||||
let mut root = message.init_root::<envelope_capnp::envelope::Builder>();
|
||||
root.set_msg_type(env.msg_type);
|
||||
root.set_group_id(&env.group_id);
|
||||
root.set_sender_id(&env.sender_id);
|
||||
root.set_payload(&env.payload);
|
||||
root.set_timestamp_ms(env.timestamp_ms);
|
||||
}
|
||||
to_bytes(&message)
|
||||
}
|
||||
|
||||
/// Deserialise unpacked Cap'n Proto wire bytes into a [`ParsedEnvelope`].
|
||||
///
|
||||
/// All data is copied out of the Cap'n Proto reader before returning, so the
|
||||
/// input slice is not retained.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`capnp::Error`] if the bytes are not valid Cap'n Proto wire format.
|
||||
/// - [`capnp::Error`] if `msgType` contains a discriminant not present in the
|
||||
/// current schema (forward-compatibility guard).
|
||||
pub fn parse_envelope(bytes: &[u8]) -> Result<ParsedEnvelope, capnp::Error> {
|
||||
let reader = from_bytes(bytes)?;
|
||||
let root = reader.get_root::<envelope_capnp::envelope::Reader>()?;
|
||||
|
||||
let msg_type = root.get_msg_type().map_err(|nis| {
|
||||
capnp::Error::failed(format!(
|
||||
"Envelope.msgType contains unknown discriminant: {nis}"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(ParsedEnvelope {
|
||||
msg_type,
|
||||
group_id: root.get_group_id()?.to_vec(),
|
||||
sender_id: root.get_sender_id()?.to_vec(),
|
||||
payload: root.get_payload()?.to_vec(),
|
||||
timestamp_ms: root.get_timestamp_ms(),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Low-level byte ↔ message conversions ──────────────────────────────────────
|
||||
|
||||
/// Serialise a Cap'n Proto message builder to unpacked wire bytes.
|
||||
///
|
||||
/// The output includes the segment table header. For transport, the
|
||||
/// `quicnprotochat-core` frame codec prepends a 4-byte little-endian length field.
|
||||
pub fn to_bytes<A: capnp::message::Allocator>(
|
||||
msg: &capnp::message::Builder<A>,
|
||||
) -> Result<Vec<u8>, capnp::Error> {
|
||||
let mut buf = Vec::new();
|
||||
capnp::serialize::write_message(&mut buf, msg)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Deserialise unpacked wire bytes into a message with owned segments.
|
||||
///
|
||||
/// Uses `ReaderOptions::new()` (default limits: 64 MiB, 512 nesting levels).
|
||||
/// Callers that receive data from untrusted peers should consider tightening
|
||||
/// the traversal limit via `ReaderOptions::traversal_limit_in_words`.
|
||||
pub fn from_bytes(
|
||||
bytes: &[u8],
|
||||
) -> Result<capnp::message::Reader<capnp::serialize::OwnedSegments>, capnp::Error> {
|
||||
let mut cursor = std::io::Cursor::new(bytes);
|
||||
capnp::serialize::read_message(&mut cursor, capnp::message::ReaderOptions::new())
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Round-trip a Ping envelope through build → parse and verify all fields.
|
||||
#[test]
|
||||
fn ping_round_trip() {
|
||||
let original = ParsedEnvelope {
|
||||
msg_type: MsgType::Ping,
|
||||
group_id: vec![],
|
||||
sender_id: vec![0xAB; 32],
|
||||
payload: vec![],
|
||||
timestamp_ms: 1_700_000_000_000,
|
||||
};
|
||||
|
||||
let bytes = build_envelope(&original).expect("build_envelope failed");
|
||||
let parsed = parse_envelope(&bytes).expect("parse_envelope failed");
|
||||
|
||||
assert!(matches!(parsed.msg_type, MsgType::Ping));
|
||||
assert_eq!(parsed.group_id, original.group_id);
|
||||
assert_eq!(parsed.sender_id, original.sender_id);
|
||||
assert_eq!(parsed.payload, original.payload);
|
||||
assert_eq!(parsed.timestamp_ms, original.timestamp_ms);
|
||||
}
|
||||
|
||||
/// Round-trip a Pong envelope.
|
||||
#[test]
|
||||
fn pong_round_trip() {
|
||||
let original = ParsedEnvelope {
|
||||
msg_type: MsgType::Pong,
|
||||
group_id: vec![],
|
||||
sender_id: vec![0xCD; 32],
|
||||
payload: vec![],
|
||||
timestamp_ms: 1_700_000_001_000,
|
||||
};
|
||||
|
||||
let bytes = build_envelope(&original).expect("build_envelope failed");
|
||||
let parsed = parse_envelope(&bytes).expect("parse_envelope failed");
|
||||
|
||||
assert!(matches!(parsed.msg_type, MsgType::Pong));
|
||||
assert_eq!(parsed.sender_id, original.sender_id);
|
||||
assert_eq!(parsed.timestamp_ms, original.timestamp_ms);
|
||||
}
|
||||
|
||||
/// Corrupted bytes must produce an error, not a panic.
|
||||
#[test]
|
||||
fn corrupted_bytes_error() {
|
||||
let result = parse_envelope(&[0xFF, 0xFF, 0xFF, 0xFF]);
|
||||
assert!(result.is_err(), "expected error for corrupted input");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user