diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e643dee093..e8f70fe7bd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -exclude: '.*\.(pcap|pcapng|dat)|(PacketExamples|PcapExamples|expected_output|pcap_examples).*\.txt' +exclude: '.*\.(pcap|pcapng|dat)|(PacketExamples|PcapExamples|expected_output|pcap_examples).*\.(txt|zst|zstd)' fail_fast: false repos: - repo: local @@ -37,6 +37,7 @@ repos: files: ^(Common\+\+|Packet\+\+|Pcap\+\+|Tests|Examples)/.*\.(cpp|h)$ - id: cppcheck args: ["--std=c++14", "--language=c++", "--suppressions-list=cppcheckSuppressions.txt", "--inline-suppr", "--force"] + exclude: ^3rdParty/json - repo: https://github.com/BlankSpruce/gersemi rev: 0.22.3 hooks: diff --git a/Pcap++/CMakeLists.txt b/Pcap++/CMakeLists.txt index feb4bae479..2f09977929 100644 --- a/Pcap++/CMakeLists.txt +++ b/Pcap++/CMakeLists.txt @@ -102,6 +102,7 @@ target_link_libraries( ) if(LIGHT_PCAPNG_ZSTD) + target_compile_definitions(Pcap++ PRIVATE -DPCPP_PCAPNG_ZSTD_SUPPORT) target_link_libraries(Pcap++ PRIVATE light_pcapng) endif() diff --git a/Pcap++/header/PcapFileDevice.h b/Pcap++/header/PcapFileDevice.h index 61513d560f..f782cc5a93 100644 --- a/Pcap++/header/PcapFileDevice.h +++ b/Pcap++/header/PcapFileDevice.h @@ -19,6 +19,36 @@ namespace pcpp /// @struct LightPcapNgHandle /// An opaque struct representing a handle for pcapng files. struct LightPcapNgHandle; + + /// @brief An enumeration representing different capture file formats. + enum class CaptureFileFormat + { + Unknown, + Pcap, // regular pcap with microsecond precision + PcapNano, // regular pcap with nanosecond precision + PcapNG, // uncompressed pcapng + PcapNGZstd, // zstd compressed pcapng + Snoop, // solaris snoop + }; + + /// @brief Heuristic file format detector that scans the magic number of the file format header. + class CaptureFileFormatDetector + { + public: + /// @brief Checks a content stream for the magic number and determines the type. + /// @param content A content stream that contains the file content. + /// @return A CaptureFileFormat value with the detected content type. + CaptureFileFormat detectFormat(std::istream& content) const; + + private: + CaptureFileFormat detectPcapFile(std::istream& content) const; + + bool isPcapNgFile(std::istream& content) const; + + bool isSnoopFile(std::istream& content) const; + + bool isZstdArchive(std::istream& content) const; + }; } // namespace internal /// @enum FileTimestampPrecision @@ -124,7 +154,29 @@ namespace pcpp /// it returns an instance of PcapFileReaderDevice /// @param[in] fileName The file name to open /// @return An instance of the reader to read the file. Notice you should free this instance when done using it + /// @deprecated Prefer `createReader` or `tryCreateReader` due to selection of reader based on file content + /// instead of extension. + PCPP_DEPRECATED("Prefer `tryCreateReader` due to selection of reader based on file content.") static IFileReaderDevice* getReader(const std::string& fileName); + + /// @brief Creates an instance of the reader best fit to read the file. + /// + /// The factory function uses heuristics based on the file content to decide the reader. + /// If the file type is known at compile time, it is better to construct a concrete reader instance directly. + /// + /// @param[in] fileName The path to the file to open. + /// @return A unique pointer to a reader instance + /// @throws std::runtime_error If the file could not be opened or unsupported. + static std::unique_ptr createReader(const std::string& fileName); + + /// @brief Tries to create an instance of the reader best fit to read the file. + /// + /// The factory function uses heuristics based on the file content to decide the reader. + /// If the file type is known at compile time, it is better to construct a concrete reader instance directly. + /// + /// @param fileName The path to the file to open. + /// @return A unique pointer to a reader instance, or nullptr if the file could not be opened or unsupported. + static std::unique_ptr tryCreateReader(const std::string& fileName); }; /// @class IFileWriterDevice @@ -313,6 +365,10 @@ namespace pcpp PcapNgFileReaderDevice& operator=(const PcapNgFileReaderDevice& other); public: + /// @brief A static method that checks if the device was built with zstd compression support + /// @return True if zstd compression is supported, false otherwise. + static bool isZstdSupported(); + /// A constructor for this class that gets the pcap-ng full path file name to open. Notice that after calling /// this constructor the file isn't opened yet, so reading packets will fail. For opening the file call open() /// @param[in] fileName The full path of the file to read @@ -397,6 +453,10 @@ namespace pcpp PcapNgFileWriterDevice& operator=(const PcapNgFileWriterDevice& other); public: + /// @brief A static method that checks if the device was built with zstd compression support. + /// @return True if zstd compression is supported, false otherwise. + static bool isZstdSupported(); + /// A constructor for this class that gets the pcap-ng full path file name to open for writing or create. Notice /// that after calling this constructor the file isn't opened yet, so writing packets will fail. For opening the /// file call open() diff --git a/Pcap++/src/PcapFileDevice.cpp b/Pcap++/src/PcapFileDevice.cpp index ee7cbd009e..0b0223dfff 100644 --- a/Pcap++/src/PcapFileDevice.cpp +++ b/Pcap++/src/PcapFileDevice.cpp @@ -1,7 +1,10 @@ +#include "PcapFileDevice.h" #define LOG_MODULE PcapLogModuleFileDevice #include #include +#include +#include #include "PcapFileDevice.h" #include "light_pcapng_ext.h" #include "Logger.h" @@ -14,6 +17,36 @@ namespace pcpp { namespace { + constexpr bool checkNanoSupport() + { +#ifdef PCAP_TSTAMP_PRECISION_NANO + return true; +#else + return false; +#endif + } + + bool checkNanoSupportWithInfo() + { + constexpr auto ret = checkNanoSupport(); + if (!ret) + { + PCPP_LOG_DEBUG( + "PcapPlusPlus was compiled without nano precision support which requires libpcap > 1.5.1. Please " + "recompile PcapPlusPlus with nano precision support to use this feature."); + } + return ret; + } + + constexpr bool checkZstdSupport() + { +#ifdef PCPP_PCAPNG_ZSTD_SUPPORT + return true; +#else + return false; +#endif + } + /// @brief Converts a light_pcapng_t* to an opaque LightPcapNgHandle*. /// @param pcapngHandle The light_pcapng_t* to convert. /// @return An pointer to the opaque handle. @@ -29,43 +62,210 @@ namespace pcpp { return reinterpret_cast(pcapngHandle); } + + struct pcap_file_header + { + uint32_t magic; + uint16_t version_major; + uint16_t version_minor; + int32_t thiszone; + uint32_t sigfigs; + uint32_t snaplen; + uint32_t linktype; + }; + + struct packet_header + { + uint32_t tv_sec; + uint32_t tv_usec; + uint32_t caplen; + uint32_t len; + }; + + class StreamPositionCheckpoint + { + public: + explicit StreamPositionCheckpoint(std::istream& stream) + : m_Stream(stream), m_State(stream.rdstate()), m_Pos(stream.tellg()) + {} + + ~StreamPositionCheckpoint() + { + m_Stream.seekg(m_Pos); + m_Stream.clear(m_State); + } + + private: + std::istream& m_Stream; + std::ios_base::iostate m_State; + std::streampos m_Pos; + }; + + /// @brief Check if a stream is seekable. + /// @param stream The stream to check. + /// @return True if the stream supports seek operations, false otherwise. + bool isStreamSeekable(std::istream& stream) + { + auto pos = stream.tellg(); + if (stream.fail()) + { + stream.clear(); + return false; + } + + if (stream.seekg(pos).fail()) + { + stream.clear(); + return false; + } + + return true; + } + + template constexpr size_t ARRAY_SIZE(T (&)[N]) + { + return N; + } } // namespace - template constexpr size_t ARRAY_SIZE(T (&)[N]) + namespace internal { - return N; - } + CaptureFileFormat CaptureFileFormatDetector::detectFormat(std::istream& content) const + { + // Check if the stream supports seeking. + if (!isStreamSeekable(content)) + { + throw std::runtime_error("Heuristic file format detection requires seekable stream"); + } - struct pcap_file_header - { - uint32_t magic; - uint16_t version_major; - uint16_t version_minor; - int32_t thiszone; - uint32_t sigfigs; - uint32_t snaplen; - uint32_t linktype; - }; + CaptureFileFormat format = detectPcapFile(content); + if (format != CaptureFileFormat::Unknown) + { + return format; + } - struct packet_header - { - uint32_t tv_sec; - uint32_t tv_usec; - uint32_t caplen; - uint32_t len; - }; + if (isPcapNgFile(content)) + { + return CaptureFileFormat::PcapNG; + } - static bool checkNanoSupport() - { -#if defined(PCAP_TSTAMP_PRECISION_NANO) - return true; -#else - PCPP_LOG_DEBUG( - "PcapPlusPlus was compiled without nano precision support which requires libpcap > 1.5.1. Please " - "recompile PcapPlusPlus with nano precision support to use this feature. Using default microsecond precision"); - return false; -#endif - } + // PcapNG backend can support ZstdCompressed Pcap files, so we assume an archive is compressed PcapNG. + if (isZstdArchive(content)) + { + return CaptureFileFormat::PcapNGZstd; + } + + if (isSnoopFile(content)) + { + return CaptureFileFormat::Snoop; + } + + return CaptureFileFormat::Unknown; + } + + CaptureFileFormat CaptureFileFormatDetector::detectPcapFile(std::istream& content) const + { + // Pcap magic numbers are taken from: https://github.com/the-tcpdump-group/libpcap/blob/master/sf-pcap.c + // There are some other reserved magic numbers but they are not supported by libpcap so we ignore them. + // The order of the magic numbers in the array is important for format detection. See switch statement + // below. + constexpr std::array pcapMagicNumbers = { + 0xa1'b2'c3'd4, // regular pcap, microsecond-precision + 0xd4'c3'b2'a1, // regular pcap, microsecond-precision (byte-swapped) + // Libpcap 0.9.1 and later support reading a modified pcap format that contains an extended header. + // Format reference: https://wiki.wireshark.org/Development/LibpcapFileFormat#modified-pcap + 0xa1'b2'cd'34, // Alexey Kuznetzov's modified libpcap format + 0x34'cd'b2'a1, // Alexey Kuznetzov's modified libpcap format (byte-swapped) + // Libpcap 1.5.0 and later support reading nanosecond-precision pcap files. + 0xa1'b2'3c'4d, // regular pcap, nanosecond-precision + 0x4d'3c'b2'a1, // regular pcap, nanosecond-precision (byte-swapped) + }; + + // Mapping of magic numbers to CaptureFileFormat values. Each format applies to two magic numbers. + // The function to select is Format Index = MagicNumber Index / 2. + constexpr std::array formatMapping = { + CaptureFileFormat::Pcap, // regular pcap + CaptureFileFormat::Pcap, // modified pcap, folded into regular pcap + CaptureFileFormat::PcapNano // nanosecond-precision pcap + }; + + static_assert(formatMapping.size() * 2 == pcapMagicNumbers.size(), + "Format mapping array size is inconsistent with magic numbers array size"); + + StreamPositionCheckpoint checkpoint(content); + + uint32_t magic = 0; + content.read(reinterpret_cast(&magic), sizeof(magic)); + if (content.gcount() != sizeof(magic)) + { + return CaptureFileFormat::Unknown; + } + + auto it = std::find(pcapMagicNumbers.begin(), pcapMagicNumbers.end(), magic); + if (it == pcapMagicNumbers.end()) + { + return CaptureFileFormat::Unknown; + } + + return formatMapping[std::distance(pcapMagicNumbers.begin(), it) / 2]; + } + + bool CaptureFileFormatDetector::isPcapNgFile(std::istream& content) const + { + constexpr std::array pcapMagicNumbers = { + 0x0A'0D'0D'0A, // pcapng magic number (palindrome) + }; + + StreamPositionCheckpoint checkpoint(content); + + uint32_t magic = 0; + content.read(reinterpret_cast(&magic), sizeof(magic)); + if (content.gcount() != sizeof(magic)) + { + return false; + } + + return std::find(pcapMagicNumbers.begin(), pcapMagicNumbers.end(), magic) != pcapMagicNumbers.end(); + } + + bool CaptureFileFormatDetector::isSnoopFile(std::istream& content) const + { + constexpr std::array snoopMagicNumbers = { + 0x73'6E'6F'6F'70'00'00'00, // snoop magic number, "snoop" in ASCII + 0x00'00'00'70'6F'6F'6E'73 // snoop magic number, "snoop" in ASCII (byte-swapped) + }; + + StreamPositionCheckpoint checkpoint(content); + + uint64_t magic = 0; + content.read(reinterpret_cast(&magic), sizeof(magic)); + if (content.gcount() != sizeof(magic)) + { + return false; + } + + return std::find(snoopMagicNumbers.begin(), snoopMagicNumbers.end(), magic) != snoopMagicNumbers.end(); + } + + bool CaptureFileFormatDetector::isZstdArchive(std::istream& content) const + { + constexpr std::array zstdMagicNumbers = { + 0x28'B5'2F'FD, // zstd archive magic number + 0xFD'2F'B5'28, // zstd archive magic number (byte-swapped) + }; + + StreamPositionCheckpoint checkpoint(content); + + uint32_t magic = 0; + content.read(reinterpret_cast(&magic), sizeof(magic)); + if (content.gcount() != sizeof(magic)) + { + return false; + } + + return std::find(zstdMagicNumbers.begin(), zstdMagicNumbers.end(), magic) != zstdMagicNumbers.end(); + } + } // namespace internal // ~~~~~~~~~~~~~~~~~~~ // IFileDevice members @@ -131,6 +331,62 @@ namespace pcpp return new PcapFileReaderDevice(fileName); } + std::unique_ptr IFileReaderDevice::createReader(const std::string& fileName) + { + std::ifstream fileContent(fileName, std::ios_base::binary); + if (fileContent.fail()) + { + throw std::runtime_error("Could not open: " + fileName); + } + + using internal::CaptureFileFormat; + using internal::CaptureFileFormatDetector; + + switch (CaptureFileFormatDetector().detectFormat(fileContent)) + { + case CaptureFileFormat::PcapNano: + { + if (!checkNanoSupport()) + { + throw std::runtime_error( + "Pcap files with nanosecond precision are not supported in this build of PcapPlusPlus"); + } + // fallthrough + } + case CaptureFileFormat::Pcap: + return std::make_unique(fileName); + case CaptureFileFormat::PcapNGZstd: + { + if (!checkZstdSupport()) + { + throw std::runtime_error( + "PcapNG Zstd compressed files are not supported in this build of PcapPlusPlus"); + } + // fallthrough + } + case CaptureFileFormat::PcapNG: + return std::make_unique(fileName); + case CaptureFileFormat::Snoop: + return std::make_unique(fileName); + default: + throw std::runtime_error("File format of " + fileName + " is not supported"); + } + } + + std::unique_ptr IFileReaderDevice::tryCreateReader(const std::string& fileName) + { + // Not the best implementation, but it is not expected to be called in hot loops + try + { + return createReader(fileName); + } + catch (const std::exception& e) + { + PCPP_LOG_ERROR(e.what()); + return nullptr; + } + } + uint64_t IFileReaderDevice::getFileSize() const { std::ifstream fileStream(m_FileName.c_str(), std::ifstream::ate | std::ifstream::binary); @@ -222,7 +478,7 @@ namespace pcpp bool PcapFileReaderDevice::isNanoSecondPrecisionSupported() { - return checkNanoSupport(); + return checkNanoSupportWithInfo(); } bool PcapFileReaderDevice::getNextPacket(RawPacket& rawPacket) @@ -364,7 +620,7 @@ namespace pcpp bool PcapFileWriterDevice::isNanoSecondPrecisionSupported() { - return checkNanoSupport(); + return checkNanoSupportWithInfo(); } bool PcapFileWriterDevice::open() @@ -536,6 +792,11 @@ namespace pcpp // PcapNgFileReaderDevice members // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + bool PcapNgFileReaderDevice::isZstdSupported() + { + return checkZstdSupport(); + } + PcapNgFileReaderDevice::PcapNgFileReaderDevice(const std::string& fileName) : IFileReaderDevice(fileName) { m_LightPcapNg = nullptr; @@ -705,6 +966,11 @@ namespace pcpp // PcapNgFileWriterDevice members // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + bool PcapNgFileWriterDevice::isZstdSupported() + { + return checkZstdSupport(); + } + PcapNgFileWriterDevice::PcapNgFileWriterDevice(const std::string& fileName, int compressionLevel) : IFileWriterDevice(fileName) { diff --git a/Tests/Pcap++Test/CMakeLists.txt b/Tests/Pcap++Test/CMakeLists.txt index 1727a4d15b..3f483ff6bc 100644 --- a/Tests/Pcap++Test/CMakeLists.txt +++ b/Tests/Pcap++Test/CMakeLists.txt @@ -19,6 +19,9 @@ add_executable( Tests/XdpTests.cpp ) +# Added as source files for IDEs +target_sources(Pcap++Test PRIVATE Common/GlobalTestArgs.h Common/PcapFileNamesDef.h Common/TestUtils.h TestDefinition.h) + target_link_libraries(Pcap++Test PUBLIC memplumber Pcap++ PcppTestFramework EndianPortable) if(MSVC) diff --git a/Tests/Pcap++Test/PcapExamples/file_heuristics/bogus-content.txt b/Tests/Pcap++Test/PcapExamples/file_heuristics/bogus-content.txt new file mode 100644 index 0000000000..15a8eef479 --- /dev/null +++ b/Tests/Pcap++Test/PcapExamples/file_heuristics/bogus-content.txt @@ -0,0 +1 @@ +4561d5a474d6as465d8as41d1as6531863d1as65d1a36d1 \ No newline at end of file diff --git a/Tests/Pcap++Test/PcapExamples/file_heuristics/microsecs.pcap b/Tests/Pcap++Test/PcapExamples/file_heuristics/microsecs.pcap new file mode 100644 index 0000000000..20cba6b855 Binary files /dev/null and b/Tests/Pcap++Test/PcapExamples/file_heuristics/microsecs.pcap differ diff --git a/Tests/Pcap++Test/PcapExamples/file_heuristics/nanosecs.pcap b/Tests/Pcap++Test/PcapExamples/file_heuristics/nanosecs.pcap new file mode 100644 index 0000000000..4353706a42 Binary files /dev/null and b/Tests/Pcap++Test/PcapExamples/file_heuristics/nanosecs.pcap differ diff --git a/Tests/Pcap++Test/PcapExamples/file_heuristics/pcap-with-dat-ext.pcap.dat b/Tests/Pcap++Test/PcapExamples/file_heuristics/pcap-with-dat-ext.pcap.dat new file mode 100644 index 0000000000..20cba6b855 Binary files /dev/null and b/Tests/Pcap++Test/PcapExamples/file_heuristics/pcap-with-dat-ext.pcap.dat differ diff --git a/Tests/Pcap++Test/PcapExamples/file_heuristics/pcapng-example.pcapng b/Tests/Pcap++Test/PcapExamples/file_heuristics/pcapng-example.pcapng new file mode 100644 index 0000000000..e188daa44b Binary files /dev/null and b/Tests/Pcap++Test/PcapExamples/file_heuristics/pcapng-example.pcapng differ diff --git a/Tests/Pcap++Test/PcapExamples/file_heuristics/pcapng-example.pcapng.zst b/Tests/Pcap++Test/PcapExamples/file_heuristics/pcapng-example.pcapng.zst new file mode 100644 index 0000000000..76b195f97f Binary files /dev/null and b/Tests/Pcap++Test/PcapExamples/file_heuristics/pcapng-example.pcapng.zst differ diff --git a/Tests/Pcap++Test/PcapExamples/file_heuristics/pcapng-with-pcap-ext.pcapng.pcap b/Tests/Pcap++Test/PcapExamples/file_heuristics/pcapng-with-pcap-ext.pcapng.pcap new file mode 100644 index 0000000000..e188daa44b Binary files /dev/null and b/Tests/Pcap++Test/PcapExamples/file_heuristics/pcapng-with-pcap-ext.pcapng.pcap differ diff --git a/Tests/Pcap++Test/TestDefinition.h b/Tests/Pcap++Test/TestDefinition.h index 8e9da6b79e..e529fe3e6f 100644 --- a/Tests/Pcap++Test/TestDefinition.h +++ b/Tests/Pcap++Test/TestDefinition.h @@ -20,6 +20,15 @@ PTF_TEST_CASE(TestLogger); PTF_TEST_CASE(TestLoggerMultiThread); // Implemented in FileTests.cpp +PTF_TEST_CASE(TestFileFormatDetector); +PTF_TEST_CASE(TestReaderFactory_Pcap_Micro); +PTF_TEST_CASE(TestReaderFactory_Pcap_Nano); +PTF_TEST_CASE(TestReaderFactory_Pcap_Nano_Unsupported); +PTF_TEST_CASE(TestReaderFactory_PcapNG); +PTF_TEST_CASE(TestReaderFactory_PcapNG_ZST); +PTF_TEST_CASE(TestReaderFactory_PcapNG_ZST_Unsupported); +PTF_TEST_CASE(TestReaderFactory_Snoop); +PTF_TEST_CASE(TestReaderFactory_InvalidFile); PTF_TEST_CASE(TestPcapFileReadWrite); PTF_TEST_CASE(TestPcapFileMicroPrecision); PTF_TEST_CASE(TestPcapFileNanoPrecision); diff --git a/Tests/Pcap++Test/Tests/FileTests.cpp b/Tests/Pcap++Test/Tests/FileTests.cpp index 2d631d3ef6..8f6f18abee 100644 --- a/Tests/Pcap++Test/Tests/FileTests.cpp +++ b/Tests/Pcap++Test/Tests/FileTests.cpp @@ -26,6 +26,204 @@ class FileReaderTeardown } }; +PTF_TEST_CASE(TestFileFormatDetector) +{ + using pcpp::internal::CaptureFileFormat; + using pcpp::internal::CaptureFileFormatDetector; + + std::vector> simpleTestCases = { + { 0xa1'b2'c3'd4, CaptureFileFormat::Pcap }, // regular pcap, microsecond-precision + { 0xd4'c3'b2'a1, CaptureFileFormat::Pcap }, // regular pcap, microsecond-precision (byte-swapped) + { 0xa1'b2'cd'34, CaptureFileFormat::Pcap }, // Alexey Kuznetzov's modified libpcap format + { 0x34'cd'b2'a1, CaptureFileFormat::Pcap }, // Alexey Kuznetzov's modified libpcap format (byte-swapped) + { 0xa1'b2'3c'4d, CaptureFileFormat::PcapNano }, // regular pcap, nanosecond-precision + { 0x4d'3c'b2'a1, CaptureFileFormat::PcapNano }, // regular pcap, nanosecond-precision (byte-swapped) + { 0x0A'0D'0D'0A, CaptureFileFormat::PcapNG }, // pcapng magic number (palindrome) + { 0x28'B5'2F'FD, CaptureFileFormat::PcapNGZstd }, // zstd archive magic number, assumed to be compressed pcap + { 0xFD'2F'B5'28, CaptureFileFormat::PcapNGZstd }, // zstd archive magic number (byte-swapped) + { 0x12'34'56'78, CaptureFileFormat::Unknown }, // unknown + { 0x00'00'00'00, CaptureFileFormat::Unknown }, // unknown + { 0xFF'FF'FF'FF, CaptureFileFormat::Unknown }, // unknown + }; + + for (const auto& testCase : simpleTestCases) + { + std::stringstream ss; + ss.write(reinterpret_cast(&testCase.first), sizeof(testCase.first)); + CaptureFileFormatDetector detector; + auto detectedFormat = detector.detectFormat(ss); + PTF_ASSERT_EQUAL(detectedFormat, testCase.second, enumclass); + } + + // Snoop is special as it uses a 64-bit magic number sequence. + std::vector> snoopTestCases = { + { 0x73'6E'6F'6F'70'00'00'00, CaptureFileFormat::Snoop }, // snoop magic number, "snoop" in ASCII + { 0x00'00'00'70'6F'6F'6E'73, CaptureFileFormat::Snoop }, // snoop magic number, "snoop" in ASCII (byte-swapped) + }; + + for (const auto& testCase : snoopTestCases) + { + std::stringstream ss; + ss.write(reinterpret_cast(&testCase.first), sizeof(testCase.first)); + CaptureFileFormatDetector detector; + auto detectedFormat = detector.detectFormat(ss); + PTF_ASSERT_EQUAL(detectedFormat, testCase.second, enumclass); + } +} + +PTF_TEST_CASE(TestReaderFactory_Pcap_Micro) +{ + // Correct format + constexpr const char* PCAP_MICROSEC_FILE_PATH = "PcapExamples/file_heuristics/microsecs.pcap"; + // Correct format, wrong extension, microsecond precision + constexpr const char* PCAP_AS_DAT_FILE_PATH = "PcapExamples/file_heuristics/pcap-with-dat-ext.pcap.dat"; + + std::unique_ptr dev; + + for (const auto& filePath : { PCAP_MICROSEC_FILE_PATH, PCAP_AS_DAT_FILE_PATH }) + { + dev = pcpp::IFileReaderDevice::createReader(filePath); + PTF_ASSERT_NOT_NULL(dev); + PTF_ASSERT_NOT_NULL(dynamic_cast(dev.get())); + PTF_ASSERT_TRUE(dev->open()); + + dev = pcpp::IFileReaderDevice::tryCreateReader(filePath); + PTF_ASSERT_NOT_NULL(dev); + PTF_ASSERT_NOT_NULL(dynamic_cast(dev.get())); + PTF_ASSERT_TRUE(dev->open()); + } +} + +PTF_TEST_CASE(TestReaderFactory_Pcap_Nano) +{ + if (!pcpp::PcapFileReaderDevice::isNanoSecondPrecisionSupported()) + { + PTF_SKIP_TEST("Nano-second precision is not supported in this platform/environment"); + } + + constexpr const char* PCAP_NANOSEC_FILE_PATH = "PcapExamples/file_heuristics/nanosecs.pcap"; + + auto dev = pcpp::IFileReaderDevice::createReader(PCAP_NANOSEC_FILE_PATH); + PTF_ASSERT_NOT_NULL(dev); + PTF_ASSERT_NOT_NULL(dynamic_cast(dev.get())); + PTF_ASSERT_TRUE(dev->open()); + + dev = pcpp::IFileReaderDevice::tryCreateReader(PCAP_NANOSEC_FILE_PATH); + PTF_ASSERT_NOT_NULL(dev); + PTF_ASSERT_NOT_NULL(dynamic_cast(dev.get())); + PTF_ASSERT_TRUE(dev->open()); +} + +PTF_TEST_CASE(TestReaderFactory_Pcap_Nano_Unsupported) +{ + if (pcpp::PcapFileReaderDevice::isNanoSecondPrecisionSupported()) + { + PTF_SKIP_TEST("Nano-second precision is supported in this platform/environment"); + } + + constexpr const char* PCAP_NANOSEC_FILE_PATH = "PcapExamples/file_heuristics/nanosecs.pcap"; + + PTF_ASSERT_RAISES(pcpp::IFileReaderDevice::createReader(PCAP_NANOSEC_FILE_PATH), std::runtime_error, + "Pcap files with nanosecond precision are not supported in this build of PcapPlusPlus"); + + auto dev = pcpp::IFileReaderDevice::tryCreateReader(PCAP_NANOSEC_FILE_PATH); + PTF_ASSERT_NULL(dev); +} + +PTF_TEST_CASE(TestReaderFactory_PcapNG) +{ + // Correct format + constexpr const char* PCAPNG_FILE_PATH = "PcapExamples/file_heuristics/pcapng-example.pcapng"; + // Correct format, wrong extension + constexpr const char* PCAPNG_AS_PCAP_FILE_PATH = "PcapExamples/file_heuristics/pcapng-with-pcap-ext.pcapng.pcap"; + + std::unique_ptr dev; + + for (const auto& filePath : { PCAPNG_FILE_PATH, PCAPNG_AS_PCAP_FILE_PATH }) + { + dev = pcpp::IFileReaderDevice::createReader(filePath); + PTF_ASSERT_NOT_NULL(dev); + PTF_ASSERT_NOT_NULL(dynamic_cast(dev.get())); + PTF_ASSERT_TRUE(dev->open()); + + dev = pcpp::IFileReaderDevice::tryCreateReader(filePath); + PTF_ASSERT_NOT_NULL(dev); + PTF_ASSERT_NOT_NULL(dynamic_cast(dev.get())); + PTF_ASSERT_TRUE(dev->open()); + } +} + +PTF_TEST_CASE(TestReaderFactory_PcapNG_ZST) +{ + if (!pcpp::PcapNgFileReaderDevice::isZstdSupported()) + { + PTF_SKIP_TEST("Zstandard compression is not supported in this platform/environment"); + } + + constexpr const char* PCAPNG_ZST_FILE_PATH = "PcapExamples/file_heuristics/pcapng-example.pcapng.zst"; + + auto dev = pcpp::IFileReaderDevice::createReader(PCAPNG_ZST_FILE_PATH); + PTF_ASSERT_NOT_NULL(dev); + PTF_ASSERT_NOT_NULL(dynamic_cast(dev.get())); + PTF_ASSERT_TRUE(dev->open()); + + dev = pcpp::IFileReaderDevice::tryCreateReader(PCAPNG_ZST_FILE_PATH); + PTF_ASSERT_NOT_NULL(dev); + PTF_ASSERT_NOT_NULL(dynamic_cast(dev.get())); + PTF_ASSERT_TRUE(dev->open()); +} + +PTF_TEST_CASE(TestReaderFactory_PcapNG_ZST_Unsupported) +{ + if (pcpp::PcapNgFileReaderDevice::isZstdSupported()) + { + PTF_SKIP_TEST("Zstandard compression is supported in this platform/environment"); + } + + constexpr const char* PCAPNG_ZST_FILE_PATH = "PcapExamples/file_heuristics/pcapng-example.pcapng.zst"; + + PTF_ASSERT_RAISES(pcpp::IFileReaderDevice::createReader(PCAPNG_ZST_FILE_PATH), std::runtime_error, + "PcapNG Zstd compressed files are not supported in this build of PcapPlusPlus"); + + auto dev = pcpp::IFileReaderDevice::tryCreateReader(PCAPNG_ZST_FILE_PATH); + PTF_ASSERT_NULL(dev); +} + +PTF_TEST_CASE(TestReaderFactory_Snoop) +{ + constexpr const char* SNOOP_FILE_PATH = EXAMPLE_SOLARIS_SNOOP; + + auto dev = pcpp::IFileReaderDevice::createReader(SNOOP_FILE_PATH); + PTF_ASSERT_NOT_NULL(dev); + PTF_ASSERT_NOT_NULL(dynamic_cast(dev.get())); + PTF_ASSERT_TRUE(dev->open()); + + dev = pcpp::IFileReaderDevice::tryCreateReader(SNOOP_FILE_PATH); + PTF_ASSERT_NOT_NULL(dev); + PTF_ASSERT_NOT_NULL(dynamic_cast(dev.get())); + PTF_ASSERT_TRUE(dev->open()); +} + +PTF_TEST_CASE(TestReaderFactory_InvalidFile) +{ + // Garbage data + constexpr const char* BOGUS_FILE_PATH = "PcapExamples/file_heuristics/bogus-content.txt"; + + // Test non-existent file + PTF_ASSERT_RAISES(pcpp::IFileReaderDevice::createReader("this-file-does-not-exist.pcap"), std::runtime_error, + "Could not open: this-file-does-not-exist.pcap"); + + auto dev = pcpp::IFileReaderDevice::tryCreateReader("this-file-does-not-exist.pcap"); + PTF_ASSERT_NULL(dev); + + // Test existent file with wrong extension and bogus content + PTF_ASSERT_RAISES(pcpp::IFileReaderDevice::createReader(BOGUS_FILE_PATH), std::runtime_error, + "File format of PcapExamples/file_heuristics/bogus-content.txt is not supported"); + + dev = pcpp::IFileReaderDevice::tryCreateReader(BOGUS_FILE_PATH); + PTF_ASSERT_NULL(dev); +} + PTF_TEST_CASE(TestPcapFileReadWrite) { pcpp::PcapFileReaderDevice readerDev(EXAMPLE_PCAP_PATH); @@ -104,8 +302,10 @@ PTF_TEST_CASE(TestPcapFileMicroPrecision) std::array testPayload = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F }; - pcpp::RawPacket rawPacketMicro(testPayload.data(), testPayload.size(), timeval({ 1, 2 }), false); // 1.000002000 - pcpp::RawPacket rawPacketNano(testPayload.data(), testPayload.size(), timespec({ 1, 1234 }), false); // 1.000001234 + pcpp::RawPacket rawPacketMicro(testPayload.data(), testPayload.size(), timeval({ 1, 2 }), + false); // 1.000002000 + pcpp::RawPacket rawPacketNano(testPayload.data(), testPayload.size(), timespec({ 1, 1234 }), + false); // 1.000001234 // Write micro precision file pcpp::PcapFileWriterDevice writerDevMicro(EXAMPLE_PCAP_MICRO_WRITE_PATH, pcpp::LINKTYPE_ETHERNET, false); @@ -166,8 +366,10 @@ PTF_TEST_CASE(TestPcapFileNanoPrecision) std::array testPayload = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F }; - pcpp::RawPacket rawPacketMicro(testPayload.data(), testPayload.size(), timeval({ 1, 2 }), false); // 1.000002000 - pcpp::RawPacket rawPacketNano(testPayload.data(), testPayload.size(), timespec({ 1, 1234 }), false); // 1.000001234 + pcpp::RawPacket rawPacketMicro(testPayload.data(), testPayload.size(), timeval({ 1, 2 }), + false); // 1.000002000 + pcpp::RawPacket rawPacketNano(testPayload.data(), testPayload.size(), timespec({ 1, 1234 }), + false); // 1.000001234 // Write nano precision file pcpp::PcapFileWriterDevice writerDevNano(EXAMPLE_PCAP_NANO_WRITE_PATH, pcpp::LINKTYPE_ETHERNET, true); @@ -772,7 +974,7 @@ PTF_TEST_CASE(TestPcapNgFileReadWriteAdv) PTF_ASSERT_EQUAL(packetCount, 161); - // ------- + // ------- IFileReaderDevice::getReader() Factory // copy the .zstd file to a similar file with .zst extension std::ifstream zstdFile(EXAMPLE2_PCAPNG_ZSTD_WRITE_PATH, std::ios::binary); @@ -1004,8 +1206,8 @@ PTF_TEST_CASE(TestPcapFileWriterDeviceDestructor) // Create some pcaps in a nested scope to test cleanup on destruction. { - // create a file to leave open on destruction. If close is properly done on destruction, the contents & size of - // this file should match the next explicitly closed file. + // create a file to leave open on destruction. If close is properly done on destruction, the contents & size + // of this file should match the next explicitly closed file. pcpp::PcapFileWriterDevice writerDevDestructorNoClose(EXAMPLE_PCAP_DESTRUCTOR1_PATH, pcpp::LINKTYPE_ETHERNET, false); PTF_ASSERT_TRUE(writerDevDestructorNoClose.open()); diff --git a/Tests/Pcap++Test/main.cpp b/Tests/Pcap++Test/main.cpp index 25ee1eba7b..e25bb573d1 100644 --- a/Tests/Pcap++Test/main.cpp +++ b/Tests/Pcap++Test/main.cpp @@ -215,6 +215,15 @@ int main(int argc, char* argv[]) PTF_RUN_TEST(TestLogger, "no_network;logger"); PTF_RUN_TEST(TestLoggerMultiThread, "no_network;logger;skip_mem_leak_check"); + PTF_RUN_TEST(TestFileFormatDetector, "no_network"); + PTF_RUN_TEST(TestReaderFactory_Pcap_Micro, "no_network;pcap"); + PTF_RUN_TEST(TestReaderFactory_Pcap_Nano, "no_network;pcap"); + PTF_RUN_TEST(TestReaderFactory_Pcap_Nano_Unsupported, "no_network;pcap"); + PTF_RUN_TEST(TestReaderFactory_PcapNG, "no_network;pcapng"); + PTF_RUN_TEST(TestReaderFactory_PcapNG_ZST, "no_network;pcapng"); + PTF_RUN_TEST(TestReaderFactory_PcapNG_ZST_Unsupported, "no_network;pcapng"); + PTF_RUN_TEST(TestReaderFactory_Snoop, "no_network;snoop"); + PTF_RUN_TEST(TestReaderFactory_InvalidFile, "no_network;pcap"); PTF_RUN_TEST(TestPcapFileReadWrite, "no_network;pcap"); PTF_RUN_TEST(TestPcapFileMicroPrecision, "no_network;pcap"); PTF_RUN_TEST(TestPcapFileNanoPrecision, "no_network;pcap"); diff --git a/Tests/PcppTestFramework/PcppTestFramework.h b/Tests/PcppTestFramework/PcppTestFramework.h index 12defe2175..6f62cdd20f 100644 --- a/Tests/PcppTestFramework/PcppTestFramework.h +++ b/Tests/PcppTestFramework/PcppTestFramework.h @@ -34,8 +34,8 @@ std::cout << std::endl #define PTF_PRINT_ASSERTION(severity, op) \ - std::cout << std::left << std::setw(35) << __FUNCTION__ << ": " << severity << " (" << __FILE__ << ":" << __LINE__ \ - << "). " \ + std::cout << std::left << std::setw(PTF_TESTNAME_WIDTH) << __FUNCTION__ << ": " << severity << " (" << __FILE__ \ + << ":" << __LINE__ << "). " \ << "Assert " << op << " failed:" << std::endl #define PTF_PRINT_COMPARE_ASSERTION(severity, op, actualExp, actualVal, expectedExp, expectedVal, objType) \ @@ -274,7 +274,7 @@ #define PTF_PRINT_VERBOSE(data) \ if (printVerbose) \ { \ - std::cout << std::left << std::setw(35) << __FUNCTION__ << ": " \ + std::cout << std::left << std::setw(PTF_TESTNAME_WIDTH) << __FUNCTION__ << ": " \ << "[VERBOSE] " << data << std::endl; \ } @@ -282,7 +282,7 @@ { \ if (showSkipped) \ { \ - std::cout << std::left << std::setw(35) << __FUNCTION__ << ": " \ + std::cout << std::left << std::setw(PTF_TESTNAME_WIDTH) << __FUNCTION__ << ": " \ << "SKIPPED (" << why << ")" << std::endl; \ } \ ptfResult = PTF_RESULT_SKIPPED; \ diff --git a/Tests/PcppTestFramework/PcppTestFrameworkCommon.h b/Tests/PcppTestFramework/PcppTestFrameworkCommon.h index e0bad8494e..8851aa6f28 100644 --- a/Tests/PcppTestFramework/PcppTestFrameworkCommon.h +++ b/Tests/PcppTestFramework/PcppTestFrameworkCommon.h @@ -3,3 +3,7 @@ #define PTF_RESULT_PASSED 1 #define PTF_RESULT_FAILED 0 #define PTF_RESULT_SKIPPED -1 + +#ifndef PTF_TESTNAME_WIDTH +# define PTF_TESTNAME_WIDTH 45 +#endif // !PTF_TESTNAME_WIDTH diff --git a/Tests/PcppTestFramework/PcppTestFrameworkRun.h b/Tests/PcppTestFramework/PcppTestFrameworkRun.h index bd5cc0d20d..e3f5f8244f 100644 --- a/Tests/PcppTestFramework/PcppTestFrameworkRun.h +++ b/Tests/PcppTestFramework/PcppTestFrameworkRun.h @@ -62,7 +62,8 @@ static bool __ptfCheckTags(const std::string& tagSet, const std::string& tagSetT { \ if (showSkippedTests) \ { \ - std::cout << std::left << std::setw(35) << #TestName << ": SKIPPED (tags don't match)" << std::endl; \ + std::cout << std::left << std::setw(PTF_TESTNAME_WIDTH) << #TestName << ": SKIPPED (tags don't match)" \ + << std::endl; \ } \ TestName##_result = PTF_RESULT_SKIPPED; \ } \ @@ -82,7 +83,8 @@ static bool __ptfCheckTags(const std::string& tagSet, const std::string& tagSetT catch (std::exception const& e) \ { \ TestName##_result = PTF_RESULT_FAILED; \ - std::cout << std::left << std::setw(35) << #TestName << ": FAILED. Unhandled exception occurred! " \ + std::cout << std::left << std::setw(PTF_TESTNAME_WIDTH) << #TestName \ + << ": FAILED. Unhandled exception occurred! " \ << "Exception: " << e.what() << std::endl; \ } \ if (runMemLeakCheck) \ @@ -100,14 +102,15 @@ static bool __ptfCheckTags(const std::string& tagSet, const std::string& tagSetT if (memLeakCount > 0 || memLeakSize > 0) \ { \ TestName##_result = PTF_RESULT_FAILED; \ - std::cout << std::left << std::setw(35) << #TestName << ": FAILED. Memory leak found! " \ - << memLeakCount << " objects and " << memLeakSize << "[bytes] leaked" << std::endl; \ + std::cout << std::left << std::setw(PTF_TESTNAME_WIDTH) << #TestName \ + << ": FAILED. Memory leak found! " << memLeakCount << " objects and " << memLeakSize \ + << "[bytes] leaked" << std::endl; \ } \ } \ } \ if (TestName##_result == PTF_RESULT_PASSED) \ { \ - std::cout << std::left << std::setw(35) << #TestName << ": PASSED" << std::endl; \ + std::cout << std::left << std::setw(PTF_TESTNAME_WIDTH) << #TestName << ": PASSED" << std::endl; \ } \ } \ if (TestName##_result == PTF_RESULT_PASSED) \