MINIFICPP-1676 Check build identifier in extensions

Approved on GitHub by 5 people
Closes #1211
Signed-off-by: Marton Szasz <szaszm@apache.org>
Co-authored-by: Marton Szasz <szaszm@apache.org>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2a51f7f..25caf38 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -124,6 +124,14 @@
     return()
 endif()
 
+# Generate the build identifier if one is not provided
+if (NOT BUILD_IDENTIFIER)
+	string(RANDOM LENGTH 24 BUILD_IDENTIFIER)
+	set(BUILD_IDENTIFIER "${BUILD_IDENTIFIER}" CACHE STRING "Build identifier" FORCE)
+endif()
+
+message("BUILD_IDENTIFIER is ${BUILD_IDENTIFIER}")
+
 if (${FORCE_COLORED_OUTPUT})
 	message("CMAKE_CXX_COMPILER_ID is ${CMAKE_CXX_COMPILER_ID}")
 	if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
@@ -613,14 +621,6 @@
 
 get_property(selected_extensions GLOBAL PROPERTY EXTENSION-OPTIONS)
 
-# Generate the build identifier if one is not provided
-if (NOT BUILD_IDENTIFIER)
-	 string(RANDOM LENGTH 24 BUILD_IDENTIFIER)
-	 set(BUILD_IDENTIFIER "${BUILD_IDENTIFIER}" CACHE STRING "Build identifier" FORCE)
-endif()
-
-message("BUILD_IDENTIFIER is ${BUILD_IDENTIFIER}")
-
 if (WIN32)
 	# Get the latest abbreviated commit hash of the working branch
 	execute_process(
diff --git a/cmake/Extensions.cmake b/cmake/Extensions.cmake
index 73a383c..bd054ba 100644
--- a/cmake/Extensions.cmake
+++ b/cmake/Extensions.cmake
@@ -22,10 +22,31 @@
 
 set_property(GLOBAL PROPERTY EXTENSION-OPTIONS "")
 
+set(extension-build-info-file "${CMAKE_CURRENT_BINARY_DIR}/ExtensionBuildInfo.cpp")
+file(GENERATE OUTPUT ${extension-build-info-file}
+    CONTENT "\
+    #include \"utils/Export.h\"\n\
+    #ifdef BUILD_ID_VARIABLE_NAME\n\
+    EXTENSIONAPI extern const char* const BUILD_ID_VARIABLE_NAME = \"__EXTENSION_BUILD_IDENTIFIER_BEGIN__${BUILD_IDENTIFIER}__EXTENSION_BUILD_IDENTIFIER_END__\";\n\
+    #else\n\
+    static_assert(false, \"BUILD_ID_VARIABLE_NAME is not defined\");\n\
+    #endif\n")
+
+function(get_build_id_variable_name extension-name output)
+  string(REPLACE "-" "_" result ${extension-name})
+  string(APPEND result "_build_identifier")
+  set("${output}" "${result}" PARENT_SCOPE)
+endfunction()
+
 macro(register_extension extension-name)
   get_property(extensions GLOBAL PROPERTY EXTENSION-OPTIONS)
   set_property(GLOBAL APPEND PROPERTY EXTENSION-OPTIONS ${extension-name})
-  target_compile_definitions(${extension-name} PRIVATE "MODULE_NAME=${extension-name}")
+  get_build_id_variable_name(${extension-name} build-id-variable-name)
+  set_source_files_properties(${extension-build-info-file} PROPERTIES GENERATED TRUE)
+  target_sources(${extension-name} PRIVATE ${extension-build-info-file})
+  target_compile_definitions(${extension-name}
+      PRIVATE "MODULE_NAME=${extension-name}"
+      PRIVATE "BUILD_ID_VARIABLE_NAME=${build-id-variable-name}")
   set_target_properties(${extension-name} PROPERTIES
           ENABLE_EXPORTS True
           POSITION_INDEPENDENT_CODE ON)
diff --git a/libminifi/include/core/extension/Utils.h b/libminifi/include/core/extension/Utils.h
new file mode 100644
index 0000000..693844e
--- /dev/null
+++ b/libminifi/include/core/extension/Utils.h
@@ -0,0 +1,98 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <algorithm>
+#include <fstream>
+#include <functional>
+#include <iterator>
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+
+#include "utils/gsl.h"
+#include "utils/StringUtils.h"
+#include "utils/file/FileUtils.h"
+
+namespace org::apache::nifi::minifi::core::extension::internal {
+
+template<typename Callback>
+class Timer {
+ public:
+  explicit Timer(Callback cb): cb_(std::move(cb)) {}
+  ~Timer() {
+    cb_(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start_));
+  }
+ private:
+  std::chrono::steady_clock::time_point start_{std::chrono::steady_clock::now()};
+  Callback cb_;
+};
+
+struct LibraryDescriptor {
+  std::string name;
+  std::filesystem::path dir;
+  std::string filename;
+
+  [[nodiscard]]
+  bool verify(const std::shared_ptr<logging::Logger>& logger) const {
+    const auto path = getFullPath();
+    if (!std::filesystem::exists(path)) {
+      throw std::runtime_error{"File not found: " + path.string()};
+    }
+    const Timer timer{[&](const std::chrono::milliseconds elapsed) {
+      core::logging::LOG_DEBUG(logger) << "Verification for '" << path << "' took " << elapsed.count() << " ms";
+    }};
+    const std::string_view begin_marker = "__EXTENSION_BUILD_IDENTIFIER_BEGIN__";
+    const std::string_view end_marker = "__EXTENSION_BUILD_IDENTIFIER_END__";
+    const std::string magic_constant = utils::StringUtils::join_pack(begin_marker, AgentBuild::BUILD_IDENTIFIER, end_marker);
+    return utils::file::contains(path, magic_constant);
+  }
+
+  [[nodiscard]]
+  std::filesystem::path getFullPath() const {
+    return dir / filename;
+  }
+};
+
+std::optional<LibraryDescriptor> asDynamicLibrary(const std::filesystem::path& path) {
+#if defined(WIN32)
+  static const std::string_view extension = ".dll";
+#elif defined(__APPLE__)
+  static const std::string_view extension = ".dylib";
+#else
+  static const std::string_view extension = ".so";
+#endif
+
+#ifdef WIN32
+  static const std::string_view prefix = "";
+#else
+  static const std::string_view prefix = "lib";
+#endif
+  const std::string filename = path.filename().string();
+  if (!utils::StringUtils::startsWith(filename, prefix) || !utils::StringUtils::endsWith(filename, extension)) {
+    return {};
+  }
+  return LibraryDescriptor{
+      filename.substr(prefix.length(), filename.length() - extension.length() - prefix.length()),
+      path.parent_path(),
+      filename
+  };
+}
+
+}  // namespace org::apache::nifi::minifi::core::extension::internal
diff --git a/libminifi/include/utils/Enum.h b/libminifi/include/utils/Enum.h
index 178b9c1..9921030 100644
--- a/libminifi/include/utils/Enum.h
+++ b/libminifi/include/utils/Enum.h
@@ -34,6 +34,7 @@
 #define INCLUDE_BASE_FIELD(x) \
   x = Base::x
 
+// [[maybe_unused]] on public members to avoid warnings when used inside an anonymous namespace
 #define SMART_ENUM_BODY(Clazz, ...) \
     constexpr Clazz(Type value = static_cast<Type>(-1)) : value_{value} {} \
     explicit Clazz(const std::string& str) : value_{parse(str.c_str()).value_} {} \
@@ -41,7 +42,7 @@
    private: \
     Type value_; \
    public: \
-    Type value() const { \
+    [[maybe_unused]] Type value() const { \
       return value_; \
     } \
     struct detail : Base::detail { \
@@ -67,35 +68,35 @@
       } \
     }; \
     static constexpr int length = Base::length + COUNT(__VA_ARGS__); \
-    friend const char* toString(Type a) { \
+    [[maybe_unused]] friend const char* toString(Type a) { \
       return detail::toStringImpl(a, #Clazz); \
     } \
-    const char* toString() const { \
+    [[maybe_unused]] const char* toString() const { \
       return detail::toStringImpl(value_, #Clazz); \
     } \
-    const char* toStringOr(const char* fallback) const { \
+    [[maybe_unused]] const char* toStringOr(const char* fallback) const { \
       if (*this) { \
         return toString(); \
       } \
       return fallback; \
     } \
-    static std::set<std::string> values() { \
+    [[maybe_unused]] static std::set<std::string> values() { \
       return detail::values(); \
     } \
-    friend bool operator==(Clazz lhs, Clazz rhs) { \
+    [[maybe_unused]] friend bool operator==(Clazz lhs, Clazz rhs) { \
       return lhs.value_ == rhs.value_; \
     } \
-    friend bool operator!=(Clazz lhs, Clazz rhs) { \
+    [[maybe_unused]] friend bool operator!=(Clazz lhs, Clazz rhs) { \
       return lhs.value_ != rhs.value_; \
     } \
-    friend bool operator<(Clazz lhs, Clazz rhs) { \
+    [[maybe_unused]] friend bool operator<(Clazz lhs, Clazz rhs) { \
       return lhs.value_ < rhs.value_;\
     } \
-    explicit operator bool() const { \
+    [[maybe_unused]] explicit operator bool() const { \
       int idx = static_cast<int>(value_); \
       return 0 <= idx && idx < length; \
     } \
-    static Clazz parse(const char* str, const ::std::optional<Clazz>& fallback = {}, bool caseSensitive = true) { \
+    [[maybe_unused]] static Clazz parse(const char* str, const ::std::optional<Clazz>& fallback = {}, bool caseSensitive = true) { \
       for (int idx = 0; idx < length; ++idx) { \
         if (::org::apache::nifi::minifi::utils::StringUtils::equals(str, detail::toStringImpl(static_cast<Type>(idx), #Clazz), caseSensitive)) \
           return static_cast<Type>(idx); \
@@ -106,7 +107,7 @@
       throw std::runtime_error(std::string("Cannot convert \"") + str + "\" to " #Clazz); \
     } \
     template<typename T, typename = typename std::enable_if<std::is_base_of<typename T::detail, detail>::value>::type> \
-    T cast() const { \
+    [[maybe_unused]] T cast() const { \
       if (0 <= value_ && value_ < T::length) { \
         return static_cast<typename T::Type>(value_); \
       } \
diff --git a/libminifi/include/utils/StringUtils.h b/libminifi/include/utils/StringUtils.h
index 5f0ebb0..a4e4e1c 100644
--- a/libminifi/include/utils/StringUtils.h
+++ b/libminifi/include/utils/StringUtils.h
@@ -209,6 +209,9 @@
   template<typename CharT>
   static size_t size(const CharT* str) noexcept { return std::char_traits<CharT>::length(str); }
 
+  template<typename CharT>
+  static size_t size(const std::basic_string_view<CharT>& str) noexcept { return str.size(); }
+
   struct detail {
     // partial detection idiom impl, from cppreference.com
     struct nonesuch{};
@@ -267,6 +270,12 @@
     return detail::join_pack<CharT>(head, tail...);
   }
 
+  template<typename CharT, typename... Strs>
+  static detail::valid_string_pack_t<std::basic_string<CharT>, CharT, Strs...>
+  join_pack(const std::basic_string_view<CharT>& head, const Strs&... tail) {
+    return detail::join_pack<CharT>(head, tail...);
+  }
+
   /**
    * Concatenates strings stored in an arbitrary container using the provided separator.
    * @tparam TChar char type of the string (char or wchar_t)
diff --git a/libminifi/include/utils/file/FileUtils.h b/libminifi/include/utils/file/FileUtils.h
index 89884b1..c629948 100644
--- a/libminifi/include/utils/file/FileUtils.h
+++ b/libminifi/include/utils/file/FileUtils.h
@@ -17,6 +17,7 @@
 #ifndef LIBMINIFI_INCLUDE_UTILS_FILE_FILEUTILS_H_
 #define LIBMINIFI_INCLUDE_UTILS_FILE_FILEUTILS_H_
 
+#include <filesystem>
 #include <fstream>
 #include <memory>
 #include <sstream>
@@ -821,6 +822,8 @@
   return content;
 }
 
+bool contains(const std::filesystem::path& file_path, std::string_view text_to_search);
+
 }  // namespace file
 }  // namespace utils
 }  // namespace minifi
diff --git a/libminifi/src/core/extension/ExtensionManager.cpp b/libminifi/src/core/extension/ExtensionManager.cpp
index fb0af5e..2295276 100644
--- a/libminifi/src/core/extension/ExtensionManager.cpp
+++ b/libminifi/src/core/extension/ExtensionManager.cpp
@@ -16,11 +16,15 @@
  */
 
 #include "core/extension/ExtensionManager.h"
+
+#include <algorithm>
+
 #include "core/logging/LoggerConfiguration.h"
-#include "utils/file/FileUtils.h"
 #include "core/extension/Executable.h"
 #include "utils/file/FilePattern.h"
 #include "core/extension/DynamicLibrary.h"
+#include "agent/agent_version.h"
+#include "core/extension/Utils.h"
 
 namespace org {
 namespace apache {
@@ -29,51 +33,6 @@
 namespace core {
 namespace extension {
 
-namespace {
-
-struct LibraryDescriptor {
-  std::string name;
-  std::filesystem::path dir;
-  std::string filename;
-
-  bool verify(const std::shared_ptr<logging::Logger>& /*logger*/) const {
-    // TODO(adebreceni): check signature
-    return true;
-  }
-
-  [[nodiscard]]
-  std::filesystem::path getFullPath() const {
-    return dir / filename;
-  }
-};
-
-std::optional<LibraryDescriptor> asDynamicLibrary(const std::filesystem::path& path) {
-#if defined(WIN32)
-  const std::string extension = ".dll";
-#elif defined(__APPLE__)
-  const std::string extension = ".dylib";
-#else
-  const std::string extension = ".so";
-#endif
-
-#ifdef WIN32
-  const std::string prefix = "";
-#else
-  const std::string prefix = "lib";
-#endif
-  std::string filename = path.filename().string();
-  if (!utils::StringUtils::startsWith(filename, prefix) || !utils::StringUtils::endsWith(filename, extension)) {
-    return {};
-  }
-  return LibraryDescriptor{
-    filename.substr(prefix.length(), filename.length() - extension.length() - prefix.length()),
-    path.parent_path(),
-    filename
-  };
-}
-
-}  // namespace
-
 const std::shared_ptr<logging::Logger> ExtensionManager::logger_ = logging::LoggerFactory<ExtensionManager>::getLogger();
 
 ExtensionManager::ExtensionManager() {
@@ -108,7 +67,7 @@
       logger_->log_error("Error in subpattern '%s': %s", std::string{subpattern}, std::string{error_msg});
     }));
     for (const auto& candidate : candidates) {
-      auto library = asDynamicLibrary(candidate);
+      auto library = internal::asDynamicLibrary(candidate);
       if (!library || !library->verify(logger_)) {
         continue;
       }
diff --git a/libminifi/src/utils/file/FileUtils.cpp b/libminifi/src/utils/file/FileUtils.cpp
index a6bba2f..e0a7f98 100644
--- a/libminifi/src/utils/file/FileUtils.cpp
+++ b/libminifi/src/utils/file/FileUtils.cpp
@@ -48,6 +48,44 @@
   return checksum;
 }
 
+bool contains(const std::filesystem::path& file_path, std::string_view text_to_search) {
+  gsl_Expects(text_to_search.size() <= 8192);
+  gsl_ExpectsAudit(std::filesystem::exists(file_path));
+  std::array<char, 8192> buf1{};
+  std::array<char, 8192> buf2{};
+  gsl::span<char> left = buf1;
+  gsl::span<char> right = buf2;
+
+  const auto charat = [&](size_t idx) {
+    if (idx < left.size()) {
+      return left[idx];
+    } else if (idx < left.size() + right.size()) {
+      return right[idx - left.size()];
+    } else {
+      return '\0';
+    }
+  };
+  const auto check_range = [&](size_t start, size_t end) -> size_t {
+    for (size_t i = start; i < end; ++i) {
+      size_t j{};
+      for (j = 0; j < text_to_search.size(); ++j) {
+        if (charat(i + j) != text_to_search[j]) break;
+      }
+      if (j == text_to_search.size()) return true;
+    }
+    return false;
+  };
+
+  std::ifstream ifs{file_path, std::ios::binary};
+  ifs.read(right.data(), gsl::narrow<std::streamsize>(right.size()));
+  do {
+    std::swap(left, right);
+    ifs.read(right.data(), gsl::narrow<std::streamsize>(right.size()));
+    if (check_range(0, left.size())) return true;
+  } while (ifs);
+  return check_range(left.size(), left.size() + right.size());
+}
+
 }  // namespace file
 }  // namespace utils
 }  // namespace minifi
diff --git a/libminifi/test/unit/ExtensionVerificationTests.cpp b/libminifi/test/unit/ExtensionVerificationTests.cpp
new file mode 100644
index 0000000..ff09206
--- /dev/null
+++ b/libminifi/test/unit/ExtensionVerificationTests.cpp
@@ -0,0 +1,99 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define CUSTOM_EXTENSION_INIT
+#undef NDEBUG
+
+#include <filesystem>
+#include "../TestBase.h"
+#include "agent/agent_version.h"
+#include "core/extension/Utils.h"
+#include "utils/IntegrationTestUtils.h"
+
+using namespace std::literals;
+
+namespace {
+
+#if defined(WIN32)
+const std::string extension_file = "extension.dll";
+#elif defined(__APPLE__)
+const std::string extension_file = "libextension.dylib";
+#else
+const std::string extension_file = "libextension.so";
+#endif
+
+
+struct Fixture : public TestController {
+  Fixture() {
+    extension_ = std::filesystem::path(createTempDirectory()) / extension_file;
+  }
+  std::filesystem::path extension_;
+};
+
+const std::shared_ptr<logging::Logger> logger{core::logging::LoggerFactory<Fixture>::getLogger()};
+
+}  // namespace
+
+TEST_CASE_METHOD(Fixture, "Could load extension with matching build id") {
+  std::ofstream{extension_} << "__EXTENSION_BUILD_IDENTIFIER_BEGIN__"
+      << minifi::AgentBuild::BUILD_IDENTIFIER << "__EXTENSION_BUILD_IDENTIFIER_END__";
+
+  auto lib = minifi::core::extension::internal::asDynamicLibrary(extension_);
+  REQUIRE(lib);
+  REQUIRE(lib->verify(logger));
+}
+
+TEST_CASE_METHOD(Fixture, "Can't load extension if the build id begin marker is missing") {
+  std::ofstream{extension_} << "__MISSING_BEGIN__"
+      << minifi::AgentBuild::BUILD_IDENTIFIER << "__EXTENSION_BUILD_IDENTIFIER_END__";
+
+  auto lib = minifi::core::extension::internal::asDynamicLibrary(extension_);
+  REQUIRE(lib);
+  REQUIRE_FALSE(lib->verify(logger));
+}
+
+TEST_CASE_METHOD(Fixture, "Can't load extension if the build id end marker is missing") {
+  std::ofstream{extension_} << "__EXTENSION_BUILD_IDENTIFIER_BEGIN__"
+      << minifi::AgentBuild::BUILD_IDENTIFIER << "__MISSING_END__";
+
+  auto lib = minifi::core::extension::internal::asDynamicLibrary(extension_);
+  REQUIRE(lib);
+  REQUIRE_FALSE(lib->verify(logger));
+}
+
+TEST_CASE_METHOD(Fixture, "Can't load extension if the build id does not match") {
+  std::ofstream{extension_} << "__EXTENSION_BUILD_IDENTIFIER_BEGIN__"
+      << "not the build id" << "__EXTENSION_BUILD_IDENTIFIER_END__";
+
+  auto lib = minifi::core::extension::internal::asDynamicLibrary(extension_);
+  REQUIRE(lib);
+  REQUIRE_FALSE(lib->verify(logger));
+}
+
+TEST_CASE_METHOD(Fixture, "Can't load extension if the file does not exist") {
+  auto lib = minifi::core::extension::internal::asDynamicLibrary(extension_);
+  REQUIRE(lib);
+  REQUIRE_THROWS_AS(lib->verify(logger), std::runtime_error);
+}
+
+TEST_CASE_METHOD(Fixture, "Can't load extension if the file has zero length") {
+  std::ofstream{extension_};
+
+  auto lib = minifi::core::extension::internal::asDynamicLibrary(extension_);
+  REQUIRE(lib);
+  REQUIRE_FALSE(lib->verify(logger));
+}
diff --git a/libminifi/test/unit/FileUtilsTests.cpp b/libminifi/test/unit/FileUtilsTests.cpp
index 43d5687..b615edc 100644
--- a/libminifi/test/unit/FileUtilsTests.cpp
+++ b/libminifi/test/unit/FileUtilsTests.cpp
@@ -403,3 +403,65 @@
   REQUIRE(FileUtils::delete_dir("") != 0);
   REQUIRE(FileUtils::delete_dir("", false) != 0);
 }
+
+TEST_CASE("FileUtils::contains", "[utils][file][contains]") {
+  TestController test_controller;
+  const auto temp_dir = std::filesystem::path{test_controller.createTempDirectory()};
+
+  SECTION("< 8k") {
+    const auto file_path = temp_dir / "test_short.txt";
+    std::ofstream{file_path} << "This is a test file";
+    REQUIRE(utils::file::contains(file_path, "This"));
+    REQUIRE(utils::file::contains(file_path, "test"));
+    REQUIRE(utils::file::contains(file_path, "file"));
+    REQUIRE_FALSE(utils::file::contains(file_path, "hello"));
+    REQUIRE_FALSE(utils::file::contains(file_path, "Thiss"));
+    REQUIRE_FALSE(utils::file::contains(file_path, "ffile"));
+  }
+  SECTION("< 16k") {
+    const auto file_path = temp_dir / "test_mid.txt";
+    {
+      std::string contents;
+      contents.resize(10240);
+      for (size_t i = 0; i < contents.size(); ++i) {
+        contents[i] = gsl::narrow<char>('a' + gsl::narrow<int>(i % size_t{'z' - 'a' + 1}));
+      }
+      const std::string_view src = "12 34 56 Test String";
+      contents.replace(8190, src.size(), src);
+      std::ofstream ofs{file_path};
+      ofs.write(contents.data(), gsl::narrow<std::streamsize>(contents.size()));
+    }
+
+    REQUIRE(utils::file::contains(file_path, "xyz"));
+    REQUIRE(utils::file::contains(file_path, "12"));
+    REQUIRE(utils::file::contains(file_path, " 34"));
+    REQUIRE(utils::file::contains(file_path, "12 34"));
+    REQUIRE_FALSE(utils::file::contains(file_path, "1234"));
+    REQUIRE(utils::file::contains(file_path, "String"));
+  }
+  SECTION("> 16k") {
+    const auto file_path = temp_dir / "test_long.txt";
+    std::string buf;
+    buf.resize(8192);
+    {
+      for (size_t i = 0; i < buf.size(); ++i) {
+        buf[i] = gsl::narrow<char>('A' + gsl::narrow<int>(i % size_t{'Z' - 'A' + 1}));
+      }
+      std::ofstream ofs{file_path};
+      ofs.write(buf.data(), gsl::narrow<std::streamsize>(buf.size()));
+      ofs << " apple banana orange 1234 ";
+      ofs.write(buf.data(), gsl::narrow<std::streamsize>(buf.size()));
+    }
+
+    REQUIRE(utils::file::contains(file_path, std::string_view{buf.data(), buf.size()}));
+    std::rotate(std::begin(buf), std::next(std::begin(buf), 6), std::end(buf));
+    buf.replace(8192 - 6, 6, " apple", 6);
+    REQUIRE(utils::file::contains(file_path, std::string_view{buf.data(), buf.size()}));
+    buf.replace(8192 - 6, 6, "banana", 6);
+    REQUIRE_FALSE(utils::file::contains(file_path, std::string_view{buf.data(), buf.size()}));
+
+    REQUIRE(utils::file::contains(file_path, "apple"));
+    REQUIRE(utils::file::contains(file_path, "banana"));
+    REQUIRE(utils::file::contains(file_path, "ABC"));
+  }
+}
diff --git a/libminifi/test/unit/ProcessorConfigUtilsTests.cpp b/libminifi/test/unit/ProcessorConfigUtilsTests.cpp
index b385b60..7e0a10a 100644
--- a/libminifi/test/unit/ProcessorConfigUtilsTests.cpp
+++ b/libminifi/test/unit/ProcessorConfigUtilsTests.cpp
@@ -27,16 +27,11 @@
   using Processor::Processor;
 };
 
-#pragma GCC diagnostic push
-#pragma GCC diagnostic warning "-Wunused-function"
-
 SMART_ENUM(TestEnum,
   (A, "A"),
   (B, "B")
 )
 
-#pragma GCC diagnostic pop
-
 TEST_CASE("Parse enum property") {
   auto prop = PropertyBuilder::createProperty("prop")
       ->withAllowableValues(TestEnum::values())