diff options
Diffstat (limited to 'src/client.c')
-rw-r--r-- | src/client.c | 5180 |
1 files changed, 5180 insertions, 0 deletions
diff --git a/src/client.c b/src/client.c new file mode 100644 index 0000000..751406b --- /dev/null +++ b/src/client.c @@ -0,0 +1,5180 @@ +/* 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. */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif /* HAVE_CONFIG_H */ + +#include "cvs.h" +#include "getline.h" +#include "edit.h" +#include "buffer.h" +#include "save-cwd.h" + +#ifdef CLIENT_SUPPORT + +# include "log-buffer.h" +# include "md5.h" + +#include "socket-client.h" +#include "rsh-client.h" + +# ifdef HAVE_GSSAPI +# include "gssapi-client.h" +# endif + +# ifdef HAVE_KERBEROS +# include "kerberos4-client.h" +# endif + + + +/* 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; + + + +static void add_prune_candidate (const char *); + +/* All the commands. */ +int add (int argc, char **argv); +int admin (int argc, char **argv); +int checkout (int argc, char **argv); +int commit (int argc, char **argv); +int diff (int argc, char **argv); +int history (int argc, char **argv); +int import (int argc, char **argv); +int cvslog (int argc, char **argv); +int patch (int argc, char **argv); +int release (int argc, char **argv); +int cvsremove (int argc, char **argv); +int rtag (int argc, char **argv); +int status (int argc, char **argv); +int tag (int argc, char **argv); +int update (int argc, char **argv); + +static size_t try_read_from_server (char *, size_t); + +static void auth_server (cvsroot_t *, struct buffer *, struct buffer *, + int, int, struct hostent *); + + + +/* This is the referrer who referred us to a primary, or write server, using + * the "Redirect" request. + */ +static cvsroot_t *client_referrer; + +/* 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; +static int +is_arg_a_parent_or_listed_dir (Node *n, void *d) +{ + char *directory = n->key; /* name of the dir sent to server */ + char *this_argv_elem = d; /* this argv element */ + + /* 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. */ + + if (!strncmp (directory, this_argv_elem, strlen (this_argv_elem))) + return 1; + + return 0; +} + + + +/* Return nonzero if this argument should not be sent to the + server. */ +static int +arg_should_not_be_sent_to_server (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, ".")) + 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 (ISSLASH (*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, 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 (NULL, NULL); + root_string = this_root->original; + } + } + + /* Now check the value for root. */ + if (root_string && current_parsed_root + && strcmp (root_string, original_parsed_root->original)) + { + /* Don't send this, since the CVSROOTs don't match. */ + return 1; + } + } + + /* 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 * +mode_to_string (mode_t mode) +{ + char 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'; + + return Xasprintf ("u=%s,g=%s,o=%s", u, g, o); +} + + + +/* + * 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 (const char *filename, const 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 */ + + const 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; + const 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 = NULL; + +/* Buffer to write to the server. */ +static struct buffer *global_to_server; + +/* Buffer used to read from the server. */ +static struct buffer *global_from_server; + + + +/* + * 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 size_t +read_line_via (struct buffer *via_from_buffer, struct buffer *via_to_buffer, + char **resultp) +{ + int status; + char *result; + size_t len; + + status = buf_flush (via_to_buffer, 1); + if (status != 0) + error (1, status, "writing to server"); + + status = buf_read_line (via_from_buffer, &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) + *resultp = result; + else + free (result); + + return len; +} + + + +static size_t +read_line (char **resultp) +{ + return read_line_via (global_from_server, global_to_server, resultp); +} +#endif /* CLIENT_SUPPORT */ + + + +#if defined(CLIENT_SUPPORT) || defined(SERVER_SUPPORT) +/* + * Zero if compression isn't supported or requested; non-zero to indicate + * a compression level to request from gzip. + */ +int gzip_level; + +/* + * 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 + +/* Whether the server asked us to force compression. */ +static bool force_gzip; + +/* + * 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; + +/* 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 (char *args, size_t len) +{ + return; +} + + + +static void +handle_error (char *args, size_t len) +{ + int something_printed; + + /* + * First there is a symbolic error code followed by a space, which + * we ignore. + */ + char *p = strchr (args, ' '); + if (!p) + { + 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 (char *args, size_t len) +{ + char *p = args; + char *q; + struct request *rq; + do + { + q = strchr (p, ' '); + if (q) + *q++ = '\0'; + for (rq = requests; rq->name; ++rq) + { + if (!strcmp (rq->name, p)) + break; + } + if (!rq->name) + /* + * 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); + for (rq = requests; rq->name; ++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); + } +} + +static void +handle_force_gzip (char *args, size_t len) +{ + force_gzip = true; +} + + + +/* Has the server told us its name since the last redirect? + */ +static bool referred_since_last_redirect = false; +static bool free_client_referrer = false; + + + +static void +handle_referrer (char *args, size_t len) +{ + TRACE (TRACE_FUNCTION, "handle_referrer (%s)", args); + client_referrer = parse_cvsroot (args); + referred_since_last_redirect = true; + free_client_referrer = true; +} + + + +/* Redirect our connection to a different server and start over. + * + * GLOBALS + * current_parsed_root The CVSROOT being accessed. + * client_referrer Used to track the server which referred us to a + * new server. Can be supplied by the referring + * server. + * free_client_referrer Used to track whether the client_referrer needs + * to be freed before changing it. + * referred_since_last_redirect + * Tracks whether the currect server told us how + * to refer to it. + * + * OUTPUTS + * current_parsed_root Updated to point to the new CVSROOT. + * referred_since_last_redirect + * Always cleared. + * client_referrer Set automatically to current_parsed_root if + * the current server did not give us a name to + * refer to it by. + * free_client_referrer Reset when necessary. + */ +static void +handle_redirect (char *args, size_t len) +{ + static List *redirects = NULL; + + TRACE (TRACE_FUNCTION, "handle_redirect (%s)", args); + + if (redirects && findnode (redirects, args)) + error (1, 0, "`Redirect' loop detected. Server misconfiguration?"); + else + { + if (!redirects) redirects = getlist(); + push_string (redirects, args); + } + + if (referred_since_last_redirect) + referred_since_last_redirect = false; + else + { + if (free_client_referrer) free (client_referrer); + client_referrer = current_parsed_root; + free_client_referrer = false; + } + + current_parsed_root = parse_cvsroot (args); + + /* We deliberately do not set ORIGINAL_PARSED_ROOT here. + * ORIGINAL_PARSED_ROOT is used by the client to determine the current root + * being processed for the purpose of looking it up in lists and such, even + * after a redirect. + * + * FIXME + * CURRENT_PARSED_ROOT should not be reset by this function. Redirects + * should be "added" to it. The REDIRECTS list should also be replaced + * by this new CURRENT_PARSED_ROOT element. This way, if, for instance, + * a multi-root workspace had two secondaries pointing to the same + * primary, then the client would not report a looping error. + * + * There is also a potential memory leak above and storing new roots as + * part of the original could help avoid it fairly elegantly. + */ + if (!current_parsed_root) + error (1, 0, "Server requested redirect to invalid root: `%s'", + args); +} + + + +/* + * 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 (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 (ISSLASH (*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 (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) 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 (const char *pathname, + void (*func) (void *, List *, const char *, const char *), + void *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); + + reposdirname_absolute = 0; + if (strncmp (reposname, toplevel_repos, strlen (toplevel_repos))) + { + 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) + 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) + { + reposdirname = xrealloc (reposdirname, 2); + reposdirname[0] = '.'; reposdirname[1] = '\0'; + } + else + *p = '\0'; + + dir_name = xstrdup (pathname); + p = strrchr (dir_name, '/'); + if (!p) + { + 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) + { + toplevel_wd = xgetcwd (); + if (!toplevel_wd) + error (1, errno, "could not get working directory"); + } + + if (CVS_CHDIR (toplevel_wd) < 0) + error (1, errno, "could not chdir to %s", toplevel_wd); + + /* Create the CVS directory at the top level if needed. The + isdir seems like an unneeded system call, but it *does* + need to be called both if the CVS_CHDIR below succeeds + (e.g. "cvs co .") or if it fails (e.g. basicb-1a in + testsuite). We only need to do this for the "." case, + since the server takes care of forcing this directory to be + created in all other cases. If we don't create CVSADM + here, the call to Entries_Open below will fail. FIXME: + perhaps this means that we should change our algorithm + below that calls Create_Admin instead of having this code + here? */ + if (/* I think the reposdirname_absolute case has to do with + things like "cvs update /foo/bar". In any event, the + code below which tries to put toplevel_repos into + CVS/Repository is almost surely unsuited to + the reposdirname_absolute case. */ + !reposdirname_absolute + && !strcmp (dir_name, ".") + && ! isdir (CVSADM)) + { + char *repo; + char *r; + + newdir = 1; + + /* If toplevel_repos doesn't have at least one character, then the + * reference to r[-1] below could be out of bounds. + */ + assert (*toplevel_repos); + + repo = xmalloc (strlen (toplevel_repos) + + 10); + strcpy (repo, toplevel_repos); + r = repo + strlen (repo); + if (r[-1] != '.' || r[-2] != '/') + strcpy (r, "/."); + + Create_Admin (".", ".", repo, NULL, NULL, 0, 1, 1); + + free (repo); + } + + 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) + /* 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")) + /* 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, NULL, NULL, 0, 0, 1); + free (repo); + + b = strrchr (dir, '/'); + if (!b) + Subdir_Register (NULL, NULL, dir); + else + { + *b = '\0'; + Subdir_Register (NULL, dir, b + 1); + *b = '/'; + } + } + + if (rdirp) + { + /* Skip the slash. */ + ++rdirp; + } + + } while (dirp); + 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")) + /* 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 = Xasprintf ("%s/%s", toplevel_repos, reposdirname); + + Create_Admin (".", ".", repo, NULL, NULL, 0, 1, 1); + if (repo != reposdirname) + free (repo); + } + + if (strcmp (cvs_cmd_name, "export")) + { + 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 (NULL, W_LOCAL, last_entries); + dellist (&dirlist); + } + } + free (reposdirname); + (*func) (data, last_entries, short_pathname, filename); + if (last_entries) + Entries_Close (last_entries); + free (dir_name); + free (short_pathname); + free (reposname); +} + + + +static void +copy_a_file (void *data, List *ent_list, const char *short_pathname, + const char *filename) +{ + char *newname; + + read_line (&newname); + +#ifdef USE_VMS_FILENAMES + { + /* Mogrify the filename so VMS is happy with it. */ + char *p; + 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 (char *args, size_t len) +{ + call_in_directory (args, copy_a_file, NULL); +} + + + +/* 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 (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"); + /* FIXME: should be doing more error checking, probably. Like using + strtoul and making sure we used up the whole line. */ + size = atoi (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) + 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, sizeof *pwrite, 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 bool 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 (char *args, size_t 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 (char *args, size_t len) +{ + if (stored_mode) + 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 (char *args, size_t len) +{ + struct timespec newtime; + if (stored_modtime_valid) + error (0, 0, "protocol error: duplicate Mod-time"); + if (get_date (&newtime, args, NULL)) + { + /* Truncate nanoseconds. */ + stored_modtime = newtime.tv_sec; + stored_modtime_valid = 1; + } + else + error (0, 0, "protocol error: cannot parse date %s", args); +} + + + +/* + * 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 (void *data_arg, List *ent_list, const char *short_pathname, + const char *filename) +{ + char *entries_line; + struct update_entries_data *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, '/'))) + error (1, 0, "bad entries line `%s' from server", entries_line); + *cp++ = '\0'; + vn = cp; + if (!(cp = strchr (vn, '/'))) + error (1, 0, "bad entries line `%s' from server", entries_line); + *cp++ = '\0'; + + ts = cp; + if (!(cp = strchr (ts, '/'))) + error (1, 0, "bad entries line `%s' from server", entries_line); + *cp++ = '\0'; + options = cp; + if (!(cp = strchr (options, '/'))) + 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) + *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; + int size; + char *buf; + char *temp_filename; + int use_gzip; + int patch_failed; + + read_line (&mode_string); + + read_line (&size_string); + if (size_string[0] == 'z') + { + use_gzip = 1; + size = atoi (size_string+1); + } + else + { + use_gzip = 0; + size = atoi (size_string); + } + 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) + { + cvs_output ("C ", 0); + cvs_output (updated_fname, 0); + cvs_output ("\n", 1); + } + failure_exit = true; + + 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) + { + free (stored_mode); + stored_mode = NULL; + } + stored_modtime_valid = 0; + stored_checksum_valid = 0; + + if (updated_fname) + { + 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) + { + 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) + { + 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. */ + md5_buffer (patchedbuf, patchedlen, checksum); + 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 = xfopen (temp_filename, + bin ? FOPEN_BINARY_WRITE : "w"); + if (fwrite (patchedbuf, sizeof *patchedbuf, 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 md5_ctx 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) + error (1, errno, "could not open %s", short_pathname); + + md5_init_ctx (&context); + while ((len = fread (buf, 1, sizeof buf, e)) != 0) + md5_process_bytes (buf, len, &context); + if (ferror (e)) + error (1, errno, "could not read %s", short_pathname); + md5_finish_ctx (&context, checksum); + + 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 = xnrealloc (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) + { + 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")) + { + char *local_timestamp; + char *file_timestamp; + + (void) time (&last_register_time); + + local_timestamp = data->timestamp; + if (!local_timestamp || 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") || vn[0] == '-') + local_timestamp = "dummy timestamp"; + else if (!local_timestamp) + { + 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 (char *args, size_t 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, &dat); +} + + + +static void +handle_new_entry (char *args, size_t 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, &dat); +} + + + +static void +handle_updated (char *args, size_t 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, &dat); +} + + + +static void +handle_created (char *args, size_t 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, &dat); +} + + + +static void +handle_update_existing (char *args, size_t 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, &dat); +} + + + +static void +handle_merged (char *args, size_t 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, &dat); +} + + + +static void +handle_patched (char *args, size_t 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, &dat); +} + + + +static void +handle_rcs_diff (char *args, size_t 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, &dat); +} + + + +static void +remove_entry (void *data, List *ent_list, const char *short_pathname, + const char *filename) +{ + Scratch_Entry (ent_list, filename); +} + + + +static void +handle_remove_entry (char *args, size_t len) +{ + call_in_directory (args, remove_entry, NULL); +} + + + +static void +remove_entry_and_file (void *data, List *ent_list, const char *short_pathname, + const 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 (char *args, size_t len) +{ + call_in_directory (args, remove_entry_and_file, NULL); +} + + + +/* Is this the top level (directory containing CVSROOT)? */ +static int +is_cvsroot_level (char *pathname) +{ + if (strcmp (toplevel_repos, current_parsed_root->directory)) + return 0; + + return !strchr (pathname, '/'); +} + + + +static void +set_static (void *data, List *ent_list, const char *short_pathname, + const char *filename) +{ + FILE *fp; + fp = xfopen (CVSADM_ENTSTAT, "w+"); + if (fclose (fp) == EOF) + error (1, errno, "cannot close %s", CVSADM_ENTSTAT); +} + + + +static void +handle_set_static_directory (char *args, size_t len) +{ + if (!strcmp (cvs_cmd_name, "export")) + { + /* Swallow the repository. */ + read_line (NULL); + return; + } + call_in_directory (args, set_static, NULL); +} + + + +static void +clear_static (void *data, List *ent_list, const char *short_pathname, + const 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 (char *pathname, size_t len) +{ + if (!strcmp (cvs_cmd_name, "export")) + { + /* 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, NULL); +} + + + +static void +set_sticky (void *data, List *ent_list, const char *short_pathname, + const 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) + { + /* 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 (char *pathname, size_t len) +{ + if (!strcmp (cvs_cmd_name, "export")) + { + /* 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, NULL); +} + + + +static void +clear_sticky (void *data, List *ent_list, const char *short_pathname, + const char *filename) +{ + if (unlink_file (CVSADM_TAG) < 0 && ! existence_error (errno)) + error (1, errno, "cannot remove %s", CVSADM_TAG); +} + + + +static void +handle_clear_sticky (char *pathname, size_t len) +{ + if (!strcmp (cvs_cmd_name, "export")) + { + /* 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, NULL); +} + + + +/* Handle the client-side support for a successful edit. + */ +static void +handle_edit_file (char *pathname, size_t len) +{ + call_in_directory (pathname, edit_file, NULL); +} + + + +static void +template (void *data, List *ent_list, const char *short_pathname, + const char *filename) +{ + char *buf = Xasprintf ("%s/%s", short_pathname, CVSADM_TEMPLATE); + read_counted_file (CVSADM_TEMPLATE, buf); + free (buf); +} + + + +static void +handle_template (char *pathname, size_t len) +{ + call_in_directory (pathname, template, NULL); +} + + + +static void +clear_template (void *data, List *ent_list, const char *short_pathname, + const char *filename) +{ + if (unlink_file (CVSADM_TEMPLATE) < 0 && ! existence_error (errno)) + error (1, errno, "cannot remove %s", CVSADM_TEMPLATE); +} + + + +static void +handle_clear_template (char *pathname, size_t len) +{ + call_in_directory (pathname, clear_template, NULL); +} + + + +struct save_dir { + char *dir; + struct save_dir *next; +}; + +struct save_dir *prune_candidates; + +static void +add_prune_candidate (const char *dir) +{ + struct save_dir *p; + + if ((dir[0] == '.' && dir[1] == '\0') + || (prune_candidates && !strcmp (dir, prune_candidates->dir))) + return; + p = xmalloc (sizeof (struct save_dir)); + p->dir = xstrdup (dir); + p->next = prune_candidates; + prune_candidates = p; +} + + + +static void +process_prune_candidates (void) +{ + struct save_dir *p; + struct save_dir *q; + + if (toplevel_wd) + { + if (CVS_CHDIR (toplevel_wd) < 0) + error (1, errno, "could not chdir to %s", toplevel_wd); + } + for (p = prune_candidates; p; ) + { + 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) + Subdir_Deregister (NULL, NULL, p->dir); + else + { + *b = '\0'; + Subdir_Deregister (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 (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) + { + /* 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 || update_dir[0] == '\0') + update_dir = "."; + + if (last_repos && !strcmp (repos, last_repos) + && last_update_dir && !strcmp (update_dir, last_update_dir)) + /* 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, ".") + && !findnode (dirs_sent_to_server, update_dir)) + { + 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 (ISSLASH (*p)) + { + buf[0] = '/'; + send_to_server (buf, 1); + } + else + { + buf[0] = *p; + send_to_server (buf, 1); + } + ++p; + } + } + send_to_server ("\012", 1); + if (supported_request ("Relative-directory")) + { + const char *short_repos = Short_Repository (repos); + send_to_server (short_repos, 0); + } + else + send_to_server (repos, 0); + send_to_server ("\012", 1); + + if (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 (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) + { + 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)) + { + send_to_server (line, 0); + nl = strchr (line, '\n'); + if (nl) + break; + } + if (!nl) + send_to_server ("\012", 1); + if (fclose (f) == EOF) + error (0, errno, "closing %s", adm_name); + } + } + free (adm_name); + if (last_repos) free (last_repos); + if (last_update_dir) 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 (const char *dir, const char *repository, + const char *update_dir_in) +{ + char *update_dir = xstrdup (update_dir_in); + + if (!toplevel_repos && repository) + { + 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) + /* 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); +} + + + +static void +notified_a_file (void *data, List *ent_list, const char *short_pathname, + const 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 = xfopen (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) + { + error (0, 0, "malformed %s file", CVSADM_NOTIFY); + goto error_exit; + } + *cp = '\0'; + if (strcmp (filename, line + 1)) + 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 = xfopen (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, sizeof *p, 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 (char *args, size_t len) +{ + call_in_directory (args, notified_a_file, NULL); +} + + + +/* The "expanded" modules. */ +static int modules_count; +static int modules_allocated; +static char **modules_vector; + +static void +handle_module_expansion (char *args, size_t len) +{ + if (!modules_vector) + { + modules_allocated = 1; /* Small for testing */ + modules_vector = xnmalloc (modules_allocated, + sizeof (modules_vector[0])); + } + else if (modules_count >= modules_allocated) + { + modules_allocated *= 2; + modules_vector = xnrealloc (modules_vector, + modules_allocated, + sizeof (modules_vector[0])); + } + modules_vector[modules_count] = xstrdup (args); + ++modules_count; +} + + + +/* Original, not "expanded" modules. */ +static int module_argc; +static char **module_argv; + +void +client_expand_modules (int argc, char **argv, int local) +{ + int errs; + int i; + + module_argc = argc; + module_argv = xnmalloc (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) free (last_repos); + last_repos = NULL; + + if (last_update_dir) free (last_update_dir); + last_update_dir = NULL; + + if (errs) + error (errs, 0, "cannot expand modules"); +} + + + +void +client_send_expansions (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 (void) +{ + 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 (char *args, size_t 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) + goto handle_error; + if (*++p != '-' + || *++p != 'k' + || *++p != ' ' + || *++p != '\'') + goto handle_error; + if (!strchr (p, '\'')) + 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 (char *args, size_t 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, sizeof *args, len, stdout); + putc ('\n', stdout); +} + + + +static void +handle_mbinary (char *args, size_t 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 = atoi (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 (char *args, size_t 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, sizeof *args, len, stderr); + putc ('\n', stderr); +} + + + +/*ARGSUSED*/ +static void +handle_f (char *args, size_t len) +{ + fflush (stderr); +} + + + +static void +handle_mt (char *args, size_t len) +{ + char *p; + char *tag = args; + char *text; + + /* See comment at handle_m for more details. */ + fflush (stderr); + + p = strchr (args, ' '); + if (!p) + text = NULL; + else + { + *p++ = '\0'; + text = p; + } + + switch (tag[0]) + { + case '+': + if (!strcmp (tag, "+updated")) + updated_seen = 1; + else if (!strcmp (tag, "+importmergecmd")) + importmergecmd.seen = 1; + break; + case '-': + if (!strcmp (tag, "-updated")) + updated_seen = 0; + else if (!strcmp (tag, "-importmergecmd")) + { + char buf[80]; + + /* Now that we have gathered the information, we can + output the suggested merge command. */ + + if (importmergecmd.conflicts == 0 + || !importmergecmd.mergetag1 + || !importmergecmd.mergetag2 + || !importmergecmd.repository) + { + error (0, 0, + "invalid server: incomplete importmergecmd tags"); + break; + } + + if (importmergecmd.conflicts == -1) + sprintf (buf, "\nNo conflicts created by this import.\n"); + else + 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) + { + 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")) + { + if (updated_fname) + { + /* 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")) + { + if (!strcmp (text, "No")) + importmergecmd.conflicts = -1; + else + importmergecmd.conflicts = atoi (text); + } + else if (!strcmp (tag, "mergetag1")) + importmergecmd.mergetag1 = xstrdup (text); + else if (!strcmp (tag, "mergetag2")) + importmergecmd.mergetag2 = xstrdup (text); + else if (!strcmp (tag, "repository")) + 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")) + printf ("\n"); + else if (!strcmp (tag, "date")) + { + char *date = format_date_alloc (text); + printf ("%s", date); + free (date); + } + else if (text) + 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("Force-gzip", handle_force_gzip, response_type_normal, + rs_optional), + RSP_LINE("Referrer", handle_referrer, response_type_normal, rs_optional), + RSP_LINE("Redirect", handle_redirect, response_type_redirect, rs_optional), + 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("Edit-file", handle_edit_file, response_type_normal, + rs_optional), + RSP_LINE("Template", handle_template, response_type_normal, + rs_optional), + RSP_LINE("Clear-template", handle_clear_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_via() computes string's length itself. + * + * Therefore, pass the real length when transmitting data that might + * contain 0's. + */ +void +send_to_server_via (struct buffer *via_buffer, const char *str, size_t len) +{ + static int nbytes; + + if (len == 0) + len = strlen (str); + + buf_output (via_buffer, 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 (via_buffer); + if (status != 0) + error (1, status, "error writing to server"); + nbytes = 0; + } +} + + + +void +send_to_server (const char *str, size_t len) +{ + send_to_server_via (global_to_server, str, len); +} + + + +/* 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( char *buf, size_t len ) +{ + int status; + size_t nread; + char *data; + + status = buf_read_data (global_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 (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 + * 0 Success + * 1 Error + * 2 Redirect + */ +int +get_server_responses (void) +{ + struct response *rs; + do + { + char *cmd; + size_t len; + + len = read_line (&cmd); + for (rs = responses; rs->name; ++rs) + if (!strncmp (cmd, rs->name, strlen (rs->name))) + { + size_t 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) + /* 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) + { + /* 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_redirect) return 2; + if (rs->type == response_type_error) return 1; + if (failure_exit) return 1; + return 0; +} + + + +static inline void +close_connection_to_server (struct buffer **to, struct buffer **from) +{ + int status; + + /* First we shut down GLOBAL_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 GLOBAL_FROM_SERVER will complete. + */ + + TRACE (TRACE_FUNCTION, "close_connection_to_server ()"); + + status = buf_shutdown (*to); + if (status != 0) + error (0, status, "shutting down buffer to server"); + buf_free (*to); + *to = NULL; + + status = buf_shutdown (*from); + if (status != 0) + error (0, status, "shutting down buffer from server"); + buf_free (*from); + *from = NULL; +} + + + +/* 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 (void) +{ + int errs = get_server_responses (); + + /* 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) + { + if (CVS_CHDIR (toplevel_wd) < 0) + error (1, errno, "could not chdir to %s", toplevel_wd); + } + + if (client_prune_dirs) + process_prune_candidates (); + + close_connection_to_server (&global_to_server, &global_from_server); + 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; +} + + + +bool +supported_request (const 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 request?"); + /* NOTREACHED */ + return 0; +} + + + +#if defined (AUTH_CLIENT_SUPPORT) || defined (SERVER_SUPPORT) || defined (HAVE_KERBEROS) || defined (HAVE_GSSAPI) + + +/* 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 (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. + */ +int +get_cvs_port_number (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; +} + + + +/* get the port number for a client to connect to based on the proxy port + * of a cvsroot_t. + */ +static int +get_proxy_port_number (const cvsroot_t *root) +{ + + if (root->proxy_port) return root->proxy_port; + + return get_port_number ("CVS_PROXY_PORT", NULL, CVS_PROXY_PORT); +} + + + +void +make_bufs_from_fds(int tofd, int fromfd, int child_pid, cvsroot_t *root, + struct buffer **to_server_p, + struct buffer **from_server_p, int is_sock) +{ +# ifdef NO_SOCKET_TO_FD + if (is_sock) + { + assert (tofd == fromfd); + *to_server_p = socket_buffer_initialize (tofd, 0, NULL); + *from_server_p = socket_buffer_initialize (tofd, 1, 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_p = fd_buffer_initialize (tofd, 0, root, false, NULL); + *from_server_p = fd_buffer_initialize (fromfd, child_pid, root, + true, NULL); + } +} +#endif /* defined (AUTH_CLIENT_SUPPORT) || defined (SERVER_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 (cvsroot_t *root, struct buffer **to_server_p, + struct buffer **from_server_p, int verify_only, + int do_gssapi) +{ + int sock; + int port_number, + proxy_port_number = 0; /* Initialize to silence -Wall. Dumb. */ + union sai { + struct sockaddr_in addr_in; + struct sockaddr addr; + } 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); + + /* if we have a proxy connect to that instead */ + if (root->proxy_hostname) + { + proxy_port_number = get_proxy_port_number (root); + hostinfo = init_sockaddr (&client_sai.addr_in, root->proxy_hostname, + proxy_port_number); + TRACE (TRACE_FUNCTION, "Connecting to %s:%d via proxy %s(%s):%d.", + root->hostname, port_number, root->proxy_hostname, + inet_ntoa (client_sai.addr_in.sin_addr), proxy_port_number); + } + else + { + hostinfo = init_sockaddr (&client_sai.addr_in, root->hostname, + port_number); + TRACE (TRACE_FUNCTION, "Connecting to %s(%s):%d.", + root->hostname, + inet_ntoa (client_sai.addr_in.sin_addr), port_number); + } + + if (connect (sock, &client_sai.addr, sizeof (client_sai)) + < 0) + error (1, 0, "connect to %s(%s):%d failed: %s", + root->proxy_hostname ? root->proxy_hostname : root->hostname, + inet_ntoa (client_sai.addr_in.sin_addr), + root->proxy_hostname ? proxy_port_number : port_number, + SOCK_STRERROR (SOCK_ERRNO)); + + make_bufs_from_fds (sock, sock, 0, root, &to_server, &from_server, 1); + + /* if we have proxy then connect to the proxy first */ + if (root->proxy_hostname) + { +#define CONNECT_STRING "CONNECT %s:%d HTTP/1.0\r\n\r\n" + /* Send a "CONNECT" command to proxy: */ + char* read_buf; + int codenum; + size_t count; + /* 4 characters for port covered by the length of %s & %d */ + char* write_buf = Xasnprintf (NULL, &count, CONNECT_STRING, + root->hostname, port_number); + send_to_server_via (to_server, write_buf, count); + + /* Wait for HTTP status code, bail out if you don't get back a 2xx + * code. + */ + read_line_via (from_server, to_server, &read_buf); + sscanf (read_buf, "%s %d", write_buf, &codenum); + + if ((codenum / 100) != 2) + error (1, 0, "proxy server %s:%d does not support http tunnelling", + root->proxy_hostname, proxy_port_number); + free (read_buf); + free (write_buf); + + /* Skip through remaining part of MIME header, recv_line + consumes the trailing \n */ + while (read_line_via (from_server, to_server, &read_buf) > 0) + { + if (read_buf[0] == '\r' || read_buf[0] == 0) + { + free (read_buf); + break; + } + free (read_buf); + } + } + + 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 (cvsroot_t *root, struct buffer *to_server, + struct buffer *from_server, int verify_only, int do_gssapi, + struct hostent *hostinfo) +{ + char *username = NULL; /* the username we use to connect */ + char no_passwd = 0; /* gets set if no password found */ + + /* Run the authorization mini-protocol before anything else. */ + if (do_gssapi) + { +# ifdef HAVE_GSSAPI + int fd = buf_get_fd (to_server); + 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) + { + no_passwd = 1; + password = scramble (""); + } + + /* Announce that we're starting the authorization protocol. */ + send_to_server_via(to_server, begin, 0); + send_to_server_via(to_server, "\012", 1); + + /* Send the data the server needs. */ + send_to_server_via(to_server, root->directory, 0); + send_to_server_via(to_server, "\012", 1); + send_to_server_via(to_server, username, 0); + send_to_server_via(to_server, "\012", 1); + send_to_server_via(to_server, password, 0); + send_to_server_via(to_server, "\012", 1); + + /* Announce that we're ending the authorization protocol. */ + send_to_server_via(to_server, end, 0); + send_to_server_via(to_server, "\012", 1); + + /* Paranoia. */ + memset (password, 0, strlen (password)); +# 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_via (from_server, to_server, &read_buf); + + if (!strcmp (read_buf, "I HATE YOU")) + { + /* 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). The + * GSSAPI case is also the case where username can be NULL + * since username is initialized in the !gssapi section. + * + * 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 ? username : "(null)"); + + /* 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"); + } + exit (EXIT_FAILURE); + } + else if (!strncmp (read_buf, "E ", 2)) + { + fprintf (stderr, "%s\n", read_buf + 2); + + /* Continue with the authentication protocol. */ + } + else if (!strncmp (read_buf, "error ", 6)) + { + 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); + exit (EXIT_FAILURE); + } + else if (!strcmp (read_buf, "I LOVE YOU")) + { + 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) */ + + + +#if defined (CLIENT_SUPPORT) || defined (SERVER_SUPPORT) +/* + * Connect to a forked server process. + */ +static void +connect_to_forked_server (cvsroot_t *root, struct buffer **to_server_p, + struct buffer **from_server_p) +{ + 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. */ + + char *command[3]; + + command[0] = (root->cvs_server + ? root->cvs_server : getenv ("CVS_SERVER")); + if (!command[0]) +# ifdef SERVER_SUPPORT + /* FIXME: + * I'm casting out the const below because I know that piped_child, the + * only function we pass COMMAND to, accepts COMMAND as a + * (char *const *) and won't alter it, and we don't alter it in this + * function. This is yucky, there should be a way to declare COMMAND + * such that this casting isn't needed, but I don't know how. If I + * declare it as (const char *command[]), the compiler complains about + * an incompatible arg 1 being passed to piped_child and if I declare + * it as (char *const command[3]), then the compiler complains when I + * assign values to command[i]. + */ + command[0] = (char *)program_path; +# else /* SERVER_SUPPORT */ + { + error( 0, 0, "You must set the CVS_SERVER environment variable when" ); + error( 0, 0, "using the :fork: access method." ); + error( 1, 0, "This CVS was not compiled with server support." ); + } +# endif /* SERVER_SUPPORT */ + + command[1] = "server"; + command[2] = NULL; + + TRACE (TRACE_FUNCTION, "Forking server: %s %s", + command[0] ? command[0] : "(null)", command[1]); + + child_pid = piped_child (command, &tofd, &fromfd, false); + if (child_pid < 0) + error (1, 0, "could not fork server process"); + + make_bufs_from_fds (tofd, fromfd, child_pid, root, to_server_p, + from_server_p, 0); +} +#endif /* CLIENT_SUPPORT || SERVER_SUPPORT */ + + + +static int +send_variable_proc (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; +} + + + +/* Open up the connection to the server and perform any necessary + * authentication. + */ +void +open_connection_to_server (cvsroot_t *root, struct buffer **to_server_p, + struct buffer **from_server_p) +{ + /* 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. */ + + TRACE (TRACE_FUNCTION, "open_connection_to_server (%s)", root->original); + + switch (root->method) + { + case pserver_method: +#ifdef AUTH_CLIENT_SUPPORT + /* Toss the return value. It will die with an error message if + * anything goes wrong anyway. + */ + connect_to_pserver (root, to_server_p, from_server_p, 0, 0); +#else /* AUTH_CLIENT_SUPPORT */ + error (0, 0, "CVSROOT is set for a pserver access method but your"); + error (1, 0, "CVS executable doesn't support it."); +#endif /* AUTH_CLIENT_SUPPORT */ + break; + + case kserver_method: +#if HAVE_KERBEROS + start_kerberos4_server (root, to_server_p, + from_server_p); +#else /* !HAVE_KERBEROS */ + error (0, 0, + "CVSROOT is set for a kerberos access method but your"); + error (1, 0, "CVS executable doesn't support it."); +#endif /* HAVE_KERBEROS */ + break; + + case gserver_method: +#ifdef HAVE_GSSAPI + /* GSSAPI authentication is handled by the pserver. */ + connect_to_pserver (root, to_server_p, from_server_p, 0, 1); +#else /* !HAVE_GSSAPI */ + error (0, 0, "CVSROOT is set for a GSSAPI access method but your"); + error (1, 0, "CVS executable doesn't support it."); +#endif /* HAVE_GSSAPI */ + break; + + case ext_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 (root, to_server_p, + from_server_p); +#endif /* NO_EXT_METHOD */ + break; + + case server_method: +#ifdef START_SERVER + { + int tofd, fromfd; + START_SERVER (&tofd, &fromfd, getcaller (), + root->username, + root->hostname, + root->directory); +# ifdef START_SERVER_RETURNS_SOCKET + make_bufs_from_fds (tofd, fromfd, 0, root, to_server_p, + from_server_p, 1); +# else /* ! START_SERVER_RETURNS_SOCKET */ + make_bufs_from_fds (tofd, fromfd, 0, root, to_server_p, + from_server_p, 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 (root, to_server_p, from_server_p); + 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; +} + + + +/* Contact the server. */ +void +start_server (void) +{ + bool rootless; + int status; + bool have_global; + + do + { + /* Clear our static variables for this invocation. */ + if (toplevel_repos) + free (toplevel_repos); + toplevel_repos = NULL; + + open_connection_to_server (current_parsed_root, &global_to_server, + &global_from_server); + setup_logfiles ("CVS_CLIENT_LOG", &global_to_server, + &global_from_server); + + /* Clear static variables. */ + if (toplevel_repos) + { + free (toplevel_repos); + toplevel_repos = NULL; + } + if (last_repos) + { + free (last_repos); + last_repos = NULL; + } + if (last_update_dir) + { + free (last_update_dir); + last_update_dir = NULL; + } + stored_checksum_valid = 0; + if (stored_mode) + { + free (stored_mode); + stored_mode = NULL; + } + + rootless = !strcmp (cvs_cmd_name, "init"); + if (!rootless) + { + send_to_server ("Root ", 0); + send_to_server (current_parsed_root->directory, 0); + send_to_server ("\012", 1); + } + + { + struct response *rs; + bool suppress_redirect = !current_parsed_root->redirect; + + send_to_server ("Valid-responses", 0); + + for (rs = responses; rs->name; ++rs) + { + if (suppress_redirect && !strcmp (rs->name, "Redirect")) + continue; + + 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 ()) + exit (EXIT_FAILURE); + + have_global = supported_request ("Global_option"); + + /* Encryption needs to come before compression. Good encryption can + * render compression useless in the other direction. + */ + 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); + initialize_kerberos4_encryption_buffers (&global_to_server, + &global_from_server); + } + 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); + initialize_gssapi_buffers (&global_to_server, + &global_from_server); + 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 */ + } + + /* Send this before compression to enable supression of the + * "Forcing compression level Z" messages. + */ + 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."); + } + + /* Compression needs to come before any of the rooted requests to + * work with compression limits. + */ + if (!rootless && (gzip_level || force_gzip)) + { + if (supported_request ("Gzip-stream")) + { + char *gzip_level_buf = Xasprintf ("%d", gzip_level); + send_to_server ("Gzip-stream ", 0); + send_to_server (gzip_level_buf, 0); + free (gzip_level_buf); + send_to_server ("\012", 1); + + /* All further communication with the server will be + compressed. */ + + global_to_server = + compress_buffer_initialize (global_to_server, 0, + gzip_level, NULL); + global_from_server = + compress_buffer_initialize (global_from_server, 1, + gzip_level, NULL); + } +#ifndef NO_CLIENT_GZIP_PROCESS + else if (supported_request ("gzip-file-contents")) + { + char *gzip_level_buf = Xasprintf ("%d", gzip_level); + send_to_server ("gzip-file-contents ", 0); + send_to_server (gzip_level_buf, 0); + free (gzip_level_buf); + 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 (client_referrer && supported_request ("Referrer")) + { + send_to_server ("Referrer ", 0); + send_to_server (client_referrer->original, 0); + send_to_server ("\012", 0); + } + + /* FIXME: I think we should still be sending this for init. */ + if (!rootless && supported_request ("Command-prep")) + { + send_to_server ("Command-prep ", 0); + send_to_server (cvs_cmd_name, 0); + send_to_server ("\012", 0); + status = get_server_responses (); + if (status == 1) exit (EXIT_FAILURE); + if (status == 2) close_connection_to_server (&global_to_server, + &global_from_server); + } + else status = 0; + } while (status == 2); + + + /* + * 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. + */ + 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 (!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) + { + int count = trace; + while (count--) 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") || !strcmp (cvs_cmd_name, "add")) + { + 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 (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); + initialize_gssapi_buffers(&global_to_server, &global_from_server); + + } + 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); +} + + + +/* Send an argument STRING. */ +void +send_arg (const char *string) +{ + const char *p = string; + + send_to_server ("Argument ", 0); + + while (*p) + { + if (*p == '\n') + send_to_server ("\012Argumentx ", 0); + else + send_to_server (p, 1); + ++p; + } + send_to_server ("\012", 1); +} + + + +/* 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 (const char *file, const char *short_pathname, Vers_TS *vers) +{ + /* File was modified, send it. */ + struct stat sb; + int fd; + unsigned char *buf; + char *mode_string; + size_t bufsize; + int bin; + + TRACE (TRACE_FUNCTION, "Sending file `%s' to server", file); + + /* Don't think we can assume fstat exists. */ + if (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 = Xasprintf ("%s.CVSBFCTMP", file); + 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); + free (tfile); + } + 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, &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; + + { + unsigned 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 = Xasprintf ("%s.CVSBFCTMP", file); + if (CVS_UNLINK (tfile) < 0) + error (0, errno, "warning: can't remove temp file %s", tfile); + free (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; +}; + +/* Deal with one file. */ +static int +send_fileproc (void *callerdat, struct file_info *finfo) +{ + struct send_data *args = 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) + filename = vers->entdata->user; + else + filename = finfo->file; + + if (vers->vn_user) + { + /* 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) + { + if (vers->ts_user && !strcmp (vers->ts_conflict, vers->ts_user)) + send_to_server ("+=", 0); + else + send_to_server ("+modified", 0); + } + send_to_server ("/", 0); + send_to_server (vers->entdata ? vers->entdata->options : vers->options, + 0); + send_to_server ("/", 0); + if (vers->entdata && vers->entdata->tag) + { + send_to_server ("T", 0); + send_to_server (vers->entdata->tag, 0); + } + else if (vers->entdata && 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) + { + /* + * 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 || args->force + || strcmp (vers->ts_conflict + ? vers->ts_conflict : vers->ts_rcs, vers->ts_user) + || (vers->ts_conflict && !strcmp (cvs_cmd_name, "diff"))) + { + 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 (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 (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; +} + + + +/* + * 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 (void *callerdat, const char *dir, const char *repository, + const char *update_dir, List *entries) +{ + struct send_data *args = 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 = Xasprintf ("%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; +} + + + +/* + * 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 (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 (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 = NULL; +#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) + { + /* 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) + { + /* 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); + 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 (ISSLASH (*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) 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 (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, &args, argc, argv, local, W_LOCAL, aflag, + CVS_LOCK_NONE, NULL, 0, NULL); + if (err) + exit (EXIT_FAILURE); + if (!toplevel_repos) + /* + * 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 (char *repository) +{ + if (!toplevel_repos) /* should always be true */ + send_a_repository ("", repository, ""); +} + + + +/* + * Process the argument import file. + */ +int +client_process_import_file (char *message, char *vfile, char *vtag, int targc, + char *targv[], char *repository, + int all_files_binary, + int modtime /* Nonzero for "import -d". */ ) +{ + char *update_dir; + char *fullname; + Vers_TS vers; + + assert (toplevel_repos); + + if (strncmp (repository, toplevel_repos, strlen (toplevel_repos))) + error (1, 0, + "internal error: pathname `%s' doesn't specify file in `%s'", + repository, toplevel_repos); + + if (!strcmp (repository, toplevel_repos)) + { + update_dir = ""; + fullname = xstrdup (vfile); + } + else + { + update_dir = repository + strlen (toplevel_repos) + 1; + + fullname = Xasprintf ("%s/%s", update_dir, vfile); + } + + send_a_repository ("", repository, update_dir); + if (all_files_binary) + vers.options = xstrdup ("-kb"); + else + vers.options = wrap_rcsoption (vfile, 1); + + if (vers.options) + { + 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 (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) + free (vers.options); + free (fullname); + return 0; +} + + + +void +client_import_done (void) +{ + if (!toplevel_repos) + /* + * 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, "."); +} + + + +void +client_notify (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 (const char *option, const char *arg) +{ + if (!arg) + 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 (const char *date) +{ + char buf[MAXDATELEN]; + + date_to_internet (buf, date); + option_with_arg ("-D", buf); +} + + + +void +send_init_command (void) +{ + /* 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); +} + + + +#if defined AUTH_CLIENT_SUPPORT || defined HAVE_KERBEROS || defined HAVE_GSSAPI + +struct hostent * +init_sockaddr (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) + { + fprintf (stderr, "Unknown host %s.\n", hostname); + exit (EXIT_FAILURE); + } + name->sin_addr = *(struct in_addr *) hostinfo->h_addr; + return hostinfo; +} + +#endif /* defined AUTH_CLIENT_SUPPORT || defined HAVE_KERBEROS + * || defined HAVE_GSSAPI + */ + +#endif /* CLIENT_SUPPORT */ |