/* 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:
 * 
 * 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 "cdrom.h"
#include "iso9660.h"
#include "rock.h"

int iso_errno;

/* A "volume" is the basic handle that we'll use for CDROM access.
 * A volume contains all necessary information for finding files on
 * a CDROM.
 */

VOLUME *Open_Volume (CDROM *p_cdrom, int p_use_rock_ridge)
{
  VOLUME *res;
  long loc = 16;
  
  res = AllocMem (sizeof (VOLUME), MEMF_PUBLIC);
  if (!res) {
    iso_errno = ISOERR_NO_MEMORY;
    return NULL;
  }

  res->cd = p_cdrom;
  
  for (;;) {
    if (!Read_Sector (res->cd, loc)) {
      iso_errno = ISOERR_SCSI_ERROR;
      FreeMem (res, sizeof (VOLUME));
      return NULL;
    }

    if (res->cd->buffer[0] == 1) {
      memcpy (&res->pvd, res->cd->buffer, sizeof (prim_vol_desc));
      break;
    }
    
    if (res->cd->buffer[0] == 255 || loc > 1000) {
      iso_errno = ISOERR_NO_PVD;
      FreeMem (res, sizeof (VOLUME));
      return NULL;
    }
    
    loc++;
  }
  
  res->use_rock_ridge = p_use_rock_ridge && Uses_Rock_Ridge_Protocol (res);
  
  res->valid = 1;  /* may be modified by application program */
  res->locks = 0;  /* may be modified by application program */

  return res;
}

/* Close a volume; deallocate all associated resources.
 */

void Close_Volume (VOLUME *p_volume)
{
  FreeMem (p_volume, sizeof (VOLUME));
}

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

CDROM_OBJ *Open_Top_Level_Directory (VOLUME *p_volume)
{
  CDROM_OBJ *obj;
  
  obj = AllocMem (sizeof (CDROM_OBJ), MEMF_PUBLIC);
  if (!obj) {
    iso_errno = ISOERR_NO_MEMORY;
    return NULL;
  }

  obj->directory_f = TRUE;
  obj->volume = p_volume;
  obj->path_table_pos = 1;
  if (obj->volume->use_rock_ridge)
    obj->parent = p_volume->pvd.root.extent_loc_m;
  else
    obj->parent = 1;
  obj->pos = 0;
  
  obj->dir_record = AllocMem (p_volume->pvd.root.length, MEMF_PUBLIC);
  if (!obj->dir_record) {
    iso_errno = ISOERR_NO_MEMORY;
    FreeMem (obj, sizeof (CDROM_OBJ));
    return NULL;
  }
  memcpy (obj->dir_record, &p_volume->pvd.root, p_volume->pvd.root.length);

  return obj;
}

/* Read a record from the path table.
 */

path_table_record *Get_Path_Table_Record (VOLUME *p_volume, unsigned long p_dir,
					  unsigned long *p_offset)
{
  static unsigned char result[256];
  unsigned long loc = p_volume->pvd.m_table;
  unsigned long size = 0;
  int pos = 0;
  int cnt = 1;
  int len;

  if (p_dir) {
    if (!Read_Sector (p_volume->cd, loc)) {
      iso_errno = ISOERR_SCSI_ERROR;
      return NULL;
    }

    while (cnt < p_dir) {
      if (size >= p_volume->pvd.path_size_m) {
        iso_errno = ISOERR_NO_SUCH_RECORD;
        return NULL;
      }
  
      len = 8 + p_volume->cd->buffer[pos];
      if (len & 1)
        len++;
      pos += len;
      size += len;
    
      if (pos >= 2048) {
        if (!Read_Sector (p_volume->cd, ++loc)) {
          iso_errno = ISOERR_SCSI_ERROR;
          return NULL;
        }
        pos -= 2048;
      }
    
      cnt++;
    }
  } else {
    loc += (*p_offset >> 11);
    if (!Read_Sector (p_volume->cd, loc)) {
      iso_errno = ISOERR_SCSI_ERROR;
      return NULL;
    }
    pos = *p_offset & 2047;
    size = *p_offset;
  }

  len = 8 + p_volume->cd->buffer[pos];
  if (len & 1)
    len++;

  if (pos + len <= 2048)
    memcpy (result, p_volume->cd->buffer + pos, len);
  else {
    memcpy (result, p_volume->cd->buffer + pos, 2048 - pos);
    if (!Read_Sector (p_volume->cd, ++loc)) {
      iso_errno = ISOERR_SCSI_ERROR;
      return NULL;
    }
    memcpy (result + (2048 - pos), p_volume->cd->buffer, len - (2048 - pos));
  }

  if (p_offset)
    *p_offset = size + len;

  return (path_table_record *) result;
}

/* Find parent directory.
 */

unsigned long Get_Parent_Directory_ISO (VOLUME *p_volume, unsigned long p_dir)
{
  path_table_record *ptr;

  ptr = Get_Path_Table_Record (p_volume, p_dir, NULL);
  if (!ptr)
    return 1;

  return ptr->parent;
}

/* Strncasecmp works like 'strncmp' but ignores case differences.
 */

int Strncasecmp (char *p_str1, char *p_str2, int p_length)
{
  int i;
  int len = 0;
  
  while (len < p_length && *p_str1 && *p_str2) {
    if (i = tolower (*p_str1++) - tolower (*p_str2++))
      return i;
    len++;
  }
  return (len == p_length) ? 0 : tolower (*p_str1) - tolower (*p_str2);
}

/* 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;
}

/* Find directory in path table.
 */

unsigned long Find_Directory_ISO (VOLUME *p_volume, unsigned long p_parent,
			          char *p_name)
{
  path_table_record *ptr;
  unsigned long rec = p_parent + 1;
  unsigned long offset;
  directory_record *dir;

  ptr = Get_Path_Table_Record (p_volume, rec, &offset);
  for (;;) {
    if (!ptr)
      return 0;

    if (ptr->parent == p_parent &&
        Directory_Names_Equal (ptr->id, ptr->id_length, p_name))
      return rec;

    if (ptr->parent > p_parent) {
      iso_errno = ISOERR_NOT_FOUND;
      return 0;
    }

    rec++;
    /* get next record: */
    ptr = Get_Path_Table_Record (p_volume, 0, &offset);
  }
}

/* Find an entry (directory or file) in a directory.
 */

directory_record *Find_Directory_Entry (VOLUME *p_volume, unsigned long p_location,
			                char *p_name)
{
  unsigned long length;
  directory_record *dir;
  unsigned long offset = 0;
  int rr_len;
  char rr_buf[256];

  if (!(dir = Get_Directory_Record (p_volume, p_location, 0)))
    return NULL;

  length = dir->data_length_m;
  
  for (;;) {
    offset += dir->length;
    for (;;) {
      if (offset >= length) {
        iso_errno = ISOERR_NOT_FOUND;
        return NULL;
      }
      dir = Get_Directory_Record (p_volume, p_location, offset);
      if (!dir)
        return NULL;

      if (dir->length == 0)
        /* goto next logical sector: */
        offset = (offset & 0xfffff800) + 2048;
      else
        break;
    }
    
    if (p_volume->use_rock_ridge) {
      rr_len = Get_RR_File_Name (p_volume, dir, rr_buf, sizeof (rr_buf));
      if (rr_len != -1) {
        if (Strncasecmp (rr_buf, p_name, rr_len) == 0 &&
	    p_name[rr_len] == 0)
	  return dir;
	else
	  continue;
      }
    }

    if (Names_Equal (dir->file_id, dir->file_id_length, p_name))
      return dir;
  }
}

/* Open a "CDROM object" on an ISO-9660 disk.
 */

CDROM_OBJ *Open_Object_ISO (CDROM_OBJ *p_result,
			    CDROM_OBJ *p_current_dir, char *p_name)
{
  CDROM_OBJ *obj = p_result;
  unsigned long path_table_pos = p_current_dir->path_table_pos;
  unsigned long child_pos;
  char *cp = p_name;
  char name[256];
  char *np;
  path_table_record *ptr;
  directory_record *dir;

  if (*cp == ':') {
    path_table_pos = 1;
    cp++;
  } else while (*cp == '/') {
    path_table_pos = Get_Parent_Directory_ISO (p_current_dir->volume, path_table_pos);
    cp++;
  }

  while (*cp) {
    for (np = name; *cp && *cp != '/'; )
      *np++ = *cp++;
    *np = 0;
    
    if (name[0] == 0) {
      iso_errno = ISOERR_ILLEGAL_NAME;
      return NULL;
    }

    child_pos = Find_Directory_ISO (p_current_dir->volume, path_table_pos, name);
    if (!child_pos) {
      if (iso_errno != ISOERR_NOT_FOUND) {
	return NULL;
      }
    
      if (*cp != 0) {
	return NULL;
      }

      ptr = Get_Path_Table_Record (p_current_dir->volume, path_table_pos, NULL);
      if (!ptr)
        return NULL;

      dir = Find_Directory_Entry (p_current_dir->volume, ptr->location, name);
      if (!dir)
	return NULL;

      obj->directory_f = FALSE;
      obj->parent = path_table_pos;
      obj->dir_record = AllocMem (dir->length, MEMF_PUBLIC);
      if (!obj->dir_record) {
        iso_errno = ISOERR_NO_MEMORY;
        return NULL;    
      }
      memcpy (obj->dir_record, dir, dir->length);

      return obj;
    }

    path_table_pos = child_pos;

    if (*cp == '/')
      cp++;
  }

  if (!(ptr = Get_Path_Table_Record (p_current_dir->volume, path_table_pos,
  				     NULL))) {
    return NULL;
  }
  
  if (!(dir = Get_Directory_Record (p_current_dir->volume, ptr->location, 0))) {
    return NULL;    
  }

  obj->directory_f = TRUE;
  obj->path_table_pos = path_table_pos;
  obj->parent = ptr->parent;
  obj->dir_record = AllocMem (dir->length, MEMF_PUBLIC);
  if (!obj->dir_record) {
    iso_errno = ISOERR_NO_MEMORY;
    return NULL;    
  }
  memcpy (obj->dir_record, dir, dir->length);

  return obj;
}

/* Open an object on an ISO or Rock Ridge disk.
 */

CDROM_OBJ *Open_Object (CDROM_OBJ *p_current_dir, char *p_name)
{
  CDROM_OBJ *obj, *res;

  if (*p_name == 0) {
    obj = Clone_Object (p_current_dir);
    if (!obj)
      iso_errno = ISOERR_NO_MEMORY;
    return obj;
  }

  obj = AllocMem (sizeof (CDROM_OBJ), MEMF_PUBLIC);
  if (!obj) {
    iso_errno = ISOERR_NO_MEMORY;
    return NULL;
  }
  
  obj->pos = 0;
  obj->volume = p_current_dir->volume;

  if (p_current_dir->volume->use_rock_ridge)
    res = Open_Object_RR (obj, p_current_dir, p_name);
  else
    res = Open_Object_ISO (obj, p_current_dir, p_name);
  
  if (!res)
    FreeMem (obj, sizeof (CDROM_OBJ));

  return res;
}

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

void Close_Object (CDROM_OBJ *p_object)
{
  FreeMem (p_object->dir_record, p_object->dir_record->length);
  FreeMem (p_object, sizeof (CDROM_OBJ));
}

/* Read bytes from a file.
 */

int 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 == p_file->dir_record->data_length_m)
    /* at end of file: */
    return 0;

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

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

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

    loc++;
  }

  return buf_pos;
}

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

int CDROM_Info (CDROM_OBJ *p_obj, CDROM_INFO *p_info)
{
  memcpy (&p_info->dir_record, p_obj->dir_record, p_obj->dir_record->length);

  if (p_obj->volume->use_rock_ridge) {
    p_info->name_length = Get_RR_File_Name (p_obj->volume, &p_info->dir_record,
    			      p_info->name, sizeof (p_info->name));
    if (p_info->name_length != -1)
      return 1;
  }

  if (p_obj->directory_f) {
    /* dir_record contains only the short directory name (00 byte).
       we have to find the full directory name from the path table. */

    path_table_record *ptr;
    
    if (p_obj->dir_record->extent_loc_m ==
    	p_obj->volume->pvd.root.extent_loc_m) {
      /* top level object: */
      p_info->name[0] = ':';
      p_info->name_length = 1;
    } else {
      /* other: */
      if (!(ptr = Get_Path_Table_Record (p_obj->volume, p_obj->path_table_pos,
   				         NULL))) {
        return 0;
      }
      p_info->name_length = ptr->id_length;
      memcpy (p_info->name, ptr->id, ptr->id_length);
    }
  } else {
    p_info->name_length = p_info->dir_record.file_id_length;
    memcpy (p_info->name, p_info->dir_record.file_id, p_info->name_length);
  }
  return 1;
}

/* Browse all entries in a directory.
 */

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

  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,
    			        p_dir->dir_record->extent_loc_m,
			        0);
    if (!rec)
      return 0;
  
    offset = rec->length;
  
    rec = Get_Directory_Record (p_dir->volume,
  			        p_dir->dir_record->extent_loc_m,
			        offset);
    if (!rec)
      return 0;
  
    *p_offset = offset + rec->length;
  }

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

    rec = Get_Directory_Record (p_dir->volume,
  			        p_dir->dir_record->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;
  }

  memcpy (&p_info->dir_record, rec, rec->length);
  *p_offset += rec->length;

  if (p_dir->volume->use_rock_ridge) {
    p_info->name_length = Get_RR_File_Name (p_dir->volume, rec,
    			      p_info->name, sizeof (p_info->name));
    if (p_info->name_length != -1)
      return 1;
  }

  p_info->name_length = rec->file_id_length;
  memcpy (p_info->name, rec->file_id, rec->length);


  return 1;
}

/* Clone a "CDROM object."
 */

CDROM_OBJ *Clone_Object (CDROM_OBJ *p_object)
{
  CDROM_OBJ *new = AllocMem (sizeof (CDROM_OBJ), MEMF_PUBLIC);
  
  if (!new)
    return NULL;
  memcpy (new, p_object, sizeof (CDROM_OBJ));

  new->dir_record = AllocMem (p_object->dir_record->length, MEMF_PUBLIC);
  if (!new->dir_record) {
    FreeMem (new, sizeof (CDROM_OBJ));
    return NULL;
  }
  memcpy (new->dir_record, p_object->dir_record, p_object->dir_record->length);

  return new;
}

/* Find parent directory.
 */

CDROM_OBJ *Find_Parent_ISO (CDROM_OBJ *p_object)
{
  path_table_record *ptr;
  directory_record *dir;
  CDROM_OBJ *obj;

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

  obj->pos = 0;
  obj->volume = p_object->volume;

  if (!(ptr = Get_Path_Table_Record (p_object->volume, p_object->parent,
  				     NULL))) {
    FreeMem (obj, sizeof (CDROM_OBJ));
    iso_errno = ISOERR_NO_MEMORY;
    return NULL;
  }
  
  if (!(dir = Get_Directory_Record (p_object->volume, ptr->location, 0))) {
    FreeMem (obj, sizeof (CDROM_OBJ));
    iso_errno = ISOERR_NO_MEMORY;
    return NULL;
  }

  obj->directory_f = TRUE;
  obj->path_table_pos = p_object->parent;
  obj->parent = Get_Parent_Directory_ISO (p_object->volume, p_object->parent);
  obj->dir_record = AllocMem (dir->length, MEMF_PUBLIC);
  if (!obj->dir_record) {
    FreeMem (obj, sizeof (CDROM_OBJ));
    iso_errno = ISOERR_NO_MEMORY;
    return NULL;    
  }
  memcpy (obj->dir_record, dir, dir->length);

  return obj;
}

/* Find parent directory on an ISO or Rock Ridge disk.
 */

CDROM_OBJ *Find_Parent (CDROM_OBJ *p_object)
{
  if (p_object->volume->use_rock_ridge)
    return Find_Parent_RR (p_object);
  else
    return Find_Parent_ISO (p_object);
}

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

int Is_Top_Level_Object (CDROM_OBJ *p_object)
{
  return p_object->directory_f &&
         p_object->dir_record->extent_loc_m ==
	 p_object->volume->pvd.root.extent_loc_m;
}

/* Find a position in a file.
 */

int Seek_Position (CDROM_OBJ *p_object, long p_offset, int p_mode)
{
  unsigned long new_pos;
  unsigned long max_len = p_object->dir_record->data_length_m;
  
  if (p_object->directory_f) {
    iso_errno = ISOERR_BAD_ARGUMENTS;
    return 0;
  }
  
  switch (p_mode) {
  case SEEK_FROM_START:
    if (p_offset < 0 || p_offset > max_len) {
      iso_errno = ISOERR_OFF_BOUNDS;
      return 0;
    }
    new_pos = p_offset;
    break;
  case SEEK_FROM_CURRENT_POS:
    if ((p_offset < 0 && -p_offset > p_object->pos) ||
    	(p_offset > 0 && p_object->pos + p_offset > max_len)) {
      iso_errno = ISOERR_OFF_BOUNDS;
      return 0;
    }
    new_pos = p_object->pos + p_offset;
    break;
  case SEEK_FROM_END:
    if (p_offset > 0 || -p_offset > max_len) {
      iso_errno = ISOERR_OFF_BOUNDS;
      return 0;
    }
    new_pos = max_len + p_offset;
    break;
  default:
    iso_errno = ISOERR_BAD_ARGUMENTS;
    return 0;    
  }
  p_object->pos = new_pos;
  return 1;
}
