/*
 * tclMacOSXFCmd.c
 *
 *	This file implements the MacOSX specific portion of file manipulation
 *	subcommands of the "file" command.
 *
 * Copyright (c) 2003-2007 Daniel A. Steffen <das@users.sourceforge.net>
 *
 * See the file "license.terms" for information on usage and redistribution of
 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
 */

#include "tclInt.h"

#ifdef HAVE_GETATTRLIST
#include <sys/attr.h>
#include <sys/paths.h>
#include <libkern/OSByteOrder.h>
#endif

/* Darwin 8 copyfile API. */
#ifdef HAVE_COPYFILE
#ifdef HAVE_COPYFILE_H
#include <copyfile.h>
#if defined(HAVE_WEAK_IMPORT) && (MAC_OS_X_VERSION_MIN_REQUIRED < 1040)
/* Support for weakly importing copyfile. */
#define WEAK_IMPORT_COPYFILE
extern int		copyfile(const char *from, const char *to,
			    copyfile_state_t state, copyfile_flags_t flags)
			    WEAK_IMPORT_ATTRIBUTE;
#endif /* HAVE_WEAK_IMPORT */
#else /* HAVE_COPYFILE_H */
int			copyfile(const char *from, const char *to,
			    void *state, uint32_t flags);
#define COPYFILE_ACL		(1<<0)
#define COPYFILE_XATTR		(1<<2)
#define COPYFILE_NOFOLLOW_SRC	(1<<18)
#if defined(HAVE_WEAK_IMPORT) && (MAC_OS_X_VERSION_MIN_REQUIRED < 1040)
/* Support for weakly importing copyfile. */
#define WEAK_IMPORT_COPYFILE
extern int		copyfile(const char *from, const char *to,
			    void *state, uint32_t flags)
			    WEAK_IMPORT_ATTRIBUTE;
#endif /* HAVE_WEAK_IMPORT */
#endif /* HAVE_COPYFILE_H */
#endif /* HAVE_COPYFILE */

#ifdef WEAK_IMPORT_COPYFILE
#define MayUseCopyFile()	(copyfile != NULL)
#elif defined(HAVE_COPYFILE)
#define MayUseCopyFile()	(1)
#else
#define MayUseCopyFile()	(0)
#endif

#include <libkern/OSByteOrder.h>

/*
 * Constants for file attributes subcommand. Need to be kept in sync with
 * tclUnixFCmd.c !
 */

enum {
    UNIX_GROUP_ATTRIBUTE,
    UNIX_OWNER_ATTRIBUTE,
    UNIX_PERMISSIONS_ATTRIBUTE,
#ifdef HAVE_CHFLAGS
    UNIX_READONLY_ATTRIBUTE,
#endif
#ifdef MAC_OSX_TCL
    MACOSX_CREATOR_ATTRIBUTE,
    MACOSX_TYPE_ATTRIBUTE,
    MACOSX_HIDDEN_ATTRIBUTE,
    MACOSX_RSRCLENGTH_ATTRIBUTE,
#endif
};

typedef u_int32_t OSType;

static int		GetOSTypeFromObj(Tcl_Interp *interp,
			    Tcl_Obj *objPtr, OSType *osTypePtr);
static Tcl_Obj *	NewOSTypeObj(const OSType newOSType);
static int		SetOSTypeFromAny(Tcl_Interp *interp, Tcl_Obj *objPtr);
static void		UpdateStringOfOSType(Tcl_Obj *objPtr);

static const Tcl_ObjType tclOSTypeType = {
    "osType",			/* name */
    NULL,			/* freeIntRepProc */
    NULL,			/* dupIntRepProc */
    UpdateStringOfOSType,	/* updateStringProc */
    SetOSTypeFromAny		/* setFromAnyProc */
};

enum {
    kIsInvisible = 0x4000,
};

#define kFinfoIsInvisible	(OSSwapHostToBigConstInt16(kIsInvisible))

typedef	struct finderinfo {
    u_int32_t type;
    u_int32_t creator;
    u_int16_t fdFlags;
    u_int32_t location;
    u_int16_t reserved;
    u_int32_t extendedFileInfo[4];
} __attribute__ ((__packed__)) finderinfo;

typedef struct {
    u_int64_t reserved1; /* Make sure data is 8-byte aligned */
    u_int32_t reserved2; /* See [992f94d847] */
    u_int32_t info_length;
    u_int32_t data[8];
} fileinfobuf;

/*
 *----------------------------------------------------------------------
 *
 * TclMacOSXGetFileAttribute
 *
 *	Gets a MacOSX attribute of a file. Which attribute is controlled by
 *	objIndex. The object will have ref count 0.
 *
 * Results:
 *	Standard TCL result. Returns a new Tcl_Obj in attributePtrPtr if there
 *	is no error.
 *
 * Side effects:
 *	A new object is allocated.
 *
 *----------------------------------------------------------------------
 */

int
TclMacOSXGetFileAttribute(
    Tcl_Interp *interp,		/* The interp we are using for errors. */
    int objIndex,		/* The index of the attribute. */
    Tcl_Obj *fileName,		/* The name of the file (UTF-8). */
    Tcl_Obj **attributePtrPtr)	/* A pointer to return the object with. */
{
#ifdef HAVE_GETATTRLIST
    int result;
    Tcl_StatBuf statBuf;
    struct attrlist alist;
    fileinfobuf finfo;
    finderinfo *finder = (finderinfo *) &finfo.data;
    off_t *rsrcForkSize = (off_t *) &finfo.data;
    const char *native;

    result = TclpObjStat(fileName, &statBuf);

    if (result != 0) {
	Tcl_SetObjResult(interp, Tcl_ObjPrintf(
		"could not read \"%s\": %s",
		TclGetString(fileName), Tcl_PosixError(interp)));
	return TCL_ERROR;
    }

    if (S_ISDIR(statBuf.st_mode) && objIndex != MACOSX_HIDDEN_ATTRIBUTE) {
	/*
	 * Directories only support attribute "-hidden".
	 */

	errno = EISDIR;
	Tcl_SetObjResult(interp, Tcl_ObjPrintf(
		"invalid attribute: %s", Tcl_PosixError(interp)));
	return TCL_ERROR;
    }

    bzero(&alist, sizeof(struct attrlist));
    alist.bitmapcount = ATTR_BIT_MAP_COUNT;
    if (objIndex == MACOSX_RSRCLENGTH_ATTRIBUTE) {
	alist.fileattr = ATTR_FILE_RSRCLENGTH;
    } else {
	alist.commonattr = ATTR_CMN_FNDRINFO;
    }
    native = (const char *)Tcl_FSGetNativePath(fileName);
    result = getattrlist(native, &alist, &finfo.info_length,
	    sizeof(fileinfobuf) - offsetof(fileinfobuf, info_length), 0);

    if (result != 0) {
	Tcl_SetObjResult(interp, Tcl_ObjPrintf(
		"could not read attributes of \"%s\": %s",
		TclGetString(fileName), Tcl_PosixError(interp)));
	return TCL_ERROR;
    }

    switch (objIndex) {
    case MACOSX_CREATOR_ATTRIBUTE:
	*attributePtrPtr = NewOSTypeObj(
		OSSwapBigToHostInt32(finder->creator));
	break;
    case MACOSX_TYPE_ATTRIBUTE:
	*attributePtrPtr = NewOSTypeObj(
		OSSwapBigToHostInt32(finder->type));
	break;
    case MACOSX_HIDDEN_ATTRIBUTE:
	TclNewIntObj(*attributePtrPtr,
		(finder->fdFlags & kFinfoIsInvisible) != 0);
	break;
    case MACOSX_RSRCLENGTH_ATTRIBUTE:
	TclNewIntObj(*attributePtrPtr, *rsrcForkSize);
	break;
    }
    return TCL_OK;
#else
    Tcl_SetObjResult(interp, Tcl_NewStringObj(
	    "Mac OS X file attributes not supported", -1));
    Tcl_SetErrorCode(interp, "TCL", "UNSUPPORTED", (char *)NULL);
    return TCL_ERROR;
#endif /* HAVE_GETATTRLIST */
}

/*
 *---------------------------------------------------------------------------
 *
 * TclMacOSXSetFileAttribute --
 *
 *	Sets a MacOSX attribute of a file. Which attribute is controlled by
 *	objIndex.
 *
 * Results:
 *	Standard TCL result.
 *
 * Side effects:
 *	As above.
 *
 *---------------------------------------------------------------------------
 */

int
TclMacOSXSetFileAttribute(
    Tcl_Interp *interp,		/* The interp for error reporting. */
    int objIndex,		/* The index of the attribute. */
    Tcl_Obj *fileName,		/* The name of the file (UTF-8). */
    Tcl_Obj *attributePtr)	/* New owner for file. */
{
#ifdef HAVE_GETATTRLIST
    int result;
    Tcl_StatBuf statBuf;
    struct attrlist alist;
    fileinfobuf finfo;
    finderinfo *finder = (finderinfo *) &finfo.data;
    off_t *rsrcForkSize = (off_t *) &finfo.data;
    const char *native;

    result = TclpObjStat(fileName, &statBuf);

    if (result != 0) {
	Tcl_SetObjResult(interp, Tcl_ObjPrintf(
		"could not read \"%s\": %s",
		TclGetString(fileName), Tcl_PosixError(interp)));
	return TCL_ERROR;
    }

    if (S_ISDIR(statBuf.st_mode) && objIndex != MACOSX_HIDDEN_ATTRIBUTE) {
	/*
	 * Directories only support attribute "-hidden".
	 */

	errno = EISDIR;
	Tcl_SetObjResult(interp, Tcl_ObjPrintf(
		"invalid attribute: %s", Tcl_PosixError(interp)));
	return TCL_ERROR;
    }

    bzero(&alist, sizeof(struct attrlist));
    alist.bitmapcount = ATTR_BIT_MAP_COUNT;
    if (objIndex == MACOSX_RSRCLENGTH_ATTRIBUTE) {
	alist.fileattr = ATTR_FILE_RSRCLENGTH;
    } else {
	alist.commonattr = ATTR_CMN_FNDRINFO;
    }
    native = (const char *)Tcl_FSGetNativePath(fileName);
    result = getattrlist(native, &alist, &finfo.info_length,
	    sizeof(fileinfobuf) - offsetof(fileinfobuf, info_length), 0);

    if (result != 0) {
	Tcl_SetObjResult(interp, Tcl_ObjPrintf(
		"could not read attributes of \"%s\": %s",
		TclGetString(fileName), Tcl_PosixError(interp)));
	return TCL_ERROR;
    }

    if (objIndex != MACOSX_RSRCLENGTH_ATTRIBUTE) {
	OSType t;
	int h;

	switch (objIndex) {
	case MACOSX_CREATOR_ATTRIBUTE:
	    if (GetOSTypeFromObj(interp, attributePtr, &t) != TCL_OK) {
		return TCL_ERROR;
	    }
	    finder->creator = OSSwapHostToBigInt32(t);
	    break;
	case MACOSX_TYPE_ATTRIBUTE:
	    if (GetOSTypeFromObj(interp, attributePtr, &t) != TCL_OK) {
		return TCL_ERROR;
	    }
	    finder->type = OSSwapHostToBigInt32(t);
	    break;
	case MACOSX_HIDDEN_ATTRIBUTE:
	    if (Tcl_GetBooleanFromObj(interp, attributePtr, &h) != TCL_OK) {
		return TCL_ERROR;
	    }
	    if (h) {
		finder->fdFlags |= kFinfoIsInvisible;
	    } else {
		finder->fdFlags &= ~kFinfoIsInvisible;
	    }
	    break;
	}

	result = setattrlist(native, &alist,
		&finfo.data, sizeof(finfo.data), 0);

	if (result != 0) {
	    Tcl_SetObjResult(interp, Tcl_ObjPrintf(
		    "could not set attributes of \"%s\": %s",
		    TclGetString(fileName), Tcl_PosixError(interp)));
	    return TCL_ERROR;
	}
    } else {
	Tcl_WideInt newRsrcForkSize;

	if (TclGetWideIntFromObj(interp, attributePtr,
		&newRsrcForkSize) != TCL_OK) {
	    return TCL_ERROR;
	}

	if (newRsrcForkSize != *rsrcForkSize) {
	    Tcl_DString ds;

	    /*
	     * Only setting rsrclength to 0 to strip a file's resource fork is
	     * supported.
	     */

	    if (newRsrcForkSize != 0) {
		Tcl_SetObjResult(interp, Tcl_NewStringObj(
			"setting nonzero rsrclength not supported", -1));
		Tcl_SetErrorCode(interp, "TCL", "UNSUPPORTED", (char *)NULL);
		return TCL_ERROR;
	    }

	    /*
	     * Construct path to resource fork.
	     */

	    Tcl_DStringInit(&ds);
	    Tcl_DStringAppend(&ds, native, -1);
	    Tcl_DStringAppend(&ds, _PATH_RSRCFORKSPEC, -1);

	    result = truncate(Tcl_DStringValue(&ds), 0);
	    if (result != 0) {
		/*
		 * truncate() on a valid resource fork path may fail with a
		 * permission error in some OS releases, try truncating with
		 * open() instead:
		 */

		int fd = open(Tcl_DStringValue(&ds), O_WRONLY | O_TRUNC);

		if (fd > 0) {
		    result = close(fd);
		}
	    }

	    Tcl_DStringFree(&ds);

	    if (result != 0) {
		Tcl_SetObjResult(interp, Tcl_ObjPrintf(
			"could not truncate resource fork of \"%s\": %s",
			TclGetString(fileName), Tcl_PosixError(interp)));
		return TCL_ERROR;
	    }
	}
    }
    return TCL_OK;
#else
    Tcl_SetObjResult(interp, Tcl_NewStringObj(
	    "Mac OS X file attributes not supported", -1));
    Tcl_SetErrorCode(interp, "TCL", "UNSUPPORTED", (char *)NULL);
    return TCL_ERROR;
#endif
}

/*
 *---------------------------------------------------------------------------
 *
 * TclMacOSXCopyFileAttributes --
 *
 *	Copy the MacOSX attributes and resource fork (if present) from one
 *	file to another.
 *
 * Results:
 *	Standard Tcl result.
 *
 * Side effects:
 *	MacOSX attributes and resource fork are updated in the new file to
 *	reflect the old file.
 *
 *---------------------------------------------------------------------------
 */

int
TclMacOSXCopyFileAttributes(
    const char *src,		/* Path name of source file (native). */
    const char *dst,		/* Path name of target file (native). */
    const Tcl_StatBuf *statBufPtr)
				/* Stat info for source file */
{
    if (MayUseCopyFile()) {
#ifdef HAVE_COPYFILE
	if (0 == copyfile(src, dst, NULL, (S_ISLNK(statBufPtr->st_mode)
		? COPYFILE_XATTR | COPYFILE_NOFOLLOW_SRC
		: COPYFILE_XATTR | COPYFILE_ACL))) {
	    return TCL_OK;
	}
#endif /* HAVE_COPYFILE */
    } else {
#if (!defined(HAVE_COPYFILE) || defined(WEAK_IMPORT_COPYFILE)) && defined(HAVE_GETATTRLIST)
	struct attrlist alist;
	fileinfobuf finfo;
	off_t *rsrcForkSize = (off_t *) &finfo.data;
	Tcl_DString srcBuf, dstBuf;
	int result;

	bzero(&alist, sizeof(struct attrlist));
	alist.bitmapcount = ATTR_BIT_MAP_COUNT;
	alist.commonattr = ATTR_CMN_FNDRINFO;

	if (getattrlist(src, &alist, &finfo.info_length,
		sizeof(fileinfobuf) - offsetof(fileinfobuf, info_length), 0)) {
	    return TCL_ERROR;
	}
	if (setattrlist(dst, &alist, &finfo.data, sizeof(finfo.data), 0)) {
	    return TCL_ERROR;
	}

	/*
	 * If we're a directory, we're done as they never have resource forks.
	 */

	if (S_ISDIR(statBufPtr->st_mode)) {
	    return TCL_OK;
	}

	/*
	 * We only copy a non-empty resource fork, so determine if that's the
	 * case first.
	 */

	alist.commonattr = 0;
	alist.fileattr = ATTR_FILE_RSRCLENGTH;
	if (getattrlist(src, &alist, &finfo.info_length,
		sizeof(fileinfobuf) - offsetof(fileinfobuf, info_length), 0)) {
	    return TCL_ERROR;
	} else if (*rsrcForkSize == 0) {
	    return TCL_OK;
	}

	/*
	 * Construct paths to resource forks.
	 */

	Tcl_DStringInit(&srcBuf);
	Tcl_DStringAppend(&srcBuf, src, -1);
	Tcl_DStringAppend(&srcBuf, _PATH_RSRCFORKSPEC, -1);
	Tcl_DStringInit(&dstBuf);
	Tcl_DStringAppend(&dstBuf, dst, -1);
	Tcl_DStringAppend(&dstBuf, _PATH_RSRCFORKSPEC, -1);

	/*
	 * Do the copy.
	 */

	result = TclUnixCopyFile(Tcl_DStringValue(&srcBuf),
		Tcl_DStringValue(&dstBuf), statBufPtr, 1);
	Tcl_DStringFree(&srcBuf);
	Tcl_DStringFree(&dstBuf);
	if (result == 0) {
	    return TCL_OK;
	}
#endif /* (!HAVE_COPYFILE || WEAK_IMPORT_COPYFILE) && HAVE_GETATTRLIST */
    }
    return TCL_ERROR;
}

/*
 *----------------------------------------------------------------------
 *
 * TclMacOSXMatchType --
 *
 *	This routine is used by the globbing code to check if a file matches a
 *	given mac type and/or creator code.
 *
 * Results:
 *	The return value is 1, 0 or -1 indicating whether the file matches the
 *	given criteria, does not match them, or an error occurred (in which
 *	case an error is left in interp).
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

int
TclMacOSXMatchType(
    Tcl_Interp *interp,		/* Interpreter to receive errors. */
    const char *pathName,	/* Native path to check. */
    const char *fileName,	/* Native filename to check. */
    Tcl_StatBuf *statBufPtr,	/* Stat info for file to check */
    Tcl_GlobTypeData *types)	/* Type description to match against. */
{
#ifdef HAVE_GETATTRLIST
    struct attrlist alist;
    fileinfobuf finfo;
    finderinfo *finder = (finderinfo *) &finfo.data;
    OSType osType;

    bzero(&alist, sizeof(struct attrlist));
    alist.bitmapcount = ATTR_BIT_MAP_COUNT;
    alist.commonattr = ATTR_CMN_FNDRINFO;
    if (getattrlist(pathName, &alist, &finfo.info_length,
	    sizeof(fileinfobuf) - offsetof(fileinfobuf, info_length), 0)) {
	return 0;
    }
    if ((types->perm & TCL_GLOB_PERM_HIDDEN) &&
	    !((finder->fdFlags & kFinfoIsInvisible) || (*fileName == '.'))) {
	return 0;
    }
    if (S_ISDIR(statBufPtr->st_mode)
	    && (types->macType || types->macCreator)) {
	/*
	 * Directories don't support types or creators.
	 */

	return 0;
    }
    if (types->macType) {
	if (GetOSTypeFromObj(interp, types->macType, &osType) != TCL_OK) {
	    return -1;
	}
	if (osType != OSSwapBigToHostInt32(finder->type)) {
	    return 0;
	}
    }
    if (types->macCreator) {
	if (GetOSTypeFromObj(interp, types->macCreator, &osType) != TCL_OK) {
	    return -1;
	}
	if (osType != OSSwapBigToHostInt32(finder->creator)) {
	    return 0;
	}
    }
#endif
    return 1;
}

/*
 *----------------------------------------------------------------------
 *
 * GetOSTypeFromObj --
 *
 *	Attempt to return an OSType from the Tcl object "objPtr".
 *
 * Results:
 *	Standard TCL result. If an error occurs during conversion, an error
 *	message is left in interp->objResult.
 *
 * Side effects:
 *	The string representation of objPtr will be updated if necessary.
 *
 *----------------------------------------------------------------------
 */

static int
GetOSTypeFromObj(
    Tcl_Interp *interp,		/* Used for error reporting if not NULL. */
    Tcl_Obj *objPtr,		/* The object from which to get an OSType. */
    OSType *osTypePtr)		/* Place to store resulting OSType. */
{
    int result = TCL_OK;

    if (objPtr->typePtr != &tclOSTypeType) {
	result = SetOSTypeFromAny(interp, objPtr);
    }
    *osTypePtr = (OSType) objPtr->internalRep.longValue;
    return result;
}

/*
 *----------------------------------------------------------------------
 *
 * NewOSTypeObj --
 *
 *	Create a new OSType object.
 *
 * Results:
 *	The newly created OSType object is returned, it has ref count 0.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

static Tcl_Obj *
NewOSTypeObj(
    const OSType osType)	/* OSType used to initialize the new
				 * object. */
{
    Tcl_Obj *objPtr;

    TclNewObj(objPtr);
    TclInvalidateStringRep(objPtr);
    objPtr->internalRep.longValue = (long) osType;
    objPtr->typePtr = &tclOSTypeType;
    return objPtr;
}

/*
 *----------------------------------------------------------------------
 *
 * SetOSTypeFromAny --
 *
 *	Attempts to force the internal representation for a Tcl object to
 *	tclOSTypeType, specifically.
 *
 * Results:
 *	The return value is a standard object Tcl result. If an error occurs
 *	during conversion, an error message is left in the interpreter's
 *	result unless "interp" is NULL.
 *
 *----------------------------------------------------------------------
 */

static int
SetOSTypeFromAny(
    Tcl_Interp *interp,		/* Tcl interpreter */
    Tcl_Obj *objPtr)		/* Pointer to the object to convert */
{
    const char *string;
    int result = TCL_OK;
    Tcl_DString ds;
    Tcl_Encoding encoding = Tcl_GetEncoding(NULL, "macRoman");
    int length;

    string = TclGetStringFromObj(objPtr, &length);
    Tcl_UtfToExternalDString(encoding, string, length, &ds);

    if (Tcl_DStringLength(&ds) > 4) {
	if (interp) {
	    Tcl_SetObjResult(interp, Tcl_ObjPrintf(
		    "expected Macintosh OS type but got \"%s\": ", string));
	    Tcl_SetErrorCode(interp, "TCL", "VALUE", "MAC_OSTYPE", (char *)NULL);
	}
	result = TCL_ERROR;
    } else {
	OSType osType;
	char bytes[4] = {'\0','\0','\0','\0'};

	memcpy(bytes, Tcl_DStringValue(&ds), Tcl_DStringLength(&ds));
	osType = (OSType) bytes[0] << 24 |
		(OSType) bytes[1] << 16 |
		(OSType) bytes[2] <<  8 |
		(OSType) bytes[3];
	TclFreeIntRep(objPtr);
	objPtr->internalRep.longValue = (long) osType;
	objPtr->typePtr = &tclOSTypeType;
    }
    Tcl_DStringFree(&ds);
    Tcl_FreeEncoding(encoding);
    return result;
}

/*
 *----------------------------------------------------------------------
 *
 * UpdateStringOfOSType --
 *
 *	Update the string representation for an OSType object. Note: This
 *	function does not free an existing old string rep so storage will be
 *	lost if this has not already been done.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The object's string is set to a valid string that results from the
 *	OSType-to-string conversion.
 *
 *----------------------------------------------------------------------
 */

static void
UpdateStringOfOSType(
    Tcl_Obj *objPtr)		/* OSType object whose string rep to
				 * update. */
{
    OSType osType = (OSType) objPtr->internalRep.longValue;
    Tcl_DString ds;
    Tcl_Encoding encoding = Tcl_GetEncoding(NULL, "macRoman");
    unsigned len;
    char src[5];

    src[0] = (char) (osType >> 24);
    src[1] = (char) (osType >> 16);
    src[2] = (char) (osType >>  8);
    src[3] = (char) (osType);
    src[4] = '\0';
    Tcl_ExternalToUtfDString(encoding, src, -1, &ds);
    len = (unsigned)Tcl_DStringLength(&ds) + 1;
    objPtr->bytes = ckalloc(len);
    memcpy(objPtr->bytes, Tcl_DStringValue(&ds), len);
    objPtr->length = Tcl_DStringLength(&ds);
    Tcl_DStringFree(&ds);
    Tcl_FreeEncoding(encoding);
}

/*
 * Local Variables:
 * mode: c
 * c-basic-offset: 4
 * fill-column: 78
 * End:
 */
