Skip to main content

netget/server/amqp/
codec.rs

1//! AMQP 0-9-1 wire codec: frames, primitive types, field tables and Basic properties.
2//!
3//! Written by hand rather than taken from a crate. `lapin` — the dependency the `amqp`
4//! feature already carries — is an AMQP *client*: it can parse and produce frames, but its
5//! types are wired into a client-side connection state machine, so a broker cannot borrow
6//! them. `lapin` stays for `src/client/amqp` and for the E2E tests, which drive this server
7//! with a real client.
8//!
9//! Everything here is bounds-checked **and depth-bounded**. [`Decoder`] never indexes without
10//! `get`, every length read off the wire is validated against the bytes remaining, every
11//! offset arithmetic uses `checked_add`, and field-table nesting is capped, so no frame —
12//! however malformed — can panic a connection task or overflow the stack.
13//!
14//! The depth half of that sentence was missing, and so was the bound. Field tables are
15//! recursive, five bytes bought a level, and a stack overflow is a `SIGSEGV` against a guard
16//! page rather than a panic — so it killed the whole process rather than one connection, and
17//! `tokio::spawn` could not contain it. See `MAX_FIELD_TABLE_DEPTH`.
18//!
19//! [`Encoder`] **refuses** what it cannot carry rather than shortening it. A `shortstr` holds
20//! 255 octets and carries every queue name, exchange name, routing key, consumer tag and
21//! field-table key in the protocol, so a 256-octet name truncated to 255 reaches the wire as a
22//! perfectly well-formed name for a *different* queue — the publisher publishes into one and
23//! the consumer waits on the other, and nothing at either end reports a problem. The one
24//! exception is [`Encoder::reply_text`], which is free-form prose rather than an identifier;
25//! it is documented where it is defined.
26//!
27//! The encoder is depth-bounded too, by the same `MAX_FIELD_TABLE_DEPTH` and with the same
28//! accounting, so it accepts exactly the tables the decoder yields.
29//!
30//! The depth half of that sentence was missing, and so was the bound. Field tables are
31//! recursive, five bytes bought a level, and a stack overflow is a `SIGSEGV` against a guard
32//! page rather than a panic — so it killed the whole process rather than one connection, and
33//! `tokio::spawn` could not contain it. See `MAX_FIELD_TABLE_DEPTH`.
34
35use crate::utils::truncate::truncate_str;
36use anyhow::{anyhow, Result};
37use serde_json::{Map, Value};
38
39// ============================================================================
40// Constants (AMQP 0-9-1 section 4.2.3 and the class/method registry)
41// ============================================================================
42
43/// The 8-byte protocol header an AMQP 0-9-1 client opens with, and the one this
44/// broker echoes when it rejects a version it does not speak.
45pub const PROTOCOL_HEADER_091: [u8; 8] = *b"AMQP\x00\x00\x09\x01";
46
47pub const FRAME_METHOD: u8 = 1;
48pub const FRAME_HEADER: u8 = 2;
49pub const FRAME_BODY: u8 = 3;
50pub const FRAME_HEARTBEAT: u8 = 8;
51pub const FRAME_END: u8 = 0xCE;
52
53/// Bytes of framing overhead around a payload: 1 type + 2 channel + 4 size + 1 end.
54pub const FRAME_OVERHEAD: usize = 8;
55
56pub const CLASS_CONNECTION: u16 = 10;
57pub const CLASS_CHANNEL: u16 = 20;
58pub const CLASS_EXCHANGE: u16 = 40;
59pub const CLASS_QUEUE: u16 = 50;
60pub const CLASS_BASIC: u16 = 60;
61
62pub const CONNECTION_START: u16 = 10;
63pub const CONNECTION_START_OK: u16 = 11;
64pub const CONNECTION_TUNE: u16 = 30;
65pub const CONNECTION_TUNE_OK: u16 = 31;
66pub const CONNECTION_OPEN: u16 = 40;
67pub const CONNECTION_OPEN_OK: u16 = 41;
68pub const CONNECTION_CLOSE: u16 = 50;
69pub const CONNECTION_CLOSE_OK: u16 = 51;
70
71pub const CHANNEL_OPEN: u16 = 10;
72pub const CHANNEL_OPEN_OK: u16 = 11;
73pub const CHANNEL_CLOSE: u16 = 40;
74pub const CHANNEL_CLOSE_OK: u16 = 41;
75
76pub const EXCHANGE_DECLARE: u16 = 10;
77pub const EXCHANGE_DECLARE_OK: u16 = 11;
78
79pub const QUEUE_DECLARE: u16 = 10;
80pub const QUEUE_DECLARE_OK: u16 = 11;
81pub const QUEUE_BIND: u16 = 20;
82pub const QUEUE_BIND_OK: u16 = 21;
83
84pub const BASIC_QOS: u16 = 10;
85pub const BASIC_QOS_OK: u16 = 11;
86pub const BASIC_CONSUME: u16 = 20;
87pub const BASIC_CONSUME_OK: u16 = 21;
88pub const BASIC_CANCEL: u16 = 30;
89pub const BASIC_CANCEL_OK: u16 = 31;
90pub const BASIC_PUBLISH: u16 = 40;
91pub const BASIC_RETURN: u16 = 50;
92pub const BASIC_DELIVER: u16 = 60;
93pub const BASIC_ACK: u16 = 80;
94pub const BASIC_REJECT: u16 = 90;
95pub const BASIC_NACK: u16 = 120;
96
97/// AMQP reply code for a method this broker does not implement (0-9-1 section 4.2.7).
98pub const REPLY_NOT_IMPLEMENTED: u16 = 540;
99/// AMQP reply code for a refused connection.
100pub const REPLY_ACCESS_REFUSED: u16 = 403;
101/// AMQP reply code for a resource another connection holds exclusively.
102pub const REPLY_RESOURCE_LOCKED: u16 = 405;
103
104/// Human name for a class/method pair, for logs and close reasons.
105pub fn method_name(class_id: u16, method_id: u16) -> String {
106    let name = match (class_id, method_id) {
107        (CLASS_CONNECTION, CONNECTION_START) => "connection.start",
108        (CLASS_CONNECTION, CONNECTION_START_OK) => "connection.start-ok",
109        (CLASS_CONNECTION, CONNECTION_TUNE) => "connection.tune",
110        (CLASS_CONNECTION, CONNECTION_TUNE_OK) => "connection.tune-ok",
111        (CLASS_CONNECTION, CONNECTION_OPEN) => "connection.open",
112        (CLASS_CONNECTION, CONNECTION_OPEN_OK) => "connection.open-ok",
113        (CLASS_CONNECTION, CONNECTION_CLOSE) => "connection.close",
114        (CLASS_CONNECTION, CONNECTION_CLOSE_OK) => "connection.close-ok",
115        (CLASS_CHANNEL, CHANNEL_OPEN) => "channel.open",
116        (CLASS_CHANNEL, CHANNEL_OPEN_OK) => "channel.open-ok",
117        (CLASS_CHANNEL, CHANNEL_CLOSE) => "channel.close",
118        (CLASS_CHANNEL, CHANNEL_CLOSE_OK) => "channel.close-ok",
119        (CLASS_EXCHANGE, EXCHANGE_DECLARE) => "exchange.declare",
120        (CLASS_QUEUE, QUEUE_DECLARE) => "queue.declare",
121        (CLASS_QUEUE, QUEUE_BIND) => "queue.bind",
122        (CLASS_BASIC, BASIC_QOS) => "basic.qos",
123        (CLASS_BASIC, BASIC_CONSUME) => "basic.consume",
124        (CLASS_BASIC, BASIC_CANCEL) => "basic.cancel",
125        (CLASS_BASIC, BASIC_PUBLISH) => "basic.publish",
126        (CLASS_BASIC, BASIC_ACK) => "basic.ack",
127        (CLASS_BASIC, BASIC_REJECT) => "basic.reject",
128        (CLASS_BASIC, BASIC_NACK) => "basic.nack",
129        _ => "",
130    };
131    if name.is_empty() {
132        format!("class {} method {}", class_id, method_id)
133    } else {
134        name.to_string()
135    }
136}
137
138// ============================================================================
139// Decoding
140// ============================================================================
141
142/// Bounds-checked reader over a frame payload.
143///
144/// Every accessor returns `Err` rather than panicking when the declared length of a field
145/// exceeds the bytes remaining, which is the whole attack surface of a length-prefixed
146/// binary protocol.
147/// How deeply an AMQP field table may nest before the frame is refused.
148///
149/// Field tables are recursive: a value may itself be a table (`F`) or an array (`A`) of
150/// values. Nothing in AMQP 0-9-1 bounds that, and nothing here did either — see
151/// `field_value_at` for why an unbounded version was a pre-authentication kill of the entire
152/// process.
153///
154/// Real tables are one or two deep. `client-properties` is a flat map with a nested
155/// `capabilities` table inside it, which is depth 2, and `amq-protocol` — what lapin emits —
156/// produces nothing deeper. 32 is generous by an order of magnitude and still bottoms the
157/// recursion out in a few hundred bytes of stack.
158const MAX_FIELD_TABLE_DEPTH: usize = 32;
159
160/// Octets a `shortstr` can carry: its length field is a single octet (AMQP 0-9-1 section
161/// 4.2.5.3). Not a policy choice and not tunable — 256 bytes is simply not expressible.
162pub const MAX_SHORT_STRING_LEN: usize = 255;
163
164pub struct Decoder<'a> {
165    buf: &'a [u8],
166    pos: usize,
167}
168
169impl<'a> Decoder<'a> {
170    pub fn new(buf: &'a [u8]) -> Self {
171        Self { buf, pos: 0 }
172    }
173
174    pub fn remaining(&self) -> usize {
175        self.buf.len().saturating_sub(self.pos)
176    }
177
178    fn take(&mut self, n: usize) -> Result<&'a [u8]> {
179        let end = self
180            .pos
181            .checked_add(n)
182            .ok_or_else(|| anyhow!("AMQP field length {} overflows the payload offset", n))?;
183        let slice = self.buf.get(self.pos..end).ok_or_else(|| {
184            anyhow!(
185                "AMQP payload truncated: {} bytes wanted at offset {}, only {} available",
186                n,
187                self.pos,
188                self.remaining()
189            )
190        })?;
191        self.pos = end;
192        Ok(slice)
193    }
194
195    pub fn u8(&mut self) -> Result<u8> {
196        let b: [u8; 1] = self
197            .take(1)?
198            .try_into()
199            .map_err(|_| anyhow!("AMQP octet read failed"))?;
200        Ok(b[0])
201    }
202
203    pub fn u16(&mut self) -> Result<u16> {
204        let b: [u8; 2] = self
205            .take(2)?
206            .try_into()
207            .map_err(|_| anyhow!("AMQP short read failed"))?;
208        Ok(u16::from_be_bytes(b))
209    }
210
211    pub fn u32(&mut self) -> Result<u32> {
212        let b: [u8; 4] = self
213            .take(4)?
214            .try_into()
215            .map_err(|_| anyhow!("AMQP long read failed"))?;
216        Ok(u32::from_be_bytes(b))
217    }
218
219    pub fn u64(&mut self) -> Result<u64> {
220        let b: [u8; 8] = self
221            .take(8)?
222            .try_into()
223            .map_err(|_| anyhow!("AMQP long-long read failed"))?;
224        Ok(u64::from_be_bytes(b))
225    }
226
227    /// `shortstr`: one length octet then that many bytes of UTF-8.
228    pub fn short_string(&mut self) -> Result<String> {
229        let len = self.u8()? as usize;
230        let bytes = self.take(len)?;
231        Ok(String::from_utf8_lossy(bytes).into_owned())
232    }
233
234    /// `longstr`: a 32-bit length then that many raw bytes.
235    pub fn long_bytes(&mut self) -> Result<&'a [u8]> {
236        let len = self.u32()? as usize;
237        self.take(len)
238    }
239
240    /// `longstr` rendered as text. AMQP long strings are byte strings; anything that is
241    /// not valid UTF-8 is rendered lossily rather than handed to the model as bytes.
242    pub fn long_string(&mut self) -> Result<String> {
243        let bytes = self.long_bytes()?;
244        Ok(String::from_utf8_lossy(bytes).into_owned())
245    }
246
247    /// `bit` fields: packed least-significant-bit first, 8 to an octet.
248    pub fn bits(&mut self, count: usize) -> Result<Vec<bool>> {
249        let bytes = self.take(count.div_ceil(8))?;
250        let mut out = Vec::with_capacity(count);
251        for i in 0..count {
252            let byte = bytes.get(i / 8).copied().unwrap_or(0);
253            out.push(byte & (1u8 << (i % 8)) != 0);
254        }
255        Ok(out)
256    }
257
258    /// `table`: a 32-bit byte length then key/value pairs, decoded to a JSON object.
259    ///
260    /// The table is consumed from the outer payload by its declared length *first*, so a
261    /// value of a type this decoder does not recognise costs only the rest of that table:
262    /// the entries decoded so far are returned and the outer payload stays in sync.
263    pub fn field_table(&mut self) -> Result<Value> {
264        self.field_table_at(0)
265    }
266
267    fn field_table_at(&mut self, depth: usize) -> Result<Value> {
268        if depth >= MAX_FIELD_TABLE_DEPTH {
269            return Err(anyhow!(
270                "AMQP field table nested deeper than {} levels",
271                MAX_FIELD_TABLE_DEPTH
272            ));
273        }
274        let body = self.long_bytes()?;
275        let mut inner = Decoder::new(body);
276        let mut map = Map::new();
277        while inner.remaining() > 0 {
278            let Ok(key) = inner.short_string() else { break };
279            match inner.field_value_at(depth + 1) {
280                Ok(value) => {
281                    map.insert(key, value);
282                }
283                Err(_) => break,
284            }
285        }
286        Ok(Value::Object(map))
287    }
288
289    /// One typed field-table value. Type ids follow RabbitMQ's dialect, which is what
290    /// `amq-protocol` (and therefore `lapin`) emits: `s` is a signed 16-bit integer, not
291    /// a short string, and both `l` and `L` are signed 64-bit.
292    ///
293    /// `depth` is not decoration. A field value may be a nested table (`F`) or a nested array
294    /// (`A`), so this is mutually recursive with itself and with `field_table_at`, and one
295    /// more level costs the peer **five bytes** on the wire (`A` plus a four-byte length). At
296    /// the default `frame_max` of 128 KiB that is ~26 000 levels in a single frame, and at the
297    /// permitted maximum of 1 MiB about 210 000 — far past what a 2 MiB tokio worker stack
298    /// survives. The overflow is a `SIGSEGV` against a guard page, not a panic, so
299    /// `tokio::spawn` cannot contain it and the whole NetGet process dies, taking every other
300    /// server and client in it.
301    ///
302    /// It was reachable **before authentication**, in two writes: the protocol header, then
303    /// one `Connection.Start-Ok` whose `client-properties` table `mod.rs` decodes in
304    /// `Phase::AwaitStartOk`, before any credential is examined and before any model decision
305    /// is asked for.
306    fn field_value_at(&mut self, depth: usize) -> Result<Value> {
307        if depth >= MAX_FIELD_TABLE_DEPTH {
308            return Err(anyhow!(
309                "AMQP field table nested deeper than {} levels",
310                MAX_FIELD_TABLE_DEPTH
311            ));
312        }
313        let tag = self.u8()?;
314        let value = match tag {
315            b't' => Value::Bool(self.u8()? != 0),
316            b'b' => Value::from(self.u8()? as i8),
317            b'B' => Value::from(self.u8()?),
318            b's' | b'U' => Value::from(self.u16()? as i16),
319            b'u' => Value::from(self.u16()?),
320            b'I' => Value::from(self.u32()? as i32),
321            b'i' => Value::from(self.u32()?),
322            b'l' | b'L' => Value::from(self.u64()? as i64),
323            b'f' => Value::from(f32::from_bits(self.u32()?)),
324            b'd' => Value::from(f64::from_bits(self.u64()?)),
325            b'D' => {
326                let scale = self.u8()? as u32;
327                let raw = self.u32()? as i32;
328                Value::from(raw as f64 / 10f64.powi(scale as i32))
329            }
330            b'S' => Value::String(self.long_string()?),
331            b'A' => {
332                let body = self.long_bytes()?;
333                let mut inner = Decoder::new(body);
334                let mut items = Vec::new();
335                while inner.remaining() > 0 {
336                    match inner.field_value_at(depth + 1) {
337                        Ok(v) => items.push(v),
338                        Err(_) => break,
339                    }
340                }
341                Value::Array(items)
342            }
343            b'T' => Value::from(self.u64()?),
344            b'F' => self.field_table_at(depth + 1)?,
345            b'x' => {
346                let bytes = self.long_bytes()?;
347                Value::String(String::from_utf8_lossy(bytes).into_owned())
348            }
349            b'V' => Value::Null,
350            other => {
351                return Err(anyhow!(
352                    "unknown AMQP field-table type id {:?}",
353                    other as char
354                ))
355            }
356        };
357        Ok(value)
358    }
359}
360
361// ============================================================================
362// Encoding
363// ============================================================================
364
365/// Writer for AMQP method arguments and property lists.
366#[derive(Default)]
367pub struct Encoder {
368    buf: Vec<u8>,
369}
370
371impl Encoder {
372    pub fn new() -> Self {
373        Self { buf: Vec::new() }
374    }
375
376    pub fn u8(&mut self, v: u8) {
377        self.buf.push(v);
378    }
379
380    pub fn u16(&mut self, v: u16) {
381        self.buf.extend_from_slice(&v.to_be_bytes());
382    }
383
384    pub fn u32(&mut self, v: u32) {
385        self.buf.extend_from_slice(&v.to_be_bytes());
386    }
387
388    pub fn u64(&mut self, v: u64) {
389        self.buf.extend_from_slice(&v.to_be_bytes());
390    }
391
392    /// `shortstr`: one length octet then that many bytes. Refuses anything longer than
393    /// [`MAX_SHORT_STRING_LEN`], leaving the buffer untouched.
394    ///
395    /// This used to truncate at a UTF-8 char boundary, which is the `encode_apn` shape: a
396    /// name shortened to fit is not an error the caller can see, it is a well-formed name for
397    /// something else. shortstr carries queue names, exchange names, routing keys, consumer
398    /// tags and every field-table key, so the cost is a queue declared under one name and
399    /// bound under another — measured, not theorised: two table keys differing only past
400    /// octet 255 collapsed into one entry and a value was lost.
401    ///
402    /// Not reachable from the wire — [`Decoder::short_string`] reads a one-octet length and
403    /// so cannot produce more than 255 bytes — which means the source is the model or NetGet
404    /// itself, and both want to be told.
405    pub fn short_string(&mut self, s: &str) -> Result<()> {
406        if s.len() > MAX_SHORT_STRING_LEN {
407            return Err(anyhow!(
408                "AMQP shortstr cannot carry {} bytes; the wire format's length octet stops at \
409                 {}. Shorten the name rather than letting it be cut, which would name \
410                 something else: {}",
411                s.len(),
412                MAX_SHORT_STRING_LEN,
413                crate::utils::truncate_for_log(s, 48)
414            ));
415        }
416        self.buf.push(s.len() as u8);
417        self.buf.extend_from_slice(s.as_bytes());
418        Ok(())
419    }
420
421    /// `shortstr` for the one AMQP field that carries free-form prose rather than an
422    /// identifier: `reply-text`, in `connection.close` and `channel.close`.
423    ///
424    /// Shortened at a UTF-8 char boundary instead of refused, and the distinction is
425    /// deliberate. A shortened *name* is a believable different name; a shortened
426    /// *diagnostic* is the same diagnostic with less of it, and nothing keys off it. The
427    /// alternative — refusing — means not sending the close frame at all, so a peer that
428    /// provoked a long error message would be left waiting on a connection NetGet has already
429    /// given up on. Never reach for this for a queue, exchange, routing key, consumer tag or
430    /// table key: those go through [`Encoder::short_string`] and are meant to fail loudly.
431    pub fn reply_text(&mut self, text: &str) {
432        let text = truncate_str(text, MAX_SHORT_STRING_LEN);
433        self.buf.push(text.len() as u8);
434        self.buf.extend_from_slice(text.as_bytes());
435    }
436
437    pub fn long_string(&mut self, bytes: &[u8]) {
438        self.u32(bytes.len() as u32);
439        self.buf.extend_from_slice(bytes);
440    }
441
442    pub fn bits(&mut self, bits: &[bool]) {
443        for chunk in bits.chunks(8) {
444            let mut byte = 0u8;
445            for (i, set) in chunk.iter().enumerate() {
446                if *set {
447                    byte |= 1u8 << i;
448                }
449            }
450            self.buf.push(byte);
451        }
452    }
453
454    /// Encode a JSON object as a field table. Non-object input encodes as an empty table.
455    ///
456    /// Refuses a key longer than a `shortstr` and a table nested past
457    /// `MAX_FIELD_TABLE_DEPTH`. Either refusal leaves this encoder's buffer exactly as it
458    /// was: a half-written table is worse than none, because the length prefix in front of it
459    /// would then describe bytes that are not there and desynchronise every argument after it
460    /// in the same method frame.
461    pub fn field_table(&mut self, value: &Value) -> Result<()> {
462        self.field_table_at(value, 0)
463    }
464
465    fn field_table_at(&mut self, value: &Value, depth: usize) -> Result<()> {
466        if depth >= MAX_FIELD_TABLE_DEPTH {
467            return Err(anyhow!(
468                "AMQP field table nested deeper than {} levels",
469                MAX_FIELD_TABLE_DEPTH
470            ));
471        }
472        let mut inner = Encoder::new();
473        if let Some(map) = value.as_object() {
474            for (key, val) in map {
475                inner.short_string(key)?;
476                inner.field_value_at(val, depth + 1)?;
477            }
478        }
479        self.long_string(&inner.buf);
480        Ok(())
481    }
482
483    /// One typed field-table value, depth-bounded with the **same accounting** as
484    /// `Decoder::field_value_at` — a value directly inside a table is at `depth + 1`, an
485    /// array's items and a nested table's entries one deeper again. The symmetry is the
486    /// point: a table the decoder hands back has to be re-encodable, because the broker
487    /// really does echo a peer's `client-properties`, and a bound that disagreed with the
488    /// decoder's by one level would refuse exactly the deepest tables a peer can send.
489    ///
490    /// Bounding the encoder is not redundant with bounding the decoder. Every table reaching
491    /// it *today* came either off the wire through the bounded decoder or through
492    /// `serde_json`, whose parser caps nesting at 128 — so neither is near a stack overflow,
493    /// and this bound fires for no caller in the tree. It is here because that argument is
494    /// about the callers rather than about this function: a `Value` built programmatically in
495    /// a loop has no cap at all, a stack overflow is a `SIGSEGV` against the guard page rather
496    /// than a panic, so `tokio::spawn` cannot contain it and the whole NetGet process dies,
497    /// and this tree has had six of them. A counter costs one comparison.
498    fn field_value_at(&mut self, value: &Value, depth: usize) -> Result<()> {
499        if depth >= MAX_FIELD_TABLE_DEPTH {
500            return Err(anyhow!(
501                "AMQP field table nested deeper than {} levels",
502                MAX_FIELD_TABLE_DEPTH
503            ));
504        }
505        match value {
506            Value::Null => self.u8(b'V'),
507            Value::Bool(b) => {
508                self.u8(b't');
509                self.u8(u8::from(*b));
510            }
511            Value::Number(n) => {
512                if let Some(i) = n.as_i64() {
513                    self.u8(b'l');
514                    self.u64(i as u64);
515                } else {
516                    self.u8(b'd');
517                    self.u64(n.as_f64().unwrap_or(0.0).to_bits());
518                }
519            }
520            Value::String(s) => {
521                self.u8(b'S');
522                self.long_string(s.as_bytes());
523            }
524            // Both composite arms encode into a scratch buffer and only then write their type
525            // id, so a refusal anywhere below leaves this level untouched too.
526            Value::Array(items) => {
527                let mut inner = Encoder::new();
528                for item in items {
529                    inner.field_value_at(item, depth + 1)?;
530                }
531                self.u8(b'A');
532                self.long_string(&inner.buf);
533            }
534            Value::Object(_) => {
535                let mut inner = Encoder::new();
536                inner.field_table_at(value, depth + 1)?;
537                self.u8(b'F');
538                self.buf.extend_from_slice(&inner.buf);
539            }
540        }
541        Ok(())
542    }
543
544    pub fn into_vec(self) -> Vec<u8> {
545        self.buf
546    }
547
548    pub fn as_slice(&self) -> &[u8] {
549        &self.buf
550    }
551}
552
553// ============================================================================
554// Frames
555// ============================================================================
556
557/// One decoded frame: its type, channel, and payload with the `0xCE` end marker removed.
558pub struct Frame {
559    pub frame_type: u8,
560    pub channel: u16,
561    pub payload: Vec<u8>,
562}
563
564/// Build a method frame: `class-id`, `method-id`, then the already-encoded arguments.
565pub fn method_frame(channel: u16, class_id: u16, method_id: u16, args: &[u8]) -> Vec<u8> {
566    let mut payload = Vec::with_capacity(4 + args.len());
567    payload.extend_from_slice(&class_id.to_be_bytes());
568    payload.extend_from_slice(&method_id.to_be_bytes());
569    payload.extend_from_slice(args);
570    raw_frame(FRAME_METHOD, channel, &payload)
571}
572
573/// Build a content header frame (0-9-1 section 4.2.6.1).
574pub fn content_header_frame(channel: u16, class_id: u16, body_size: u64, props: &[u8]) -> Vec<u8> {
575    let mut payload = Vec::with_capacity(12 + props.len());
576    payload.extend_from_slice(&class_id.to_be_bytes());
577    payload.extend_from_slice(&0u16.to_be_bytes()); // weight, unused
578    payload.extend_from_slice(&body_size.to_be_bytes());
579    payload.extend_from_slice(props);
580    raw_frame(FRAME_HEADER, channel, &payload)
581}
582
583/// Split a message body into as many body frames as the negotiated frame size requires.
584///
585/// A zero-length body produces no frames at all, which is what the spec expects when the
586/// content header declares `body-size` 0.
587pub fn body_frames(channel: u16, body: &[u8], max_frame_size: usize) -> Vec<Vec<u8>> {
588    let chunk_size = max_frame_size.saturating_sub(FRAME_OVERHEAD).max(1);
589    body.chunks(chunk_size)
590        .map(|chunk| raw_frame(FRAME_BODY, channel, chunk))
591        .collect()
592}
593
594pub fn heartbeat_frame() -> Vec<u8> {
595    raw_frame(FRAME_HEARTBEAT, 0, &[])
596}
597
598fn raw_frame(frame_type: u8, channel: u16, payload: &[u8]) -> Vec<u8> {
599    let mut out = Vec::with_capacity(FRAME_OVERHEAD + payload.len());
600    out.push(frame_type);
601    out.extend_from_slice(&channel.to_be_bytes());
602    out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
603    out.extend_from_slice(payload);
604    out.push(FRAME_END);
605    out
606}
607
608// ============================================================================
609// Basic content properties
610// ============================================================================
611
612/// The Basic class property list carried in a content header frame.
613///
614/// Property presence is a 16-bit flag word, most significant bit first, and a set bit 0
615/// continues the flags into another word. Both directions are handled here.
616#[derive(Default, Clone, Debug)]
617pub struct BasicProperties {
618    pub content_type: Option<String>,
619    pub content_encoding: Option<String>,
620    pub headers: Option<Value>,
621    pub delivery_mode: Option<u8>,
622    pub priority: Option<u8>,
623    pub correlation_id: Option<String>,
624    pub reply_to: Option<String>,
625    pub expiration: Option<String>,
626    pub message_id: Option<String>,
627    pub timestamp: Option<u64>,
628    pub kind: Option<String>,
629    pub user_id: Option<String>,
630    pub app_id: Option<String>,
631    pub cluster_id: Option<String>,
632}
633
634impl BasicProperties {
635    pub fn decode(d: &mut Decoder<'_>) -> Result<Self> {
636        // Bit 0 of a flags word means the flags continue in another word. All words are
637        // read before any property value; no defined Basic property lives past the first
638        // word, so the continuations are consumed and ignored.
639        let flags = d.u16()?;
640        let mut word = flags;
641        let mut words = 1;
642        while word & 1 != 0 {
643            if words >= 8 {
644                return Err(anyhow!(
645                    "AMQP content header declares more than 8 property flag words"
646                ));
647            }
648            word = d.u16()?;
649            words += 1;
650        }
651
652        let has = |bit: u32| flags & (1 << (15 - bit)) != 0;
653        let mut props = BasicProperties::default();
654        if has(0) {
655            props.content_type = Some(d.short_string()?);
656        }
657        if has(1) {
658            props.content_encoding = Some(d.short_string()?);
659        }
660        if has(2) {
661            props.headers = Some(d.field_table()?);
662        }
663        if has(3) {
664            props.delivery_mode = Some(d.u8()?);
665        }
666        if has(4) {
667            props.priority = Some(d.u8()?);
668        }
669        if has(5) {
670            props.correlation_id = Some(d.short_string()?);
671        }
672        if has(6) {
673            props.reply_to = Some(d.short_string()?);
674        }
675        if has(7) {
676            props.expiration = Some(d.short_string()?);
677        }
678        if has(8) {
679            props.message_id = Some(d.short_string()?);
680        }
681        if has(9) {
682            props.timestamp = Some(d.u64()?);
683        }
684        if has(10) {
685            props.kind = Some(d.short_string()?);
686        }
687        if has(11) {
688            props.user_id = Some(d.short_string()?);
689        }
690        if has(12) {
691            props.app_id = Some(d.short_string()?);
692        }
693        if has(13) {
694            props.cluster_id = Some(d.short_string()?);
695        }
696        Ok(props)
697    }
698
699    /// Serialise the property list.
700    ///
701    /// Returns `Err` for a `shortstr` property longer than 255 octets or a `headers` table
702    /// nested past the decoder's bound, rather than shortening either. `content_type`,
703    /// `reply_to`, `correlation_id` and `message_id` are all names something downstream
704    /// matches on, so a shortened one is the wrong name rather than a shorter one — and the
705    /// presence flags are written before the values, so a silently dropped property would
706    /// leave the flag word claiming a field that is not in the payload.
707    pub fn encode(&self) -> Result<Vec<u8>> {
708        let present = [
709            self.content_type.is_some(),
710            self.content_encoding.is_some(),
711            self.headers.is_some(),
712            self.delivery_mode.is_some(),
713            self.priority.is_some(),
714            self.correlation_id.is_some(),
715            self.reply_to.is_some(),
716            self.expiration.is_some(),
717            self.message_id.is_some(),
718            self.timestamp.is_some(),
719            self.kind.is_some(),
720            self.user_id.is_some(),
721            self.app_id.is_some(),
722            self.cluster_id.is_some(),
723        ];
724        let mut flags: u16 = 0;
725        for (bit, set) in present.iter().enumerate() {
726            if *set {
727                flags |= 1 << (15 - bit as u16);
728            }
729        }
730
731        let mut e = Encoder::new();
732        e.u16(flags);
733        if let Some(v) = &self.content_type {
734            e.short_string(v)?;
735        }
736        if let Some(v) = &self.content_encoding {
737            e.short_string(v)?;
738        }
739        if let Some(v) = &self.headers {
740            e.field_table(v)?;
741        }
742        if let Some(v) = self.delivery_mode {
743            e.u8(v);
744        }
745        if let Some(v) = self.priority {
746            e.u8(v);
747        }
748        if let Some(v) = &self.correlation_id {
749            e.short_string(v)?;
750        }
751        if let Some(v) = &self.reply_to {
752            e.short_string(v)?;
753        }
754        if let Some(v) = &self.expiration {
755            e.short_string(v)?;
756        }
757        if let Some(v) = &self.message_id {
758            e.short_string(v)?;
759        }
760        if let Some(v) = self.timestamp {
761            e.u64(v);
762        }
763        if let Some(v) = &self.kind {
764            e.short_string(v)?;
765        }
766        if let Some(v) = &self.user_id {
767            e.short_string(v)?;
768        }
769        if let Some(v) = &self.app_id {
770            e.short_string(v)?;
771        }
772        if let Some(v) = &self.cluster_id {
773            e.short_string(v)?;
774        }
775        Ok(e.into_vec())
776    }
777
778    /// Render for an event: only the properties the publisher actually set, as JSON.
779    pub fn to_json(&self) -> Value {
780        let mut map = Map::new();
781        if let Some(v) = &self.content_type {
782            map.insert("content_type".into(), Value::String(v.clone()));
783        }
784        if let Some(v) = &self.content_encoding {
785            map.insert("content_encoding".into(), Value::String(v.clone()));
786        }
787        if let Some(v) = &self.headers {
788            map.insert("headers".into(), v.clone());
789        }
790        if let Some(v) = self.delivery_mode {
791            map.insert("delivery_mode".into(), Value::from(v));
792        }
793        if let Some(v) = self.priority {
794            map.insert("priority".into(), Value::from(v));
795        }
796        if let Some(v) = &self.correlation_id {
797            map.insert("correlation_id".into(), Value::String(v.clone()));
798        }
799        if let Some(v) = &self.reply_to {
800            map.insert("reply_to".into(), Value::String(v.clone()));
801        }
802        if let Some(v) = &self.expiration {
803            map.insert("expiration".into(), Value::String(v.clone()));
804        }
805        if let Some(v) = &self.message_id {
806            map.insert("message_id".into(), Value::String(v.clone()));
807        }
808        if let Some(v) = self.timestamp {
809            map.insert("timestamp".into(), Value::from(v));
810        }
811        if let Some(v) = &self.kind {
812            map.insert("type".into(), Value::String(v.clone()));
813        }
814        if let Some(v) = &self.user_id {
815            map.insert("user_id".into(), Value::String(v.clone()));
816        }
817        if let Some(v) = &self.app_id {
818            map.insert("app_id".into(), Value::String(v.clone()));
819        }
820        if let Some(v) = &self.cluster_id {
821            map.insert("cluster_id".into(), Value::String(v.clone()));
822        }
823        Value::Object(map)
824    }
825}
826
827/// Split the `\0user\0password` payload of a SASL PLAIN response.
828///
829/// Returns `(username, has_password)`. The password itself is deliberately not returned:
830/// nothing in this protocol has a reason to put it in an event or a log.
831pub fn parse_plain_response(response: &str) -> (Option<String>, bool) {
832    let mut parts = response.split('\0');
833    // The first field is the authorization identity, normally empty.
834    let _authzid = parts.next();
835    let username = parts.next().filter(|s| !s.is_empty()).map(str::to_string);
836    let has_password = parts.next().is_some_and(|p| !p.is_empty());
837    (username, has_password)
838}