Files
TapTap 91197fd7cf fix(fs): rsync push iconv direction; gate empty-dir emission; reclassify --temp-dir
- --iconv now matches rsync's push direction: the destination charset is the
  client spec's REMOTE half, so a default receiver writes wire names verbatim;
  a server's own --iconv LOCAL overrides it (daemon charset analog). Updated
  unit + integration tests and added a default-server differential gate case.
- Empty-directory emission is gated behind a new ScannerOptions.emit_empty_dirs
  set only by the real sender, so low-level scanner helpers keep the historical
  file-only list.
- --temp-dir reclassified to Divergent: relative dirs match rsync exactly
  (resolved under the destination), but an absolute path is deliberately
  rejected by the confined receiver; differential test added.
- Docs/tally: 109 Parity / 22 Caveat / 25 Divergent.
2026-09-18 20:39:20 +02:00

234 lines
7.7 KiB
C

#include "test_iconv.h"
#include "charset.h"
#include "protocol.h"
#include "test_utils.h"
#include "utils.h"
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
/* --- CONVERT_SPEC parsing ------------------------------------------------ */
static void test_iconv_spec_parse_split() {
char* local = NULL;
char* remote = NULL;
EXPECT_EQ_INT(charset_spec_parse("utf-8,iso-8859-1", &local, &remote), 0);
EXPECT_EQ_STR(local, "utf-8");
EXPECT_EQ_STR(remote, "iso-8859-1");
free(local);
free(remote);
}
static void test_iconv_spec_parse_single_defaults_to_local() {
char* local = NULL;
char* remote = NULL;
EXPECT_EQ_INT(charset_spec_parse("utf-8", &local, &remote), 0);
EXPECT_EQ_STR(local, "utf-8");
EXPECT_EQ_STR(remote, "utf-8");
free(local);
free(remote);
}
static void test_iconv_spec_parse_garbage() {
char* local = NULL;
char* remote = NULL;
EXPECT_EQ_INT(charset_spec_parse(NULL, &local, &remote), -1);
EXPECT_EQ_INT(charset_spec_parse("", &local, &remote), -1);
EXPECT_EQ_INT(charset_spec_parse(",", &local, &remote), -1);
EXPECT_EQ_INT(charset_spec_parse("utf-8,", &local, &remote), -1);
EXPECT_EQ_INT(charset_spec_parse(",utf-8", &local, &remote), -1);
}
static void test_iconv_spec_valid() {
EXPECT_TRUE(charset_spec_valid(NULL));
EXPECT_TRUE(charset_spec_valid("utf-8"));
EXPECT_TRUE(charset_spec_valid("utf-8,iso-8859-1"));
EXPECT_TRUE(charset_spec_valid("iso-8859-1,ascii"));
EXPECT_FALSE(charset_spec_valid("no-such-charset,utf-8"));
EXPECT_FALSE(charset_spec_valid("utf-8,no-such-charset"));
EXPECT_FALSE(charset_spec_valid(",,,"));
EXPECT_FALSE(charset_spec_valid("utf-8,"));
/* A target charset whose conversion emits embedded NUL bytes would be
truncated by the C-string wire helpers; it must be rejected up front. */
EXPECT_FALSE(charset_spec_valid("utf-8,utf-16"));
EXPECT_FALSE(charset_spec_valid("utf-16"));
EXPECT_FALSE(charset_spec_valid("iso-8859-1,utf-16"));
}
/* --- one-shot conversion ------------------------------------------------ */
static void test_iconv_utf8_to_latin1() {
void* conv = charset_conversion_open("utf-8", "iso-8859-1");
EXPECT_NOT_NULL(conv);
char* out = charset_convert(conv, "caf\xc3\xa9", NULL);
EXPECT_NOT_NULL(out);
EXPECT_EQ_INT(strcmp(out, "caf\xe9"), 0);
free(out);
charset_conversion_close(conv);
}
static void test_iconv_latin1_to_utf8() {
void* conv = charset_conversion_open("iso-8859-1", "utf-8");
EXPECT_NOT_NULL(conv);
char* out = charset_convert(conv, "caf\xe9", NULL);
EXPECT_NOT_NULL(out);
EXPECT_EQ_INT(strcmp(out, "caf\xc3\xa9"), 0);
free(out);
charset_conversion_close(conv);
}
static void test_iconv_invalid_sequence_fails() {
int err = 0;
/* 0xff is not a valid UTF-8 sequence. */
void* conv = charset_conversion_open("utf-8", "ascii");
EXPECT_NOT_NULL(conv);
EXPECT_TRUE(charset_convert(conv, "bad\xff", &err) == NULL);
EXPECT_TRUE(err == EILSEQ || err == EINVAL);
charset_conversion_close(conv);
}
static void test_iconv_unrepresentable_fails() {
/* "caf\xc3\xa9" (UTF-8 for cafe) has no ASCII representation. */
void* conv = charset_conversion_open("utf-8", "ascii");
EXPECT_NOT_NULL(conv);
EXPECT_TRUE(charset_convert(conv, "caf\xc3\xa9", NULL) == NULL);
charset_conversion_close(conv);
}
/* A latin1 high-bit byte expands to two UTF-8 bytes. With exactly 16 high
* bytes the output is exactly cap = in_len + 16, so the final iconv call fills
* the buffer completely and a naive NUL-terminator write would overflow. */
static void test_iconv_exact_fill_no_overflow() {
char name[64];
strcpy(name, "dir/");
int n = 4;
for (int i = 0; i < 16; i++)
name[n++] = (char)(0x80 + i);
name[n] = '\0';
void* conv = charset_conversion_open("iso-8859-1", "utf-8");
EXPECT_NOT_NULL(conv);
char* out = charset_convert(conv, name, NULL);
EXPECT_NOT_NULL(out);
EXPECT_EQ_INT((int)strlen(out), n + 16);
charset_conversion_close(conv);
free(out);
}
/* Many high-bit bytes force the output buffer past its initial cap, exercising
* the E2BIG growth path (input partially consumed/produced before the grow). */
static void test_iconv_growth_expanding_name() {
char name[256];
strcpy(name, "dir/");
int n = 4;
for (int i = 0; i < 80; i++)
name[n++] = (char)(0x80 + (i % 0x80));
name[n] = '\0';
void* conv = charset_conversion_open("iso-8859-1", "utf-8");
EXPECT_NOT_NULL(conv);
char* out = charset_convert(conv, name, NULL);
EXPECT_NOT_NULL(out);
EXPECT_EQ_INT((int)strlen(out), n + 80);
charset_conversion_close(conv);
free(out);
}
/* --- process-wide wire conversion ---------------------------------------- */
static void test_iconv_wire_sender_converts_local_to_remote() {
EXPECT_TRUE(charset_wire_init_sender("utf-8,iso-8859-1"));
char* wire = charset_wire_apply("caf\xc3\xa9");
EXPECT_NOT_NULL(wire);
EXPECT_EQ_INT(strcmp(wire, "caf\xe9"), 0);
free(wire);
charset_wire_free();
}
/* rsync push parity: with no server --iconv the destination charset is the
* client spec's REMOTE half, so the receiver writes the wire bytes verbatim. */
static void test_iconv_wire_receiver_default_writes_remote() {
EXPECT_TRUE(charset_wire_init_receiver("utf-8,iso-8859-1", NULL));
char* local = charset_wire_apply("caf\xe9");
EXPECT_NOT_NULL(local);
EXPECT_EQ_INT(strcmp(local, "caf\xe9"), 0);
free(local);
charset_wire_free();
}
/* A server that declares its own --iconv LOCAL converts wire(REMOTE) into that
* declared charset (the daemon "charset" analog). */
static void test_iconv_wire_receiver_server_local_override() {
EXPECT_TRUE(charset_wire_init_receiver("utf-8,iso-8859-1", "utf-8"));
char* local = charset_wire_apply("caf\xe9");
EXPECT_NOT_NULL(local);
EXPECT_EQ_INT(strcmp(local, "caf\xc3\xa9"), 0);
free(local);
charset_wire_free();
}
static void test_iconv_wire_disabled_passthrough() {
charset_wire_init_sender(NULL);
EXPECT_FALSE(charset_wire_active());
char* out = charset_wire_apply("plain/name\xff");
EXPECT_NOT_NULL(out);
EXPECT_EQ_INT(strcmp(out, "plain/name\xff"), 0);
free(out);
charset_wire_free();
}
static void test_iconv_wire_str_roundtrip() {
EXPECT_TRUE(charset_wire_init_sender("utf-8,iso-8859-1"));
int p[2];
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
pid_t pid = fork();
if (pid == 0) {
close(p[1]);
io_set_fds(p[0], p[0]);
charset_wire_free();
charset_wire_init_receiver("utf-8,iso-8859-1", "utf-8");
char* got = receive_wire_str(p[0]);
bool ok = got != NULL && strcmp(got, "caf\xc3\xa9") == 0;
free(got);
charset_wire_free();
close(p[0]);
_exit(ok ? 0 : 1);
} else {
close(p[0]);
io_set_fds(p[1], p[1]);
bool sent = send_wire_str(p[1], "caf\xc3\xa9");
int status;
waitpid(pid, &status, 0);
close(p[1]);
charset_wire_free();
EXPECT_TRUE(sent);
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
}
void test_iconv() {
test_iconv_spec_parse_split();
test_iconv_spec_parse_single_defaults_to_local();
test_iconv_spec_parse_garbage();
test_iconv_spec_valid();
test_iconv_utf8_to_latin1();
test_iconv_latin1_to_utf8();
test_iconv_invalid_sequence_fails();
test_iconv_unrepresentable_fails();
test_iconv_exact_fill_no_overflow();
test_iconv_growth_expanding_name();
test_iconv_wire_sender_converts_local_to_remote();
test_iconv_wire_receiver_default_writes_remote();
test_iconv_wire_receiver_server_local_override();
test_iconv_wire_disabled_passthrough();
// This subtest forks to exercise the wire string handshake; the instrumented
// parent is too slow under valgrind for the child's blocking reads.
if (!is_running_under_valgrind())
test_iconv_wire_str_roundtrip();
}