/*
 * lsattr.c		- List 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
 * 95/02/17	- Completly rewriten to display compression information
 *		  and to behave much more like `ls'.  Useless calls to
 *		  intermediate routines that just call ioctl() were
 *		  replaced by direct calls to ioctl().  Uses less space
 *		  in the stack to store the path name when recursive
 *		  listing.  Alphabetic listing thanks to scandir().
 *		  (Antoine de Maricourt)
 * 98/12/29	- Display version/generation info when -V specified
 *		  (G M Sipe)
 */

#define _LARGEFILE64_SOURCE

#include <sys/types.h>
#include <dirent.h>
#ifdef HAVE_ERRNO_H
#include <errno.h>
#endif
#include <fcntl.h>
#ifdef HAVE_GETOPT_H
#include <getopt.h>
#else
extern int optind;
extern char *optarg;
#endif
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include <assert.h>

#include <sys/ioctl.h>
#include <sys/param.h>
#include <sys/stat.h>

#include "ext2fs/ext2_fs.h"
#include "et/com_err.h"
#include "e2p/e2p.h"  /* print_flags() */
#ifdef ENABLE_COMPRESSION
# include "e2p/ext2_meth.h" /* meth_id2name */
#endif
#include "ext2fs/ext2_ioctls.h"
#include "ext2fs/ext2_iflags.h"

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

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

static const char * program_name = "lsattr";

/* static int all; */
static int dirs_opt;
static unsigned pf_options = 0;
static int recursive;
static int generation_opt;
#ifdef ENABLE_COMPRESSION
static int nblocks_opt = 0;
static int ratio_opt = 0;
#endif
static char *path = NULL;
static size_t path_size = 1; /* Either I'm lazy or I'm safety-conscious. */

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

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

/* String used in getopt calls and in usage messages. */
#ifdef ENABLE_COMPRESSION
# define GETOPT_FLAGS "ARVadlruv"
#else
# define GETOPT_FLAGS "ARVadlv"
#endif
static void help (void)
{
	print_version_string(stdout);
	printf (_("usage: %s [-" GETOPT_FLAGS "] [files...]\n"
		"  -A         show almost every file\n"
		"  -R         recursive mode\n"
		"  -a         show every file\n"
		"  -d         directories behave like normal files\n"
		"  -l         list attributes verbosely\n"
#ifdef ENABLE_COMPRESSION
		"  -r         show the compression ratio\n"
		"  -u         show space usage for as-uncompressed and as-current forms\n"
#endif
		"  -v         display file generation number\n"
		"  --help     show this help and exit\n"
		"  --version  show program version information and exit\n"),
		program_name);
	exit (0);
}

static void usage (void)
{
	fprintf (stderr,
		_("Usage: %s [-" GETOPT_FLAGS "] [files...]\n"
		"Try `%s --help' for more information.\n"),
		program_name, program_name);
	exit (1);
}

/*
 * Used by scandir to select file names.
 */

static int select_normal (const struct dirent *d)
{
	return (d->d_name[0] != '.');
}

static int
select_almost_all (const struct dirent *d)
{
	return (d->d_name[0] != '.'
		|| (d->d_name[1] != '\0'
		&& (d->d_name[1] != '.'
		|| d->d_name[2] != '\0')));
}

#define select_all NULL

static int (*select_scope) (const struct dirent *) = select_normal;

static int list_attributes (const char * name)
{
	int fd;
	STRUCT_STAT st;
	unsigned long flags;
	unsigned long generation;
#ifdef ENABLE_COMPRESSION
	unsigned long size;
	unsigned long method = ~0ul;
	unsigned long ratio[2];
	int unpatched_kernel = 0;
#endif

	if (LSTAT (path, &st) == -1) {
		com_err (program_name, errno, _("during lstat on %s"), path);
		return -1;
	}

	/*
	 * todo: This could be improved, e.g. displaying the name but
	 * question marks for other fields.
	 */
	if ((!S_ISREG(st.st_mode)) && (!S_ISDIR(st.st_mode)))
		return 0;

	if ((fd = open (path, O_RDONLY)) == -1) {
		com_err (program_name, errno, _("opening %s"), path);
		return -1;
	}

	if (fgetflags (path, &flags) == -1) {
		com_err (program_name, errno, _("While reading flags on %s"),
			 path);
		close (fd);
		return -1;
	}

	if (generation_opt) {
		if (fgetversion (path, &generation) == -1) {
			com_err (program_name, errno,
				 _("While reading version on %s"),
				 path);
			close (fd);
			return -1;
		}
		printf ("%5lu ", generation);
	}

#ifdef ENABLE_COMPRESSION
	/*
	 * This really ought to use a libe2p function.  lsattr shouldn't call
	 * ioctl directly.
	 */
	if ((ioctl (fd, EXT2_IOC_GETCLUSTERSIZE, &size) == -1)
		|| (ioctl (fd, EXT2_IOC_GETCOMPRMETHOD, &method) == -1)) {
		assert (errno != EFAULT); /* Having three assert's gives a */
		assert (errno != EBADFD); /* finer diagnostic than one. */
		assert (errno == ENOTTY);
		/*
		 * Presumably we are running under a kernel without e2compr
		 * support.  Kernels without e2compr support pass the
		 * cluster size and compression algorithm id in the flags
		 * field.  Unfortunately, we don't know for sure whether
		 * the filesystem is written with e2compr-0.4 or e2compr-0.3
		 * (which store cluster size differently).
		 */

# define UNP_UNPATCHED 8
# define UNP_WARNED 4
# define UNP_04_FOUND 2	/* At least one file has been found where the
			 * flags word suggests e2compr-0.4 format. */
# define UNP_03_FOUND 1	/* At least one file has been found where the
			 * flags word suggests e2compr-0.3 format. */

		unpatched_kernel |= UNP_UNPATCHED;
		if ((flags & (EXT2FS_COMPR_FL | EXT2FS_COMPRBLK_FL))
			&& !(unpatched_kernel & UNP_WARNED)) {
				fprintf (stderr,
				_("%s: warning: cluster size display is "
				"unreliable on unpatched kernels.\n"),
				program_name);
			/*
			 * With unpatched kernels, we do our best, but it's
			 * still just a heuristic (e.g. if different
			 * filesystems involved, or files we see before seeing
			 * evidence of either filesystem, or files written
			 * with a more recent version of e2compr than this
			 * chattr).
			 */
			unpatched_kernel |= UNP_WARNED;
		}
		if ((unpatched_kernel & UNP_04_FOUND) || (flags & (1 << 23))) {
			unpatched_kernel |= UNP_04_FOUND;
			size = 1 << ((flags >> 23) & 7);
		} else {
			/* assume e2compr-0.4 unless size out of usual bounds */
			if ((unpatched_kernel & UNP_03_FOUND)
				|| ((size = 1 << ((flags >> 23) & 7)) < 4)
				|| (size > 32)) {
				/*
				 * Current versions of e2compr don't allow
				 * cluster sizes outside of [4, 32], so assume
				 * e2compr-0.3.
				 */
				unpatched_kernel |= UNP_03_FOUND;
				size = 1 << (((flags >> 24) & 0x03) + 2);
			}
		}
		method = (flags >> 26) & 0x1f;
		ratio_opt = 0;
	}

	/*
	 * This really ought to use a libe2p function.  lsattr shouldn't call
	 * ioctl directly.
	 */
	if ((nblocks_opt || ratio_opt)
		&& (ioctl (fd, EXT2_IOC_GETCOMPRRATIO, ratio) == -1)) {
		if (errno == ENOSYS)
			ratio[0] = ~0UL;  /* Not a regular file. */
		else {
			com_err (program_name, errno,
				_("computing ratio of %s"), name);
			close (fd);
			return -1;
		}
	}
#endif /* ENABLE_COMPRESSION */

	/*
	 * Display flags on left unless long option.
	 */
	if (!(pf_options & PFOPT_LONG))
		print_flags (stdout, flags, pf_options);

#ifdef ENABLE_COMPRESSION
	/*
	 * Display Cluster size
	 */
	if (flags & (EXT2FS_COMPR_FL | EXT2FS_COMPRBLK_FL))
		printf (" %2lu", size);
	else
		printf ("  -");

	/*
	 * Display Compression method
	 */
	if ((flags & EXT2FS_COMPR_FL))
		printf (" %-6s", meth_id2name (method));
	else
		printf (" -     ");

	/*
	 * Optionally display compression ratio and nblocks.
	 */
	if (nblocks_opt) {
		/*
		 * todo: We should behave as `du' wrt POSIXLY_CORRECT
		 * environment variable.  Of course POSIX doesn't specify
		 * lsattr behaviour, but people who get this behaviour from
		 * `du' would probably want the same behaviour here.  Same
		 * goes for e2ratio.
		 */
		if (unpatched_kernel)
			printf ("      ? %7llu ", st.st_blocks / 2);
		else {
			if (ratio[0] == ~0UL) { /* Not a regular file. */
				if (ratio_opt)
					printf ("                ");
				else
					printf (_("            N/A "));
			} else
				printf ("%7lu %7lu ", ratio[0], ratio[1]);
		}
	}

	if (ratio_opt) {
		if (ratio[0] == ~0UL)
			printf (_("    N/A "));  /* Not a regular file. */
		else
			if (ratio[0] == 0)
				printf (" 100.0%% ");  /* 0 / 0 */
			else
				printf (" %5.1f%% ", 100.0 * (double) ratio[1]
					/ ratio[0]);
	}
#endif

	if (pf_options & PFOPT_LONG) {
		printf("%-28s ", name);
		print_flags(stdout, flags, pf_options);
		fputc('\n', stdout);
	} else
		printf (" %s\n", name);

	close (fd);
	return 0;
}

static void grow_path_to(size_t needed_len)
{
	if (path_size < needed_len) {
		do
			path_size *= 2;
		while (path_size < needed_len);
		path = realloc (path, path_size);
		if (path == NULL)
			com_err (program_name, ENOMEM,
				_("while allocating %u bytes for path"),
				path_size);
        }
  }

/*
 * Show attributes for every file in a directory, and go deeper if
 * recursive.  Files are displayed in alphabetical order.
 */

enum ph {NO_PRINT_HEAD = 0, PRINT_HEAD};

static int lsattr_dir (int len, int do_print_head)
{
	STRUCT_STAT st;
	struct dirent **de;
	int i, count, retval = 0;

	if ((len > 0) && (LSTAT (path, &st) == -1)) {
		com_err (program_name, errno, _("during lstat on %s"), path);
		return -1;
	}

	if ((len != 0) && !S_ISDIR (st.st_mode))
		return 0;

	if (do_print_head)
		printf ("\n%s:\n", path);

	/*
	 *	Scan the directory and sort files in alphabetical order.
	 */

	if ((count = scandir ((len == 0 ? "." : path), &de, select_scope,
		alphasort)) < 0) {
		com_err (program_name, errno, _("scanning directory \"%s\""),
			(len == 0 ? "." : path));
		return -1;
	}

	if (len > 0 && path[len - 1] != '/')
		path[len++] = '/';

	for (i = 0; i < count; i++) {
		size_t needed_len = len + strlen (de[i]->d_name) + 1;

		grow_path_to (needed_len);
		strcpy (path + len, de[i]->d_name);
		retval |= list_attributes (de[i]->d_name);
	}

	if (recursive) {
		for (i = 0; i < count; i++) {
			if ((select_scope != select_all)
				|| select_almost_all (de[i])) {
				assert (len + strlen (de[i]->d_name) <
					path_size);
				strcpy (path + len, de[i]->d_name);
				retval |= lsattr_dir (len + strlen (de[i]->
					d_name), PRINT_HEAD);
			}
		}
	}

	/*
	 *      Free the list.
	 */
	for (i = 0; i < count; i++)
		free (de[i]);

	free (de);
	return retval;
}

int main (int argc, char ** argv)
{
	int c;
	int i;
	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();

	while ((c = getopt (argc, argv, GETOPT_FLAGS)) != EOF)
		switch (c)
		{
			case 'R':
				recursive = 1;
				break;
			case 'a':
				select_scope = select_all;
				break;
			case 'A':
				select_scope = select_almost_all;
				break;
			case 'd':
				dirs_opt = 1;
				break;
			case 'l':
				pf_options = PFOPT_LONG;
				break;
#ifdef ENABLE_COMPRESSION
			case 'r':
				ratio_opt = 1;
				break;
			case 'u':
				nblocks_opt = 1;
				break;
#endif
			case 'v':
				generation_opt = 1;
				break;
			default:
				usage();
		}

	/*
	 * Antoine was thinking that `lsattr .' shouldn't prefix everything
	 * with `./'.  I don't know what he intended to happen with `lsattr
	 * foo . bar'.  The current behaviour is that with `lsattr' (no file
	 * argument), no prefix is given; but with `lsattr .' and `lsattr
	 * foo . bar', `.' is treated as any other directory name.  I think
	 * this is the right behaviour as it is, so I haven't changed it.
	 */

	if (optind >= argc) {
		if (dirs_opt) {
			grow_path_to (2);
			strcpy (path, ".");
			retval = list_attributes (".");
		} else
			retval = lsattr_dir (0, NO_PRINT_HEAD);
	} else {
		for (i = optind; i < argc; i++) {
			grow_path_to (strlen(argv[i]) + 1);
			strcpy (path, argv[i]);
			if (dirs_opt)
				retval = list_attributes (argv[i]);
			else {
				STRUCT_STAT st;

				if (LSTAT (argv[i], &st) == -1)
					fprintf (stderr, _("%s: %s: can't "
						"lstat file (%s)\n"),
						program_name, argv[i],
						strerror (errno));
				else
					if (S_ISDIR (st.st_mode))
						retval = lsattr_dir (strlen
							(argv[i]), argc -
							optind - 1);
					else
						retval = list_attributes (
							argv[i]);
			}
		}
	}
	return (retval & 0x1);
}
