mode.c (6479B)
- // Collection of Unix tools, comparable to coreutils
- // SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
- // SPDX-License-Identifier: MPL-2.0
- #define _POSIX_C_SOURCE 200809L
- #define _XOPEN_SOURCE 700 // S_ISVTX
- #include "mode.h"
- #include "bitmasks.h"
- #include <assert.h>
- #include <errno.h>
- #include <stdbool.h>
- #include <stdint.h> // int8_t
- #include <stdio.h> // strtol
- #include <stdlib.h> // abort
- #include <string.h> // strlen
- #include <sys/stat.h> // umask
- enum perm_who_e
- {
- WHO_RST = 0, // 0b000
- WHO_USER = 1, // 0b001
- WHO_GROUP = 2, // 0b010
- WHO_OTHER = 4, // 0b100
- WHO_ALL = 7, // 0b111
- };
- enum operation_e
- {
- OP_RST,
- OP_ADD,
- OP_DEL,
- OP_SET,
- };
- struct modes
- {
- int8_t set;
- int8_t user;
- int8_t group;
- int8_t other;
- };
- struct modes_meta
- {
- enum perm_who_e dest;
- enum operation_e op;
- struct modes new_modes;
- };
- static void
- apply(struct modes_meta *meta, int8_t mode)
- {
- // get old, then reset to old. Yay to POSIX nonsense
- mode_t mask = umask(0);
- umask(mask);
- // negate to get a normal bitmask
- mask ^= 0777;
- switch(meta->op)
- {
- case OP_ADD:
- if(meta->dest == WHO_RST)
- {
- FIELD_SET(meta->new_modes.user, mode & ((mask >> 6) & 07));
- FIELD_SET(meta->new_modes.group, mode & ((mask >> 3) & 07));
- FIELD_SET(meta->new_modes.other, mode & ((mask >> 0) & 07));
- }
- if(FIELD_MATCH(meta->dest, WHO_USER)) FIELD_SET(meta->new_modes.user, mode);
- if(FIELD_MATCH(meta->dest, WHO_GROUP)) FIELD_SET(meta->new_modes.group, mode);
- if(FIELD_MATCH(meta->dest, WHO_OTHER)) FIELD_SET(meta->new_modes.other, mode);
- break;
- case OP_DEL:
- if(meta->dest == WHO_RST)
- {
- FIELD_CLR(meta->new_modes.user, mode & ((mask >> 6) & 07));
- FIELD_CLR(meta->new_modes.group, mode & ((mask >> 3) & 07));
- FIELD_CLR(meta->new_modes.other, mode & ((mask >> 0) & 07));
- }
- if(FIELD_MATCH(meta->dest, WHO_USER)) FIELD_CLR(meta->new_modes.user, mode);
- if(FIELD_MATCH(meta->dest, WHO_GROUP)) FIELD_CLR(meta->new_modes.group, mode);
- if(FIELD_MATCH(meta->dest, WHO_OTHER)) FIELD_CLR(meta->new_modes.other, mode);
- break;
- case OP_SET:
- if(meta->dest == WHO_RST)
- {
- meta->new_modes.user = mode & ((mask >> 6) & 07);
- meta->new_modes.group = mode & ((mask >> 3) & 07);
- meta->new_modes.other = mode & ((mask >> 0) & 07);
- }
- if(FIELD_MATCH(meta->dest, WHO_USER)) meta->new_modes.user = mode;
- if(FIELD_MATCH(meta->dest, WHO_GROUP)) meta->new_modes.group = mode;
- if(FIELD_MATCH(meta->dest, WHO_OTHER)) meta->new_modes.other = mode;
- // So a=rw is a=r+w rather than a=r,a=w
- meta->op = OP_ADD;
- break;
- default:
- abort();
- }
- }
- mode_t
- new_mode(const char *mode, mode_t old, const char **errstr)
- {
- // NULL? Then nothing to change :)
- if(mode == NULL) return old;
- size_t mode_len = strlen(mode);
- if(mode_len == 0) return old;
- bool symbolic = false;
- for(size_t i = 0; i < mode_len; i++)
- {
- if(mode[i] < '0' || mode[i] > '7')
- {
- if(mode[i] == '8' || mode[i] == '9')
- {
- *errstr = "contains digit outside of [0-7]";
- return old;
- }
- symbolic = true;
- break;
- }
- }
- if(!symbolic)
- {
- char *endptr = NULL;
- assert(errno == 0);
- long new = strtol(mode, &endptr, 8);
- if(errno != 0)
- {
- *errstr = strerror(errno);
- errno = 0;
- return old;
- }
- if(new < 0)
- {
- *errstr = "can't be negative";
- return old;
- }
- if(new > 07777)
- {
- *errstr = "can't be higher than 0o7777";
- return old;
- }
- return (old & 0770000) | new;
- }
- // ((^|,)[ugoa]*([+-=]|[ugo]|[rwxXst]+)+)+
- struct modes_meta meta = {.dest = WHO_RST,
- .op = OP_RST,
- .new_modes = {
- .set = (old & 07000) >> 9,
- .user = (old & 00700) >> 6,
- .group = (old & 00070) >> 3,
- .other = (old & 00007) >> 0,
- }};
- for(size_t i = 0; i < mode_len; i++)
- {
- char m = mode[i];
- switch(m)
- {
- // separator
- case ',':
- meta.dest = WHO_RST;
- meta.op = OP_RST;
- break;
- // who
- case 'u':
- if(meta.op == OP_RST)
- FIELD_SET(meta.dest, WHO_USER);
- else
- apply(&meta, meta.new_modes.user);
- break;
- case 'g':
- if(meta.op == OP_RST)
- FIELD_SET(meta.dest, WHO_GROUP);
- else
- apply(&meta, meta.new_modes.group);
- break;
- case 'o':
- if(meta.op == OP_RST)
- FIELD_SET(meta.dest, WHO_OTHER);
- else
- apply(&meta, meta.new_modes.other);
- break;
- case 'a':
- if(meta.op == OP_RST) FIELD_SET(meta.dest, WHO_ALL);
- // ignore =a, POSIX doesn't allows "a" in permcopy (would be a noop anyway)
- break;
- // op
- // Implementation-note: When another operator was set, override it
- case '+':
- meta.op = OP_ADD;
- break;
- case '-':
- meta.op = OP_DEL;
- break;
- case '=':
- meta.op = OP_SET;
- apply(&meta, 00);
- break;
- // perm
- case 'r':
- if(meta.op == OP_RST)
- {
- *errstr = "perm 'r' given without operator";
- return 0;
- }
- apply(&meta, 04);
- break;
- case 'w':
- if(meta.op == OP_RST)
- {
- *errstr = "perm 'w' given without operator";
- return 0;
- }
- apply(&meta, 02);
- break;
- case 'x':
- if(meta.op == OP_RST)
- {
- *errstr = "perm 'x' given without operator";
- return 0;
- }
- apply(&meta, 01);
- break;
- case 'X':
- if(meta.op == OP_RST)
- {
- *errstr = "perm 'X' given without operator";
- return 0;
- }
- if(S_ISDIR(old)) apply(&meta, 01);
- break;
- case 's':
- if(meta.op == OP_RST)
- {
- *errstr = "perm 's' given without operator";
- return 0;
- }
- if(FIELD_MATCH(meta.dest, WHO_USER))
- meta.new_modes.set = S_ISUID >> 9;
- else if(FIELD_MATCH(meta.dest, WHO_GROUP))
- meta.new_modes.set = S_ISGID >> 9;
- else
- meta.new_modes.set = S_ISUID >> 9;
- break;
- case 't':
- // Implementation defined behavior outside of directories and !(WHO_ALL|WHO_RST)
- if(meta.op == OP_RST)
- {
- *errstr = "perm 't' given without operator";
- return 0;
- }
- meta.new_modes.set = S_ISVTX >> 9;
- break;
- default:
- // Would need to either be non-thread-safe with a shared read-write buffer
- // or only allocate in this situation, meaning a memory leak.
- *errstr = "syntax error, got invalid character";
- return 0;
- }
- }
- // clang-format off
- return (old & 0770000)
- | ((meta.new_modes.set & 07) << 9)
- | ((meta.new_modes.user & 07) << 6)
- | ((meta.new_modes.group & 07) << 3)
- | ((meta.new_modes.other & 07) << 0);
- // clang-format on
- }