/* cd5.c - checksum multi track cdrom
 *
 * Copyright (C) 2007 Yann Droneaud <ydroneaud@meuh.org>.
 *
 * 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, 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 *
 */

/*
 * Quick and dirty program to compute checksum of individual track
 *
 * It reports for each track:
 *  - size (read)
 *  - MD5 digest
 *
 * Supports
 *   - multi track CD-ROM
 *   - mode 1
 *   - Linux ioctl()
 *   - libmhash
 *
 * Doesn't support
 *  - DVD
 *  - Audio CD
 *  - Multi session
 *  - Other operating system
 *
 * TODO:
 *  - support options
 *  - support other track format
 *  - support multi session
 *  - support other interfaces (ioctl, packet, ...)
 *  - support other OS
 *
 * Known problem:
 *  - Track are often bigger than the data written inside
 *    The program will report an error at the end of the data
 *    I don't know the way to detect the end of data.
 *    The checksum is good, and the size too.
 *
 * Be aware:
 *  - Track shorter than 4s -> 300 frames -> 614400 bytes are padded
 *    To check the checksum, pad the img/iso file to 614400 and run md5sum
 *
 * Build:
 *  - gcc -W -Wall cd5.c -o cd5 -lmhash
 *
 */

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>

#include <linux/cdrom.h>

#include <mhash.h>

#include <assert.h>

#define VERSION "0.1" /* cd5 version 0.1 */

static char *device = "/dev/cdrom";

struct track
{
  int track_no;      /* track number */
  int track_data;    /* is data ? */
  int track_addr;    /* LBA */
  int track_length;  /* size */
};

union cdrom_read_arg{
  struct cdrom_msf msf;           /* input */
  char buffer[CD_FRAMESIZE];      /* return */
};

static const unsigned char base16[] = "0123456789abcdef";

int main(void)
{
  int i;

  int fd;

  struct cdrom_tochdr tochdr; /* TOC header (track number) */
  struct cdrom_tocentry tocentry; /* TOC entry */

  struct track *tracks;

  int track_count;
  int track_offset;

  int caps; /* capability */

  fd = open(device, O_RDONLY | O_NONBLOCK);

  if (fd == -1) {
    fprintf(stderr, "open(%s) failed: %s\n", device, strerror(errno));
    return 1;
  }

  /* This test that the device openned is CDROM compatible */
  caps = ioctl(fd, CDROM_GET_CAPABILITY);
  if (caps == -1) {
    fprintf(stderr, "ioctl(%s, CDROM_GET_CAPABILITY) failed: %s\n", device, strerror(errno));

    close(fd);    
  }

  /* Read number of track */
  if (ioctl(fd, CDROMREADTOCHDR, &tochdr) != 0) {
    fprintf(stderr, "ioctl(%s, CDROMREADTOCHDR) failed: %s\n", device, strerror(errno));

    close(fd);

    return 2;
  }

  track_offset = tochdr.cdth_trk0;
  track_count = 1 + (tochdr.cdth_trk1 - tochdr.cdth_trk0);

  printf("Track %d to %d (%d tracks)\n", 
	 track_offset, track_offset + (track_count - 1),
	 track_count);

  /* allocate memory for the track information */
  tracks = (struct track *) malloc(sizeof(struct track) * track_count);
  if (tracks == NULL) {

    fprintf(stderr, "malloc() failed: %s\n", strerror(errno));
      
    close(fd);
      
    return 1;
  }

  /* Read all track */
  for(i = 0; i < track_count; i++) {

    /* Read track information */
    tocentry.cdte_track  = (__u8)i + track_offset;
    tocentry.cdte_format = CDROM_LBA;
    
    if (ioctl(fd, CDROMREADTOCENTRY, &tocentry) != 0) {
      fprintf(stderr, "ioctl(%s, CDROMREADTOCENTRY(%d)) failed: %s\n", 
	      device, i, strerror(errno));
      
      continue;
    }

    tracks[i].track_no = tocentry.cdte_track;
    tracks[i].track_data = ((tocentry.cdte_ctrl & CDROM_DATA_TRACK) != 0);

    tracks[i].track_addr = tocentry.cdte_addr.lba;

    /* compute track size */
    if (i > 0) {
      tracks[i - 1].track_length = 
	tracks[i].track_addr - tracks[i - 1].track_addr;
    }
  }

  /* Read last track information */
  tocentry.cdte_track  = (__u8)CDROM_LEADOUT;
  tocentry.cdte_format = CDROM_LBA;
  
  if (ioctl(fd, CDROMREADTOCENTRY, &tocentry) != 0) {
    fprintf(stderr, "ioctl(%s, CDROMREADTOCENTRY(CDROM_LEADOUT)) failed: %s\n",
	    device, strerror(errno));
    
    close(fd);
    
    return 3;
  }
  
  /* compute last track size */
  tracks[i - 1].track_length = 
    tocentry.cdte_addr.lba - tracks[i - 1].track_addr;


  /* Now compute check sum for each track */
  for(i = 0; i < track_count; i ++) {

    MHASH md5;
    unsigned char hash[128 / 8]; /* MD5 buffer */

    int j;

    md5 = mhash_init(MHASH_MD5);
    if (md5 == MHASH_FAILED) {
      fprintf(stderr, "Can't setup MD5 hash\n");
      return 5;
    }

    printf(" Track %d, %s, %d + %d",
	   tracks[i].track_no, 
	   (tracks[i].track_data) ? "data" : "audio",
	   tracks[i].track_addr,
	   tracks[i].track_length);

    /* read the track and update digest */
    for(j = 0; j < tracks[i].track_length; j ++) {

      union cdrom_read_arg data;

      int lba;

      lba = tracks[i].track_addr + j;

      /* Convert LBA to MSF format */
      data.msf.cdmsf_frame0 = lba % CD_FRAMES;
      lba /= CD_FRAMES;
      lba += 2;
      data.msf.cdmsf_sec0 = lba % CD_SECS;
      data.msf.cdmsf_min0 = lba / CD_SECS;

      /* Read a block */
      if (ioctl(fd, CDROMREADMODE1, &data) != 0) {
	fprintf(stderr, 
		"ioctl(%s, CDROMREADMODE1(%d)) failed: %s\n",
		device, tracks[i].track_addr + j, strerror(errno));
	
	break;
      }

      /* update digest */
      mhash(md5, &data.buffer, CD_FRAMESIZE);
    }

    /* get digest */
    mhash_deinit(md5, hash);

    /* Print read size */
    printf(" | %d ", j * CD_FRAMESIZE);

    for(j = 0; j < (128 / 8); j ++) {
      printf("%02x", hash[j]);
    }

    printf("\n");
  }

  free(tracks);

  close(fd);
	 
  return 0;
}
