/* iso9660.c:
 *
 * Support for the ISO-9660 filing system.
 *
 * ----------------------------------------------------------------------
 * This code is (C) Copyright 1993 by Frank Munkert.
 * All rights reserved.
 * This software may be freely distributed and redistributed for
 * non-commercial purposes, provided this notice is included.
 * ----------------------------------------------------------------------
 * History:
 * 
 * 12-Oct-93   fmu   Adapted to new VOLUME and CDROM_OBJ structures.
 * 24-Sep-93   fmu   Two further bugs in Seek_Position fixed.
 * 16-Sep-93   fmu   Fixed bug in Seek_Position.
 * 16-Sep-93   fmu   Bugfix: Top level object recognition in CDROM_Info
 *                   had to be changed for Rock Ridge disks.
 */

#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#include <exec/types.h>
#include <exec/memory.h>
#include <clib/exec_protos.h>
#include <clib/utility_protos.h>
#ifdef AZTEC_C
#include <pragmas/exec_lib.h>
#include <pragmas/utility_lib.h>
#endif
#ifdef LATTICE
#include <pragmas/exec_pragmas.h>
#include <pragmas/utility_pragmas.h>
#endif

#include "cdrom.h"
#include "iso9660.h"
#include "rock.h"

extern struct Library *UtilityBase;

t_bool Iso_Is_Top_Level_Object (CDROM_OBJ *);

int iso_errno;

#define VOL(vol,tag) (((t_iso_vol_info *)(vol->vol_info))->tag)
#define OBJ(obj,tag) (((t_iso_obj_info *)(obj->obj_info))->tag)

/* Check whether the given volume uses the ISO 9660 Protocol.
 * The protocol is identified by the sequence
 *            'C' 'D' '0' '0' '1'
 * in the 2nd..6th byte of sector 16.
 *
 * Returns TRUE iff the ISO protocol is used; FALSE otherwise.
 */

t_bool Uses_Iso_Protocol (CDROM *p_cdrom)
{
  if (!Read_Sector (p_cdrom, 16))
    return FALSE;
  
  return strncmp ((char *) p_cdrom->buffer + 1, "CD001", 5) == 0;
}

t_bool Iso_Init_Vol_Info (VOLUME *p_volume, int p_skip)
{
  long loc = 16;
  extern t_handler g_iso_handler, g_rr_handler;

  if (p_volume->protocol == PRO_ISO)
    p_volume->handler = &g_iso_handler;
  else
    p_volume->handler = &g_rr_handler;

  p_volume->vol_info = AllocMem (sizeof (t_iso_vol_info), MEMF_PUBLIC);
  if (!p_volume->vol_info) {
    iso_errno = ISOERR_NO_MEMORY;
    return FALSE;
  }

  for (;;) {
    if (!Read_Sector (p_volume->cd, loc)) {
      iso_errno = ISOERR_SCSI_ERROR;
      FreeMem (p_volume->vol_info, sizeof (t_iso_vol_info));
      return FALSE;
    }

    if (p_volume->cd->buffer[0] == 1) {
      memcpy (&VOL(p_volume,pvd), p_volume->cd->buffer, sizeof (prim_vol_desc));
      break;
    }
    
    if (p_volume->cd->buffer[0] == 255 || loc > 1000) {
      iso_errno = ISOERR_NO_PVD;
      FreeMem (p_volume->vol_info, sizeof (t_iso_vol_info));
      return FALSE;
    }
    
    loc++;
  }
  
  VOL(p_volume,skip) = p_skip;
  
  return TRUE;
}

void Iso_Close_Vol_Info (VOLUME *p_volume)
{
  FreeMem (p_volume->vol_info, sizeof (t_iso_vol_info));
}

CDROM_OBJ *Iso_Alloc_Obj (int p_length_of_dir_record)
{
  CDROM_OBJ *obj;

  obj = AllocMem (sizeof (CDROM_OBJ), MEMF_PUBLIC | MEMF_CLEAR);
  if (!obj) {
    iso_errno = ISOERR_NO_MEMORY;
    return NULL;
  }

  obj->obj_info = AllocMem (sizeof (t_iso_obj_info), MEMF_PUBLIC);
  if (!obj->obj_info) {
    FreeMem (obj, sizeof (CDROM_OBJ));
    return NULL;
  }

  OBJ(obj,dir) = AllocMem (p_length_of_dir_record, MEMF_PUBLIC);
  if (!OBJ(obj,dir)) {
    iso_errno = ISOERR_NO_MEMORY;
    FreeMem (obj->obj_info, sizeof (t_iso_obj_info));
    FreeMem (obj, sizeof (CDROM_OBJ));
    return NULL;
  }

  return obj;
}

/* Get the "CDROM object" for the root directory of the volume.
 */

CDROM_OBJ *Iso_Open_Top_Level_Directory (VOLUME *p_volume)
{
  CDROM_OBJ *obj;

  obj = Iso_Alloc_Obj (VOL(p_volume,pvd).root.length);
  if (!obj)
    return NULL;

  obj->directory_f = TRUE;
  obj->volume = p_volume;
  obj->pos = 0;
  memcpy (OBJ(obj,dir), &VOL(p_volume,pvd).root, VOL(p_volume,pvd).root.length);

  return obj;
}

/* Test on equality of directory names (ignoring case).
 */

int Directory_Names_Equal (char *p_iso_name, int p_length, char *p_name)
{
  return Strncasecmp (p_iso_name, p_name, p_length) == 0 &&
  	 p_name[p_length] == 0;
}

/* Compare the name of the directory entry p_iso_name (with length p_length)
 * with the C string p_name, and return 1 iff both strings are equal.
 * NOTE: p_iso_name may be a file name (with version number) or a directory
 *       name (without version number).
 */

int Names_Equal (char *p_iso_name, int p_length, char *p_name)
{
  if (!strchr (p_name, ';')) {
    /* compare without version number: */
    int pos = p_length-1;

    for (; pos>=0; pos--)
      if (p_iso_name[pos] == ';')
        break;
    
    if (pos>=0)
      p_length = pos;
      
  }
  return (Strncasecmp (p_iso_name, p_name, p_length) == 0 &&
  	  p_name[p_length] == 0);
}

/* Get a record from a directory.
 */

directory_record *Get_Directory_Record (VOLUME *p_volume,
					unsigned long p_location,
					unsigned long p_offset)
{
  static unsigned char result[256];
  int len;
  int loc;
  
  loc = p_location + (p_offset >> 11);
  if (!Read_Sector (p_volume->cd, loc)) {
    iso_errno = ISOERR_SCSI_ERROR;
    return NULL;
  }

  len = p_volume->cd->buffer[p_offset & 2047];
  if (len)
    memcpy (result, p_volume->cd->buffer + (p_offset & 2047), len);
  else
    result[0] = 0;  /* mark as last record */
  
  return (directory_record *) result;
}

/* Create a "CDROM object" for the directory which is located
 * at sector p_location.
 */

CDROM_OBJ *Iso_Create_Directory_Obj (VOLUME *p_volume, unsigned long p_location)
{
  directory_record *dir;
  unsigned long loc;
  int offset = 0;
  CDROM_OBJ *obj;
  unsigned long len;

  if (p_location == VOL(p_volume,pvd).root.extent_loc_m)
    return Iso_Open_Top_Level_Directory (p_volume);

  dir = Get_Directory_Record (p_volume, p_location, 0);
  if (!dir)
    return NULL;
  
  dir = Get_Directory_Record (p_volume, p_location, dir->length);
  if (!dir)
    return NULL;
  
  loc = dir->extent_loc_m;
  len = dir->data_length_m;
  for (;;) {
    if (offset >= len)
      return NULL;
    dir = Get_Directory_Record (p_volume, loc, offset);
    if (!dir)
      return NULL;
    if (!dir->length) {
      /* go to next logical sector: */
      offset = (offset & 0xfffff800) + 2048;
      continue;
    }
    if (dir->extent_loc_m == p_location)
      break;
    offset += dir->length;
  }

  obj = Iso_Alloc_Obj (dir->length);
  if (!obj)
    return NULL;

  obj->directory_f = TRUE;
  obj->volume = p_volume;
  obj->pos = 0;
  memcpy (OBJ(obj,dir), dir, dir->length);

  return obj;
}


/* Open the object with name p_name in the directory p_dir.
 * p_name must not contain '/' or ':' characters.
 */

CDROM_OBJ *Iso_Open_Obj_In_Directory (CDROM_OBJ *p_dir, char *p_name)
{
  unsigned long loc = OBJ(p_dir,dir)->extent_loc_m;
  unsigned long len = OBJ(p_dir,dir)->data_length_m;
  directory_record *dir;
  int offset;
  CDROM_OBJ *obj;

  /* skip first two entries: */

  dir = Get_Directory_Record (p_dir->volume, loc, 0);
  if (!dir)
    return NULL;
  
  offset = dir->length;
  dir = Get_Directory_Record (p_dir->volume, loc, offset);
  if (!dir)
    return NULL;

  offset += dir->length;
  for (;;) {
    if (offset >= len)
      return NULL;
    dir = Get_Directory_Record (p_dir->volume, loc, offset);
    if (!dir)
      return NULL;
    if (!dir->length) {
      /* go to next logical sector: */
      offset = (offset & 0xfffff800) + 2048;
      continue;
    }
    if (p_dir->volume->protocol == PRO_ROCK) {
      char buf[256];
      int len;

      if ((len = Get_RR_File_Name (p_dir->volume, dir, buf, sizeof (buf))) > 0 &&
          Strncasecmp (buf, p_name, len) == 0 &&
	  p_name[len] == 0)
	break;
    }

    if (Names_Equal (dir->file_id, dir->file_id_length, p_name))
      break;
    offset += dir->length;
  }

  obj = Iso_Alloc_Obj (dir->length);
  if (!obj)
    return NULL;

  obj->directory_f = (dir->flags & 2);
  obj->volume = p_dir->volume;
  obj->pos = 0;
  memcpy (OBJ(obj,dir), dir, dir->length);

  return obj;
}

/* Close a "CDROM object" and deallocate all associated resources.
 */

void Iso_Close_Obj (CDROM_OBJ *p_object)
{
  FreeMem (OBJ(p_object,dir), OBJ(p_object,dir)->length);
  FreeMem (p_object->obj_info, sizeof (t_iso_obj_info));
  FreeMem (p_object, sizeof (CDROM_OBJ));
}

/* Read bytes from a file.
 */

int Iso_Read_From_File (CDROM_OBJ *p_file, char *p_buffer, int p_buffer_length)
{
  unsigned long loc;
  int remain_block, remain_file, remain;
  int len;
  CDROM *cd;
  int pos;
  int buf_pos = 0;
  int todo;
  unsigned long last_loc;

  if (p_file->pos == OBJ(p_file,dir)->data_length_m)
    /* at end of file: */
    return 0;

  cd = p_file->volume->cd;
  loc = OBJ(p_file,dir)->extent_loc_m + (p_file->pos >> 11);
  last_loc = OBJ(p_file,dir)->extent_loc_m +
  	     ((OBJ(p_file,dir)->data_length_m-1) >> 11);
  todo = p_buffer_length;

  while (todo) {
    if (!Read_Contiguous_Sectors (cd, loc, last_loc)) {
      iso_errno = ISOERR_SCSI_ERROR;
      return -1;
    }

    remain_block = 2048 - (pos = (p_file->pos & 2047));
    remain_file = OBJ(p_file,dir)->data_length_m - p_file->pos;
    remain = (remain_block < remain_file) ? remain_block : remain_file;
    len = (todo < remain) ? todo : remain;
    CopyMem ((APTR) (cd->buffer + pos), (APTR) (p_buffer + buf_pos), len);
    buf_pos += len;
    p_file->pos += len;
    todo -= len;
    
    if (p_file->pos >= OBJ(p_file,dir)->data_length_m)
      break;

    loc++;
  }

  return buf_pos;
}

t_ulong Extract_Date (directory_record *p_dir_record)
{
  struct ClockData ClockData;

  ClockData.sec   = p_dir_record->second;
  ClockData.min	  = p_dir_record->minute;
  ClockData.hour  = p_dir_record->hour;
  ClockData.mday  = p_dir_record->day;
  ClockData.wday  = 0; /* is ignored by CheckDate() and Date2Amiga() */
  ClockData.month = p_dir_record->month;
  ClockData.year  = p_dir_record->year + 1900;

  if (CheckDate (&ClockData))
    return Date2Amiga (&ClockData);
  else
    return 0;
}

/* Return information on a "CDROM object."
 */

int Iso_CDROM_Info (CDROM_OBJ *p_obj, CDROM_INFO *p_info)
{
  int len;

  if (Iso_Is_Top_Level_Object (p_obj)) {
    p_info->name_length = 1;
    p_info->name[0] = ':';
    p_info->directory_f = TRUE;
    p_info->file_length = 0;
    p_info->date = Volume_Creation_Date (p_obj->volume);
  } else {
    if (p_obj->volume->protocol == PRO_ROCK &&
        (len = Get_RR_File_Name (p_obj->volume, OBJ(p_obj,dir),
      				 p_info->name, sizeof (p_info->name))) > 0) {
      p_info->name_length = len;
    } else {
      p_info->name_length = OBJ(p_obj,dir)->file_id_length;
      memcpy (p_info->name, OBJ(p_obj,dir)->file_id, p_info->name_length);
    }
    p_info->directory_f = p_obj->directory_f;
    p_info->file_length = OBJ(p_obj,dir)->data_length_m;
    p_info->date = Extract_Date (OBJ(p_obj,dir));
  }

  return 1;
}

/* Browse all entries in a directory.
 */

int Iso_Examine_Next (CDROM_OBJ *p_dir, CDROM_INFO *p_info,
		      unsigned long *p_offset)
{
  unsigned long offset;
  directory_record *rec;
  int len;

  if (!p_dir->directory_f) {
    iso_errno = ISOERR_BAD_ARGUMENTS;
    return 0;
  }

  if (*p_offset == 0) {
    /* skip first two directory entries: */
  
    rec = Get_Directory_Record (p_dir->volume,
    			        OBJ(p_dir,dir)->extent_loc_m,
			        0);
    if (!rec)
      return 0;
  
    offset = rec->length;
  
    rec = Get_Directory_Record (p_dir->volume,
  			        OBJ(p_dir,dir)->extent_loc_m,
			        offset);
    if (!rec)
      return 0;
  
    *p_offset = offset + rec->length;
  }

  for (;;) {
    if (OBJ(p_dir,dir)->data_length_m <= *p_offset)
      return 0;

    rec = Get_Directory_Record (p_dir->volume,
  			        OBJ(p_dir,dir)->extent_loc_m,
			        *p_offset);
    if (!rec)
      return 0;
  
    if (rec->length == 0)
      /* go to next logical sector: */
      *p_offset = (*p_offset & 0xfffff800) + 2048;
    else
      break;
  }

  *p_offset += rec->length;

  if (p_dir->volume->protocol == PRO_ROCK &&
      (len = Get_RR_File_Name (p_dir->volume, rec,
      			       p_info->name, sizeof (p_info->name))) > 0) {
      p_info->name_length = len;
  } else {
    p_info->name_length = rec->file_id_length;
    memcpy (p_info->name, rec->file_id, rec->file_id_length);
  }
  p_info->directory_f = rec->flags & 2;
  p_info->file_length = rec->data_length_m;
  p_info->date = Extract_Date (rec);

  return 1;
}

/* Clone a "CDROM object info."
 */

void *Iso_Clone_Obj_Info (void *p_info)
{
  t_iso_obj_info *info = (t_iso_obj_info *) p_info;
  t_iso_obj_info *new;
  
  new = AllocMem (sizeof (t_iso_obj_info), MEMF_PUBLIC);
  if (!new)
    return NULL;

  memcpy (new, info, sizeof (t_iso_obj_info));

  new->dir = AllocMem (info->dir->length, MEMF_PUBLIC);
  if (!new->dir) {
    FreeMem (new, sizeof (t_iso_obj_info));
    return NULL;
  }
  memcpy (new->dir, info->dir, info->dir->length);

  return new;
}

/* Find parent directory.
 */

CDROM_OBJ *Iso_Find_Parent (CDROM_OBJ *p_object)
{
  directory_record *dir;
  
  dir = Get_Directory_Record (p_object->volume,
  			      OBJ(p_object,dir)->extent_loc_m,
			      0);
  if (!dir)
    return NULL;
  
  dir = Get_Directory_Record (p_object->volume,
			      OBJ(p_object,dir)->extent_loc_m,
			      dir->length);
  if (!dir)
    return NULL;

  return Iso_Create_Directory_Obj (p_object->volume, dir->extent_loc_m);
}

/* Test if p_object is the root directory.
 */

t_bool Iso_Is_Top_Level_Object (CDROM_OBJ *p_object)
{
  return p_object->directory_f &&
         OBJ(p_object,dir)->extent_loc_m ==
	 VOL(p_object->volume,pvd).root.extent_loc_m;
}

/* Test if two objects are equal.
 */

t_bool Iso_Same_Objects (CDROM_OBJ *p_obj1, CDROM_OBJ *p_obj2)
{
  return (OBJ(p_obj1,dir)->extent_loc_m ==
	  OBJ(p_obj2,dir)->extent_loc_m);
}

/*
 * Convert p_num digits into an integer value:
 */

int Digs_To_Int (char *p_digits, int p_num)
{
  int result = 0;
  int i;
  
  for (i=0; i<p_num; i++)
    result = result * 10 + p_digits[i] - '0';
    
  return result;
}

/*
 * Return volume creation date as number of seconds since 1-Jan-1978:
 */

t_ulong Iso_Creation_Date (VOLUME *p_volume)
{
  struct ClockData ClockData;
  char *dt = VOL(p_volume,pvd).vol_creation;

  ClockData.sec   = Digs_To_Int (dt+12, 2);
  ClockData.min	  = Digs_To_Int (dt+10, 2);
  ClockData.hour  = Digs_To_Int (dt+8, 2);
  ClockData.mday  = Digs_To_Int (dt+6, 2);
  ClockData.wday  = 0; /* is ignored by CheckDate() and Date2Amiga() */
  ClockData.month = Digs_To_Int (dt+4, 2);
  ClockData.year  = Digs_To_Int (dt, 4);

  if (CheckDate (&ClockData))
    return Date2Amiga (&ClockData);
  else
    return 0;
}

t_ulong Iso_Volume_Size (VOLUME *p_volume)
{
  return VOL(p_volume,pvd).space_size_m;
}

void Iso_Volume_ID (VOLUME *p_volume, char *p_buffer, int p_buf_length)
{
  char *iso_name = VOL(p_volume,pvd).volume_id;
  int iso_len;
  int len;
  
  for (iso_len = 32; iso_len; iso_len--) {
    if (iso_name[iso_len-1] != ' ')
      break;
  }

  len = (iso_len > p_buf_length-1) ? p_buf_length-1 : iso_len;
  memcpy (p_buffer, iso_name, len);
  p_buffer[len] = 0;
}

t_ulong Iso_Location (CDROM_OBJ *p_object)
{
  return OBJ(p_object,dir)->extent_loc_m;
}

t_ulong Iso_File_Length (CDROM_OBJ *p_obj)
{
  return OBJ(p_obj,dir)->data_length_m;
}

t_handler g_iso_handler = {
  Iso_Close_Vol_Info,
  Iso_Open_Top_Level_Directory,
  Iso_Open_Obj_In_Directory,
  Iso_Find_Parent,
  Iso_Close_Obj,
  Iso_Read_From_File,
  Iso_CDROM_Info,
  Iso_Examine_Next,
  Iso_Clone_Obj_Info,
  Iso_Is_Top_Level_Object,
  Iso_Same_Objects,
  Iso_Creation_Date,
  Iso_Volume_Size,
  Iso_Volume_ID,
  Iso_Location,
  Iso_File_Length
};

t_handler g_rr_handler = {
  Iso_Close_Vol_Info,
  Iso_Open_Top_Level_Directory,
  Iso_Open_Obj_In_Directory,
  Iso_Find_Parent,
  Iso_Close_Obj,
  Iso_Read_From_File,
  Iso_CDROM_Info,
  Iso_Examine_Next,
  Iso_Clone_Obj_Info,
  Iso_Is_Top_Level_Object,
  Iso_Same_Objects,
  Iso_Creation_Date,
  Iso_Volume_Size,
  Iso_Volume_ID,
  Iso_Location,
  Iso_File_Length
};

