225 lines
8.5 KiB
C++
225 lines
8.5 KiB
C++
#include <algorithm>
|
|
#include <array>
|
|
#include <cctype>
|
|
#include <cstddef>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <initializer_list>
|
|
#include <span>
|
|
#include <vector>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <yams/cli/yams_cli.h>
|
|
#include <yams/detection/file_type_detector.h>
|
|
|
|
#include "detection_test_helpers.h"
|
|
|
|
using namespace yams::detection;
|
|
using namespace yams::cli;
|
|
using yams::detection::test_utils::ScopedCurrentPath;
|
|
using yams::detection::test_utils::TestDirectory;
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace {
|
|
struct CommandIntegrationFixture {
|
|
CommandIntegrationFixture() : cwdGuard(dir.root()) {
|
|
auto repoMagic = dir.repoMagicNumbers();
|
|
if (repoMagic.empty()) {
|
|
dir.createDataFile("data/magic_numbers.json", R"({"version":"1.0","patterns":[]})");
|
|
}
|
|
|
|
createFile("document.pdf",
|
|
{std::byte{0x25}, std::byte{0x50}, std::byte{0x44}, std::byte{0x46}});
|
|
createFile("image.jpg", {std::byte{0xFF}, std::byte{0xD8}, std::byte{0xFF}});
|
|
createFile("text.txt",
|
|
{std::byte{0x48}, std::byte{0x65}, std::byte{0x6C}, std::byte{0x6C}});
|
|
|
|
cli = std::make_unique<YamsCLI>();
|
|
cli->setDataPath(dir.testDataDir());
|
|
}
|
|
|
|
~CommandIntegrationFixture() { FileTypeDetector::instance().clearCache(); }
|
|
|
|
fs::path createFile(const std::string& name, std::initializer_list<std::byte> bytes) {
|
|
auto path =
|
|
dir.createBinaryFile(name, std::span<const std::byte>(bytes.begin(), bytes.size()));
|
|
std::ofstream file(path, std::ios::app);
|
|
file << " additional content for testing";
|
|
testFiles.push_back(path);
|
|
return path;
|
|
}
|
|
|
|
FileTypeDetectorConfig detectorConfig() const {
|
|
FileTypeDetectorConfig config{};
|
|
auto magic = YamsCLI::findMagicNumbersFile();
|
|
config.patternsFile = magic;
|
|
config.useCustomPatterns = !magic.empty();
|
|
config.useBuiltinPatterns = true;
|
|
return config;
|
|
}
|
|
|
|
TestDirectory dir{"detection_cli"};
|
|
ScopedCurrentPath cwdGuard;
|
|
std::unique_ptr<YamsCLI> cli;
|
|
std::vector<fs::path> testFiles;
|
|
};
|
|
} // namespace
|
|
|
|
TEST_CASE_METHOD(CommandIntegrationFixture, "findMagicNumbersFile resolves local data",
|
|
"[detection][cli]") {
|
|
auto foundPath = YamsCLI::findMagicNumbersFile();
|
|
REQUIRE_NOTHROW(std::string(foundPath.string()));
|
|
if (!foundPath.empty()) {
|
|
CHECK(fs::exists(foundPath));
|
|
CHECK(fs::is_regular_file(foundPath));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_METHOD(CommandIntegrationFixture, "findMagicNumbersFile respects search order",
|
|
"[detection][cli]") {
|
|
auto localMagic =
|
|
dir.createDataFile("data/magic_numbers.json", R"({"version":"1.0","patterns":[]})");
|
|
auto foundPath = YamsCLI::findMagicNumbersFile();
|
|
#if defined(__APPLE__)
|
|
// macOS: /var is a symlink to /private/var, so canonicalize both paths
|
|
std::error_code ec;
|
|
auto canonicalFound = fs::canonical(foundPath, ec);
|
|
auto canonicalLocal = fs::canonical(localMagic, ec);
|
|
CHECK(canonicalFound == canonicalLocal);
|
|
#elif defined(_WIN32)
|
|
auto normalizedPathString = [](const fs::path& path) {
|
|
std::error_code ec;
|
|
auto resolved = fs::weakly_canonical(path, ec);
|
|
if (ec) {
|
|
resolved = path.lexically_normal();
|
|
}
|
|
auto normalized = resolved.generic_string();
|
|
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
return normalized;
|
|
};
|
|
CHECK(normalizedPathString(foundPath) == normalizedPathString(localMagic));
|
|
#else
|
|
// Linux: direct comparison
|
|
CHECK(foundPath == localMagic);
|
|
#endif
|
|
}
|
|
|
|
TEST_CASE_METHOD(CommandIntegrationFixture, "detector initializes from CLI config",
|
|
"[detection][cli]") {
|
|
auto config = detectorConfig();
|
|
auto result = FileTypeDetector::instance().initialize(config);
|
|
CHECK((result.has_value() || !result.has_value()));
|
|
|
|
bool isText = FileTypeDetector::instance().isTextMimeType("text/plain");
|
|
bool isBinary = FileTypeDetector::instance().isBinaryMimeType("image/jpeg");
|
|
auto category = FileTypeDetector::instance().getFileTypeCategory("application/pdf");
|
|
|
|
CHECK(isText);
|
|
CHECK(isBinary);
|
|
CHECK_FALSE(category.empty());
|
|
}
|
|
|
|
TEST_CASE_METHOD(CommandIntegrationFixture, "detector falls back when JSON missing",
|
|
"[detection][cli]") {
|
|
FileTypeDetectorConfig config{};
|
|
config.patternsFile = fs::path("/nonexistent/path/magic_numbers.json");
|
|
config.useCustomPatterns = true;
|
|
config.useBuiltinPatterns = true;
|
|
|
|
auto initResult = FileTypeDetector::instance().initialize(config);
|
|
CHECK(initResult);
|
|
|
|
auto category = FileTypeDetector::instance().getFileTypeCategory("image/jpeg");
|
|
CHECK_FALSE(category.empty());
|
|
}
|
|
|
|
TEST_CASE_METHOD(CommandIntegrationFixture, "file detection handles CLI files",
|
|
"[detection][cli]") {
|
|
auto config = detectorConfig();
|
|
FileTypeDetector::instance().initialize(config);
|
|
|
|
for (const auto& testFile : testFiles) {
|
|
auto result = FileTypeDetector::instance().detectFromFile(testFile);
|
|
if (result) {
|
|
const auto sig = result.value();
|
|
CHECK_FALSE(sig.mimeType.empty());
|
|
CHECK_FALSE(sig.fileType.empty());
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE_METHOD(CommandIntegrationFixture, "stats-style filtering uses classification",
|
|
"[detection][cli]") {
|
|
auto config = detectorConfig();
|
|
auto init = FileTypeDetector::instance().initialize(config);
|
|
CHECK((init.has_value() || !init.has_value()));
|
|
|
|
for (const auto& testFile : testFiles) {
|
|
auto extension = testFile.extension().string();
|
|
auto mimeType = FileTypeDetector::getMimeTypeFromExtension(extension);
|
|
auto fileType = FileTypeDetector::instance().getFileTypeCategory(mimeType);
|
|
auto isBinary = FileTypeDetector::instance().isBinaryMimeType(mimeType);
|
|
CHECK_FALSE(fileType.empty());
|
|
CHECK((isBinary || !isBinary));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_METHOD(CommandIntegrationFixture, "list command filtering remains consistent",
|
|
"[detection][cli]") {
|
|
auto config = detectorConfig();
|
|
auto init = FileTypeDetector::instance().initialize(config);
|
|
CHECK((init.has_value() || !init.has_value()));
|
|
|
|
std::vector<std::string> mimeTypes = {"image/jpeg", "application/pdf", "text/plain",
|
|
"application/json"};
|
|
for (const auto& mimeType : mimeTypes) {
|
|
auto isText = FileTypeDetector::instance().isTextMimeType(mimeType);
|
|
auto isBinary = FileTypeDetector::instance().isBinaryMimeType(mimeType);
|
|
auto category = FileTypeDetector::instance().getFileTypeCategory(mimeType);
|
|
CHECK(isText != isBinary);
|
|
CHECK_FALSE(category.empty());
|
|
}
|
|
}
|
|
|
|
TEST_CASE_METHOD(CommandIntegrationFixture, "get command filtering aligns with detection",
|
|
"[detection][cli]") {
|
|
auto config = detectorConfig();
|
|
FileTypeDetector::instance().initialize(config);
|
|
|
|
for (const auto& testFile : testFiles) {
|
|
if (!fs::exists(testFile)) {
|
|
continue;
|
|
}
|
|
auto detectResult = FileTypeDetector::instance().detectFromFile(testFile);
|
|
if (detectResult) {
|
|
const auto sig = detectResult.value();
|
|
[[maybe_unused]] auto isText =
|
|
FileTypeDetector::instance().isTextMimeType(sig.mimeType);
|
|
auto isBinary = FileTypeDetector::instance().isBinaryMimeType(sig.mimeType);
|
|
auto category = FileTypeDetector::instance().getFileTypeCategory(sig.mimeType);
|
|
|
|
CHECK(sig.isBinary == isBinary);
|
|
CHECK(sig.fileType == category);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST_CASE_METHOD(CommandIntegrationFixture, "initialization failure degrades gracefully",
|
|
"[detection][cli]") {
|
|
FileTypeDetectorConfig badConfig{};
|
|
badConfig.useLibMagic = false;
|
|
badConfig.useBuiltinPatterns = false;
|
|
badConfig.useCustomPatterns = true;
|
|
badConfig.patternsFile = fs::path("/definitely/does/not/exist/anywhere.json");
|
|
|
|
auto result = FileTypeDetector::instance().initialize(badConfig);
|
|
CHECK((result.has_value() || !result.has_value()));
|
|
|
|
auto mime = FileTypeDetector::getMimeTypeFromExtension(".txt");
|
|
CHECK(mime == "text/plain");
|
|
|
|
auto isText = FileTypeDetector::instance().isTextMimeType("text/plain");
|
|
auto isBinary = FileTypeDetector::instance().isBinaryMimeType("image/jpeg");
|
|
CHECK((isText || !isText));
|
|
CHECK((isBinary || !isBinary));
|
|
}
|