Files
codebase-memory-mcp/src/cli/config_text_edit.c
T
Martin Vogel d35589cf1b fix(cli): make --dry-run predict a refused hook-script rewrite (#1387)
The dry run claimed EVERY hook group as installable because gate_ok /
session_ok / subagent_ok were simply set to `true` when dry_run was on. When
the on-disk hook script is not ours - user-modified, or written by a manual
install pointing at another binary - the real install refuses to rewrite it,
so the preview promised what the run could not deliver. That is the second
half of #1387: the reporter had no way to see the loss coming, and said so.

- New read-only predicate cbm_text_owned_document_status() mirrors
  text_migrate_owned_document's decision WITHOUT writing: absent / already
  current / an exact released document => a write would proceed; anything else
  => it would be refused.
- The dry run uses it per script, so the three 'hooks:' lines now reflect what
  will actually happen, and each refused script is NAMED with the reason.
  Silence was the original defect: an unmentioned group reads as 'nothing to
  do', not 'this will be skipped'.

Test: cli_dry_run_predicts_refused_hook_script_issue1387 runs the real
dry-run path over an unowned gate script, capturing stdout, and asserts the
warning names the script. RED before, GREEN after, RED again on revert.
cli+agent_profiles+config_text_edit: 292 passed; lint-ci clean.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-06 16:41:27 +02:00

1439 lines
51 KiB
C

/*
* config_text_edit.c — Fail-closed edits for managed instruction text.
*/
#include "cli/config_text_edit.h"
#include "foundation/compat.h"
#include "foundation/compat_fs.h"
#include <errno.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include "foundation/win_utf8.h"
#include <fcntl.h>
#include <io.h>
#include <process.h>
#include <sys/stat.h>
#define text_close _close
#define text_fdopen _fdopen
#define TEXT_PROCESS_ID _getpid
#define TEXT_SYNC _commit
#else
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#define text_close close
#define text_fdopen fdopen
#define TEXT_PROCESS_ID getpid
#define TEXT_SYNC fsync
#endif
#define TEXT_OK 0
#define TEXT_ERROR (-1)
#define TEXT_UNOWNED 1
#define TEXT_MAX_BYTES (16U * 1024U * 1024U)
#define TEXT_MAX_PATH_BYTES 32768U
#define TEXT_MAX_MARKER_BYTES 4096U
#define TEXT_TEMP_SUFFIX_BYTES 80U
#define TEXT_TEMP_ATTEMPTS 64U
#define TEXT_OWNER_READ 0400U
#ifdef CBM_TEXT_EDIT_ENABLE_TEST_API
static CBM_TLS cbm_text_precommit_test_hook_t text_precommit_test_hook = NULL;
static CBM_TLS void *text_precommit_test_context = NULL;
static CBM_TLS cbm_text_precommit_test_hook_t text_prepublish_test_hook = NULL;
static CBM_TLS void *text_prepublish_test_context = NULL;
static CBM_TLS cbm_text_precommit_test_hook_t text_temp_closed_test_hook = NULL;
static CBM_TLS void *text_temp_closed_test_context = NULL;
#endif
static atomic_uint text_temp_sequence = 1U;
typedef struct {
int exists;
#ifdef _WIN32
DWORD volume_serial;
DWORD file_index_high;
DWORD file_index_low;
DWORD attributes;
DWORD link_count;
FILETIME creation_time;
FILETIME write_time;
uint64_t size;
#else
dev_t device;
ino_t inode;
mode_t mode;
nlink_t link_count;
uid_t owner;
gid_t group;
off_t size;
int64_t modified_sec;
long modified_nsec;
int64_t changed_sec;
long changed_nsec;
#endif
} text_file_snapshot_t;
typedef struct {
char *data;
size_t len;
size_t capacity;
} text_buffer_t;
typedef struct {
int present;
size_t begin_start;
size_t begin_text_end;
size_t begin_full_end;
size_t end_start;
size_t end_text_end;
size_t end_full_end;
} text_managed_region_t;
static void text_buffer_dispose(text_buffer_t *buffer) {
if (!buffer) {
return;
}
free(buffer->data);
buffer->data = NULL;
buffer->len = 0U;
buffer->capacity = 0U;
}
static int text_buffer_reserve(text_buffer_t *buffer, size_t additional) {
if (!buffer || additional > SIZE_MAX - buffer->len - 1U) {
return TEXT_ERROR;
}
size_t required = buffer->len + additional + 1U;
if (required > (size_t)TEXT_MAX_BYTES + 1U) {
return TEXT_ERROR;
}
if (required <= buffer->capacity) {
return TEXT_OK;
}
size_t capacity = buffer->capacity ? buffer->capacity : 256U;
while (capacity < required) {
if (capacity > SIZE_MAX / 2U) {
capacity = required;
break;
}
capacity *= 2U;
}
char *grown = (char *)realloc(buffer->data, capacity);
if (!grown) {
return TEXT_ERROR;
}
buffer->data = grown;
buffer->capacity = capacity;
return TEXT_OK;
}
static int text_buffer_append(text_buffer_t *buffer, const char *data, size_t len) {
if ((!data && len != 0U) || text_buffer_reserve(buffer, len) != TEXT_OK) {
return TEXT_ERROR;
}
if (len != 0U) {
memcpy(buffer->data + buffer->len, data, len);
buffer->len += len;
}
buffer->data[buffer->len] = '\0';
return TEXT_OK;
}
static int text_bounded_strlen(const char *text, size_t maximum, size_t *len_out) {
if (!text || !len_out) {
return TEXT_ERROR;
}
size_t len = 0U;
while (len <= maximum && text[len] != '\0') {
len++;
}
if (len > maximum) {
return TEXT_ERROR;
}
*len_out = len;
return TEXT_OK;
}
static int text_decode_utf8(const unsigned char *text, size_t remaining, uint32_t *codepoint,
size_t *byte_count) {
if (!text || !codepoint || !byte_count || remaining == 0U) {
return TEXT_ERROR;
}
unsigned char first = text[0];
if (first <= 0x7fU) {
*codepoint = first;
*byte_count = 1U;
return TEXT_OK;
}
size_t count;
uint32_t value;
uint32_t minimum;
if (first >= 0xc2U && first <= 0xdfU) {
count = 2U;
value = first & 0x1fU;
minimum = 0x80U;
} else if (first >= 0xe0U && first <= 0xefU) {
count = 3U;
value = first & 0x0fU;
minimum = 0x800U;
} else if (first >= 0xf0U && first <= 0xf4U) {
count = 4U;
value = first & 0x07U;
minimum = 0x10000U;
} else {
return TEXT_ERROR;
}
if (count > remaining) {
return TEXT_ERROR;
}
for (size_t i = 1U; i < count; i++) {
unsigned char next = text[i];
if ((next & 0xc0U) != 0x80U) {
return TEXT_ERROR;
}
value = (value << 6U) | (uint32_t)(next & 0x3fU);
}
if (value < minimum || value > 0x10ffffU || (value >= 0xd800U && value <= 0xdfffU)) {
return TEXT_ERROR;
}
*codepoint = value;
*byte_count = count;
return TEXT_OK;
}
static int text_validate_bytes(const char *data, size_t len, int allow_initial_bom) {
if (!data && len != 0U) {
return TEXT_ERROR;
}
size_t pos = 0U;
while (pos < len) {
uint32_t codepoint = 0U;
size_t byte_count = 0U;
if (text_decode_utf8((const unsigned char *)data + pos, len - pos, &codepoint,
&byte_count) != TEXT_OK) {
return TEXT_ERROR;
}
if (codepoint == 0xfeffU) {
if (!(allow_initial_bom && pos == 0U && byte_count == 3U)) {
return TEXT_ERROR;
}
} else if (codepoint < 0x20U) {
if (codepoint != '\t' && codepoint != '\n' && codepoint != '\r') {
return TEXT_ERROR;
}
if (codepoint == '\r' && (pos + 1U >= len || data[pos + 1U] != '\n')) {
return TEXT_ERROR;
}
} else if (codepoint == 0x7fU || (codepoint >= 0x80U && codepoint <= 0x9fU)) {
return TEXT_ERROR;
}
pos += byte_count;
}
return TEXT_OK;
}
static int text_valid_path(const char *path) {
size_t len = 0U;
return path && path[0] != '\0' &&
text_bounded_strlen(path, TEXT_MAX_PATH_BYTES, &len) == TEXT_OK &&
text_validate_bytes(path, len, 0) == TEXT_OK && !strchr(path, '\n') &&
!strchr(path, '\r');
}
static int text_valid_marker(const char *marker, size_t *len_out) {
size_t len = 0U;
if (!marker || marker[0] == '\0' ||
text_bounded_strlen(marker, TEXT_MAX_MARKER_BYTES, &len) != TEXT_OK ||
text_validate_bytes(marker, len, 0) != TEXT_OK || strchr(marker, '\n') ||
strchr(marker, '\r')) {
return TEXT_ERROR;
}
*len_out = len;
return TEXT_OK;
}
static int text_snapshot_equal(const text_file_snapshot_t *left,
const text_file_snapshot_t *right) {
if (left->exists != right->exists) {
return 0;
}
if (!left->exists) {
return 1;
}
#ifdef _WIN32
return left->volume_serial == right->volume_serial &&
left->file_index_high == right->file_index_high &&
left->file_index_low == right->file_index_low && left->attributes == right->attributes &&
left->link_count == right->link_count &&
left->creation_time.dwLowDateTime == right->creation_time.dwLowDateTime &&
left->creation_time.dwHighDateTime == right->creation_time.dwHighDateTime &&
left->write_time.dwLowDateTime == right->write_time.dwLowDateTime &&
left->write_time.dwHighDateTime == right->write_time.dwHighDateTime &&
left->size == right->size;
#else
return left->device == right->device && left->inode == right->inode &&
left->mode == right->mode && left->link_count == right->link_count &&
left->owner == right->owner && left->group == right->group &&
left->size == right->size && left->modified_sec == right->modified_sec &&
left->modified_nsec == right->modified_nsec && left->changed_sec == right->changed_sec &&
left->changed_nsec == right->changed_nsec;
#endif
}
static int text_snapshot_publication_equal(const text_file_snapshot_t *trusted,
const text_file_snapshot_t *reopened) {
#ifdef _WIN32
return trusted->exists == reopened->exists && trusted->exists &&
trusted->volume_serial == reopened->volume_serial &&
trusted->file_index_high == reopened->file_index_high &&
trusted->file_index_low == reopened->file_index_low &&
trusted->attributes == reopened->attributes &&
trusted->link_count == reopened->link_count && trusted->size == reopened->size;
#else
return text_snapshot_equal(trusted, reopened);
#endif
}
#ifdef _WIN32
static int text_snapshot_from_handle(HANDLE handle, text_file_snapshot_t *snapshot) {
BY_HANDLE_FILE_INFORMATION info;
if (GetFileType(handle) != FILE_TYPE_DISK || !GetFileInformationByHandle(handle, &info) ||
(info.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) != 0 ||
info.nNumberOfLinks != 1U || (info.nFileIndexHigh == 0U && info.nFileIndexLow == 0U)) {
return TEXT_ERROR;
}
uint64_t size = ((uint64_t)info.nFileSizeHigh << 32U) | (uint64_t)info.nFileSizeLow;
if (size > TEXT_MAX_BYTES) {
return TEXT_ERROR;
}
*snapshot = (text_file_snapshot_t){
.exists = 1,
.volume_serial = info.dwVolumeSerialNumber,
.file_index_high = info.nFileIndexHigh,
.file_index_low = info.nFileIndexLow,
.attributes = info.dwFileAttributes,
.link_count = info.nNumberOfLinks,
.creation_time = info.ftCreationTime,
.write_time = info.ftLastWriteTime,
.size = size,
};
return TEXT_OK;
}
#else
static int text_snapshot_from_stat(const struct stat *state, text_file_snapshot_t *snapshot) {
if (!S_ISREG(state->st_mode) || state->st_ino == 0 || state->st_nlink != 1U ||
state->st_size < 0 || (uint64_t)state->st_size > TEXT_MAX_BYTES ||
(state->st_mode & (S_ISUID | S_ISGID | S_ISVTX)) != 0) {
return TEXT_ERROR;
}
*snapshot = (text_file_snapshot_t){
.exists = 1,
.device = state->st_dev,
.inode = state->st_ino,
.mode = state->st_mode,
.link_count = state->st_nlink,
.owner = state->st_uid,
.group = state->st_gid,
.size = state->st_size,
#ifdef __APPLE__
.modified_sec = state->st_mtimespec.tv_sec,
.modified_nsec = state->st_mtimespec.tv_nsec,
.changed_sec = state->st_ctimespec.tv_sec,
.changed_nsec = state->st_ctimespec.tv_nsec,
#else
.modified_sec = state->st_mtim.tv_sec,
.modified_nsec = state->st_mtim.tv_nsec,
.changed_sec = state->st_ctim.tv_sec,
.changed_nsec = state->st_ctim.tv_nsec,
#endif
};
return TEXT_OK;
}
#endif
static int text_snapshot_from_file(FILE *file, text_file_snapshot_t *snapshot) {
if (!file || !snapshot) {
return TEXT_ERROR;
}
#ifdef _WIN32
intptr_t native_handle = _get_osfhandle(cbm_fileno(file));
return native_handle == -1
? TEXT_ERROR
: text_snapshot_from_handle((HANDLE)(uintptr_t)native_handle, snapshot);
#else
struct stat state;
return fstat(cbm_fileno(file), &state) == 0 ? text_snapshot_from_stat(&state, snapshot)
: TEXT_ERROR;
#endif
}
static int text_requested_mode_valid(int override_mode, unsigned int requested_mode) {
return !override_mode ||
((requested_mode & ~0777U) == 0U && (requested_mode & TEXT_OWNER_READ) != 0U);
}
static int text_read_file(const char *path, char **data_out, size_t *len_out,
text_file_snapshot_t *snapshot_out) {
if (!path || !data_out || !len_out || !snapshot_out) {
return TEXT_ERROR;
}
*data_out = NULL;
*len_out = 0U;
memset(snapshot_out, 0, sizeof(*snapshot_out));
#ifdef _WIN32
wchar_t *wide_path = cbm_utf8_to_wide(path);
if (!wide_path) {
return TEXT_ERROR;
}
HANDLE handle = CreateFileW(
wide_path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
free(wide_path);
if (handle == INVALID_HANDLE_VALUE) {
DWORD error = GetLastError();
if (error != ERROR_FILE_NOT_FOUND && error != ERROR_PATH_NOT_FOUND) {
return TEXT_ERROR;
}
char *empty = (char *)calloc(1U, 1U);
if (!empty) {
return TEXT_ERROR;
}
*data_out = empty;
return TEXT_OK;
}
text_file_snapshot_t before;
if (text_snapshot_from_handle(handle, &before) != TEXT_OK) {
CloseHandle(handle);
return TEXT_ERROR;
}
size_t len = (size_t)before.size;
char *data = (char *)malloc(len + 1U);
if (!data) {
CloseHandle(handle);
return TEXT_ERROR;
}
DWORD read_count = 0U;
BOOL read_ok = ReadFile(handle, data, (DWORD)len, &read_count, NULL);
text_file_snapshot_t after;
int after_result = text_snapshot_from_handle(handle, &after);
BOOL close_ok = CloseHandle(handle);
if (!read_ok || read_count != (DWORD)len || after_result != TEXT_OK || !close_ok ||
!text_snapshot_equal(&before, &after)) {
free(data);
return TEXT_ERROR;
}
#else
#ifndef O_NOFOLLOW
(void)path;
return TEXT_ERROR;
#else
int flags = O_RDONLY | O_NOFOLLOW | O_NONBLOCK;
#ifdef O_CLOEXEC
flags |= O_CLOEXEC;
#endif
int descriptor = open(path, flags);
if (descriptor < 0) {
if (errno != ENOENT) {
return TEXT_ERROR;
}
struct stat path_state;
if (lstat(path, &path_state) == 0 || errno != ENOENT) {
return TEXT_ERROR;
}
char *empty = (char *)calloc(1U, 1U);
if (!empty) {
return TEXT_ERROR;
}
*data_out = empty;
return TEXT_OK;
}
struct stat before_state;
text_file_snapshot_t before;
if (fstat(descriptor, &before_state) != 0 ||
text_snapshot_from_stat(&before_state, &before) != TEXT_OK) {
text_close(descriptor);
return TEXT_ERROR;
}
FILE *file = text_fdopen(descriptor, "rb");
if (!file) {
text_close(descriptor);
return TEXT_ERROR;
}
size_t len = (size_t)before.size;
char *data = (char *)malloc(len + 1U);
if (!data) {
fclose(file);
return TEXT_ERROR;
}
size_t read_count = len == 0U ? 0U : fread(data, 1U, len, file);
int read_failed = ferror(file);
struct stat after_state;
text_file_snapshot_t after;
int after_result = fstat(cbm_fileno(file), &after_state) == 0
? text_snapshot_from_stat(&after_state, &after)
: TEXT_ERROR;
int close_failed = fclose(file);
if (read_count != len || read_failed || close_failed != 0 || after_result != TEXT_OK ||
!text_snapshot_equal(&before, &after)) {
free(data);
return TEXT_ERROR;
}
#endif
#endif
data[len] = '\0';
*data_out = data;
*len_out = len;
*snapshot_out = before;
return TEXT_OK;
}
#ifndef _WIN32
static char *text_parent_directory(const char *path) {
const char *separator = strrchr(path, '/');
if (!separator) {
return cbm_strdup(".");
}
if (separator == path) {
return cbm_strdup("/");
}
return cbm_strndup(path, (size_t)(separator - path));
}
#endif
static int text_snapshot_matches_path(const char *path, const char *expected_data,
size_t expected_len,
const text_file_snapshot_t *expected_snapshot) {
char *current_data = NULL;
size_t current_len = 0U;
text_file_snapshot_t current_snapshot;
if (text_read_file(path, &current_data, &current_len, &current_snapshot) != TEXT_OK) {
return TEXT_ERROR;
}
int matches = expected_snapshot->exists == current_snapshot.exists &&
text_snapshot_equal(expected_snapshot, &current_snapshot) &&
current_len == expected_len &&
(expected_len == 0U || memcmp(current_data, expected_data, expected_len) == 0);
free(current_data);
return matches ? TEXT_OK : TEXT_ERROR;
}
#ifndef _WIN32
static int text_sync_parent_directory(const char *path) {
char *parent = text_parent_directory(path);
if (!parent) {
return TEXT_ERROR;
}
int flags = O_RDONLY;
#ifdef O_DIRECTORY
flags |= O_DIRECTORY;
#endif
#ifdef O_CLOEXEC
flags |= O_CLOEXEC;
#endif
int descriptor = open(parent, flags);
free(parent);
if (descriptor < 0) {
return TEXT_ERROR;
}
struct stat state;
int result = fstat(descriptor, &state) == 0 && S_ISDIR(state.st_mode) && fsync(descriptor) == 0
? TEXT_OK
: TEXT_ERROR;
if (text_close(descriptor) != 0) {
result = TEXT_ERROR;
}
return result;
}
#endif
static int text_replace_file(const char *temp_path, const char *path, int destination_exists) {
#ifdef _WIN32
wchar_t *wide_temp = cbm_utf8_to_wide(temp_path);
wchar_t *wide_path = cbm_utf8_to_wide(path);
if (!wide_temp || !wide_path) {
free(wide_temp);
free(wide_path);
return TEXT_ERROR;
}
BOOL replaced = destination_exists ? ReplaceFileW(wide_path, wide_temp, NULL,
REPLACEFILE_WRITE_THROUGH, NULL, NULL)
: MoveFileExW(wide_temp, wide_path, MOVEFILE_WRITE_THROUGH);
free(wide_temp);
free(wide_path);
return replaced ? TEXT_OK : TEXT_ERROR;
#else
if (!destination_exists) {
if (link(temp_path, path) != 0) {
return TEXT_ERROR;
}
if (cbm_unlink(temp_path) != 0) {
return TEXT_ERROR;
}
return text_sync_parent_directory(path);
}
if (rename(temp_path, path) != 0) {
return TEXT_ERROR;
}
return text_sync_parent_directory(path);
#endif
}
static int text_write_atomic_mode(const char *path, const char *new_data, size_t new_len,
const char *old_data, size_t old_len,
const text_file_snapshot_t *snapshot, int override_mode,
unsigned int requested_mode) {
if (new_len > TEXT_MAX_BYTES || old_len > TEXT_MAX_BYTES ||
!text_requested_mode_valid(override_mode, requested_mode)) {
return TEXT_ERROR;
}
int content_same =
new_len == old_len && (new_len == 0U || memcmp(new_data, old_data, new_len) == 0);
#ifndef _WIN32
int mode_same =
snapshot->exists && (snapshot->mode & 0777U) == (mode_t)(requested_mode & 0777U);
#else
int mode_same = snapshot->exists;
#endif
if (content_same && (!override_mode || mode_same)) {
return TEXT_OK;
}
size_t path_len = 0U;
if (text_bounded_strlen(path, TEXT_MAX_PATH_BYTES, &path_len) != TEXT_OK ||
path_len > SIZE_MAX - TEXT_TEMP_SUFFIX_BYTES - 1U) {
return TEXT_ERROR;
}
size_t capacity = path_len + TEXT_TEMP_SUFFIX_BYTES + 1U;
char *temp_path = (char *)malloc(capacity);
if (!temp_path) {
return TEXT_ERROR;
}
FILE *file = NULL;
for (unsigned attempt = 0U; attempt < TEXT_TEMP_ATTEMPTS; attempt++) {
unsigned sequence =
atomic_fetch_add_explicit(&text_temp_sequence, 1U, memory_order_relaxed);
int written = snprintf(temp_path, capacity, "%s.cbm-text-%ld-%u.tmp", path,
(long)TEXT_PROCESS_ID(), sequence);
if (written < 0 || (size_t)written >= capacity) {
free(temp_path);
return TEXT_ERROR;
}
errno = 0;
#ifdef _WIN32
/* The descriptor-bound identity snapshot uses GetFileInformationByHandle,
* so request read access as well as exclusive binary creation. */
file = cbm_fopen(temp_path, "w+bx");
#else
#ifndef O_NOFOLLOW
free(temp_path);
return TEXT_ERROR;
#else
int flags = O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW;
#ifdef O_CLOEXEC
flags |= O_CLOEXEC;
#endif
int descriptor = open(temp_path, flags, 0600);
if (descriptor >= 0) {
file = text_fdopen(descriptor, "wb");
if (!file) {
int saved_error = errno;
text_close(descriptor);
(void)cbm_unlink(temp_path);
errno = saved_error;
}
}
#endif
#endif
if (file) {
break;
}
if (errno != EEXIST) {
free(temp_path);
return TEXT_ERROR;
}
}
if (!file) {
free(temp_path);
return TEXT_ERROR;
}
int failed = new_len != 0U && fwrite(new_data, 1U, new_len, file) != new_len;
if (!failed && fflush(file) != 0) {
failed = 1;
}
#ifndef _WIN32
if (!failed && snapshot->exists &&
fchown(cbm_fileno(file), snapshot->owner, snapshot->group) != 0) {
failed = 1;
}
mode_t mode = override_mode ? (mode_t)(requested_mode & 0777U)
: snapshot->exists ? snapshot->mode & 0777U
: 0600U;
if (!failed && fchmod(cbm_fileno(file), mode) != 0) {
failed = 1;
}
#endif
if (!failed && TEXT_SYNC(cbm_fileno(file)) != 0) {
failed = 1;
}
text_file_snapshot_t trusted_temp_snapshot = {0};
if (!failed && text_snapshot_from_file(file, &trusted_temp_snapshot) != TEXT_OK) {
failed = 1;
}
#ifndef _WIN32
if (!failed && override_mode &&
(trusted_temp_snapshot.owner != geteuid() ||
(trusted_temp_snapshot.mode & 0777U) != (mode_t)(requested_mode & 0777U))) {
failed = 1;
}
#endif
if (fclose(file) != 0) {
failed = 1;
}
if (failed) {
(void)cbm_unlink(temp_path);
free(temp_path);
return TEXT_ERROR;
}
#ifdef CBM_TEXT_EDIT_ENABLE_TEST_API
if (text_temp_closed_test_hook) {
text_temp_closed_test_hook(temp_path, text_temp_closed_test_context);
}
#endif
char *temp_data = NULL;
size_t temp_len = 0U;
text_file_snapshot_t temp_snapshot;
if (text_read_file(temp_path, &temp_data, &temp_len, &temp_snapshot) != TEXT_OK ||
!text_snapshot_publication_equal(&trusted_temp_snapshot, &temp_snapshot) ||
temp_len != new_len || (new_len != 0U && memcmp(temp_data, new_data, new_len) != 0)) {
free(temp_data);
(void)cbm_unlink(temp_path);
free(temp_path);
return TEXT_ERROR;
}
free(temp_data);
#ifdef CBM_TEXT_EDIT_ENABLE_TEST_API
if (text_precommit_test_hook) {
text_precommit_test_hook(path, text_precommit_test_context);
}
#endif
if (text_snapshot_matches_path(path, old_data, old_len, snapshot) != TEXT_OK) {
(void)cbm_unlink(temp_path);
free(temp_path);
return TEXT_ERROR;
}
#ifdef CBM_TEXT_EDIT_ENABLE_TEST_API
if (text_prepublish_test_hook) {
text_prepublish_test_hook(path, text_prepublish_test_context);
}
#endif
if (text_snapshot_matches_path(path, old_data, old_len, snapshot) != TEXT_OK ||
text_snapshot_matches_path(temp_path, new_data, new_len, &temp_snapshot) != TEXT_OK ||
text_replace_file(temp_path, path, snapshot->exists) != TEXT_OK) {
(void)cbm_unlink(temp_path);
free(temp_path);
return TEXT_ERROR;
}
free(temp_path);
return TEXT_OK;
}
static int text_write_atomic(const char *path, const char *new_data, size_t new_len,
const char *old_data, size_t old_len,
const text_file_snapshot_t *snapshot) {
return text_write_atomic_mode(path, new_data, new_len, old_data, old_len, snapshot, 0, 0U);
}
static int text_delete_file(const char *path, const char *old_data, size_t old_len,
const text_file_snapshot_t *snapshot) {
#ifdef CBM_TEXT_EDIT_ENABLE_TEST_API
if (text_precommit_test_hook) {
text_precommit_test_hook(path, text_precommit_test_context);
}
#endif
if (text_snapshot_matches_path(path, old_data, old_len, snapshot) != TEXT_OK) {
return TEXT_ERROR;
}
#ifdef CBM_TEXT_EDIT_ENABLE_TEST_API
if (text_prepublish_test_hook) {
text_prepublish_test_hook(path, text_prepublish_test_context);
}
#endif
if (text_snapshot_matches_path(path, old_data, old_len, snapshot) != TEXT_OK) {
return TEXT_ERROR;
}
#ifdef _WIN32
wchar_t *wide_path = cbm_utf8_to_wide(path);
if (!wide_path) {
return TEXT_ERROR;
}
BOOL removed = DeleteFileW(wide_path);
free(wide_path);
return removed ? TEXT_OK : TEXT_ERROR;
#else
if (unlink(path) != 0) {
return TEXT_ERROR;
}
return text_sync_parent_directory(path);
#endif
}
#ifdef CBM_TEXT_EDIT_ENABLE_TEST_API
void cbm_text_set_precommit_hook_for_testing(cbm_text_precommit_test_hook_t hook, void *context) {
text_precommit_test_hook = hook;
text_precommit_test_context = context;
}
void cbm_text_set_prepublish_hook_for_testing(cbm_text_precommit_test_hook_t hook, void *context) {
text_prepublish_test_hook = hook;
text_prepublish_test_context = context;
}
void cbm_text_set_temp_closed_hook_for_testing(cbm_text_precommit_test_hook_t hook, void *context) {
text_temp_closed_test_hook = hook;
text_temp_closed_test_context = context;
}
#endif
static int text_bytes_equal(const char *data, size_t start, size_t end, const char *value,
size_t value_len) {
return end >= start && end - start == value_len &&
(value_len == 0U || memcmp(data + start, value, value_len) == 0);
}
static size_t text_count_occurrences(const char *data, size_t len, const char *needle,
size_t needle_len) {
size_t count = 0U;
if (needle_len == 0U || needle_len > len) {
return 0U;
}
for (size_t pos = 0U; pos <= len - needle_len;) {
if (memcmp(data + pos, needle, needle_len) == 0) {
count++;
pos += needle_len;
} else {
pos++;
}
}
return count;
}
static int text_scan_managed_region(const char *data, size_t len, size_t bom_len,
const char *begin_marker, size_t begin_len,
const char *end_marker, size_t end_len,
text_managed_region_t *region) {
memset(region, 0, sizeof(*region));
size_t begin_count = 0U;
size_t end_count = 0U;
size_t cursor = bom_len;
while (cursor < len) {
size_t line_start = cursor;
while (cursor < len && data[cursor] != '\n') {
cursor++;
}
size_t text_end = cursor;
if (text_end > line_start && data[text_end - 1U] == '\r') {
text_end--;
}
size_t full_end = cursor < len ? cursor + 1U : cursor;
if (text_bytes_equal(data, line_start, text_end, begin_marker, begin_len)) {
begin_count++;
if (begin_count == 1U) {
region->begin_start = line_start;
region->begin_text_end = text_end;
region->begin_full_end = full_end;
}
}
if (text_bytes_equal(data, line_start, text_end, end_marker, end_len)) {
end_count++;
if (end_count == 1U) {
region->end_start = line_start;
region->end_text_end = text_end;
region->end_full_end = full_end;
}
}
cursor = full_end;
}
size_t raw_begin_count =
text_count_occurrences(data + bom_len, len - bom_len, begin_marker, begin_len);
size_t raw_end_count =
text_count_occurrences(data + bom_len, len - bom_len, end_marker, end_len);
if (raw_begin_count != begin_count || raw_end_count != end_count || begin_count > 1U ||
end_count > 1U || begin_count != end_count) {
return TEXT_ERROR;
}
if (begin_count == 0U) {
return TEXT_OK;
}
if (region->begin_start >= region->end_start ||
region->begin_full_end <= region->begin_text_end ||
region->begin_full_end > region->end_start) {
return TEXT_ERROR;
}
region->present = 1;
return TEXT_OK;
}
static void text_detect_eol(const char *data, size_t len, const char **eol_out,
size_t *eol_len_out) {
for (size_t pos = 0U; pos < len; pos++) {
if (data[pos] == '\n') {
if (pos > 0U && data[pos - 1U] == '\r') {
*eol_out = "\r\n";
*eol_len_out = 2U;
} else {
*eol_out = "\n";
*eol_len_out = 1U;
}
return;
}
}
*eol_out = "\n";
*eol_len_out = 1U;
}
static int text_append_normalized(text_buffer_t *buffer, const char *content, size_t content_len,
const char *eol, size_t eol_len) {
size_t segment_start = 0U;
size_t pos = 0U;
while (pos < content_len) {
if (content[pos] == '\n') {
size_t segment_end = pos;
if (segment_end > segment_start && content[segment_end - 1U] == '\r') {
segment_end--;
}
if (text_buffer_append(buffer, content + segment_start, segment_end - segment_start) !=
TEXT_OK ||
text_buffer_append(buffer, eol, eol_len) != TEXT_OK) {
return TEXT_ERROR;
}
pos++;
segment_start = pos;
} else {
pos++;
}
}
return text_buffer_append(buffer, content + segment_start, content_len - segment_start);
}
static int text_content_ends_eol(const char *content, size_t len) {
return len != 0U && content[len - 1U] == '\n';
}
static int text_append_managed_block(text_buffer_t *buffer, const char *begin_marker,
size_t begin_len, const char *end_marker, size_t end_len,
const char *content, size_t content_len, const char *eol,
size_t eol_len, const char *trailing, size_t trailing_len) {
if (text_buffer_append(buffer, begin_marker, begin_len) != TEXT_OK ||
text_buffer_append(buffer, eol, eol_len) != TEXT_OK ||
text_append_normalized(buffer, content, content_len, eol, eol_len) != TEXT_OK) {
return TEXT_ERROR;
}
if (content_len != 0U && !text_content_ends_eol(content, content_len) &&
text_buffer_append(buffer, eol, eol_len) != TEXT_OK) {
return TEXT_ERROR;
}
return text_buffer_append(buffer, end_marker, end_len) == TEXT_OK &&
text_buffer_append(buffer, trailing, trailing_len) == TEXT_OK
? TEXT_OK
: TEXT_ERROR;
}
static size_t text_bom_length(const char *data, size_t len) {
static const unsigned char bom[] = {0xefU, 0xbbU, 0xbfU};
return len >= sizeof(bom) && memcmp(data, bom, sizeof(bom)) == 0 ? sizeof(bom) : 0U;
}
static int text_validate_markers(const char *begin_marker, const char *end_marker,
size_t *begin_len, size_t *end_len) {
if (text_valid_marker(begin_marker, begin_len) != TEXT_OK ||
text_valid_marker(end_marker, end_len) != TEXT_OK ||
(*begin_len == *end_len && memcmp(begin_marker, end_marker, *begin_len) == 0) ||
text_count_occurrences(begin_marker, *begin_len, end_marker, *end_len) != 0U ||
text_count_occurrences(end_marker, *end_len, begin_marker, *begin_len) != 0U) {
return TEXT_ERROR;
}
return TEXT_OK;
}
static int text_upsert_managed_block(const char *file_path, const char *begin_marker,
const char *end_marker, const char *owned_content,
size_t max_document_bytes) {
size_t begin_len = 0U;
size_t end_len = 0U;
size_t content_len = 0U;
if (max_document_bytes == 0U || max_document_bytes > TEXT_MAX_BYTES ||
!text_valid_path(file_path) ||
text_validate_markers(begin_marker, end_marker, &begin_len, &end_len) != TEXT_OK ||
text_bounded_strlen(owned_content, TEXT_MAX_BYTES, &content_len) != TEXT_OK ||
text_validate_bytes(owned_content, content_len, 0) != TEXT_OK ||
text_count_occurrences(owned_content, content_len, begin_marker, begin_len) != 0U ||
text_count_occurrences(owned_content, content_len, end_marker, end_len) != 0U) {
return TEXT_ERROR;
}
char *old_data = NULL;
size_t old_len = 0U;
text_file_snapshot_t snapshot;
if (text_read_file(file_path, &old_data, &old_len, &snapshot) != TEXT_OK ||
text_validate_bytes(old_data, old_len, 1) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
size_t bom_len = text_bom_length(old_data, old_len);
text_managed_region_t region;
if (text_scan_managed_region(old_data, old_len, bom_len, begin_marker, begin_len, end_marker,
end_len, &region) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
text_buffer_t updated = {0};
int result = TEXT_ERROR;
if (region.present) {
size_t eol_start = region.begin_text_end;
size_t eol_len = region.begin_full_end - region.begin_text_end;
const char *eol = old_data + eol_start;
size_t trailing_len = region.end_full_end - region.end_text_end;
if (text_buffer_append(&updated, old_data, region.begin_start) == TEXT_OK &&
text_append_managed_block(&updated, begin_marker, begin_len, end_marker, end_len,
owned_content, content_len, eol, eol_len,
old_data + region.end_text_end, trailing_len) == TEXT_OK &&
text_buffer_append(&updated, old_data + region.end_full_end,
old_len - region.end_full_end) == TEXT_OK &&
updated.len <= max_document_bytes) {
result = text_write_atomic(file_path, updated.data, updated.len, old_data, old_len,
&snapshot);
}
} else {
const char *eol = NULL;
size_t eol_len = 0U;
text_detect_eol(old_data + bom_len, old_len - bom_len, &eol, &eol_len);
int meaningful_empty = old_len == bom_len;
int had_final_eol = old_len > bom_len && old_data[old_len - 1U] == '\n';
if (text_buffer_append(&updated, old_data, old_len) == TEXT_OK &&
(!meaningful_empty && !had_final_eol ? text_buffer_append(&updated, eol, eol_len)
: TEXT_OK) == TEXT_OK &&
text_append_managed_block(
&updated, begin_marker, begin_len, end_marker, end_len, owned_content, content_len,
eol, eol_len, meaningful_empty || had_final_eol ? eol : "",
meaningful_empty || had_final_eol ? eol_len : 0U) == TEXT_OK &&
updated.len <= max_document_bytes) {
result = text_write_atomic(file_path, updated.data, updated.len, old_data, old_len,
&snapshot);
}
}
text_buffer_dispose(&updated);
free(old_data);
return result;
}
int cbm_text_upsert_managed_block(const char *file_path, const char *begin_marker,
const char *end_marker, const char *owned_content) {
return text_upsert_managed_block(file_path, begin_marker, end_marker, owned_content,
TEXT_MAX_BYTES);
}
int cbm_text_upsert_managed_block_limited(const char *file_path, const char *begin_marker,
const char *end_marker, const char *owned_content,
size_t max_document_bytes) {
return text_upsert_managed_block(file_path, begin_marker, end_marker, owned_content,
max_document_bytes);
}
int cbm_text_remove_managed_block(const char *file_path, const char *begin_marker,
const char *end_marker) {
size_t begin_len = 0U;
size_t end_len = 0U;
if (!text_valid_path(file_path) ||
text_validate_markers(begin_marker, end_marker, &begin_len, &end_len) != TEXT_OK) {
return TEXT_ERROR;
}
char *old_data = NULL;
size_t old_len = 0U;
text_file_snapshot_t snapshot;
if (text_read_file(file_path, &old_data, &old_len, &snapshot) != TEXT_OK ||
text_validate_bytes(old_data, old_len, 1) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
size_t bom_len = text_bom_length(old_data, old_len);
text_managed_region_t region;
if (text_scan_managed_region(old_data, old_len, bom_len, begin_marker, begin_len, end_marker,
end_len, &region) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
if (!region.present) {
free(old_data);
return TEXT_OK;
}
size_t remove_start = region.begin_start;
if (region.end_full_end == old_len && region.end_full_end == region.end_text_end &&
remove_start > bom_len && old_data[remove_start - 1U] == '\n') {
remove_start--;
if (remove_start > bom_len && old_data[remove_start - 1U] == '\r') {
remove_start--;
}
}
text_buffer_t updated = {0};
int result = TEXT_ERROR;
if (text_buffer_append(&updated, old_data, remove_start) == TEXT_OK &&
text_buffer_append(&updated, old_data + region.end_full_end,
old_len - region.end_full_end) == TEXT_OK) {
result =
text_write_atomic(file_path, updated.data, updated.len, old_data, old_len, &snapshot);
}
text_buffer_dispose(&updated);
free(old_data);
return result;
}
int cbm_text_write_owned_document(const char *file_path, const char *owned_content) {
size_t content_len = 0U;
if (!text_valid_path(file_path) ||
text_bounded_strlen(owned_content, TEXT_MAX_BYTES, &content_len) != TEXT_OK ||
text_validate_bytes(owned_content, content_len, 1) != TEXT_OK) {
return TEXT_ERROR;
}
char *old_data = NULL;
size_t old_len = 0U;
text_file_snapshot_t snapshot;
if (text_read_file(file_path, &old_data, &old_len, &snapshot) != TEXT_OK ||
text_validate_bytes(old_data, old_len, 1) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
int result =
text_write_atomic(file_path, owned_content, content_len, old_data, old_len, &snapshot);
free(old_data);
return result;
}
int cbm_text_write_owned_document_if_unchanged(const char *file_path, const char *owned_content,
const char *expected_content,
size_t expected_length) {
size_t content_len = 0U;
if (!text_valid_path(file_path) ||
text_bounded_strlen(owned_content, TEXT_MAX_BYTES, &content_len) != TEXT_OK ||
text_validate_bytes(owned_content, content_len, 1) != TEXT_OK ||
(!expected_content && expected_length != 0U) || expected_length > TEXT_MAX_BYTES ||
(expected_content &&
text_validate_bytes(expected_content, expected_length, 1) != TEXT_OK)) {
return TEXT_ERROR;
}
char *old_data = NULL;
size_t old_len = 0U;
text_file_snapshot_t snapshot;
if (text_read_file(file_path, &old_data, &old_len, &snapshot) != TEXT_OK ||
text_validate_bytes(old_data, old_len, 1) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
int expected_missing = expected_content == NULL;
int matches = expected_missing ? !snapshot.exists
: snapshot.exists && old_len == expected_length &&
(expected_length == 0U ||
memcmp(old_data, expected_content, expected_length) == 0);
if (!matches) {
free(old_data);
return TEXT_ERROR;
}
int result =
text_write_atomic(file_path, owned_content, content_len, old_data, old_len, &snapshot);
free(old_data);
return result;
}
int cbm_text_create_owned_document(const char *file_path, const char *owned_content) {
size_t content_len = 0U;
if (!text_valid_path(file_path) ||
text_bounded_strlen(owned_content, TEXT_MAX_BYTES, &content_len) != TEXT_OK ||
text_validate_bytes(owned_content, content_len, 1) != TEXT_OK) {
return TEXT_ERROR;
}
char *old_data = NULL;
size_t old_len = 0U;
text_file_snapshot_t snapshot;
if (text_read_file(file_path, &old_data, &old_len, &snapshot) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
if (snapshot.exists) {
free(old_data);
return TEXT_ERROR;
}
int result =
text_write_atomic(file_path, owned_content, content_len, old_data, old_len, &snapshot);
free(old_data);
return result;
}
int cbm_text_ensure_owned_document(const char *file_path, const char *owned_content) {
size_t content_len = 0U;
if (!text_valid_path(file_path) ||
text_bounded_strlen(owned_content, TEXT_MAX_BYTES, &content_len) != TEXT_OK ||
text_validate_bytes(owned_content, content_len, 1) != TEXT_OK) {
return TEXT_ERROR;
}
char *old_data = NULL;
size_t old_len = 0U;
text_file_snapshot_t snapshot;
if (text_read_file(file_path, &old_data, &old_len, &snapshot) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
if (snapshot.exists) {
int result = old_len == content_len &&
(old_len == 0U || memcmp(old_data, owned_content, old_len) == 0)
? TEXT_OK
: TEXT_ERROR;
free(old_data);
return result;
}
int result =
text_write_atomic(file_path, owned_content, content_len, old_data, old_len, &snapshot);
free(old_data);
return result;
}
static int text_matches_candidate(const char *data, size_t data_len, const char *candidate,
bool *matches) {
size_t candidate_len = 0U;
if (!matches || text_bounded_strlen(candidate, TEXT_MAX_BYTES, &candidate_len) != TEXT_OK ||
text_validate_bytes(candidate, candidate_len, 1) != TEXT_OK) {
return TEXT_ERROR;
}
*matches =
data_len == candidate_len && (data_len == 0U || memcmp(data, candidate, data_len) == 0);
return TEXT_OK;
}
/* Read-only classification: WOULD a migrate of this document succeed?
* Returns TEXT_OK when the path is absent or already byte-identical to the
* current or an exact released document (a write would proceed), TEXT_UNOWNED
* when it holds other bytes (a write would be refused), TEXT_ERROR on unsafe
* state. Mirrors text_migrate_owned_document's decision WITHOUT writing, so
* `install --dry-run` can predict a refusal instead of promising an install it
* cannot deliver (#1387). */
int cbm_text_owned_document_status(const char *file_path, const char *current_content,
const char *const *released_contents, size_t released_count) {
size_t current_len = 0U;
if (!text_valid_path(file_path) ||
text_bounded_strlen(current_content, TEXT_MAX_BYTES, &current_len) != TEXT_OK ||
text_validate_bytes(current_content, current_len, 1) != TEXT_OK ||
(released_count > 0U && !released_contents)) {
return TEXT_ERROR;
}
char *old_data = NULL;
size_t old_len = 0U;
text_file_snapshot_t snapshot;
if (text_read_file(file_path, &old_data, &old_len, &snapshot) != TEXT_OK ||
text_validate_bytes(old_data, old_len, 1) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
if (!snapshot.exists) {
free(old_data);
return TEXT_OK; /* absent -> the write creates it */
}
bool matches = false;
if (text_matches_candidate(old_data, old_len, current_content, &matches) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
if (matches) {
free(old_data);
return TEXT_OK;
}
for (size_t i = 0U; i < released_count; i++) {
if (text_matches_candidate(old_data, old_len, released_contents[i], &matches) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
if (matches) {
free(old_data);
return TEXT_OK; /* an exact released doc upgrades */
}
}
free(old_data);
return TEXT_UNOWNED;
}
static int text_migrate_owned_document(const char *file_path, const char *current_content,
const char *const *released_contents, size_t released_count,
int override_mode, unsigned int requested_mode) {
size_t current_len = 0U;
if (!text_valid_path(file_path) ||
text_bounded_strlen(current_content, TEXT_MAX_BYTES, &current_len) != TEXT_OK ||
text_validate_bytes(current_content, current_len, 1) != TEXT_OK ||
(released_count > 0U && !released_contents) ||
!text_requested_mode_valid(override_mode, requested_mode)) {
return TEXT_ERROR;
}
for (size_t i = 0U; i < released_count; i++) {
bool ignored = false;
if (text_matches_candidate("", 0U, released_contents[i], &ignored) != TEXT_OK) {
return TEXT_ERROR;
}
}
char *old_data = NULL;
size_t old_len = 0U;
text_file_snapshot_t snapshot;
if (text_read_file(file_path, &old_data, &old_len, &snapshot) != TEXT_OK ||
text_validate_bytes(old_data, old_len, 1) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
#ifndef _WIN32
if (override_mode && snapshot.exists && snapshot.owner != geteuid()) {
free(old_data);
return TEXT_ERROR;
}
#endif
if (!snapshot.exists) {
int result = text_write_atomic_mode(file_path, current_content, current_len, old_data,
old_len, &snapshot, override_mode, requested_mode);
free(old_data);
return result;
}
bool matches = false;
if (text_matches_candidate(old_data, old_len, current_content, &matches) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
if (matches) {
#ifndef _WIN32
if (override_mode && (snapshot.mode & 0777U) != (mode_t)(requested_mode & 0777U)) {
int result = text_write_atomic_mode(file_path, current_content, current_len, old_data,
old_len, &snapshot, override_mode, requested_mode);
free(old_data);
return result;
}
#endif
free(old_data);
return TEXT_OK;
}
for (size_t i = 0U; i < released_count; i++) {
if (text_matches_candidate(old_data, old_len, released_contents[i], &matches) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
if (matches) {
int result = text_write_atomic_mode(file_path, current_content, current_len, old_data,
old_len, &snapshot, override_mode, requested_mode);
free(old_data);
return result;
}
}
free(old_data);
return TEXT_UNOWNED;
}
int cbm_text_migrate_owned_document(const char *file_path, const char *current_content,
const char *const *released_contents, size_t released_count) {
return text_migrate_owned_document(file_path, current_content, released_contents,
released_count, 0, 0U);
}
int cbm_text_migrate_owned_document_mode(const char *file_path, const char *current_content,
const char *const *released_contents,
size_t released_count, unsigned int mode) {
return text_migrate_owned_document(file_path, current_content, released_contents,
released_count, 1, mode);
}
int cbm_text_remove_owned_document(const char *file_path, const char *expected_owned_content) {
size_t expected_len = 0U;
if (!text_valid_path(file_path) ||
text_bounded_strlen(expected_owned_content, TEXT_MAX_BYTES, &expected_len) != TEXT_OK ||
text_validate_bytes(expected_owned_content, expected_len, 1) != TEXT_OK) {
return TEXT_ERROR;
}
char *old_data = NULL;
size_t old_len = 0U;
text_file_snapshot_t snapshot;
if (text_read_file(file_path, &old_data, &old_len, &snapshot) != TEXT_OK ||
text_validate_bytes(old_data, old_len, 1) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
if (!snapshot.exists) {
free(old_data);
return TEXT_OK;
}
if (old_len != expected_len ||
(old_len != 0U && memcmp(old_data, expected_owned_content, old_len) != 0)) {
free(old_data);
return TEXT_UNOWNED;
}
int result = text_delete_file(file_path, old_data, old_len, &snapshot);
free(old_data);
return result;
}
int cbm_text_remove_owned_document_any(const char *file_path, const char *current_content,
const char *const *released_contents,
size_t released_count) {
if (!text_valid_path(file_path) || (released_count > 0U && !released_contents)) {
return TEXT_ERROR;
}
bool ignored = false;
if (text_matches_candidate("", 0U, current_content, &ignored) != TEXT_OK) {
return TEXT_ERROR;
}
for (size_t i = 0U; i < released_count; i++) {
if (text_matches_candidate("", 0U, released_contents[i], &ignored) != TEXT_OK) {
return TEXT_ERROR;
}
}
char *old_data = NULL;
size_t old_len = 0U;
text_file_snapshot_t snapshot;
if (text_read_file(file_path, &old_data, &old_len, &snapshot) != TEXT_OK ||
text_validate_bytes(old_data, old_len, 1) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
if (!snapshot.exists) {
free(old_data);
return TEXT_OK;
}
bool matches = false;
if (text_matches_candidate(old_data, old_len, current_content, &matches) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
for (size_t i = 0U; !matches && i < released_count; i++) {
if (text_matches_candidate(old_data, old_len, released_contents[i], &matches) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
}
if (!matches) {
free(old_data);
return TEXT_UNOWNED;
}
int result = text_delete_file(file_path, old_data, old_len, &snapshot);
free(old_data);
return result;
}