#include #include #include c_err_t c_utf8_file_read(const char* filepath, c_StringBuffer_t* out_sb) { if (!filepath || !out_sb || !out_sb->buffer) { return C_ERR_PARAM; } FILE* file = fopen(filepath, "rb"); // Open in binary mode to prevent Windows crlf translations if (!file) { return C_ERR_FAIL; } // Step 1: Detect and handle the optional 3-byte UTF-8 BOM sequence unsigned char bom[3]; c_size_t bom_read = fread(bom, 1, 3, file); c_bool_t has_bom = C_FALSE; if (bom_read == 3 && bom[0] == 0xEF && bom[1] == 0xBB && bom[2] == 0xBF) { has_bom = C_TRUE; // BOM sequence matched; file pointer is positioned right past it } else { // No BOM found; rewind file pointer back to the absolute beginning of the stream fseek(file, 0, SEEK_SET); } // Step 2: Read data sequentially via stream chunking loops char read_chunk[1024]; c_size_t bytes_read = 0; c_size_t partial_offset = 0; while ((bytes_read = fread(read_chunk + partial_offset, 1, sizeof(read_chunk) - partial_offset, file)) > 0) { c_size_t total_available_bytes = bytes_read + partial_offset; c_size_t valid_process_boundary = total_available_bytes; // Verify that the chunk boundary does not break a multi-byte character in half. // Look back from the absolute end of the chunk to catch multi-byte headers. if (read_chunk[total_available_bytes - 1] & 0x80) { c_size_t lookback = 1; // Scan backward up to 4 bytes to find the leading byte of the fractured character while (lookback <= 4 && lookback <= total_available_bytes) { unsigned char b = (unsigned char)read_chunk[total_available_bytes - lookback]; if ((b & 0xC0) == 0xC0) { // Found a multi-byte lead byte c_size_t expected_len = c_utf8_char_len((char)b); if (lookback < expected_len) { // Character is indeed fractured; shrink chunk boundary to omit it valid_process_boundary = total_available_bytes - lookback; } break; } if ((b & 0x80) == 0) { // Standard ASCII character, boundary is clean break; } lookback++; } } // Pipe valid, cohesive text fragments into your dynamic string buffer tracker if (valid_process_boundary > 0) { c_err_t err = c_StringBuffer_Append(out_sb, read_chunk, valid_process_boundary); if (err != C_ERR_OK) { fclose(file); return err; } } // Move remaining fractured bytes to the front of the next chunk buffer iteration pass partial_offset = total_available_bytes - valid_process_boundary; if (partial_offset > 0) { memmove(read_chunk, read_chunk + valid_process_boundary, partial_offset); } } // Process residual bytes if the file stream terminates abruptly with an incomplete character sequence if (partial_offset > 0) { c_StringBuffer_Append(out_sb, read_chunk, partial_offset); } fclose(file); return C_ERR_OK; } c_err_t c_utf8_file_write(const char* filepath, c_StringBuffer_t* sb, c_bool_t write_bom) { if (!filepath || !sb || !sb->buffer) { return C_ERR_PARAM; } FILE* file = fopen(filepath, "wb"); // Open in binary mode for precise byte preservation if (!file) { return C_ERR_FAIL; } // Explicitly inject the UTF-8 BOM sequence if requested by the configuration parameter if (write_bom) { unsigned char bom[3] = {0xEF, 0xBB, 0xBF}; if (fwrite(bom, 1, 3, file) != 3) { fclose(file); return C_ERR_FAIL; } } // Flush the string buffer's raw tracking payload into disk blocks if (sb->size > 0) { c_size_t written = fwrite(sb->buffer, 1, sb->size, file); if (written != sb->size) { fclose(file); return C_ERR_FAIL; } } fclose(file); return C_ERR_OK; } /* ------------------------------------------------------------------------------------------------------------------ */ /* */ c_err_t c_utf8_file_append(const char* filepath, c_StringBuffer_t* sb, c_bool_t write_bom) { if (!filepath || !sb || !sb->buffer) { return C_ERR_PARAM; } // Check if the file already exists by attempting to open it in read mode FILE* check_file = fopen(filepath, "rb"); c_bool_t file_exists = (check_file != NULL); if (file_exists) { fclose(check_file); } // Open the file in append-binary mode FILE* file = fopen(filepath, "ab"); if (!file) { return C_ERR_FAIL; } // Write the BOM only if requested AND the file is brand new if (write_bom && !file_exists) { unsigned char bom[3] = {0xEF, 0xBB, 0xBF}; if (fwrite(bom, 1, 3, file) != 3) { fclose(file); return C_ERR_FAIL; } } // Append the string buffer's raw tracking payload if (sb->size > 0) { c_size_t written = fwrite(sb->buffer, 1, sb->size, file); if (written != sb->size) { fclose(file); return C_ERR_FAIL; } } fclose(file); return C_ERR_OK; } c_err_t c_utf8_file_readline(FILE* file, c_StringBuffer_t* out_line) { if (!file || !out_line || !out_line->buffer) { return C_ERR_PARAM; } // Clear previous string buffer trackers to prepare for fresh line ingestion c_StringBuffer_Clear(out_line); char read_chunk[256]; c_bool_t data_extracted = C_FALSE; long line_start_pos = ftell(file); while (fgets(read_chunk, sizeof(read_chunk), file) != NULL) { data_extracted = C_TRUE; c_size_t chunk_len = strlen(read_chunk); // Check if the chunk contains a newline character char* newline_ptr = strchr(read_chunk, '\n'); if (newline_ptr != NULL) { // Calculate exact copy length up to the newline boundary c_size_t copy_len = newline_ptr - read_chunk; if (copy_len > 0) { // Strip carriage returns '\r' for safe cross-platform matching if (read_chunk[copy_len - 1] == '\r') { copy_len--; } } if (copy_len > 0) { c_err_t err = c_StringBuffer_Append(out_line, read_chunk, copy_len); if (err != C_ERR_OK) return err; } return C_ERR_OK; // Line read complete } // If no newline is found, the line is longer than our chunk; append everything and keep reading c_err_t err = c_StringBuffer_Append(out_line, read_chunk, chunk_len); if (err != C_ERR_OK) return err; } // Handle end-of-file (EOF) state if (data_extracted) { return C_ERR_OK; // Returned the final trailing line containing no newline char } return C_ERR_FAIL; // Reached EOF without extracting any data bytes }