/*
 * This file is part of DesktopBSD, see the README file.
 *
 * Author: Peter Hofer <ph@desktopbsd.net>
 *			(C) 2004, 2005
 *
 * Copyright: BSD, see the COPYING file included in this distribution
 *
 */

#include "mntdevice.h"

/* mount(2) */
#include <sys/param.h>
#include <sys/ucred.h>
#include <sys/mount.h>

/* stat(2) */
#include <sys/types.h>
#include <sys/stat.h>

/* libcam(3) and requirements */
#include <camlib.h>
#include <fcntl.h>

#include <qdir.h>
#include <qregexp.h>
#include <qstringlist.h>

#include <executor.h>
#include <dbsdtranslator.h>


MntDevice::MntDevice()
	: FSDevice()
{
}



MntDevice::MntDevice(const QString& devname)
	: FSDevice(devname),

	devtype(MntDev)
{
	readSubDevices();
}



MntDevice::MntDevice(const QString& devname,  Type  devtype,
							FSType fstype,  Size size)
  : FSDevice(devname,  size,  fstype),

  devtype(devtype)
{
	readSubDevices();
}



/** Reads all the sub devices this device has.
  *
  * This includes slices, partitions (in slices),
  * and extended partitions.
  */
void  MntDevice::readSubDevices() {
	const QString&  devname = getName();

	subdevs.setAutoDelete(true);
	subdevs.clear();

	/* Check for unsupported devices */
	if(devtype == MntDev) {
		switch(getDeviceType()) {
		case  ATAPI_CD:
		case  SCSI_CD:
			fstype = CDROM;
			return;

		case  Floppy:
			/* Almost every floppy uses FAT as file system, so
			 * it's the only one we support here.
			 */
			fstype = FAT;
			return;

		default:
			break;
		}
	}

	if(devtype == MntDev || (devtype == Slice && fstype == Extended)) {
		/* The returned QStringList consists of SysId and Blocks line pairs.
		 * Unused chunks are indicated by a single '<UNUSED>' line.
		 * 
		 * Example:
		 *  165
		 *  150994872
		 *  <UNUSED>
		 *  7
		 *  61239780
		 *  <UNUSED>
		 */
		static const  QString  cmd = "/sbin/fdisk -t '%1' | /usr/bin/awk '$1==\"sysid\" {print $2; getline; print $4}; $0==\"<UNUSED>\"'";
		QStringList  strl = Executor::strlistFromCmd(cmd.arg(devname));

		uint	  i;
		QString  partname;
		QStringList::ConstIterator  it = strl.begin();
		if(devtype == MntDev) {
			i = 1;
			partname = devname + "s%1";
		} else {
			/* Logical partitions start at s5 */
			i = 5;
			/* remove slice number, e.g. ad0s3 -> ad0s: */
			partname = devname.mid(0, devname.findRev(QRegExp("[0-9]+"))) + "%1";
		}

		uint totalblk = 0;
		for(;  it != strl.end();  ++i) {
			uint sysid, blocks;

			/* Skip unused slices, but still count them */
			if (*it != "<UNUSED>")
			{
				sysid = (*it++).toUInt(),
				blocks = (*it).toUInt();
	
				totalblk += blocks;
	
				subdevs.append(new MntDevice(partname.arg(i),
													Type (devtype + 1),
													typeFromSysId(sysid),
													Size(blocks, Size::Blocks)));
			}

			++it;
		}

		this->size.set(totalblk, Size::Blocks);
	}  else if(devtype == Slice && fstype == UFS) {
		/* Uses bsdlabel(8) to read the partitions in a UFS/FFS slice.
	  *
	  * The returned QStringList consists of Suffix-Size-Pairs, e.g.:
	  * a - 393216 - d - 524288 - ...
	  */
		const  QString  cmd = "/sbin/bsdlabel '%1' | /usr/bin/awk '$4~/BSD/ { printf \"%c\\n\", $1; print $2 }'";
		QStringList  strl = Executor::strlistFromCmd(cmd.arg(devname));

		QStringList::ConstIterator  it = strl.begin();
		while(it != strl.end()) {
			QString  partname = devname + *it++;
			uint	  blocks = (*it++).toUInt();

			subdevs.append(new  MntDevice(partname,  Type (devtype + 1),
													UFS,  Size(blocks, Size::Blocks)));
		}
	}
}


/** Returns which path the device is mounted on.
  * If the device is not mounted, it returns
  * an empty string.
  */
QString  MntDevice::mountedOn()  const {
	if(getName().isEmpty())
		return  "";

	/* Number of mounted filesystems */
	int  mounted = getfsstat(NULL, 0, MNT_WAIT);
	if(mounted < 0)
		return  "";

	QString  retVal;
	struct statfs  *mntbuf = new struct statfs[mounted];
	mounted = getfsstat(mntbuf, mounted * sizeof(struct statfs), MNT_WAIT);
	if(mounted > 0) {
		QString  disk = "/dev/" + getName();
		for(int i = 0;  i < mounted;  ++i)
			if(strcmp(mntbuf[i].f_mntfromname, disk) == 0) {
				retVal = mntbuf[i].f_mntonname;
				break;
			}
	}

	delete mntbuf;
	return retVal;
}



/** Unmounts the device.
  *
  * @warning
  *	If empty, the directory the file-system was mounted
  *	on will be removed afterwards.
  *
  * @param  force  This unmounts the file system even
  *					 if there are still active files. DANGEROUS!
  *
  * @return TRUE when unmount(2) succeeded.
  * @todo	We need better error-checking here that
  *			allows to find out why this function failed.
  */
bool  MntDevice::unmount(bool force) {
	QString  mountpnt = mountedOn();
	if(mountpnt.isEmpty())
		return false;

	bool  success = ::unmount(mountpnt, force ? (MNT_FORCE) : (0)) == 0;

	if(success)
		rmMountDir(mountpnt);

	return success;
}


bool  MntDevice::isMounted()  const {
	return (!mountedOn().isEmpty());
}



/** Creates a new directory in /media that contains
  * the given string that describes the mounted resource.
  *
  * @param  contains  Name of the mountpoint.
  * @return Empty string if mkdir(2) failed, otherwise
  *			the full path to the newly created directory.
  */
QString  MntDevice::newMountDir(const QString& contains,  mode_t  mode) {
	Size	  size = getSize();
	QString  sizeStr;
	if(size.get(Size::GB) > 0)
		sizeStr = size.toString(Size::GB);
	else
		sizeStr = size.toString(Size::MB);

	/* Naming template, e.g.: '/media/FAT (123 MB)' */
	QString  templ;
	if(size.get() > 0)
		templ = QString("/media/%1 (%2)")
						 .arg(contains)
						 .arg(sizeStr);
	else
		templ = QString("/media/%1").arg(contains);

	QString  dirname = templ;
	templ += " - ";
	struct stat  st;
	for(uint i = 2;  stat(dirname, &st) == 0;  ++i)
		dirname = templ + QString::number(i);

	return	(mkdir(dirname,  mode) == 0) ? dirname : "";
}



/** Removes an empty directory if it resides in /media.
  *
  * @return FALSE if not in /media or when rmdir(2) failed.
  */
bool  MntDevice::rmMountDir(const QString  &dir) {
	if(dir.startsWith("/media/"))
		return rmdir(dir);

	return false;
}



/** @return The number of direct subdevices of this
  *			device. A subdevice can be a slice within
  *			within a disk or a partition within
  *			a slice or an extended partition.
  */
uint  MntDevice::subdevCount()  const {
	return  subdevs.count();
}



/** @return A pointer to the subdevice at position "pos". */
MntDevice  *MntDevice::operator[] (uint  pos)  throw (std::out_of_range) {
	MntDevice *mdev = subdevs.at(pos);
	if(mdev == NULL)
		throw  std::out_of_range("pos");

	return  mdev;
}



bool  MntDevice::_mount(int  flags,  QString  mntpnt,  FSType  fstype) {
	QString  fs = mountType(fstype);
	if(fs.isEmpty())
		return  false;

	/* Check for flags not supported
	 * by some file-systems
	 */
	if(fstype == NTFS)
		flags |= ReadOnly;
	if(fstype == NTFS || fstype == ISO9660)
		flags &= ~Synchronous;
	if(fstype != FAT)
		flags &= ~Large;

	bool  mpntCreated = false;
	if(mntpnt.isEmpty()) {
		mntpnt = newMountDir(getDesc(), 0774);
		if(mntpnt.isEmpty())
			return false;

		mpntCreated = true;
	}

	QString  options;
	if(flags & NoSUID)
		options += "-o nosuid ";
	if(flags & ReadOnly)
		options += "-o ro ";
	if(flags & NoDevices)
		options += "-o nodev ";
	if(flags & Synchronous)
		options += "-o sync ";
	if(flags & Large)
		options += "-o large ";

	QString  cmd = QString("/sbin/mount -t '%1' %2 '/dev/%3' '%4'")
								.arg(fs)
								.arg(options)
								.arg(getName())
								.arg(mntpnt);

	bool success = (Executor::exec(cmd) == Executor::NoError) && isMounted();

	if(!success && mpntCreated)
	{
		rmMountDir(mntpnt);
	}
	
	return  success;
}



/** Mounts the device. If called without specifying
  * a mount-point, it will create one in /media and
  * name it after the file system.
  *
  * @sa newMountDir()
  * @sa getDesc()
  */
bool  MntDevice::mount(int  flags,  QString  mntpnt) {
	if(isMounted())
		return  false;

	if(fstype == CDROM)
		return	(_mount(flags,  mntpnt,  UDF)
					 || _mount(flags,  mntpnt,  ISO9660));

	if(fstype == Linux)
		return	(_mount(flags,  mntpnt,  ext2fs)
					 || _mount(flags,  mntpnt,  ReiserFS));

	return  _mount(flags,  mntpnt,  fstype);
}



MntDevice&  MntDevice::operator= (const MntDevice&  mdev) {
	if(this != &mdev) {
		Device::operator=(mdev);

		size = mdev.size;
		devtype = mdev.devtype;
		fstype = mdev.fstype;
		readSubDevices();
	}

	return  *this;
}



/** Returns the file-system type if this is a slice
  * or a partition, otherwise the device type.
  */
QString  MntDevice::getDesc()  const {
	if(devtype == MntDev)
		switch(getDeviceType()) {
		case ATAPI_CD:
		case SCSI_CD:
			return  NonUITr::tr("CD, DVD");

		case ATA_RAID:
			return  NonUITr::tr("RAID");

		case Floppy:
			return  NonUITr::tr("Floppy");

		default:
			return  NonUITr::tr("Storage Device");
		}
	else
		return  FSDevice::getDesc();
}



/** Returns the same devices as getAllDeviceNames(), but
  * as a MntDevice object. Be aware that this consumes
  * quite some cpu time because all sub-devices are created.
  * Returns the Devices in the wrong order!
  */
QPtrList<MntDevice>	MntDevice::getAllDevices() {
	QPtrList<MntDevice>	list;

	QStringList	names = getAllDeviceNames();
	QStringList::ConstIterator	it = names.begin();
	while(it != names.end())
		list.prepend(new MntDevice(*it++));

	return	list;
}



/** This returns all wd, ad, da, ar, cd, acd and fd devices
  * in this system.
  *
  * It can be possible that acd devices are also listed
  * as cd devices if ATAPICAM is in the kernel.
  */
QStringList MntDevice::getAllDeviceNames() {
	QStringList  sl = QDir("/dev").entryList(QDir::System);
	return sl.grep(QRegExp("^(wd|ad|da|ar|cd|acd|fd)[0-9]+$"));
}


/** Returns TRUE when this device is passed through by ATAPICAM,
  * otherwise FALSE.
  *
  * Example: acd0 is mapped to cd0, so this method returns TRUE
  * for cd0 and FALSE for acd0.
  */
bool MntDevice::isATAPICAMDevice() const {
	if(getDeviceType() != SCSI_CD)
		return false;

	int	unit = 0;
	char  device[32];
	if(cam_get_device(getName(), device, sizeof device, &unit) == -1)
		return false;

	struct cam_device *dev = cam_open_spec_device(device, unit, O_RDWR, NULL);
	if(dev == NULL)
		return false;

	bool ata = (strcmp(dev->sim_name, "ata") == 0);
	cam_close_device(dev);
	return ata;
}
