#include "vdm/net/content_disposition.hpp" #include #include #include "vtest.hpp" using vdm::net::ContentDisposition; using vdm::net::parse_content_disposition; using Type = vdm::net::ContentDisposition::Type; namespace { // "€" is U+20AC -> UTF-8 E2 82 AC. "£" is U+00A3 -> UTF-8 C2 A3. const std::string kEuro = "\xE2\x82\xAC"; const std::string kPound = "\xC2\xA3"; const std::string kEAcute = "\xC3\xA9"; // é U+00E9 } // namespace VT_TEST(cd_plain_quoted) { auto cd = parse_content_disposition(R"(attachment; filename="report.pdf")"); VT_CHECK(cd.type == Type::attachment); VT_CHECK_EQ(cd.filename, std::string("report.pdf")); VT_CHECK(!cd.filename_from_ext); } VT_TEST(cd_plain_token_unquoted) { auto cd = parse_content_disposition("attachment; filename=report.pdf"); VT_CHECK_EQ(cd.filename, std::string("report.pdf")); } VT_TEST(cd_inline_no_filename) { auto cd = parse_content_disposition("inline"); VT_CHECK(cd.type == Type::inline_); VT_CHECK(!cd.has_filename()); } VT_TEST(cd_rfc5987_utf8_ext_value) { auto cd = parse_content_disposition("attachment; filename*=UTF-8''%e2%82%ac%20rates.pdf"); VT_CHECK_EQ(cd.filename, kEuro + " rates.pdf"); VT_CHECK(cd.filename_from_ext); } VT_TEST(cd_prefers_ext_over_plain) { auto cd = parse_content_disposition( R"(attachment; filename="EURO rates.pdf"; filename*=UTF-8''%e2%82%ac%20rates.pdf)"); VT_CHECK_EQ(cd.filename, kEuro + " rates.pdf"); VT_CHECK(cd.filename_from_ext); } VT_TEST(cd_rfc5987_latin1_ext_value) { // %A3 = £ in ISO-8859-1 auto cd = parse_content_disposition("attachment; filename*=ISO-8859-1''%A3rates.pdf"); VT_CHECK_EQ(cd.filename, kPound + "rates.pdf"); } VT_TEST(cd_legacy_rfc2047_base64) { // base64(" rates.pdf") with euro as UTF-8 // "€ rates.pdf" -> bytes E2 82 AC 20 72 61 74 65 73 2E 70 64 66 -> base64: auto cd = parse_content_disposition(R"(attachment; filename="=?UTF-8?B?4oKsIHJhdGVzLnBkZg==?=")"); VT_CHECK_EQ(cd.filename, kEuro + " rates.pdf"); } VT_TEST(cd_legacy_rfc2047_qencoded_latin1) { // =?ISO-8859-1?Q?=A3rates.pdf?= -> £rates.pdf auto cd = parse_content_disposition(R"(attachment; filename="=?ISO-8859-1?Q?=A3rates.pdf?=")"); VT_CHECK_EQ(cd.filename, kPound + "rates.pdf"); } VT_TEST(cd_raw_utf8_bytes_in_quotes) { std::string h = "attachment; filename=\"caf" + kEAcute + ".txt\""; auto cd = parse_content_disposition(h); VT_CHECK_EQ(cd.filename, "caf" + kEAcute + ".txt"); } VT_TEST(cd_raw_latin1_byte_in_quotes) { // 0xE9 is 'é' in Latin-1; not valid UTF-8 alone -> transcoded std::string h = "attachment; filename=\"caf\xE9.txt\""; auto cd = parse_content_disposition(h); VT_CHECK_EQ(cd.filename, "caf" + kEAcute + ".txt"); } VT_TEST(cd_strips_path_components) { VT_CHECK_EQ(parse_content_disposition(R"(attachment; filename="../../etc/passwd")").filename, std::string("passwd")); // real Windows paths in the wild use unescaped backslashes as separators VT_CHECK_EQ(parse_content_disposition(R"(attachment; filename="C:\Windows\evil.exe")").filename, std::string("evil.exe")); // a base64 payload can contain '/', so decode must happen before path stripping VT_CHECK_EQ(parse_content_disposition(R"(attachment; filename="=?UTF-8?B?Li4vLi4vc2VjcmV0?=")") .filename, std::string("secret")); // decodes to "../../secret", then stripped } VT_TEST(cd_quoted_dquote_escape) { // \" is the one escape we resolve, so a quote can appear mid-name auto cd = parse_content_disposition(R"(attachment; filename="quote\"here.txt")"); VT_CHECK_EQ(cd.filename, std::string("quote\"here.txt")); } VT_TEST(cd_semicolon_inside_quotes_is_not_a_separator) { auto cd = parse_content_disposition(R"(attachment; filename="a;b;c.txt")"); VT_CHECK_EQ(cd.filename, std::string("a;b;c.txt")); } VT_TEST(cd_form_data) { auto cd = parse_content_disposition(R"(form-data; name="file"; filename="upload.bin")"); VT_CHECK(cd.type == Type::form_data); VT_CHECK_EQ(cd.filename, std::string("upload.bin")); } VT_TEST(cd_rfc2231_continuations) { auto cd = parse_content_disposition( "attachment; filename*0*=UTF-8''%e2%82%ac; filename*1*=%20rates; filename*2=.pdf"); VT_CHECK_EQ(cd.filename, kEuro + " rates.pdf"); } VT_TEST(cd_empty_and_garbage_do_not_crash) { VT_CHECK(!parse_content_disposition("").has_filename()); VT_CHECK(!parse_content_disposition(";;;;").has_filename()); VT_CHECK(!parse_content_disposition("attachment;").has_filename()); VT_CHECK(!parse_content_disposition(R"(attachment; filename=)").has_filename()); VT_CHECK(!parse_content_disposition(R"(attachment; filename="")").has_filename()); // truncated ext-value auto cd = parse_content_disposition("attachment; filename*=UTF-8''%e2%82"); VT_CHECK(cd.type == Type::attachment); // no crash; filename is whatever fell out // truncated encoded-word auto trunc = parse_content_disposition(R"(attachment; filename="=?UTF-8?B?4oKs")"); (void)trunc; } VT_TEST(cd_bad_percent_escapes_in_ext_value) { // stray % and non-hex digits are emitted literally, no crash auto cd = parse_content_disposition("attachment; filename*=UTF-8''%ZZ%%file%2"); VT_CHECK(cd.type == Type::attachment); } VT_TEST(cd_case_insensitive_keys_and_type) { auto cd = parse_content_disposition(R"(ATTACHMENT; FileName="x.txt")"); VT_CHECK(cd.type == Type::attachment); VT_CHECK_EQ(cd.filename, std::string("x.txt")); } VT_TEST(cd_unknown_type_is_other) { auto cd = parse_content_disposition(R"(signal; filename="x.txt")"); VT_CHECK(cd.type == Type::other); VT_CHECK_EQ(cd.filename, std::string("x.txt")); }