/*
 * chattr.c		- Change file attributes on an ext2 file system
 *
 * Copyright (C) 1993, 1994  Remy Card <card@masi.ibp.fr>
 *                           Laboratoire MASI, Institut Blaise Pascal
 *                           Universite Pierre et Marie Curie (Paris VI)
 *
 * This file can be redistributed under the terms of the GNU General
 * Public License
 */

/*
 * History:
 * 93/10/30	- Creation
 * 93/11/13	- Replace stat() calls by lstat() to avoid loops
 * 94/02/27	- Integrated in Ted's distribution
 * 96/09/29	- Integrated Antoine de Maricourt's e2compr changes.
 * 98/12/29	- Ignore symlinks when working recursively (G M Sipe)
 * 98/12/29	- Display version info only when -V specified (G M Sipe)
 */

#define _LARGEFILE64_SOURCE

#include <sys/types.h>
#include <dirent.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include "ext2fs/ext2_fs.h"

#ifdef __GNUC__
#define EXT2FS_ATTR(x) __attribute__(x)
#else
#define EXT2FS_ATTR(x)
#endif

#ifndef S_ISLNK			/* So we can compile even with gcc-warn */
# ifdef __S_IFLNK
#  define S_ISLNK(mode)	 __S_ISTYPE((mode), __S_IFLNK)
# else
#  define S_ISLNK(mode)  0
# endif
#endif

#include "et/com_err.h"
#include "e2p/e2p.h"
#ifdef ENABLE_COMPRESSION
# include "e2p/ext2_meth.h"
#endif
#include "ext2fs/ext2_iflags.h"
#include "ext2fs/ext2_ioctls.h"

#include "../version.h"
#include "nls-enable.h"

static const char * program_name = "chattr";

static int add;
static int rem;
static int set;
static int set_version;
#ifdef ENABLE_COMPRESSION
static unsigned long cluster_size = 0;
static unsigned compr_meth = ~0u;
#endif

static unsigned long version;

static int recursive;
static int verbose;
static int silent;

static unsigned long af;
static unsigned long rf;
static unsigned long sf;

#ifdef _LFS64_LARGEFILE
#define LSTAT		lstat64
#define STRUCT_STAT	struct stat64
#else
#define LSTAT		lstat
#define STRUCT_STAT	struct stat
#endif

#ifdef ENABLE_COMPRESSION
# define USAGE_STRING \
	"usage: %s " \
	"[-RV] [-+=AacDdEeijSsu] " \
	"[-b size] [-m name] " \
	"[-v version] files...\n" \
	"Try `--help' for more information.\n"
#else
# define USAGE_STRING \
	"usage: %s " \
	"[-RV] [-+=AaDdeijSsu] " \
	"[-v version] files...\n" \
	"Try `--help' for more information.\n"
#endif

static void usage(void)
{
	fprintf(stderr, USAGE_STRING, program_name);
	exit(1);
}

static void print_version_string(FILE *fp)
{
	fprintf (fp, _("chattr %s (%s)\n"),
		E2FSPROGS_VERSION, E2FSPROGS_DATE);
}

static void help (void)
{
	print_version_string(stdout);
	fprintf (stdout,
		_(USAGE_STRING
		"  +-=A       set/reset no-atime flag\n"
		"  +-=a       set/reset append-only flag\n"
#ifdef ENABLE_COMPRESSION
		"  +-=B       ignored; see man page\n"
		"  +-=c       set/reset compress flag\n"
#endif
		"  +-=D       set/reset synchronous directory updates\n"
		"  +-=d       set/reset no-dump flag\n"
#ifdef ENABLE_COMPRESSION
		"  +-=E       set/reset compression error flag\n"
#endif
		"  +-=e	      set/reset extent format flag\n"
		"  +-=i       set/reset immutable flag\n"
		"  +-=j       set/reset data journalling flag\n"
		"  +-=S       set/reset sync flag\n"
		"  +-=s       set/reset secure deletion flag\n"
		"  +-=u       set/reset undelete flag\n"
#ifdef ENABLE_COMPRESSION
		"  +-=X       enable or disable compression on I/O\n"
		"  +-=Z       ignored; see man page\n"
		"  -b size    set cluster size (in blocks)\n"
		"  -m name    set compression method\n"
#endif
		"  -v number  set file version\n"
		"  -R         recursive mode\n"
		"  -V         verbosely describe changes\n"
		"  --help     show this help message and exit\n"
		"  --version  show program version and exit\n"),
		program_name);

	exit (0);
}

struct flags_char {
	unsigned long	flag;
	char 		optchar;
};

static const struct flags_char flags_array[] = {
	{ EXT2_NOATIME_FL, 'A' },
	{ EXT2_APPEND_FL, 'a' },
#ifdef ENABLE_COMPRESSION
	{ -1, 'B' },
	{ EXT2_COMPR_FL, 'c' },
#endif
	{ EXT2_DIRSYNC_FL, 'D' },
	{ EXT2_NODUMP_FL, 'd' },
#ifdef ENABLE_COMPRESSION
	{ EXT2_ECOMPR_FL, 'E' },
#endif
	{ EXT4_EXTENTS_FL, 'e'},
	{ EXT2_IMMUTABLE_FL, 'i' },
	{ EXT3_JOURNAL_DATA_FL, 'j' },
	{ EXT2_SYNC_FL, 'S' },
	{ EXT2_SECRM_FL, 's' },
	{ EXT2_TOPDIR_FL, 'T' },
	{ EXT2_NOTAIL_FL, 't' },
	{ EXT2_UNRM_FL, 'u' },
#ifdef ENABLE_COMPRESSION
	{ EXT2_NOCOMPR_FL, 'X' },
	{ -1, 'Z' },
#endif
	{ 0, 0 }
};

static unsigned long get_flag(char c)
{
	const struct flags_char *fp;

	for (fp = flags_array; fp->flag != 0; fp++) {
		if (fp->optchar == c)
			return fp->flag;
	}
	return 0;
}


static int decode_arg (int * i, int argc, char ** argv)
{
	char * p;
	char * tmp;
	unsigned long fl;

	switch (argv[*i][0])
	{
	case '-':
		for (p = &argv[*i][1]; *p; p++) {
			if (*p == 'R') {
				recursive = 1;
				continue;
			}
			if (*p == 'V') {
				verbose = 1;
				continue;
			}
			if (*p == 'f') {
				silent = 1;
				continue;
			}
#ifdef ENABLE_COMPRESSION
			if (*p == 'b') {
				(*i)++;
				if (*i >= argc)
					usage ();
				cluster_size = strtol (argv[*i], &tmp, 0);

				if (*tmp || (cluster_size !=  4 &&
					     cluster_size !=  8 &&
					     cluster_size != 16 &&
					     cluster_size != 32)) {
					com_err (program_name, 0,
						 _("invalid cluster size: %s\n"),
						 argv[*i]);
					usage ();
				}
				continue;
			}

			if (*p == 'm') {
				(*i)++;
				if (*i >= argc)
					usage ();
				compr_meth = meth_name2id(argv[*i]);
				if (compr_meth == ~0u) {
					com_err (program_name, 0,
						_("unknown compression "
						"method: %s\n"), argv[*i]);
					exit (1);
				}
				continue;
			}
#endif
			if (*p == 'v') {
				(*i)++;
				if (*i >= argc)
					usage ();
				version = strtol (argv[*i], &tmp, 0);
				if (*tmp) {
					com_err (program_name, 0,
						 _("bad version - %s\n"),
						 argv[*i]);
					usage ();
				}
				set_version = 1;
				continue;
			}
			if ((fl = get_flag(*p)) == 0)
				usage();
			if (fl != -1) {
				rf |= fl;
				rem = 1;
			}
		}
		break;
	case '+':
		add = 1;
		for (p = &argv[*i][1]; *p; p++) {
			if ((fl = get_flag(*p)) == 0)
				usage();
			if (fl != -1)
				af |= fl;
		}
		break;
	case '=':
		set = 1;
		for (p = &argv[*i][1]; *p; p++) {
			if ((fl = get_flag(*p)) == 0)
				usage();
			if (fl != -1) {
				sf |= fl;
#ifdef ENABLE_COMPRESSION
				if (*p == 'E')
					rem = 1;
#endif
			}
		}
		break;
	default:
		return EOF;
		break;
	}
	return 1;
}

static int chattr_dir_proc(const char *, struct dirent *, void *);

static int change_attributes(const char * name)
{
	unsigned long flags;
	STRUCT_STAT	st;
	int fd, extent_file = 0;

	if (LSTAT (name, &st) == -1) {
		if (!silent)
			com_err (program_name, errno,
				 _("while trying to stat %s"), name);
		return -1;
	}

	/* The problem is that we mustn't close the file between
	   turning on the `c' attribute and setting the compression
	   method or cluster size.  Thus we can't use fgetflags or
	   fsetflags. */

	fd = open (name, O_RDONLY | O_NONBLOCK);
	if (fd == -1) {
		if (!silent)
			com_err (program_name, errno, _("while opening %s"),
				name);
		return -1;
	}
	if (ioctl (fd, EXT2_IOC_GETFLAGS, &flags) == -1) {
		if (!silent)
			com_err (program_name, errno,
				_("while reading flags on %s"), name);
		close (fd);
		return -1;
	}
	if (flags & EXT4_EXTENTS_FL)
		extent_file = 1;
	if (set) {
		flags = sf;
		if (extent_file && !(sf & EXT4_EXTENTS_FL)) {
			if (!silent)
				com_err(program_name, 0,
				_("Clearing extent flag not supported on %s"),
					name);
			close (fd);
			return -1;
		}
	} else {
		if (rem)
			flags &= ~rf;
		if (add)
			flags |= af;
		if (extent_file && !(flags & EXT4_EXTENTS_FL)) {
			if (!silent)
				com_err(program_name, 0,
				_("Clearing extent flag not supported on %s"),
					name);
			close (fd);
			return -1;
		}
	}
	if (set || add || rem) {
		if (verbose) {
			printf(_("Flags of %s set as "), name);
			print_flags(stdout, flags, 0);
			printf("\n");
		}
		if (!S_ISDIR(st.st_mode))
			flags &= ~EXT2_DIRSYNC_FL;
		if (ioctl (fd, EXT2_IOC_SETFLAGS, &flags) == -1) {
			if (!silent) {
				com_err(program_name, errno,
						_("while setting flags on %s"),
						name);
			}
			close(fd);
			return -1;
		}
	}
#ifdef ENABLE_COMPRESSION
	if (cluster_size != 0) {
#ifndef HAVE_EXT2_IOCTLS
		if (!silent)
			com_err (program_name, EOPNOTSUPP,
				_("while setting cluster size"));
		close(fd);
		return -1;
#else
		if (verbose)
			printf (_("Cluster size of %s set to %lu\n"), name,
				cluster_size);
		flags = cluster_size;
		if (cluster_size && (ioctl (fd,
				EXT2_IOC_SETCLUSTERSIZE, &flags) == -1) &&
				!silent)
			com_err (program_name, 0,
				_("can't set cluster size of %s"), name);
#endif
	}
	if (compr_meth != ~0u) {
#ifndef HAVE_EXT2_IOCTLS
		if (!silent)
			com_err (program_name, EOPNOTSUPP,
				_("while setting compression method"));
		close(fd);
		return -1;
#else
		if (verbose)
			printf (_("Compression method of %s set to %s\n"),
				name, meth_id2name(compr_meth));
		flags = compr_meth;
		if (ioctl (fd, EXT2_IOC_SETCOMPRMETHOD, &flags) == -1)
			if (!silent)
				com_err (program_name, 0,
					_("can't set compression method of %s"),
					name);
		/* e2compr 0.4 kernels write 1 to flags if the method was
		   recognised, 0 otherwise.  e2compr-0.3 kernels don't write
		   anything.  We try to accommodate both cases. */
		else
		if ((!flags) && compr_meth && !silent)
			fprintf (stderr, _("%s: warning: kernel doesn't "
				"recognise compression method %u.\n"),
				program_name, compr_meth);
#endif
	}
#endif /* ENABLE_COMPRESSION */
	close (fd);

	if (set_version) {
		if (verbose)
			printf (_("Version of %s set as %lu\n"), name, version);
		if (fsetversion (name, version) == -1) {
			if (!silent)
				com_err (program_name, errno,
					 _("while setting version on %s"),
					 name);
			return -1;
		}
	}
	if (S_ISDIR(st.st_mode) && recursive)
		return iterate_on_dir (name, chattr_dir_proc, NULL);
	return 0;
}

static int chattr_dir_proc (const char * dir_name, struct dirent * de,
			    void * private EXT2FS_ATTR((unused)))
{
	int ret = 0;

	if (strcmp (de->d_name, ".") && strcmp (de->d_name, "..")) {
	        char *path;

		path = malloc(strlen (dir_name) + 1 + strlen (de->d_name) + 1);
		if (!path) {
			fprintf(stderr, _("Couldn't allocate path variable "
					  "in chattr_dir_proc"));
			return -1;
		}
		sprintf(path, "%s/%s", dir_name, de->d_name);
		ret = change_attributes(path);
		free(path);
	}
	return ret;
}

int main (int argc, char ** argv)
{
	int i, j;
	int end_arg = 0;
	int err, retval = 0;

#ifdef ENABLE_NLS
	setlocale(LC_MESSAGES, "");
	setlocale(LC_CTYPE, "");
	bindtextdomain(NLS_CAT_NAME, LOCALEDIR);
	textdomain(NLS_CAT_NAME);
#endif
	if (argc && *argv)
		program_name = *argv;

	if ((argc == 2) && (strcmp (argv[1], "--version") == 0)) {
		print_version_string(stdout);
		exit (0);
	}
	if ((argc >= 2) && (strcmp (argv[1], "--help") == 0))
		help();

	i = 1;
	while (i < argc && !end_arg) {
		/* '--' arg should end option processing */
		if (strcmp(argv[i], "--") == 0) {
			i++;
			end_arg = 1;
		} else if (decode_arg (&i, argc, argv) == EOF)
			end_arg = 1;
		else
			i++;
	}
	if (i >= argc)
		usage ();
	if (set && (add || rem)) {
		fputs(_("= is incompatible with - and +\n"), stderr);
		exit (1);
	}
	if ((rf & af) != 0) {
		fputs("Can't both set and unset same flag.\n", stderr);
		exit (1);
	}
#ifdef ENABLE_COMPRESSION
	if (!(add || rem || set || set_version || cluster_size || (compr_meth
			!= ~0u))) {
		fputs(_("Must use `-b', `-m', `-v', =, - or +\n"), stderr);
#else
	if (!(add || rem || set || set_version)) {
		fputs(_("Must use '-v', =, - or +\n"), stderr);
#endif
		exit (1);
	}
	if (verbose)
		print_version_string(stderr);
	for (j = i; j < argc; j++) {
		err = change_attributes (argv[j]);
		if (err)
			retval = 1;
	}
	exit(retval);
}
