Chat file attachments, stage 1: protocol + data model + pure seams
First slice of in-chat file/photo sharing (dedicated file plane, images inline + file chips, session-only). This stage adds the wire types and the pure, unit-tested logic; no transport or UI yet. - protocol: new FILES_ALPN / FILES_PROTO (peerspeak/files/1) for the dedicated file-transfer plane. Bump GOSSIP_PROTO 1->2 + sig domain v2 (Chat gained an attachment field, so cross-version peers fail fast rather than half-work) and Cargo 0.2.0 -> 0.3.0 per VERSIONING.md. BREAKING wire change: all peers must run >= 0.3.0. - new src/files.rs: ChatAttachment descriptor (name/size/kind/id; bytes travel off-gossip), AttachmentKind, plus pure seams — sanitize_filename (path-traversal/control-char/length-safe), size_within_cap, image magic-byte sniffing + defensive limited decode (decode-bomb guard), 32-byte request parsing, human_size. 13 unit tests. - GossipMessage::Chat and RoomEvent::ChatMessage carry an optional ChatAttachment; send_chat takes Option<ChatAttachment>. Untrusted inbound descriptors are filename-sanitized + size-validated on ingest. serde(default) keeps the field forward-compatible at the JSON layer; +round-trip and pre-v2 back-compat tests. The attachment id is a random 32-byte handle (rand, already a dep), not a content hash — the fetch is authenticated + encrypted + member-gated, so no crypto-hash dep is needed. 349 lib tests pass, clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+13
-2
@@ -22,16 +22,26 @@ pub const FRIENDS_PROTO: u32 = 1;
|
||||
/// Gossip plane version (`GossipPayload`/`GossipMessage`/`PeerState`, signing,
|
||||
/// freshness). Bump on any change. Mirrored in [`GOSSIP_SIG_DOMAIN`] and folded
|
||||
/// into [`versioned_topic`].
|
||||
pub const GOSSIP_PROTO: u32 = 1;
|
||||
///
|
||||
/// v2 (0.3.0): `GossipMessage::Chat` gained an optional file attachment
|
||||
/// (`ChatAttachment`), so a pre-v2 peer can't interpret/serve chat files — bumped
|
||||
/// to fail fast rather than half-work.
|
||||
pub const GOSSIP_PROTO: u32 = 2;
|
||||
/// File-transfer plane version (chat attachment request/stream shape). Bump on
|
||||
/// any change. Mirrored in [`FILES_ALPN`].
|
||||
pub const FILES_PROTO: u32 = 1;
|
||||
|
||||
/// ALPN for the audio datagram plane: `peerspeak/audio/<AUDIO_PROTO>`.
|
||||
pub const AUDIO_ALPN: &[u8] = b"peerspeak/audio/1";
|
||||
/// ALPN for the friends/presence plane: `peerspeak/friends/<FRIENDS_PROTO>`.
|
||||
pub const FRIENDS_ALPN: &[u8] = b"peerspeak/friends/1";
|
||||
/// ALPN for the file-transfer plane: `peerspeak/files/<FILES_PROTO>`. Carries
|
||||
/// chat attachment bytes via direct QUIC streams (not gossip).
|
||||
pub const FILES_ALPN: &[u8] = b"peerspeak/files/1";
|
||||
/// ed25519 gossip signature domain: `peerspeak-gossip-v<GOSSIP_PROTO>`. Carries
|
||||
/// the gossip protocol version into every signed payload — a version mismatch
|
||||
/// fails verification (cryptographic separation between gossip versions).
|
||||
pub const GOSSIP_SIG_DOMAIN: &str = "peerspeak-gossip-v1";
|
||||
pub const GOSSIP_SIG_DOMAIN: &str = "peerspeak-gossip-v2";
|
||||
|
||||
/// Version-namespace a room topic so peers on different gossip protocol versions
|
||||
/// derive **different subscription topics from the same ticket** and therefore
|
||||
@@ -62,6 +72,7 @@ mod tests {
|
||||
fn alpns_match_their_proto_versions() {
|
||||
assert_eq!(AUDIO_ALPN, format!("peerspeak/audio/{AUDIO_PROTO}").as_bytes());
|
||||
assert_eq!(FRIENDS_ALPN, format!("peerspeak/friends/{FRIENDS_PROTO}").as_bytes());
|
||||
assert_eq!(FILES_ALPN, format!("peerspeak/files/{FILES_PROTO}").as_bytes());
|
||||
assert_eq!(GOSSIP_SIG_DOMAIN, format!("peerspeak-gossip-v{GOSSIP_PROTO}"));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user