From 11d93c78141dc60c3edbb17090ecee008a34135c Mon Sep 17 00:00:00 2001 From: Gareth Sylvester-Bradley Date: Tue, 28 Jul 2026 15:43:56 +0100 Subject: [PATCH] Expose SDP grammar converters for application-defined attributes session_description already accepted custom attribute_converters, but the helper converters needed to build them were internal, and the lifetime of those converters relative to the returned grammar was undocumented. Declare the reusable converters and generators, document the by-reference capture and static-initialization constraints, and add a test that extends the default grammar with a=x-example-foo. --- Development/sdp/sdp_grammar.cpp | 4 +- Development/sdp/sdp_grammar.h | 44 +++++++++++++++ Development/sdp/test/sdp_test.cpp | 91 +++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 2 deletions(-) diff --git a/Development/sdp/sdp_grammar.cpp b/Development/sdp/sdp_grammar.cpp index 25b559e23..cc925a76d 100644 --- a/Development/sdp/sdp_grammar.cpp +++ b/Development/sdp/sdp_grammar.cpp @@ -139,7 +139,7 @@ namespace sdp }; } - converter array_converter(const converter& converter, const std::string& delimiter = " ") + converter array_converter(const converter& converter, const std::string& delimiter) { return{ [=](const web::json::value& v) { @@ -211,7 +211,7 @@ namespace sdp // in other RFCs and SMPTE standards are inconsistent, so allow additional whitespace const converter named_values_converter = array_converter(key_value_converter('=', { sdp::fields::name, string_converter }, { sdp::fields::value, string_converter }), "; ", "[ \\t]*(;[ \\t]*|$)"); - converter object_converter(const std::vector>& field_converters, const std::string& delimiter = " ") + converter object_converter(const std::vector>& field_converters, const std::string& delimiter) { return{ [=](const web::json::value& v) { diff --git a/Development/sdp/sdp_grammar.h b/Development/sdp/sdp_grammar.h index 0ac97c362..0c3bac627 100644 --- a/Development/sdp/sdp_grammar.h +++ b/Development/sdp/sdp_grammar.h @@ -26,6 +26,45 @@ namespace sdp parser parse; }; + // converters for the value types used by this grammar, and generators for compound + // values, which may also be used to define additional attribute converters + // see https://tools.ietf.org/html/rfc4566#section-9 + // none of these, or get_default_attribute_converters, default_attribute_converter + // or session_description below, are safe to use during static initialization; + // prefer a function-local static for application-defined grammars + + // + extern const converter string_converter; + + // a number, e.g. in "a=framerate:" + extern const converter number_converter; + + // 1*DIGIT + extern const converter digits_converter; + + // 1*DIGIT, represented as a numeric string, for values which may exceed the range + // of a number, e.g. + extern const converter big_digits_converter; + + // a space-separated list of + extern const converter strings_converter; + + // a semicolon-separated list of [=], e.g. + extern const converter named_values_converter; + + // [] + converter key_value_converter(char separator, const std::pair& key_converter, const std::pair& value_converter); + + // a delimited list of values, represented as a json array + converter array_converter(const converter& converter, const std::string& delimiter = " "); + + // identical to above except that parse_delimiter is a regex pattern + converter array_converter(const converter& converter, const std::string& format_delimiter, const std::string& parse_delimiter); + + // a delimited sequence of values, represented as a json object with the specified field names + // an empty field name indicates the values of the converted json object are merged into the result + converter object_converter(const std::vector>& field_converters, const std::string& delimiter = " "); + // "An SDP session description consists of a number of lines of text of // the form: = // where MUST be exactly one case-significant character and @@ -80,7 +119,12 @@ namespace sdp // See https://tools.ietf.org/html/rfc4566#section-6 attribute_converters get_default_attribute_converters(); + // converter for an attribute with no specific converter, which treats the as an opaque + // and an attribute with no as a property attribute + extern const converter default_attribute_converter; + // construct a grammar with the specified attribute converters + // the converters and default_converter are captured by reference and must outlive the returned grammar description session_description(const attribute_converters& converters, const converter& default_converter); } diff --git a/Development/sdp/test/sdp_test.cpp b/Development/sdp/test/sdp_test.cpp index ee6cf52af..23fec2cdf 100644 --- a/Development/sdp/test/sdp_test.cpp +++ b/Development/sdp/test/sdp_test.cpp @@ -3,6 +3,7 @@ #include "bst/test/test.h" #include "sdp/json.h" +#include "sdp/sdp_grammar.h" //////////////////////////////////////////////////////////////////////////////////////////// BST_TEST_CASE(testSdpRoundtrip) @@ -1120,3 +1121,93 @@ a=mid:SECONDARY } while (!expected.fail() && !actual.fail()); } } + +//////////////////////////////////////////////////////////////////////////////////////////// +namespace +{ + // an example application-defined attribute + // a=x-example-foo: + const utility::string_t example_foo{ U("x-example-foo") }; + + namespace example_fields + { + const web::json::field_as_integer format{ U("format") }; + const web::json::field_as_string bar{ U("bar") }; + } + + // session_description captures converters by reference, so they must outlive + // the returned grammar; a function-local static is the natural application pattern + const sdp::grammar::attribute_converters& example_attribute_converters() + { + static const auto converters = [] { + auto converters = sdp::grammar::get_default_attribute_converters(); + converters[example_foo] = sdp::grammar::object_converter({ + { example_fields::format, sdp::grammar::digits_converter }, + { example_fields::bar, sdp::grammar::string_converter } + }); + return converters; + }(); + return converters; + } + + const sdp::grammar::description& example_grammar() + { + static const auto grammar = sdp::grammar::session_description( + example_attribute_converters(), + sdp::grammar::default_attribute_converter + ); + return grammar; + } +} + +BST_TEST_CASE(testSdpApplicationDefinedAttributes) +{ + const auto& grammar = example_grammar(); + + const std::string test_sdp = R"(v=0 +o=- 3745911798 3745911798 IN IP4 192.168.9.142 +s=Example Sender 1 (Video) +t=0 0 +a=x-example-foo:0 session-level +m=video 50020 RTP/AVP 96 +c=IN IP4 239.22.142.1/32 +a=rtpmap:96 raw/90000 +a=x-example-foo:96 media-level +a=x-example-bar:unknown attributes are still handled by the default converter +a=recvonly +)"; + + auto session_description = sdp::parse_session_description(test_sdp, grammar); + + auto& session_attributes = sdp::fields::attributes(session_description).as_array(); + auto session_foo = sdp::find_name(session_attributes, example_foo); + BST_REQUIRE(session_attributes.end() != session_foo); + BST_REQUIRE_EQUAL(0, example_fields::format(sdp::fields::value(*session_foo))); + BST_REQUIRE_EQUAL(U("session-level"), example_fields::bar(sdp::fields::value(*session_foo))); + + auto& media_attributes = sdp::fields::attributes(sdp::fields::media_descriptions(session_description).at(0)).as_array(); + auto media_foo = sdp::find_name(media_attributes, example_foo); + BST_REQUIRE(media_attributes.end() != media_foo); + BST_REQUIRE_EQUAL(96, example_fields::format(sdp::fields::value(*media_foo))); + BST_REQUIRE_EQUAL(U("media-level"), example_fields::bar(sdp::fields::value(*media_foo))); + + auto media_bar = sdp::find_name(media_attributes, U("x-example-bar")); + BST_REQUIRE(media_attributes.end() != media_bar); + BST_REQUIRE_EQUAL(U("unknown attributes are still handled by the default converter"), sdp::fields::value(*media_bar).as_string()); + + auto recvonly = sdp::find_name(media_attributes, sdp::attributes::recvonly); + BST_REQUIRE(media_attributes.end() != recvonly); + BST_REQUIRE(sdp::fields::value(*recvonly).is_null()); + + auto test_sdp2 = sdp::make_session_description(session_description, grammar); + std::istringstream expected(test_sdp), actual(test_sdp2); + do + { + std::string expected_line, actual_line; + std::getline(expected, expected_line); + std::getline(actual, actual_line); + // CR cannot appear in a raw string literal, so remove it from the actual line + if (!actual_line.empty() && '\r' == actual_line.back()) actual_line.pop_back(); + BST_CHECK_EQUAL(expected_line, actual_line); + } while (!expected.fail() && !actual.fail()); +}