/* Copyright (C) 2004  Jeroen Dekkers <jeroen@dekkers.cx>
   Copyright (C) 2004  Ben Asselstine <benasselstine@canada.com>
   Copyright (C) 2021  Bastian Germann

   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.

   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, write to the Free Software Foundation, Inc.,
   51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#define _GNU_SOURCE
#include <archive.h>
#include <archive_entry.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <glob.h>
#include <libgen.h>
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "opts.h"

#define BLOCKSIZE 4096

extern struct arguments_t arguments;

void
show_copyright (struct unrar_arguments_t *unrar)
{
  fprintf (unrar->outfile,
           "\n%s %s  Copyright (C) 2004  Ben Asselstine, Jeroen Dekkers\n\n",
           PACKAGE, VERSION);
}

void
show_list_header (struct unrar_arguments_t *unrar)
{
  fprintf (unrar->outfile, "\nRAR archive %s\n\n", unrar->archive_filename);
  fprintf (unrar->outfile, "Pathname/Comment\n");
  fprintf (unrar->outfile, "                  Size   Date   Time     Attr\n");
  fprintf (unrar->outfile, "----------------------------------------------\n");
}

void
show_list_footer (struct unrar_arguments_t *unrar)
{
  fprintf (unrar->outfile, "----------------------------------------------\n");
}

void
show_list_stats (struct unrar_arguments_t *unrar, unsigned int num_files,
                 uint64_t num_bytes)
{
  fprintf (unrar->outfile, "%5u       %10lu\n", num_files, num_bytes);
}

void
replace_backslash_with_slash (char *s1)
{
  char *ptr;
  if (!s1)
    return;
  while ((ptr = strchr (s1, '\\'))) {
    ptr[0] = '/';
    s1 = &ptr[1];
  }
}

void
unrar_list_item (struct unrar_arguments_t *unrar, struct archive_entry *entry)
{
  char datetime[20];
  char *attr;

  fprintf (unrar->outfile, " %s\n", archive_entry_pathname (entry));
  if (archive_entry_mtime_is_set (entry)) {
    struct tm filetime;
    time_t unixtime;
    unixtime = archive_entry_mtime (entry);
    localtime_r (&unixtime, &filetime);
    strftime (datetime, sizeof (datetime), "%d-%m-%y %H:%M", &filetime);
  } else
    snprintf (datetime, sizeof (datetime), "00-00-00 00:00");

  if (archive_entry_filetype (entry) == S_IFDIR)
    attr = ".D....";
  else
    attr = ".....A";

  fprintf (unrar->outfile, "            %10lu %s   %s\n",
           archive_entry_size (entry), datetime, attr);
}

int
unrar_list (struct unrar_arguments_t *unrar, int num_files, char **files)
{
  struct archive_entry *entry;
  int j, status, ret = 0;
  unsigned int count_files = 0;
  uint64_t count_bytes = 0;

  struct archive *a = archive_read_new();
  if (!a)
    return -1;

  if (archive_read_support_format_all (a) != ARCHIVE_OK) {
    ret = -2;
    goto err;
  }

  archive_read_add_passphrase (a, unrar->password);

  if (unrar->multivolume_filenames != NULL) {
    if (archive_read_open_filenames (a, unrar->multivolume_filenames,
                                     BLOCKSIZE) != ARCHIVE_OK) {
      ret = -3;
      goto err;
    }
  } else if (archive_read_open_filename (a, unrar->archive_filename,
                                         BLOCKSIZE) != ARCHIVE_OK) {
    ret = -3;
    goto err;
  }

  if (unrar->verbose)
    fprintf (unrar->outfile, "given a list of %d files.\n", num_files);
  show_list_header (unrar);

  while ((status = archive_read_next_header (a, &entry)) == ARCHIVE_OK) {
    char *entry_name = strdup (archive_entry_pathname (entry));
    replace_backslash_with_slash (entry_name);
    if (num_files) {
      int found = 0;
      for (j = 0; j < num_files; j++) {
        if (unrar->verbose)
          fprintf (unrar->outfile, "comparing '%s' vs '%s'\n", files[j],
                   entry_name);
        if (strcmp (files[j], entry_name) == 0) {
          found = 1;
          break;
        }
      }
      if (!found) {
        free (entry_name);
        continue;
      }
    }
    count_files++;
    count_bytes += archive_entry_size (entry);
    unrar_list_item (unrar, entry);
    free (entry_name);
  }

  show_list_footer (unrar);
  show_list_stats (unrar, count_files, count_bytes);

  switch (status) {
    case ARCHIVE_WARN:
    case ARCHIVE_FATAL:
      ret = -4;
      break;
    case ARCHIVE_RETRY:
      ret = -5;
      // fallthrough; no error message
    default:
      goto out;
  }

err:
  unrar_error ("%s", archive_error_string (a));
out:
  archive_read_free (a);

  return ret;
}

int
unrar_mkpath (char *path)
{
  char *path_tmp;
  int i, ret = 0;
  if (path == NULL || path[0] == '\0')
    return (-1);
  path_tmp = strdup (path);
  if (path_tmp == NULL)
    return (-1);
  for (i = 0; path_tmp[i] != '\0'; i++) {
    if (path_tmp[i] == '/') {
      path_tmp[i] = '\0';
      if (path_tmp[0] != '\0') {
        ret = mkdir (path_tmp, 0777);
      }
      path_tmp[i] = '/';
    }
  }
  free (path_tmp);
  return ret;
}

/**
 * dump_file is based on GPL-2.0-or-later code from a simple unrar which is
 * (C) Andreas F. Borchert, 2002, <unrarlib@andreas-borchert.de>.
 */
int
dump_file (struct unrar_arguments_t *unrar, char *filename,
           struct archive *archive)
{
  int fd, read_status;
  int fcntl_flags = O_WRONLY | O_CREAT;
  if (filename == NULL) {
    fd = STDOUT_FILENO;
    goto read;
  }
  if (unrar->force)
    fcntl_flags |= O_TRUNC;
  else
    fcntl_flags |= O_EXCL;
  fd = open (filename, fcntl_flags, 0666);
  if (fd < 0 && errno == ENOENT) {
    unrar_mkpath (filename);
    fd = open (filename, fcntl_flags, 0666);
  }
  if (fd < 0)
    return -1;

read:
  read_status = archive_read_data_into_fd (archive, fd);
  if (read_status != ARCHIVE_OK) {
    unrar_error ("%s", archive_error_string (archive));
  }

  if (unrar->mode == MODE_EXTRACT && close (fd) < 0)
    return -1;
  return read_status;
}

void
show_status_line (struct unrar_arguments_t *unrar, char *action, char *file,
                  char *status)
{
  fprintf (unrar->outfile, "%-11s %-57s %-10s\n", action, file, status);
}

int
unrar_extract_directory (struct unrar_arguments_t *unrar, char *dir)
{
  char *destination = NULL;
  int ret = 0;
  if (unrar->junk_paths)
    return 0;
  if (asprintf (&destination, "%s/%s/", unrar->destination_dir, dir) == -1) {
    unrar_error ("asprintf failed: %s\n", strerror (errno));
    return -1;
  }
  if (mkdir (destination, 0777) < 0) {
    switch (errno) {
      case EEXIST:
        if (unrar->force)
          chmod (destination, 0777);
        break;
      case ENOENT:
        ret = unrar_mkpath (destination);
        if (ret == (-1)) {
          unrar_error ("mkpath failed '%s': %s\n", destination,
                       strerror (errno));
        }
        break;
      default:
        unrar_error ("mkdir failed '%s': %s\n", destination, strerror (errno));
        ret = (-1);
        break;
    }
  }
  free (destination);
  return ret;
}

int
unrar_extract_file (struct unrar_arguments_t *unrar, char *filename,
                    struct archive *archive,
                    struct archive_entry *archive_member)
{
  char *destination = NULL;
  char *file;
  if (unrar->junk_paths) {
    file = strrchr (filename, '/');
    if (!file)
      file = filename;
  } else
    file = filename;

  if (unrar->mode == MODE_EXTRACT &&
      asprintf (&destination, "%s/%s", unrar->destination_dir, file) == -1) {
    unrar_error ("asprintf failed: %s\n", strerror (errno));
    return 0;
  }
  if (!strncmp ("../", file, strlen ("../")) || strstr (file, "/../")) {
    unrar_error ("archive contains unsafe filename: '%s'\n", file);
    return -1;
  }
  // where do i put it?  in destination.
  if (unrar->mode == MODE_EXTRACT && unrar->extract_newer) {
    struct stat statbuf;
    time_t unixtime;
    if (lstat (destination, &statbuf) < 0) {
      free (destination);
      return 1;
    } else {
      unixtime = archive_entry_mtime (archive_member);
      if (unixtime <= statbuf.st_mtime) {
        free (destination);
        return 1;
      }
    }
  }
  // okay put it in destination.
  if (dump_file (unrar, destination, archive) < 0) {
    free (destination);
    return -1;
  }
  free (destination);
  return 0;
}

int
unrar_extract (struct unrar_arguments_t *unrar, int num_files, char **files)
{
  struct archive_entry *entry;
  int j, n, ret = 0;
  int retval;
  char *action;
  char *status;
  char *orig_name;
  int num_failed = 0;

  struct archive *a = archive_read_new();
  if (!a)
    return -1;

  if (archive_read_support_format_all (a) != ARCHIVE_OK) {
    ret = -2;
    goto err;
  }

  fprintf (unrar->outfile, "\nExtracting from %s\n\n", unrar->archive_filename);

  archive_read_add_passphrase (a, unrar->password);

  if (unrar->multivolume_filenames != NULL) {
    if (archive_read_open_filenames (a, unrar->multivolume_filenames,
                                     BLOCKSIZE) != ARCHIVE_OK) {
      ret = -3;
      goto err;
    }
  } else if (archive_read_open_filename (a, unrar->archive_filename,
                                         BLOCKSIZE) != ARCHIVE_OK) {
    ret = -3;
    goto err;
  }

  if (unrar->verbose) {
    fprintf (unrar->outfile, "given a list of %d files.\n", num_files);
  }

  while ((n = archive_read_next_header (a, &entry)) == ARCHIVE_OK) {
    orig_name = strdup (archive_entry_pathname (entry));

    replace_backslash_with_slash (orig_name);
    if (num_files) {
      int found = 0;
      for (j = 0; j < num_files; j++) {
        if (unrar->verbose)
          fprintf (unrar->outfile, "comparing '%s' vs '%s'\n", files[j],
                   orig_name);
        if (strcmp (files[j], orig_name) == 0) {
          found = 1;
          break;
        }
      }
      if (!found) {
        free (orig_name);
        if (archive_entry_filetype (entry) == S_IFDIR)
          continue;
        action = "Skipping";
        status = "";
        show_status_line (unrar, action, orig_name, status);
        continue;
      }
    }

    status = "OK";
    if (archive_entry_filetype (entry) == S_IFDIR) {
      action = "Creating";
      if ((retval = unrar_extract_directory (unrar, orig_name)) < 0) {
        status = "Failed";
      } else if (retval == 0) {
        free (orig_name);
        continue;
      }

    } else  // is file.
    {
      action = "Extracting";
      if ((retval = unrar_extract_file (unrar, orig_name, a, entry)) < 0) {
        status = "Failed";
      } else if (retval == 1) {
        action = "Skipping";
        status = "";
        show_status_line (unrar, action, orig_name, status);
        free (orig_name);
        continue;
      } else if (retval == 0) {
        status = "OK";
        show_status_line (unrar, action, orig_name, status);
        free (orig_name);
        continue;
      }
    }
    free (orig_name);
    show_status_line (unrar, action, orig_name, status);
    if (strcmp (status, "Failed") == 0)
      num_failed++;
  }

  switch (n) {
    case ARCHIVE_WARN:
    case ARCHIVE_FATAL:
      ret = -4;
      break;
    case ARCHIVE_RETRY:
      ret = -5;
      // fallthrough; no error message
    default:
      goto out;
  }

err:
  unrar_error ("%s", archive_error_string (a));
out:
  archive_read_free (a);

  if (num_failed) {
    fprintf (unrar->outfile, "%d Failed\n", num_failed);
    ret = -1;
  } else if (ret == 0)
    fprintf (unrar->outfile, "All OK\n");

  return ret;
}

int
unrar_multivolume_glob (const struct unrar_arguments_t *unrar,
                        glob_t *restrict pglob)
{
  char *filecp = strdup (unrar->archive_filename);
  char *strstart = basename (filecp);
  char *strend = strrchr (strstart, '\0');
  char *search = strrchr (strstart, '.');
  _Bool has_rar_ext =
      search && search + 4 == strend && strcasecmp (search, ".rar") == 0;
  if (!has_rar_ext)
    search = strend;

  int subsequent_digits;
  for (subsequent_digits = 0; --search >= strstart && isdigit (*search);
       subsequent_digits++);
  if (subsequent_digits <= 0) {
    free (filecp);
    return GLOB_NOMATCH;
  }

  char *tryfile =
      calloc (strlen (unrar->archive_filename) + 4 * subsequent_digits + 1, 1);
  strncpy (tryfile, filecp, search - filecp + 1);
  for (int i = 0; i < subsequent_digits; i++) {
    strncpy (tryfile + (search - filecp + 5 * i + 1), "[0-9]", 5);
  }
  if (has_rar_ext)
    strncpy (tryfile + (search - filecp + 5 * subsequent_digits + 1),
             strend - 4, 4);

  free (filecp);
  return glob (tryfile, 0, NULL, pglob);
}

int
main (int argc, char **argv)
{
  int retval = 0;
  char **files = NULL;
  int num_files = 0;
  arguments.unrar.outfile = stdout;

  setlocale (LC_CTYPE, "");

  if (compat_parse_opts (argc, argv, &arguments) == 0) {
    /* compatible mode success */
  } else if (parse_opts (argc, argv, &arguments) < 0)
    exit (1);

  glob_t pglob;
  int rglob = unrar_multivolume_glob (&arguments.unrar, &pglob);
  if (rglob == 0 && pglob.gl_pathc > 1)
    arguments.unrar.multivolume_filenames = (const char **)pglob.gl_pathv;
  else
    arguments.unrar.multivolume_filenames = NULL;

  if (arguments.unrar.verbose) {
    fprintf (arguments.unrar.outfile, "archive name is '%s'\n",
             arguments.unrar.archive_filename);
    if (arguments.unrar.multivolume_filenames != NULL)
      fprintf (arguments.unrar.outfile, "trying to open multivolume archive\n");
    fprintf (arguments.unrar.outfile, "destination directory is '%s'\n",
             arguments.unrar.destination_dir);
    fprintf (arguments.unrar.outfile, "mode = %d\n", arguments.unrar.mode);
    fprintf (arguments.unrar.outfile, "force = %d\n", arguments.unrar.force);
    fprintf (arguments.unrar.outfile, "extract_newer = %d\n",
             arguments.unrar.extract_newer);
    fprintf (arguments.unrar.outfile, "junk_paths = %d\n",
             arguments.unrar.junk_paths);
  }

  if (arguments.arraylen > 1) {
    files = &arguments.args[1];
    num_files = arguments.arraylen - 1;
  }

  show_copyright (&arguments.unrar);
  if (arguments.unrar.mode == MODE_LIST)
    retval = unrar_list (&arguments.unrar, num_files, files);
  else if (arguments.unrar.mode == MODE_EXTRACT ||
           arguments.unrar.mode == MODE_PRINT)
    retval = unrar_extract (&arguments.unrar, num_files, files);

  if (rglob != GLOB_NOMATCH)
    globfree (&pglob);

  if (retval < 0)
    exit (1);

  exit (0);
}
