/*
 *   zsync - client side rsync over http
 *   Copyright (C) 2004,2005 Colin Phipps <cph@moria.org.uk>
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the Artistic License v2 (see the accompanying 
 *   file COPYING for the full license terms), or, at your option, any later 
 *   version of the same license.
 *
 *   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
 *   COPYING file for details.
 */

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>

#include <arpa/inet.h>

#include "config.h"
#include "librcksum/rcksum.h"
#include "zsync.h"
#include "sha1.h"
#include "zmap.h"
#include "../url.h"
#include "zlib/zlib.h"

struct zsync_state {
  struct rcksum_state* rs;
  off64_t filelen;
  int blocks;
  long blocksize;

  char* checksum;
  const char* checksum_method;

  char** url;
  int nurl;

  struct zmap* zmap;
  char** zurl;
  int nzurl;

  char* cur_filename; /* If we have taken the filename from rcksum, it is here */
  char* filename;     /* This is just the Filename: header from the .zsync */
  char* zfilename;    /* ditto Z-Filename: */

  char* gzopts;
  char* gzhead;
};

static char** append_ptrlist(int *n, char** p, char* a) {
  if (!a) return p;
  p = realloc(p,(*n + 1) * sizeof *p);
  if (!p) { fprintf(stderr,"out of memory\n"); exit(1); }
  p[*n] = a;
  (*n)++;
  return p;
}

struct zsync_state* zsync_begin(FILE* f)
{
  struct zsync_state* zs = calloc(sizeof *zs,1);
  int checksum_bytes = 16,rsum_bytes = 4,seq_matches=1;
  char* safelines = NULL;

  if (!zs) return NULL;

  for (;;) {
    char buf[1024];
    char *p = NULL;
    int l;

    if (fgets(buf, sizeof(buf), f) != NULL) {
      if (buf[0] == '\n') break;
      l = strlen(buf) - 1;
      while (l >= 0 && (buf[l] == '\n' || buf[l] == '\r' || buf[l] == ' '))
	buf[l--] = 0;

      p = strchr(buf,':');
    }
    if (p && *(p+1) == ' ') {
      *p++ = 0;
      p++;
      if (!strcmp(buf, "zsync")) {
	if (!strcmp(p,"0.0.4")) {
	  fprintf(stderr,"This version of zsync is not compatible with zsync 0.0.4 streams.\n");
	  free(zs); return NULL;
	}
      } else if (!strcmp(buf, "Min-Version")) {
	if (strcmp(p,VERSION) > 0) {
	  fprintf(stderr,"control file indicates that zsync-%s or better is required\n",p);
	  free(zs); return NULL;
	}
      } else if (!strcmp(buf, "Length")) {
	zs->filelen = atol(p);
      } else if (!strcmp(buf, "Filename")) {
	zs->filename = strdup(p);
      } else if (!strcmp(buf, "Z-Filename")) {
	zs->zfilename = strdup(p);
      } else if (!strcmp(buf, "URL")) {
	zs->url = (char**)append_ptrlist(&(zs->nurl), zs->url, strdup(p));
      } else if (!strcmp(buf, "Z-URL")) {
	zs->zurl = (char**)append_ptrlist(&(zs->nzurl), zs->zurl, strdup(p));
      } else if (!strcmp(buf, "Blocksize")) {
	zs->blocksize = atol(p);
	if (zs->blocksize < 0 || (zs->blocksize & (zs->blocksize-1))) {
	  fprintf(stderr,"nonsensical blocksize %ld\n",zs->blocksize); 
	  free(zs); return NULL;
	}
      } else if (!strcmp(buf, "Hash-Lengths")) {
        if (sscanf(p,"%d,%d,%d",&seq_matches,&rsum_bytes,&checksum_bytes) != 3 || rsum_bytes < 1 || rsum_bytes > 4 || checksum_bytes < 3 || checksum_bytes > 16 || seq_matches > 2 || seq_matches < 1) {
	  fprintf(stderr,"nonsensical hash lengths line %s\n",p);
	  free(zs); return NULL;
	}
      } else if (zs->blocks && !strcmp(buf,"Z-Map2")) {
	int nzblocks;
	struct gzblock* zblock;

	nzblocks = atoi(p);
	if (nzblocks < 0) {
	  fprintf(stderr,"bad Z-Map line\n");
	  free(zs); return NULL;
	}

	zblock = malloc(nzblocks * sizeof *zblock);
	if (zblock) {
	  if (fread(zblock,sizeof *zblock,nzblocks,f) < nzblocks) {
	    fprintf(stderr,"premature EOF after Z-Map\n"); 
	    free(zs); return NULL;
	  }

	  zs->zmap = zmap_make(zblock,nzblocks);
	  free(zblock);
	}
      } else if (!strcmp(buf,"SHA-1")) {
	zs->checksum = strdup(p);
	zs->checksum_method = "SHA-1";
      } else if (!strcmp(buf,"Safe")) {
	safelines = strdup(p);
      } else if (!strcmp(buf,"Recompress")) {
	zs->gzhead = strdup(p);
	if (zs->gzhead) {
	  char *q = strchr(zs->gzhead,' ');
	  if (!q)
	    q = zs->gzhead + strlen(zs->gzhead);
	  {
	    if (*q) *q++ = 0;
	    /* Whitelist for safe options for gzip command line */
	    if (!strcmp(q,"--best") || !strcmp(q,"--rsync --best") || !strcmp(q,"--rsync") || !strcmp(q,""))
	      zs->gzopts = strdup(q);
	    else {
	      fprintf(stderr,"bad recompress options, rejected\n");
	      free(zs->gzhead);
	    }
	  }
	}
      } else if (!safelines || !strstr(safelines,buf)) {
	fprintf(stderr,"unrecognised tag %s - you need a newer version of zsync.\n",buf);
	free(zs); return NULL;
      }
      if (zs->filelen && zs->blocksize)
	zs->blocks = (zs->filelen + zs->blocksize-1)/zs->blocksize;
    } else {
      fprintf(stderr, "Bad line - not a zsync file? \"%s\"\n", buf);
      free(zs); return NULL;
    }
  }
  if (!zs->filelen || !zs->blocksize) {
    fprintf(stderr,"Not a zsync file (looked for Blocksize and Length lines)\n");
    free(zs); return NULL;
  }
  if (!(zs->rs = rcksum_init(zs->blocks, zs->blocksize, rsum_bytes, checksum_bytes, seq_matches))) {
    free(zs); return NULL;
  }
  {
    zs_blockid id = 0;
    for (;id < zs->blocks; id++) {
      struct rsum r = { 0,0 };
      unsigned char checksum[CHECKSUM_SIZE];

      if (fread(((char*)&r)+4-rsum_bytes,rsum_bytes,1,f) < 1 || fread((void*)&checksum,checksum_bytes,1,f) < 1) {
	fprintf(stderr,"short read on control file; %s\n",strerror(ferror(f)));
	rcksum_end(zs->rs);
	free(zs); return NULL;
      }
      r.a = ntohs(r.a); r.b = ntohs(r.b);
      rcksum_add_target_block(zs->rs, id, r, checksum);
    }
  }
  return zs;
}

int zsync_hint_decompress(const struct zsync_state* zs)
{
  return (zs->nzurl > 0 ? 1 : 0);
}

int zsync_blocksize(struct zsync_state* zs)
{ return zs->blocksize; }

char* zsync_filename(const struct zsync_state* zs)
{ return strdup(zs->gzhead && zs->zfilename ? zs->zfilename : zs->filename); }

int zsync_status(struct zsync_state* zs)
{
  int todo = rcksum_blocks_todo(zs->rs);

  if (todo == zs->blocks) return 0;
  if (todo > 0) return 1;
  return 2; /* TODO: more? */
}

void zsync_progress(const struct zsync_state* zs, long long* got, long long* total)
{

  if (got) {
    int todo = zs->blocks - rcksum_blocks_todo(zs->rs);
    *got = todo * zs->blocksize;
  }
  if (total) *total = zs->blocks * zs->blocksize;
}

const char * const * zsync_get_urls(struct zsync_state* zs, int* n, int* t)
{
  if (zs->zmap && zs->nzurl) {
    *n = zs->nzurl; *t = 1;
    return zs->zurl;
  } else {
    *n = zs->nurl; *t = 0;
    return zs->url;
  }
}

off64_t* zsync_needed_byte_ranges(struct zsync_state* zs, int* num, int type)
{
  int nrange;
  zs_blockid* blrange;
  off64_t* byterange;
  int i;
  
  blrange = rcksum_needed_block_ranges(zs->rs, &nrange, 0, 0x7fffffff);
  if (!blrange) return NULL;

  byterange = malloc(2 * nrange * sizeof *byterange);
  if (!byterange) { free(blrange); return NULL; }

  for (i=0; i<nrange; i++) {
    byterange[2*i] = blrange[2*i] * zs->blocksize;
    byterange[2*i+1] = blrange[2*i+1] * zs->blocksize-1;
  }
  free(blrange);

  switch (type) {
  case 0:
    *num = nrange;
    return byterange;
  case 1:
    {
      off64_t* zbyterange = zmap_to_compressed_ranges(zs->zmap, byterange, nrange, &nrange);
      if (zbyterange) {
	*num = nrange;
      }
      free(byterange);
      return zbyterange;
    }
  default:
    free(byterange); return NULL;
  }
}

int zsync_submit_source_file(struct zsync_state* zs, FILE* f, int progress)
{
  return rcksum_submit_source_file(zs->rs, f, progress);
}

char* zsync_cur_filename(struct zsync_state* zs)
{
  if (!zs->cur_filename)
    zs->cur_filename = rcksum_filename(zs->rs);

  return zs->cur_filename;
}

int zsync_rename_file(struct zsync_state* zs, const char* f)
{
  char* rf = zsync_cur_filename(zs);

  int x = rename(rf, f);

  if (!x) { free(rf); zs->cur_filename = strdup(f); }
  else perror("rename");

  return x;
}

static int hexdigit(char c) {
  return (isdigit(c) ? (c-'0') : isupper(c) ? (0xa + (c-'A')) : islower(c) ? (0xa + (c-'a')) : 0);
}

int zsync_complete(struct zsync_state* zs)
{
  int fh = rcksum_filehandle(zs->rs);
  int rc;

  zsync_cur_filename(zs);
  rcksum_end(zs->rs); zs->rs = NULL;
    
  if (ftruncate(fh,zs->filelen) != 0) { perror("ftruncate"); return -1; }
  if (lseek(fh,0,SEEK_SET) != 0) { perror("lseek"); return -1; }
  if (zs->checksum && !strcmp(zs->checksum_method,"SHA-1")) {
    SHA1_CTX shactx;

    if (strlen(zs->checksum) != SHA1_DIGEST_LENGTH*2) {
      fprintf(stderr,"SHA-1 digest from control file is wrong length.\n");
      return -1;
    }
    {
      char buf[4096];
      int rc;
      
      SHA1Init(&shactx);
      while (0 < (rc = read(fh,buf,sizeof buf))) {
	SHA1Update(&shactx,buf,rc);
      }
      if (rc < 0) { perror("read"); close(fh); return -1; }
    }
    close(fh);
    {
      unsigned char digest[SHA1_DIGEST_LENGTH];
      int i;

      SHA1Final(digest, &shactx);

      for (i=0; i<SHA1_DIGEST_LENGTH; i++) {
	int j;
	sscanf(&(zs->checksum[2*i]),"%2x",&j);
	if (j != digest[i]) {
	  return -1;
	}
      }
    }
    rc = 1;
  }
  else
  {
    rc = 0;
  }
  /* Recompression. This is a fugly mess, calling gzip on the temporary file with options
   *  read out of the .zsync, reading its output and replacing the gzip header. Ugh. */
  if (zs->gzhead && zs->gzopts) {
    FILE* g;
    FILE* zout;
    
    char cmd[1024];
    snprintf(cmd,sizeof(cmd),"gzip -n %s < ",zs->gzopts);
    
    {
      int i=0;
      int j = strlen(cmd);
      char c;
      
      while ((c = zs->cur_filename[i++]) != 0 && j < sizeof(cmd) - 2) {
	if (!isalnum(c)) cmd[j++] = '\\';
	cmd[j++] = c;
      }
      cmd[j] = 0;
    }
    
    g = popen(cmd,"r");
    if (g) {
      char zoname[1024];
      
      snprintf(zoname,sizeof(zoname),"%s.gz",zs->cur_filename);
      zout=fopen(zoname,"w");
      
      if (zout) {
	char *p = zs->gzhead;
	int skip = 1;
	
	while (p[0] && p[1]) {
	  if (fputc((hexdigit(p[0]) << 4)+hexdigit(p[1]), zout) == EOF) {
	    perror("putc"); rc = -1;
	  }
	  p+= 2;
	}
	while (!feof(g)) {
	  char buf[1024];
	  int r;
	  char *p = buf;
	  
	  if ((r = fread(buf,1,sizeof(buf),g)) < 0) {
	    perror("fread"); rc = -1; goto leave_it;
	  }
	  if (skip) { p = skip_zhead(buf); skip = 0; }
	  if (fwrite(p,1,r - (p-buf),zout) != r - (p-buf)) {
	    perror("fwrite"); rc = -1; goto leave_it;
	  }
	}
	
      leave_it:
	if (fclose(zout) != 0) { perror("close"); rc = -1; }
      }
      if (fclose(g) != 0) { perror("close"); rc = -1; }

      unlink(zs->cur_filename);
      free(zs->cur_filename);
      zs->cur_filename = strdup(zoname);
    } else {
      fprintf(stderr,"problem with gzip, unable to compress.\n");
    }
  }
  return rc;
}

char* zsync_end(struct zsync_state* zs)
{
  char *f = zsync_cur_filename(zs);

  if (zs->rs) rcksum_end(zs->rs);
  if (zs->zmap) zmap_free(zs->zmap);
  {
    int i;
    for (i=0; i<zs->nurl; i++) free(zs->url[i]);
    for (i=0; i<zs->nzurl; i++) free(zs->zurl[i]);
  }
  free(zs->url); free(zs->zurl); free(zs->checksum);
  free(zs->filename); free(zs->zfilename);
  free(zs);
  return f;
}

/* Functions for receiving data from supplied URLs below */

void zsync_configure_zstream_for_zdata(const struct zsync_state* zs, struct z_stream_s* zstrm, long zoffset, long long* poutoffset)
{
  configure_zstream_for_zdata(zs->zmap, zstrm, zoffset, poutoffset);
  { /* Load in prev 32k sliding window for backreferences */
    long long pos = *poutoffset;
    int lookback = (pos > 32768) ? 32768 : pos;
    char wbuf[32768];
    
    rcksum_read_known_data(zs->rs, wbuf, pos-lookback,lookback);
    /* Fake an output buffer of 32k filled with data to zlib */
    zstrm->next_out = wbuf+lookback; zstrm->avail_out = 0;
    updatewindow(zstrm,lookback);
  }
}

struct zsync_receiver {
  struct zsync_state* zs;
  struct z_stream_s strm;
  int url_type;
  char* outbuf;
  off64_t outoffset;
};

static int zsync_submit_data(struct zsync_state* zs, unsigned char* buf, off64_t offset, int blocks)
{
  zs_blockid blstart = offset / zs->blocksize;
  zs_blockid blend = blstart + blocks - 1;

  return rcksum_submit_blocks(zs->rs, buf, blstart, blend);
}

struct zsync_receiver* zsync_begin_receive(struct zsync_state*zs, int url_type)
{
  struct zsync_receiver* zr = malloc(sizeof(struct zsync_receiver));

  if (!zr) return NULL;
  zr->zs = zs;

  zr->outbuf = malloc(zs->blocksize);
  if (!zr->outbuf) { free(zr); return NULL; }

  /* Set up new inflate object */
  zr->strm.zalloc = Z_NULL; zr->strm.zfree = Z_NULL; zr->strm.opaque = NULL;
  zr->strm.total_in = 0;

  zr->url_type = url_type;
  zr->outoffset = 0;

  return zr;
}

int zsync_receive_data(struct zsync_receiver* zr, unsigned char* buf, off64_t offset, size_t len)
{
  int blocksize = zr->zs->blocksize;

  if (zr->url_type == 1) { 
    int ret=0;
    int eoz=0;

    if (!len) return 0;

    /* Now set up for the downloaded block */
    zr->strm.next_in = buf; zr->strm.avail_in = len;
    
    if (zr->strm.total_in == 0 || offset != zr->strm.total_in) {
      zsync_configure_zstream_for_zdata(zr->zs, &(zr->strm), offset, &(zr->outoffset));
      
      /* On first iteration, we might be reading an incomplete block from zsync's point of view. Limit avail_out so we can stop after doing that and realign with the buffer. */
      zr->strm.avail_out = blocksize - (zr->outoffset % blocksize);
      zr->strm.next_out = zr->outbuf;
    } else {
      if (zr->outoffset == -1) { fprintf(stderr,"data didn't align with block boundary in compressed stream\n"); return 1; }
      zr->strm.next_in = buf; zr->strm.avail_in = len;
    }
    
    while (zr->strm.avail_in && !eoz) {
      int rc;
      
      /* Read in up to the next block (in the libzsync sense on the output stream) boundary */

      rc = inflate(&(zr->strm),Z_SYNC_FLUSH);
      switch (rc) {
      case Z_STREAM_END: eoz = 1;
      case Z_BUF_ERROR:
      case Z_OK:
	if (zr->strm.avail_out == 0 || eoz) {
	  /* If this was at the start of a block, try submitting it */
	  if (!(zr->outoffset % blocksize)) {
	    int rc;
	    
	    if (zr->strm.avail_out) memset(zr->strm.next_out,0,zr->strm.avail_out);
	    rc = zsync_submit_data(zr->zs, zr->outbuf, zr->outoffset, 1);
	    if (!zr->strm.avail_out) ret |= rc;
	    zr->outoffset += blocksize;
	  } else {
	    /* We were reading a block fragment; update outoffset, and we are nwo block-aligned. */
	    zr->outoffset += (((char*)(zr->strm.next_out)) - (zr->outbuf));
	  }
	  zr->strm.avail_out = blocksize; zr->strm.next_out = zr->outbuf;
	}
	break;
      default:
	fprintf(stderr,"zlib error: %s (%d)\n",zr->strm.msg, rc);
	eoz=1; ret = -1; break;
      }
    }
    return ret;
  } else {
    int ret = 0;

    if (0 != (offset % blocksize)) {
      size_t x = len;

      if (x > blocksize - (offset % blocksize)) x = blocksize - (offset % blocksize);

      if (zr->outoffset == offset) {
	/* Half-way through a block, so let's try and complete it */
	if (len)
	  memcpy(zr->outbuf + offset % blocksize, buf, x);
	else {
	  // Pad with 0s to length.
	  memset(zr->outbuf + offset % blocksize, 0, len = x = blocksize - (offset % blocksize));
	}

	if ( (x + offset) % blocksize == 0)
	  if (zsync_submit_data(zr->zs, zr->outbuf, zr->outoffset + x - blocksize, blocksize))
	    ret = 1;
      }
      buf += x; len -= x; offset += x;
      if (!len) return 0;
    }

    /* Now we are block-aligned */
    if (len >= blocksize) {
      int w = len / blocksize;

      if (zsync_submit_data(zr->zs, buf, offset, w))
	ret = 1;

      w *= blocksize;
      buf += w; len -= w; offset += w;
      
    }
    /* Store incomplete block */
    if (len) {
      memcpy(zr->outbuf, buf, len);
      offset += len; /* not needed: buf += len; len -= len; */
    }

    zr->outoffset = offset;
    return ret;
  }  
}

void zsync_end_receive(struct zsync_receiver* zr)
{
  if (zr->strm.total_in > 0) { inflateEnd(&(zr->strm)); }
  free(zr->outbuf);
  free(zr);
}



syntax highlighted by Code2HTML, v. 0.9.1