#include "vdm/rules/filename.hpp" #include #include "vtest.hpp" using vdm::rules::sanitize_filename; VT_TEST(filename_passthrough_when_already_clean) { VT_CHECK_EQ(sanitize_filename("report.pdf"), std::string("report.pdf")); } VT_TEST(filename_strips_path_separators) { VT_CHECK_EQ(sanitize_filename("a/b\\c.txt"), std::string("abc.txt")); } VT_TEST(filename_collapses_dotdot_after_separator_strip) { // "../../etc/passwd" -> separators stripped, then the resulting ".." runs collapse to // a single '.', which strip-trailing-dot then removes entirely. auto out = sanitize_filename("../../etc/passwd"); VT_CHECK(out.find("..") == std::string::npos); } VT_TEST(filename_strips_control_bytes) { std::string raw = "bad"; raw.push_back('\0'); raw += "name.txt"; auto out = sanitize_filename(raw); VT_CHECK_EQ(out, std::string("badname.txt")); } VT_TEST(filename_replaces_ntfs_illegal_chars) { VT_CHECK_EQ(sanitize_filename("ac:d\"e|f?g*h.txt"), std::string("a_b_c_d_e_f_g_h.txt")); } VT_TEST(filename_strips_trailing_dot_and_space) { VT_CHECK_EQ(sanitize_filename("name. "), std::string("name")); } VT_TEST(filename_empty_falls_back_to_download) { VT_CHECK_EQ(sanitize_filename(""), std::string("download")); } VT_TEST(filename_all_stripped_falls_back_to_download) { VT_CHECK_EQ(sanitize_filename("/\\"), std::string("download")); } VT_TEST(filename_reserved_device_name_bare) { VT_CHECK_EQ(sanitize_filename("CON"), std::string("CON_")); VT_CHECK_EQ(sanitize_filename("con"), std::string("con_")); } VT_TEST(filename_reserved_device_name_with_extension) { VT_CHECK_EQ(sanitize_filename("NUL.txt"), std::string("NUL_.txt")); VT_CHECK_EQ(sanitize_filename("com3.tar.gz"), std::string("com3_.tar.gz")); } VT_TEST(filename_reserved_device_name_not_a_false_positive) { // "CONTEST" is not "CON" — must not get mangled. VT_CHECK_EQ(sanitize_filename("CONTEST.txt"), std::string("CONTEST.txt")); VT_CHECK_EQ(sanitize_filename("COM99.txt"), std::string("COM99.txt")); // not COM1-9 } VT_TEST(filename_truncates_long_name_keeping_extension) { std::string stem(500, 'a'); auto out = sanitize_filename(stem + ".txt", 255); VT_CHECK(out.size() <= 255); VT_CHECK(out.ends_with(".txt")); } VT_TEST(filename_truncation_is_utf8_safe) { // Each "é" is 2 bytes (C3 A9); a 5-byte budget can fit 2 whole codepoints (4 bytes) but // not a 3rd (needs 6) -- an unguarded byte-length cut at 5 would split the 3rd // codepoint's C3 from its A9, leaving a dangling lead byte. std::string stem; for (int i = 0; i < 20; ++i) stem += "\xC3\xA9"; auto out = sanitize_filename(stem, 5); VT_CHECK_EQ(out, std::string("\xC3\xA9\xC3\xA9")); // 2 whole codepoints, 4 bytes } VT_TEST(filename_preserves_non_ascii) { VT_CHECK_EQ(sanitize_filename("caf\xC3\xA9.pdf"), std::string("caf\xC3\xA9.pdf")); }