diff options
Diffstat (limited to 'contrib/cvs/src/client.c')
-rw-r--r-- | contrib/cvs/src/client.c | 5948 |
1 files changed, 5948 insertions, 0 deletions
diff --git a/contrib/cvs/src/client.c b/contrib/cvs/src/client.c new file mode 100644 index 0000000..1c8a5d0 --- /dev/null +++ b/contrib/cvs/src/client.c @@ -0,0 +1,5948 @@ +/* CVS client-related stuff. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. */ + +/* + * $FreeBSD$ + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif /* HAVE_CONFIG_H */ + +#include <assert.h> +#include "cvs.h" +#include "getline.h" +#include "edit.h" +#include "buffer.h" +#include "savecwd.h" + +#ifdef CLIENT_SUPPORT + +# include "md5.h" + +# if defined(AUTH_CLIENT_SUPPORT) || defined(HAVE_KERBEROS) || defined(HAVE_GSSAPI) || defined(SOCK_ERRNO) || defined(SOCK_STRERROR) +# ifdef HAVE_WINSOCK_H +# include <winsock.h> +# else /* No winsock.h */ +# include <sys/socket.h> +# include <netinet/in.h> +# include <arpa/inet.h> +# include <netdb.h> +# endif /* No winsock.h */ +# endif + +/* If SOCK_ERRNO is defined, then send()/recv() and other socket calls + do not set errno, but that this macro should be used to obtain an + error code. This probably doesn't make sense unless + NO_SOCKET_TO_FD is also defined. */ +# ifndef SOCK_ERRNO +# define SOCK_ERRNO errno +# endif + +/* If SOCK_STRERROR is defined, then the error codes returned by + socket operations are not known to strerror, and this macro must be + used instead to convert those error codes to strings. */ +# ifndef SOCK_STRERROR +# define SOCK_STRERROR strerror + +# if STDC_HEADERS +# include <string.h> +# endif + +# ifndef strerror +extern char *strerror (); +# endif +# endif /* ! SOCK_STRERROR */ + +# if HAVE_KERBEROS + +# include <krb.h> + +extern char *krb_realmofhost (); +# ifndef HAVE_KRB_GET_ERR_TEXT +# define krb_get_err_text(status) krb_err_txt[status] +# endif /* HAVE_KRB_GET_ERR_TEXT */ + +/* Information we need if we are going to use Kerberos encryption. */ +static C_Block kblock; +static Key_schedule sched; + +# endif /* HAVE_KERBEROS */ + +# ifdef HAVE_GSSAPI + +# include "xgssapi.h" + +/* This is needed for GSSAPI encryption. */ +static gss_ctx_id_t gcontext; + +static int connect_to_gserver PROTO((cvsroot_t *, int, struct hostent *)); + +# endif /* HAVE_GSSAPI */ + + + +/* Keep track of any paths we are sending for Max-dotdot so that we can verify + * that uplevel paths coming back form the server are valid. + * + * FIXME: The correct way to do this is probably provide some sort of virtual + * path map on the client side. This would be generic enough to be applied to + * absolute paths supplied by the user too. + */ +static List *uppaths = NULL; + + + +static void add_prune_candidate PROTO((const char *)); + +/* All the commands. */ +int add PROTO((int argc, char **argv)); +int admin PROTO((int argc, char **argv)); +int checkout PROTO((int argc, char **argv)); +int commit PROTO((int argc, char **argv)); +int diff PROTO((int argc, char **argv)); +int history PROTO((int argc, char **argv)); +int import PROTO((int argc, char **argv)); +int cvslog PROTO((int argc, char **argv)); +int patch PROTO((int argc, char **argv)); +int release PROTO((int argc, char **argv)); +int cvsremove PROTO((int argc, char **argv)); +int rtag PROTO((int argc, char **argv)); +int status PROTO((int argc, char **argv)); +int tag PROTO((int argc, char **argv)); +int update PROTO((int argc, char **argv)); + +/* All the response handling functions. */ +static void handle_ok PROTO((char *, int)); +static void handle_error PROTO((char *, int)); +static void handle_valid_requests PROTO((char *, int)); +static void handle_checked_in PROTO((char *, int)); +static void handle_new_entry PROTO((char *, int)); +static void handle_checksum PROTO((char *, int)); +static void handle_copy_file PROTO((char *, int)); +static void handle_updated PROTO((char *, int)); +static void handle_merged PROTO((char *, int)); +static void handle_patched PROTO((char *, int)); +static void handle_rcs_diff PROTO((char *, int)); +static void handle_removed PROTO((char *, int)); +static void handle_remove_entry PROTO((char *, int)); +static void handle_set_static_directory PROTO((char *, int)); +static void handle_clear_static_directory PROTO((char *, int)); +static void handle_set_sticky PROTO((char *, int)); +static void handle_clear_sticky PROTO((char *, int)); +static void handle_module_expansion PROTO((char *, int)); +static void handle_wrapper_rcs_option PROTO((char *, int)); +static void handle_m PROTO((char *, int)); +static void handle_e PROTO((char *, int)); +static void handle_f PROTO((char *, int)); +static void handle_notified PROTO((char *, int)); + +static size_t try_read_from_server PROTO ((char *, size_t)); + +static void auth_server PROTO ((cvsroot_t *, struct buffer *, struct buffer *, + int, int, struct hostent *)); + +/* We need to keep track of the list of directories we've sent to the + server. This list, along with the current CVSROOT, will help us + decide which command-line arguments to send. */ +List *dirs_sent_to_server = NULL; + +static int is_arg_a_parent_or_listed_dir PROTO((Node *, void *)); + +static int +is_arg_a_parent_or_listed_dir (n, d) + Node *n; + void *d; +{ + char *directory = n->key; /* name of the dir sent to server */ + char *this_argv_elem = xstrdup (d); /* this argv element */ + int retval; + + /* Say we should send this argument if the argument matches the + beginning of a directory name sent to the server. This way, + the server will know to start at the top of that directory + hierarchy and descend. */ + + strip_trailing_slashes (this_argv_elem); + if (strncmp (directory, this_argv_elem, strlen (this_argv_elem)) == 0) + retval = 1; + else + retval = 0; + + free (this_argv_elem); + return retval; +} + +static int arg_should_not_be_sent_to_server PROTO((char *)); + +/* Return nonzero if this argument should not be sent to the + server. */ + +static int +arg_should_not_be_sent_to_server (arg) + char *arg; +{ + /* Decide if we should send this directory name to the server. We + should always send argv[i] if: + + 1) the list of directories sent to the server is empty (as it + will be for checkout, etc.). + + 2) the argument is "." + + 3) the argument is a file in the cwd and the cwd is checked out + from the current root + + 4) the argument lies within one of the paths in + dirs_sent_to_server. + + */ + + if (list_isempty (dirs_sent_to_server)) + return 0; /* always send it */ + + if (strcmp (arg, ".") == 0) + return 0; /* always send it */ + + /* We should send arg if it is one of the directories sent to the + server or the parent of one; this tells the server to descend + the hierarchy starting at this level. */ + if (isdir (arg)) + { + if (walklist (dirs_sent_to_server, is_arg_a_parent_or_listed_dir, arg)) + return 0; + + /* If arg wasn't a parent, we don't know anything about it (we + would have seen something related to it during the + send_files phase). Don't send it. */ + return 1; + } + + /* Try to decide whether we should send arg to the server by + checking the contents of the corresponding CVSADM directory. */ + { + char *t, *root_string; + cvsroot_t *this_root = NULL; + + /* Calculate "dirname arg" */ + for (t = arg + strlen (arg) - 1; t >= arg; t--) + { + if (ISDIRSEP(*t)) + break; + } + + /* Now we're either poiting to the beginning of the + string, or we found a path separator. */ + if (t >= arg) + { + /* Found a path separator. */ + char c = *t; + *t = '\0'; + + /* First, check to see if we sent this directory to the + server, because it takes less time than actually + opening the stuff in the CVSADM directory. */ + if (walklist (dirs_sent_to_server, is_arg_a_parent_or_listed_dir, + arg)) + { + *t = c; /* make sure to un-truncate the arg */ + return 0; + } + + /* Since we didn't find it in the list, check the CVSADM + files on disk. */ + this_root = Name_Root (arg, (char *) NULL); + root_string = this_root->original; + *t = c; + } + else + { + /* We're at the beginning of the string. Look at the + CVSADM files in cwd. */ + if (CVSroot_cmdline) + root_string = CVSroot_cmdline; + else + { + this_root = Name_Root ((char *) NULL, (char *) NULL); + root_string = this_root->original; + } + } + + /* Now check the value for root. */ + if (CVSroot_cmdline == NULL && + root_string && current_parsed_root + && (strcmp (root_string, current_parsed_root->original) != 0)) + { + /* Don't send this, since the CVSROOTs don't match. */ + if (this_root) free_cvsroot_t (this_root); + return 1; + } + if (this_root) free_cvsroot_t (this_root); + } + + /* OK, let's send it. */ + return 0; +} + + + +#endif /* CLIENT_SUPPORT */ + + + +#if defined(CLIENT_SUPPORT) || defined(SERVER_SUPPORT) + +/* Shared with server. */ + +/* + * Return a malloc'd, '\0'-terminated string + * corresponding to the mode in SB. + */ +char * +#ifdef __STDC__ +mode_to_string (mode_t mode) +#else /* ! __STDC__ */ +mode_to_string (mode) + mode_t mode; +#endif /* __STDC__ */ +{ + char buf[18], u[4], g[4], o[4]; + int i; + + i = 0; + if (mode & S_IRUSR) u[i++] = 'r'; + if (mode & S_IWUSR) u[i++] = 'w'; + if (mode & S_IXUSR) u[i++] = 'x'; + u[i] = '\0'; + + i = 0; + if (mode & S_IRGRP) g[i++] = 'r'; + if (mode & S_IWGRP) g[i++] = 'w'; + if (mode & S_IXGRP) g[i++] = 'x'; + g[i] = '\0'; + + i = 0; + if (mode & S_IROTH) o[i++] = 'r'; + if (mode & S_IWOTH) o[i++] = 'w'; + if (mode & S_IXOTH) o[i++] = 'x'; + o[i] = '\0'; + + sprintf(buf, "u=%s,g=%s,o=%s", u, g, o); + return xstrdup(buf); +} + +/* + * Change mode of FILENAME to MODE_STRING. + * Returns 0 for success or errno code. + * If RESPECT_UMASK is set, then honor the umask. + */ +int +change_mode (filename, mode_string, respect_umask) + char *filename; + char *mode_string; + int respect_umask; +{ +#ifdef CHMOD_BROKEN + char *p; + int writeable = 0; + + /* We can only distinguish between + 1) readable + 2) writeable + 3) Picasso's "Blue Period" + We handle the first two. */ + p = mode_string; + while (*p != '\0') + { + if ((p[0] == 'u' || p[0] == 'g' || p[0] == 'o') && p[1] == '=') + { + char *q = p + 2; + while (*q != ',' && *q != '\0') + { + if (*q == 'w') + writeable = 1; + ++q; + } + } + /* Skip to the next field. */ + while (*p != ',' && *p != '\0') + ++p; + if (*p == ',') + ++p; + } + + /* xchmod honors the umask for us. In the !respect_umask case, we + don't try to cope with it (probably to handle that well, the server + needs to deal with modes in data structures, rather than via the + modes in temporary files). */ + xchmod (filename, writeable); + return 0; + +#else /* ! CHMOD_BROKEN */ + + char *p; + mode_t mode = 0; + mode_t oumask; + + p = mode_string; + while (*p != '\0') + { + if ((p[0] == 'u' || p[0] == 'g' || p[0] == 'o') && p[1] == '=') + { + int can_read = 0, can_write = 0, can_execute = 0; + char *q = p + 2; + while (*q != ',' && *q != '\0') + { + if (*q == 'r') + can_read = 1; + else if (*q == 'w') + can_write = 1; + else if (*q == 'x') + can_execute = 1; + ++q; + } + if (p[0] == 'u') + { + if (can_read) + mode |= S_IRUSR; + if (can_write) + mode |= S_IWUSR; + if (can_execute) + mode |= S_IXUSR; + } + else if (p[0] == 'g') + { + if (can_read) + mode |= S_IRGRP; + if (can_write) + mode |= S_IWGRP; + if (can_execute) + mode |= S_IXGRP; + } + else if (p[0] == 'o') + { + if (can_read) + mode |= S_IROTH; + if (can_write) + mode |= S_IWOTH; + if (can_execute) + mode |= S_IXOTH; + } + } + /* Skip to the next field. */ + while (*p != ',' && *p != '\0') + ++p; + if (*p == ',') + ++p; + } + + if (respect_umask) + { + oumask = umask (0); + (void) umask (oumask); + mode &= ~oumask; + } + + if (chmod (filename, mode) < 0) + return errno; + return 0; +#endif /* ! CHMOD_BROKEN */ +} + +#endif /* CLIENT_SUPPORT or SERVER_SUPPORT */ + +#ifdef CLIENT_SUPPORT + +int client_prune_dirs; + +static List *ignlist = (List *) NULL; + +/* Buffer to write to the server. */ +static struct buffer *to_server; + +/* Buffer used to read from the server. */ +static struct buffer *from_server; + + +/* We want to be able to log data sent between us and the server. We + do it using log buffers. Each log buffer has another buffer which + handles the actual I/O, and a file to log information to. + + This structure is the closure field of a log buffer. */ + +struct log_buffer +{ + /* The underlying buffer. */ + struct buffer *buf; + /* The file to log information to. */ + FILE *log; +}; + +static struct buffer *log_buffer_initialize + PROTO((struct buffer *, FILE *, int, void (*) (struct buffer *))); +static int log_buffer_input PROTO((void *, char *, int, int, int *)); +static int log_buffer_output PROTO((void *, const char *, int, int *)); +static int log_buffer_flush PROTO((void *)); +static int log_buffer_block PROTO((void *, int)); +static int log_buffer_shutdown PROTO((struct buffer *)); + +/* Create a log buffer. */ + +static struct buffer * +log_buffer_initialize (buf, fp, input, memory) + struct buffer *buf; + FILE *fp; + int input; + void (*memory) PROTO((struct buffer *)); +{ + struct log_buffer *n; + + n = (struct log_buffer *) xmalloc (sizeof *n); + n->buf = buf; + n->log = fp; + return buf_initialize (input ? log_buffer_input : NULL, + input ? NULL : log_buffer_output, + input ? NULL : log_buffer_flush, + log_buffer_block, + log_buffer_shutdown, + memory, + n); +} + +/* The input function for a log buffer. */ + +static int +log_buffer_input (closure, data, need, size, got) + void *closure; + char *data; + int need; + int size; + int *got; +{ + struct log_buffer *lb = (struct log_buffer *) closure; + int status; + size_t n_to_write; + + if (lb->buf->input == NULL) + abort (); + + status = (*lb->buf->input) (lb->buf->closure, data, need, size, got); + if (status != 0) + return status; + + if (*got > 0) + { + n_to_write = *got; + if (fwrite (data, 1, n_to_write, lb->log) != n_to_write) + error (0, errno, "writing to log file"); + } + + return 0; +} + +/* The output function for a log buffer. */ + +static int +log_buffer_output (closure, data, have, wrote) + void *closure; + const char *data; + int have; + int *wrote; +{ + struct log_buffer *lb = (struct log_buffer *) closure; + int status; + size_t n_to_write; + + if (lb->buf->output == NULL) + abort (); + + status = (*lb->buf->output) (lb->buf->closure, data, have, wrote); + if (status != 0) + return status; + + if (*wrote > 0) + { + n_to_write = *wrote; + if (fwrite (data, 1, n_to_write, lb->log) != n_to_write) + error (0, errno, "writing to log file"); + } + + return 0; +} + +/* The flush function for a log buffer. */ + +static int +log_buffer_flush (closure) + void *closure; +{ + struct log_buffer *lb = (struct log_buffer *) closure; + + if (lb->buf->flush == NULL) + abort (); + + /* We don't really have to flush the log file here, but doing it + will let tail -f on the log file show what is sent to the + network as it is sent. */ + if (fflush (lb->log) != 0) + error (0, errno, "flushing log file"); + + return (*lb->buf->flush) (lb->buf->closure); +} + +/* The block function for a log buffer. */ + +static int +log_buffer_block (closure, block) + void *closure; + int block; +{ + struct log_buffer *lb = (struct log_buffer *) closure; + + if (block) + return set_block (lb->buf); + else + return set_nonblock (lb->buf); +} + +/* The shutdown function for a log buffer. */ + +static int +log_buffer_shutdown (buf) + struct buffer *buf; +{ + struct log_buffer *lb = (struct log_buffer *) buf->closure; + int retval; + + retval = buf_shutdown (lb->buf); + if (fclose (lb->log) < 0) + error (0, errno, "closing log file"); + return retval; +} + +#ifdef NO_SOCKET_TO_FD + +/* Under certain circumstances, we must communicate with the server + via a socket using send() and recv(). This is because under some + operating systems (OS/2 and Windows 95 come to mind), a socket + cannot be converted to a file descriptor -- it must be treated as a + socket and nothing else. + + We may also need to deal with socket routine error codes differently + in these cases. This is handled through the SOCK_ERRNO and + SOCK_STRERROR macros. */ + +/* These routines implement a buffer structure which uses send and + recv. The buffer is always in blocking mode so we don't implement + the block routine. */ + +/* Note that it is important that these routines always handle errors + internally and never return a positive errno code, since it would in + general be impossible for the caller to know in general whether any + error code came from a socket routine (to decide whether to use + SOCK_STRERROR or simply strerror to print an error message). */ + +/* We use an instance of this structure as the closure field. */ + +struct socket_buffer +{ + /* The socket number. */ + int socket; +}; + +static struct buffer *socket_buffer_initialize + PROTO ((int, int, void (*) (struct buffer *))); +static int socket_buffer_input PROTO((void *, char *, int, int, int *)); +static int socket_buffer_output PROTO((void *, const char *, int, int *)); +static int socket_buffer_flush PROTO((void *)); +static int socket_buffer_shutdown PROTO((struct buffer *)); + + + +/* Create a buffer based on a socket. */ + +static struct buffer * +socket_buffer_initialize (socket, input, memory) + int socket; + int input; + void (*memory) PROTO((struct buffer *)); +{ + struct socket_buffer *n; + + n = (struct socket_buffer *) xmalloc (sizeof *n); + n->socket = socket; + return buf_initialize (input ? socket_buffer_input : NULL, + input ? NULL : socket_buffer_output, + input ? NULL : socket_buffer_flush, + (int (*) PROTO((void *, int))) NULL, + socket_buffer_shutdown, + memory, + n); +} + + + +/* The buffer input function for a buffer built on a socket. */ + +static int +socket_buffer_input (closure, data, need, size, got) + void *closure; + char *data; + int need; + int size; + int *got; +{ + struct socket_buffer *sb = (struct socket_buffer *) closure; + int nbytes; + + /* I believe that the recv function gives us exactly the semantics + we want. If there is a message, it returns immediately with + whatever it could get. If there is no message, it waits until + one comes in. In other words, it is not like read, which in + blocking mode normally waits until all the requested data is + available. */ + + *got = 0; + + do + { + + /* Note that for certain (broken?) networking stacks, like + VMS's UCX (not sure what version, problem reported with + recv() in 1997), and (according to windows-NT/config.h) + Windows NT 3.51, we must call recv or send with a + moderately sized buffer (say, less than 200K or something), + or else there may be network errors (somewhat hard to + produce, e.g. WAN not LAN or some such). buf_read_data + makes sure that we only recv() BUFFER_DATA_SIZE bytes at + a time. */ + + nbytes = recv (sb->socket, data, size, 0); + if (nbytes < 0) + error (1, 0, "reading from server: %s", SOCK_STRERROR (SOCK_ERRNO)); + if (nbytes == 0) + { + /* End of file (for example, the server has closed + the connection). If we've already read something, we + just tell the caller about the data, not about the end of + file. If we've read nothing, we return end of file. */ + if (*got == 0) + return -1; + else + return 0; + } + need -= nbytes; + size -= nbytes; + data += nbytes; + *got += nbytes; + } + while (need > 0); + + return 0; +} + + + +/* The buffer output function for a buffer built on a socket. */ + +static int +socket_buffer_output (closure, data, have, wrote) + void *closure; + const char *data; + int have; + int *wrote; +{ + struct socket_buffer *sb = (struct socket_buffer *) closure; + + *wrote = have; + + /* See comment in socket_buffer_input regarding buffer size we pass + to send and recv. */ + +#ifdef SEND_NEVER_PARTIAL + /* If send() never will produce a partial write, then just do it. This + is needed for systems where its return value is something other than + the number of bytes written. */ + if (send (sb->socket, data, have, 0) < 0) + error (1, 0, "writing to server socket: %s", SOCK_STRERROR (SOCK_ERRNO)); +#else + while (have > 0) + { + int nbytes; + + nbytes = send (sb->socket, data, have, 0); + if (nbytes < 0) + error (1, 0, "writing to server socket: %s", SOCK_STRERROR (SOCK_ERRNO)); + + have -= nbytes; + data += nbytes; + } +#endif + + return 0; +} + + + +/* The buffer flush function for a buffer built on a socket. */ + +/*ARGSUSED*/ +static int +socket_buffer_flush (closure) + void *closure; +{ + /* Nothing to do. Sockets are always flushed. */ + return 0; +} + + + +static int +socket_buffer_shutdown (buf) + struct buffer *buf; +{ + struct socket_buffer *n = (struct socket_buffer *) buf->closure; + char tmp; + + /* no need to flush children of an endpoint buffer here */ + + if (buf->input) + { + int err = 0; + if (! buf_empty_p (buf) + || (err = recv (n->socket, &tmp, 1, 0)) > 0) + error (0, 0, "dying gasps from %s unexpected", current_parsed_root->hostname); + else if (err == -1) + error (0, 0, "reading from %s: %s", current_parsed_root->hostname, SOCK_STRERROR (SOCK_ERRNO)); + + /* shutdown() socket */ +# ifdef SHUTDOWN_SERVER + if (current_parsed_root->method != server_method) +# endif + if (shutdown (n->socket, 0) < 0) + { + error (1, 0, "shutting down server socket: %s", SOCK_STRERROR (SOCK_ERRNO)); + } + + buf->input = NULL; + } + else if (buf->output) + { + /* shutdown() socket */ +# ifdef SHUTDOWN_SERVER + /* FIXME: Should have a SHUTDOWN_SERVER_INPUT & + * SHUTDOWN_SERVER_OUTPUT + */ + if (current_parsed_root->method == server_method) + SHUTDOWN_SERVER (n->socket); + else +# endif + if (shutdown (n->socket, 1) < 0) + { + error (1, 0, "shutting down server socket: %s", SOCK_STRERROR (SOCK_ERRNO)); + } + + buf->output = NULL; + } + + return 0; +} + +#endif /* NO_SOCKET_TO_FD */ + +/* + * Read a line from the server. Result does not include the terminating \n. + * + * Space for the result is malloc'd and should be freed by the caller. + * + * Returns number of bytes read. + */ +static int +read_line (resultp) + char **resultp; +{ + int status; + char *result; + int len; + + status = buf_flush (to_server, 1); + if (status != 0) + error (1, status, "writing to server"); + + status = buf_read_line (from_server, &result, &len); + if (status != 0) + { + if (status == -1) + error (1, 0, "end of file from server (consult above messages if any)"); + else if (status == -2) + error (1, 0, "out of memory"); + else + error (1, status, "reading from server"); + } + + if (resultp != NULL) + *resultp = result; + else + free (result); + + return len; +} + +#endif /* CLIENT_SUPPORT */ + + +#if defined(CLIENT_SUPPORT) || defined(SERVER_SUPPORT) + +/* + * Level of compression to use when running gzip on a single file. + */ +int file_gzip_level; + +#endif /* CLIENT_SUPPORT or SERVER_SUPPORT */ + +#ifdef CLIENT_SUPPORT + +/* + * The Repository for the top level of this command (not necessarily + * the CVSROOT, just the current directory at the time we do it). + */ +static char *toplevel_repos = NULL; + +/* Working directory when we first started. Note: we could speed things + up on some systems by using savecwd.h here instead of just always + storing a name. */ +char *toplevel_wd; + +static void +handle_ok (args, len) + char *args; + int len; +{ + return; +} + +static void +handle_error (args, len) + char *args; + int len; +{ + int something_printed; + + /* + * First there is a symbolic error code followed by a space, which + * we ignore. + */ + char *p = strchr (args, ' '); + if (p == NULL) + { + error (0, 0, "invalid data from cvs server"); + return; + } + ++p; + + /* Next we print the text of the message from the server. We + probably should be prefixing it with "server error" or some + such, because if it is something like "Out of memory", the + current behavior doesn't say which machine is out of + memory. */ + + len -= p - args; + something_printed = 0; + for (; len > 0; --len) + { + something_printed = 1; + putc (*p++, stderr); + } + if (something_printed) + putc ('\n', stderr); +} + +static void +handle_valid_requests (args, len) + char *args; + int len; +{ + char *p = args; + char *q; + struct request *rq; + do + { + q = strchr (p, ' '); + if (q != NULL) + *q++ = '\0'; + for (rq = requests; rq->name != NULL; ++rq) + { + if (strcmp (rq->name, p) == 0) + break; + } + if (rq->name == NULL) + /* + * It is a request we have never heard of (and thus never + * will want to use). So don't worry about it. + */ + ; + else + { + if (rq->flags & RQ_ENABLEME) + { + /* + * Server wants to know if we have this, to enable the + * feature. + */ + send_to_server (rq->name, 0); + send_to_server ("\012", 0); + } + else + rq->flags |= RQ_SUPPORTED; + } + p = q; + } while (q != NULL); + for (rq = requests; rq->name != NULL; ++rq) + { + if ((rq->flags & RQ_SUPPORTED) + || (rq->flags & RQ_ENABLEME)) + continue; + if (rq->flags & RQ_ESSENTIAL) + error (1, 0, "request `%s' not supported by server", rq->name); + } +} + + + +/* + * This is a proc for walklist(). It inverts the error return premise of + * walklist. + * + * RETURNS + * True If this path is prefixed by one of the paths in walklist and + * does not step above the prefix path. + * False Otherwise. + */ +static +int path_list_prefixed (p, closure) + Node *p; + void *closure; +{ + const char *questionable = closure; + const char *prefix = p->key; + if (strncmp (prefix, questionable, strlen (prefix))) return 0; + questionable += strlen (prefix); + while (ISDIRSEP (*questionable)) questionable++; + if (*questionable == '\0') return 1; + return pathname_levels (questionable); +} + + + +/* + * Need to validate the client pathname. Disallowed paths include: + * + * 1. Absolute paths. + * 2. Pathnames that do not reference a specifically requested update + * directory. + * + * In case 2, we actually only check that the directory is under the uppermost + * directories mentioned on the command line. + * + * RETURNS + * True If the path is valid. + * False Otherwise. + */ +static +int is_valid_client_path (pathname) + const char *pathname; +{ + /* 1. Absolute paths. */ + if (isabsolute (pathname)) return 0; + /* 2. No up-references in path. */ + if (pathname_levels (pathname) == 0) return 1; + /* 2. No Max-dotdot paths registered. */ + if (uppaths == NULL) return 0; + + return walklist (uppaths, path_list_prefixed, (void *)pathname); +} + + + +/* + * Do all the processing for PATHNAME, where pathname consists of the + * repository and the filename. The parameters we pass to FUNC are: + * DATA is just the DATA parameter which was passed to + * call_in_directory; ENT_LIST is a pointer to an entries list (which + * we manage the storage for); SHORT_PATHNAME is the pathname of the + * file relative to the (overall) directory in which the command is + * taking place; and FILENAME is the filename portion only of + * SHORT_PATHNAME. When we call FUNC, the curent directory points to + * the directory portion of SHORT_PATHNAME. */ + +static void +call_in_directory (pathname, func, data) + char *pathname; + void (*func) PROTO((char *data, List *ent_list, char *short_pathname, + char *filename)); + char *data; +{ + /* This variable holds the result of Entries_Open. */ + List *last_entries = NULL; + char *dir_name; + char *filename; + /* This is what we get when we hook up the directory (working directory + name) from PATHNAME with the filename from REPOSNAME. For example: + pathname: ccvs/src/ + reposname: /u/src/master/ccvs/foo/ChangeLog + short_pathname: ccvs/src/ChangeLog + */ + char *short_pathname; + char *p; + + /* + * Do the whole descent in parallel for the repositories, so we + * know what to put in CVS/Repository files. I'm not sure the + * full hair is necessary since the server does a similar + * computation; I suspect that we only end up creating one + * directory at a time anyway. + * + * Also note that we must *only* worry about this stuff when we + * are creating directories; `cvs co foo/bar; cd foo/bar; cvs co + * CVSROOT; cvs update' is legitimate, but in this case + * foo/bar/CVSROOT/CVS/Repository is not a subdirectory of + * foo/bar/CVS/Repository. + */ + char *reposname; + char *short_repos; + char *reposdirname; + char *rdirp; + int reposdirname_absolute; + int newdir = 0; + + assert (pathname); + + reposname = NULL; + read_line (&reposname); + assert (reposname != NULL); + + reposdirname_absolute = 0; + if (strncmp (reposname, toplevel_repos, strlen (toplevel_repos)) != 0) + { + reposdirname_absolute = 1; + short_repos = reposname; + } + else + { + short_repos = reposname + strlen (toplevel_repos) + 1; + if (short_repos[-1] != '/') + { + reposdirname_absolute = 1; + short_repos = reposname; + } + } + + /* Now that we have SHORT_REPOS, we can calculate the path to the file we + * are being requested to operate on. + */ + filename = strrchr (short_repos, '/'); + if (filename == NULL) + filename = short_repos; + else + ++filename; + + short_pathname = xmalloc (strlen (pathname) + strlen (filename) + 5); + strcpy (short_pathname, pathname); + strcat (short_pathname, filename); + + /* Now that we know the path to the file we were requested to operate on, + * we can verify that it is valid. + * + * For security reasons, if SHORT_PATHNAME is absolute or attempts to + * ascend outside of the current sanbbox, we abort. The server should not + * send us anything but relative paths which remain inside the sandbox + * here. Anything less means a trojan CVS server could create and edit + * arbitrary files on the client. + */ + if (!is_valid_client_path (short_pathname)) + { + error (0, 0, + "Server attempted to update a file via an invalid pathname:"); + error (1, 0, "`%s'.", short_pathname); + } + + reposdirname = xstrdup (short_repos); + p = strrchr (reposdirname, '/'); + if (p == NULL) + { + reposdirname = xrealloc (reposdirname, 2); + reposdirname[0] = '.'; reposdirname[1] = '\0'; + } + else + *p = '\0'; + + dir_name = xstrdup (pathname); + p = strrchr (dir_name, '/'); + if (p == NULL) + { + dir_name = xrealloc (dir_name, 2); + dir_name[0] = '.'; dir_name[1] = '\0'; + } + else + *p = '\0'; + if (client_prune_dirs) + add_prune_candidate (dir_name); + + if (toplevel_wd == NULL) + { + toplevel_wd = xgetwd (); + if (toplevel_wd == NULL) + error (1, errno, "could not get working directory"); + } + + if (CVS_CHDIR (toplevel_wd) < 0) + error (1, errno, "could not chdir to %s", toplevel_wd); + + if (CVS_CHDIR (dir_name) < 0) + { + char *dir; + char *dirp; + + if (! existence_error (errno)) + error (1, errno, "could not chdir to %s", dir_name); + + /* Directory does not exist, we need to create it. */ + newdir = 1; + + /* Provided we are willing to assume that directories get + created one at a time, we could simplify this a lot. + Do note that one aspect still would need to walk the + dir_name path: the checking for "fncmp (dir, CVSADM)". */ + + dir = xmalloc (strlen (dir_name) + 1); + dirp = dir_name; + rdirp = reposdirname; + + /* This algorithm makes nested directories one at a time + and create CVS administration files in them. For + example, we're checking out foo/bar/baz from the + repository: + + 1) create foo, point CVS/Repository to <root>/foo + 2) .. foo/bar .. <root>/foo/bar + 3) .. foo/bar/baz .. <root>/foo/bar/baz + + As you can see, we're just stepping along DIR_NAME (with + DIRP) and REPOSDIRNAME (with RDIRP) respectively. + + We need to be careful when we are checking out a + module, however, since DIR_NAME and REPOSDIRNAME are not + going to be the same. Since modules will not have any + slashes in their names, we should watch the output of + STRCHR to decide whether or not we should use STRCHR on + the RDIRP. That is, if we're down to a module name, + don't keep picking apart the repository directory name. */ + + do + { + dirp = strchr (dirp, '/'); + if (dirp) + { + strncpy (dir, dir_name, dirp - dir_name); + dir[dirp - dir_name] = '\0'; + /* Skip the slash. */ + ++dirp; + if (rdirp == NULL) + /* This just means that the repository string has + fewer components than the dir_name string. But + that is OK (e.g. see modules3-8 in testsuite). */ + ; + else + rdirp = strchr (rdirp, '/'); + } + else + { + /* If there are no more slashes in the dir name, + we're down to the most nested directory -OR- to + the name of a module. In the first case, we + should be down to a DIRP that has no slashes, + so it won't help/hurt to do another STRCHR call + on DIRP. It will definitely hurt, however, if + we're down to a module name, since a module + name can point to a nested directory (that is, + DIRP will still have slashes in it. Therefore, + we should set it to NULL so the routine below + copies the contents of REMOTEDIRNAME onto the + root repository directory (does this if rdirp + is set to NULL, because we used to do an extra + STRCHR call here). */ + + rdirp = NULL; + strcpy (dir, dir_name); + } + + if (fncmp (dir, CVSADM) == 0) + { + error (0, 0, "cannot create a directory named %s", dir); + error (0, 0, "because CVS uses \"%s\" for its own uses", + CVSADM); + error (1, 0, "rename the directory and try again"); + } + + if (mkdir_if_needed (dir)) + { + /* It already existed, fine. Just keep going. */ + } + else if (strcmp (cvs_cmd_name, "export") == 0) + /* Don't create CVSADM directories if this is export. */ + ; + else + { + /* + * Put repository in CVS/Repository. For historical + * (pre-CVS/Root) reasons, this is an absolute pathname, + * but what really matters is the part of it which is + * relative to cvsroot. + */ + char *repo; + char *r, *b; + + repo = xmalloc (strlen (reposdirname) + + strlen (toplevel_repos) + + 80); + if (reposdirname_absolute) + r = repo; + else + { + strcpy (repo, toplevel_repos); + strcat (repo, "/"); + r = repo + strlen (repo); + } + + if (rdirp) + { + /* See comment near start of function; the only + way that the server can put the right thing + in each CVS/Repository file is to create the + directories one at a time. I think that the + CVS server has been doing this all along. */ + error (0, 0, "\ +warning: server is not creating directories one at a time"); + strncpy (r, reposdirname, rdirp - reposdirname); + r[rdirp - reposdirname] = '\0'; + } + else + strcpy (r, reposdirname); + + Create_Admin (dir, dir, repo, + (char *)NULL, (char *)NULL, 0, 0, 1); + free (repo); + + b = strrchr (dir, '/'); + if (b == NULL) + Subdir_Register ((List *) NULL, (char *) NULL, dir); + else + { + *b = '\0'; + Subdir_Register ((List *) NULL, dir, b + 1); + *b = '/'; + } + } + + if (rdirp != NULL) + { + /* Skip the slash. */ + ++rdirp; + } + + } while (dirp != NULL); + free (dir); + /* Now it better work. */ + if ( CVS_CHDIR (dir_name) < 0) + error (1, errno, "could not chdir to %s", dir_name); + } + else if (strcmp (cvs_cmd_name, "export") == 0) + /* Don't create CVSADM directories if this is export. */ + ; + else if (!isdir (CVSADM)) + { + /* + * Put repository in CVS/Repository. For historical + * (pre-CVS/Root) reasons, this is an absolute pathname, + * but what really matters is the part of it which is + * relative to cvsroot. + */ + char *repo; + + if (reposdirname_absolute) + repo = reposdirname; + else + { + repo = xmalloc (strlen (reposdirname) + + strlen (toplevel_repos) + + 10); + strcpy (repo, toplevel_repos); + strcat (repo, "/"); + strcat (repo, reposdirname); + } + + Create_Admin (".", ".", repo, (char *)NULL, (char *)NULL, 0, 1, 1); + if (repo != reposdirname) + free (repo); + } + + if (strcmp (cvs_cmd_name, "export") != 0) + { + last_entries = Entries_Open (0, dir_name); + + /* If this is a newly created directory, we will record + all subdirectory information, so call Subdirs_Known in + case there are no subdirectories. If this is not a + newly created directory, it may be an old working + directory from before we recorded subdirectory + information in the Entries file. We force a search for + all subdirectories now, to make sure our subdirectory + information is up to date. If the Entries file does + record subdirectory information, then this call only + does list manipulation. */ + if (newdir) + Subdirs_Known (last_entries); + else + { + List *dirlist; + + dirlist = Find_Directories ((char *) NULL, W_LOCAL, + last_entries); + dellist (&dirlist); + } + } + free (reposdirname); + (*func) (data, last_entries, short_pathname, filename); + if (last_entries != NULL) + Entries_Close (last_entries); + free (dir_name); + free (short_pathname); + free (reposname); +} + +static void +copy_a_file (data, ent_list, short_pathname, filename) + char *data; + List *ent_list; + char *short_pathname; + char *filename; +{ + char *newname; +#ifdef USE_VMS_FILENAMES + char *p; +#endif + + read_line (&newname); + +#ifdef USE_VMS_FILENAMES + /* Mogrify the filename so VMS is happy with it. */ + for(p = newname; *p; p++) + if(*p == '.' || *p == '#') *p = '_'; +#endif + /* cvsclient.texi has said for a long time that newname must be in the + same directory. Wouldn't want a malicious or buggy server overwriting + ~/.profile, /etc/passwd, or anything like that. */ + if (last_component (newname) != newname) + error (1, 0, "protocol error: Copy-file tried to specify directory"); + + if (unlink_file (newname) && !existence_error (errno)) + error (0, errno, "unable to remove %s", newname); + copy_file (filename, newname); + free (newname); +} + +static void +handle_copy_file (args, len) + char *args; + int len; +{ + call_in_directory (args, copy_a_file, (char *)NULL); +} + + + +/* Attempt to read a file size from a string. Accepts base 8 (0N), base 16 + * (0xN), or base 10. Exits on error. + * + * RETURNS + * The file size, in a size_t. + * + * FATAL ERRORS + * 1. As strtoul(). + * 2. If the number read exceeds SIZE_MAX. + */ +static size_t +strto_file_size (const char *s) +{ + unsigned long tmp; + char *endptr; + + /* Read it. */ + errno = 0; + tmp = strtoul (s, &endptr, 0); + + /* Check for errors. */ + if (errno || endptr == s) + error (1, errno, "Server sent invalid file size `%s'", s); + if (*endptr != '\0') + error (1, 0, + "Server sent trailing characters in file size `%s'", + endptr); + if (tmp > SIZE_MAX) + error (1, 0, "Server sent file size exceeding client max."); + + /* Return it. */ + return (size_t)tmp; +} + + + +static void read_counted_file PROTO ((char *, char *)); + +/* Read from the server the count for the length of a file, then read + the contents of that file and write them to FILENAME. FULLNAME is + the name of the file for use in error messages. FIXME-someday: + extend this to deal with compressed files and make update_entries + use it. On error, gives a fatal error. */ +static void +read_counted_file (filename, fullname) + char *filename; + char *fullname; +{ + char *size_string; + size_t size; + char *buf; + + /* Pointers in buf to the place to put data which will be read, + and the data which needs to be written, respectively. */ + char *pread; + char *pwrite; + /* Number of bytes left to read and number of bytes in buf waiting to + be written, respectively. */ + size_t nread; + size_t nwrite; + + FILE *fp; + + read_line (&size_string); + if (size_string[0] == 'z') + error (1, 0, "\ +protocol error: compressed files not supported for that operation"); + size = strto_file_size (size_string); + free (size_string); + + /* A more sophisticated implementation would use only a limited amount + of buffer space (8K perhaps), and read that much at a time. We allocate + a buffer for the whole file only to make it easy to keep track what + needs to be read and written. */ + buf = xmalloc (size); + + /* FIXME-someday: caller should pass in a flag saying whether it + is binary or not. I haven't carefully looked into whether + CVS/Template files should use local text file conventions or + not. */ + fp = CVS_FOPEN (filename, "wb"); + if (fp == NULL) + error (1, errno, "cannot write %s", fullname); + nread = size; + nwrite = 0; + pread = buf; + pwrite = buf; + while (nread > 0 || nwrite > 0) + { + size_t n; + + if (nread > 0) + { + n = try_read_from_server (pread, nread); + nread -= n; + pread += n; + nwrite += n; + } + + if (nwrite > 0) + { + n = fwrite (pwrite, 1, nwrite, fp); + if (ferror (fp)) + error (1, errno, "cannot write %s", fullname); + nwrite -= n; + pwrite += n; + } + } + free (buf); + if (fclose (fp) < 0) + error (1, errno, "cannot close %s", fullname); +} + +/* OK, we want to swallow the "U foo.c" response and then output it only + if we can update the file. In the future we probably want some more + systematic approach to parsing tagged text, but for now we keep it + ad hoc. "Why," I hear you cry, "do we not just look at the + Update-existing and Created responses?" That is an excellent question, + and the answer is roughly conservatism/laziness--I haven't read through + update.c enough to figure out the exact correspondence or lack thereof + between those responses and a "U foo.c" line (note that Merged, from + join_file, can be either "C foo" or "U foo" depending on the context). */ +/* Nonzero if we have seen +updated and not -updated. */ +static int updated_seen; +/* Filename from an "fname" tagged response within +updated/-updated. */ +static char *updated_fname; + +/* This struct is used to hold data when reading the +importmergecmd + and -importmergecmd tags. We put the variables in a struct only + for namespace issues. FIXME: As noted above, we need to develop a + more systematic approach. */ +static struct +{ + /* Nonzero if we have seen +importmergecmd and not -importmergecmd. */ + int seen; + /* Number of conflicts, from a "conflicts" tagged response. */ + int conflicts; + /* First merge tag, from a "mergetag1" tagged response. */ + char *mergetag1; + /* Second merge tag, from a "mergetag2" tagged response. */ + char *mergetag2; + /* Repository, from a "repository" tagged response. */ + char *repository; +} importmergecmd; + +/* Nonzero if we should arrange to return with a failure exit status. */ +static int failure_exit; + + +/* + * The time stamp of the last file we registered. + */ +static time_t last_register_time; + +/* + * The Checksum response gives the checksum for the file transferred + * over by the next Updated, Merged or Patch response. We just store + * it here, and then check it in update_entries. + */ + +static int stored_checksum_valid; +static unsigned char stored_checksum[16]; + +static void +handle_checksum (args, len) + char *args; + int len; +{ + char *s; + char buf[3]; + int i; + + if (stored_checksum_valid) + error (1, 0, "Checksum received before last one was used"); + + s = args; + buf[2] = '\0'; + for (i = 0; i < 16; i++) + { + char *bufend; + + buf[0] = *s++; + buf[1] = *s++; + stored_checksum[i] = (char) strtol (buf, &bufend, 16); + if (bufend != buf + 2) + break; + } + + if (i < 16 || *s != '\0') + error (1, 0, "Invalid Checksum response: `%s'", args); + + stored_checksum_valid = 1; +} + +/* Mode that we got in a "Mode" response (malloc'd), or NULL if none. */ +static char *stored_mode; + +static void handle_mode PROTO ((char *, int)); + +static void +handle_mode (args, len) + char *args; + int len; +{ + if (stored_mode != NULL) + error (1, 0, "protocol error: duplicate Mode"); + stored_mode = xstrdup (args); +} + +/* Nonzero if time was specified in Mod-time. */ +static int stored_modtime_valid; +/* Time specified in Mod-time. */ +static time_t stored_modtime; + +static void handle_mod_time PROTO ((char *, int)); + +static void +handle_mod_time (args, len) + char *args; + int len; +{ + if (stored_modtime_valid) + error (0, 0, "protocol error: duplicate Mod-time"); + stored_modtime = get_date (args, NULL); + if (stored_modtime == (time_t) -1) + error (0, 0, "protocol error: cannot parse date %s", args); + else + stored_modtime_valid = 1; +} + +/* + * If we receive a patch, but the patch program fails to apply it, we + * want to request the original file. We keep a list of files whose + * patches have failed. + */ + +char **failed_patches; +int failed_patches_count; + +struct update_entries_data +{ + enum { + /* + * We are just getting an Entries line; the local file is + * correct. + */ + UPDATE_ENTRIES_CHECKIN, + /* We are getting the file contents as well. */ + UPDATE_ENTRIES_UPDATE, + /* + * We are getting a patch against the existing local file, not + * an entire new file. + */ + UPDATE_ENTRIES_PATCH, + /* + * We are getting an RCS change text (diff -n output) against + * the existing local file, not an entire new file. + */ + UPDATE_ENTRIES_RCS_DIFF + } contents; + + enum { + /* We are replacing an existing file. */ + UPDATE_ENTRIES_EXISTING, + /* We are creating a new file. */ + UPDATE_ENTRIES_NEW, + /* We don't know whether it is existing or new. */ + UPDATE_ENTRIES_EXISTING_OR_NEW + } existp; + + /* + * String to put in the timestamp field or NULL to use the timestamp + * of the file. + */ + char *timestamp; +}; + +/* Update the Entries line for this file. */ +static void +update_entries (data_arg, ent_list, short_pathname, filename) + char *data_arg; + List *ent_list; + char *short_pathname; + char *filename; +{ + char *entries_line; + struct update_entries_data *data = (struct update_entries_data *)data_arg; + + char *cp; + char *user; + char *vn; + /* Timestamp field. Always empty according to the protocol. */ + char *ts; + char *options = NULL; + char *tag = NULL; + char *date = NULL; + char *tag_or_date; + char *scratch_entries = NULL; + int bin; + +#ifdef UTIME_EXPECTS_WRITABLE + int change_it_back = 0; +#endif + + read_line (&entries_line); + + /* + * Parse the entries line. + */ + scratch_entries = xstrdup (entries_line); + + if (scratch_entries[0] != '/') + error (1, 0, "bad entries line `%s' from server", entries_line); + user = scratch_entries + 1; + if ((cp = strchr (user, '/')) == NULL) + error (1, 0, "bad entries line `%s' from server", entries_line); + *cp++ = '\0'; + vn = cp; + if ((cp = strchr (vn, '/')) == NULL) + error (1, 0, "bad entries line `%s' from server", entries_line); + *cp++ = '\0'; + + ts = cp; + if ((cp = strchr (ts, '/')) == NULL) + error (1, 0, "bad entries line `%s' from server", entries_line); + *cp++ = '\0'; + options = cp; + if ((cp = strchr (options, '/')) == NULL) + error (1, 0, "bad entries line `%s' from server", entries_line); + *cp++ = '\0'; + tag_or_date = cp; + + /* If a slash ends the tag_or_date, ignore everything after it. */ + cp = strchr (tag_or_date, '/'); + if (cp != NULL) + *cp = '\0'; + if (*tag_or_date == 'T') + tag = tag_or_date + 1; + else if (*tag_or_date == 'D') + date = tag_or_date + 1; + + /* Done parsing the entries line. */ + + if (data->contents == UPDATE_ENTRIES_UPDATE + || data->contents == UPDATE_ENTRIES_PATCH + || data->contents == UPDATE_ENTRIES_RCS_DIFF) + { + char *size_string; + char *mode_string; + size_t size; + char *buf; + char *temp_filename; + int use_gzip; + int patch_failed; + char *s; + + read_line (&mode_string); + + read_line (&size_string); + if (size_string[0] == 'z') + { + use_gzip = 1; + s = size_string + 1; + } + else + { + use_gzip = 0; + s = size_string; + } + size = strto_file_size (s); + free (size_string); + + /* Note that checking this separately from writing the file is + a race condition: if the existence or lack thereof of the + file changes between now and the actual calls which + operate on it, we lose. However (a) there are so many + cases, I'm reluctant to try to fix them all, (b) in some + cases the system might not even have a system call which + does the right thing, and (c) it isn't clear this needs to + work. */ + if (data->existp == UPDATE_ENTRIES_EXISTING + && !isfile (filename)) + /* Emit a warning and update the file anyway. */ + error (0, 0, "warning: %s unexpectedly disappeared", + short_pathname); + + if (data->existp == UPDATE_ENTRIES_NEW + && isfile (filename)) + { + /* Emit a warning and refuse to update the file; we don't want + to clobber a user's file. */ + size_t nread; + size_t toread; + + /* size should be unsigned, but until we get around to fixing + that, work around it. */ + size_t usize; + + char buf[8192]; + + /* This error might be confusing; it isn't really clear to + the user what to do about it. Keep in mind that it has + several causes: (1) something/someone creates the file + during the time that CVS is running, (2) the repository + has two files whose names clash for the client because + of case-insensitivity or similar causes, See 3 for + additional notes. (3) a special case of this is that a + file gets renamed for example from a.c to A.C. A + "cvs update" on a case-insensitive client will get this + error. In this case and in case 2, the filename + (short_pathname) printed in the error message will likely _not_ + have the same case as seen by the user in a directory listing. + (4) the client has a file which the server doesn't know + about (e.g. "? foo" file), and that name clashes with a file + the server does know about, (5) classify.c will print the same + message for other reasons. + + I hope the above paragraph makes it clear that making this + clearer is not a one-line fix. */ + error (0, 0, "move away %s; it is in the way", short_pathname); + if (updated_fname != NULL) + { + cvs_output ("C ", 0); + cvs_output (updated_fname, 0); + cvs_output ("\n", 1); + } + failure_exit = 1; + + discard_file_and_return: + /* Now read and discard the file contents. */ + usize = size; + nread = 0; + while (nread < usize) + { + toread = usize - nread; + if (toread > sizeof buf) + toread = sizeof buf; + + nread += try_read_from_server (buf, toread); + if (nread == usize) + break; + } + + free (mode_string); + free (scratch_entries); + free (entries_line); + + /* The Mode, Mod-time, and Checksum responses should not carry + over to a subsequent Created (or whatever) response, even + in the error case. */ + if (stored_mode != NULL) + { + free (stored_mode); + stored_mode = NULL; + } + stored_modtime_valid = 0; + stored_checksum_valid = 0; + + if (updated_fname != NULL) + { + free (updated_fname); + updated_fname = NULL; + } + return; + } + + temp_filename = xmalloc (strlen (filename) + 80); +#ifdef USE_VMS_FILENAMES + /* A VMS rename of "blah.dat" to "foo" to implies a + destination of "foo.dat" which is unfortinate for CVS */ + sprintf (temp_filename, "%s_new_", filename); +#else +#ifdef _POSIX_NO_TRUNC + sprintf (temp_filename, ".new.%.9s", filename); +#else /* _POSIX_NO_TRUNC */ + sprintf (temp_filename, ".new.%s", filename); +#endif /* _POSIX_NO_TRUNC */ +#endif /* USE_VMS_FILENAMES */ + + buf = xmalloc (size); + + /* Some systems, like OS/2 and Windows NT, end lines with CRLF + instead of just LF. Format translation is done in the C + library I/O funtions. Here we tell them whether or not to + convert -- if this file is marked "binary" with the RCS -kb + flag, then we don't want to convert, else we do (because + CVS assumes text files by default). */ + + if (options) + bin = !(strcmp (options, "-kb")); + else + bin = 0; + + if (data->contents == UPDATE_ENTRIES_RCS_DIFF) + { + /* This is an RCS change text. We just hold the change + text in memory. */ + + if (use_gzip) + error (1, 0, + "server error: gzip invalid with RCS change text"); + + read_from_server (buf, size); + } + else + { + int fd; + + fd = CVS_OPEN (temp_filename, + (O_WRONLY | O_CREAT | O_TRUNC + | (bin ? OPEN_BINARY : 0)), + 0777); + + if (fd < 0) + { + /* I can see a case for making this a fatal error; for + a condition like disk full or network unreachable + (for a file server), carrying on and giving an + error on each file seems unnecessary. But if it is + a permission problem, or some such, then it is + entirely possible that future files will not have + the same problem. */ + error (0, errno, "cannot write %s", short_pathname); + free (temp_filename); + free (buf); + goto discard_file_and_return; + } + + if (size > 0) + { + read_from_server (buf, size); + + if (use_gzip) + { + if (gunzip_and_write (fd, short_pathname, + (unsigned char *) buf, size)) + error (1, 0, "aborting due to compression error"); + } + else if (write (fd, buf, size) != size) + error (1, errno, "writing %s", short_pathname); + } + + if (close (fd) < 0) + error (1, errno, "writing %s", short_pathname); + } + + /* This is after we have read the file from the net (a change + from previous versions, where the server would send us + "M U foo.c" before Update-existing or whatever), but before + we finish writing the file (arguably a bug). The timing + affects a user who wants status info about how far we have + gotten, and also affects whether "U foo.c" appears in addition + to various error messages. */ + if (updated_fname != NULL) + { + cvs_output ("U ", 0); + cvs_output (updated_fname, 0); + cvs_output ("\n", 1); + free (updated_fname); + updated_fname = 0; + } + + patch_failed = 0; + + if (data->contents == UPDATE_ENTRIES_UPDATE) + { + rename_file (temp_filename, filename); + } + else if (data->contents == UPDATE_ENTRIES_PATCH) + { + /* You might think we could just leave Patched out of + Valid-responses and not get this response. However, if + memory serves, the CVS 1.9 server bases this on -u + (update-patches), and there is no way for us to send -u + or not based on whether the server supports "Rcs-diff". + + Fall back to transmitting entire files. */ + patch_failed = 1; + } + else + { + char *filebuf; + size_t filebufsize; + size_t nread; + char *patchedbuf; + size_t patchedlen; + + /* Handle UPDATE_ENTRIES_RCS_DIFF. */ + + if (!isfile (filename)) + error (1, 0, "patch original file %s does not exist", + short_pathname); + filebuf = NULL; + filebufsize = 0; + nread = 0; + + get_file (filename, short_pathname, bin ? FOPEN_BINARY_READ : "r", + &filebuf, &filebufsize, &nread); + /* At this point the contents of the existing file are in + FILEBUF, and the length of the contents is in NREAD. + The contents of the patch from the network are in BUF, + and the length of the patch is in SIZE. */ + + if (! rcs_change_text (short_pathname, filebuf, nread, buf, size, + &patchedbuf, &patchedlen)) + patch_failed = 1; + else + { + if (stored_checksum_valid) + { + struct cvs_MD5Context context; + unsigned char checksum[16]; + + /* We have a checksum. Check it before writing + the file out, so that we don't have to read it + back in again. */ + cvs_MD5Init (&context); + cvs_MD5Update (&context, + (unsigned char *) patchedbuf, patchedlen); + cvs_MD5Final (checksum, &context); + if (memcmp (checksum, stored_checksum, 16) != 0) + { + error (0, 0, + "checksum failure after patch to %s; will refetch", + short_pathname); + + patch_failed = 1; + } + + stored_checksum_valid = 0; + } + + if (! patch_failed) + { + FILE *e; + + e = open_file (temp_filename, + bin ? FOPEN_BINARY_WRITE : "w"); + if (fwrite (patchedbuf, 1, patchedlen, e) != patchedlen) + error (1, errno, "cannot write %s", temp_filename); + if (fclose (e) == EOF) + error (1, errno, "cannot close %s", temp_filename); + rename_file (temp_filename, filename); + } + + free (patchedbuf); + } + + free (filebuf); + } + + free (temp_filename); + + if (stored_checksum_valid && ! patch_failed) + { + FILE *e; + struct cvs_MD5Context context; + unsigned char buf[8192]; + unsigned len; + unsigned char checksum[16]; + + /* + * Compute the MD5 checksum. This will normally only be + * used when receiving a patch, so we always compute it + * here on the final file, rather than on the received + * data. + * + * Note that if the file is a text file, we should read it + * here using text mode, so its lines will be terminated the same + * way they were transmitted. + */ + e = CVS_FOPEN (filename, "r"); + if (e == NULL) + error (1, errno, "could not open %s", short_pathname); + + cvs_MD5Init (&context); + while ((len = fread (buf, 1, sizeof buf, e)) != 0) + cvs_MD5Update (&context, buf, len); + if (ferror (e)) + error (1, errno, "could not read %s", short_pathname); + cvs_MD5Final (checksum, &context); + + fclose (e); + + stored_checksum_valid = 0; + + if (memcmp (checksum, stored_checksum, 16) != 0) + { + if (data->contents != UPDATE_ENTRIES_PATCH) + error (1, 0, "checksum failure on %s", + short_pathname); + + error (0, 0, + "checksum failure after patch to %s; will refetch", + short_pathname); + + patch_failed = 1; + } + } + + if (patch_failed) + { + /* Save this file to retrieve later. */ + failed_patches = (char **) xrealloc ((char *) failed_patches, + ((failed_patches_count + 1) + * sizeof (char *))); + failed_patches[failed_patches_count] = xstrdup (short_pathname); + ++failed_patches_count; + + stored_checksum_valid = 0; + + free (mode_string); + free (buf); + free (scratch_entries); + free (entries_line); + + return; + } + + { + int status = change_mode (filename, mode_string, 1); + if (status != 0) + error (0, status, "cannot change mode of %s", short_pathname); + } + + free (mode_string); + free (buf); + } + + if (stored_mode != NULL) + { + change_mode (filename, stored_mode, 1); + free (stored_mode); + stored_mode = NULL; + } + + if (stored_modtime_valid) + { + struct utimbuf t; + + memset (&t, 0, sizeof (t)); + t.modtime = stored_modtime; + (void) time (&t.actime); + +#ifdef UTIME_EXPECTS_WRITABLE + if (!iswritable (filename)) + { + xchmod (filename, 1); + change_it_back = 1; + } +#endif /* UTIME_EXPECTS_WRITABLE */ + + if (utime (filename, &t) < 0) + error (0, errno, "cannot set time on %s", filename); + +#ifdef UTIME_EXPECTS_WRITABLE + if (change_it_back) + { + xchmod (filename, 0); + change_it_back = 0; + } +#endif /* UTIME_EXPECTS_WRITABLE */ + + stored_modtime_valid = 0; + } + + /* + * Process the entries line. Do this after we've written the file, + * since we need the timestamp. + */ + if (strcmp (cvs_cmd_name, "export") != 0) + { + char *local_timestamp; + char *file_timestamp; + + (void) time (&last_register_time); + + local_timestamp = data->timestamp; + if (local_timestamp == NULL || ts[0] == '+') + file_timestamp = time_stamp (filename); + else + file_timestamp = NULL; + + /* + * These special version numbers signify that it is not up to + * date. Create a dummy timestamp which will never compare + * equal to the timestamp of the file. + */ + if (vn[0] == '\0' || strcmp (vn, "0") == 0 || vn[0] == '-') + local_timestamp = "dummy timestamp"; + else if (local_timestamp == NULL) + { + local_timestamp = file_timestamp; + + /* Checking for cvs_cmd_name of "commit" doesn't seem like + the cleanest way to handle this, but it seem to roughly + parallel what the :local: code which calls + mark_up_to_date ends up amounting to. Some day, should + think more about what the Checked-in response means + vis-a-vis both Entries and Base and clarify + cvsclient.texi accordingly. */ + + if (!strcmp (cvs_cmd_name, "commit")) + mark_up_to_date (filename); + } + + Register (ent_list, filename, vn, local_timestamp, + options, tag, date, ts[0] == '+' ? file_timestamp : NULL); + + if (file_timestamp) + free (file_timestamp); + + } + free (scratch_entries); + free (entries_line); +} + +static void +handle_checked_in (args, len) + char *args; + int len; +{ + struct update_entries_data dat; + dat.contents = UPDATE_ENTRIES_CHECKIN; + dat.existp = UPDATE_ENTRIES_EXISTING_OR_NEW; + dat.timestamp = NULL; + call_in_directory (args, update_entries, (char *)&dat); +} + +static void +handle_new_entry (args, len) + char *args; + int len; +{ + struct update_entries_data dat; + dat.contents = UPDATE_ENTRIES_CHECKIN; + dat.existp = UPDATE_ENTRIES_EXISTING_OR_NEW; + dat.timestamp = "dummy timestamp from new-entry"; + call_in_directory (args, update_entries, (char *)&dat); +} + +static void +handle_updated (args, len) + char *args; + int len; +{ + struct update_entries_data dat; + dat.contents = UPDATE_ENTRIES_UPDATE; + dat.existp = UPDATE_ENTRIES_EXISTING_OR_NEW; + dat.timestamp = NULL; + call_in_directory (args, update_entries, (char *)&dat); +} + +static void handle_created PROTO((char *, int)); + +static void +handle_created (args, len) + char *args; + int len; +{ + struct update_entries_data dat; + dat.contents = UPDATE_ENTRIES_UPDATE; + dat.existp = UPDATE_ENTRIES_NEW; + dat.timestamp = NULL; + call_in_directory (args, update_entries, (char *)&dat); +} + +static void handle_update_existing PROTO((char *, int)); + +static void +handle_update_existing (args, len) + char *args; + int len; +{ + struct update_entries_data dat; + dat.contents = UPDATE_ENTRIES_UPDATE; + dat.existp = UPDATE_ENTRIES_EXISTING; + dat.timestamp = NULL; + call_in_directory (args, update_entries, (char *)&dat); +} + +static void +handle_merged (args, len) + char *args; + int len; +{ + struct update_entries_data dat; + dat.contents = UPDATE_ENTRIES_UPDATE; + /* Think this could be UPDATE_ENTRIES_EXISTING, but just in case... */ + dat.existp = UPDATE_ENTRIES_EXISTING_OR_NEW; + dat.timestamp = "Result of merge"; + call_in_directory (args, update_entries, (char *)&dat); +} + +static void +handle_patched (args, len) + char *args; + int len; +{ + struct update_entries_data dat; + dat.contents = UPDATE_ENTRIES_PATCH; + /* Think this could be UPDATE_ENTRIES_EXISTING, but just in case... */ + dat.existp = UPDATE_ENTRIES_EXISTING_OR_NEW; + dat.timestamp = NULL; + call_in_directory (args, update_entries, (char *)&dat); +} + +static void +handle_rcs_diff (args, len) + char *args; + int len; +{ + struct update_entries_data dat; + dat.contents = UPDATE_ENTRIES_RCS_DIFF; + /* Think this could be UPDATE_ENTRIES_EXISTING, but just in case... */ + dat.existp = UPDATE_ENTRIES_EXISTING_OR_NEW; + dat.timestamp = NULL; + call_in_directory (args, update_entries, (char *)&dat); +} + +static void +remove_entry (data, ent_list, short_pathname, filename) + char *data; + List *ent_list; + char *short_pathname; + char *filename; +{ + Scratch_Entry (ent_list, filename); +} + +static void +handle_remove_entry (args, len) + char *args; + int len; +{ + call_in_directory (args, remove_entry, (char *)NULL); +} + +static void +remove_entry_and_file (data, ent_list, short_pathname, filename) + char *data; + List *ent_list; + char *short_pathname; + char *filename; +{ + Scratch_Entry (ent_list, filename); + /* Note that we don't ignore existence_error's here. The server + should be sending Remove-entry rather than Removed in cases + where the file does not exist. And if the user removes the + file halfway through a cvs command, we should be printing an + error. */ + if (unlink_file (filename) < 0) + error (0, errno, "unable to remove %s", short_pathname); +} + +static void +handle_removed (args, len) + char *args; + int len; +{ + call_in_directory (args, remove_entry_and_file, (char *)NULL); +} + +/* Is this the top level (directory containing CVSROOT)? */ +static int +is_cvsroot_level (pathname) + char *pathname; +{ + if (strcmp (toplevel_repos, current_parsed_root->directory) != 0) + return 0; + + return strchr (pathname, '/') == NULL; +} + +static void +set_static (data, ent_list, short_pathname, filename) + char *data; + List *ent_list; + char *short_pathname; + char *filename; +{ + FILE *fp; + fp = open_file (CVSADM_ENTSTAT, "w+"); + if (fclose (fp) == EOF) + error (1, errno, "cannot close %s", CVSADM_ENTSTAT); +} + +static void +handle_set_static_directory (args, len) + char *args; + int len; +{ + if (strcmp (cvs_cmd_name, "export") == 0) + { + /* Swallow the repository. */ + read_line (NULL); + return; + } + call_in_directory (args, set_static, (char *)NULL); +} + +static void +clear_static (data, ent_list, short_pathname, filename) + char *data; + List *ent_list; + char *short_pathname; + char *filename; +{ + if (unlink_file (CVSADM_ENTSTAT) < 0 && ! existence_error (errno)) + error (1, errno, "cannot remove file %s", CVSADM_ENTSTAT); +} + +static void +handle_clear_static_directory (pathname, len) + char *pathname; + int len; +{ + if (strcmp (cvs_cmd_name, "export") == 0) + { + /* Swallow the repository. */ + read_line (NULL); + return; + } + + if (is_cvsroot_level (pathname)) + { + /* + * Top level (directory containing CVSROOT). This seems to normally + * lack a CVS directory, so don't try to create files in it. + */ + return; + } + call_in_directory (pathname, clear_static, (char *)NULL); +} + +static void +set_sticky (data, ent_list, short_pathname, filename) + char *data; + List *ent_list; + char *short_pathname; + char *filename; +{ + char *tagspec; + FILE *f; + + read_line (&tagspec); + + /* FIXME-update-dir: error messages should include the directory. */ + f = CVS_FOPEN (CVSADM_TAG, "w+"); + if (f == NULL) + { + /* Making this non-fatal is a bit of a kludge (see dirs2 + in testsuite). A better solution would be to avoid having + the server tell us about a directory we shouldn't be doing + anything with anyway (e.g. by handling directory + addition/removal better). */ + error (0, errno, "cannot open %s", CVSADM_TAG); + free (tagspec); + return; + } + if (fprintf (f, "%s\n", tagspec) < 0) + error (1, errno, "writing %s", CVSADM_TAG); + if (fclose (f) == EOF) + error (1, errno, "closing %s", CVSADM_TAG); + free (tagspec); +} + +static void +handle_set_sticky (pathname, len) + char *pathname; + int len; +{ + if (strcmp (cvs_cmd_name, "export") == 0) + { + /* Swallow the repository. */ + read_line (NULL); + /* Swallow the tag line. */ + read_line (NULL); + return; + } + if (is_cvsroot_level (pathname)) + { + /* + * Top level (directory containing CVSROOT). This seems to normally + * lack a CVS directory, so don't try to create files in it. + */ + + /* Swallow the repository. */ + read_line (NULL); + /* Swallow the tag line. */ + read_line (NULL); + return; + } + + call_in_directory (pathname, set_sticky, (char *)NULL); +} + +static void +clear_sticky (data, ent_list, short_pathname, filename) + char *data; + List *ent_list; + char *short_pathname; + char *filename; +{ + if (unlink_file (CVSADM_TAG) < 0 && ! existence_error (errno)) + error (1, errno, "cannot remove %s", CVSADM_TAG); +} + +static void +handle_clear_sticky (pathname, len) + char *pathname; + int len; +{ + if (strcmp (cvs_cmd_name, "export") == 0) + { + /* Swallow the repository. */ + read_line (NULL); + return; + } + + if (is_cvsroot_level (pathname)) + { + /* + * Top level (directory containing CVSROOT). This seems to normally + * lack a CVS directory, so don't try to create files in it. + */ + return; + } + + call_in_directory (pathname, clear_sticky, (char *)NULL); +} + + +static void template PROTO ((char *, List *, char *, char *)); + +static void +template (data, ent_list, short_pathname, filename) + char *data; + List *ent_list; + char *short_pathname; + char *filename; +{ + char *buf = xmalloc ( strlen ( short_pathname ) + + strlen ( CVSADM_TEMPLATE ) + + 2 ); + sprintf ( buf, "%s/%s", short_pathname, CVSADM_TEMPLATE ); + read_counted_file ( CVSADM_TEMPLATE, buf ); + free ( buf ); +} + +static void handle_template PROTO ((char *, int)); + +static void +handle_template (pathname, len) + char *pathname; + int len; +{ + call_in_directory (pathname, template, NULL); +} + + + +struct save_dir { + char *dir; + struct save_dir *next; +}; + +struct save_dir *prune_candidates; + +static void +add_prune_candidate (dir) + const char *dir; +{ + struct save_dir *p; + + if ((dir[0] == '.' && dir[1] == '\0') + || (prune_candidates != NULL + && strcmp (dir, prune_candidates->dir) == 0)) + return; + p = (struct save_dir *) xmalloc (sizeof (struct save_dir)); + p->dir = xstrdup (dir); + p->next = prune_candidates; + prune_candidates = p; +} + +static void process_prune_candidates PROTO((void)); + +static void +process_prune_candidates () +{ + struct save_dir *p; + struct save_dir *q; + + if (toplevel_wd != NULL) + { + if (CVS_CHDIR (toplevel_wd) < 0) + error (1, errno, "could not chdir to %s", toplevel_wd); + } + for (p = prune_candidates; p != NULL; ) + { + if (isemptydir (p->dir, 1)) + { + char *b; + + if (unlink_file_dir (p->dir) < 0) + error (0, errno, "cannot remove %s", p->dir); + b = strrchr (p->dir, '/'); + if (b == NULL) + Subdir_Deregister ((List *) NULL, (char *) NULL, p->dir); + else + { + *b = '\0'; + Subdir_Deregister ((List *) NULL, p->dir, b + 1); + } + } + free (p->dir); + q = p->next; + free (p); + p = q; + } + prune_candidates = NULL; +} + +/* Send a Repository line. */ + +static char *last_repos; +static char *last_update_dir; + +static void send_repository PROTO((const char *, const char *, const char *)); + +static void +send_repository (dir, repos, update_dir) + const char *dir; + const char *repos; + const char *update_dir; +{ + char *adm_name; + + /* FIXME: this is probably not the best place to check; I wish I + * knew where in here's callers to really trap this bug. To + * reproduce the bug, just do this: + * + * mkdir junk + * cd junk + * cvs -d some_repos update foo + * + * Poof, CVS seg faults and dies! It's because it's trying to + * send a NULL string to the server but dies in send_to_server. + * That string was supposed to be the repository, but it doesn't + * get set because there's no CVSADM dir, and somehow it's not + * getting set from the -d argument either... ? + */ + if (repos == NULL) + { + /* Lame error. I want a real fix but can't stay up to track + this down right now. */ + error (1, 0, "no repository"); + } + + if (update_dir == NULL || update_dir[0] == '\0') + update_dir = "."; + + if (last_repos != NULL + && strcmp (repos, last_repos) == 0 + && last_update_dir != NULL + && strcmp (update_dir, last_update_dir) == 0) + /* We've already sent it. */ + return; + + if (client_prune_dirs) + add_prune_candidate (update_dir); + + /* Add a directory name to the list of those sent to the + server. */ + if (update_dir && (*update_dir != '\0') + && (strcmp (update_dir, ".") != 0) + && (findnode (dirs_sent_to_server, update_dir) == NULL)) + { + Node *n; + n = getnode (); + n->type = NT_UNKNOWN; + n->key = xstrdup (update_dir); + n->data = NULL; + + if (addnode (dirs_sent_to_server, n)) + error (1, 0, "cannot add directory %s to list", n->key); + } + + /* 80 is large enough for any of CVSADM_*. */ + adm_name = xmalloc (strlen (dir) + 80); + + send_to_server ("Directory ", 0); + { + /* Send the directory name. I know that this + sort of duplicates code elsewhere, but each + case seems slightly different... */ + char buf[1]; + const char *p = update_dir; + while (*p != '\0') + { + assert (*p != '\012'); + if (ISDIRSEP (*p)) + { + buf[0] = '/'; + send_to_server (buf, 1); + } + else + { + buf[0] = *p; + send_to_server (buf, 1); + } + ++p; + } + } + send_to_server ("\012", 1); + send_to_server (repos, 0); + send_to_server ("\012", 1); + + if (strcmp (cvs_cmd_name, "import") + && supported_request ("Static-directory")) + { + adm_name[0] = '\0'; + if (dir[0] != '\0') + { + strcat (adm_name, dir); + strcat (adm_name, "/"); + } + strcat (adm_name, CVSADM_ENTSTAT); + if (isreadable (adm_name)) + { + send_to_server ("Static-directory\012", 0); + } + } + if (strcmp (cvs_cmd_name, "import") + && supported_request ("Sticky")) + { + FILE *f; + if (dir[0] == '\0') + strcpy (adm_name, CVSADM_TAG); + else + sprintf (adm_name, "%s/%s", dir, CVSADM_TAG); + + f = CVS_FOPEN (adm_name, "r"); + if (f == NULL) + { + if (! existence_error (errno)) + error (1, errno, "reading %s", adm_name); + } + else + { + char line[80]; + char *nl = NULL; + send_to_server ("Sticky ", 0); + while (fgets (line, sizeof (line), f) != NULL) + { + send_to_server (line, 0); + nl = strchr (line, '\n'); + if (nl != NULL) + break; + } + if (nl == NULL) + send_to_server ("\012", 1); + if (fclose (f) == EOF) + error (0, errno, "closing %s", adm_name); + } + } + free (adm_name); + if (last_repos != NULL) + free (last_repos); + if (last_update_dir != NULL) + free (last_update_dir); + last_repos = xstrdup (repos); + last_update_dir = xstrdup (update_dir); +} + +/* Send a Repository line and set toplevel_repos. */ + +void +send_a_repository (dir, repository, update_dir_in) + const char *dir; + const char *repository; + const char *update_dir_in; +{ + char *update_dir; + + assert (update_dir_in); + update_dir = xstrdup (update_dir_in); + + if (toplevel_repos == NULL && repository != NULL) + { + if (update_dir[0] == '\0' + || (update_dir[0] == '.' && update_dir[1] == '\0')) + toplevel_repos = xstrdup (repository); + else + { + /* + * Get the repository from a CVS/Repository file if update_dir + * is absolute. This is not correct in general, because + * the CVS/Repository file might not be the top-level one. + * This is for cases like "cvs update /foo/bar" (I'm not + * sure it matters what toplevel_repos we get, but it does + * matter that we don't hit the "internal error" code below). + */ + if (update_dir[0] == '/') + toplevel_repos = Name_Repository (update_dir, update_dir); + else + { + /* + * Guess the repository of that directory by looking at a + * subdirectory and removing as many pathname components + * as are in update_dir. I think that will always (or at + * least almost always) be 1. + * + * So this deals with directories which have been + * renamed, though it doesn't necessarily deal with + * directories which have been put inside other + * directories (and cvs invoked on the containing + * directory). I'm not sure the latter case needs to + * work. + * + * 21 Aug 1998: Well, Mr. Above-Comment-Writer, it + * does need to work after all. When we are using the + * client in a multi-cvsroot environment, it will be + * fairly common that we have the above case (e.g., + * cwd checked out from one repository but + * subdirectory checked out from another). We can't + * assume that by walking up a directory in our wd we + * necessarily walk up a directory in the repository. + */ + /* + * This gets toplevel_repos wrong for "cvs update ../foo" + * but I'm not sure toplevel_repos matters in that case. + */ + + int repository_len, update_dir_len; + + strip_trailing_slashes (update_dir); + + repository_len = strlen (repository); + update_dir_len = strlen (update_dir); + + /* Try to remove the path components in UPDATE_DIR + from REPOSITORY. If the path elements don't exist + in REPOSITORY, or the removal of those path + elements mean that we "step above" + current_parsed_root->directory, set toplevel_repos to + current_parsed_root->directory. */ + if ((repository_len > update_dir_len) + && (strcmp (repository + repository_len - update_dir_len, + update_dir) == 0) + /* TOPLEVEL_REPOS shouldn't be above current_parsed_root->directory */ + && ((size_t)(repository_len - update_dir_len) + > strlen (current_parsed_root->directory))) + { + /* The repository name contains UPDATE_DIR. Set + toplevel_repos to the repository name without + UPDATE_DIR. */ + + toplevel_repos = xmalloc (repository_len - update_dir_len); + /* Note that we don't copy the trailing '/'. */ + strncpy (toplevel_repos, repository, + repository_len - update_dir_len - 1); + toplevel_repos[repository_len - update_dir_len - 1] = '\0'; + } + else + { + toplevel_repos = xstrdup (current_parsed_root->directory); + } + } + } + } + + send_repository (dir, repository, update_dir); + free (update_dir); +} + + + +/* The "expanded" modules. */ +static int modules_count; +static int modules_allocated; +static char **modules_vector; + +static void +handle_module_expansion (args, len) + char *args; + int len; +{ + if (modules_vector == NULL) + { + modules_allocated = 1; /* Small for testing */ + modules_vector = (char **) xmalloc + (modules_allocated * sizeof (modules_vector[0])); + } + else if (modules_count >= modules_allocated) + { + modules_allocated *= 2; + modules_vector = (char **) xrealloc + ((char *) modules_vector, + modules_allocated * sizeof (modules_vector[0])); + } + modules_vector[modules_count] = xmalloc (strlen (args) + 1); + strcpy (modules_vector[modules_count], args); + ++modules_count; +} + +/* Original, not "expanded" modules. */ +static int module_argc; +static char **module_argv; + +void +client_expand_modules (argc, argv, local) + int argc; + char **argv; + int local; +{ + int errs; + int i; + + module_argc = argc; + module_argv = (char **) xmalloc ((argc + 1) * sizeof (module_argv[0])); + for (i = 0; i < argc; ++i) + module_argv[i] = xstrdup (argv[i]); + module_argv[argc] = NULL; + + for (i = 0; i < argc; ++i) + send_arg (argv[i]); + send_a_repository ("", current_parsed_root->directory, ""); + + send_to_server ("expand-modules\012", 0); + + errs = get_server_responses (); + if (last_repos != NULL) + free (last_repos); + last_repos = NULL; + if (last_update_dir != NULL) + free (last_update_dir); + last_update_dir = NULL; + if (errs) + error (errs, 0, "cannot expand modules"); +} + +void +client_send_expansions (local, where, build_dirs) + int local; + char *where; + int build_dirs; +{ + int i; + char *argv[1]; + + /* Send the original module names. The "expanded" module name might + not be suitable as an argument to a co request (e.g. it might be + the result of a -d argument in the modules file). It might be + cleaner if we genuinely expanded module names, all the way to a + local directory and repository, but that isn't the way it works + now. */ + send_file_names (module_argc, module_argv, 0); + + for (i = 0; i < modules_count; ++i) + { + argv[0] = where ? where : modules_vector[i]; + if (isfile (argv[0])) + send_files (1, argv, local, 0, build_dirs ? SEND_BUILD_DIRS : 0); + } + send_a_repository ("", current_parsed_root->directory, ""); +} + +void +client_nonexpanded_setup () +{ + send_a_repository ("", current_parsed_root->directory, ""); +} + +/* Receive a cvswrappers line from the server; it must be a line + containing an RCS option (e.g., "*.exe -k 'b'"). + + Note that this doesn't try to handle -t/-f options (which are a + whole separate issue which noone has thought much about, as far + as I know). + + We need to know the keyword expansion mode so we know whether to + read the file in text or binary mode. */ + +static void +handle_wrapper_rcs_option (args, len) + char *args; + int len; +{ + char *p; + + /* Enforce the notes in cvsclient.texi about how the response is not + as free-form as it looks. */ + p = strchr (args, ' '); + if (p == NULL) + goto handle_error; + if (*++p != '-' + || *++p != 'k' + || *++p != ' ' + || *++p != '\'') + goto handle_error; + if (strchr (p, '\'') == NULL) + goto handle_error; + + /* Add server-side cvswrappers line to our wrapper list. */ + wrap_add (args, 0); + return; + handle_error: + error (0, errno, "protocol error: ignoring invalid wrappers %s", args); +} + + +static void +handle_m (args, len) + char *args; + int len; +{ + /* In the case where stdout and stderr point to the same place, + fflushing stderr will make output happen in the correct order. + Often stderr will be line-buffered and this won't be needed, + but not always (is that true? I think the comment is probably + based on being confused between default buffering between + stdout and stderr. But I'm not sure). */ + fflush (stderr); + fwrite (args, len, sizeof (*args), stdout); + putc ('\n', stdout); +} + +static void handle_mbinary PROTO ((char *, int)); + +static void +handle_mbinary (args, len) + char *args; + int len; +{ + char *size_string; + size_t size; + size_t totalread; + size_t nread; + size_t toread; + char buf[8192]; + + /* See comment at handle_m about (non)flush of stderr. */ + + /* Get the size. */ + read_line (&size_string); + size = strto_file_size (size_string); + free (size_string); + + /* OK, now get all the data. The algorithm here is that we read + as much as the network wants to give us in + try_read_from_server, and then we output it all, and then + repeat, until we get all the data. */ + totalread = 0; + while (totalread < size) + { + toread = size - totalread; + if (toread > sizeof buf) + toread = sizeof buf; + + nread = try_read_from_server (buf, toread); + cvs_output_binary (buf, nread); + totalread += nread; + } +} + +static void +handle_e (args, len) + char *args; + int len; +{ + /* In the case where stdout and stderr point to the same place, + fflushing stdout will make output happen in the correct order. */ + fflush (stdout); + fwrite (args, len, sizeof (*args), stderr); + putc ('\n', stderr); +} + +/*ARGSUSED*/ +static void +handle_f (args, len) + char *args; + int len; +{ + fflush (stderr); +} + +static void handle_mt PROTO ((char *, int)); + +static void +handle_mt (args, len) + char *args; + int len; +{ + char *p; + char *tag = args; + char *text; + + /* See comment at handle_m for more details. */ + fflush (stderr); + + p = strchr (args, ' '); + if (p == NULL) + text = NULL; + else + { + *p++ = '\0'; + text = p; + } + + switch (tag[0]) + { + case '+': + if (strcmp (tag, "+updated") == 0) + updated_seen = 1; + else if (strcmp (tag, "+importmergecmd") == 0) + importmergecmd.seen = 1; + break; + case '-': + if (strcmp (tag, "-updated") == 0) + updated_seen = 0; + else if (strcmp (tag, "-importmergecmd") == 0) + { + char buf[80]; + + /* Now that we have gathered the information, we can + output the suggested merge command. */ + + if (importmergecmd.conflicts == 0 + || importmergecmd.mergetag1 == NULL + || importmergecmd.mergetag2 == NULL + || importmergecmd.repository == NULL) + { + error (0, 0, + "invalid server: incomplete importmergecmd tags"); + break; + } + + sprintf (buf, "\n%d conflicts created by this import.\n", + importmergecmd.conflicts); + cvs_output (buf, 0); + cvs_output ("Use the following command to help the merge:\n\n", + 0); + cvs_output ("\t", 1); + cvs_output (program_name, 0); + if (CVSroot_cmdline != NULL) + { + cvs_output (" -d ", 0); + cvs_output (CVSroot_cmdline, 0); + } + cvs_output (" checkout -j", 0); + cvs_output (importmergecmd.mergetag1, 0); + cvs_output (" -j", 0); + cvs_output (importmergecmd.mergetag2, 0); + cvs_output (" ", 1); + cvs_output (importmergecmd.repository, 0); + cvs_output ("\n\n", 0); + + /* Clear the static variables so that everything is + ready for any subsequent importmergecmd tag. */ + importmergecmd.conflicts = 0; + free (importmergecmd.mergetag1); + importmergecmd.mergetag1 = NULL; + free (importmergecmd.mergetag2); + importmergecmd.mergetag2 = NULL; + free (importmergecmd.repository); + importmergecmd.repository = NULL; + + importmergecmd.seen = 0; + } + break; + default: + if (updated_seen) + { + if (strcmp (tag, "fname") == 0) + { + if (updated_fname != NULL) + { + /* Output the previous message now. This can happen + if there was no Update-existing or other such + response, due to the -n global option. */ + cvs_output ("U ", 0); + cvs_output (updated_fname, 0); + cvs_output ("\n", 1); + free (updated_fname); + } + updated_fname = xstrdup (text); + } + /* Swallow all other tags. Either they are extraneous + or they reflect future extensions that we can + safely ignore. */ + } + else if (importmergecmd.seen) + { + if (strcmp (tag, "conflicts") == 0) + importmergecmd.conflicts = text ? atoi (text) : -1; + else if (strcmp (tag, "mergetag1") == 0) + importmergecmd.mergetag1 = xstrdup (text); + else if (strcmp (tag, "mergetag2") == 0) + importmergecmd.mergetag2 = xstrdup (text); + else if (strcmp (tag, "repository") == 0) + importmergecmd.repository = xstrdup (text); + /* Swallow all other tags. Either they are text for + which we are going to print our own version when we + see -importmergecmd, or they are future extensions + we can safely ignore. */ + } + else if (strcmp (tag, "newline") == 0) + printf ("\n"); + else if (text != NULL) + printf ("%s", text); + } +} + +#endif /* CLIENT_SUPPORT */ +#if defined(CLIENT_SUPPORT) || defined(SERVER_SUPPORT) + +/* This table must be writeable if the server code is included. */ +struct response responses[] = +{ +#ifdef CLIENT_SUPPORT +#define RSP_LINE(n, f, t, s) {n, f, t, s} +#else /* ! CLIENT_SUPPORT */ +#define RSP_LINE(n, f, t, s) {n, s} +#endif /* CLIENT_SUPPORT */ + + RSP_LINE("ok", handle_ok, response_type_ok, rs_essential), + RSP_LINE("error", handle_error, response_type_error, rs_essential), + RSP_LINE("Valid-requests", handle_valid_requests, response_type_normal, + rs_essential), + RSP_LINE("Checked-in", handle_checked_in, response_type_normal, + rs_essential), + RSP_LINE("New-entry", handle_new_entry, response_type_normal, rs_optional), + RSP_LINE("Checksum", handle_checksum, response_type_normal, rs_optional), + RSP_LINE("Copy-file", handle_copy_file, response_type_normal, rs_optional), + RSP_LINE("Updated", handle_updated, response_type_normal, rs_essential), + RSP_LINE("Created", handle_created, response_type_normal, rs_optional), + RSP_LINE("Update-existing", handle_update_existing, response_type_normal, + rs_optional), + RSP_LINE("Merged", handle_merged, response_type_normal, rs_essential), + RSP_LINE("Patched", handle_patched, response_type_normal, rs_optional), + RSP_LINE("Rcs-diff", handle_rcs_diff, response_type_normal, rs_optional), + RSP_LINE("Mode", handle_mode, response_type_normal, rs_optional), + RSP_LINE("Mod-time", handle_mod_time, response_type_normal, rs_optional), + RSP_LINE("Removed", handle_removed, response_type_normal, rs_essential), + RSP_LINE("Remove-entry", handle_remove_entry, response_type_normal, + rs_optional), + RSP_LINE("Set-static-directory", handle_set_static_directory, + response_type_normal, + rs_optional), + RSP_LINE("Clear-static-directory", handle_clear_static_directory, + response_type_normal, + rs_optional), + RSP_LINE("Set-sticky", handle_set_sticky, response_type_normal, + rs_optional), + RSP_LINE("Clear-sticky", handle_clear_sticky, response_type_normal, + rs_optional), + RSP_LINE("Template", handle_template, response_type_normal, + rs_optional), + RSP_LINE("Notified", handle_notified, response_type_normal, rs_optional), + RSP_LINE("Module-expansion", handle_module_expansion, response_type_normal, + rs_optional), + RSP_LINE("Wrapper-rcsOption", handle_wrapper_rcs_option, + response_type_normal, + rs_optional), + RSP_LINE("M", handle_m, response_type_normal, rs_essential), + RSP_LINE("Mbinary", handle_mbinary, response_type_normal, rs_optional), + RSP_LINE("E", handle_e, response_type_normal, rs_essential), + RSP_LINE("F", handle_f, response_type_normal, rs_optional), + RSP_LINE("MT", handle_mt, response_type_normal, rs_optional), + /* Possibly should be response_type_error. */ + RSP_LINE(NULL, NULL, response_type_normal, rs_essential) + +#undef RSP_LINE +}; + +#endif /* CLIENT_SUPPORT or SERVER_SUPPORT */ +#ifdef CLIENT_SUPPORT + +/* + * If LEN is 0, then send_to_server() computes string's length itself. + * + * Therefore, pass the real length when transmitting data that might + * contain 0's. + */ +void +send_to_server (str, len) + const char *str; + size_t len; +{ + static int nbytes; + + if (len == 0) + len = strlen (str); + + buf_output (to_server, str, len); + + /* There is no reason not to send data to the server, so do it + whenever we've accumulated enough information in the buffer to + make it worth sending. */ + nbytes += len; + if (nbytes >= 2 * BUFFER_DATA_SIZE) + { + int status; + + status = buf_send_output (to_server); + if (status != 0) + error (1, status, "error writing to server"); + nbytes = 0; + } +} + +/* Read up to LEN bytes from the server. Returns actual number of + bytes read, which will always be at least one; blocks if there is + no data available at all. Gives a fatal error on EOF or error. */ +static size_t +try_read_from_server (buf, len) + char *buf; + size_t len; +{ + int status, nread; + char *data; + + status = buf_read_data (from_server, len, &data, &nread); + if (status != 0) + { + if (status == -1) + error (1, 0, + "end of file from server (consult above messages if any)"); + else if (status == -2) + error (1, 0, "out of memory"); + else + error (1, status, "reading from server"); + } + + memcpy (buf, data, nread); + + return nread; +} + +/* + * Read LEN bytes from the server or die trying. + */ +void +read_from_server (buf, len) + char *buf; + size_t len; +{ + size_t red = 0; + while (red < len) + { + red += try_read_from_server (buf + red, len - red); + if (red == len) + break; + } +} + +/* + * Get some server responses and process them. Returns nonzero for + * error, 0 for success. */ +int +get_server_responses () +{ + struct response *rs; + do + { + char *cmd; + int len; + + len = read_line (&cmd); + for (rs = responses; rs->name != NULL; ++rs) + if (strncmp (cmd, rs->name, strlen (rs->name)) == 0) + { + int cmdlen = strlen (rs->name); + if (cmd[cmdlen] == '\0') + ; + else if (cmd[cmdlen] == ' ') + ++cmdlen; + else + /* + * The first len characters match, but it's a different + * response. e.g. the response is "oklahoma" but we + * matched "ok". + */ + continue; + (*rs->func) (cmd + cmdlen, len - cmdlen); + break; + } + if (rs->name == NULL) + /* It's OK to print just to the first '\0'. */ + /* We might want to handle control characters and the like + in some other way other than just sending them to stdout. + One common reason for this error is if people use :ext: + with a version of rsh which is doing CRLF translation or + something, and so the client gets "ok^M" instead of "ok". + Right now that will tend to print part of this error + message over the other part of it. It seems like we could + do better (either in general, by quoting or omitting all + control characters, and/or specifically, by detecting the CRLF + case and printing a specific error message). */ + error (0, 0, + "warning: unrecognized response `%s' from cvs server", + cmd); + free (cmd); + } while (rs->type == response_type_normal); + + if (updated_fname != NULL) + { + /* Output the previous message now. This can happen + if there was no Update-existing or other such + response, due to the -n global option. */ + cvs_output ("U ", 0); + cvs_output (updated_fname, 0); + cvs_output ("\n", 1); + free (updated_fname); + updated_fname = NULL; + } + + if (rs->type == response_type_error) + return 1; + if (failure_exit) + return 1; + return 0; +} + + + +/* Get the responses and then close the connection. */ + +/* + * Flag var; we'll set it in start_server() and not one of its + * callees, such as start_rsh_server(). This means that there might + * be a small window between the starting of the server and the + * setting of this var, but all the code in that window shouldn't care + * because it's busy checking return values to see if the server got + * started successfully anyway. + */ +int server_started = 0; + +int +get_responses_and_close () +{ + int errs = get_server_responses (); + int status; + + /* The following is necessary when working with multiple cvsroots, at least + * with commit. It used to be buried nicely in do_deferred_progs() before + * that function was removed. I suspect it wouldn't be necessary if + * call_in_directory() saved its working directory via save_cwd() before + * changing its directory and restored the saved working directory via + * restore_cwd() before exiting. Of course, calling CVS_CHDIR only once, + * here, may be more efficient. + */ + if( toplevel_wd != NULL ) + { + if( CVS_CHDIR( toplevel_wd ) < 0 ) + error( 1, errno, "could not chdir to %s", toplevel_wd ); + } + + if (client_prune_dirs) + process_prune_candidates (); + + /* First we shut down TO_SERVER. That tells the server that its input is + * finished. It then shuts down the buffer it is sending to us, at which + * point our shut down of FROM_SERVER will complete. + */ + + status = buf_shutdown (to_server); + if (status != 0) + error (0, status, "shutting down buffer to server"); + buf_free (to_server); + to_server = NULL; + + status = buf_shutdown (from_server); + if (status != 0) + error (0, status, "shutting down buffer from server"); + buf_free (from_server); + from_server = NULL; + server_started = 0; + + /* see if we need to sleep before returning to avoid time-stamp races */ + if (last_register_time) + { + sleep_past (last_register_time); + } + + return errs; +} + +#ifndef NO_EXT_METHOD +static void start_rsh_server PROTO((cvsroot_t *, struct buffer **, struct buffer **)); +#endif + +int +supported_request (name) + char *name; +{ + struct request *rq; + + for (rq = requests; rq->name; rq++) + if (!strcmp (rq->name, name)) + return (rq->flags & RQ_SUPPORTED) != 0; + error (1, 0, "internal error: testing support for unknown option?"); + /* NOTREACHED */ + return 0; +} + + + +#if defined (AUTH_CLIENT_SUPPORT) || defined (HAVE_KERBEROS) || defined (HAVE_GSSAPI) +static struct hostent *init_sockaddr PROTO ((struct sockaddr_in *, char *, + unsigned int)); + +static struct hostent * +init_sockaddr (name, hostname, port) + struct sockaddr_in *name; + char *hostname; + unsigned int port; +{ + struct hostent *hostinfo; + unsigned short shortport = port; + + memset (name, 0, sizeof (*name)); + name->sin_family = AF_INET; + name->sin_port = htons (shortport); + hostinfo = gethostbyname (hostname); + if (hostinfo == NULL) + { + fprintf (stderr, "Unknown host %s.\n", hostname); + error_exit (); + } + name->sin_addr = *(struct in_addr *) hostinfo->h_addr; + return hostinfo; +} + + + +/* Generic function to do port number lookup tasks. + * + * In order of precedence, will return: + * getenv (envname), if defined + * getservbyname (portname), if defined + * defaultport + */ +static int +get_port_number (envname, portname, defaultport) + const char *envname; + const char *portname; + int defaultport; +{ + struct servent *s; + char *port_s; + + if (envname && (port_s = getenv (envname))) + { + int port = atoi (port_s); + if (port <= 0) + { + error (0, 0, "%s must be a positive integer! If you", envname); + error (0, 0, "are trying to force a connection via rsh, please"); + error (0, 0, "put \":server:\" at the beginning of your CVSROOT"); + error (1, 0, "variable."); + } + return port; + } + else if (portname && (s = getservbyname (portname, "tcp"))) + return ntohs (s->s_port); + else + return defaultport; +} + + + +/* get the port number for a client to connect to based on the port + * and method of a cvsroot_t. + * + * we do this here instead of in parse_cvsroot so that we can keep network + * code confined to a localized area and also to delay the lookup until the + * last possible moment so it remains possible to run cvs client commands that + * skip opening connections to the server (i.e. skip network operations + * entirely) + * + * and yes, I know none of the commands do that now, but here's to planning + * for the future, eh? cheers. + * + * FIXME - We could cache the port lookup safely right now as we never change + * it for a single root on the fly, but we'd have to un'const some other + * functions - REMOVE_FIXME? This may be unecessary. We're talking about, + * what, usually one, sometimes two lookups of the port per invocation. I + * think twice is by far the rarer of the two cases - only the login function + * will need to do it to save the canonical CVSROOT. -DRP + */ +int +get_cvs_port_number (root) + const cvsroot_t *root; +{ + + if (root->port) return root->port; + + switch (root->method) + { +# ifdef HAVE_GSSAPI + case gserver_method: +# endif /* HAVE_GSSAPI */ +# ifdef AUTH_CLIENT_SUPPORT + case pserver_method: +# endif /* AUTH_CLIENT_SUPPORT */ +# if defined (AUTH_CLIENT_SUPPORT) || defined (HAVE_GSSAPI) + return get_port_number ("CVS_CLIENT_PORT", "cvspserver", CVS_AUTH_PORT); +# endif /* defined (AUTH_CLIENT_SUPPORT) || defined (HAVE_GSSAPI) */ +# ifdef HAVE_KERBEROS + case kserver_method: + return get_port_number ("CVS_CLIENT_PORT", "cvs", CVS_PORT); +# endif /* HAVE_KERBEROS */ + default: + error(1, EINVAL, "internal error: get_cvs_port_number called for invalid connection method (%s)", + method_names[root->method]); + break; + } + /* NOTREACHED */ + return -1; +} + + + +void +make_bufs_from_fds (tofd, fromfd, child_pid, to_server, from_server, is_sock) + int tofd; + int fromfd; + int child_pid; + struct buffer **to_server; + struct buffer **from_server; + int is_sock; +{ + FILE *to_server_fp; + FILE *from_server_fp; + +# ifdef NO_SOCKET_TO_FD + if (is_sock) + { + assert (tofd == fromfd); + *to_server = socket_buffer_initialize (tofd, 0, + (BUFMEMERRPROC) NULL); + *from_server = socket_buffer_initialize (tofd, 1, + (BUFMEMERRPROC) NULL); + } + else +# endif /* NO_SOCKET_TO_FD */ + { + /* todo: some OS's don't need these calls... */ + close_on_exec (tofd); + close_on_exec (fromfd); + + /* SCO 3 and AIX have a nasty bug in the I/O libraries which precludes + fdopening the same file descriptor twice, so dup it if it is the + same. */ + if (tofd == fromfd) + { + fromfd = dup (tofd); + if (fromfd < 0) + error (1, errno, "cannot dup net connection"); + } + + /* These will use binary mode on systems which have it. */ + /* + * Also, we know that from_server is shut down second, so we pass + * child_pid in there. In theory, it should be stored in both + * buffers with a ref count... + */ + to_server_fp = fdopen (tofd, FOPEN_BINARY_WRITE); + if (to_server_fp == NULL) + error (1, errno, "cannot fdopen %d for write", tofd); + *to_server = stdio_buffer_initialize (to_server_fp, 0, 0, + (BUFMEMERRPROC) NULL); + + from_server_fp = fdopen (fromfd, FOPEN_BINARY_READ); + if (from_server_fp == NULL) + error (1, errno, "cannot fdopen %d for read", fromfd); + *from_server = stdio_buffer_initialize (from_server_fp, child_pid, 1, + (BUFMEMERRPROC) NULL); + } +} +#endif /* defined (AUTH_CLIENT_SUPPORT) || defined (HAVE_KERBEROS) || defined(HAVE_GSSAPI) */ + + + +#if defined (AUTH_CLIENT_SUPPORT) || defined(HAVE_GSSAPI) +/* Connect to the authenticating server. + + If VERIFY_ONLY is non-zero, then just verify that the password is + correct and then shutdown the connection. + + If VERIFY_ONLY is 0, then really connect to the server. + + If DO_GSSAPI is non-zero, then we use GSSAPI authentication rather + than the pserver password authentication. + + If we fail to connect or if access is denied, then die with fatal + error. */ +void +connect_to_pserver (root, to_server_p, from_server_p, verify_only, do_gssapi) + cvsroot_t *root; + struct buffer **to_server_p; + struct buffer **from_server_p; + int verify_only; + int do_gssapi; +{ + int sock; + int port_number; + struct sockaddr_in client_sai; + struct hostent *hostinfo; + struct buffer *to_server, *from_server; + + sock = socket (AF_INET, SOCK_STREAM, 0); + if (sock == -1) + { + error (1, 0, "cannot create socket: %s", SOCK_STRERROR (SOCK_ERRNO)); + } + port_number = get_cvs_port_number (root); + hostinfo = init_sockaddr (&client_sai, root->hostname, port_number); + if (trace) + { + fprintf (stderr, " -> Connecting to %s(%s):%d\n", + root->hostname, + inet_ntoa (client_sai.sin_addr), port_number); + } + if (connect (sock, (struct sockaddr *) &client_sai, sizeof (client_sai)) + < 0) + error (1, 0, "connect to %s(%s):%d failed: %s", + root->hostname, + inet_ntoa (client_sai.sin_addr), + port_number, SOCK_STRERROR (SOCK_ERRNO)); + + make_bufs_from_fds (sock, sock, 0, &to_server, &from_server, 1); + + auth_server (root, to_server, from_server, verify_only, do_gssapi, hostinfo); + + if (verify_only) + { + int status; + + status = buf_shutdown (to_server); + if (status != 0) + error (0, status, "shutting down buffer to server"); + buf_free (to_server); + to_server = NULL; + + status = buf_shutdown (from_server); + if (status != 0) + error (0, status, "shutting down buffer from server"); + buf_free (from_server); + from_server = NULL; + + /* Don't need to set server_started = 0 since we don't set it to 1 + * until returning from this call. + */ + } + else + { + *to_server_p = to_server; + *from_server_p = from_server; + } + + return; +} + + + +static void +auth_server (root, lto_server, lfrom_server, verify_only, do_gssapi, hostinfo) + cvsroot_t *root; + struct buffer *lto_server; + struct buffer *lfrom_server; + int verify_only; + int do_gssapi; + struct hostent *hostinfo; +{ + char *username = ""; /* the username we use to connect */ + char no_passwd = 0; /* gets set if no password found */ + + /* FIXME!!!!!!!!!!!!!!!!!! + * + * THIS IS REALLY UGLY! + * + * I'm setting the globals here so we can make calls to send_to_server & + * read_line. This happens again _after_ we return if we're not in + * verify_only mode. We should be relying on the values we passed in, but + * sent_to_server and read_line don't require an outside buf yet. + */ + to_server = lto_server; + from_server = lfrom_server; + + /* Run the authorization mini-protocol before anything else. */ + if (do_gssapi) + { +# ifdef HAVE_GSSAPI + FILE *fp = stdio_buffer_get_file(lto_server); + int fd = fp ? fileno(fp) : -1; + struct stat s; + + if ((fd < 0) || (fstat (fd, &s) < 0) || !S_ISSOCK(s.st_mode)) + { + error (1, 0, "gserver currently only enabled for socket connections"); + } + + if (! connect_to_gserver (root, fd, hostinfo)) + { + error (1, 0, + "authorization failed: server %s rejected access to %s", + root->hostname, root->directory); + } +# else /* ! HAVE_GSSAPI */ + error (1, 0, "INTERNAL ERROR: This client does not support GSSAPI authentication"); +# endif /* HAVE_GSSAPI */ + } + else /* ! do_gssapi */ + { +# ifdef AUTH_CLIENT_SUPPORT + char *begin = NULL; + char *password = NULL; + char *end = NULL; + + if (verify_only) + { + begin = "BEGIN VERIFICATION REQUEST"; + end = "END VERIFICATION REQUEST"; + } + else + { + begin = "BEGIN AUTH REQUEST"; + end = "END AUTH REQUEST"; + } + + /* Get the password, probably from ~/.cvspass. */ + password = get_cvs_password (); + username = root->username ? root->username : getcaller(); + + /* Send the empty string by default. This is so anonymous CVS + access doesn't require client to have done "cvs login". */ + if (password == NULL) + { + no_passwd = 1; + password = scramble (""); + } + + /* Announce that we're starting the authorization protocol. */ + send_to_server(begin, 0); + send_to_server("\012", 1); + + /* Send the data the server needs. */ + send_to_server(root->directory, 0); + send_to_server("\012", 1); + send_to_server(username, 0); + send_to_server("\012", 1); + send_to_server(password, 0); + send_to_server("\012", 1); + + /* Announce that we're ending the authorization protocol. */ + send_to_server(end, 0); + send_to_server("\012", 1); + + free_cvs_password (password); + password = NULL; +# else /* ! AUTH_CLIENT_SUPPORT */ + error (1, 0, "INTERNAL ERROR: This client does not support pserver authentication"); +# endif /* AUTH_CLIENT_SUPPORT */ + } /* if (do_gssapi) */ + + { + char *read_buf; + + /* Loop, getting responses from the server. */ + while (1) + { + read_line (&read_buf); + + if (strcmp (read_buf, "I HATE YOU") == 0) + { + /* Authorization not granted. + * + * This is a little confusing since we can reach this while loop in GSSAPI + * mode, but if GSSAPI authentication failed, we already jumped to the + * rejected label (there is no case where the connect_to_gserver function + * can return 1 and we will not receive "I LOVE YOU" from the server, barring + * broken connections and garbled messages, of course). + * + * i.e. This is a pserver specific error message and should be since + * GSSAPI doesn't use username. + */ + error (0, 0, + "authorization failed: server %s rejected access to %s for user %s", + root->hostname, root->directory, username); + + /* Output a special error message if authentication was attempted + with no password -- the user should be made aware that they may + have missed a step. */ + if (no_passwd) + { + error (0, 0, + "used empty password; try \"cvs login\" with a real password"); + } + error_exit(); + } + else if (strncmp (read_buf, "E ", 2) == 0) + { + fprintf (stderr, "%s\n", read_buf + 2); + + /* Continue with the authentication protocol. */ + } + else if (strncmp (read_buf, "error ", 6) == 0) + { + char *p; + + /* First skip the code. */ + p = read_buf + 6; + while (*p != ' ' && *p != '\0') + ++p; + + /* Skip the space that follows the code. */ + if (*p == ' ') + ++p; + + /* Now output the text. */ + fprintf (stderr, "%s\n", p); + error_exit(); + } + else if (strcmp (read_buf, "I LOVE YOU") == 0) + { + free (read_buf); + break; + } + else + { + error (1, 0, + "unrecognized auth response from %s: %s", + root->hostname, read_buf); + } + free (read_buf); + } + } +} +#endif /* defined (AUTH_CLIENT_SUPPORT) || defined(HAVE_GSSAPI) */ + + + +#ifdef CLIENT_SUPPORT +/* void + * connect_to_forked_server ( struct buffer **to_server, + * struct buffer **from_server ) + * + * Connect to a forked server process. + */ +void +connect_to_forked_server (to_server, from_server) + struct buffer **to_server; + struct buffer **from_server; +{ + int tofd, fromfd; + int child_pid; + + /* This is pretty simple. All we need to do is choose the correct + cvs binary and call piped_child. */ + + const char *command[3]; + + command[0] = getenv ("CVS_SERVER"); + if (! command[0]) + command[0] = program_path; + + command[1] = "server"; + command[2] = NULL; + + if (trace) + { + fprintf (stderr, " -> Forking server: %s %s\n", command[0], command[1]); + } + + child_pid = piped_child (command, &tofd, &fromfd, 0); + if (child_pid < 0) + error (1, 0, "could not fork server process"); + + make_bufs_from_fds (tofd, fromfd, child_pid, to_server, from_server, 0); +} +#endif /* CLIENT_SUPPORT */ + + + +#ifdef HAVE_KERBEROS +/* This function has not been changed to deal with NO_SOCKET_TO_FD + (i.e., systems on which sockets cannot be converted to file + descriptors). The first person to try building a kerberos client + on such a system (OS/2, Windows 95, and maybe others) will have to + take care of this. */ +void +start_tcp_server (root, to_server, from_server) + cvsroot_t *root; + struct buffer **to_server; + struct buffer **from_server; +{ + int s; + const char *portenv; + int port; + struct hostent *hp; + struct sockaddr_in sin; + char *hname; + + s = socket (AF_INET, SOCK_STREAM, 0); + if (s < 0) + error (1, 0, "cannot create socket: %s", SOCK_STRERROR (SOCK_ERRNO)); + + port = get_cvs_port_number (root); + + hp = init_sockaddr (&sin, root->hostname, port); + + hname = xstrdup (hp->h_name); + + if (trace) + { + fprintf (stderr, " -> Connecting to %s(%s):%d\n", + root->hostname, + inet_ntoa (sin.sin_addr), port); + } + + if (connect (s, (struct sockaddr *) &sin, sizeof sin) < 0) + error (1, 0, "connect to %s(%s):%d failed: %s", + root->hostname, + inet_ntoa (sin.sin_addr), + port, SOCK_STRERROR (SOCK_ERRNO)); + + { + const char *realm; + struct sockaddr_in laddr; + int laddrlen; + KTEXT_ST ticket; + MSG_DAT msg_data; + CREDENTIALS cred; + int status; + + realm = krb_realmofhost (hname); + + laddrlen = sizeof (laddr); + if (getsockname (s, (struct sockaddr *) &laddr, &laddrlen) < 0) + error (1, 0, "getsockname failed: %s", SOCK_STRERROR (SOCK_ERRNO)); + + /* We don't care about the checksum, and pass it as zero. */ + status = krb_sendauth (KOPT_DO_MUTUAL, s, &ticket, "rcmd", + hname, realm, (unsigned long) 0, &msg_data, + &cred, sched, &laddr, &sin, "KCVSV1.0"); + if (status != KSUCCESS) + error (1, 0, "kerberos authentication failed: %s", + krb_get_err_text (status)); + memcpy (kblock, cred.session, sizeof (C_Block)); + } + + close_on_exec (s); + + free (hname); + + /* Give caller the values it wants. */ + make_bufs_from_fds (s, s, 0, to_server, from_server, 1); +} + +#endif /* HAVE_KERBEROS */ + +#ifdef HAVE_GSSAPI + +/* Receive a given number of bytes. */ + +static void +recv_bytes (sock, buf, need) + int sock; + char *buf; + int need; +{ + while (need > 0) + { + int got; + + got = recv (sock, buf, need, 0); + if (got <= 0) + error (1, 0, "recv() from server %s: %s", current_parsed_root->hostname, + got == 0 ? "EOF" : SOCK_STRERROR (SOCK_ERRNO)); + + buf += got; + need -= got; + } +} + +/* Connect to the server using GSSAPI authentication. */ + +/* FIXME + * + * This really needs to be rewritten to use a buffer and not a socket. + * This would enable gserver to work with the SSL code I'm about to commit + * since the SSL connection is going to look like a FIFO and not a socket. + * + * I think, basically, it will need to use buf_output and buf_read directly + * since I don't think there is a read_bytes function - only read_line. + * + * recv_bytes could then be removed too. + * + * Besides, I added some cruft to reenable the socket which shouldn't be + * there. This would also enable its removal. + */ +#define BUFSIZE 1024 +static int +connect_to_gserver (root, sock, hostinfo) + cvsroot_t *root; + int sock; + struct hostent *hostinfo; +{ + char *str; + char buf[BUFSIZE]; + gss_buffer_desc *tok_in_ptr, tok_in, tok_out; + OM_uint32 stat_min, stat_maj; + gss_name_t server_name; + + str = "BEGIN GSSAPI REQUEST\012"; + + if (send (sock, str, strlen (str), 0) < 0) + error (1, 0, "cannot send: %s", SOCK_STRERROR (SOCK_ERRNO)); + + if (strlen (hostinfo->h_name) > BUFSIZE - 5) + error (1, 0, "Internal error: hostname exceeds length of buffer"); + sprintf (buf, "cvs@%s", hostinfo->h_name); + tok_in.length = strlen (buf); + tok_in.value = buf; + gss_import_name (&stat_min, &tok_in, GSS_C_NT_HOSTBASED_SERVICE, + &server_name); + + tok_in_ptr = GSS_C_NO_BUFFER; + gcontext = GSS_C_NO_CONTEXT; + + do + { + stat_maj = gss_init_sec_context (&stat_min, GSS_C_NO_CREDENTIAL, + &gcontext, server_name, + GSS_C_NULL_OID, + (GSS_C_MUTUAL_FLAG + | GSS_C_REPLAY_FLAG), + 0, NULL, tok_in_ptr, NULL, &tok_out, + NULL, NULL); + if (stat_maj != GSS_S_COMPLETE && stat_maj != GSS_S_CONTINUE_NEEDED) + { + OM_uint32 message_context; + OM_uint32 new_stat_min; + + message_context = 0; + gss_display_status (&new_stat_min, stat_maj, GSS_C_GSS_CODE, + GSS_C_NULL_OID, &message_context, &tok_out); + error (0, 0, "GSSAPI authentication failed: %s", + (char *) tok_out.value); + + message_context = 0; + gss_display_status (&new_stat_min, stat_min, GSS_C_MECH_CODE, + GSS_C_NULL_OID, &message_context, &tok_out); + error (1, 0, "GSSAPI authentication failed: %s", + (char *) tok_out.value); + } + + if (tok_out.length == 0) + { + tok_in.length = 0; + } + else + { + char cbuf[2]; + int need; + + cbuf[0] = (tok_out.length >> 8) & 0xff; + cbuf[1] = tok_out.length & 0xff; + if (send (sock, cbuf, 2, 0) < 0) + error (1, 0, "cannot send: %s", SOCK_STRERROR (SOCK_ERRNO)); + if (send (sock, tok_out.value, tok_out.length, 0) < 0) + error (1, 0, "cannot send: %s", SOCK_STRERROR (SOCK_ERRNO)); + + recv_bytes (sock, cbuf, 2); + need = ((cbuf[0] & 0xff) << 8) | (cbuf[1] & 0xff); + + if (need > sizeof buf) + { + ssize_t got; + size_t total; + + /* This usually means that the server sent us an error + message. Read it byte by byte and print it out. + FIXME: This is a terrible error handling strategy. + However, even if we fix the server, we will still + want to do this to work with older servers. */ + buf[0] = cbuf[0]; + buf[1] = cbuf[1]; + total = 2; + while (got = recv (sock, buf + total, sizeof buf - total, 0)) + { + if (got < 0) + error (1, 0, "recv() from server %s: %s", + root->hostname, SOCK_STRERROR (SOCK_ERRNO)); + total += got; + if (strrchr (buf + total - got, '\n')) + break; + } + buf[total] = '\0'; + if (buf[total - 1] == '\n') + buf[total - 1] = '\0'; + error (1, 0, "error from server %s: %s", root->hostname, + buf); + } + + recv_bytes (sock, buf, need); + tok_in.length = need; + } + + tok_in.value = buf; + tok_in_ptr = &tok_in; + } + while (stat_maj == GSS_S_CONTINUE_NEEDED); + + return 1; +} + +#endif /* HAVE_GSSAPI */ + + + +static int send_variable_proc PROTO ((Node *, void *)); + +static int +send_variable_proc (node, closure) + Node *node; + void *closure; +{ + send_to_server ("Set ", 0); + send_to_server (node->key, 0); + send_to_server ("=", 1); + send_to_server (node->data, 0); + send_to_server ("\012", 1); + return 0; +} + + + +/* Contact the server. */ +void +start_server () +{ + int rootless; + char *log = getenv ("CVS_CLIENT_LOG"); + + /* Clear our static variables for this invocation. */ + if (toplevel_repos != NULL) + free (toplevel_repos); + toplevel_repos = NULL; + + /* Note that generally speaking we do *not* fall back to a different + way of connecting if the first one does not work. This is slow + (*really* slow on a 14.4kbps link); the clean way to have a CVS + which supports several ways of connecting is with access methods. */ + + switch (current_parsed_root->method) + { + +#ifdef AUTH_CLIENT_SUPPORT + case pserver_method: + /* Toss the return value. It will die with an error message if + * anything goes wrong anyway. + */ + connect_to_pserver (current_parsed_root, &to_server, &from_server, 0, 0); + break; +#endif /* AUTH_CLIENT_SUPPORT */ + +#if HAVE_KERBEROS + case kserver_method: + start_tcp_server (current_parsed_root, &to_server, &from_server); + break; +#endif /* HAVE_KERBEROS */ + +#ifdef HAVE_GSSAPI + case gserver_method: + /* GSSAPI authentication is handled by the pserver. */ + connect_to_pserver (current_parsed_root, &to_server, &from_server, 0, 1); + break; +#endif /* HAVE_GSSAPI */ + + case ext_method: + case extssh_method: +#ifdef NO_EXT_METHOD + error (0, 0, ":ext: method not supported by this port of CVS"); + error (1, 0, "try :server: instead"); +#else /* ! NO_EXT_METHOD */ + start_rsh_server (current_parsed_root, &to_server, &from_server); +#endif /* NO_EXT_METHOD */ + break; + + case server_method: +#ifdef START_SERVER + { + int tofd, fromfd; + START_SERVER (&tofd, &fromfd, getcaller (), + current_parsed_root->username, current_parsed_root->hostname, + current_parsed_root->directory); +# ifdef START_SERVER_RETURNS_SOCKET + make_bufs_from_fds (tofd, fromfd, 0, &to_server, &from_server, 1); +# else /* ! START_SERVER_RETURNS_SOCKET */ + make_bufs_from_fds (tofd, fromfd, 0, &to_server, &from_server, 0); +# endif /* START_SERVER_RETURNS_SOCKET */ + } +#else /* ! START_SERVER */ + /* FIXME: It should be possible to implement this portably, + like pserver, which would get rid of the duplicated code + in {vms,windows-NT,...}/startserver.c. */ + error (1, 0, +"the :server: access method is not supported by this port of CVS"); +#endif /* START_SERVER */ + break; + + case fork_method: + connect_to_forked_server (&to_server, &from_server); + break; + + default: + error (1, 0, "\ +(start_server internal error): unknown access method"); + break; + } + + /* "Hi, I'm Darlene and I'll be your server tonight..." */ + server_started = 1; + + /* Set up logfiles, if any. + * + * We do this _after_ authentication on purpose. Wouldn't really like to + * worry about logging passwords... + */ + if (log) + { + int len = strlen (log); + char *buf = xmalloc (len + 5); + char *p; + FILE *fp; + + strcpy (buf, log); + p = buf + len; + + /* Open logfiles in binary mode so that they reflect + exactly what was transmitted and received (that is + more important than that they be maximally + convenient to view). */ + /* Note that if we create several connections in a single CVS client + (currently used by update.c), then the last set of logfiles will + overwrite the others. There is currently no way around this. */ + strcpy (p, ".in"); + fp = open_file (buf, "wb"); + if (fp == NULL) + error (0, errno, "opening to-server logfile %s", buf); + else + to_server = log_buffer_initialize (to_server, fp, 0, + (BUFMEMERRPROC) NULL); + + strcpy (p, ".out"); + fp = open_file (buf, "wb"); + if (fp == NULL) + error (0, errno, "opening from-server logfile %s", buf); + else + from_server = log_buffer_initialize (from_server, fp, 1, + (BUFMEMERRPROC) NULL); + + free (buf); + } + + /* Clear static variables. */ + if (toplevel_repos != NULL) + free (toplevel_repos); + toplevel_repos = NULL; + if (last_repos != NULL) + free (last_repos); + last_repos = NULL; + if (last_update_dir != NULL) + free (last_update_dir); + last_update_dir = NULL; + stored_checksum_valid = 0; + if (stored_mode != NULL) + { + free (stored_mode); + stored_mode = NULL; + } + + rootless = (strcmp (cvs_cmd_name, "init") == 0); + if (!rootless) + { + send_to_server ("Root ", 0); + send_to_server (current_parsed_root->directory, 0); + send_to_server ("\012", 1); + } + + { + struct response *rs; + + send_to_server ("Valid-responses", 0); + + for (rs = responses; rs->name != NULL; ++rs) + { + send_to_server (" ", 0); + send_to_server (rs->name, 0); + } + send_to_server ("\012", 1); + } + send_to_server ("valid-requests\012", 0); + + if (get_server_responses ()) + error_exit (); + + /* + * Now handle global options. + * + * -H, -f, -d, -e should be handled OK locally. + * + * -b we ignore (treating it as a server installation issue). + * FIXME: should be an error message. + * + * -v we print local version info; FIXME: Add a protocol request to get + * the version from the server so we can print that too. + * + * -l -t -r -w -q -n and -Q need to go to the server. + */ + + { + int have_global = supported_request ("Global_option"); + + if (noexec) + { + if (have_global) + { + send_to_server ("Global_option -n\012", 0); + } + else + error (1, 0, + "This server does not support the global -n option."); + } + if (quiet) + { + if (have_global) + { + send_to_server ("Global_option -q\012", 0); + } + else + error (1, 0, + "This server does not support the global -q option."); + } + if (really_quiet) + { + if (have_global) + { + send_to_server ("Global_option -Q\012", 0); + } + else + error (1, 0, + "This server does not support the global -Q option."); + } + if (!cvswrite) + { + if (have_global) + { + send_to_server ("Global_option -r\012", 0); + } + else + error (1, 0, + "This server does not support the global -r option."); + } + if (trace) + { + if (have_global) + { + send_to_server ("Global_option -t\012", 0); + } + else + error (1, 0, + "This server does not support the global -t option."); + } + } + + /* Find out about server-side cvswrappers. An extra network + turnaround for cvs import seems to be unavoidable, unless we + want to add some kind of client-side place to configure which + filenames imply binary. For cvs add, we could avoid the + problem by keeping a copy of the wrappers in CVSADM (the main + reason to bother would be so we could make add work without + contacting the server, I suspect). */ + + if ((strcmp (cvs_cmd_name, "import") == 0) + || (strcmp (cvs_cmd_name, "add") == 0)) + { + if (supported_request ("wrapper-sendme-rcsOptions")) + { + int err; + send_to_server ("wrapper-sendme-rcsOptions\012", 0); + err = get_server_responses (); + if (err != 0) + error (err, 0, "error reading from server"); + } + } + + if (cvsencrypt && !rootless) + { +#ifdef ENCRYPTION + /* Turn on encryption before turning on compression. We do + not want to try to compress the encrypted stream. Instead, + we want to encrypt the compressed stream. If we can't turn + on encryption, bomb out; don't let the user think the data + is being encrypted when it is not. */ +#ifdef HAVE_KERBEROS + if (current_parsed_root->method == kserver_method) + { + if (! supported_request ("Kerberos-encrypt")) + error (1, 0, "This server does not support encryption"); + send_to_server ("Kerberos-encrypt\012", 0); + to_server = krb_encrypt_buffer_initialize (to_server, 0, sched, + kblock, + (BUFMEMERRPROC) NULL); + from_server = krb_encrypt_buffer_initialize (from_server, 1, + sched, kblock, + (BUFMEMERRPROC) NULL); + } + else +#endif /* HAVE_KERBEROS */ +#ifdef HAVE_GSSAPI + if (current_parsed_root->method == gserver_method) + { + if (! supported_request ("Gssapi-encrypt")) + error (1, 0, "This server does not support encryption"); + send_to_server ("Gssapi-encrypt\012", 0); + to_server = cvs_gssapi_wrap_buffer_initialize (to_server, 0, + gcontext, + ((BUFMEMERRPROC) + NULL)); + from_server = cvs_gssapi_wrap_buffer_initialize (from_server, 1, + gcontext, + ((BUFMEMERRPROC) + NULL)); + cvs_gssapi_encrypt = 1; + } + else +#endif /* HAVE_GSSAPI */ + error (1, 0, "Encryption is only supported when using GSSAPI or Kerberos"); +#else /* ! ENCRYPTION */ + error (1, 0, "This client does not support encryption"); +#endif /* ! ENCRYPTION */ + } + + if (gzip_level && !rootless) + { + if (supported_request ("Gzip-stream")) + { + char gzip_level_buf[5]; + send_to_server ("Gzip-stream ", 0); + sprintf (gzip_level_buf, "%d", gzip_level); + send_to_server (gzip_level_buf, 0); + send_to_server ("\012", 1); + + /* All further communication with the server will be + compressed. */ + + to_server = compress_buffer_initialize (to_server, 0, gzip_level, + (BUFMEMERRPROC) NULL); + from_server = compress_buffer_initialize (from_server, 1, + gzip_level, + (BUFMEMERRPROC) NULL); + } +#ifndef NO_CLIENT_GZIP_PROCESS + else if (supported_request ("gzip-file-contents")) + { + char gzip_level_buf[5]; + send_to_server ("gzip-file-contents ", 0); + sprintf (gzip_level_buf, "%d", gzip_level); + send_to_server (gzip_level_buf, 0); + + send_to_server ("\012", 1); + + file_gzip_level = gzip_level; + } +#endif + else + { + fprintf (stderr, "server doesn't support gzip-file-contents\n"); + /* Setting gzip_level to 0 prevents us from giving the + error twice if update has to contact the server again + to fetch unpatchable files. */ + gzip_level = 0; + } + } + + if (cvsauthenticate && ! cvsencrypt && !rootless) + { + /* Turn on authentication after turning on compression, so + that we can compress the authentication information. We + assume that encrypted data is always authenticated--the + ability to decrypt the data stream is itself a form of + authentication. */ +#ifdef HAVE_GSSAPI + if (current_parsed_root->method == gserver_method) + { + if (! supported_request ("Gssapi-authenticate")) + error (1, 0, + "This server does not support stream authentication"); + send_to_server ("Gssapi-authenticate\012", 0); + to_server = cvs_gssapi_wrap_buffer_initialize (to_server, 0, + gcontext, + ((BUFMEMERRPROC) + NULL)); + from_server = cvs_gssapi_wrap_buffer_initialize (from_server, 1, + gcontext, + ((BUFMEMERRPROC) + NULL)); + } + else + error (1, 0, "Stream authentication is only supported when using GSSAPI"); +#else /* ! HAVE_GSSAPI */ + error (1, 0, "This client does not support stream authentication"); +#endif /* ! HAVE_GSSAPI */ + } + + /* If "Set" is not supported, just silently fail to send the variables. + Users with an old server should get a useful error message when it + fails to recognize the ${=foo} syntax. This way if someone uses + several servers, some of which are new and some old, they can still + set user variables in their .cvsrc without trouble. */ + if (supported_request ("Set")) + walklist (variable_list, send_variable_proc, NULL); +} + + + +#ifndef NO_EXT_METHOD + +/* Contact the server by starting it with rsh. */ + +/* Right now, we have two different definitions for this function, + depending on whether we start the rsh server using popenRW or not. + This isn't ideal, and the best thing would probably be to change + the OS/2 port to be more like the regular Unix client (i.e., by + implementing piped_child)... but I'm doing something else at the + moment, and wish to make only one change at a time. -Karl */ + +# ifdef START_RSH_WITH_POPEN_RW + +/* This is actually a crock -- it's OS/2-specific, for no one else + uses it. If I get time, I want to make piped_child and all the + other stuff in os2/run.c work right. In the meantime, this gets us + up and running, and that's most important. */ + +static void +start_rsh_server (root, to_server, from_server) + cvsroot_t *root; + struct buffer **to_server; + struct buffer **from_server; +{ + int pipes[2]; + int child_pid; + + /* If you're working through firewalls, you can set the + CVS_RSH environment variable to a script which uses rsh to + invoke another rsh on a proxy machine. */ + char *env_cvs_rsh = getenv ("CVS_RSH"); + char *env_cvs_ssh = getenv ("CVS_SSH"); + char *cvs_rsh; + char *cvs_server = getenv ("CVS_SERVER"); + int i = 0; + /* This needs to fit "rsh", "-b", "-l", "USER", "host", + "cmd (w/ args)", and NULL. We leave some room to grow. */ + char *rsh_argv[10]; + + if (root->method == extssh_method) + cvs_rsh = env_cvs_ssh ? env_cvs_ssh : SSH_DFLT; + else + cvs_rsh = env_cvs_rsh ? env_cvs_rsh : RSH_DFLT; + + if (!cvs_server) + cvs_server = "cvs"; + + /* The command line starts out with rsh. */ + rsh_argv[i++] = cvs_rsh; + +# ifdef RSH_NEEDS_BINARY_FLAG + /* "-b" for binary, under OS/2. */ + rsh_argv[i++] = "-b"; +# endif /* RSH_NEEDS_BINARY_FLAG */ + + /* Then we strcat more things on the end one by one. */ + if (root->username != NULL) + { + rsh_argv[i++] = "-l"; + rsh_argv[i++] = root->username; + } + + rsh_argv[i++] = root->hostname; + rsh_argv[i++] = cvs_server; + rsh_argv[i++] = "server"; + + /* Mark the end of the arg list. */ + rsh_argv[i] = (char *) NULL; + + if (trace) + { + fprintf (stderr, " -> Starting server: "); + for (i = 0; rsh_argv[i]; i++) + fprintf (stderr, "%s ", rsh_argv[i]); + putc ('\n', stderr); + } + + /* Do the deed. */ + child_pid = popenRW (rsh_argv, pipes); + if (child_pid < 0) + error (1, errno, "cannot start server via rsh"); + + /* Give caller the file descriptors in a form it can deal with. */ + make_bufs_from_fds (pipes[0], pipes[1], child_pid, to_server, from_server, 0); +} + +# else /* ! START_RSH_WITH_POPEN_RW */ + +static void +start_rsh_server (root, to_server, from_server) + cvsroot_t *root; + struct buffer **to_server; + struct buffer **from_server; +{ + /* If you're working through firewalls, you can set the + CVS_RSH environment variable to a script which uses rsh to + invoke another rsh on a proxy machine. */ + char *env_cvs_rsh = getenv ("CVS_RSH"); + char *env_cvs_ssh = getenv ("CVS_SSH"); + char *cvs_rsh; + char *cvs_server = getenv ("CVS_SERVER"); + char *command; + int tofd, fromfd; + int child_pid; + + if (root->method == extssh_method) + cvs_rsh = env_cvs_ssh ? env_cvs_ssh : SSH_DFLT; + else + cvs_rsh = env_cvs_rsh ? env_cvs_rsh : RSH_DFLT; + + if (!cvs_server) + cvs_server = "cvs"; + + /* Pass the command to rsh as a single string. This shouldn't + affect most rsh servers at all, and will pacify some buggy + versions of rsh that grab switches out of the middle of the + command (they're calling the GNU getopt routines incorrectly). */ + command = xmalloc (strlen (cvs_server) + 8); + + /* If you are running a very old (Nov 3, 1994, before 1.5) + * version of the server, you need to make sure that your .bashrc + * on the server machine does not set CVSROOT to something + * containing a colon (or better yet, upgrade the server). */ + sprintf (command, "%s server", cvs_server); + + { + const char *argv[10]; + const char **p = argv; + + *p++ = cvs_rsh; + + /* If the login names differ between client and server + * pass it on to rsh. + */ + if (root->username != NULL) + { + *p++ = "-l"; + *p++ = root->username; + } + + *p++ = root->hostname; + *p++ = command; + *p++ = NULL; + + if (trace) + { + int i; + + fprintf (stderr, " -> Starting server: "); + for (i = 0; argv[i]; i++) + fprintf (stderr, "%s ", argv[i]); + putc ('\n', stderr); + } + child_pid = piped_child (argv, &tofd, &fromfd, 1); + + if (child_pid < 0) + error (1, errno, "cannot start server via rsh"); + } + free (command); + + make_bufs_from_fds (tofd, fromfd, child_pid, to_server, from_server, 0); +} + +# endif /* START_RSH_WITH_POPEN_RW */ + +#endif /* NO_EXT_METHOD */ + + + +/* Send an argument STRING. */ +void +send_arg (string) + const char *string; +{ + char buf[1]; + const char *p = string; + + send_to_server ("Argument ", 0); + + while (*p) + { + if (*p == '\n') + { + send_to_server ("\012Argumentx ", 0); + } + else + { + buf[0] = *p; + send_to_server (buf, 1); + } + ++p; + } + send_to_server ("\012", 1); +} + + + +static void send_modified PROTO ((const char *, const char *, Vers_TS *)); + +/* VERS->OPTIONS specifies whether the file is binary or not. NOTE: BEFORE + using any other fields of the struct vers, we would need to fix + client_process_import_file to set them up. */ + +static void +send_modified (file, short_pathname, vers) + const char *file; + const char *short_pathname; + Vers_TS *vers; +{ + /* File was modified, send it. */ + struct stat sb; + int fd; + char *buf; + char *mode_string; + size_t bufsize; + int bin; + + if (trace) + (void) fprintf (stderr, " -> Sending file `%s' to server\n", file); + + /* Don't think we can assume fstat exists. */ + if ( CVS_STAT (file, &sb) < 0) + error (1, errno, "reading %s", short_pathname); + + mode_string = mode_to_string (sb.st_mode); + + /* Beware: on systems using CRLF line termination conventions, + the read and write functions will convert CRLF to LF, so the + number of characters read is not the same as sb.st_size. Text + files should always be transmitted using the LF convention, so + we don't want to disable this conversion. */ + bufsize = sb.st_size; + buf = xmalloc (bufsize); + + /* Is the file marked as containing binary data by the "-kb" flag? + If so, make sure to open it in binary mode: */ + + if (vers && vers->options) + bin = !(strcmp (vers->options, "-kb")); + else + bin = 0; + +#ifdef BROKEN_READWRITE_CONVERSION + if (!bin) + { + /* If only stdio, not open/write/etc., do text/binary + conversion, use convert_file which can compensate + (FIXME: we could just use stdio instead which would + avoid the whole problem). */ + char tfile[1024]; strcpy(tfile, file); strcat(tfile, ".CVSBFCTMP"); + convert_file (file, O_RDONLY, + tfile, O_WRONLY | O_CREAT | O_TRUNC | OPEN_BINARY); + fd = CVS_OPEN (tfile, O_RDONLY | OPEN_BINARY); + if (fd < 0) + error (1, errno, "reading %s", short_pathname); + } + else + fd = CVS_OPEN (file, O_RDONLY | OPEN_BINARY); +#else + fd = CVS_OPEN (file, O_RDONLY | (bin ? OPEN_BINARY : 0)); +#endif + + if (fd < 0) + error (1, errno, "reading %s", short_pathname); + + if (file_gzip_level && sb.st_size > 100) + { + size_t newsize = 0; + + if (read_and_gzip (fd, short_pathname, (unsigned char **)&buf, + &bufsize, &newsize, + file_gzip_level)) + error (1, 0, "aborting due to compression error"); + + if (close (fd) < 0) + error (0, errno, "warning: can't close %s", short_pathname); + + { + char tmp[80]; + + send_to_server ("Modified ", 0); + send_to_server (file, 0); + send_to_server ("\012", 1); + send_to_server (mode_string, 0); + send_to_server ("\012z", 2); + sprintf (tmp, "%lu\n", (unsigned long) newsize); + send_to_server (tmp, 0); + + send_to_server (buf, newsize); + } + } + else + { + int newsize; + + { + char *bufp = buf; + int len; + + /* FIXME: This is gross. It assumes that we might read + less than st_size bytes (true on NT), but not more. + Instead of this we should just be reading a block of + data (e.g. 8192 bytes), writing it to the network, and + so on until EOF. */ + while ((len = read (fd, bufp, (buf + sb.st_size) - bufp)) > 0) + bufp += len; + + if (len < 0) + error (1, errno, "reading %s", short_pathname); + + newsize = bufp - buf; + } + if (close (fd) < 0) + error (0, errno, "warning: can't close %s", short_pathname); + + { + char tmp[80]; + + send_to_server ("Modified ", 0); + send_to_server (file, 0); + send_to_server ("\012", 1); + send_to_server (mode_string, 0); + send_to_server ("\012", 1); + sprintf (tmp, "%lu\012", (unsigned long) newsize); + send_to_server (tmp, 0); + } +#ifdef BROKEN_READWRITE_CONVERSION + if (!bin) + { + char tfile[1024]; strcpy(tfile, file); strcat(tfile, ".CVSBFCTMP"); + if (CVS_UNLINK (tfile) < 0) + error (0, errno, "warning: can't remove temp file %s", tfile); + } +#endif + + /* + * Note that this only ends with a newline if the file ended with + * one. + */ + if (newsize > 0) + send_to_server (buf, newsize); + } + free (buf); + free (mode_string); +} + +/* The address of an instance of this structure is passed to + send_fileproc, send_filesdoneproc, and send_direntproc, as the + callerdat parameter. */ + +struct send_data +{ + /* Each of the following flags are zero for clear or nonzero for set. */ + int build_dirs; + int force; + int no_contents; + int backup_modified; +}; + +static int send_fileproc PROTO ((void *callerdat, struct file_info *finfo)); + +/* Deal with one file. */ +static int +send_fileproc (callerdat, finfo) + void *callerdat; + struct file_info *finfo; +{ + struct send_data *args = (struct send_data *) callerdat; + Vers_TS *vers; + struct file_info xfinfo; + /* File name to actually use. Might differ in case from + finfo->file. */ + const char *filename; + + send_a_repository ("", finfo->repository, finfo->update_dir); + + xfinfo = *finfo; + xfinfo.repository = NULL; + xfinfo.rcs = NULL; + vers = Version_TS (&xfinfo, NULL, NULL, NULL, 0, 0); + + if (vers->entdata != NULL) + filename = vers->entdata->user; + else + filename = finfo->file; + + if (vers->vn_user != NULL) + { + /* The Entries request. */ + send_to_server ("Entry /", 0); + send_to_server (filename, 0); + send_to_server ("/", 0); + send_to_server (vers->vn_user, 0); + send_to_server ("/", 0); + if (vers->ts_conflict != NULL) + { + if (vers->ts_user != NULL && + strcmp (vers->ts_conflict, vers->ts_user) == 0) + send_to_server ("+=", 0); + else + send_to_server ("+modified", 0); + } + send_to_server ("/", 0); + send_to_server (vers->entdata != NULL + ? vers->entdata->options + : vers->options, + 0); + send_to_server ("/", 0); + if (vers->entdata != NULL && vers->entdata->tag) + { + send_to_server ("T", 0); + send_to_server (vers->entdata->tag, 0); + } + else if (vers->entdata != NULL && vers->entdata->date) + { + send_to_server ("D", 0); + send_to_server (vers->entdata->date, 0); + } + send_to_server ("\012", 1); + } + else + { + /* It seems a little silly to re-read this on each file, but + send_dirent_proc doesn't get called if filenames are specified + explicitly on the command line. */ + wrap_add_file (CVSDOTWRAPPER, 1); + + if (wrap_name_has (filename, WRAP_RCSOPTION)) + { + /* No "Entry", but the wrappers did give us a kopt so we better + send it with "Kopt". As far as I know this only happens + for "cvs add". Question: is there any reason why checking + for options from wrappers isn't done in Version_TS? + + Note: it might have been better to just remember all the + kopts on the client side, rather than send them to the server, + and have it send us back the same kopts. But that seemed like + a bigger change than I had in mind making now. */ + + if (supported_request ("Kopt")) + { + char *opt; + + send_to_server ("Kopt ", 0); + opt = wrap_rcsoption (filename, 1); + send_to_server (opt, 0); + send_to_server ("\012", 1); + free (opt); + } + else + error (0, 0, + "\ +warning: ignoring -k options due to server limitations"); + } + } + + if (vers->ts_user == NULL) + { + /* + * Do we want to print "file was lost" like normal CVS? + * Would it always be appropriate? + */ + /* File no longer exists. Don't do anything, missing files + just happen. */ + } + else if (vers->ts_rcs == NULL + || args->force + || strcmp (vers->ts_conflict + && supported_request ("Empty-conflicts") + ? vers->ts_conflict : vers->ts_rcs, vers->ts_user) + || (vers->ts_conflict && !strcmp (cvs_cmd_name, "diff")) + || (vers->vn_user && *vers->vn_user == '0')) + { + if (args->no_contents + && supported_request ("Is-modified")) + { + send_to_server ("Is-modified ", 0); + send_to_server (filename, 0); + send_to_server ("\012", 1); + } + else + send_modified (filename, finfo->fullname, vers); + + if (args->backup_modified) + { + char *bakname; + bakname = backup_file (filename, vers->vn_user); + /* This behavior is sufficiently unexpected to + justify overinformativeness, I think. */ + if (! really_quiet) + printf ("(Locally modified %s moved to %s)\n", + filename, bakname); + free (bakname); + } + } + else + { + send_to_server ("Unchanged ", 0); + send_to_server (filename, 0); + send_to_server ("\012", 1); + } + + /* if this directory has an ignore list, add this file to it */ + if (ignlist) + { + Node *p; + + p = getnode (); + p->type = FILES; + p->key = xstrdup (finfo->file); + (void) addnode (ignlist, p); + } + + freevers_ts (&vers); + return 0; +} + + + +static void send_ignproc PROTO ((const char *, const char *)); + +static void +send_ignproc (file, dir) + const char *file; + const char *dir; +{ + if (ign_inhibit_server || !supported_request ("Questionable")) + { + if (dir[0] != '\0') + (void) printf ("? %s/%s\n", dir, file); + else + (void) printf ("? %s\n", file); + } + else + { + send_to_server ("Questionable ", 0); + send_to_server (file, 0); + send_to_server ("\012", 1); + } +} + + + +static int send_filesdoneproc PROTO ((void *, int, const char *, const char *, + List *)); + +static int +send_filesdoneproc (callerdat, err, repository, update_dir, entries) + void *callerdat; + int err; + const char *repository; + const char *update_dir; + List *entries; +{ + /* if this directory has an ignore list, process it then free it */ + if (ignlist) + { + ignore_files (ignlist, entries, update_dir, send_ignproc); + dellist (&ignlist); + } + + return (err); +} + +static Dtype send_dirent_proc PROTO ((void *, const char *, const char *, + const char *, List *)); + +/* + * send_dirent_proc () is called back by the recursion processor before a + * sub-directory is processed for update. + * A return code of 0 indicates the directory should be + * processed by the recursion code. A return of non-zero indicates the + * recursion code should skip this directory. + * + */ +static Dtype +send_dirent_proc (callerdat, dir, repository, update_dir, entries) + void *callerdat; + const char *dir; + const char *repository; + const char *update_dir; + List *entries; +{ + struct send_data *args = (struct send_data *) callerdat; + int dir_exists; + char *cvsadm_name; + + if (ignore_directory (update_dir)) + { + /* print the warm fuzzy message */ + if (!quiet) + error (0, 0, "Ignoring %s", update_dir); + return (R_SKIP_ALL); + } + + /* + * If the directory does not exist yet (e.g. "cvs update -d foo"), + * no need to send any files from it. If the directory does not + * have a CVS directory, then we pretend that it does not exist. + * Otherwise, we will fail when trying to open the Entries file. + * This case will happen when checking out a module defined as + * ``-a .''. + */ + cvsadm_name = xmalloc (strlen (dir) + sizeof (CVSADM) + 10); + sprintf (cvsadm_name, "%s/%s", dir, CVSADM); + dir_exists = isdir (cvsadm_name); + free (cvsadm_name); + + /* + * If there is an empty directory (e.g. we are doing `cvs add' on a + * newly-created directory), the server still needs to know about it. + */ + + if (dir_exists) + { + /* + * Get the repository from a CVS/Repository file whenever possible. + * The repository variable is wrong if the names in the local + * directory don't match the names in the repository. + */ + char *repos = Name_Repository (dir, update_dir); + send_a_repository (dir, repos, update_dir); + free (repos); + + /* initialize the ignore list for this directory */ + ignlist = getlist (); + } + else + { + /* It doesn't make sense to send a non-existent directory, + because there is no way to get the correct value for + the repository (I suppose maybe via the expand-modules + request). In the case where the "obvious" choice for + repository is correct, the server can figure out whether + to recreate the directory; in the case where it is wrong + (that is, does not match what modules give us), we might as + well just fail to recreate it. + + Checking for noexec is a kludge for "cvs -n add dir". */ + /* Don't send a non-existent directory unless we are building + new directories (build_dirs is true). Otherwise, CVS may + see a D line in an Entries file, and recreate a directory + which the user removed by hand. */ + if (args->build_dirs && noexec) + send_a_repository (dir, repository, update_dir); + } + + return (dir_exists ? R_PROCESS : R_SKIP_ALL); +} + + + +static int send_dirleave_proc PROTO ((void *, const char *, int, const char *, + List *)); + +/* + * send_dirleave_proc () is called back by the recursion code upon leaving + * a directory. All it does is delete the ignore list if it hasn't already + * been done (by send_filesdone_proc). + */ +/* ARGSUSED */ +static int +send_dirleave_proc (callerdat, dir, err, update_dir, entries) + void *callerdat; + const char *dir; + int err; + const char *update_dir; + List *entries; +{ + + /* Delete the ignore list if it hasn't already been done. */ + if (ignlist) + dellist (&ignlist); + return err; +} + +/* + * Send each option in an array to the server, one by one. + * argv might be "--foo=bar", "-C", "5", "-y". + */ +void +send_options (int argc, char *const *argv) +{ + int i; + for (i = 0; i < argc; i++) + send_arg (argv[i]); +} + + + +/* Send the names of all the argument files to the server. */ +void +send_file_names (argc, argv, flags) + int argc; + char **argv; + unsigned int flags; +{ + int i; + + /* The fact that we do this here as well as start_recursion is a bit + of a performance hit. Perhaps worth cleaning up someday. */ + if (flags & SEND_EXPAND_WILD) + expand_wild (argc, argv, &argc, &argv); + + for (i = 0; i < argc; ++i) + { + char buf[1]; + char *p; +#ifdef FILENAMES_CASE_INSENSITIVE + char *line = xmalloc (1); + *line = '\0'; +#endif /* FILENAMES_CASE_INSENSITIVE */ + + if (arg_should_not_be_sent_to_server (argv[i])) + continue; + +#ifdef FILENAMES_CASE_INSENSITIVE + /* We want to send the path as it appears in the + CVS/Entries files. We put this inside an ifdef + to avoid doing all these system calls in + cases where fncmp is just strcmp anyway. */ + /* The isdir (CVSADM) check could more gracefully be replaced + with a way of having Entries_Open report back the + error to us and letting us ignore existence_error. + Or some such. */ + { + List *stack; + size_t line_len = 0; + char *q, *r; + struct saved_cwd sdir; + + /* Split the argument onto the stack. */ + stack = getlist(); + r = xstrdup (argv[i]); + /* It's okay to discard the const from the last_component return + * below since we know we passed in an arg that was not const. + */ + while ((q = (char *)last_component (r)) != r) + { + push (stack, xstrdup (q)); + *--q = '\0'; + } + push (stack, r); + + /* Normalize the path into outstr. */ + save_cwd (&sdir); + while (q = pop (stack)) + { + Node *node = NULL; + if (isdir (CVSADM)) + { + List *entries; + + /* Note that if we are adding a directory, + the following will read the entry + that we just wrote there, that is, we + will get the case specified on the + command line, not the case of the + directory in the filesystem. This + is correct behavior. */ + entries = Entries_Open (0, NULL); + node = findnode_fn (entries, q); + if (node != NULL) + { + /* Add the slash unless this is our first element. */ + if (line_len) + xrealloc_and_strcat (&line, &line_len, "/"); + xrealloc_and_strcat (&line, &line_len, node->key); + delnode (node); + } + Entries_Close (entries); + } + + /* If node is still NULL then we either didn't find CVSADM or + * we didn't find an entry there. + */ + if (node == NULL) + { + /* Add the slash unless this is our first element. */ + if (line_len) + xrealloc_and_strcat (&line, &line_len, "/"); + xrealloc_and_strcat (&line, &line_len, q); + break; + } + + /* And descend the tree. */ + if (isdir (q)) + CVS_CHDIR (q); + free (q); + } + restore_cwd (&sdir, NULL); + free_cwd (&sdir); + + /* Now put everything we didn't find entries for back on. */ + while (q = pop (stack)) + { + if (line_len) + xrealloc_and_strcat (&line, &line_len, "/"); + xrealloc_and_strcat (&line, &line_len, q); + free (q); + } + + p = line; + + dellist (&stack); + } +#else /* !FILENAMES_CASE_INSENSITIVE */ + p = argv[i]; +#endif /* FILENAMES_CASE_INSENSITIVE */ + + send_to_server ("Argument ", 0); + + while (*p) + { + if (*p == '\n') + { + send_to_server ("\012Argumentx ", 0); + } + else if (ISDIRSEP (*p)) + { + buf[0] = '/'; + send_to_server (buf, 1); + } + else + { + buf[0] = *p; + send_to_server (buf, 1); + } + ++p; + } + send_to_server ("\012", 1); +#ifdef FILENAMES_CASE_INSENSITIVE + free (line); +#endif /* FILENAMES_CASE_INSENSITIVE */ + } + + if (flags & SEND_EXPAND_WILD) + { + int i; + for (i = 0; i < argc; ++i) + free (argv[i]); + free (argv); + } +} + + + +/* Calculate and send max-dotdot to the server */ +static void +send_max_dotdot (argc, argv) + int argc; + char **argv; +{ + int i; + int level = 0; + int max_level = 0; + + /* Send Max-dotdot if needed. */ + for (i = 0; i < argc; ++i) + { + level = pathname_levels (argv[i]); + if (level > 0) + { + if (uppaths == NULL) uppaths = getlist(); + push_string (uppaths, xstrdup (argv[i])); + } + if (level > max_level) + max_level = level; + } + + if (max_level > 0) + { + if (supported_request ("Max-dotdot")) + { + char buf[10]; + sprintf (buf, "%d", max_level); + + send_to_server ("Max-dotdot ", 0); + send_to_server (buf, 0); + send_to_server ("\012", 1); + } + else + { + error (1, 0, +"backreference in path (`..') not supported by old (pre-Max-dotdot) servers"); + } + } +} + + + +/* Send Repository, Modified and Entry. argc and argv contain only + the files to operate on (or empty for everything), not options. + local is nonzero if we should not recurse (-l option). flags & + SEND_BUILD_DIRS is nonzero if nonexistent directories should be + sent. flags & SEND_FORCE is nonzero if we should send unmodified + files to the server as though they were modified. flags & + SEND_NO_CONTENTS means that this command only needs to know + _whether_ a file is modified, not the contents. Also sends Argument + lines for argc and argv, so should be called after options are sent. */ +void +send_files (argc, argv, local, aflag, flags) + int argc; + char **argv; + int local; + int aflag; + unsigned int flags; +{ + struct send_data args; + int err; + + send_max_dotdot (argc, argv); + + /* + * aflag controls whether the tag/date is copied into the vers_ts. + * But we don't actually use it, so I don't think it matters what we pass + * for aflag here. + */ + args.build_dirs = flags & SEND_BUILD_DIRS; + args.force = flags & SEND_FORCE; + args.no_contents = flags & SEND_NO_CONTENTS; + args.backup_modified = flags & BACKUP_MODIFIED_FILES; + err = start_recursion + (send_fileproc, send_filesdoneproc, + send_dirent_proc, send_dirleave_proc, (void *) &args, + argc, argv, local, W_LOCAL, aflag, CVS_LOCK_NONE, (char *) NULL, 0, + (char *) NULL); + if (err) + error_exit (); + if (toplevel_repos == NULL) + /* + * This happens if we are not processing any files, + * or for checkouts in directories without any existing stuff + * checked out. The following assignment is correct for the + * latter case; I don't think toplevel_repos matters for the + * former. + */ + toplevel_repos = xstrdup (current_parsed_root->directory); + send_repository ("", toplevel_repos, "."); +} + +void +client_import_setup (repository) + char *repository; +{ + if (toplevel_repos == NULL) /* should always be true */ + send_a_repository ("", repository, ""); +} + +/* + * Process the argument import file. + */ +int +client_process_import_file (message, vfile, vtag, targc, targv, repository, + all_files_binary, modtime) + char *message; + char *vfile; + char *vtag; + int targc; + char *targv[]; + char *repository; + int all_files_binary; + + /* Nonzero for "import -d". */ + int modtime; +{ + char *update_dir; + char *fullname; + Vers_TS vers; + + assert (toplevel_repos != NULL); + + if (strncmp (repository, toplevel_repos, strlen (toplevel_repos)) != 0) + error (1, 0, + "internal error: pathname `%s' doesn't specify file in `%s'", + repository, toplevel_repos); + + if (strcmp (repository, toplevel_repos) == 0) + { + update_dir = ""; + fullname = xstrdup (vfile); + } + else + { + update_dir = repository + strlen (toplevel_repos) + 1; + + fullname = xmalloc (strlen (vfile) + strlen (update_dir) + 10); + strcpy (fullname, update_dir); + strcat (fullname, "/"); + strcat (fullname, vfile); + } + + send_a_repository ("", repository, update_dir); + if (all_files_binary) + { + vers.options = xmalloc (4); /* strlen("-kb") + 1 */ + strcpy (vers.options, "-kb"); + } + else + { + vers.options = wrap_rcsoption (vfile, 1); + } + if (vers.options != NULL) + { + if (supported_request ("Kopt")) + { + send_to_server ("Kopt ", 0); + send_to_server (vers.options, 0); + send_to_server ("\012", 1); + } + else + error (0, 0, + "warning: ignoring -k options due to server limitations"); + } + if (modtime) + { + if (supported_request ("Checkin-time")) + { + struct stat sb; + char *rcsdate; + char netdate[MAXDATELEN]; + + if (CVS_STAT (vfile, &sb) < 0) + error (1, errno, "cannot stat %s", fullname); + rcsdate = date_from_time_t (sb.st_mtime); + date_to_internet (netdate, rcsdate); + free (rcsdate); + + send_to_server ("Checkin-time ", 0); + send_to_server (netdate, 0); + send_to_server ("\012", 1); + } + else + error (0, 0, + "warning: ignoring -d option due to server limitations"); + } + send_modified (vfile, fullname, &vers); + if (vers.options != NULL) + free (vers.options); + free (fullname); + return 0; +} + +void +client_import_done () +{ + if (toplevel_repos == NULL) + /* + * This happens if we are not processing any files, + * or for checkouts in directories without any existing stuff + * checked out. The following assignment is correct for the + * latter case; I don't think toplevel_repos matters for the + * former. + */ + /* FIXME: "can't happen" now that we call client_import_setup + at the beginning. */ + toplevel_repos = xstrdup (current_parsed_root->directory); + send_repository ("", toplevel_repos, "."); +} + + + +static void +notified_a_file (data, ent_list, short_pathname, filename) + char *data; + List *ent_list; + char *short_pathname; + char *filename; +{ + FILE *fp; + FILE *newf; + size_t line_len = 8192; + char *line = xmalloc (line_len); + char *cp; + int nread; + int nwritten; + char *p; + + fp = open_file (CVSADM_NOTIFY, "r"); + if (getline (&line, &line_len, fp) < 0) + { + if (feof (fp)) + error (0, 0, "cannot read %s: end of file", CVSADM_NOTIFY); + else + error (0, errno, "cannot read %s", CVSADM_NOTIFY); + goto error_exit; + } + cp = strchr (line, '\t'); + if (cp == NULL) + { + error (0, 0, "malformed %s file", CVSADM_NOTIFY); + goto error_exit; + } + *cp = '\0'; + if (strcmp (filename, line + 1) != 0) + { + error (0, 0, "protocol error: notified %s, expected %s", filename, + line + 1); + } + + if (getline (&line, &line_len, fp) < 0) + { + if (feof (fp)) + { + free (line); + if (fclose (fp) < 0) + error (0, errno, "cannot close %s", CVSADM_NOTIFY); + if ( CVS_UNLINK (CVSADM_NOTIFY) < 0) + error (0, errno, "cannot remove %s", CVSADM_NOTIFY); + return; + } + else + { + error (0, errno, "cannot read %s", CVSADM_NOTIFY); + goto error_exit; + } + } + newf = open_file (CVSADM_NOTIFYTMP, "w"); + if (fputs (line, newf) < 0) + { + error (0, errno, "cannot write %s", CVSADM_NOTIFYTMP); + goto error2; + } + while ((nread = fread (line, 1, line_len, fp)) > 0) + { + p = line; + while ((nwritten = fwrite (p, 1, nread, newf)) > 0) + { + nread -= nwritten; + p += nwritten; + } + if (ferror (newf)) + { + error (0, errno, "cannot write %s", CVSADM_NOTIFYTMP); + goto error2; + } + } + if (ferror (fp)) + { + error (0, errno, "cannot read %s", CVSADM_NOTIFY); + goto error2; + } + if (fclose (newf) < 0) + { + error (0, errno, "cannot close %s", CVSADM_NOTIFYTMP); + goto error_exit; + } + free (line); + if (fclose (fp) < 0) + { + error (0, errno, "cannot close %s", CVSADM_NOTIFY); + return; + } + + { + /* In this case, we want rename_file() to ignore noexec. */ + int saved_noexec = noexec; + noexec = 0; + rename_file (CVSADM_NOTIFYTMP, CVSADM_NOTIFY); + noexec = saved_noexec; + } + + return; + error2: + (void) fclose (newf); + error_exit: + free (line); + (void) fclose (fp); +} + +static void +handle_notified (args, len) + char *args; + int len; +{ + call_in_directory (args, notified_a_file, NULL); +} + +void +client_notify (repository, update_dir, filename, notif_type, val) + const char *repository; + const char *update_dir; + const char *filename; + int notif_type; + const char *val; +{ + char buf[2]; + + send_a_repository ("", repository, update_dir); + send_to_server ("Notify ", 0); + send_to_server (filename, 0); + send_to_server ("\012", 1); + buf[0] = notif_type; + buf[1] = '\0'; + send_to_server (buf, 1); + send_to_server ("\t", 1); + send_to_server (val, 0); +} + +/* + * Send an option with an argument, dealing correctly with newlines in + * the argument. If ARG is NULL, forget the whole thing. + */ +void +option_with_arg (option, arg) + char *option; + char *arg; +{ + if (arg == NULL) + return; + + send_to_server ("Argument ", 0); + send_to_server (option, 0); + send_to_server ("\012", 1); + + send_arg (arg); +} + +/* Send a date to the server. The input DATE is in RCS format. + The time will be GMT. + + We then convert that to the format required in the protocol + (including the "-D" option) and send it. According to + cvsclient.texi, RFC 822/1123 format is preferred. */ + +void +client_senddate (date) + const char *date; +{ + char buf[MAXDATELEN]; + + date_to_internet (buf, (char *)date); + option_with_arg ("-D", buf); +} + +void +send_init_command () +{ + /* This is here because we need the current_parsed_root->directory variable. */ + send_to_server ("init ", 0); + send_to_server (current_parsed_root->directory, 0); + send_to_server ("\012", 0); +} + +#endif /* CLIENT_SUPPORT */ |