/*
 * QUOTA    An implementation of the diskquota system for the LINUX operating
 *          system. QUOTA is implemented using the BSD systemcall interface
 *          as the means of communication with the user level. Should work for
 *          all filesystems because of integration into the VFS layer of the
 *          operating system. This is based on the Melbourne quota system wich
 *          uses both user and group quota files.
 * 
 *          Program to check disk quotas.
 * 
 * Authors:
 *          Disk reading routines: Edvard Tuinder <ed@ow.org>
 *          Quota storing routines: Marco van Wieringen <mvw@mcs.ow.org>
 *
 *          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 2 of
 *          the License, or (at your option) any later version.
 */

/* NOTE: This is a PRE-ALPHA version !

   This version of quotacheck is based on the `hints' of Remy Card and has
   *NOT* been fully tested. It *SHOULD* not be able to destroy any data,
   but *ANYTHING* may happen.

   Comments please to ed@ow.org.
 */

#include <sys/types.h>
#include <sys/param.h>
#include <dirent.h>
#include <sys/stat.h>
#include <stdio.h>
#include <linux/quota.h>
#include <stdarg.h>
#include <sys/file.h>
#include <mntent.h>
#include <getopt.h>
#include <limits.h>
#include <unistd.h>
#include <stdlib.h>
#include <e2fsck.h>

#define DEF_BLOCKSIZE 1024
#define NODQUOT (struct dquot *)NULL

#ifndef lint
static char RCS_checkquota[] = "$Id: quotacheck.c,v 2.6 1995/02/18 19:57:20 ed Exp root $";
#endif

struct dquot {
   int          dq_id;  /* id this applies to (uid, gid) */
   struct dqblk dq_dqb; /* diskquota for id */
   struct dquot *next;  /* pointer to next id */
};

struct dlinks {
   ino_t ino;
   size_t size;
   size_t blksize;
   u_long id;
   struct dlinks *next;
};

struct dirs {
   char *dir_name;
   struct dirs *next;
};

void add_to_quota(struct stat *, int);
void dump_to_file(char *, char *, int);
void store_dlinks(struct stat *, int);
void remove_list(void);
void scan_dir(char *);
void scan_fs(char *);
void add_to_quota_ext2(int, long, int);
void abort_now(const char *, const char *, ...);
void add_dlinks(u_long *, u_long *, int, int);

static char bits[] = "|/-\\";
#define BITS_SIZE 4 /* sizeof(bits) == 5 */

dev_t cur_dev;
char dflag = 0, vflag = 0;
char check_usr, check_grp;
long files_done, dirs_done;
u_long highestid[MAXQUOTAS];
char *quotatypes[] = INITQFNAMES;
char log_buf[16384]; /* for dflag stderr buffering */
#ifdef DEBUG_MALLOC
static size_t malloc_mem = 0;
static size_t free_mem = 0;
#endif

struct dquot *dquot_list[MAXQUOTAS] = {NODQUOT, NODQUOT};
struct dquot *mru_dquot[MAXQUOTAS] = {NODQUOT, NODQUOT};
struct dlinks *stored_links[MAXQUOTAS] = {(struct dlinks *)NULL, (struct dlinks *)NULL};

/*
 * This simple algorithm calculates the size of a file in blocks.
 * It is not perfect but works most of the time.
 */
static inline size_t isize_to_blocks(size_t isize, size_t blksize)
{
   u_long blocks;
   u_long indirect;

   if (!blksize)
      blksize = DEF_BLOCKSIZE;

   blocks = (isize / blksize) + ((isize % blksize) ? 1 : 0);
   if (blocks > 10) {
      indirect = ((blocks - 11) >> 8) + 1; /* single indirect blocks */
      if (blocks > (10 + 256)) {
         indirect += ((blocks - 267) >> 16) + 1; /* double indirect blocks */
         if (blocks > (10 + 256 + (256 << 8)))
            indirect++; /* triple indirect blocks */
      }
      blocks += indirect;
   }
   return blocks;
}

/*
 * Ok check each memory allocation.
 */
void *xmalloc(size_t size)
{
   void *ptr;

#ifdef DEBUG_MALLOC
   malloc_mem += size;
#endif
   ptr = malloc(size);
   if (ptr == (void *)NULL)
      abort_now("xmalloc", "Virtual memory exhausted\n");
   memset(ptr, 0, size);
   return(ptr);
}

/*
 * Do a lookup of a type of quota for a specific id. Use short cut with
 * most recently used dquot struct pointer.
 */
static inline struct dquot *lookup_dquot(int id, int type)
{
   register struct dquot *lptr = NODQUOT;

   /*
    * First fast lookup when same as used before.
    */
   if (mru_dquot[type] != NODQUOT && mru_dquot[type]->dq_id == id)
      return (mru_dquot[type]);

   for (lptr = dquot_list[type]; lptr != NODQUOT; lptr = lptr->next)
      if (lptr->dq_id == id) {
         mru_dquot[type] = lptr;
         return (lptr);
      }
   return(NODQUOT);
}

/*
 * Add a new dquot for a new id to the list.
 */
static inline struct dquot *add_dquot(int id, int type)
{
   register struct dquot *lptr;

   if (dflag)
      fprintf(stderr, "Adding dquot structure type %s for %d\n",
	      quotatypes[type], id);

   lptr = (struct dquot *)xmalloc(sizeof(struct dquot));

   lptr->dq_id = id;
   lptr->next = dquot_list[type];
   dquot_list[type] = lptr;
   lptr->dq_btime = lptr->dq_itime = (time_t) 0;

   if (id > highestid[type])
      highestid[type] = id;

   return(lptr);
}

/*
 * Check the programs arguments for a specific target.
 */
int oneof(char *target, char *list[], int cnt)
{
   register int i;

   for (i = 0; i < cnt; i++)
      if (strcmp(target, list[i]) == 0)
         return (i);
   return(-1);
}

/*
 * Show a blitting cursor as means of visual progress indicator.
 */
static void blit()
{
   static short bitc = 0;

   putc(bits[bitc], stdout);
   putc('\b', stdout);
   bitc++;
   bitc %= BITS_SIZE;
}

void usage()
{
   fputs("Usage:\n\tquotacheck [-g] [-u] [-vd] -a\n", stderr);
   fputs("\tquotacheck [-g] [-u] [-vd] filesys ...\n", stderr);
   exit(1);
}

int main(int argc, char **argv)
{
   FILE *fp;
   int cnt, ch;
   struct stat st;
   char aflag = 0, gflag = 0, uflag = 0;
   long argnum, done;
   char *usr_qfnp, *grp_qfnp;
   register struct mntent *mnt;

   while ((ch = getopt(argc, argv, "avugd")) != EOF) {
      switch (ch) {
         case 'a':
            aflag++;
            break;
         case 'g':
            gflag++;
            break;
         case 'u':
            uflag++;
            break;
         case 'd':
            dflag++;
	    setbuf(stderr, log_buf);
	    break;
         case 'v':
            vflag++;
	    setbuf(stdout, (char *)NULL);
            break;
         default:
            usage();
      }
   }
   argc -= optind;
   argv += optind;

   if (vflag && dflag)
     vflag = 0;

   if (!uflag && !gflag)
      uflag++;

   if (!aflag && argc == 0)
      usage();

   fp = setmntent(MNTTAB, "r");
   while ((mnt = getmntent(fp)) != (struct mntent *) NULL) {
      check_usr = check_grp = 0;
      if (argc && ((argnum = oneof(mnt->mnt_dir, argv, argc)) >= 0) ||
                  ((argnum = oneof(mnt->mnt_fsname, argv, argc)) >= 0)) {
          done |= 1 << argnum;
      } else
         if (!aflag || hasmntopt(mnt, MNTOPT_NOAUTO) ||
                       hasmntopt(mnt, MNTOPT_NOQUOTA))
            continue;

      if (gflag && hasquota(mnt, GRPQUOTA, &grp_qfnp))
         check_grp++;
      if (uflag && hasquota(mnt, USRQUOTA, &usr_qfnp))
         check_usr++;
      if (check_usr || check_grp) {
	 if ((lstat(mnt->mnt_dir, &st)) == -1) {
	   fprintf(stderr, "%s: not found\n", mnt->mnt_dir);
	   perror("lstat");
           continue;
	 }

	 if (vflag)
	   fprintf(stdout,"Scanning %s [%s] ", mnt->mnt_fsname, mnt->mnt_dir);

	 if (S_ISDIR(st.st_mode)) {
           cur_dev = st.st_dev;
           files_done = dirs_done = 0;
           if (check_usr)
	      add_to_quota(&st, USRQUOTA);
           if (check_grp)
	      add_to_quota(&st, GRPQUOTA);
	   if (strcmp(mnt->mnt_type, MNTTYPE_EXT2)) {
	     scan_dir(mnt->mnt_dir);
	   } else {
	     scan_fs(mnt->mnt_fsname);
	   }
	   dirs_done++;
	   if (vflag)
	      fputs("done\n", stderr);
           if (vflag || dflag)
              fprintf(stderr, "Checked %d directories and %d files\n",
                      dirs_done, files_done);
	 } else {
	   fprintf(stderr, "%s: not a directory\n", mnt->mnt_dir);
	   exit(0);
	 }

         if (check_usr)
            dump_to_file(mnt->mnt_fsname, usr_qfnp, USRQUOTA);
         if (check_grp)
            dump_to_file(mnt->mnt_fsname, grp_qfnp, GRPQUOTA);
         remove_list();
      }
   }
   endmntent(fp);

   for (cnt = 0; cnt < argc; cnt++)
      if ((done & (1 << cnt)) == 0)
         fprintf(stderr, "%s not found in fstab\n", argv[cnt]);

#ifdef DEBUG_MALLOC
   fprintf (stderr, "Allocated %d bytes memory\nFree'd %d bytes\nLost %d bytes\n", malloc_mem,
	free_mem, malloc_mem - free_mem);
#endif
   exit(0);
}

/* 
  Scan a certain device by walking thru its inode bitmap. This applies to ext2
  *ONLY*.
 
  Please note that this is a preliminary implementation. It is not final,
  perfect or whatever. It works. Nuff said.

  It needs to be modified though!
 */
void scan_fs(char *device)
{
  ext2_filsys	fs;
  errcode_t	error;
  int inode_buffer_blocks = 0;
  ino_t			ino;
  struct ext2_inode	inode;
  ext2_inode_scan		scan;
  ext2fs_inode_bitmap	inode_used_map;
  ext2fs_inode_bitmap	inode_dir_map;

  error = ext2fs_open(device, 0, 0, 0, unix_io_manager, &fs);
  if (error) {
    fprintf(stderr, "quotacheck: error while opening %s\n", device);
    exit (0);
  }
  error = ext2fs_allocate_inode_bitmap(fs, "in-use inode map", &inode_used_map);
  if (error) {
    fprintf (stderr, "quotacheck: error while allocating inode file bitmap\n");
    exit (0);
  }
  error = ext2fs_allocate_inode_bitmap(fs, "directory inode map", &inode_dir_map);
  if (error) {
    fprintf (stderr, "quotacheck: error while allocating inode directory bitmap\n");
    exit (0);
  }
  error = ext2fs_open_inode_scan(fs, inode_buffer_blocks, &scan);
  if (error) {
    fprintf(stderr, "quotacheck: error while opening inode scan\n");
    exit (0);
  }
  error = ext2fs_get_next_inode(scan, &ino, &inode);
  if (error) {
    fprintf(stderr, "quotacheck: error while starting inode scan\n");
    exit (0);
  }
  while (ino) {
    if (inode.i_links_count) {
      if (dflag)
	printf ("%ld\n", ino);
      if (vflag)
	blit();
      if (check_usr)
	add_to_quota_ext2(inode.i_uid, inode.i_blocks, USRQUOTA);
      if (check_grp)
	add_to_quota_ext2(inode.i_gid, inode.i_blocks, GRPQUOTA);
      if (S_ISDIR(inode.i_mode))
	dirs_done++;
      else
	files_done++;
    }
    error = ext2fs_get_next_inode(scan, &ino, &inode);
    if (error) {
      fprintf (stderr, "Something weird while scanning\n");
      exit (0);
    }
  }
}

/*
 * Scan a directory with the readdir systemcall. Stat the files and add the sizes
 * of the files to the appropriate quotas. When we find a dir we recursivly call
 * ourself to scan that dir.
 */
void scan_dir(char *pathname)
{
   struct dirs *dir_stack = {(struct dirs *)NULL};
   struct dirs *new_dir;
   struct dirent *de;
   struct stat st;
   DIR *dp;

   if ((dp = opendir(pathname)) == (DIR *)NULL)
      abort_now("opendir", "\n%s\n", pathname);

   chdir(pathname);
   while ((de = readdir(dp)) != (struct dirent *)NULL) {
      if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, ".."))
         continue;
      if (vflag)
         blit();

      if ((lstat(de->d_name, &st)) == -1) {
	 fprintf(stderr, "Hmm, file `%s/%s' not found\n", pathname, de->d_name);
         fputs("Guess you'd better run fsck first !\nexiting...\n", stderr);
         perror("lstat");
         exit(1);
      }

      if (check_usr)
	 add_to_quota(&st, USRQUOTA);
      if (check_grp)
	 add_to_quota(&st, GRPQUOTA);

      if (S_ISDIR(st.st_mode)) {
         if (st.st_dev != cur_dev)
            continue;
         /*
          * Add this to the directory stack and check this later on.
          */
	 if (dflag)
	    fprintf(stderr, "pushd %s/%s\n", pathname, de->d_name);
         new_dir = xmalloc(sizeof(struct dirs));
         new_dir->dir_name = xmalloc(strlen(pathname) + strlen(de->d_name) + 2);
         sprintf(new_dir->dir_name, "%s/%s", pathname, de->d_name);
         new_dir->next = dir_stack;
         dir_stack = new_dir;
      } else {
	 if (dflag)
	    fprintf(stderr, "\tAdding %s size %d ino %d links %d\n", de->d_name,
		    st.st_size, st.st_ino, st.st_nlink);
         files_done++;
      }
   }
   closedir(dp);

   /*
    * Traverse the directory stack, and check it.
    */
   if (dflag)
      fputs("Scanning stored directories from directory stack\n", stderr);
   while (dir_stack != (struct dirs *)NULL) {
      new_dir = dir_stack;
      dir_stack = dir_stack->next;
      if (dflag)
         fprintf(stderr, "popd %s\nEntering directory %s\n",
                 new_dir->dir_name, new_dir->dir_name);
      scan_dir(new_dir->dir_name);
      dirs_done++;
#ifdef DEBUG_MALLOC
      free_mem += sizeof(struct dirs) + strlen(new_dir->dir_name) + 1;
#endif
      free(new_dir->dir_name);
      free(new_dir);
   }
   if (dflag)
      fprintf(stderr, "Leaving %s\n", pathname);
}
    
/*
 * Store a hardlinked file for later. Add the end we add this to a users
 * quota because we don't wanna count it more then ones.
 */
void store_dlinks(struct stat *st, int type)
{
   struct dlinks *lptr;

   if (dflag)
      fprintf(stderr, "Adding hardlink for ino %d\n", st->st_ino);

   for (lptr = stored_links[type]; lptr != (struct dlinks *)NULL; lptr = lptr->next)
      if (lptr->ino == st->st_ino)
         return;

   lptr = (struct dlinks *)xmalloc(sizeof(struct dlinks));

   if (type == USRQUOTA)
     lptr->id = st->st_uid;
   else 
     lptr->id = st->st_gid;

   lptr->ino = st->st_ino;
   lptr->size = st->st_size;
   lptr->blksize = st->st_blksize;

   lptr->next = stored_links[type];
   stored_links[type] = lptr;
}


/*
 * Add a number of blocks and inodes to a quota.
 */
void add_to_quota(struct stat *st, int type)
{
   int wanted;
   struct dquot *lptr;

   switch(type)
   {
      case USRQUOTA:
         wanted = st->st_uid;
         break;
      case GRPQUOTA:
         wanted = st->st_gid;
         break;
      default:
         return;
   }

   if ((lptr = lookup_dquot(wanted, type)) == NODQUOT)
      if ((lptr = add_dquot(wanted, type)) == NODQUOT)
	 abort_now("add_to_quota", "Can't add dquot structure type %s for uid %d\n",
		   quotatypes[type], wanted);

   /*
    * A dir is a special case, we count it for 1 inode and 1 block.
    */
   if (S_ISDIR(st->st_mode)) {
      lptr->dq_curinodes++;
      lptr->dq_curblocks++;
   } else {
      /*
       * A symlink is always counted as 1 inode and 1 block even if the 
       * filesystem is smart and stuffs it into the inode. Be fair to other
       * users that have there files on other filesystems that aren't that smart
       * or have symlinks that don't go into the inode (e.g. to long).
       */
      if (S_ISLNK(st->st_mode)) {
         lptr->dq_curinodes++;
         lptr->dq_curblocks++;
      } else {
         if (st->st_nlink != 1) {
            store_dlinks(st, type);
            return;
         }
         lptr->dq_curinodes++;
         if (st->st_size)
            lptr->dq_curblocks += isize_to_blocks(st->st_size, st->st_blksize);
      }
   }
}

void abort_now(const char *perror_mes, const char *fmt, ...)
{
   va_list args;

   va_start(args, fmt);
   vfprintf(stderr, fmt, args);
   va_end(args);

   fflush(stderr);
   perror(perror_mes);
   exit(-1);
}

void add_dlinks(u_long * inodes, u_long * blocks, int id, int type)
{
   struct dlinks  *lptr;

   if (dflag)
      fprintf(stderr, "Adding blocks from hardlinks for %s %d\n",
              quotatypes[type], id);

   for (lptr = stored_links[type]; lptr != (struct dlinks *)NULL; lptr = lptr->next) {
      if (lptr->id == id) {
         (*inodes)++;
         if (lptr->size)
            *blocks += isize_to_blocks(lptr->size, lptr->blksize);
         files_done++;
      }
   }
}

/*
 * Clean up all list from a previous run.
 */
void remove_list()
{
   int cnt;
   struct dquot *dquot, *dquot_free;
   struct dlinks *dlink, *dlink_free;

   for (cnt = 0; cnt < MAXQUOTAS; cnt++) {
      if (dquot_list[cnt] != NODQUOT) {
         dquot = dquot_list[cnt];
         while (dquot != NODQUOT) {
            dquot_free = dquot;
            dquot = dquot->next;
#ifdef DEBUG_MALLOC
	    free_mem += sizeof(struct dquot);
#endif
            free(dquot_free);
         }
         mru_dquot[cnt] = NODQUOT;
      }
      dquot_list[cnt] = NODQUOT;
      if (stored_links[cnt] != (struct dlinks *)NULL) {
         dlink = stored_links[cnt];
         while (dlink != (struct dlinks *)NULL) {
            dlink_free = dlink;
            dlink = dlink->next;
#ifdef DEBUG_MALLOC
	    free_mem += sizeof(struct dlinks);
#endif
            free(dlink_free);
         }
      }
      stored_links[cnt] = (struct dlinks *)NULL;
   }
}

/*
 * Dump the quota info that we have in memory now to the appropriate
 * quota file. We lock it during the time we update it.
 */
void dump_to_file(char *fsname, char *quotafile, int type)
{
   struct dqblk dq_dqb;
   struct dquot *dquot;
   int quota_enabled = 0, max_id;
   int fd, id = 0;

   if (vflag || dflag)
      fprintf(stderr, "Using quotafile %s\n", quotafile);

   if (quotactl(QCMD(Q_GETQUOTA, type), fsname, 0, (caddr_t)&dq_dqb) == 0)
      quota_enabled = 1;

   if ((vflag || dflag) && quota_enabled)
      fprintf(stderr, "Updating in-core %s quotas\n", quotatypes[type]);

   if ((fd = open(quotafile, O_RDWR | O_CREAT, 0600)) < 0)
      abort_now("open", "dump_to_file(%s): ", quotafile);

   if (flock(fd, LOCK_EX) < 0)
      abort_now("flock", "dump_to_file(%s): ", quotafile);

   /*
    * First dump the gracetimes that are always a first in the 
    * quotafile. Only dump new gracetimes when creating a new 
    * quotafile.
    */
   
   if (read(fd, &dq_dqb, sizeof(struct dqblk)) <= 0) {
      memset((caddr_t *)&dq_dqb, 0, sizeof(struct dqblk));
      dq_dqb.dqb_btime = MAX_DQ_TIME;
      dq_dqb.dqb_itime = MAX_IQ_TIME;
      write(fd, &dq_dqb, sizeof(struct dqblk));
   }

   max_id = highestid[type];
   while (id <= max_id) {
      if (lseek(fd, dqoff(id), SEEK_SET))
         read(fd, &dq_dqb, sizeof(struct dqblk));
      if ((dquot = lookup_dquot(id, type)) != NODQUOT) {
         dq_curinodes = dquot->dq_curinodes;
         dq_curblocks = dquot->dq_curblocks;
         if (dflag)
            fprintf(stderr, "%s %d: curinodes: %d curblocks: %d\n",
                    quotatypes[type], id, dq_curinodes, dq_curblocks);
         add_dlinks(&dq_curinodes, &dq_curblocks, id, type);
         if (dflag)
            fprintf(stderr, "%s %d: curinodes: %d curblocks: %d\n",
                    quotatypes[type], id, dq_curinodes, dq_curblocks);
      } else
         memset((caddr_t *)&dq_dqb, 0, sizeof(struct dqblk));

      /*
       * If the quota is updated with the systemcall it isn't needed to update
       * it in the file. Because the kernel will do that with the next sync.
       */
      if (quota_enabled)
         if (quotactl(QCMD(Q_SETUSE, type), fsname, id, (caddr_t)&dq_dqb) == 0) {
            id++;
            continue;
         }

      if (lseek(fd, dqoff(id), SEEK_SET))
         write(fd, &dq_dqb, sizeof(struct dqblk));
      id++;
   }
   flock(fd, LOCK_UN);
   close(fd);
}

/*
 * Add a number of blocks and inodes to a quota.
 */
void add_to_quota_ext2(int id, long blocks, int type)
{
   int wanted;
   struct dquot *lptr;

   if ((lptr = lookup_dquot(id, type)) == NODQUOT)
      if ((lptr = add_dquot(id, type)) == NODQUOT)
	 abort_now("add_to_quota", "Can't add dquot structure type %s for uid %d\n",
		   quotatypes[type], id);
   lptr->dq_curinodes++;
   lptr->dq_curblocks += blocks/2;	/* Dunno why /2 but otherwise everthing is wrong */
}

