blob: b0bb78d9f34fe8849e0c738908fac1ab2c6c1b1e [file] [edit]
/*
* Test process implementing the diff process protocol (diff.<driver>.process).
*
* Speaks the long-running process protocol over stdin/stdout and
* answers command=hunks-by-oid requests from the blob object names
* alone; no content is exchanged. The --mode= switch selects the
* response shape:
*
* oid-fixed packet: git< hunk 5 2 5 2
* oid-equal packet: git< status=success (zero hunks: equivalent)
* oid-need-content packet: git< status=need-content
* oid-empty packet: git< hunk 0 0 1 2 (empty old side)
*
* and the adversarial shapes the protocol error paths are tested
* with:
*
* oid-trailing a hunk line with a trailing token to ignore
* oid-malformed a hunk line that does not parse
* oid-huge coordinates far past the end of any test blob
* oid-erange a count too large for any long
* oid-overlap two hunks out of order
* oid-misaligned two hunks whose unchanged runs differ in length
* oid-badstart a start of 0 paired with a nonzero count
* oid-unknown-status status=frobnicate
* oid-abort status=abort
* oid-bare-status a status packet without the hunk-section flush
* oid-empty-packet an empty packet (0004) inside the hunk section
* oid-crash one hunk line, then exit with no flush or status
* oid-garbage raw non-pkt-line bytes, then exit
* cap-none handshake announcing no capability at all
*
* Success responses end with:
*
* packet: git< 0000
* packet: git< status=success
* packet: git< 0000
*
* Each request is logged to --log as:
*
* command=<cmd> pathname=<path> old-oid=<hex> new-oid=<hex>
*/
#include "test-tool.h"
#include "pkt-line.h"
#include "parse-options.h"
#include "strbuf.h"
static FILE *logfile;
enum mode {
MODE_OID_FIXED,
MODE_OID_EQUAL,
MODE_OID_NEED_CONTENT,
MODE_OID_EMPTY,
MODE_OID_TRAILING,
MODE_OID_MALFORMED,
MODE_OID_HUGE,
MODE_OID_ERANGE,
MODE_OID_OVERLAP,
MODE_OID_MISALIGNED,
MODE_OID_BADSTART,
MODE_OID_UNKNOWN_STATUS,
MODE_OID_ABORT,
MODE_OID_BARE_STATUS,
MODE_OID_EMPTY_PACKET,
MODE_OID_CRASH,
MODE_OID_GARBAGE,
MODE_CAP_NONE,
};
static enum mode parse_mode(const char *s)
{
if (!strcmp(s, "oid-fixed"))
return MODE_OID_FIXED;
if (!strcmp(s, "oid-equal"))
return MODE_OID_EQUAL;
if (!strcmp(s, "oid-need-content"))
return MODE_OID_NEED_CONTENT;
if (!strcmp(s, "oid-empty"))
return MODE_OID_EMPTY;
if (!strcmp(s, "oid-trailing"))
return MODE_OID_TRAILING;
if (!strcmp(s, "oid-malformed"))
return MODE_OID_MALFORMED;
if (!strcmp(s, "oid-huge"))
return MODE_OID_HUGE;
if (!strcmp(s, "oid-erange"))
return MODE_OID_ERANGE;
if (!strcmp(s, "oid-overlap"))
return MODE_OID_OVERLAP;
if (!strcmp(s, "oid-misaligned"))
return MODE_OID_MISALIGNED;
if (!strcmp(s, "oid-badstart"))
return MODE_OID_BADSTART;
if (!strcmp(s, "oid-unknown-status"))
return MODE_OID_UNKNOWN_STATUS;
if (!strcmp(s, "oid-abort"))
return MODE_OID_ABORT;
if (!strcmp(s, "oid-bare-status"))
return MODE_OID_BARE_STATUS;
if (!strcmp(s, "oid-empty-packet"))
return MODE_OID_EMPTY_PACKET;
if (!strcmp(s, "oid-crash"))
return MODE_OID_CRASH;
if (!strcmp(s, "oid-garbage"))
return MODE_OID_GARBAGE;
if (!strcmp(s, "cap-none"))
return MODE_CAP_NONE;
die("unknown --mode=%s", s);
}
/*
* Read "key=value" packets up to a flush, capturing "command" and
* "pathname". Returns 1 if a request was read, 0 on EOF.
*
* The first packet uses the gentle variant so that a clean shutdown
* by Git (EOF) does not produce a spurious "the remote end hung up
* unexpectedly" on stderr. Subsequent packets use the non-gentle
* variant: once inside a request, truncation is a protocol violation
* and dying loudly is the correct response.
*/
static int read_request_header(char **command, char **pathname,
char **old_oid, char **new_oid)
{
int first = 1;
char *line;
*command = *pathname = *old_oid = *new_oid = NULL;
for (;;) {
const char *value;
if (first) {
if (packet_read_line_gently(0, NULL, &line) < 0)
return 0;
first = 0;
} else {
line = packet_read_line(0, NULL);
}
if (!line)
break;
if (skip_prefix(line, "command=", &value))
*command = xstrdup(value);
else if (skip_prefix(line, "pathname=", &value))
*pathname = xstrdup(value);
else if (skip_prefix(line, "old-oid=", &value))
*old_oid = xstrdup(value);
else if (skip_prefix(line, "new-oid=", &value))
*new_oid = xstrdup(value);
}
return 1;
}
static void send_status(const char *status)
{
packet_flush(1);
packet_write_fmt(1, "%s\n", status);
packet_flush(1);
}
static void command_loop(enum mode mode)
{
for (;;) {
char *command = NULL, *pathname = NULL;
char *old_oid = NULL, *new_oid = NULL;
if (!read_request_header(&command, &pathname,
&old_oid, &new_oid))
break; /* EOF: Git closed its end */
if (!command || strcmp(command, "hunks-by-oid"))
die("unexpected command: '%s'",
command ? command : "(none)");
if (logfile) {
fprintf(logfile,
"command=%s pathname=%s old-oid=%s new-oid=%s\n",
command,
pathname ? pathname : "(none)",
old_oid ? old_oid : "(none)",
new_oid ? new_oid : "(none)");
fflush(logfile);
}
switch (mode) {
case MODE_OID_FIXED:
packet_write_fmt(1, "hunk 5 2 5 2\n");
send_status("status=success");
break;
case MODE_OID_EQUAL:
send_status("status=success");
break;
case MODE_OID_EMPTY:
/*
* An empty old side: the "git diff" convention
* addresses it with a start of 0 and a count of 0.
* Claims two lines added, fewer than the builtin
* would show, so the answer is observable.
*/
packet_write_fmt(1, "hunk 0 0 1 2\n");
send_status("status=success");
break;
case MODE_OID_TRAILING:
/*
* Git must ignore trailing space-separated tokens
* on a hunk line (the appendability rule), so this
* must behave exactly like oid-fixed.
*/
packet_write_fmt(1, "hunk 5 2 5 2 moved=yes\n");
send_status("status=success");
break;
case MODE_OID_MALFORMED:
packet_write_fmt(1, "hunk five two 5 2\n");
send_status("status=success");
break;
case MODE_OID_HUGE:
/*
* In-range for int32 (and for a 32-bit long), so
* only the blob-size bound can reject it.
*/
packet_write_fmt(1, "hunk 1 1000000000 1 1000000000\n");
send_status("status=success");
break;
case MODE_OID_ERANGE:
/* Overflows strtol() even where long is 64-bit. */
packet_write_fmt(1, "hunk 1 99999999999999999999 1 1\n");
send_status("status=success");
break;
case MODE_OID_OVERLAP:
packet_write_fmt(1, "hunk 3 2 3 2\n");
packet_write_fmt(1, "hunk 2 2 2 2\n");
send_status("status=success");
break;
case MODE_OID_MISALIGNED:
packet_write_fmt(1, "hunk 2 1 2 1\n");
packet_write_fmt(1, "hunk 5 1 6 1\n");
send_status("status=success");
break;
case MODE_OID_BADSTART:
/*
* A start of 0 names an empty side, so a nonzero
* count beside it names no line; the coordinate is
* rejected per pair while the process stays alive.
*/
packet_write_fmt(1, "hunk 0 2 1 2\n");
send_status("status=success");
break;
case MODE_OID_UNKNOWN_STATUS:
send_status("status=frobnicate");
break;
case MODE_OID_ABORT:
send_status("status=abort");
break;
case MODE_OID_BARE_STATUS:
/* No hunk-section flush: a protocol violation. */
packet_write_fmt(1, "status=success\n");
packet_flush(1);
break;
case MODE_OID_EMPTY_PACKET:
/*
* An empty packet is not a flush; inside the hunk
* section it is a protocol violation.
*/
if (write(1, "0004", 4) < 0)
die_errno("write empty packet");
send_status("status=success");
break;
case MODE_OID_CRASH:
packet_write_fmt(1, "hunk 5 2 5 2\n");
exit(0);
case MODE_OID_GARBAGE:
if (write(1, "@@@@ not a pkt-line @@@@", 24) < 0)
die_errno("write garbage");
exit(0);
default:
send_status("status=need-content");
break;
}
free(command);
free(pathname);
free(old_oid);
free(new_oid);
}
}
static void handshake(enum mode mode)
{
char *line;
line = packet_read_line(0, NULL);
if (!line || strcmp(line, "git-diff-client"))
die("bad welcome: '%s'", line ? line : "(eof)");
line = packet_read_line(0, NULL);
if (!line || strcmp(line, "version=1"))
die("bad version: '%s'", line ? line : "(eof)");
if (packet_read_line(0, NULL))
die("expected flush after version");
packet_write_fmt(1, "git-diff-server\n");
packet_write_fmt(1, "version=1\n");
packet_flush(1);
/* Drain capabilities advertised by Git */
while ((line = packet_read_line(0, NULL)))
; /* drain */
if (mode != MODE_CAP_NONE)
packet_write_fmt(1, "capability=hunks-by-oid\n");
packet_flush(1);
}
static const char *const usage_str[] = {
"test-tool diff-process-backend --mode=<mode> [--log=<path>]",
NULL
};
int cmd__diff_process_backend(int argc, const char **argv)
{
const char *mode_str = NULL, *log_path = NULL;
enum mode mode = MODE_OID_FIXED;
struct option options[] = {
OPT_STRING(0, "mode", &mode_str, "mode",
"response shape (default oid-fixed);"
" see the file header for the full list of modes"),
OPT_STRING(0, "log", &log_path, "path",
"append per-request summary to this file"),
OPT_END()
};
argc = parse_options(argc, argv, NULL, options, usage_str, 0);
if (argc)
usage_with_options(usage_str, options);
if (mode_str)
mode = parse_mode(mode_str);
if (log_path) {
logfile = fopen(log_path, "a");
if (!logfile)
die_errno("failed to open log '%s'", log_path);
}
handshake(mode);
command_loop(mode);
if (logfile && fclose(logfile))
die_errno("error closing log");
return 0;
}