/******************************************************************
 * csscat.c - decrypt DVD .vob file to standard output
 * License: GNU General Public License
 * Usage: csscat [-d dvd-device] file.vob [file.mpeg]
 *****************************************************************/


#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
extern char *optarg;
extern int optind;
#include <string.h>
#include <stdarg.h>
#include <errno.h>

#include <dvdcss/dvdcss.h>
#include "dvd_udf.h"


#define PROGNAME "csscat"
#define BUFFER_BLOCKS	16
#define DEFAULT_DVD_DEVICE "/dev/dvd"
#define MAX_PATH 1024


void fail(char *msg, ...)
{
  va_list ap;
  va_start(ap, msg);
  vfprintf(stderr, msg, ap);
  va_end(ap);
  exit(1);
}

void fail_perr(char *msg, ...)
{
  va_list ap;
  va_start(ap, msg);
  vfprintf(stderr, msg, ap);
  va_end(ap);
  if (errno)
    fprintf(stderr, ": %s", strerror(errno));
  putc('\n', stderr);
  exit(1);
}


/* Remove symbolic links, "..", ".", etc from path. */
char *get_canonical_path(char *path)
{
  static char res[MAX_PATH];
  char *p, buf[MAX_PATH];
  int len;
  
  while (1)
    {
      if ((p = strrchr(path, '/')) != NULL)
	{
	  char tmp = p[1];
	  p[1] = '\0';
	  if (chdir(path) == -1)
	    fail_perr(PROGNAME ": chdir");
	  p[1] = tmp;
	  path = p + 1;
	}
      
      len = readlink(path, buf, MAX_PATH);
      if (len == -1)
	break;
      if (len >= MAX_PATH)
	fail(PROGNAME ": link too long.\n");
      buf[len] = '\0';
      path = buf;
    }
  
  if (getcwd(res, MAX_PATH - 3) == NULL)
    fail_perr(PROGNAME ": getcwd");
  
  len = strlen(res);
  if (len > 1)
    res[len++] = '/';
  strncpy(res + len, path, MAX_PATH - len - 1);
  buf[MAX_PATH - 1] = '\0';

  return res;
}

/* Find path from root of device of file. */
char *get_device_path(char *path, struct stat *path_stat)
{
  struct stat dir_stat;
  char *p, *res;
  
  p = strrchr(path, '/');
  if (p == NULL)
    return path;
  
  res = p+1;
  while (p >= path)
    {
      char tmp = p[1];
      p[1] = '\0';
      if (stat(path, &dir_stat) < 0)
	fail_perr(PROGNAME ": stat");
      p[1] = tmp;
      
      if (dir_stat.st_dev != path_stat->st_dev)
	return res;  /* found it! */
      
      res = p+1;
      while (--p >= path && *p != '/')
	;
    }
  return res;
}

void print_usage()
{
  fprintf(stderr, "usage: "PROGNAME" [-d dvd-device] file.vob [file.mpeg]\n");
  exit(1);
}

int main(int ac, char **av)
{
  dvdcss_handle dvdcss;
  unsigned char p_buffer[DVDCSS_BLOCK_SIZE * BUFFER_BLOCKS];
  unsigned int sector;
  int i, nread, c;
  off_t blocks;
  struct stat vob_stat;
  char *vob_file, *can_vf, *udf_file, save_path[MAX_PATH];
  char *dvd_device = DEFAULT_DVD_DEVICE;
  FILE *out;
  
  while ((c = getopt(ac, av, "d:")) != EOF)
    switch (c)
      {
      case 'd':
	dvd_device = optarg;
        break;
      default:
	print_usage();
      }
  
  if (ac <= optind || optind < ac - 2)
    print_usage();
  
  
  /* Get .vob file size */
  
  vob_file = av[optind++];
  if (stat(vob_file, &vob_stat) == -1)
    fail_perr(PROGNAME ": can't stat %s", vob_file);
  if (! S_ISREG(vob_stat.st_mode))
    fail(PROGNAME ": input must be a regular file.\n");
  
  blocks = (vob_stat.st_size + DVDCSS_BLOCK_SIZE - 1) / DVDCSS_BLOCK_SIZE;
  
  
  /* Initialize libdvdcss */
  
  dvdcss = dvdcss_open(dvd_device);
  if (dvdcss == NULL)
    fail_perr(PROGNAME ": dvdcss_open(%s) failed", dvd_device);
  
  
  /* Save current directory before calling get_canonical_path() */
  
  if (getcwd(save_path, MAX_PATH) == NULL)
    fail_perr(PROGNAME ": getcwd");
  
  
  /* Produce a filename that DVDUDFFindFile will hopefully understand.
   * Should work unless some funny mounting is used? */
  
  can_vf = get_canonical_path(vob_file);
  udf_file = get_device_path(can_vf, &vob_stat);
  
  
  /* Get .vob file location on dvd */
  
  sector = DVDUDFFindFile(dvdcss, udf_file);
  if (sector == 0 && udf_file != vob_file)
    sector = DVDUDFFindFile(dvdcss, vob_file);  /* try cmd line name */
  if (sector == 0)
    fail(PROGNAME ": DVDUDFFindFile(%s) failed.\n", udf_file);
  
  if (dvdcss_seek(dvdcss, sector, DVDCSS_SEEK_KEY) == -1)
    fail_perr(PROGNAME ": dvdcss_seek");
  
  
  /* Open output file */
  
  if (chdir(save_path) == -1)
    fail_perr(PROGNAME ": chdir(%s) failed", save_path);
  
  out = stdout;
  if (optind < ac)
    {
      out = fopen(av[optind], "w");
      if (out == NULL)
	fail_perr(PROGNAME ": can't write to %s", av[optind]);
    }
  

  /* Decrypt .vob file using libdvdcss */

  i = 0;
  while (i < blocks)
    {
      nread = blocks - i;
      if (nread > BUFFER_BLOCKS) 
	nread = BUFFER_BLOCKS;
      
      if ((nread = dvdcss_read(dvdcss, p_buffer, nread, 
			       DVDCSS_READ_DECRYPT)) < 0)
	fail_perr(PROGNAME ": dvdcss_read");
      
      if (fwrite(p_buffer, DVDCSS_BLOCK_SIZE, nread, out) != nread)
	fail_perr(PROGNAME ": fwrite");
      
      i += nread;
    }
  
  dvdcss_close(dvdcss);
  
  return 0;
}
