/* 
 * tclXchmod.c --
 *
 *  Chmod, chown and chgrp Tcl commands.
 *-----------------------------------------------------------------------------
 * Copyright 1991-1999 Karl Lehenbauer and Mark Diekhans.
 *
 * Permission to use, copy, modify, and distribute this software and its
 * documentation for any purpose and without fee is hereby granted, provided
 * that the above copyright notice appear in all copies.  Karl Lehenbauer and
 * Mark Diekhans make no representations about the suitability of this
 * software for any purpose.  It is provided "as is" without express or
 * implied warranty.
 *-----------------------------------------------------------------------------
 * $Id: tclXchmod.c,v 8.8 1999/03/31 06:37:43 markd Exp $
 *-----------------------------------------------------------------------------
 */

#include "tclExtdInt.h"

/*
 * Type used for returning parsed mode informtion.
 */
typedef struct {
    char  *symMode;  /* Symbolic mode. If NULL, use absolute mode. */
    int    absMode;  /* Numeric mode. */
} modeInfo_t;

static char *FILE_ID_OPT = "-fileid";

/*
 * Prototypes of internal functions.
 */
static int
ConvSymMode (Tcl_Interp  *interp,
             char        *symMode,
             int          modeVal);
static int 
TclX_ChmodObjCmd (ClientData clientData, 
                  Tcl_Interp *interp,
                  Tcl_Size objc,
                  Tcl_Obj *const *objv);

static int 
TclX_ChownObjCmd (ClientData clientData, 
                  Tcl_Interp *interp,
                  Tcl_Size objc,
                  Tcl_Obj *const *objv);

static int 
TclX_ChgrpObjCmd (ClientData clientData, 
                  Tcl_Interp *interp,
                  Tcl_Size objc,
                  Tcl_Obj *const *objv);


/*-----------------------------------------------------------------------------
 * ConvSymMode --
 *   Parse and convert symbolic file permissions as specified by chmod(C).
 *
 * Parameters:
 *   o interp - Pointer to the current interpreter, error messages will be
 *     returned in the result.
 *   o symMode - The symbolic permissions to parse.
 *   o modeVal - The existing permissions value on a file.
 *
 * Returns:
 *   The new permissions, or -1 if invalid permissions where supplied.
 *-----------------------------------------------------------------------------
 */
static int
ConvSymMode (Tcl_Interp *interp, char *symMode, int modeVal)
{
    int  user, group, other;
    char operator, *scanPtr;
    int  rwxMask, ugoMask, setUID, sticky, locking;
    int  newMode;

    scanPtr = symMode;

    while (*scanPtr != '\0') {
        user = group = other = FALSE;

        /* 
         * Scan who field.
         */
        while (! ((*scanPtr == '+') || 
                  (*scanPtr == '-') || 
                  (*scanPtr == '='))) {
            switch (*scanPtr) {
                case 'a':
                    user = group = other = TRUE;
                    break;
                case 'u':
                    user = TRUE;
                    break;
                case 'g':
                    group = TRUE;
                    break;
                case 'o':
                    other = TRUE;
                    break;
                default:
                    goto invalidMode;
            }
            scanPtr++;
        }

        /*
         * If none where specified, that means all.
         */

        if (! (user || group || other))
            user = group = other = TRUE;

        operator = *scanPtr++;

        /* 
         * Decode the permissions
         */

        rwxMask = 0;
        setUID = sticky = locking = FALSE;

        /* 
         * Scan permissions field
         */
        while (! ((*scanPtr == ',') || (*scanPtr == 0))) {
            switch (*scanPtr) {
                case 'r':
                    rwxMask |= 4;
                    break;
                case 'w':
                    rwxMask |= 2;
                    break;
                case 'x':
                    rwxMask |= 1;
                    break;
                case 's':
                    setUID = TRUE;
                    break;
                case 't':
                    sticky = TRUE;
                    break;
                case 'l':
                    locking = TRUE;
                    break;
                default:
                    goto invalidMode;
            }
            scanPtr++;
        }

        /*
         * Build mode map of specified values.
         */

        newMode = 0;
        ugoMask = 0;
        if (user) {
            newMode |= rwxMask << 6;
            ugoMask |= 0700;
        }
        if (group) {
            newMode |= rwxMask << 3;
            ugoMask |= 0070;
        }
        if (other) {
            newMode |= rwxMask;
            ugoMask |= 0007;
        }
        if (setUID && user)
            newMode |= 04000;
        if ((setUID || locking) && group)
            newMode |= 02000;
        if (sticky)
            newMode |= 01000;

        /* 
         * Add to cumulative mode based on operator.
         */

        if (operator == '+')
            modeVal |= newMode;
        else if (operator == '-')
            modeVal &= ~newMode;
        else if (operator == '=')
            modeVal |= (modeVal & ugoMask) | newMode;
        if (*scanPtr == ',')
            scanPtr++;
    }

    return modeVal;

  invalidMode:
    TclX_AppendObjResult (interp, "invalid file mode \"", symMode, "\"",
                          (char *) NULL);
    return -1;
}

/*-----------------------------------------------------------------------------
 * ChmodFileNameObj --
 *   Change the mode of a file by name.
 *
 * Parameters:
 *   o interp - Pointer to the current interpreter, error messages will be
 *     returned in the result.
 *   o modeInfo - Infomation with the mode to set the file to.
 *   o fileName - Name of the file to change.
 * Returns:
 *   TCL_OK or TCL_ERROR.
 *-----------------------------------------------------------------------------
 */
static int
ChmodFileNameObj (Tcl_Interp *interp, modeInfo_t modeInfo, Tcl_Obj *fileNameObj)
{
    char         *filePath;
    struct stat   fileStat;
    Tcl_DString   pathBuf;
    int           newMode;
    char         *fileName;

    Tcl_DStringInit (&pathBuf);

    fileName = Tcl_GetStringFromObj (fileNameObj, NULL);
    filePath = Tcl_TranslateFileName (interp, fileName, &pathBuf);
    if (filePath == NULL) {
        Tcl_DStringFree (&pathBuf);
        return TCL_ERROR;
    }

    if (modeInfo.symMode != NULL) {
        if (stat (filePath, &fileStat) != 0)
            goto fileError;
        newMode = ConvSymMode (interp, modeInfo.symMode,
                               fileStat.st_mode & 07777);
        if (newMode < 0)
            goto errorExit;
    } else {
        newMode = modeInfo.absMode;
    }
    if (TclXOSchmod (interp, filePath, (unsigned short) newMode) < 0)
        return TCL_ERROR;

    Tcl_DStringFree (&pathBuf);
    return TCL_OK;

  fileError:
    TclX_AppendObjResult (interp, filePath, ": ",
                          Tcl_PosixError (interp), (char *) NULL);
  errorExit:
    Tcl_DStringFree (&pathBuf);
    return TCL_ERROR;
}

/*-----------------------------------------------------------------------------
 * ChmodFileIdObj --
 *   Change the mode of a file by file id.
 *
 * Parameters:
 *   o interp - Pointer to the current interpreter, error messages will be
 *     returned in the result.
 *   o modeInfo - Infomation with the mode to set the file to.
 *   o fileId - The Tcl file id.
 * Returns:
 *   TCL_OK or TCL_ERROR.
 *-----------------------------------------------------------------------------
 */
static int
ChmodFileIdObj (Tcl_Interp *interp, modeInfo_t modeInfo, Tcl_Obj *fileIdObj)
{
    Tcl_Channel channel;
    struct stat fileStat;
    int         newMode;

    channel = TclX_GetOpenChannelObj (interp, fileIdObj, 0);
    if (channel == NULL) {
        return TCL_ERROR;
    }

    if (modeInfo.symMode != NULL) {
        if (TclXOSFstat (interp, channel, &fileStat, NULL) != 0)
            return TCL_ERROR;
        newMode = ConvSymMode (interp, modeInfo.symMode,
                               fileStat.st_mode & 07777);
        if (newMode < 0)
            return TCL_ERROR;
    } else {
        newMode = modeInfo.absMode;
    }
    if (TclXOSfchmod (interp, channel, (unsigned short) newMode,
                      FILE_ID_OPT) == TCL_ERROR)
        return TCL_ERROR;

    return TCL_OK;
}

/*-----------------------------------------------------------------------------
 * Tcl_ChmodObjCmd --
 *     Implements the TCL chmod command:
 *     chmod [fileid] mode filelist
 *
 * Results:
 *  Standard TCL results, may return the UNIX system error message.
 *
 *-----------------------------------------------------------------------------
 */
static int
TclX_ChmodObjCmd (ClientData clientData, Tcl_Interp *interp, Tcl_Size objc, Tcl_Obj *const *objv)
{
    Tcl_Size      objIdx, idx, fileObjc;
    int           fileIds, result;
    modeInfo_t    modeInfo;
    Tcl_Obj     **fileObjv;
    char         *fileIdsString;
    char         *modeString;
    int          modeBits;

    /*
     * Options are not parsable just looking for "-", since modes can
     * start with "-".
     */
    fileIds = FALSE;
    objIdx = 1;
    if (objc > 1) {
	fileIdsString = Tcl_GetStringFromObj (objv [objIdx], NULL);
        if (STREQU (fileIdsString, FILE_ID_OPT)) {
	    fileIds = TRUE;
	    objIdx++;
	}
    }

    if (objIdx != objc - 2)
	return TclX_WrongArgs (interp, objv [0], "[-fileid] mode filelist");

    modeString = Tcl_GetStringFromObj (objv [objIdx], NULL);
    if (modeString[0] == '0' && modeString[1] == 'd') {

      /* Allow special case for explicit decimal representation */
      if (Tcl_GetIntFromObj (interp, objv [objIdx], &modeBits) != TCL_OK) {
	return TCL_ERROR;
      }
      modeInfo.absMode = modeBits;
      modeInfo.symMode = NULL;

    } else if (ISDIGIT (modeString[0])) {

      /* parse out the octal number */
        int i = 0;
	modeBits = 0;
	while (modeString[i]) {
	    if ((!ISDIGIT(modeString[i])) || modeString[i] > '7') {
	        return TCL_ERROR;
	    }
	    modeBits = (modeBits << 3 | (0x7 & (modeString[i] - '0')));
	    i++;
	}
	modeInfo.absMode = modeBits;
        modeInfo.symMode = NULL;
	
    } else {
        /* String mode */
        modeInfo.absMode = 0;
        modeInfo.symMode = modeString;
    }

    if (Tcl_ListObjGetElements (interp, objv [objIdx + 1], &fileObjc,
                       &fileObjv) != TCL_OK)
        return TCL_ERROR;

    result = TCL_OK;
    for (idx = 0; (idx < fileObjc) && (result == TCL_OK); idx++) {
        if (fileIds) {
            result = ChmodFileIdObj (interp, modeInfo, fileObjv [idx]); 
        } else {
            result = ChmodFileNameObj (interp, modeInfo, fileObjv [idx]);
        }
    }

    return result;
}

/*-----------------------------------------------------------------------------
 * Tcl_ChownObjCmd --
 *     Implements the TCL chown command:
 *     chown [-fileid] userGrpSpec filelist
 *
 * The valid formats of userGrpSpec are:
 *   {owner}. {owner group} or {owner {}}
 * Results:
 *  Standard TCL results, may return the UNIX system error message.
 *-----------------------------------------------------------------------------
 */
static int
TclX_ChownObjCmd (ClientData clientData, Tcl_Interp *interp, Tcl_Size objc, Tcl_Obj *const *objv)
{
    Tcl_Size   objIdx, ownerObjc;
    int        fileIds;
    Tcl_Obj  **ownerObjv = NULL;
    unsigned   options;
    char      *fileIdsSwitch;
    char      *owner, *group;
    Tcl_Size   groupStrLen;


    /*
     * Parse options.
     */
    fileIds = FALSE;
    for (objIdx = 1; objIdx < objc ; objIdx++) {
	fileIdsSwitch = Tcl_GetStringFromObj (objv[objIdx], NULL);
        if (fileIdsSwitch[0] != '-')
            break;
        if (STREQU (fileIdsSwitch, FILE_ID_OPT)) {
            fileIds = TRUE;
        } else {
            TclX_AppendObjResult (interp, "Invalid option \"", fileIdsSwitch,
                                  "\", expected \"", FILE_ID_OPT, "\"",
                                  (char *) NULL);
            return TCL_ERROR;
        }
    }

    if (objIdx != objc - 2)
	return TclX_WrongArgs (interp, objv[0],
                          "[-fileid] user|{user group} filelist");
    /*
     * Parse the owner/group parameter.
     */
    if (Tcl_ListObjGetElements (interp, objv [objIdx], &ownerObjc,
				&ownerObjv) != TCL_OK)
        return TCL_ERROR;

    if ((ownerObjc < 1) || (ownerObjc > 2)) {
        TclX_AppendObjResult (interp,
                              "owner arg should be: user or {user group}",
                              (char *) NULL);
        goto errorExit;
    }
    options = TCLX_CHOWN;
    owner = Tcl_GetStringFromObj (ownerObjv [0], NULL);
    group = NULL;
    if (ownerObjc == 2) {
        options |= TCLX_CHGRP;
	group = Tcl_GetStringFromObj (ownerObjv [1], &groupStrLen);
        if (groupStrLen == 0)
            group = NULL;
    }

    /*
     * Finally, change ownership.
     */
    if (fileIds) {
        if (TclXOSFChangeOwnGrpObj (interp, options, owner, group,
				objv [objIdx + 1], "chown -fileid") != TCL_OK)
            goto errorExit;
    } else {
        if (TclXOSChangeOwnGrpObj (interp, options, owner, group,
			       objv [objIdx + 1], "chown") != TCL_OK)
            goto errorExit;
    }

    return TCL_OK;

  errorExit:
    return TCL_ERROR;
}

/*-----------------------------------------------------------------------------
 * Tcl_ChgrpObjCmd --
 *     Implements the TCL chgrp command:
 *     chgrp [-fileid] group filelist
 *
 * Results:
 *  Standard TCL results, may return the UNIX system error message.
 *
 *-----------------------------------------------------------------------------
 */
static int
TclX_ChgrpObjCmd (ClientData clientData, Tcl_Interp *interp, Tcl_Size objc, Tcl_Obj *const *objv)
{
    int        objIdx, fileIds;
    char      *fileIdsSwitch, *groupString;

    fileIds = FALSE;
    for (objIdx = 1; objIdx < objc; objIdx++) {
	fileIdsSwitch = Tcl_GetStringFromObj (objv [objIdx], NULL);
        if (fileIdsSwitch[0] != '-')
            break;
        if (STREQU (fileIdsSwitch, FILE_ID_OPT)) {
            fileIds = TRUE;
        } else {
            TclX_AppendObjResult (interp, "Invalid option \"", fileIdsSwitch,
                                  "\", expected \"", FILE_ID_OPT, "\"",
                                  (char *) NULL);
            return TCL_ERROR;
        }
    }

    if (objIdx != objc - 2)
	return TclX_WrongArgs (interp, objv [0], "[-fileid] group filelist");

    groupString = Tcl_GetStringFromObj (objv [objIdx], NULL);
    
    if (fileIds) {
        if (TclXOSFChangeOwnGrpObj (interp, TCLX_CHGRP, NULL, groupString,
				objv [objIdx + 1], "chgrp - fileid") != TCL_OK)
            goto errorExit;
    } else {
        if (TclXOSChangeOwnGrpObj (interp, TCLX_CHGRP, NULL, groupString,
			       objv [objIdx + 1], "chgrp") != TCL_OK)
            goto errorExit;
    }

    return TCL_OK;

  errorExit:
    return TCL_ERROR;
}


/*-----------------------------------------------------------------------------
 * TclX_ChmodInit --
 *     Initialize the chmod, chgrp and chown commands.
 *-----------------------------------------------------------------------------
 */
void
TclX_ChmodInit (Tcl_Interp *interp)
{
    Tcl_CreateObjCommand2 (interp, 
			  "chgrp",
			  TclX_ChgrpObjCmd,
                          (ClientData) NULL,
			  (Tcl_CmdDeleteProc*) NULL);

    Tcl_CreateObjCommand2 (interp,
			  "chmod",
			  TclX_ChmodObjCmd,
                          (ClientData) NULL,
			  (Tcl_CmdDeleteProc*) NULL);

    Tcl_CreateObjCommand2 (interp,
                          "chown",
			  TclX_ChownObjCmd,
                          (ClientData) NULL,
			  (Tcl_CmdDeleteProc*) NULL);
}

/* vim: set ts=4 sw=4 sts=4 et : */
