209 lines
7.0 KiB
C
209 lines
7.0 KiB
C
#include <c_utf8_file.h>
|
|||
|
|
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
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
|
||
|
|
}
|
||
|
|
|