/*
 * libhfs - library for reading and writing Macintosh HFS volumes
 * Copyright (C) 1996-1998 Robert Leslie
 *
 * 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 of the License, 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., 51 Franklin Street - Fifth Floor, Boston,
 * MA 02110-1301, USA.
 *
 * $Id: hfs.c,v 1.15 1998/11/02 22:09:00 rob Exp $
 */

#include "config.h"
#include "libhfs.h"
#include "data.h"
#include "block.h"
#include "medium.h"
#include "file.h"
#include "btree.h"
#include "node.h"
#include "record.h"
#include "volume.h"

const char *hfs_error = "no error";	/* static error string */

hfsvol *hfs_mounts;			/* linked list of mounted volumes */

static
hfsvol *curvol;				/* current volume */


/*
 * NAME:	getvol()
 * DESCRIPTION:	validate a volume reference
 */
static
int getvol(hfsvol **vol)
{
  if (*vol == NULL)
    {
      if (curvol == NULL)
	ERROR(EINVAL, "no volume is current");

      *vol = curvol;
    }

  return 0;

fail:
  return -1;
}

/* High-Level Volume Routines ============================================== */

/*
 * NAME:	hfs->mount()
 * DESCRIPTION:	open an HFS volume; return volume descriptor or 0 (error)
 */
hfsvol *hfs_mount( int os_fd, int pnum)
{
  hfsvol *vol, *check;
  int mode = HFS_MODE_RDONLY;

  /* see if the volume is already mounted */
  for (check = hfs_mounts; check; check = check->next)
    {
      if (check->pnum == pnum && v_same(check, os_fd) == 1)
	{
	    vol = check;
	    goto done;
	}
    }

  vol = ALLOC(hfsvol, 1);
  if (vol == NULL)
    ERROR(ENOMEM, NULL);

  v_init(vol, mode);

  vol->flags |= HFS_VOL_READONLY;
  if( v_open(vol, os_fd) == -1 )
	  goto fail;

  /* mount the volume */

  if (v_geometry(vol, pnum) == -1 ||
      v_mount(vol) == -1)
    goto fail;

  /* add to linked list of volumes */

  vol->prev = NULL;
  vol->next = hfs_mounts;

  if (hfs_mounts)
    hfs_mounts->prev = vol;

  hfs_mounts = vol;

done:
  ++vol->refs;
  curvol = vol;

  return vol;

fail:
  if (vol)
    {
      v_close(vol);
      FREE(vol);
    }

  return NULL;
}


/*
 * NAME:	hfs->umount()
 * DESCRIPTION:	close an HFS volume
 */
int hfs_umount(hfsvol *vol)
{
  int result = 0;

  if (getvol(&vol) == -1)
    goto fail;

  if (--vol->refs)
    {
      goto done;
    }

  /* close all open files and directories */

  while (vol->files)
    {
      if (hfs_close(vol->files) == -1)
	result = -1;
    }

  while (vol->dirs)
    {
      if (hfs_closedir(vol->dirs) == -1)
	result = -1;
    }

  /* close medium */

  if (v_close(vol) == -1)
    result = -1;

  /* remove from linked list of volumes */

  if (vol->prev)
    vol->prev->next = vol->next;
  if (vol->next)
    vol->next->prev = vol->prev;

  if (vol == hfs_mounts)
    hfs_mounts = vol->next;
  if (vol == curvol)
    curvol = NULL;

  FREE(vol);

done:
  return result;

fail:
  return -1;
}

/*
 * NAME:	hfs->umountall()
 * DESCRIPTION:	unmount all mounted volumes
 */
void hfs_umountall(void)
{
  while (hfs_mounts)
    hfs_umount(hfs_mounts);
}

/*
 * NAME:	hfs->getvol()
 * DESCRIPTION:	return a pointer to a mounted volume
 */
hfsvol *hfs_getvol(const char *name)
{
  hfsvol *vol;

  if (name == NULL)
    return curvol;

  for (vol = hfs_mounts; vol; vol = vol->next)
    {
      if (d_relstring(name, vol->mdb.drVN) == 0)
	return vol;
    }

  return NULL;
}

/*
 * NAME:	hfs->setvol()
 * DESCRIPTION:	change the current volume
 */
void hfs_setvol(hfsvol *vol)
{
  curvol = vol;
}

/*
 * NAME:	hfs->vstat()
 * DESCRIPTION:	return volume statistics
 */
int hfs_vstat(hfsvol *vol, hfsvolent *ent)
{
  if (getvol(&vol) == -1)
    goto fail;

  strcpy(ent->name, vol->mdb.drVN);

  ent->flags     = (vol->flags & HFS_VOL_READONLY) ? HFS_ISLOCKED : 0;

  ent->totbytes  = vol->mdb.drNmAlBlks * vol->mdb.drAlBlkSiz;
  ent->freebytes = vol->mdb.drFreeBks  * vol->mdb.drAlBlkSiz;

  ent->alblocksz = vol->mdb.drAlBlkSiz;
  ent->clumpsz   = vol->mdb.drClpSiz;

  ent->numfiles  = vol->mdb.drFilCnt;
  ent->numdirs   = vol->mdb.drDirCnt;

  ent->crdate    = d_ltime(vol->mdb.drCrDate);
  ent->mddate    = d_ltime(vol->mdb.drLsMod);
  ent->bkdate    = d_ltime(vol->mdb.drVolBkUp);

  ent->blessed   = vol->mdb.drFndrInfo[0];

  return 0;

fail:
  return -1;
}


/* High-Level Directory Routines =========================================== */

/*
 * NAME:	hfs->chdir()
 * DESCRIPTION:	change current HFS directory
 */
int hfs_chdir(hfsvol *vol, const char *path)
{
  CatDataRec data;

  if (getvol(&vol) == -1 ||
      v_resolve(&vol, path, &data, NULL, NULL, NULL) <= 0)
    goto fail;

  if (data.cdrType != cdrDirRec)
    ERROR(ENOTDIR, NULL);

  vol->cwd = data.u.dir.dirDirID;

  return 0;

fail:
  return -1;
}

/*
 * NAME:	hfs->getcwd()
 * DESCRIPTION:	return the current working directory ID
 */
unsigned long hfs_getcwd(hfsvol *vol)
{
  if (getvol(&vol) == -1)
    return 0;

  return vol->cwd;
}

/*
 * NAME:	hfs->setcwd()
 * DESCRIPTION:	set the current working directory ID
 */
int hfs_setcwd(hfsvol *vol, unsigned long id)
{
  if (getvol(&vol) == -1)
    goto fail;

  if (id == vol->cwd)
    goto done;

  /* make sure the directory exists */

  if (v_getdthread(vol, id, NULL, NULL) <= 0)
    goto fail;

  vol->cwd = id;

done:
  return 0;

fail:
  return -1;
}

/*
 * NAME:	hfs->dirinfo()
 * DESCRIPTION:	given a directory ID, return its (name and) parent ID
 */
int hfs_dirinfo(hfsvol *vol, unsigned long *id, char *name)
{
  CatDataRec thread;

  if (getvol(&vol) == -1 ||
      v_getdthread(vol, *id, &thread, NULL) <= 0)
    goto fail;

  *id = thread.u.dthd.thdParID;

  if (name)
    strcpy(name, thread.u.dthd.thdCName);

  return 0;

fail:
  return -1;
}

/*
 * NAME:	hfs->opendir()
 * DESCRIPTION:	prepare to read the contents of a directory
 */
hfsdir *hfs_opendir(hfsvol *vol, const char *path)
{
  hfsdir *dir = NULL;
  CatKeyRec key;
  CatDataRec data;
  byte pkey[HFS_CATKEYLEN];

  if (getvol(&vol) == -1)
    goto fail;

  dir = ALLOC(hfsdir, 1);
  if (dir == NULL)
    ERROR(ENOMEM, NULL);

  dir->vol = vol;

  if (*path == 0)
    {
      /* meta-directory containing root dirs from all mounted volumes */

      dir->dirid = 0;
      dir->vptr  = hfs_mounts;
    }
  else
    {
      if (v_resolve(&vol, path, &data, NULL, NULL, NULL) <= 0)
	goto fail;

      if (data.cdrType != cdrDirRec)
        ERROR(ENOTDIR, NULL);

      dir->dirid = data.u.dir.dirDirID;
      dir->vptr  = NULL;

      r_makecatkey(&key, dir->dirid, "");
      r_packcatkey(&key, pkey, NULL);

      if (bt_search(&vol->cat, pkey, &dir->n) <= 0)
	goto fail;
    }

  dir->prev = NULL;
  dir->next = vol->dirs;

  if (vol->dirs)
    vol->dirs->prev = dir;

  vol->dirs = dir;

  return dir;

fail:
  FREE(dir);
  return NULL;
}

/*
 * NAME:	hfs->readdir()
 * DESCRIPTION:	return the next entry in the directory
 */
int hfs_readdir(hfsdir *dir, hfsdirent *ent)
{
  CatKeyRec key;
  CatDataRec data;
  const byte *ptr;

  if (dir->dirid == 0)
    {
      hfsvol *vol;
      char cname[HFS_MAX_FLEN + 1];

      for (vol = hfs_mounts; vol; vol = vol->next)
	{
	  if (vol == dir->vptr)
	    break;
	}

      if (vol == NULL)
	ERROR(ENOENT, "no more entries");

      if (v_getdthread(vol, HFS_CNID_ROOTDIR, &data, NULL) <= 0 ||
	  v_catsearch(vol, HFS_CNID_ROOTPAR, data.u.dthd.thdCName,
                      &data, cname, NULL) <= 0)
	goto fail;

      r_unpackdirent(HFS_CNID_ROOTPAR, cname, &data, ent);

      dir->vptr = vol->next;

      goto done;
    }

  if (dir->n.rnum == -1)
    ERROR(ENOENT, "no more entries");

  while (1)
    {
      ++dir->n.rnum;

      while (dir->n.rnum >= dir->n.nd.ndNRecs)
	{
	  if (dir->n.nd.ndFLink == 0)
	    {
	      dir->n.rnum = -1;
	      ERROR(ENOENT, "no more entries");
	    }

	  if (bt_getnode(&dir->n, dir->n.bt, dir->n.nd.ndFLink) == -1)
	    {
	      dir->n.rnum = -1;
	      goto fail;
	    }

	  dir->n.rnum = 0;
	}

      ptr = HFS_NODEREC(dir->n, dir->n.rnum);

      r_unpackcatkey(ptr, &key);

      if (key.ckrParID != dir->dirid)
	{
	  dir->n.rnum = -1;
	  ERROR(ENOENT, "no more entries");
	}

      r_unpackcatdata(HFS_RECDATA(ptr), &data);

      switch (data.cdrType)
	{
	case cdrDirRec:
	case cdrFilRec:
	  r_unpackdirent(key.ckrParID, key.ckrCName, &data, ent);
	  goto done;

	case cdrThdRec:
	case cdrFThdRec:
	  break;

	default:
	  dir->n.rnum = -1;
	  ERROR(EIO, "unexpected directory entry found");
	}
    }

done:
  return 0;

fail:
  return -1;
}

/*
 * NAME:	hfs->closedir()
 * DESCRIPTION:	stop reading a directory
 */
int hfs_closedir(hfsdir *dir)
{
  hfsvol *vol = dir->vol;

  if (dir->prev)
    dir->prev->next = dir->next;
  if (dir->next)
    dir->next->prev = dir->prev;
  if (dir == vol->dirs)
    vol->dirs = dir->next;

  FREE(dir);

  return 0;
}

/* High-Level File Routines ================================================ */

/*
 * NAME:	hfs->open()
 * DESCRIPTION:	prepare a file for I/O
 */
hfsfile *hfs_open(hfsvol *vol, const char *path)
{
  hfsfile *file = NULL;

  if (getvol(&vol) == -1)
    goto fail;

  file = ALLOC(hfsfile, 1);
  if (file == NULL)
    ERROR(ENOMEM, NULL);

  if (v_resolve(&vol, path, &file->cat, &file->parid, file->name, NULL) <= 0)
    goto fail;

  if (file->cat.cdrType != cdrFilRec)
    ERROR(EISDIR, NULL);

  /* package file handle for user */

  file->vol   = vol;
  file->flags = 0;

  f_selectfork(file, fkData);

  file->prev = NULL;
  file->next = vol->files;

  if (vol->files)
    vol->files->prev = file;

  vol->files = file;

  return file;

fail:
  FREE(file);
  return NULL;
}

/*
 * NAME:	hfs->setfork()
 * DESCRIPTION:	select file fork for I/O operations
 */
int hfs_setfork(hfsfile *file, int fork)
{
  int result = 0;

  f_selectfork(file, fork ? fkRsrc : fkData);

  return result;
}

/*
 * NAME:	hfs->getfork()
 * DESCRIPTION:	return the current fork for I/O operations
 */
int hfs_getfork(hfsfile *file)
{
  return file->fork != fkData;
}

/*
 * NAME:	hfs->read()
 * DESCRIPTION:	read from an open file
 */
unsigned long hfs_read(hfsfile *file, void *buf, unsigned long len)
{
  unsigned long *lglen, count;
  byte *ptr = buf;

  f_getptrs(file, NULL, &lglen, NULL);

  if (file->pos + len > *lglen)
    len = *lglen - file->pos;

  count = len;
  while (count)
    {
      unsigned long bnum, offs, chunk;

      bnum  = file->pos >> HFS_BLOCKSZ_BITS;
      offs  = file->pos & (HFS_BLOCKSZ - 1);

      chunk = HFS_BLOCKSZ - offs;
      if (chunk > count)
	chunk = count;

      if (offs == 0 && chunk == HFS_BLOCKSZ)
	{
	  if (f_getblock(file, bnum, (block *) ptr) == -1)
	    goto fail;
	}
      else
	{
	  block b;

	  if (f_getblock(file, bnum, &b) == -1)
	    goto fail;

	  memcpy(ptr, b + offs, chunk);
	}

      ptr += chunk;

      file->pos += chunk;
      count     -= chunk;
    }

  return len;

fail:
  return -1;
}

/*
 * NAME:	hfs->seek()
 * DESCRIPTION:	change file seek pointer
 */
unsigned long hfs_seek(hfsfile *file, long offset, int from)
{
  unsigned long *lglen, newpos;

  f_getptrs(file, NULL, &lglen, NULL);

  switch (from)
    {
    case HFS_SEEK_SET:
      newpos = (offset < 0) ? 0 : offset;
      break;

    case HFS_SEEK_CUR:
      if (offset < 0 && (unsigned long) -offset > file->pos)
	newpos = 0;
      else
	newpos = file->pos + offset;
      break;

    case HFS_SEEK_END:
      if (offset < 0 && (unsigned long) -offset > *lglen)
	newpos = 0;
      else
	newpos = *lglen + offset;
      break;

    default:
      ERROR(EINVAL, NULL);
    }

  if (newpos > *lglen)
    newpos = *lglen;

  file->pos = newpos;

  return newpos;

fail:
  return -1;
}

/*
 * NAME:	hfs->close()
 * DESCRIPTION:	close a file
 */
int hfs_close(hfsfile *file)
{
  hfsvol *vol = file->vol;
  int result = 0;

  if (file->prev)
    file->prev->next = file->next;
  if (file->next)
    file->next->prev = file->prev;
  if (file == vol->files)
    vol->files = file->next;

  FREE(file);

  return result;
}

/* High-Level Catalog Routines ============================================= */

/*
 * NAME:	hfs->stat()
 * DESCRIPTION:	return catalog information for an arbitrary path
 */
int hfs_stat(hfsvol *vol, const char *path, hfsdirent *ent)
{
  CatDataRec data;
  unsigned long parid;
  char name[HFS_MAX_FLEN + 1];

  if (getvol(&vol) == -1 ||
      v_resolve(&vol, path, &data, &parid, name, NULL) <= 0)
    goto fail;

  r_unpackdirent(parid, name, &data, ent);

  return 0;

fail:
  return -1;
}

/*
 * NAME:	hfs->fstat()
 * DESCRIPTION:	return catalog information for an open file
 */
int hfs_fstat(hfsfile *file, hfsdirent *ent)
{
  r_unpackdirent(file->parid, file->name, &file->cat, ent);

  return 0;
}

/*
 * NAME:	hfs->probe()
 * DESCRIPTION:	return whether a HFS filesystem is present at the given offset
 */
int hfs_probe(int fd, long long offset)
{
  return v_probe(fd, offset);
}