// Copyright (c) 2025 YAMS Contributors // SPDX-License-Identifier: GPL-3.0-or-later #include #include #include #include #include #include #include using namespace yams::magic; // Helper to create byte vectors from hex strings std::vector hex_to_vector(const std::string& hex) { std::vector result; for (size_t i = 0; i < hex.length(); i += 2) { uint8_t byte = std::stoi(hex.substr(i, 2), nullptr, 16); result.push_back(byte); } return result; } //============================================================================= // Pattern Matching Tests //============================================================================= TEST_CASE("Image format detection", "[core][magic][image]") { SECTION("JPEG detection") { auto jpeg_data = hex_to_vector("FFD8FF"); REQUIRE(detect_mime_type(std::span(jpeg_data)) == "image/jpeg"); } SECTION("PNG detection") { auto png_data = hex_to_vector("89504E470D0A1A0A"); REQUIRE(detect_mime_type(std::span(png_data)) == "image/png"); } SECTION("GIF87a detection") { auto gif87_data = hex_to_vector("474946383761"); REQUIRE(detect_mime_type(std::span(gif87_data)) == "image/gif"); } SECTION("GIF89a detection") { auto gif89_data = hex_to_vector("474946383961"); REQUIRE(detect_mime_type(std::span(gif89_data)) == "image/gif"); } SECTION("BMP detection") { auto bmp_data = hex_to_vector("424D"); REQUIRE(detect_mime_type(std::span(bmp_data)) == "image/bmp"); } SECTION("TIFF detection") { auto tiff_data = hex_to_vector("49492A00"); REQUIRE(detect_mime_type(std::span(tiff_data)) == "image/tiff"); } } TEST_CASE("Archive format detection", "[core][magic][archive]") { SECTION("PDF detection") { auto pdf_data = hex_to_vector("255044462D"); REQUIRE(detect_mime_type(std::span(pdf_data)) == "application/pdf"); } SECTION("ZIP detection") { auto zip_data = hex_to_vector("504B0304"); REQUIRE(detect_mime_type(std::span(zip_data)) == "application/zip"); } SECTION("GZIP detection") { auto gzip_data = hex_to_vector("1F8B"); REQUIRE(detect_mime_type(std::span(gzip_data)) == "application/gzip"); } SECTION("BZIP2 detection") { auto bzip2_data = hex_to_vector("425A68"); REQUIRE(detect_mime_type(std::span(bzip2_data)) == "application/x-bzip2"); } SECTION("7-Zip detection") { auto sevenz_data = hex_to_vector("377ABCAF271C"); REQUIRE(detect_mime_type(std::span(sevenz_data)) == "application/x-7z-compressed"); } SECTION("RAR detection") { auto rar_data = hex_to_vector("526172211A0700"); REQUIRE(detect_mime_type(std::span(rar_data)) == "application/x-rar-compressed"); } SECTION("XZ detection") { auto xz_data = hex_to_vector("FD377A585A00"); REQUIRE(detect_mime_type(std::span(xz_data)) == "application/x-xz"); } } TEST_CASE("Executable format detection", "[core][magic][executable]") { SECTION("ELF detection") { auto elf_data = hex_to_vector("7F454C46"); REQUIRE(detect_mime_type(std::span(elf_data)) == "application/x-executable"); } SECTION("Windows PE detection") { auto pe_data = hex_to_vector("4D5A"); REQUIRE(detect_mime_type(std::span(pe_data)) == "application/x-msdownload"); } SECTION("Java class file detection") { // Java class file: magic (CAFEBABE) + minor version (0000) + major version (0034 = Java 8) // The version bytes distinguish it from Mach-O fat binary which also uses CAFEBABE auto class_data = hex_to_vector("CAFEBABE00000034"); REQUIRE(detect_mime_type(std::span(class_data)) == "application/java-archive"); } } TEST_CASE("Audio format detection", "[core][magic][audio]") { SECTION("MP3 detection") { auto mp3_data = hex_to_vector("494433"); REQUIRE(detect_mime_type(std::span(mp3_data)) == "audio/mpeg"); } SECTION("FLAC detection") { auto flac_data = hex_to_vector("664C6143"); REQUIRE(detect_mime_type(std::span(flac_data)) == "audio/flac"); } SECTION("Ogg detection") { auto ogg_data = hex_to_vector("4F676753"); REQUIRE(detect_mime_type(std::span(ogg_data)) == "audio/ogg"); } } TEST_CASE("Video format detection", "[core][magic][video]") { SECTION("MP4 detection") { auto mp4_data = hex_to_vector("000000146674797069736F6D"); REQUIRE(detect_mime_type(std::span(mp4_data)) == "video/mp4"); } SECTION("Matroska detection") { auto mkv_data = hex_to_vector("1A45DFA3"); REQUIRE(detect_mime_type(std::span(mkv_data)) == "video/x-matroska"); } SECTION("FLV detection") { auto flv_data = hex_to_vector("464C56"); REQUIRE(detect_mime_type(std::span(flv_data)) == "video/x-flv"); } } TEST_CASE("Font format detection", "[core][magic][font]") { SECTION("TrueType detection") { auto ttf_data = hex_to_vector("0001000000"); REQUIRE(detect_mime_type(std::span(ttf_data)) == "font/ttf"); } SECTION("OpenType detection") { auto otf_data = hex_to_vector("4F54544F"); REQUIRE(detect_mime_type(std::span(otf_data)) == "font/otf"); } SECTION("WOFF detection") { auto woff_data = hex_to_vector("774F4646"); REQUIRE(detect_mime_type(std::span(woff_data)) == "font/woff"); } } TEST_CASE("Database format detection", "[core][magic][database]") { auto sqlite_data = hex_to_vector("53514C69746520666F726D6174"); REQUIRE(detect_mime_type(std::span(sqlite_data)) == "application/x-sqlite3"); } TEST_CASE("Text encoding detection", "[core][magic][text]") { SECTION("UTF-8 BOM detection") { auto utf8_bom = hex_to_vector("EFBBBF"); REQUIRE(detect_mime_type(std::span(utf8_bom)) == "text/plain"); } SECTION("UTF-16 BE BOM detection") { auto utf16be_bom = hex_to_vector("FEFF"); REQUIRE(detect_mime_type(std::span(utf16be_bom)) == "text/plain"); } SECTION("UTF-16 LE BOM detection") { auto utf16le_bom = hex_to_vector("FFFE"); REQUIRE(detect_mime_type(std::span(utf16le_bom)) == "text/plain"); } } //============================================================================= // Prune Category Tests //============================================================================= TEST_CASE("Prune category detection", "[core][magic][prune]") { SECTION("Build output directories") { REQUIRE(matchesPruneGroup(getPruneCategory("build/output.o"), "build")); REQUIRE(matchesPruneGroup(getPruneCategory("dist/bundle.js"), "build")); REQUIRE(matchesPruneGroup(getPruneCategory("cmake-build-debug/CMakeFiles/app.dir/main.obj"), "build")); REQUIRE(matchesPruneGroup( getPruneCategory("builddir-nosan/tests/catch2_idle_zero_concurrency"), "build")); REQUIRE(matchesPruneGroup( getPruneCategory("subprojects/foo/builddir-asan/tests/helper_binary"), "build")); REQUIRE(matchesPruneGroup(getPruneCategory("target/release/app"), "build")); REQUIRE(matchesPruneGroup(getPruneCategory(".build/debug.yaml"), "build")); REQUIRE(matchesPruneGroup(getPruneCategory("DerivedData/App/Logs/build.log"), "build")); REQUIRE(matchesPruneGroup(getPruneCategory("buck-out/v2/gen/app"), "build")); REQUIRE(matchesPruneGroup(getPruneCategory("bazel-out/darwin-fastbuild/bin/app"), "build")); REQUIRE(matchesPruneGroup(getPruneCategory(".next/static/chunks/app.js"), "build")); REQUIRE(matchesPruneGroup(getPruneCategory("obj/Debug/app.pdb"), "build")); REQUIRE(matchesPruneGroup(getPruneCategory("x64/Release/app.ilk"), "build-artifacts")); REQUIRE(matchesPruneGroup( getPruneCategory("build/Debug/app.dSYM/Contents/Resources/DWARF/app"), "build")); REQUIRE(matchesPruneGroup(getPruneCategory("build-release/tools/yams"), "build")); REQUIRE(matchesPruneGroup(getPruneCategory("build-release/tools/yams"), "build")); } SECTION("Package caches and deps") { REQUIRE(matchesPruneGroup(getPruneCategory("node_modules/pkg/index.js"), "packages")); REQUIRE(matchesPruneGroup(getPruneCategory(".pnpm-store/v3/files/ab/cd"), "packages")); REQUIRE(matchesPruneGroup(getPruneCategory(".yarn/cache/react-npm.zip"), "packages")); REQUIRE(matchesPruneGroup(getPruneCategory(".pytest_cache/v/cache.db"), "packages")); REQUIRE(matchesPruneGroup(getPruneCategory(".hypothesis/examples/123"), "packages")); REQUIRE( matchesPruneGroup(getPruneCategory(".ipynb_checkpoints/notebook.ipynb"), "packages")); REQUIRE(matchesPruneGroup(getPruneCategory(".gradle/caches/modules.bin"), "packages")); REQUIRE(matchesPruneGroup(getPruneCategory("ios/Pods/Alamofire/Source.swift"), "packages")); REQUIRE(matchesPruneGroup( getPruneCategory(".terraform/providers/registry.terraform.io/provider"), "packages")); REQUIRE(matchesPruneGroup(getPruneCategory("ios/Pods/Alamofire/Source.swift"), "packages")); REQUIRE(matchesPruneGroup( getPruneCategory(".terraform/providers/registry.terraform.io/provider"), "packages")); } SECTION("Coverage and IDE outputs") { REQUIRE(getPruneCategory("coverage/lcov.info") == PruneCategory::Coverage); REQUIRE(getPruneCategory(".nyc_output/processinfo/index.json") == PruneCategory::Coverage); REQUIRE(getPruneCategory(".vs/slnx.sqlite") == PruneCategory::IdeProject); REQUIRE(getPruneCategory("meson-private/coredata.dat") == PruneCategory::SystemMeson); REQUIRE(getPruneCategory("CMakeFiles/rules.ninja") == PruneCategory::SystemCMake); REQUIRE(getPruneCategory(".cache/tool/entry") == PruneCategory::Cache); REQUIRE(getPruneCategory(".cache/tool/entry") == PruneCategory::Cache); } SECTION("Stored extensions retain their leading dot") { REQUIRE(getPruneCategory("/corpus/daemon.log", ".log") == PruneCategory::Logs); REQUIRE(getPruneCategory("/corpus/module.o", ".o") == PruneCategory::BuildObject); REQUIRE(getPruneCategory("/corpus/default.gcda", ".gcda") == PruneCategory::Coverage); } SECTION("MIME evidence identifies extensionless build artifacts") { REQUIRE(getPruneCategory("/corpus/worker", "", "application/x-executable") == PruneCategory::BuildExecutable); REQUIRE(getPruneCategory("/corpus/module", "", "application/x-llvm-bc") == PruneCategory::BuildObject); } SECTION("Git artifacts") { REQUIRE(getPruneCategory(".git/objects/aa/bb") == PruneCategory::GitArtifacts); } } //============================================================================= // Pattern Structure Tests //============================================================================= TEST_CASE("Pattern database is valid", "[core][magic]") { SECTION("Database is not empty") { const auto& patterns = get_magic_patterns(); #if YAMS_HAS_CONSTEXPR_CONTAINERS REQUIRE(patterns.size() > 0); REQUIRE(patterns.size() == 97); // High-confidence patterns only #endif } SECTION("Magic database info is accessible") { size_t count = MagicDatabaseInfo::pattern_count(); const char* mode = MagicDatabaseInfo::mode(); REQUIRE(count > 0); REQUIRE(mode != nullptr); REQUIRE(strlen(mode) > 0); #if YAMS_HAS_CONSTEXPR_CONTAINERS std::string mode_str(mode); REQUIRE(mode_str.find("compile-time") != std::string::npos); #endif } SECTION("Pattern fields are valid") { const auto& patterns = get_magic_patterns(); #if YAMS_HAS_CONSTEXPR_CONTAINERS REQUIRE(patterns.size() > 0); const auto& first = patterns[0]; REQUIRE(first.length > 0); REQUIRE(first.length <= 32); REQUIRE_FALSE(first.mime_type.empty()); REQUIRE_FALSE(first.file_type.empty()); REQUIRE_FALSE(first.description.empty()); REQUIRE(first.confidence >= 0.0f); REQUIRE(first.confidence <= 1.0f); #endif } } //============================================================================= // Edge Cases //============================================================================= TEST_CASE("Edge case handling", "[core][magic][edge]") { SECTION("Empty data returns default") { std::vector empty; REQUIRE(detect_mime_type(std::span(empty)) == "application/octet-stream"); } SECTION("Unknown data returns default") { auto unknown_data = hex_to_vector("DEADBEEFCAFE"); REQUIRE(detect_mime_type(std::span(unknown_data)) == "application/octet-stream"); } SECTION("Partial match fails") { // PNG signature is 8 bytes, provide only 4 auto partial_png = hex_to_vector("89504E47"); auto mime = detect_mime_type(std::span(partial_png)); REQUIRE(mime != "image/png"); } SECTION("Short data handled gracefully") { std::vector short_data = {0x00}; REQUIRE(detect_mime_type(std::span(short_data)) == "application/octet-stream"); } } //============================================================================= // Hex Conversion Tests //============================================================================= TEST_CASE("Hex conversion utilities", "[core][magic][hex]") { SECTION("hex_char_to_byte") { REQUIRE(hex_char_to_byte('0') == 0x0); REQUIRE(hex_char_to_byte('9') == 0x9); REQUIRE(hex_char_to_byte('A') == 0xA); REQUIRE(hex_char_to_byte('F') == 0xF); REQUIRE(hex_char_to_byte('a') == 0xa); REQUIRE(hex_char_to_byte('f') == 0xf); } SECTION("hex_to_bytes") { auto bytes = hex_to_bytes("FFD8FF"); REQUIRE(bytes[0] == 0xFF); REQUIRE(bytes[1] == 0xD8); REQUIRE(bytes[2] == 0xFF); } SECTION("hex_length") { REQUIRE(hex_length("FF") == 1); REQUIRE(hex_length("FFD8") == 2); REQUIRE(hex_length("FFD8FF") == 3); REQUIRE(hex_length("89504E470D0A1A0A") == 8); } } //============================================================================= // Pattern Matching Logic Tests //============================================================================= TEST_CASE("Pattern matching logic", "[core][magic][matching]") { SECTION("Pattern matches at correct offset") { MagicPattern pattern{hex_to_bytes("7573746172"), hex_length("7573746172"), 257, // TAR magic at offset 257 "archive", "application/x-tar", "POSIX tar archive", 1.0f}; // Create buffer with pattern at offset 257 std::vector buffer(300, 0x00); auto tar_magic = hex_to_vector("7573746172"); std::copy(tar_magic.begin(), tar_magic.end(), buffer.begin() + 257); // Should NOT match at offset 0 REQUIRE_FALSE(pattern.matches(std::span(buffer.data(), 10), 0)); // Should match at offset 257 REQUIRE(pattern.matches(std::span(buffer.data() + 257, 10), 257)); } SECTION("Pattern match fails on mismatch") { MagicPattern pattern{hex_to_bytes("FFD8FF"), 3, 0, "image", "image/jpeg", "JPEG", 1.0f}; auto wrong_data = hex_to_vector("89504E"); REQUIRE_FALSE(pattern.matches(std::span(wrong_data), 0)); } SECTION("Pattern match fails on insufficient data") { MagicPattern pattern{ hex_to_bytes("89504E470D0A1A0A"), 8, 0, "image", "image/png", "PNG", 1.0f}; // Only 4 bytes provided, pattern needs 8 auto short_data = hex_to_vector("89504E47"); REQUIRE_FALSE(pattern.matches(std::span(short_data), 0)); } } //============================================================================= // C++23 Constexpr Tests //============================================================================= #if YAMS_HAS_CONSTEXPR_CONTAINERS TEST_CASE("Constexpr magic number detection", "[core][magic][constexpr]") { SECTION("Constexpr detection works") { std::array jpeg_magic = {0xFF, 0xD8, 0xFF}; auto mime = detect_mime_type(std::span(jpeg_magic.data(), jpeg_magic.size())); REQUIRE(mime == "image/jpeg"); } SECTION("Pattern count is compile-time") { size_t count = MagicDatabaseInfo::pattern_count(); REQUIRE(count > 80); } SECTION("Patterns are accessible at compile-time") { const auto& patterns = get_magic_patterns(); REQUIRE(patterns.size() > 0); const auto& first = patterns[0]; REQUIRE(first.length > 0); } } #endif //============================================================================= // Performance/Stress Tests //============================================================================= TEST_CASE("Performance and stress testing", "[core][magic][performance]") { SECTION("Large data handled efficiently") { // Create 1MB of data std::vector large_data(1024 * 1024, 0x00); // Put JPEG magic at start large_data[0] = 0xFF; large_data[1] = 0xD8; large_data[2] = 0xFF; // Should still detect correctly REQUIRE(detect_mime_type(std::span(large_data.data(), 100)) == "image/jpeg"); } SECTION("Multiple detections are consistent") { auto jpeg_data = hex_to_vector("FFD8FF"); // Run detection 100 times, should always return same result for (int i = 0; i < 100; ++i) { REQUIRE(detect_mime_type(std::span(jpeg_data)) == "image/jpeg"); } } }