/*
 *  setpwnam.c --
 *  edit an entry in a password database.
 *
 *  (c) 1994 Salvatore Valente <svalente@mit.edu>
 *  This file is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Library General Public License as
 *  published by the Free Software Foundation; either version 2 of the
 *  License, or (at your option) any later version.
 *
 *  Edited 11/10/96 (DD/MM/YY ;-) by Nicolai Langfeldt (janl@math.uio.no)
 *  to read /etc/passwd directly so that passwd, chsh and chfn can work
 *  on machines that run NIS (né YP).  Changes will not be made to
 *  usernames starting with +.
 *  
 *  This file is distributed with no warranty.
 *
 *  Usage:
 *  1) get a struct passwd * from getpwnam().
 *     You should assume a struct passwd has an infinite number of fields,
 *     so you should not try to create one from scratch.
 *  2) edit the fields you want to edit.
 *  3) call setpwnam() with the edited struct passwd.
 *
 *  A _normal user_ program should never directly manipulate
 *  /etc/passwd but use getpwnam() and (family, as well as)
 *  setpwnam().
 *
 *  But, setpwnam was made to _edit_ the password file.  For use by
 *  chfn, chsh and passwd.  _I_ _HAVE_ to read and write /etc/passwd
 *  directly.  Let those who say nay be forever silent and think about
 *  how getpwnam (and family) works on a machine running YP.
 *
 *  Added checks for failure of malloc() and removed error reporting
 *  to stderr, this is a library function and should not print on the
 *  screen, but return appropriate error codes.
 *  27-Jan-97  - poe@daimi.aau.dk
 *
 *  Thanks to "two guys named Ian".
 *
 *   $Author: poer $
 *   $Revision: 1.13 $
 *   $Date: 1997/06/23 08:26:29 $
 *
 */

#undef DEBUG

/*  because I use getpwent(), putpwent(), etc... */
#define _SVID_SOURCE

#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pwd.h>
#include <errno.h>
#include <signal.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <paths.h>

#include "setpwnam.h"

#define false 0
#define true 1

typedef int boolean;

static void pw_init(void);

/*
 *  setpwnam () --
 *	takes a struct passwd in which every field is filled in and valid.
 *	If the given username exists in the passwd file, the entry is
 *	replaced with the given entry.
 */
int 
setpwnam (struct passwd *pwd)
{
    FILE *fp = NULL, *pwf = NULL;
    int x, save_errno, fd, ret;
    boolean found;
    int oldumask;
    int namelen;
    int buflen = 256;
    int contlen;
    char *linebuf = malloc(buflen);

    if (!linebuf) return -1;

    oldumask = umask(0);   /* Create with exact permissions */

    pw_init();

    /* sanity check */
    for (x = 0; x < 3; x++) {
	if (x > 0) sleep(1);
	fd = open(PTMPTMP_FILE, O_WRONLY|O_CREAT|O_EXCL, 0644);
	if (fd == -1) {
	    umask(oldumask);
	    return -1;
	}
	ret = link(PTMPTMP_FILE, PTMP_FILE);
	unlink(PTMPTMP_FILE);
	if (ret == -1)
	    close(fd);
	else
	    break;
    }
    umask(oldumask);
    if (ret == -1) return -1;

    /* ptmp should be owned by root.root or root.wheel */
    if (chown(PTMP_FILE, (uid_t) 0, (gid_t) 0) < 0) return -1;

    /* open ptmp for writing and passwd for reading */
    fp = fdopen(fd, "w");
    if (!fp) goto fail;

    pwf = fopen(PASSWD_FILE, "r");
    if (!pwf) goto fail;

    namelen = strlen(pwd->pw_name);

    /* parse the passwd file */
    found = false;
    /* Do you wonder why I don't use getpwent? Read comments at top of file */
    while (fgets(linebuf, buflen, pwf) != NULL) {
	contlen = strlen(linebuf);
	while (linebuf[contlen-1] != '\n' && !feof(pwf)) {
	    /* Extend input buffer if it failed getting the whole line */

	    /* So now we double the buffer size */
	    buflen *= 2;

	    linebuf = realloc(linebuf, buflen);
	    if (linebuf == NULL) goto fail;

	    /* And fill the rest of the buffer */
	    if (fgets(&linebuf[contlen], buflen/2, pwf) == NULL) break;
	    contlen = strlen(linebuf);
      
	    /* That was a lot of work for nothing.  Gimme perl! */
	}

	/* Is this the username we were sent to change? */
	if (!found && linebuf[namelen] == ':' &&
	    !strncmp(linebuf, pwd->pw_name, namelen)) {
	    /* Yes! So go forth in the name of the Lord and change it! */
	    if (putpwent(pwd, fp) < 0) goto fail;
	    found = true;
	    continue;
	}
	/* Nothing in particular happened, copy input to output */
	fputs(linebuf, fp);
    }

    if (fclose(fp) < 0) goto fail;
    fp = NULL;
    close (fd);
    fd = -1;
    fclose (pwf); /* I don't think I want to know if this failed */
    pwf = NULL;

    if (!found) {
	errno = ENOENT; /* give me something better */
	goto fail;
    }

    /* we don't care if we can't remove the backup file */
    unlink(PASSWD_FILE".OLD");
    /* we don't care if we can't create the backup file */
    link(PASSWD_FILE, PASSWD_FILE".OLD");
    /* we DO care if we can't rename to the passwd file */
    if(rename(PTMP_FILE, PASSWD_FILE) < 0)
	goto fail;
    /* finally:  success */
    return 0;

fail:
    save_errno = errno;
    if (fp != NULL) fclose (fp);
    if (pwf != NULL) fclose(pwf);
    if (fd >= 0) close (fd);
    if (linebuf != NULL) free(linebuf);
    unlink(PTMP_FILE);
    errno = save_errno;
    return -1;
}

/* Set up the limits so that we're not foiled */

static void 
pw_init()
{
    struct rlimit rlim;

    /* Unlimited resource limits. */
    rlim.rlim_cur = rlim.rlim_max = RLIM_INFINITY;
    setrlimit(RLIMIT_CPU, &rlim);
    setrlimit(RLIMIT_FSIZE, &rlim);
    setrlimit(RLIMIT_STACK, &rlim);
    setrlimit(RLIMIT_DATA, &rlim);
    setrlimit(RLIMIT_RSS, &rlim);

#ifndef DEBUG
    /* Don't drop core (not really necessary, but GP's). */
    rlim.rlim_cur = rlim.rlim_max = 0;
    setrlimit(RLIMIT_CORE, &rlim);
#endif

    /* Turn off signals. */
    signal(SIGALRM, SIG_IGN);
    signal(SIGHUP, SIG_IGN);
    signal(SIGINT, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGQUIT, SIG_IGN);
    signal(SIGTERM, SIG_IGN);
    signal(SIGTSTP, SIG_IGN);
    signal(SIGTTOU, SIG_IGN);

    /* Create with exact permissions. */
    umask(0);
}
