/*
 * Implement the core of the --chmod option.
 *
 * Copyright (C) 2002 Scott Howard
 * Copyright (C) 2005-2020 Wayne Davison
 *
 * 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 3 of the License, 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.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, visit the http://fsf.org website.
 */

#include "rsync.h"
#include "itypes.h"

extern mode_t orig_umask;

#define FLAG_X_KEEP (1<<0)
#define FLAG_DIRS_ONLY (1<<1)
#define FLAG_FILES_ONLY (1<<2)

struct chmod_mode_struct {
	struct chmod_mode_struct *next;
	int ModeAND, ModeOR;
	char flags;
};

#define CHMOD_ADD 1
#define CHMOD_SUB 2
#define CHMOD_EQ  3
#define CHMOD_SET 4

#define STATE_ERROR 0
#define STATE_1ST_HALF 1
#define STATE_2ND_HALF 2
#define STATE_OCTAL_NUM 3

/* Parse a chmod-style argument, and break it down into one or more AND/OR
 * pairs in a linked list.  We return a pointer to new items on success
 * (appending the items to the specified list), or NULL on error. */
struct chmod_mode_struct *parse_chmod(const char *modestr,
				      struct chmod_mode_struct **root_mode_ptr)
{
	int state = STATE_1ST_HALF;
	int where = 0, what = 0, op = 0, topbits = 0, topoct = 0, flags = 0;
	struct chmod_mode_struct *first_mode = NULL, *curr_mode = NULL,
				 *prev_mode = NULL;

	while (state != STATE_ERROR) {
		if (!*modestr || *modestr == ',') {
			int bits;

			if (!op) {
				state = STATE_ERROR;
				break;
			}
			prev_mode = curr_mode;
			curr_mode = new_array(struct chmod_mode_struct, 1);
			if (prev_mode)
				prev_mode->next = curr_mode;
			else
				first_mode = curr_mode;
			curr_mode->next = NULL;

			if (where)
				bits = where * what;
			else {
				where = 0111;
				bits = (where * what) & ~orig_umask;
			}

			switch (op) {
			case CHMOD_ADD:
				curr_mode->ModeAND = CHMOD_BITS;
				curr_mode->ModeOR  = bits + topoct;
				break;
			case CHMOD_SUB:
				curr_mode->ModeAND = CHMOD_BITS - bits - topoct;
				curr_mode->ModeOR  = 0;
				break;
			case CHMOD_EQ:
				curr_mode->ModeAND = CHMOD_BITS - (where * 7) - (topoct ? topbits : 0);
				curr_mode->ModeOR  = bits + topoct;
				break;
			case CHMOD_SET:
				curr_mode->ModeAND = 0;
				curr_mode->ModeOR  = bits;
				break;
			}

			curr_mode->flags = flags;

			if (!*modestr)
				break;
			modestr++;

			state = STATE_1ST_HALF;
			where = what = op = topoct = topbits = flags = 0;
		}

		switch (state) {
		case STATE_1ST_HALF:
			switch (*modestr) {
			case 'D':
				if (flags & FLAG_FILES_ONLY)
					state = STATE_ERROR;
				flags |= FLAG_DIRS_ONLY;
				break;
			case 'F':
				if (flags & FLAG_DIRS_ONLY)
					state = STATE_ERROR;
				flags |= FLAG_FILES_ONLY;
				break;
			case 'u':
				where |= 0100;
				topbits |= 04000;
				break;
			case 'g':
				where |= 0010;
				topbits |= 02000;
				break;
			case 'o':
				where |= 0001;
				break;
			case 'a':
				where |= 0111;
				break;
			case '+':
				op = CHMOD_ADD;
				state = STATE_2ND_HALF;
				break;
			case '-':
				op = CHMOD_SUB;
				state = STATE_2ND_HALF;
				break;
			case '=':
				op = CHMOD_EQ;
				state = STATE_2ND_HALF;
				break;
			default:
				if (isDigit(modestr) && *modestr < '8' && !where) {
					op = CHMOD_SET;
					state =  STATE_OCTAL_NUM;
					where = 1;
					what = *modestr - '0';
				} else
					state = STATE_ERROR;
				break;
			}
			break;
		case STATE_2ND_HALF:
			switch (*modestr) {
			case 'r':
				what |= 4;
				break;
			case 'w':
				what |= 2;
				break;
			case 'X':
				flags |= FLAG_X_KEEP;
				/* FALL THROUGH */
			case 'x':
				what |= 1;
				break;
			case 's':
				if (topbits)
					topoct |= topbits;
				else
					topoct = 04000;
				break;
			case 't':
				topoct |= 01000;
				break;
			default:
				state = STATE_ERROR;
				break;
			}
			break;
		case STATE_OCTAL_NUM:
			if (isDigit(modestr) && *modestr < '8') {
				what = what*8 + *modestr - '0';
				if (what > CHMOD_BITS)
					state = STATE_ERROR;
			} else
				state = STATE_ERROR;
			break;
		}
		modestr++;
	}

	if (state == STATE_ERROR) {
		free_chmod_mode(first_mode);
		return NULL;
	}

	if (!(curr_mode = *root_mode_ptr))
		*root_mode_ptr = first_mode;
	else {
		while (curr_mode->next)
			curr_mode = curr_mode->next;
		curr_mode->next = first_mode;
	}

	return first_mode;
}


/* Takes an existing file permission and a list of AND/OR changes, and
 * create a new permissions. */
int tweak_mode(int mode, struct chmod_mode_struct *chmod_modes)
{
	int IsX = mode & 0111;
	int NonPerm = mode & ~CHMOD_BITS;

	for ( ; chmod_modes; chmod_modes = chmod_modes->next) {
		if ((chmod_modes->flags & FLAG_DIRS_ONLY) && !S_ISDIR(NonPerm))
			continue;
		if ((chmod_modes->flags & FLAG_FILES_ONLY) && S_ISDIR(NonPerm))
			continue;
		mode &= chmod_modes->ModeAND;
		if ((chmod_modes->flags & FLAG_X_KEEP) && !IsX && !S_ISDIR(NonPerm))
			mode |= chmod_modes->ModeOR & ~0111;
		else
			mode |= chmod_modes->ModeOR;
	}

	return mode | NonPerm;
}

/* Free the linked list created by parse_chmod. */
int free_chmod_mode(struct chmod_mode_struct *chmod_modes)
{
	struct chmod_mode_struct *next;

	while (chmod_modes) {
		next = chmod_modes->next;
		free(chmod_modes);
		chmod_modes = next;
	}
	return 0;
}
