diff --git a/src/rrdEventProcess.c b/src/rrdEventProcess.c index 5164e7832..86c978014 100644 --- a/src/rrdEventProcess.c +++ b/src/rrdEventProcess.c @@ -79,7 +79,10 @@ void processIssueTypeEvent(data_buf *rbuf) cmdBuff = (data_buf *)malloc(sizeof(data_buf)); if (cmdBuff) { - dataMsgLen = strlen(cmdMap[index]) + 1; + char base[BUF_LEN_128] = {0}; + char local_suffix[BUF_LEN_128] = {0}; + split_issue_type(cmdMap[index], base, sizeof(base), local_suffix, sizeof(local_suffix)); + dataMsgLen = strlen(base) + 1; RRD_data_buff_init(cmdBuff, EVENT_MSG, RRD_DEEPSLEEP_INVALID_DEFAULT); /* Setting Deafult Values*/ cmdBuff->inDynamic = rbuf->inDynamic; if(cmdBuff->inDynamic) @@ -88,9 +91,23 @@ void processIssueTypeEvent(data_buf *rbuf) } cmdBuff->appendMode = rbuf->appendMode; cmdBuff->mdata = (char *)calloc(1, dataMsgLen); + /* Suffix is now persisted via file, no struct field needed */ if (cmdBuff->mdata) { - strncpy((char *)cmdBuff->mdata, cmdMap[index], dataMsgLen); + strncpy((char *)cmdBuff->mdata, base, dataMsgLen); + /* Only persist suffix if input contains an underscore (i.e., is not just the base name) */ + if (strchr(cmdMap[index], '_') && local_suffix[0] != '\0') + { + RDK_LOG(RDK_LOG_DEBUG, LOG_REMDEBUG, "[%s:%d]: [DEBUG] Persisting suffix: '%s' from input: '%s' (index=%d)\n", __FUNCTION__, __LINE__, local_suffix, cmdMap[index], index); + if (persist_suffix_to_file(local_suffix) != 0) + { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] Failed to persist suffix '%s' from input: '%s' (index=%d)\n", __FUNCTION__, __LINE__, local_suffix, cmdMap[index], index); + } + } + else + { + RDK_LOG(RDK_LOG_DEBUG, LOG_REMDEBUG, "[%s:%d]: [DEBUG] Not persisting suffix for input: '%s' (index=%d)\n", __FUNCTION__, __LINE__, cmdMap[index], index); + } processIssueType(cmdBuff); } else @@ -628,7 +645,7 @@ static void freeParsedJson(cJSON *jsonParsed) /* * @function removeSpecialCharacterfromIssueTypeList * @brief Removes special characters from the issue type list, retaining only alphanumeric - * characters, commas, and periods. + * characters, commas, periods, underscores and hyphens * @param char *str - The string from which special characters will be removed. * @return void */ @@ -639,7 +656,7 @@ static void removeSpecialCharacterfromIssueTypeList(char *str) while (str[source] != '\0') { - if (isalnum(str[source]) || str[source] == ',' || str[source] == '.') + if (isalnum((unsigned char)str[source]) || str[source] == ',' || str[source] == '.' || str[source] == '_'|| str[source] == '-') { str[destination] = str[source]; ++destination; diff --git a/src/rrdJsonParser.c b/src/rrdJsonParser.c index e06d93ac2..03206a0e7 100644 --- a/src/rrdJsonParser.c +++ b/src/rrdJsonParser.c @@ -23,7 +23,14 @@ #include "rrdCommandSanity.h" #include #include +#include #include +#include +#include + +#define RRD_SUFFIX_DIR "/tmp/rrd" +#define RRD_SUFFIX_PATH "/tmp/rrd/rrd_suffix.txt" + /* * @function removeSpecialChar @@ -46,6 +53,225 @@ void removeSpecialChar(char *str) } } +int persist_suffix_to_file(const char *suffix) { + struct stat st; + uid_t uid = getuid(); + int dirfd = open(RRD_SUFFIX_DIR, O_NOFOLLOW | O_DIRECTORY); + if (dirfd == -1) { + // Directory does not exist, try to create + if (mkdir(RRD_SUFFIX_DIR, 0700) != 0 && errno != EEXIST) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] Failed to create %s: %s\n", __FUNCTION__, __LINE__, RRD_SUFFIX_DIR, strerror(errno)); + return -1; + } + dirfd = open(RRD_SUFFIX_DIR, O_NOFOLLOW | O_DIRECTORY); + if (dirfd == -1) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] Could not open %s after creation: %s\n", __FUNCTION__, __LINE__, RRD_SUFFIX_DIR, strerror(errno)); + return -1; + } + } + if (fstat(dirfd, &st) != 0 || !S_ISDIR(st.st_mode) || st.st_uid != uid || (st.st_mode & 0777) != 0700) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] %s not owned by current user or not mode 0700!\n", __FUNCTION__, __LINE__, RRD_SUFFIX_DIR); + close(dirfd); + return -1; + } + close(dirfd); + + // Write to a temp file first + char tmp_path[256]; + snprintf(tmp_path, sizeof(tmp_path), "%s/.rrd_suffix.tmpXXXXXX", RRD_SUFFIX_DIR); + mode_t old_umask = umask(077); + int tmpfd = mkstemp(tmp_path); + umask(old_umask); + if (tmpfd == -1) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] Failed to create temp file in %s: %s\n", __FUNCTION__, __LINE__, RRD_SUFFIX_DIR, strerror(errno)); + return -1; + } + // Set restrictive permissions + if (fchmod(tmpfd, 0600) != 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] Failed to set permissions on temp file: %s\n", __FUNCTION__, __LINE__, strerror(errno)); + close(tmpfd); + if (unlink(tmp_path) != 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] unlink failed on %s: %s\n", __FUNCTION__, __LINE__, tmp_path, strerror(errno)); + } + return -1; + } + // Write suffix + if (suffix && suffix[0] != '\0') { + size_t len = strlen(suffix); + size_t total_written = 0; + while (total_written < len) { + /* Coverity fix: ensure no underflow/overflow in len - total_written */ + if (total_written > len) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] total_written (%zu) > len (%zu), possible integer overflow\n", __FUNCTION__, __LINE__, total_written, len); + close(tmpfd); + if (unlink(tmp_path) != 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] unlink failed on %s: %s\n", __FUNCTION__, __LINE__, tmp_path, strerror(errno)); + } + return -1; + } + size_t to_write = len - total_written; + if (to_write > SSIZE_MAX) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] to_write (%zu) > SSIZE_MAX (%zd), refusing to write\n", __FUNCTION__, __LINE__, to_write, (ssize_t)SSIZE_MAX); + close(tmpfd); + if (unlink(tmp_path) != 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] unlink failed on %s: %s\n", __FUNCTION__, __LINE__, tmp_path, strerror(errno)); + } + return -1; + } + if (to_write == 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] to_write is 0, nothing to write\n", __FUNCTION__, __LINE__); + close(tmpfd); + if (unlink(tmp_path) != 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] unlink failed on %s: %s\n", __FUNCTION__, __LINE__, tmp_path, strerror(errno)); + } + return -1; + } + /* Defensive: never cast a negative value, but size_t is unsigned, so only check upper bound */ + ssize_t write_len = (ssize_t)to_write; + if (write_len <= 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] write_len (%zd) <= 0, refusing to write\n", __FUNCTION__, __LINE__, write_len); + close(tmpfd); + if (unlink(tmp_path) != 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] unlink failed on %s: %s\n", __FUNCTION__, __LINE__, tmp_path, strerror(errno)); + } + return -1; + } + ssize_t written = write(tmpfd, suffix + total_written, write_len); + if (written == -1) { + if (errno == EINTR) + continue; + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] Failed to write suffix to temp file: %s\n", __FUNCTION__, __LINE__, strerror(errno)); + close(tmpfd); + if (unlink(tmp_path) != 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] unlink failed on %s: %s\n", __FUNCTION__, __LINE__, tmp_path, strerror(errno)); + } + return -1; + } + if (written == 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] Short write (0 bytes) to temp file\n", __FUNCTION__, __LINE__); + close(tmpfd); + if (unlink(tmp_path) != 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] unlink failed on %s: %s\n", __FUNCTION__, __LINE__, tmp_path, strerror(errno)); + } + return -1; + } + total_written += (size_t)written; + } + } + // Flush and verify + if (fsync(tmpfd) != 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] fsync failed on temp file: %s\n", __FUNCTION__, __LINE__, strerror(errno)); + close(tmpfd); + if (unlink(tmp_path) != 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] unlink failed on %s: %s\n", __FUNCTION__, __LINE__, tmp_path, strerror(errno)); + } + return -1; + } + struct stat fst; + if (fstat(tmpfd, &fst) != 0 || !S_ISREG(fst.st_mode)) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] Temp file is not a regular file!\n", __FUNCTION__, __LINE__); + close(tmpfd); + if (unlink(tmp_path) != 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] unlink failed on %s: %s\n", __FUNCTION__, __LINE__, tmp_path, strerror(errno)); + } + return -1; + } + close(tmpfd); + + // Atomically rename temp file to target + if (rename(tmp_path, RRD_SUFFIX_PATH) != 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] Failed to rename temp file to %s: %s\n", __FUNCTION__, __LINE__, RRD_SUFFIX_PATH, strerror(errno)); + if (unlink(tmp_path) != 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] unlink failed on %s: %s\n", __FUNCTION__, __LINE__, tmp_path, strerror(errno)); + } + return -1; + } + return 0; +} + +void read_suffix_from_file_to_buf(char *buf, size_t buflen) +{ + if (!buf || buflen == 0) return; + int fd = open(RRD_SUFFIX_PATH, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd == -1) { + buf[0] = '\0'; + return; + } + struct stat st; + if (fstat(fd, &st) != 0 || !S_ISREG(st.st_mode) || st.st_uid != getuid() || (st.st_mode & 022) != 0) { + // Not a regular file, not owned by us, or group/other-writable + buf[0] = '\0'; + close(fd); + return; + } + FILE *fp = fdopen(fd, "r"); + if (!fp) { + buf[0] = '\0'; + close(fd); + return; + } + if (fgets(buf, buflen, fp) == NULL) { + buf[0] = '\0'; + fclose(fp); + return; + } + fclose(fp); + size_t len = strlen(buf); + if (len > 0 && buf[len-1] == '\n') buf[len-1] = '\0'; +} + +/* + * @function split_issue_type + * @brief Utility to split base and suffix from issue type string. + * Example: Input: Device.DeviceTime_Search-b6877385-9463-45fc-b19d-a24d77fd0790 + * Output: base = Device.DeviceTime, suffix = _Search-b6877385-9463-45fc-b19d-a24d77fd0790 + * @param const char *input - The input string to split. + * @param char *base - Buffer to store the base part (before the first underscore). + * @param size_t base_len - Size of the base buffer. + * @param char *suffix - Buffer to store the suffix part (from the first underscore onwards). + * @param size_t suffix_len - Size of the suffix buffer. + * @return void + */ +void split_issue_type(const char *input, char *base, size_t base_len, char *suffix, size_t suffix_len) { + if (base && base_len > 0) + { + base[0] = '\0'; + } + if (suffix && suffix_len > 0) + { + suffix[0] = '\0'; + } + + if (!input || !base || !suffix) + { + return; + } + + if (base_len == 0 || suffix_len == 0) + { + return; + } + RDK_LOG(RDK_LOG_DEBUG, LOG_REMDEBUG, "[%s:%d]: split_issue_type called with input='%s'\n", __FUNCTION__, __LINE__, input); + const char *underscore = strchr(input, '_'); + if (underscore) + { + size_t b_len = underscore - input; + if (b_len >= base_len) b_len = base_len - 1; + strncpy(base, input, b_len); + base[b_len] = '\0'; + strncpy(suffix, underscore, suffix_len - 1); + suffix[suffix_len - 1] = '\0'; + } + else + { + strncpy(base, input, base_len - 1); + base[base_len - 1] = '\0'; + suffix[0] = '\0'; + } + RDK_LOG(RDK_LOG_DEBUG, LOG_REMDEBUG, "[%s:%d]: split_issue_type result: base='%s', suffix='%s'\n", __FUNCTION__, __LINE__, base, suffix); +} + + /* * @function getParamcount * @brief Calculates the total number of nodes (elements) in the input string, excluding delimiters. @@ -516,6 +742,9 @@ void checkIssueNodeInfo(issueNodeData *issuestructNode, cJSON *jsoncfg, data_buf RDK_LOG(RDK_LOG_ERROR,LOG_REMDEBUG,"[%s:%d]: Memory allocation failed for rfcbuf\n",__FUNCTION__,__LINE__); free(buff->mdata); // free rfc data free(buff->jsonPath); // free rrd path info + if (persist_suffix_to_file("") != 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] Failed to clear suffix file on early exit\n", __FUNCTION__, __LINE__); + } return; } @@ -536,6 +765,9 @@ void checkIssueNodeInfo(issueNodeData *issuestructNode, cJSON *jsoncfg, data_buf free(rfcbuf); // free duplicated rfc data free(buff->mdata); // free rfc data free(buff->jsonPath); // free rrd path info + if (persist_suffix_to_file("") != 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] Failed to clear suffix file on early exit\n", __FUNCTION__, __LINE__); + } return; } else @@ -576,7 +808,29 @@ void checkIssueNodeInfo(issueNodeData *issuestructNode, cJSON *jsoncfg, data_buf else { RDK_LOG(RDK_LOG_DEBUG,LOG_REMDEBUG,"[%s:%d]: Continue uploading Debug Report for %s from %s... \n",__FUNCTION__,__LINE__,buff->mdata,outdir); - status = uploadDebugoutput(outdir,buff->mdata); + // Use the persisted suffix from file for upload + char suffix[128] = {0}; + read_suffix_from_file_to_buf(suffix, sizeof(suffix)); + char tarName[512] = {0}; + int tar_name_len = 0; + if (suffix[0] != '\0') { + tar_name_len = snprintf(tarName, sizeof(tarName), "%s%s", buff->mdata, suffix); + } + else + { + tar_name_len = snprintf(tarName, sizeof(tarName), "%s", buff->mdata); + } + if ((tar_name_len < 0) || ((size_t)tar_name_len >= sizeof(tarName))) + { + RDK_LOG(RDK_LOG_ERROR,LOG_REMDEBUG,"[%s:%d]: Failed to build upload file name for %s. snprintf result:%d, buffer size:%zu\n", __FUNCTION__,__LINE__,buff->mdata,tar_name_len,sizeof(tarName)); + status = -1; + } + else + { + RDK_LOG(RDK_LOG_INFO, LOG_REMDEBUG, "[%s:%d]: [INFO] Tar file name for upload: '%s'\n", __FUNCTION__, __LINE__, tarName); + status = uploadDebugoutput(outdir, tarName); + } + if(status != 0) { RDK_LOG(RDK_LOG_ERROR,LOG_REMDEBUG,"[%s:%d]: RRD Upload Script Execution Failed!!! status:%d\n",__FUNCTION__,__LINE__,status); @@ -589,6 +843,9 @@ void checkIssueNodeInfo(issueNodeData *issuestructNode, cJSON *jsoncfg, data_buf free(rfcbuf); // free duplicated rfc data free(buff->mdata); // free rfc data free(buff->jsonPath); // free rrd path info + if (persist_suffix_to_file("") != 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] Failed to clear suffix file after upload\n", __FUNCTION__, __LINE__); + } } else { @@ -596,6 +853,9 @@ void checkIssueNodeInfo(issueNodeData *issuestructNode, cJSON *jsoncfg, data_buf free(rfcbuf); // free duplicated rfc data free(buff->mdata); // free rfc data free(buff->jsonPath); // free rrd path info + if (persist_suffix_to_file("") != 0) { + RDK_LOG(RDK_LOG_ERROR, LOG_REMDEBUG, "[%s:%d]: [ERROR] Failed to clear suffix file after upload\n", __FUNCTION__, __LINE__); + } } } } diff --git a/src/rrdJsonParser.h b/src/rrdJsonParser.h index 299436101..8ca7bd261 100644 --- a/src/rrdJsonParser.h +++ b/src/rrdJsonParser.h @@ -47,6 +47,11 @@ issueData* getIssueCommandInfo(issueNodeData *issuestructNode, cJSON *jsoncfg,ch bool processAllDebugCommand(cJSON *jsoncfg, issueNodeData *issuestructNode, char *rfcbuf); bool processAllDeepSleepAwkMetricsCommands(cJSON *jsoncfg, issueNodeData *issuestructNode, char *rfcbuf); + +int persist_suffix_to_file(const char *suffix); +void read_suffix_from_file_to_buf(char *buf, size_t buflen); +void split_issue_type(const char *input, char *base, size_t base_len, char *suffix, size_t suffix_len); + #ifdef __cplusplus } #endif diff --git a/src/unittest/rrdUnitTestRunner.cpp b/src/unittest/rrdUnitTestRunner.cpp index 7b8c6c837..c050489f1 100644 --- a/src/unittest/rrdUnitTestRunner.cpp +++ b/src/unittest/rrdUnitTestRunner.cpp @@ -5817,6 +5817,208 @@ TEST_F(RRDProfileHandlerTest, SetHandler_MaxLengthString) EXPECT_STREQ(RRDProfileCategory, maxString.c_str()); } +/* ====================== split_issue_type / persist_suffix / read_suffix ================*/ + +class SuffixUtilsTest : public ::testing::Test { +protected: + void TearDown() override { + // Remove suffix temp file to avoid state leakage between tests + remove("/tmp/rrd/rrd_suffix.txt"); + } +}; + +/* --------------- Test split_issue_type() from rrdJsonParser --------------- */ + +TEST_F(SuffixUtilsTest, SplitIssueType_WithUnderscore) +{ + char base[64] = {0}; + char suffix[64] = {0}; + split_issue_type("Device.DeviceTime_Search123", base, sizeof(base), suffix, sizeof(suffix)); + EXPECT_STREQ(base, "Device.DeviceTime"); + EXPECT_STREQ(suffix, "_Search123"); +} + +TEST_F(SuffixUtilsTest, SplitIssueType_WithNoUnderscore) +{ + char base[64] = {0}; + char suffix[64] = {0}; + split_issue_type("Device.DeviceTime", base, sizeof(base), suffix, sizeof(suffix)); + EXPECT_STREQ(base, "Device.DeviceTime"); + EXPECT_STREQ(suffix, ""); +} + +TEST_F(SuffixUtilsTest, SplitIssueType_WithMultipleUnderscores) +{ + char base[64] = {0}; + char suffix[64] = {0}; + split_issue_type("foo_bar_baz", base, sizeof(base), suffix, sizeof(suffix)); + EXPECT_STREQ(base, "foo"); + EXPECT_STREQ(suffix, "_bar_baz"); +} + +TEST_F(SuffixUtilsTest, SplitIssueType_StartsWithUnderscore) +{ + char base[64] = {0}; + char suffix[64] = {0}; + split_issue_type("_suffix_only", base, sizeof(base), suffix, sizeof(suffix)); + EXPECT_STREQ(base, ""); + EXPECT_STREQ(suffix, "_suffix_only"); +} + +TEST_F(SuffixUtilsTest, SplitIssueType_EmptyString) +{ + char base[64] = {0}; + char suffix[64] = {0}; + split_issue_type("", base, sizeof(base), suffix, sizeof(suffix)); + EXPECT_STREQ(base, ""); + EXPECT_STREQ(suffix, ""); +} + +TEST_F(SuffixUtilsTest, SplitIssueType_NullInputDoesNotCrash) +{ + char base[64] = {0}; + char suffix[64] = {0}; + split_issue_type(NULL, base, sizeof(base), suffix, sizeof(suffix)); + // base and suffix should remain unmodified (empty) + EXPECT_STREQ(base, ""); + EXPECT_STREQ(suffix, ""); +} + +TEST_F(SuffixUtilsTest, SplitIssueType_NullBaseDoesNotCrash) +{ + char suffix[64] = {0}; + // Should not crash when base is NULL + split_issue_type("Device.DeviceTime_Search", NULL, 64, suffix, sizeof(suffix)); +} + +TEST_F(SuffixUtilsTest, SplitIssueType_NullSuffixDoesNotCrash) +{ + char base[64] = {0}; + // Should not crash when suffix is NULL + split_issue_type("Device.DeviceTime_Search", base, sizeof(base), NULL, 64); +} + +TEST_F(SuffixUtilsTest, SplitIssueType_BaseTruncation) +{ + char base[5] = {0}; + char suffix[64] = {0}; + // base_len=5 means max 4 chars + null terminator + split_issue_type("ABCDEF_suffix", base, sizeof(base), suffix, sizeof(suffix)); + EXPECT_STREQ(base, "ABCD"); + EXPECT_STREQ(suffix, "_suffix"); +} + +TEST_F(SuffixUtilsTest, SplitIssueType_RealWorldInput) +{ + char base[128] = {0}; + char suffix[128] = {0}; + split_issue_type("Device.DeviceTime_Search-b6877385-9463-45fc-b19d-a24d77fd0790", + base, sizeof(base), suffix, sizeof(suffix)); + EXPECT_STREQ(base, "Device.DeviceTime"); + EXPECT_STREQ(suffix, "_Search-b6877385-9463-45fc-b19d-a24d77fd0790"); +} + +/* --------------- Test persist_suffix_to_file() from rrdJsonParser --------------- */ + +TEST_F(SuffixUtilsTest, PersistSuffix_NormalValue) +{ + persist_suffix_to_file("/tmp/rrd/rrd_suffix.txt", "_Search123"); + // Verify the file was written + FILE *fp = fopen("/tmp/rrd/rrd_suffix.txt", "r"); + ASSERT_NE(fp, nullptr); + char buf[64] = {0}; + ASSERT_NE(fgets(buf, sizeof(buf), fp), nullptr); + fclose(fp); + EXPECT_STREQ(buf, "_Search123"); +} + +TEST_F(SuffixUtilsTest, PersistSuffix_EmptyString) +{ + persist_suffix_to_file("/tmp/rrd/rrd_suffix.txt", ""); + FILE *fp = fopen("/tmp/rrd/rrd_suffix.txt", "r"); + ASSERT_NE(fp, nullptr); + char buf[64] = {0}; + // fgets may return NULL for an empty file - that's fine, buf stays empty + fgets(buf, sizeof(buf), fp); + fclose(fp); + EXPECT_STREQ(buf, ""); +} + +TEST_F(SuffixUtilsTest, PersistSuffix_NullDoesNotCrash) +{ + // Passing NULL should not crash; file should be created but empty + persist_suffix_to_file("/tmp/rrd/rrd_suffix.txt",NULL); + FILE *fp = fopen("/tmp/rrd/rrd_suffix.txt", "r"); + ASSERT_NE(fp, nullptr); + char buf[64] = {0}; + fgets(buf, sizeof(buf), fp); + fclose(fp); + EXPECT_STREQ(buf, ""); +} + +TEST_F(SuffixUtilsTest, PersistSuffix_OverwritesPreviousValue) +{ + persist_suffix_to_file("/tmp/rrd/rrd_suffix.txt","_OldSuffix"); + persist_suffix_to_file("/tmp/rrd/rrd_suffix.txt","_NewSuffix"); + char buf[64] = {0}; + read_suffix_from_file_to_buf("/tmp/rrd/rrd_suffix.txt",buf, sizeof(buf)); + EXPECT_STREQ(buf, "_NewSuffix"); +} + +/* --------------- Test read_suffix_from_file_to_buf() from rrdJsonParser --------------- */ + +TEST_F(SuffixUtilsTest, ReadSuffix_AfterPersist) +{ + persist_suffix_to_file("/tmp/rrd/rrd_suffix.txt","_Search123"); + char buf[64] = {0}; + read_suffix_from_file_to_buf("/tmp/rrd/rrd_suffix.txt",buf, sizeof(buf)); + EXPECT_STREQ(buf, "_Search123"); +} + +TEST_F(SuffixUtilsTest, ReadSuffix_WhenFileAbsent) +{ + remove("/tmp/rrd/rrd_suffix.txt"); + char buf[64] = {0}; + read_suffix_from_file_to_buf("/tmp/rrd/rrd_suffix.txt",buf, sizeof(buf)); + EXPECT_STREQ(buf, ""); +} + +TEST_F(SuffixUtilsTest, ReadSuffix_NullBufDoesNotCrash) +{ + persist_suffix_to_file("/tmp/rrd/rrd_suffix.txt","_test"); + // Should not crash when buf is NULL + read_suffix_from_file_to_buf("/tmp/rrd/rrd_suffix.txt",NULL, 64); +} + +TEST_F(SuffixUtilsTest, ReadSuffix_ZeroBuflenDoesNotCrash) +{ + persist_suffix_to_file("/tmp/rrd/rrd_suffix.txt","_test"); + char buf[64] = {0}; + // Should not crash when buflen is 0 + read_suffix_from_file_to_buf("/tmp/rrd/rrd_suffix.txt",buf, 0); +} + +TEST_F(SuffixUtilsTest, ReadSuffix_RoundTrip) +{ + const char *expected = "_Search-b6877385-9463-45fc-b19d-a24d77fd0790"; + persist_suffix_to_file("/tmp/rrd/rrd_suffix.txt",expected); + char buf[128] = {0}; + read_suffix_from_file_to_buf("/tmp/rrd/rrd_suffix.txt",buf, sizeof(buf)); + EXPECT_STREQ(buf, expected); +} + +TEST_F(SuffixUtilsTest, ReadSuffix_StripsTrailingNewline) +{ + // Write a value with a trailing newline to the file + FILE *fp = fopen("/tmp/rrd/rrd_suffix.txt", "w"); + ASSERT_NE(fp, nullptr); + fputs("_suffix\n", fp); + fclose(fp); + char buf[64] = {0}; + read_suffix_from_file_to_buf("/tmp/rrd/rrd_suffix.txt",buf, sizeof(buf)); + EXPECT_STREQ(buf, "_suffix"); +} +