/* inode-sig.c:
 *
 ****************************************************************
 * Copyright (C) 2003 Tom Lord
 *
 * See the file "COPYING" for further information about
 * the copyright and warranty status of this work.
 */


#include "hackerlab/bugs/panic.h"
#include "hackerlab/mem/mem.h"
#include "hackerlab/char/str.h"
#include "hackerlab/fs/file-names.h"
#include "hackerlab/vu/safe.h"
#include "tla/libfsutils/ensure-dir.h"
#include "tla/libfsutils/dir-listing.h"
#include "tla/libfsutils/string-files.h"
#include "tla/libawk/relational.h"
#include "tla/libawk/relassoc.h"
#include "tla/libarch/namespace.h"
#include "tla/libarch/invent.h"
#include "tla/libarch/inode-sig.h"



#define MAX_INODE_SIG_FILES 5


/* __STDC__ prototypes for static functions */
static void arch_update_inode_sig (rel_table sig,
                                   assoc_table newsig,
                                   assoc_table id_list);
static void inode_sig_callback (const t_uchar * path,
                                struct stat * stat_buf,
                                enum arch_inventory_category category,
                                const t_uchar * id,
                                int has_source_name,
                                void * closure,
                                int escape_classes);
static t_uchar * arch_inode_sig_dir (const t_uchar * tree_root);
static t_uchar * arch_inode_sig_file (const t_uchar * tree_root,
                                      const t_uchar * archive,
                                      const t_uchar * revision);
static t_uchar * arch_inode_sig_tmp_file (const t_uchar * tree_root,
                                          const t_uchar * archive,
                                          const t_uchar * revision);
static int arch_creat_inode_sig_file (const t_uchar * tree_root,
                                      const t_uchar * archive,
                                      const t_uchar * revision);
static void arch_finish_inode_sig_file (const t_uchar * tree_root,
                                        const t_uchar * archive,
                                        const t_uchar * revision,
                                        int fd);
static void arch_prune_inode_sig_dir (const t_uchar * tree_root,
                                      const t_uchar * current_inode_sig_file);
static void prune_old_ctime_cruft (rel_table the_table);
static void prune_old_dev_cruft (rel_table the_table);



t_uchar *
arch_statb_inode_sig (struct stat * statb)
{
  int fd = make_output_to_string_fd ();

  safe_printfmt (fd, "ino=%lu:mtime=%lu:size=%lu",
                 (t_ulong)statb->st_ino,
                 (t_ulong)statb->st_mtime,
                 (t_ulong)statb->st_size);

  return string_fd_close (fd);
}


rel_table
arch_tree_inode_sig (const t_uchar * tree_root)
{
  struct arch_inventory_options options = {0, };
  int here_fd;
  rel_table answer = rel_table_nil;

  here_fd = safe_open (".", O_RDONLY, 0);
  safe_chdir (tree_root);

  options.categories = arch_inventory_source;
  options.want_ids = 1;
  options.include_excluded = 1;

  arch_get_inventory_naming_conventions (&options, ".");

  arch_inventory_traversal (&options, ".", inode_sig_callback, (void *)&answer, 0);

  arch_free_inventory_naming_conventions (&options);

  rel_sort_table_by_field (0, answer, 0);

  safe_fchdir (here_fd);
  safe_close (here_fd);

  return answer;
}

void
arch_snap_inode_sig (const t_uchar * tree_root,
                     const t_uchar * archive,
                     const t_uchar * revision)
{
  rel_table sig = arch_tree_inode_sig (tree_root);
  int fd = arch_creat_inode_sig_file (tree_root, archive, revision);

  rel_print_table (fd, sig);
  arch_finish_inode_sig_file (tree_root, archive, revision, fd);

  rel_free_table (sig);
}

 
void
arch_snap_inode_sig_files (const t_uchar * tree_root,
                           const t_uchar * archive,
                           const t_uchar * prev_revision,
                           const t_uchar * revision,
                           rel_table file_list)
{
  rel_table oldsig;
  assoc_table newsig_as;
  rel_table newsig_rel;
  assoc_table id_list;
  int fd = -1;
  
  arch_read_inode_sig (&oldsig, 0, tree_root, archive, prev_revision);
  newsig_rel = arch_tree_inode_sig (tree_root);
  newsig_as = rel_to_assoc (newsig_rel, 0, 1);

  id_list = arch_filenames_ids ( &file_list, tree_root);

  arch_update_inode_sig (oldsig, newsig_as, id_list);

  fd = arch_creat_inode_sig_file (tree_root, archive, revision);
  rel_print_table (fd, oldsig);
  arch_finish_inode_sig_file (tree_root, archive, revision, fd);

  rel_free_table (oldsig);
  rel_free_table (newsig_rel);
  free_assoc_table (newsig_as);
  free_assoc_table (id_list);
}


static void 
arch_update_inode_sig (rel_table sig,
                       assoc_table newsig,
                       assoc_table id_list)
{
  int sig_size = rel_n_records (sig);
  int i;

  for (i = 0; i != sig_size; ++ i)
    {
      if (assoc_get_str_taking (id_list, rel_get_field (sig, i, 0)))
	{
	  rel_record new_record = rel_record_nil;
	  rel_field vals_field = assoc_get_taking (newsig, rel_get_field (sig, i, 0));

	  invariant (rel_field_str (vals_field) != 0);

	  new_record = rel_make_record_2_taking (rel_get_field (sig, i, 0), rel_field_ref (vals_field));
          rel_set_record (sig, i, new_record);
          rel_field_unref (vals_field);
	}
    }
}


int
arch_valid_inode_sig (const t_uchar * tree_root,
                      const t_uchar * archive,
                      const t_uchar * revision)
{
  int result = 1;
  t_uchar * sig_file = arch_inode_sig_file(tree_root, archive, revision);
  
  if (safe_access (sig_file, F_OK))
    {
      safe_printfmt (2, "Missing inode signature. Cannot verify reference tree integrity: you should remove and recreate this reference tree.\n    tree: %s\n    archive: %s\n    revision: %s\n",
      tree_root, archive, revision);
    }
  else 
    {
      rel_table existing_sig = rel_table_nil;
      rel_table current_sig = arch_tree_inode_sig (tree_root);

      arch_read_inode_sig (&existing_sig, NULL, tree_root, archive, revision);
      /* Is there a convenience function (a rel_join magic foo?) for this */
      if (rel_n_records (existing_sig) == rel_n_records (current_sig))
        {
          int counter,inner;
          for (counter = 0; counter < rel_n_records (existing_sig); ++counter)
            for (inner = 0; inner < 2; ++inner)
              if (str_cmp (rel_peek_str (existing_sig, counter, inner), rel_peek_str (current_sig, counter, inner)))
                result = 0;
        }
      else result = 0;
      rel_free_table (existing_sig);
      rel_free_table (current_sig);
    }

  lim_free (0, sig_file);
  return result;
}


void
arch_read_inode_sig (rel_table * as_table,
                     assoc_table * as_assoc,
                     const t_uchar * tree_root,
                     const t_uchar * archive,
                     const t_uchar * revision)
{
  t_uchar * sig_file = arch_inode_sig_file (tree_root, archive, revision);

  if (as_table)
    *as_table = rel_table_nil;

  if (as_assoc)
    *as_assoc = 0;

  if (!safe_access (sig_file, F_OK))
    {
      int fd = safe_open (sig_file, O_RDONLY, 0);
      rel_table the_table;

      the_table = rel_read_table (fd, 2, "arch", sig_file);

      prune_old_ctime_cruft (the_table);
      prune_old_dev_cruft (the_table);

      if (as_assoc)
        *as_assoc = rel_to_assoc (the_table, 0, 1);

      if (as_table)
        *as_table = the_table;
      else
        rel_free_table (the_table);

      safe_close (fd);
    }

  lim_free (0, sig_file);
}


void
arch_read_id_shortcut (assoc_table * ids_shortcut,
                       const t_uchar * tree_root)
{
  t_uchar * inode_sig_dir = arch_inode_sig_dir (tree_root);
  rel_table dir_listing = maybe_directory_files (inode_sig_dir);
  assoc_table answer = 0;
  assoc_table rejected = 0;
  int x;

  for (x = 0; x < rel_n_records (dir_listing); ++x)
    {
      t_uchar * path = file_name_in_vicinity (0, inode_sig_dir, rel_peek_str (dir_listing, x, 0));
      t_uchar * pct = str_chr_index (rel_peek_str (dir_listing, x, 0), '%');

      if (pct && str_cmp (".", rel_peek_str (dir_listing, x, 0)) && str_cmp ("..", rel_peek_str (dir_listing, x, 0)))
        {
          t_uchar * maybe_archive = str_save_n (0, rel_peek_str (dir_listing, x, 0), pct - rel_peek_str (dir_listing, x, 0));
          t_uchar * maybe_revision = str_save (0, pct + 1);

          if (arch_valid_archive_name (maybe_archive) && arch_valid_package_name (maybe_revision, arch_no_archive, arch_req_patch_level, 0))
            {
              rel_table sig_table = rel_table_nil;
              int y;

              arch_read_inode_sig (&sig_table, 0, tree_root, maybe_archive, maybe_revision);

              for (y = 0; y < rel_n_records (sig_table); ++y)
                {
                  rel_field this_id_field;
                  rel_field this_sig_field;
                  const t_uchar * is_rejected;
                  const t_uchar * already_have;
                  
                  this_id_field = rel_get_field (sig_table, y, 0);
                  this_sig_field = rel_get_field (sig_table, y, 1);

                  rel_field_ref (this_sig_field);
                  is_rejected = assoc_get_str_taking (rejected, this_sig_field);

                  rel_field_ref (this_sig_field);
                  already_have = assoc_get_str_taking (answer, this_sig_field);

                  if (!is_rejected)
                    {
                      if (already_have)
                        {
                          if (!str_cmp (rel_field_str (this_id_field), already_have))
                            {
                              assoc_del_taking (answer, rel_field_ref (this_sig_field));
                              assoc_set_taking (&rejected, rel_field_ref (this_sig_field), rel_make_field_str ("yes"));
                            }
                        }
                      else
                        {
                          assoc_set_taking (&answer, rel_field_ref (this_sig_field), rel_field_ref (this_id_field));
                        }
                    }

                  rel_field_unref (this_id_field);
                  rel_field_unref (this_sig_field);
                }

              rel_free_table (sig_table);
            }

          lim_free (0, maybe_archive);
          lim_free (0, maybe_revision);
        }

      lim_free (0, path);
    }

  lim_free (0, inode_sig_dir);
  rel_free_table (dir_listing);
  free_assoc_table (rejected);

  *ids_shortcut = answer;
}




static void
inode_sig_callback (const t_uchar * path,
                    struct stat * stat_buf,
                    enum arch_inventory_category category,
                    const t_uchar * id,
                    int has_source_name,
                    void * closure,
                    int escape_classes)
{
  rel_table * answer = (rel_table *)closure;
  t_uchar * signature = arch_statb_inode_sig (stat_buf);

  if (!S_ISDIR (stat_buf->st_mode) && !S_ISLNK (stat_buf->st_mode))
    rel_add_records (answer, rel_make_record_2_taking (rel_make_field_str (id), rel_make_field_str (signature)), rel_record_null);
  lim_free(0, signature);
}


static t_uchar *
arch_inode_sig_dir (const t_uchar * tree_root)
{
  return file_name_in_vicinity (0, tree_root, "{arch}/,,inode-sigs");
}


static t_uchar *
arch_inode_sig_file (const t_uchar * tree_root,
                     const t_uchar * archive,
                     const t_uchar * revision)
{
  t_uchar * inode_sig_dir = arch_inode_sig_dir (tree_root);
  t_uchar * basename = str_alloc_cat_many (0, archive, "%", revision, str_end);
  t_uchar * answer = file_name_in_vicinity (0, inode_sig_dir, basename);

  lim_free (0, inode_sig_dir);
  lim_free (0, basename);
  return answer;
}


static t_uchar *
arch_inode_sig_tmp_file (const t_uchar * tree_root,
                         const t_uchar * archive,
                         const t_uchar * revision)
{
  t_uchar * inode_sig_dir = arch_inode_sig_dir (tree_root);
  t_uchar * basename = str_alloc_cat_many (0, ",,", archive, "%", revision, str_end);
  t_uchar * answer = file_name_in_vicinity (0, inode_sig_dir, basename);

  lim_free (0, inode_sig_dir);
  lim_free (0, basename);
  return answer;
}


static int
arch_creat_inode_sig_file (const t_uchar * tree_root,
                           const t_uchar * archive,
                           const t_uchar * revision)
{
  t_uchar * inode_sig_dir = arch_inode_sig_dir (tree_root);
  t_uchar * inode_sig_tmp_file = arch_inode_sig_tmp_file (tree_root, archive, revision);
  int ign;
  int answer;

  ensure_directory_exists (inode_sig_dir);
  vu_unlink (&ign, inode_sig_tmp_file);
  answer = safe_open (inode_sig_tmp_file, (O_WRONLY | O_CREAT | O_EXCL), 0444);

  lim_free (0, inode_sig_dir);
  lim_free (0, inode_sig_tmp_file);

  return answer;
}


static void
arch_finish_inode_sig_file (const t_uchar * tree_root,
                            const t_uchar * archive,
                            const t_uchar * revision,
                            int fd)
{
  t_uchar * inode_sig_tmp_file = arch_inode_sig_tmp_file (tree_root, archive, revision);
  t_uchar * inode_sig_file = arch_inode_sig_file (tree_root, archive, revision);

  safe_close (fd);
  safe_rename (inode_sig_tmp_file, inode_sig_file);
  arch_prune_inode_sig_dir (tree_root, inode_sig_file);

  lim_free (0, inode_sig_tmp_file);
  lim_free (0, inode_sig_file);
}


static void
arch_prune_inode_sig_dir (const t_uchar * tree_root,
                          const t_uchar * current_inode_sig_file)
{
  t_uchar * inode_sig_dir = arch_inode_sig_dir (tree_root);
  rel_table dir_listing = maybe_directory_files (inode_sig_dir);
  int dead_one = -1;
  time_t oldest_time;
  int n_inode_sig_files;
  int x;

  do
    {
      n_inode_sig_files = 0;

      for (x = 0; x < rel_n_records (dir_listing); ++x)
        {
          t_uchar * path = file_name_in_vicinity (0, inode_sig_dir, rel_peek_str (dir_listing, x, 0));
          t_uchar * pct = str_chr_index (rel_peek_str (dir_listing, x, 0), '%');

          if (pct && str_cmp (".", rel_peek_str (dir_listing, x, 0)) && str_cmp ("..", rel_peek_str (dir_listing, x, 0)))
            {
              t_uchar * maybe_archive = str_save_n (0, rel_peek_str (dir_listing, x, 0), pct - rel_peek_str (dir_listing, x, 0));
              t_uchar * maybe_revision = str_save (0, pct + 1);

              if (arch_valid_archive_name (maybe_archive) && arch_valid_package_name (maybe_revision, arch_no_archive, arch_req_patch_level, 0))
                {
                  struct stat statb;

                  ++n_inode_sig_files;

                  safe_lstat (path, &statb);

                  if (str_cmp (current_inode_sig_file, path) && ((dead_one < 0) || (statb.st_mtime < oldest_time)))
                    {
                      dead_one = x;
                      oldest_time = statb.st_mtime;
                    }
                }

              lim_free (0, maybe_archive);
              lim_free (0, maybe_revision);
            }

          lim_free (0, path);
        }

      if (n_inode_sig_files > MAX_INODE_SIG_FILES)
        {
          t_uchar * dead_path = file_name_in_vicinity (0, inode_sig_dir, rel_peek_str (dir_listing, dead_one, 0));

          safe_unlink (dead_path);
          --n_inode_sig_files;

          lim_free (0, dead_path);
        }


    } while (n_inode_sig_files > MAX_INODE_SIG_FILES); /* usually only one iteration expected */

  lim_free (0, inode_sig_dir);
  rel_free_table (dir_listing);
}


static void
prune_old_ctime_cruft (rel_table the_table)
{
  int x;

#undef MIN
#define MIN(A,B) (((A) < (B)) ? (A) : (B))

  for (x = 0; x < rel_n_records (the_table); ++x)
    {
      const t_uchar * sig = rel_peek_str (the_table, x, 1);
      const t_uchar * third_colon;
      const t_uchar * fourth_colon;
      t_uchar * fixed_sig = 0;

      third_colon = str_chr_index (sig, ':');
      if (!third_colon)
        return;
      
      third_colon = str_chr_index (third_colon + 1, ':');
      if (!third_colon)
        return;

      third_colon = str_chr_index (third_colon + 1, ':');
      if (!third_colon)
        return;

      if (str_cmp_n (third_colon + 1, MIN (5, str_length (third_colon + 1)), "ctime", (size_t)5))
        return;

      fourth_colon = str_chr_index (third_colon + 1, ':');
      if (!fourth_colon)
        return;

      fixed_sig = str_save_n (0, sig, third_colon - sig);
      fixed_sig = str_realloc_cat_n (0, fixed_sig, fourth_colon, 1 + str_length (fourth_colon));

      rel_set_taking (the_table, x, 1, rel_make_field_str (fixed_sig));

      lim_free (0, fixed_sig);
    }
}


static void
prune_old_dev_cruft (rel_table the_table)
{
  int x;

#undef MIN
#define MIN(A,B) (((A) < (B)) ? (A) : (B))

  for (x = 0; x < rel_n_records (the_table); ++x)
    {
      const t_uchar * sig = rel_peek_str (the_table, x, 1);
      const t_uchar * first_colon;

      if (str_cmp_n (sig, MIN (3, str_length (sig)), "dev", (size_t)3))
        return;

      first_colon = str_chr_index (sig, ':');
      if (!first_colon)
        return;

      rel_set_taking (the_table, x, 1, rel_make_field_str_n (first_colon + 1, str_length (first_colon)));
    }
}





/* tag: Tom Lord Fri Sep 12 09:56:44 2003 (inode-sig.c)
 */
