Skip to main content

packet_dissector_sip/
lib.rs

1//! SIP (Session Initiation Protocol) dissector.
2//!
3//! Parses SIP request and response messages as defined in RFC 3261.
4//! SIP reuses HTTP-like syntax (RFC 3261, Section 7). This dissector parses
5//! the SIP start-line manually and uses the [`httparse`] crate for header parsing.
6//!
7//! ## References
8//! - RFC 3261: SIP: Session Initiation Protocol <https://www.rfc-editor.org/rfc/rfc3261>
9//! - RFC 3262: Reliability of Provisional Responses (PRACK) <https://www.rfc-editor.org/rfc/rfc3262>
10//! - RFC 3265: SIP-Specific Event Notification (SUBSCRIBE/NOTIFY) <https://www.rfc-editor.org/rfc/rfc3265>
11//! - RFC 3311: UPDATE Method <https://www.rfc-editor.org/rfc/rfc3311>
12//! - RFC 3428: SIP Extension for Instant Messaging (MESSAGE) <https://www.rfc-editor.org/rfc/rfc3428>
13//! - RFC 3515: Refer Method <https://www.rfc-editor.org/rfc/rfc3515>
14//! - RFC 3903: SIP Extension for Event State Publication (PUBLISH) <https://www.rfc-editor.org/rfc/rfc3903>
15//! - RFC 6086: INFO Method and Package Framework <https://www.rfc-editor.org/rfc/rfc6086>
16
17#![deny(missing_docs)]
18
19use packet_dissector_core::dissector::{DispatchHint, DissectResult, Dissector};
20use packet_dissector_core::error::PacketError;
21use packet_dissector_core::field::{FieldDescriptor, FieldType, FieldValue};
22use packet_dissector_core::packet::DissectBuffer;
23use packet_dissector_core::util::{intern_content_type, slice_offset, str_offset, trim_ows};
24
25/// Maximum number of SIP headers to parse.
26const MAX_HEADERS: usize = 64;
27
28/// Minimum valid SIP start-line length.
29///
30/// Shortest request:  `ACK sip:x SIP/2.0\r\n` = 19 bytes
31/// Shortest response: `SIP/2.0 100 T\r\n`     = 15 bytes
32const MIN_START_LINE_LEN: usize = 15;
33
34// ---------------------------------------------------------------------------
35// Field descriptors
36// ---------------------------------------------------------------------------
37
38/// Field descriptor indices for [`FIELD_DESCRIPTORS`].
39const FD_IS_RESPONSE: usize = 0;
40const FD_METHOD: usize = 1;
41const FD_URI: usize = 2;
42const FD_VERSION: usize = 3;
43const FD_STATUS_CODE: usize = 4;
44const FD_REASON_PHRASE: usize = 5;
45const FD_HEADERS: usize = 6;
46const FD_CONTENT_LENGTH: usize = 7;
47const FD_CONTENT_TYPE: usize = 8;
48
49/// Child descriptor indices for [`HEADER_CHILDREN`].
50const HC_NAME: usize = 0;
51const HC_VALUE: usize = 1;
52
53/// Child descriptors for each header entry object.
54static HEADER_CHILDREN: &[FieldDescriptor] = &[
55    FieldDescriptor::new("name", "Name", FieldType::Str),
56    FieldDescriptor::new("value", "Value", FieldType::Str),
57];
58
59/// Descriptor for the SIP header Object container.
60///
61/// The outer label ("Header") no longer collides with the inner `Name`
62/// child. The header's own name is a borrowed string from the packet and
63/// therefore cannot be returned through
64/// [`DissectBuffer::resolve_container_display_name`], which requires a
65/// `&'static str`.
66static FD_HEADER: FieldDescriptor = FieldDescriptor {
67    name: "header",
68    display_name: "Header",
69    field_type: FieldType::Object,
70    optional: false,
71    children: None,
72    display_fn: None,
73    format_fn: None,
74};
75
76/// All field descriptors for the SIP dissector.
77static FIELD_DESCRIPTORS: &[FieldDescriptor] = &[
78    // RFC 3261, Section 7 — distinguishes request from response
79    FieldDescriptor::new("is_response", "Is Response", FieldType::U8),
80    // RFC 3261, Section 7.1 — request method
81    FieldDescriptor::new("method", "Method", FieldType::Str).optional(),
82    // RFC 3261, Section 7.1 — Request-URI
83    FieldDescriptor::new("uri", "Request URI", FieldType::Str).optional(),
84    // RFC 3261, Section 7 — SIP-Version
85    FieldDescriptor::new("version", "Version", FieldType::Str),
86    // RFC 3261, Section 7.2 — Status-Code
87    FieldDescriptor::new("status_code", "Status Code", FieldType::U16).optional(),
88    // RFC 3261, Section 7.2 — Reason-Phrase
89    FieldDescriptor::new("reason_phrase", "Reason Phrase", FieldType::Str).optional(),
90    // RFC 3261, Section 7.3 — Header fields
91    FieldDescriptor::new("headers", "Headers", FieldType::Array)
92        .optional()
93        .with_children(HEADER_CHILDREN),
94    // RFC 3261, Section 20.14 — Content-Length
95    FieldDescriptor::new("content_length", "Content Length", FieldType::U32).optional(),
96    // RFC 3261, Section 20.15 — Content-Type
97    FieldDescriptor::new("content_type", "Content Type", FieldType::Str).optional(),
98];
99
100/// SIP dissector.
101///
102/// Parses both SIP request and response messages. The dissector detects
103/// whether the message is a request or response by checking if the
104/// start-line begins with `"SIP/"` (response) or a method token (request).
105pub struct SipDissector;
106
107impl Dissector for SipDissector {
108    fn name(&self) -> &'static str {
109        "Session Initiation Protocol"
110    }
111
112    fn short_name(&self) -> &'static str {
113        "SIP"
114    }
115
116    fn field_descriptors(&self) -> &'static [FieldDescriptor] {
117        FIELD_DESCRIPTORS
118    }
119
120    fn dissect<'pkt>(
121        &self,
122        data: &'pkt [u8],
123        buf: &mut DissectBuffer<'pkt>,
124        offset: usize,
125    ) -> Result<DissectResult, PacketError> {
126        if data.len() < MIN_START_LINE_LEN {
127            return Err(PacketError::Truncated {
128                expected: MIN_START_LINE_LEN,
129                actual: data.len(),
130            });
131        }
132
133        // RFC 3261, Section 7 — detect request vs response
134        let is_response = data.starts_with(b"SIP/");
135
136        buf.begin_layer("SIP", None, FIELD_DESCRIPTORS, offset..offset);
137
138        buf.push_field(
139            &FIELD_DESCRIPTORS[FD_IS_RESPONSE],
140            FieldValue::U8(u8::from(is_response)),
141            offset..offset + 1,
142        );
143
144        let header_len = if is_response {
145            parse_response(data, offset, buf)?
146        } else {
147            parse_request(data, offset, buf)?
148        };
149
150        // Extract Content-Length and Content-Type from parsed headers
151        let content_length = extract_header_value(buf, "Content-Length")
152            .or_else(|| extract_header_value(buf, "l"))
153            .and_then(|v| v.parse::<u32>().ok());
154        let content_type =
155            extract_header_value(buf, "Content-Type").or_else(|| extract_header_value(buf, "c"));
156
157        if let Some(cl) = content_length {
158            buf.push_field(
159                &FIELD_DESCRIPTORS[FD_CONTENT_LENGTH],
160                FieldValue::U32(cl),
161                offset..offset + header_len,
162            );
163        }
164
165        if let Some(ct) = content_type {
166            buf.push_field(
167                &FIELD_DESCRIPTORS[FD_CONTENT_TYPE],
168                FieldValue::Str(ct),
169                offset..offset + header_len,
170            );
171        }
172
173        let body_len = content_length.unwrap_or(0) as usize;
174        let total = header_len + body_len;
175
176        if total > data.len() {
177            if let Some(layer) = buf.last_layer_mut() {
178                layer.range = offset..offset + header_len;
179            }
180            buf.end_layer();
181            return Err(PacketError::Truncated {
182                expected: total,
183                actual: data.len(),
184            });
185        }
186
187        // RFC 3261, Section 7.4 — dispatch body by Content-Type.
188        if body_len > 0 {
189            if let Some(ct) =
190                extract_header_value(buf, "Content-Type").or_else(|| extract_header_value(buf, "c"))
191            {
192                if let Some(interned) = intern_content_type(ct) {
193                    if let Some(layer) = buf.last_layer_mut() {
194                        layer.range = offset..offset + header_len;
195                    }
196                    buf.end_layer();
197                    return Ok(DissectResult::new(
198                        header_len,
199                        DispatchHint::ByContentType(interned),
200                    ));
201                }
202            }
203        }
204
205        if let Some(layer) = buf.last_layer_mut() {
206            layer.range = offset..offset + total;
207        }
208        buf.end_layer();
209
210        Ok(DissectResult::new(total, DispatchHint::End))
211    }
212}
213
214// ---------------------------------------------------------------------------
215// Start-line parsing helpers
216// ---------------------------------------------------------------------------
217
218/// Find the end of the first line (CRLF or LF), returning the position
219/// after the line terminator. Returns `None` if no terminator is found.
220fn find_line_end(data: &[u8]) -> Option<usize> {
221    data.iter().position(|&b| b == b'\n').map(|i| i + 1)
222}
223
224/// Extract the first line from `data`, returning `(line_str, line_end)`.
225///
226/// `line_end` is the byte position after the CRLF/LF terminator.
227/// `line_str` is the line content with the terminator stripped.
228fn take_start_line(data: &[u8]) -> Result<(&str, usize), PacketError> {
229    let line_end = find_line_end(data).ok_or(PacketError::Truncated {
230        expected: data.len() + 1,
231        actual: data.len(),
232    })?;
233    let line = &data[..line_end];
234    let trimmed = if line.ends_with(b"\r\n") {
235        &line[..line.len() - 2]
236    } else {
237        &line[..line.len() - 1]
238    };
239    let line_str = core::str::from_utf8(trimmed)
240        .map_err(|_| PacketError::InvalidHeader("start-line is not valid UTF-8"))?;
241    Ok((line_str, line_end))
242}
243
244/// Parse headers after the start-line and return total header section length.
245fn parse_remaining_headers<'pkt>(
246    data: &'pkt [u8],
247    offset: usize,
248    line_end: usize,
249    buf: &mut DissectBuffer<'pkt>,
250) -> Result<usize, PacketError> {
251    let header_len = parse_headers(&data[line_end..], offset, line_end, buf)?;
252    Ok(line_end + header_len)
253}
254
255// ---------------------------------------------------------------------------
256// Request / response parsing
257// ---------------------------------------------------------------------------
258
259/// Parse a SIP request start-line and headers, populating fields.
260fn parse_request<'pkt>(
261    data: &'pkt [u8],
262    offset: usize,
263    buf: &mut DissectBuffer<'pkt>,
264) -> Result<usize, PacketError> {
265    let (line_str, line_end) = take_start_line(data)?;
266
267    // RFC 3261, Section 7.1 — Request-Line = Method SP Request-URI SP SIP-Version
268    let first_sp = line_str
269        .find(' ')
270        .ok_or(PacketError::InvalidHeader("missing SP in request-line"))?;
271    let method = &line_str[..first_sp];
272    if method.is_empty() {
273        return Err(PacketError::InvalidHeader("empty method in request-line"));
274    }
275    let rest = &line_str[first_sp + 1..];
276
277    let last_sp = rest
278        .rfind(' ')
279        .ok_or(PacketError::InvalidHeader("missing SP before SIP-Version"))?;
280    let uri = &rest[..last_sp];
281    if uri.is_empty() {
282        return Err(PacketError::InvalidHeader(
283            "empty Request-URI in request-line",
284        ));
285    }
286    let version = &rest[last_sp + 1..];
287
288    if !version.starts_with("SIP/") {
289        return Err(PacketError::InvalidHeader("invalid SIP version"));
290    }
291
292    buf.push_field(
293        &FIELD_DESCRIPTORS[FD_METHOD],
294        FieldValue::Str(method),
295        offset..offset + method.len(),
296    );
297
298    let uri_start = first_sp + 1;
299    buf.push_field(
300        &FIELD_DESCRIPTORS[FD_URI],
301        FieldValue::Str(uri),
302        offset + uri_start..offset + uri_start + uri.len(),
303    );
304
305    buf.push_field(
306        &FIELD_DESCRIPTORS[FD_VERSION],
307        FieldValue::Str(version),
308        offset..offset + line_end,
309    );
310
311    parse_remaining_headers(data, offset, line_end, buf)
312}
313
314/// Parse a SIP response start-line and headers, populating fields.
315fn parse_response<'pkt>(
316    data: &'pkt [u8],
317    offset: usize,
318    buf: &mut DissectBuffer<'pkt>,
319) -> Result<usize, PacketError> {
320    let (line_str, line_end) = take_start_line(data)?;
321
322    // RFC 3261, Section 7.2 — Status-Line = SIP-Version SP Status-Code SP Reason-Phrase
323    let first_sp = line_str
324        .find(' ')
325        .ok_or(PacketError::InvalidHeader("missing SP in status-line"))?;
326    let version = &line_str[..first_sp];
327    let rest = &line_str[first_sp + 1..];
328
329    if !version.starts_with("SIP/") {
330        return Err(PacketError::InvalidHeader("invalid SIP version"));
331    }
332
333    buf.push_field(
334        &FIELD_DESCRIPTORS[FD_VERSION],
335        FieldValue::Str(version),
336        offset..offset + line_end,
337    );
338
339    // Status-Code (3 digits) and optional Reason-Phrase
340    let (code_str, reason) = match rest.find(' ') {
341        Some(sp) => (&rest[..sp], Some(&rest[sp + 1..])),
342        None => (rest, None),
343    };
344
345    // RFC 3261, Section 7.2 — Status-Code is exactly 3 digits
346    if code_str.len() != 3 || !code_str.bytes().all(|b| b.is_ascii_digit()) {
347        return Err(PacketError::InvalidHeader("invalid status code format"));
348    }
349
350    let code: u16 = code_str
351        .parse()
352        .map_err(|_| PacketError::InvalidHeader("invalid status code"))?;
353
354    // Be conservative: only accept standard SIP/HTTP-style ranges
355    if !(100..=699).contains(&code) {
356        return Err(PacketError::InvalidHeader("status code out of range"));
357    }
358
359    buf.push_field(
360        &FIELD_DESCRIPTORS[FD_STATUS_CODE],
361        FieldValue::U16(code),
362        offset..offset + line_end,
363    );
364
365    if let Some(reason) = reason {
366        if !reason.is_empty() {
367            buf.push_field(
368                &FIELD_DESCRIPTORS[FD_REASON_PHRASE],
369                FieldValue::Str(reason),
370                offset..offset + line_end,
371            );
372        }
373    }
374
375    parse_remaining_headers(data, offset, line_end, buf)
376}
377
378// ---------------------------------------------------------------------------
379// Header helpers
380// ---------------------------------------------------------------------------
381
382/// Parse header fields using `httparse::parse_headers`.
383fn parse_headers<'pkt>(
384    header_data: &'pkt [u8],
385    base_offset: usize,
386    line_end: usize,
387    buf: &mut DissectBuffer<'pkt>,
388) -> Result<usize, PacketError> {
389    let mut headers_buf = [httparse::EMPTY_HEADER; MAX_HEADERS];
390
391    match httparse::parse_headers(header_data, &mut headers_buf) {
392        Ok(httparse::Status::Complete((len, headers))) => {
393            build_header_fields(header_data, base_offset + line_end, headers, buf)?;
394            Ok(len)
395        }
396        Ok(httparse::Status::Partial) => Err(PacketError::Truncated {
397            expected: header_data.len() + 1,
398            actual: header_data.len(),
399        }),
400        Err(_) => Err(PacketError::InvalidHeader("invalid SIP header")),
401    }
402}
403
404/// Convert httparse headers into container fields in the buffer, with OWS trimming.
405fn build_header_fields<'pkt>(
406    data: &'pkt [u8],
407    offset: usize,
408    headers: &[httparse::Header<'pkt>],
409    buf: &mut DissectBuffer<'pkt>,
410) -> Result<(), PacketError> {
411    if headers.is_empty() {
412        return Ok(());
413    }
414
415    // Compute the overall headers range
416    // SAFETY of indexing: `is_empty()` check above guarantees at least one header.
417    let first_header = &headers[0];
418    let last_header = &headers[headers.len() - 1];
419    let first_name_start = str_offset(data, first_header.name)? + offset;
420    let last_value_end = slice_offset(data, last_header.value)? + last_header.value.len() + offset;
421
422    let array_idx = buf.begin_container(
423        &FIELD_DESCRIPTORS[FD_HEADERS],
424        FieldValue::Array(0..0),
425        first_name_start..last_value_end,
426    );
427
428    for header in headers {
429        let name = header.name;
430        let trimmed_value = trim_ows(header.value);
431        let value_str = core::str::from_utf8(trimmed_value)
432            .map_err(|_| PacketError::InvalidHeader("header value is not valid UTF-8"))?;
433
434        // Compute byte range relative to data slice, then add offset
435        let name_start = str_offset(data, name)?;
436        let value_end = slice_offset(data, header.value)? + header.value.len();
437        let header_range = offset + name_start..offset + value_end;
438
439        let obj_idx =
440            buf.begin_container(&FD_HEADER, FieldValue::Object(0..0), header_range.clone());
441        buf.push_field(
442            &HEADER_CHILDREN[HC_NAME],
443            FieldValue::Str(name),
444            header_range.clone(),
445        );
446        buf.push_field(
447            &HEADER_CHILDREN[HC_VALUE],
448            FieldValue::Str(value_str),
449            header_range,
450        );
451        buf.end_container(obj_idx);
452    }
453
454    buf.end_container(array_idx);
455
456    Ok(())
457}
458
459/// Extract the value of a named header from the buffer's fields during construction.
460fn extract_header_value<'pkt>(buf: &DissectBuffer<'pkt>, header_name: &str) -> Option<&'pkt str> {
461    let layer = buf.layers().last()?;
462    let start = layer.field_range.start as usize;
463    let fields = &buf.fields()[start..];
464    let headers_field = fields.iter().find(|f| f.name() == "headers")?;
465    let array_range = match &headers_field.value {
466        FieldValue::Array(r) => r,
467        _ => return None,
468    };
469
470    let children = buf.nested_fields(array_range);
471    for field in children {
472        if let FieldValue::Object(ref obj_range) = field.value {
473            let obj_fields = buf.nested_fields(obj_range);
474            let name_field = obj_fields.iter().find(|f| f.name() == "name")?;
475            let value_field = obj_fields.iter().find(|f| f.name() == "value")?;
476            if let FieldValue::Str(n) = &name_field.value {
477                if n.eq_ignore_ascii_case(header_name) {
478                    if let FieldValue::Str(v) = &value_field.value {
479                        return Some(v);
480                    }
481                }
482            }
483        }
484    }
485    None
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    // # RFC 3261 (SIP) Coverage
493    //
494    // | RFC Section | Description             | Test                                    |
495    // |-------------|-------------------------|-----------------------------------------|
496    // | 7           | Message Format          | parse_sip_invite_request                |
497    // | 7.1         | Request Line            | parse_sip_invite_request                |
498    // | 7.1         | Method token            | parse_sip_register_request              |
499    // | 7.1         | All standard methods    | parse_sip_methods_*                     |
500    // | 7.2         | Status Line             | parse_sip_200_ok_response               |
501    // | 7.2         | Reason Phrase            | parse_sip_trying_response               |
502    // | 7.3         | Header Fields           | parse_sip_request_with_headers          |
503    // | 7.3.1       | Header OWS trimming     | parse_sip_header_ows_trimming           |
504    // | 7.4         | Message Body            | parse_sip_invite_with_sdp_body          |
505    // | 20.14       | Content-Length           | parse_sip_invite_with_sdp_body          |
506    // | 20.15       | Content-Type             | parse_sip_invite_with_sdp_body          |
507    // | 7.3.3       | Compact header forms    | parse_sip_compact_content_length        |
508    // | -           | Truncated               | parse_sip_truncated                     |
509    // | -           | Invalid start-line      | parse_sip_invalid_request               |
510    // | -           | Body dispatch hint      | parse_sip_content_type_dispatch         |
511    // | -           | No body → End hint      | parse_sip_no_body_dispatch_end          |
512    // | -           | Offset handling         | parse_sip_with_offset                   |
513    // | -           | Dissector metadata       | dissector_metadata                      |
514
515    fn dissect(data: &[u8]) -> Result<DissectBuffer<'_>, PacketError> {
516        let dissector = SipDissector;
517        let mut buf = DissectBuffer::new();
518        dissector.dissect(data, &mut buf, 0)?;
519        Ok(buf)
520    }
521
522    fn dissect_err(data: &[u8]) -> PacketError {
523        let dissector = SipDissector;
524        let mut buf = DissectBuffer::new();
525        dissector.dissect(data, &mut buf, 0).unwrap_err()
526    }
527
528    #[test]
529    fn parse_sip_invite_request() {
530        let data = b"INVITE sip:bob@example.net SIP/2.0\r\n\
531                     Via: SIP/2.0/UDP pc33.example.com;branch=z9hG4bK776asdhds\r\n\
532                     To: Bob <sip:bob@example.net>\r\n\
533                     From: Alice <sip:alice@example.com>;tag=1928301774\r\n\
534                     Call-ID: a84b4c76e66710@pc33.example.com\r\n\
535                     CSeq: 314159 INVITE\r\n\
536                     Contact: <sip:alice@pc33.example.com>\r\n\
537                     Content-Length: 0\r\n\r\n";
538        let buf = dissect(data).unwrap();
539        let layer = buf.layer_by_name("SIP").unwrap();
540
541        assert_eq!(
542            buf.field_by_name(layer, "is_response").unwrap().value,
543            FieldValue::U8(0)
544        );
545        assert_eq!(
546            buf.field_by_name(layer, "method").unwrap().value,
547            FieldValue::Str("INVITE")
548        );
549        assert_eq!(
550            buf.field_by_name(layer, "uri").unwrap().value,
551            FieldValue::Str("sip:bob@example.net")
552        );
553        assert_eq!(
554            buf.field_by_name(layer, "version").unwrap().value,
555            FieldValue::Str("SIP/2.0")
556        );
557        assert!(buf.field_by_name(layer, "status_code").is_none());
558    }
559
560    #[test]
561    fn parse_sip_register_request() {
562        let data = b"REGISTER sip:registrar.example.net SIP/2.0\r\n\
563                     Via: SIP/2.0/UDP bobspc.example.net:5060;branch=z9hG4bKnashds7\r\n\
564                     To: Bob <sip:bob@example.net>\r\n\
565                     From: Bob <sip:bob@example.net>;tag=456248\r\n\
566                     Call-ID: 843817637684230@998sdasdh09\r\n\
567                     CSeq: 1826 REGISTER\r\n\
568                     Contact: <sip:bob@192.0.2.4>\r\n\
569                     Content-Length: 0\r\n\r\n";
570        let buf = dissect(data).unwrap();
571        let layer = buf.layer_by_name("SIP").unwrap();
572
573        assert_eq!(
574            buf.field_by_name(layer, "method").unwrap().value,
575            FieldValue::Str("REGISTER")
576        );
577        assert_eq!(
578            buf.field_by_name(layer, "uri").unwrap().value,
579            FieldValue::Str("sip:registrar.example.net")
580        );
581    }
582
583    #[test]
584    fn parse_sip_200_ok_response() {
585        let data = b"SIP/2.0 200 OK\r\n\
586                     Via: SIP/2.0/UDP server10.example.net;branch=z9hG4bKnashds8\r\n\
587                     To: Bob <sip:bob@example.net>;tag=2493k59kd\r\n\
588                     From: Alice <sip:alice@example.com>;tag=1928301774\r\n\
589                     Call-ID: a84b4c76e66710@pc33.example.com\r\n\
590                     CSeq: 314159 INVITE\r\n\
591                     Contact: <sip:bob@192.0.2.4>\r\n\
592                     Content-Length: 0\r\n\r\n";
593        let buf = dissect(data).unwrap();
594        let layer = buf.layer_by_name("SIP").unwrap();
595
596        assert_eq!(
597            buf.field_by_name(layer, "is_response").unwrap().value,
598            FieldValue::U8(1)
599        );
600        assert_eq!(
601            buf.field_by_name(layer, "version").unwrap().value,
602            FieldValue::Str("SIP/2.0")
603        );
604        assert_eq!(
605            buf.field_by_name(layer, "status_code").unwrap().value,
606            FieldValue::U16(200)
607        );
608        assert_eq!(
609            buf.field_by_name(layer, "reason_phrase").unwrap().value,
610            FieldValue::Str("OK")
611        );
612        assert!(buf.field_by_name(layer, "method").is_none());
613    }
614
615    #[test]
616    fn parse_sip_trying_response() {
617        let data = b"SIP/2.0 100 Trying\r\n\
618                     Via: SIP/2.0/UDP pc33.example.com;branch=z9hG4bK776asdhds\r\n\
619                     To: Bob <sip:bob@example.net>\r\n\
620                     From: Alice <sip:alice@example.com>;tag=1928301774\r\n\
621                     Call-ID: a84b4c76e66710@pc33.example.com\r\n\
622                     CSeq: 314159 INVITE\r\n\
623                     Content-Length: 0\r\n\r\n";
624        let buf = dissect(data).unwrap();
625        let layer = buf.layer_by_name("SIP").unwrap();
626
627        assert_eq!(
628            buf.field_by_name(layer, "status_code").unwrap().value,
629            FieldValue::U16(100)
630        );
631        assert_eq!(
632            buf.field_by_name(layer, "reason_phrase").unwrap().value,
633            FieldValue::Str("Trying")
634        );
635    }
636
637    #[test]
638    fn parse_sip_request_with_headers() {
639        let data = b"OPTIONS sip:carol@example.org SIP/2.0\r\n\
640                     Via: SIP/2.0/UDP pc33.example.com;branch=z9hG4bKhjhs8ass877\r\n\
641                     Max-Forwards: 70\r\n\
642                     To: <sip:carol@example.org>\r\n\
643                     From: Alice <sip:alice@example.com>;tag=1928301774\r\n\
644                     Call-ID: testcallid@pc33.example.com\r\n\
645                     CSeq: 63104 OPTIONS\r\n\
646                     Contact: <sip:alice@pc33.example.com>\r\n\
647                     Accept: application/sdp\r\n\
648                     Content-Length: 0\r\n\r\n";
649        let buf = dissect(data).unwrap();
650        let layer = buf.layer_by_name("SIP").unwrap();
651
652        let headers_field = buf.field_by_name(layer, "headers").unwrap();
653        let array_range = match &headers_field.value {
654            FieldValue::Array(r) => r,
655            _ => panic!("expected Array"),
656        };
657        let children = buf.nested_fields(array_range);
658        let objects: Vec<_> = children.iter().filter(|f| f.value.is_object()).collect();
659        // Via, Max-Forwards, To, From, Call-ID, CSeq, Contact, Accept, Content-Length
660        assert_eq!(objects.len(), 9);
661
662        if let FieldValue::Object(ref r) = objects[0].value {
663            let f = buf.nested_fields(r);
664            assert_eq!(f[0].value, FieldValue::Str("Via"));
665        }
666
667        // Check Accept header
668        if let FieldValue::Object(ref r) = objects[7].value {
669            let f = buf.nested_fields(r);
670            assert_eq!(f[0].value, FieldValue::Str("Accept"));
671            assert_eq!(f[1].value, FieldValue::Str("application/sdp"));
672        }
673    }
674
675    #[test]
676    fn parse_sip_invite_with_sdp_body() {
677        let sdp_body = b"v=0\r\n\
678                         o=alice 2890844526 2890844526 IN IP4 host.example.com\r\n\
679                         s=-\r\n\
680                         c=IN IP4 host.example.com\r\n\
681                         t=0 0\r\n\
682                         m=audio 49170 RTP/AVP 0\r\n\
683                         a=rtpmap:0 PCMU/8000\r\n";
684        let content_length = sdp_body.len();
685        let header = format!(
686            "INVITE sip:bob@example.net SIP/2.0\r\n\
687             Via: SIP/2.0/UDP pc33.example.com;branch=z9hG4bK776asdhds\r\n\
688             To: Bob <sip:bob@example.net>\r\n\
689             From: Alice <sip:alice@example.com>;tag=1928301774\r\n\
690             Call-ID: a84b4c76e66710@pc33.example.com\r\n\
691             CSeq: 314159 INVITE\r\n\
692             Contact: <sip:alice@pc33.example.com>\r\n\
693             Content-Type: application/sdp\r\n\
694             Content-Length: {content_length}\r\n\r\n"
695        );
696        let mut data = Vec::new();
697        data.extend_from_slice(header.as_bytes());
698        data.extend_from_slice(sdp_body);
699
700        let buf = dissect(&data).unwrap();
701        let layer = buf.layer_by_name("SIP").unwrap();
702
703        assert_eq!(
704            buf.field_by_name(layer, "content_length").unwrap().value,
705            FieldValue::U32(content_length as u32)
706        );
707        assert_eq!(
708            buf.field_by_name(layer, "content_type").unwrap().value,
709            FieldValue::Str("application/sdp")
710        );
711        // SIP layer covers headers only; body is left for the body dissector.
712        let header_len = header.len();
713        assert_eq!(layer.range, 0..header_len);
714    }
715
716    #[test]
717    fn parse_sip_truncated() {
718        let data = b"INVITE sip:";
719        assert!(matches!(dissect_err(data), PacketError::Truncated { .. }));
720    }
721
722    #[test]
723    fn parse_sip_truncated_headers() {
724        // Start-line complete but headers not terminated
725        let data = b"INVITE sip:bob@example.net SIP/2.0\r\nVia: SIP/2.0/UDP pc33.example.com";
726        assert!(matches!(dissect_err(data), PacketError::Truncated { .. }));
727    }
728
729    #[test]
730    fn parse_sip_truncated_body() {
731        let data = b"INVITE sip:bob@example.net SIP/2.0\r\nContent-Length: 100\r\n\r\nShort";
732        assert!(matches!(dissect_err(data), PacketError::Truncated { .. }));
733    }
734
735    #[test]
736    fn parse_sip_invalid_request() {
737        // Missing SP between method and URI (must be >= MIN_START_LINE_LEN bytes)
738        let data = b"INVALIDREQUESTLIN\r\n\r\n";
739        assert!(matches!(dissect_err(data), PacketError::InvalidHeader(_)));
740    }
741
742    #[test]
743    fn parse_sip_header_ows_trimming() {
744        let data = b"OPTIONS sip:carol@example.org SIP/2.0\r\n\
745                     Via:   SIP/2.0/UDP pc33.example.com  \r\n\
746                     Content-Length: 0\r\n\r\n";
747        let buf = dissect(data).unwrap();
748        let layer = buf.layer_by_name("SIP").unwrap();
749
750        let headers_field = buf.field_by_name(layer, "headers").unwrap();
751        if let FieldValue::Array(ref r) = headers_field.value {
752            let children = buf.nested_fields(r);
753            let obj = children.iter().find(|f| f.value.is_object()).unwrap();
754            if let FieldValue::Object(ref obj_r) = obj.value {
755                let f = buf.nested_fields(obj_r);
756                assert_eq!(f[1].value, FieldValue::Str("SIP/2.0/UDP pc33.example.com"));
757            }
758        }
759    }
760
761    #[test]
762    fn parse_sip_content_type_dispatch() {
763        let sdp = b"v=0\r\n";
764        let cl = sdp.len();
765        let header = format!(
766            "INVITE sip:bob@example.net SIP/2.0\r\n\
767             Content-Type: application/sdp\r\n\
768             Content-Length: {cl}\r\n\r\n"
769        );
770        let mut data = Vec::new();
771        data.extend_from_slice(header.as_bytes());
772        data.extend_from_slice(sdp);
773
774        let dissector = SipDissector;
775        let mut buf = DissectBuffer::new();
776        let result = dissector.dissect(&data, &mut buf, 0).unwrap();
777
778        assert_eq!(result.next, DispatchHint::ByContentType("application/sdp"));
779    }
780
781    #[test]
782    fn parse_sip_content_type_with_params_dispatch() {
783        // Content-Type with parameters should strip them for dispatch
784        let body = b"body";
785        let cl = body.len();
786        let header = format!(
787            "INVITE sip:bob@example.net SIP/2.0\r\n\
788             Content-Type: application/sdp; charset=utf-8\r\n\
789             Content-Length: {cl}\r\n\r\n"
790        );
791        let mut data = Vec::new();
792        data.extend_from_slice(header.as_bytes());
793        data.extend_from_slice(body);
794
795        let dissector = SipDissector;
796        let mut buf = DissectBuffer::new();
797        let result = dissector.dissect(&data, &mut buf, 0).unwrap();
798
799        assert_eq!(result.next, DispatchHint::ByContentType("application/sdp"));
800    }
801
802    #[test]
803    fn parse_sip_no_body_dispatch_end() {
804        let data = b"OPTIONS sip:carol@example.org SIP/2.0\r\n\
805                     Content-Length: 0\r\n\r\n";
806        let dissector = SipDissector;
807        let mut buf = DissectBuffer::new();
808        let result = dissector.dissect(data, &mut buf, 0).unwrap();
809
810        assert_eq!(result.next, DispatchHint::End);
811    }
812
813    #[test]
814    fn parse_sip_with_offset() {
815        let data = b"OPTIONS sip:carol@example.org SIP/2.0\r\n\
816                     Content-Length: 0\r\n\r\n";
817        let dissector = SipDissector;
818        let mut buf = DissectBuffer::new();
819        let result = dissector.dissect(data, &mut buf, 42).unwrap();
820
821        let layer = buf.layer_by_name("SIP").unwrap();
822        assert_eq!(layer.range.start, 42);
823        assert_eq!(layer.range.end, 42 + data.len());
824        assert_eq!(result.bytes_consumed, data.len());
825    }
826
827    #[test]
828    fn parse_sip_compact_content_length() {
829        // RFC 3261, Section 7.3.3 — compact form "l" for Content-Length
830        let data = b"OPTIONS sip:carol@example.org SIP/2.0\r\n\
831                     l: 0\r\n\r\n";
832        let buf = dissect(data).unwrap();
833        let layer = buf.layer_by_name("SIP").unwrap();
834
835        assert_eq!(
836            buf.field_by_name(layer, "content_length").unwrap().value,
837            FieldValue::U32(0)
838        );
839    }
840
841    #[test]
842    fn parse_sip_methods_ack() {
843        let data = b"ACK sip:bob@example.net SIP/2.0\r\nContent-Length: 0\r\n\r\n";
844        let buf = dissect(data).unwrap();
845        let layer = buf.layer_by_name("SIP").unwrap();
846        assert_eq!(
847            buf.field_by_name(layer, "method").unwrap().value,
848            FieldValue::Str("ACK")
849        );
850    }
851
852    #[test]
853    fn parse_sip_methods_bye() {
854        let data = b"BYE sip:bob@example.net SIP/2.0\r\nContent-Length: 0\r\n\r\n";
855        let buf = dissect(data).unwrap();
856        let layer = buf.layer_by_name("SIP").unwrap();
857        assert_eq!(
858            buf.field_by_name(layer, "method").unwrap().value,
859            FieldValue::Str("BYE")
860        );
861    }
862
863    #[test]
864    fn parse_sip_methods_cancel() {
865        let data = b"CANCEL sip:bob@example.net SIP/2.0\r\nContent-Length: 0\r\n\r\n";
866        let buf = dissect(data).unwrap();
867        let layer = buf.layer_by_name("SIP").unwrap();
868        assert_eq!(
869            buf.field_by_name(layer, "method").unwrap().value,
870            FieldValue::Str("CANCEL")
871        );
872    }
873
874    #[test]
875    fn parse_sip_response_no_reason() {
876        // Some implementations may omit the reason phrase
877        let data = b"SIP/2.0 200\r\nContent-Length: 0\r\n\r\n";
878        let buf = dissect(data).unwrap();
879        let layer = buf.layer_by_name("SIP").unwrap();
880
881        assert_eq!(
882            buf.field_by_name(layer, "status_code").unwrap().value,
883            FieldValue::U16(200)
884        );
885        assert!(buf.field_by_name(layer, "reason_phrase").is_none());
886    }
887
888    #[test]
889    fn dissector_metadata() {
890        let d = SipDissector;
891        assert_eq!(d.name(), "Session Initiation Protocol");
892        assert_eq!(d.short_name(), "SIP");
893        assert!(!d.field_descriptors().is_empty());
894    }
895
896    #[test]
897    fn header_container_descriptor_distinct_from_inner_name() {
898        // The per-header Object container must use a descriptor distinct
899        // from the inner `name` child so that the outer display label does
900        // not collide with the child's "Name" label.
901        let data = b"OPTIONS sip:carol@example.com SIP/2.0\r\n\
902                     Via: SIP/2.0/UDP pc.example.com\r\n\
903                     Content-Length: 0\r\n\r\n";
904        let buf = dissect(data).unwrap();
905
906        let (idx, field) = buf
907            .fields()
908            .iter()
909            .enumerate()
910            .find(|(_, f)| f.name() == "header")
911            .expect("header container not found");
912        assert!(matches!(field.value, FieldValue::Object(_)));
913        assert_eq!(field.display_name(), "Header");
914        assert_eq!(buf.resolve_container_display_name(idx as u32), None);
915    }
916}