[comp.os.minix] Full V7 chmod for Minix

jaime@killer.UUCP (10/03/87)

I have written a chmod for Minix that interprets the symbolic parameters 
supported by UNIX chmod and used extensively in shar files.  The source 
and a Minix-style man page are included at the end of this posting. 

I tried to implement chmod as described in the 7th Edition Unix 
Programmers Manual, Volume 1, published by Holt, Rinehart and Wilson in 
1983.  (You know; that big blue book with the children's block letters 
spelling out UNIX on the front)  Since I don't have access to an actual 
V7 Unix implementation, I don't know if I've duplicated all the quirks. 

I was able to test my chmod on a 3B2 running System V, and a Sun running 
Sun OS 4.2 Release 3.2.  It works fine as far as I can tell.  Two 
differences: 

1.  System V chmod ignores the current umask in all cases.  Version 7 
(and Berkeley) look at the umask if you leave off a <who> specification.  
For example: 'chmod +w file' will have no effect if the umask is 0222.  
I followed the V7 convention. 

2.  I took the manual literally when it said, concerning <who>, that 
"The letter 'a' stands for 'ugo'.  If <who> is omitted, the default is 
'a' ...".  Therefore, all the following should be equivalent (except for 
the umask business): 
                ugo+s
                a+s
                +s
My chmod treats all the above equally, setting the SETUID and SETGID 
bits in each case.  Both the Berkeley and System V chmods took 'ugo+s' 
but ignored 'a+s' and '+s'.  Since this inconsistent quirk (bug?) 
(feature?) exists in both 4BSD and SYSV, I imagine it comes from V7 
chmod.  Anyone with a real V7 care to see if 'chmod +s file' does 
anything? 

                                - James da Silva

------------------------------- chmod.man ------------------------------

Command:  chmod - change access mode for files
Syntax:   chmod octal-number files
          chmod [augo][+-=][rwxst] files
Flags:    (none)
Examples: chmod 755 file            # Owner: rwx Group: r-x Others: r-x
          chmod +x file1 file2      # Make file1 and file2 executable
          chmod a-w file            # Make file read only
          chmod u+s file            # Turn on SETUID for file
          chmod +t file             # sticky - file stays in swap space (haha)
          chmod g=u                 # group perms set to same as user perms.

The given mode is applied to each file in the file list.  The mode can be 
either absolute or symbolic.  Absolute modes are given as an octal number
that represents the new file mode.  The mode bits are defined as follows: 

        4000    Set effective user id on execution to file's owner id
        2000    Set effective group id on execution to file's group id

        0400    file is readable by the owner of the file
        0200    writeable by owner
        0100    executable by owner

        0070    same as above, for other users in the same group
        0007    same as above, for all other users

Symbolic modes modify the current file mode in a specified way.  They take
the form: 

        [who] op permissions { op permissions }

The possibilities for [who] are 'u', 'g', 'o', and 'a'; standing for user, 
group, other and all, respectively.  If who is omitted, 'a' is assumed, but 
the current umask is used.  The op can be '+', '-', or '=';  '+' turns on the 
given permissions, '-' turns them off; '=' sets the permissions exclusively
for the given who.  For example 'g=x' sets the group permissions to '--x'. 

The possible permissions are 'r', 'w', 'x'; which stand for read, write, and 
execute permission;  's' turns on the set effective user/group id bits.  'u', 
'g' and 'o' in the permissions field stand for the current user, group, or
other permission bits, respectively.  Only one of these may be used at a time.
The 't' permission is included for compatibility with 7th Edition Unix.  It
normally sets the sticky bit, to keep a file in the swap space after
execution, but it has no effect in Minix. 's' only makes sense with 'u' and 
'g'; 'o+s' is harmless and does nothing. 

Multiple symbolic modes may be specified, separated by commas.

-------------------------------- chmod.c -------------------------------
/*
 *  chmod.c             Author: James da Silva
 *                              (ihnp4!killer!jaime)
 *
 *  a (hopefully) 7th Edition Unix compatible chmod for Minix.
 */

#include <stat.h>               /* for Minix */

/* #include <sys/types.h>       /* for Berkeley, System V */
/* #include <sys/stat.h>        /* (and Version 7 ?)      */

#define isop(c)         ((c=='+')||(c=='-')||(c=='='))
#define isperm(c)       ((c=='r')||(c=='w')||(c=='x')||(c=='s')||(c=='t')||\
                         (c=='u')||(c=='g')||(c=='o'))

/* the bits associated with user, group, other */

#define U_MSK   (0700 | S_ISUID)
#define G_MSK   (0070 | S_ISGID)
#define O_MSK    0007

typedef unsigned short bitset;  /* type used for modes */

struct stat st;         /* structure returned by stat() */
char *pname, *arg;
bitset newmode, absolute(), symbolic();
int isabsolute;

main(argc, argv)
int argc;
char **argv;
{
    int i;

    pname = *(argv++);
    if(argc < 3) usage();

    arg = *argv;        /* save pointer to mode arg */

    /* check for octal mode */

    if( isabsolute = ((*arg>='0') && (*arg<='7')) )
        newmode = absolute();

    /* apply the mode to all files listed */

    for(i=2; i<argc; i++)
    {
        argv++;
        if(stat(*argv, &st))    /* get current file mode */
        {
            printf("%s: cannot find `%s'\n", pname, *argv);
            exit(1);
        }

        /* calculate new mode for this file */

        if( !isabsolute )
            newmode = symbolic(st.st_mode);

        if(chmod(*argv, newmode))  /* change the mode */
        {
            printf("%s: cannot chmod `%s'\n", pname, *argv);
            exit(1);
        }
    }
}


/*
 * absolute
 *
 * Interprets an octal mode.
 * The file modes will be set to this value.
 */
bitset absolute()
{
    bitset m;
    char *s;

    m = 0;
    s = arg;

    /* convert octal string to integer */

    while((*s>='0') && (*s<='7'))
        m = m * 8 + (*(s++) - '0');

    /* if something else is there, choke */

    if(*s) badmode(s);

    return m;
}


/*
 * symbolic
 *
 * Processes symbolic mode of the form (in EBNF):
 *      <symbolic> ::= <pgroup> { ',' <pgroup> }.
 *      <pgroup> ::= [ <who> ] <op> <permissions> { <op> <permissions> }.
 *
 *      <who> ::= <whoch> { <whoch> }.
 *      <whoch> ::= 'a' | 'u' | 'g' | 'o'.
 *
 *      <op> ::= '+' | '-' | '='.
 *
 *      <permissions> ::= <permch> { <permch> }.
 *      <permch> ::= 'r' | 'w' | 'x' | 's' | 't' | 'u' | 'g' | 'o'.
 *
 * If <who> is omitted, 'a' is assumed, BUT umask()ed bits are uneffected.
 * If <op> is '=', all unspecified permissions are turned off for this <who>.
 * For permissions 'u', 'g', and 'o', the permissions are taken from the
 * specified set.  i.e.  o=g sets the permissions for other the same as for
 * group.
 *
 * Pain in the duff, isn't it?
 */
bitset symbolic(mode)
bitset mode;
{
    int g,o,u, haswho, haspcopy;
    bitset u_mask, emask, partial, other, applyop();
    char *s, c, op;

    s = arg;
    u_mask = umask(0);  /* get the umasked bits */

    do      /* pgroup */
    {
        haswho = u = g = o = 0;

        while(!isop(*s))
        {
            /* we must have a 'who' then */
            haswho = 1;
            switch(*s)
            {
                case 'a': u=g=o=1; break;
                case 'u': u = 1;   break;
                case 'g': g = 1;   break;
                case 'o': o = 1;   break;

                default: badmode(s);
            }
            s++;
        }

        if(!haswho)
        {
            u=g=o=1;            /* assume all */
            emask = ~u_mask;    /* effective umask */
        }
        else emask = ~0;


        /* process each given operator */

        while(isop(*s))
        {
            op = *(s++);
            other = partial = haspcopy = 0;

            /* collect the specified permissions */

            while(isperm(*s))
            {

                /* Berkeley only allows one of 'u' 'g' or 'o' as permissions */

                if((*s=='u')||(*s=='g')||(*s=='o'))
                    if( haspcopy ) badmode(s);
                    else haspcopy = 1;

                switch(*s)
                {
                    case 'r': partial |= 4; break;
                    case 'w': partial |= 2; break;
                    case 'x': partial |= 1; break;

                    case 'u': partial |= (mode & U_MSK &~S_ISUID) >> 6;
                              other |= mode & S_ISUID;
                              break;
                    case 'g': partial |= (mode & G_MSK &~S_ISGID) >> 3;
                              other |= mode & S_ISGID;
                              break;
                    case 'o': partial |= (mode & O_MSK);
                              break;


                    case 't': other |= S_ISVTX; break;

                    case 's': if(u) other |= S_ISUID;
                              if(g) other |= S_ISGID;
                              break;

                    default: badmode(s);
                }
                s++;
            }

            /* apply the op using the affected bits and masks */

            if(u)
                mode = applyop(mode,op,(other | (partial << 6)),emask,U_MSK);
            if(g)
                mode = applyop(mode,op,(other | (partial << 3)),emask,G_MSK);
            if(o)
                mode = applyop(mode,op,(other | partial),emask,O_MSK);
        }

    } while(*(s++) == ',');

    /* not at end - choke */

    if(*(--s)) badmode(s);

    return mode;
}


/*
 * applyop
 *
 * applies the operator to the current mode using the specified bitset
 * and mask.  'bits' will contain 1's in every bit affected by the
 * operator '+', '-', or '='.  In the case of '=', msk is used to
 * determine which bits will be forced off. 'emask' is the effective
 * umask.
 */
bitset applyop(mode, op, bits, emask, msk)
char op;
bitset mode, bits, emask, msk;
{
    switch(op)
    {
        case '+': mode |= bits & emask;      /* turn these bits on */
                  break;
        case '-': mode &= ~(bits & emask);   /* turn these off */
                  break;
        case '=': mode |= bits & emask;      /* turn these bits on */
                  mode &= ~(~bits & msk & emask);    /* others off */
                  break;
        default: /* should never get here (famous last words) */
                printf("%s: panic: bad op `%c' passed\n", pname, op);
    }
    return mode;
}


/*
 * usage
 *
 * Prints a terse usage message and exits.
 */
usage()
{
    printf("Usage: %s [absolute-mode | symbolic-mode] files\n", pname);
    exit(1);
}


/*
 * badmode
 *
 * Called when the parser chokes on the given mode.
 * Prints a message showing the offending character and exits.
 */
badmode(s)
char *s;
{
    int i,sp;
    char buffer[80], *bp;

    sp = s - arg + strlen(pname) + 21;
    sp = sp > 79 ? 79 : sp;          /* check for buffer overflow */

    for(i=0, bp = buffer; i<sp; i++, bp++) *bp = ' ';
    *bp = '\0';

    printf("%s: badly formed mode `%s'\n", pname, arg);
    printf("%s^\n", buffer);
    exit(1);
}

------------------------------------------------------------------------
James A. da Silva                             14102 Bramble Court #203
ihnp4!killer!jaime                            Laurel, Maryland 20708