/*
**  Copyright 1998-2003 University of Illinois Board of Trustees
**  Copyright 1998-2003 Mark D. Roth
**  All rights reserved.
**
**  block.c - libtar code to handle tar archive header blocks
**
**  Mark D. Roth <roth@uiuc.edu>
**  Campus Information Technologies and Educational Services
**  University of Illinois at Urbana-Champaign
*/

#include <libtarint/internal.h>

#include <errno.h>

#ifdef STDC_HEADERS
# include <string.h>
# include <stdlib.h>
#endif


#define BIT_ISSET(bitmask, bit) ((bitmask) & (bit))


/* read a header block */
int
th_read_internal(TAR *t)
{
  int i;
  int num_zero_blocks = 0;

#ifdef DEBUG
  printf("==> th_read_internal(TAR=\"%s\")\n", t->pathname);
#endif

  while ((i = tar_block_read(t, &(t->th_buf))) == T_BLOCKSIZE)
  {
    /* two all-zero blocks mark EOF */
    if (t->th_buf.name[0] == '\0')
    {
      num_zero_blocks++;
      if (!BIT_ISSET(t->options, TAR_IGNORE_EOT)
          && num_zero_blocks >= 2)
        return 0;  /* EOF */
      else
        continue;
    }

    /* verify magic and version */
    if (BIT_ISSET(t->options, TAR_CHECK_MAGIC)
        && strncmp(t->th_buf.magic, TMAGIC, TMAGLEN - 1) != 0)
    {
#ifdef DEBUG
      puts("!!! unknown magic value in tar header");
#endif
      return -2;
    }

    if (BIT_ISSET(t->options, TAR_CHECK_VERSION)
        && strncmp(t->th_buf.version, TVERSION, TVERSLEN) != 0)
    {
#ifdef DEBUG
      puts("!!! unknown version value in tar header");
#endif
      return -2;
    }

    /* check chksum */
    if (!BIT_ISSET(t->options, TAR_IGNORE_CRC)
        && !th_crc_ok(t))
    {
#ifdef DEBUG
      puts("!!! tar header checksum error");
#endif
      return -2;
    }

    break;
  }

#ifdef DEBUG
  printf("<== th_read_internal(): returning %d\n", i);
#endif
  return i;
}


/* wrapper function for th_read_internal() to handle GNU extensions */
int
th_read(TAR *t)
{
  int i, j;
  size_t sz;
  char *ptr;

#ifdef DEBUG
  printf("==> th_read(t=0x%lx)\n", t);
#endif

  if (t->th_buf.gnu_longname != NULL)
    free(t->th_buf.gnu_longname);
  if (t->th_buf.gnu_longlink != NULL)
    free(t->th_buf.gnu_longlink);
  memset(&(t->th_buf), 0, sizeof(struct tar_header));

  i = th_read_internal(t);
  if (i == 0)
    return 1;
  else if (i != T_BLOCKSIZE)
  {
    if (i != -1)
      errno = EINVAL;
    return -1;
  }

  /* check for GNU long link extention */
  if (TH_ISLONGLINK(t))
  {
    sz = th_get_size(t);
    j = (sz / T_BLOCKSIZE) + (sz % T_BLOCKSIZE ? 1 : 0);
#ifdef DEBUG
    printf("    th_read(): GNU long linkname detected "
           "(%ld bytes, %d blocks)\n", sz, j);
#endif
    t->th_buf.gnu_longlink = (char *)malloc(j * T_BLOCKSIZE);
    if (t->th_buf.gnu_longlink == NULL)
      return -1;

    for (ptr = t->th_buf.gnu_longlink; j > 0;
         j--, ptr += T_BLOCKSIZE)
    {
#ifdef DEBUG
      printf("    th_read(): reading long linkname "
             "(%d blocks left, ptr == %ld)\n", j, ptr);
#endif
      i = tar_block_read(t, ptr);
      if (i != T_BLOCKSIZE)
      {
        if (i != -1)
          errno = EINVAL;
        return -1;
      }
#ifdef DEBUG
      printf("    th_read(): read block == \"%s\"\n", ptr);
#endif
    }
#ifdef DEBUG
    printf("    th_read(): t->th_buf.gnu_longlink == \"%s\"\n",
           t->th_buf.gnu_longlink);
#endif

    i = th_read_internal(t);
    if (i != T_BLOCKSIZE)
    {
      if (i != -1)
        errno = EINVAL;
      return -1;
    }
  }

  /* check for GNU long name extention */
  if (TH_ISLONGNAME(t))
  {
    sz = th_get_size(t);
    j = (sz / T_BLOCKSIZE) + (sz % T_BLOCKSIZE ? 1 : 0);
#ifdef DEBUG
    printf("    th_read(): GNU long filename detected "
           "(%ld bytes, %d blocks)\n", sz, j);
#endif
    t->th_buf.gnu_longname = (char *)malloc(j * T_BLOCKSIZE);
    if (t->th_buf.gnu_longname == NULL)
      return -1;

    for (ptr = t->th_buf.gnu_longname; j > 0;
         j--, ptr += T_BLOCKSIZE)
    {
#ifdef DEBUG
      printf("    th_read(): reading long filename "
             "(%d blocks left, ptr == %ld)\n", j, ptr);
#endif
      i = tar_block_read(t, ptr);
      if (i != T_BLOCKSIZE)
      {
        if (i != -1)
          errno = EINVAL;
        return -1;
      }
#ifdef DEBUG
      printf("    th_read(): read block == \"%s\"\n", ptr);
#endif
    }
#ifdef DEBUG
    printf("    th_read(): t->th_buf.gnu_longname == \"%s\"\n",
           t->th_buf.gnu_longname);
#endif

    i = th_read_internal(t);
    if (i != T_BLOCKSIZE)
    {
      if (i != -1)
        errno = EINVAL;
      return -1;
    }
  }

#if 0
  /*
  ** work-around for old archive files with broken typeflag fields
  ** NOTE: I fixed this in the TH_IS*() macros instead
  */

  /*
  ** (directories are signified with a trailing '/')
  */
  if (t->th_buf.typeflag == AREGTYPE
      && t->th_buf.name[strlen(t->th_buf.name) - 1] == '/')
    t->th_buf.typeflag = DIRTYPE;

  /*
  ** fallback to using mode bits
  */
  if (t->th_buf.typeflag == AREGTYPE)
  {
    mode = (mode_t)oct_to_int(t->th_buf.mode);

    if (S_ISREG(mode))
      t->th_buf.typeflag = REGTYPE;
    else if (S_ISDIR(mode))
      t->th_buf.typeflag = DIRTYPE;
    else if (S_ISFIFO(mode))
      t->th_buf.typeflag = FIFOTYPE;
    else if (S_ISCHR(mode))
      t->th_buf.typeflag = CHRTYPE;
    else if (S_ISBLK(mode))
      t->th_buf.typeflag = BLKTYPE;
    else if (S_ISLNK(mode))
      t->th_buf.typeflag = SYMTYPE;
  }
#endif

  return 0;
}


/* write a header block */
int
th_write(TAR *t)
{
  int i, j;
  char type2;
  size_t sz, sz2;
  char *ptr;
  char buf[T_BLOCKSIZE];

#ifdef DEBUG
  printf("==> th_write(TAR=\"%s\")\n", t->pathname);
  th_print(t);
#endif

  if ((t->options & TAR_GNU) && t->th_buf.gnu_longlink != NULL)
  {
#ifdef DEBUG
    printf("th_write(): using gnu_longlink (\"%s\")\n",
           t->th_buf.gnu_longlink);
#endif
    /* save old size and type */
    type2 = t->th_buf.typeflag;
    sz2 = th_get_size(t);

    /* write out initial header block with fake size and type */
    t->th_buf.typeflag = GNU_LONGLINK_TYPE;
    sz = strlen(t->th_buf.gnu_longlink);
    th_set_size(t, sz);
    th_finish(t);
    i = tar_block_write(t, &(t->th_buf));
    if (i != T_BLOCKSIZE)
    {
      if (i != -1)
        errno = EINVAL;
      return -1;
    }

    /* write out extra blocks containing long name */
    for (j = (sz / T_BLOCKSIZE) + (sz % T_BLOCKSIZE ? 1 : 0),
         ptr = t->th_buf.gnu_longlink; j > 1;
         j--, ptr += T_BLOCKSIZE)
    {
      i = tar_block_write(t, ptr);
      if (i != T_BLOCKSIZE)
      {
        if (i != -1)
          errno = EINVAL;
        return -1;
      }
    }
    memset(buf, 0, T_BLOCKSIZE);
    strncpy(buf, ptr, T_BLOCKSIZE);
    i = tar_block_write(t, &buf);
    if (i != T_BLOCKSIZE)
    {
      if (i != -1)
        errno = EINVAL;
      return -1;
    }

    /* reset type and size to original values */
    t->th_buf.typeflag = type2;
    th_set_size(t, sz2);
  }

  if ((t->options & TAR_GNU) && t->th_buf.gnu_longname != NULL)
  {
#ifdef DEBUG
    printf("th_write(): using gnu_longname (\"%s\")\n",
           t->th_buf.gnu_longname);
#endif
    /* save old size and type */
    type2 = t->th_buf.typeflag;
    sz2 = th_get_size(t);

    /* write out initial header block with fake size and type */
    t->th_buf.typeflag = GNU_LONGNAME_TYPE;
    sz = strlen(t->th_buf.gnu_longname);
    th_set_size(t, sz);
    th_finish(t);
    i = tar_block_write(t, &(t->th_buf));
    if (i != T_BLOCKSIZE)
    {
      if (i != -1)
        errno = EINVAL;
      return -1;
    }

    /* write out extra blocks containing long name */
    for (j = (sz / T_BLOCKSIZE) + (sz % T_BLOCKSIZE ? 1 : 0),
         ptr = t->th_buf.gnu_longname; j > 1;
         j--, ptr += T_BLOCKSIZE)
    {
      i = tar_block_write(t, ptr);
      if (i != T_BLOCKSIZE)
      {
        if (i != -1)
          errno = EINVAL;
        return -1;
      }
    }
    memset(buf, 0, T_BLOCKSIZE);
    strncpy(buf, ptr, T_BLOCKSIZE);
    i = tar_block_write(t, &buf);
    if (i != T_BLOCKSIZE)
    {
      if (i != -1)
        errno = EINVAL;
      return -1;
    }

    /* reset type and size to original values */
    t->th_buf.typeflag = type2;
    th_set_size(t, sz2);
  }

  th_finish(t);

#ifdef DEBUG
  /* print tar header */
  th_print(t);
#endif

  i = tar_block_write(t, &(t->th_buf));
  if (i != T_BLOCKSIZE)
  {
    if (i != -1)
      errno = EINVAL;
    return -1;
  }

#ifdef DEBUG
  puts("th_write(): returning 0");
#endif
  return 0;
}