view tango/tango/io/TempFile.d @ 132:1700239cab2e trunk

[svn r136] MAJOR UNSTABLE UPDATE!!! Initial commit after moving to Tango instead of Phobos. Lots of bugfixes... This build is not suitable for most things.
author lindquist
date Fri, 11 Jan 2008 17:57:40 +0100
parents
children
line wrap: on
line source

/******************************************************************************
 *
 * copyright:   Copyright © 2007 Daniel Keep.  All rights reserved.
 * license:     BSD style: $(LICENSE)
 * version:     Initial release: December 2007
 * authors:     Daniel Keep
 * credits:     Thanks to John Reimer for helping test this module under
 *              Linux.
 *
 ******************************************************************************/

module tango.io.TempFile;

import tango.math.Random : Random;
import tango.io.DeviceConduit : DeviceConduit;
import tango.io.FileConduit : FileConduit;
import tango.io.FilePath : FilePath/*, PathView*/;
import tango.stdc.stringz : toStringz, toString16z;

/******************************************************************************
 ******************************************************************************/

version( Win32 )
{
    import tango.sys.Common : DWORD, LONG;

    enum : DWORD { FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 }

    version( Win32SansUnicode )
    {
        import tango.sys.Common :
            GetVersionExA, OSVERSIONINFO,
            CreateFileA, GENERIC_READ, GENERIC_WRITE,
            CREATE_NEW, FILE_ATTRIBUTE_NORMAL, FILE_FLAG_DELETE_ON_CLOSE,
            FILE_SHARE_READ, FILE_SHARE_WRITE,
            LPSECURITY_ATTRIBUTES,
            HANDLE, INVALID_HANDLE_VALUE,
            GetTempPathA, SetFilePointer, GetLastError, ERROR_SUCCESS;

        HANDLE CreateFile(FilePath fn, DWORD da, DWORD sm,
                LPSECURITY_ATTRIBUTES sa, DWORD cd, DWORD faa, HANDLE tf)
        {
            return CreateFileA(fn.cString.ptr, da, sm, sa, cd, faa, tf);
        }

        char[] GetTempPath()
        {
            auto len = GetTempPathA(0, null);
            if( len == 0 )
                throw new Exception("could not obtain temporary path");

            auto result = new char[len+1];
            len = GetTempPathA(len+1, result.ptr);
            if( len == 0 )
                throw new Exception("could not obtain temporary path");
            return result[0..len];
        }
    }
    else
    {
        import tango.sys.Common :
            GetVersionExW, OSVERSIONINFO,
            CreateFileW, GENERIC_READ, GENERIC_WRITE,
            CREATE_NEW, FILE_ATTRIBUTE_NORMAL, FILE_FLAG_DELETE_ON_CLOSE,
            FILE_SHARE_READ, FILE_SHARE_WRITE,
            LPSECURITY_ATTRIBUTES,
            HANDLE, INVALID_HANDLE_VALUE,
            GetTempPathW, SetFilePointer, GetLastError, ERROR_SUCCESS;

        import tango.text.convert.Utf : toString, toString16;

        HANDLE CreateFile(FilePath fn, DWORD da, DWORD sm,
                LPSECURITY_ATTRIBUTES sa, DWORD cd, DWORD faa, HANDLE tf)
        {
            return CreateFileW(toString16(fn.cString).ptr,
                    da, sm, sa, cd, faa, tf);
        }

        char[] GetTempPath()
        {
            auto len = GetTempPathW(0, null);
            if( len == 0 )
                throw new Exception("could not obtain temporary path");

            auto result = new wchar[len+1];
            len = GetTempPathW(len+1, result.ptr);
            if( len == 0 )
                throw new Exception("could not obtain temporary path");
            return toString(result[0..len]);
        }
    }

    // Determines if reparse points (aka: symlinks) are supported.  Support
    // was introduced in Windows Vista.
    bool reparseSupported()
    {
        OSVERSIONINFO versionInfo;
        versionInfo.dwOSVersionInfoSize = versionInfo.sizeof;

        void e(){throw new Exception("could not determine Windows version");};

        version( Win32SansUnicode )
            if( !GetVersionExA(&versionInfo) ) e();
        else
            if( !GetVersionExW(&versionInfo) ) e();

        return (versionInfo.dwMajorVersion >= 6);
    }
}

else version( Posix )
{
    import tango.stdc.posix.fcntl : open, O_CREAT, O_EXCL, O_RDWR;
    import tango.stdc.posix.pwd : getpwnam;
    import tango.stdc.posix.unistd : access, getuid, lseek, unlink, W_OK;
    import tango.stdc.posix.sys.stat : stat, stat_t;
    
    import tango.sys.Environment : Environment;

    enum { O_LARGEFILE = 0x8000 }

    version( linux )
    {
        // NOTE: This constant is actually platform-dependant for some
        // God-only-knows reason.  *sigh*  It should be fine for 'generic'
        // architectures, but other ones will need to be double-checked.
        enum { O_NOFOLLOW = 00400000 }
    }
    else version( darwin )
    {
        enum { O_NOFOLLOW = 0x0100 }
    }
    else
    {
        pragma(msg, "Cannot use TempFile: O_NOFOLLOW is not "
                "defined for this platform.");
        static assert(false);
    }
}

/******************************************************************************
 *
 * The TempFile class aims to provide a safe way of creating and destroying
 * temporary files.  The TempFile class will automatically close temporary
 * files when the object is destroyed, so it is recommended that you make
 * appropriate use of scoped destruction.
 *
 * Temporary files can be created with one of several styles, much like normal
 * FileConduits.  TempFile styles have the following properties:
 *
 * $(UL
 * $(LI $(B Transience): this determines whether the file should be destroyed
 * as soon as it is closed (transient,) or continue to persist even after the
 * application has terminated (permanent.))
 * )
 *
 * Eventually, this will be expanded to give you greater control over the
 * temporary file's properties.
 *
 * For the typical use-case (creating a file to temporarily store data too
 * large to fit into memory,) the following is sufficient:
 *
 * -----
 *  {
 *      scope temp = new TempFile;
 *      
 *      // Use temp as a normal conduit; it will be automatically closed when
 *      // it goes out of scope.
 *  }
 * -----
 *
 * $(B Important):
 * It is recommended that you $(I do not) use files created by this class to
 * store sensitive information.  There are several known issues with the
 * current implementation that could allow an attacker to access the contents
 * of these temporary files.
 *
 * $(B Todo): Detail security properties and guarantees.
 *
 ******************************************************************************/

class TempFile : DeviceConduit, DeviceConduit.Seek
{
    //alias FileConduit.Cache Cache;
    //alias FileConduit.Share Share;

    /+enum Visibility : ubyte
    {
        /**
         * The temporary file will have read and write access to it restricted
         * to the current user.
         */
        User,
        /**
         * The temporary file will have read and write access available to any
         * user on the system.
         */
        World
    }+/

    /**************************************************************************
     * 
     * This enumeration is used to control whether the temporary file should
     * persist after the TempFile object has been destroyed.
     *
     **************************************************************************/

    enum Transience : ubyte
    {
        /**
         * The temporary file should be destroyed along with the owner object.
         */
        Transient,
        /**
         * The temporary file should persist after the object has been
         * destroyed.
         */
        Permanent
    }

    /+enum Sensitivity : ubyte
    {
        /**
         * Transient files will be truncated to zero length immediately
         * before closure to prevent casual filesystem inspection to recover
         * their contents.
         *
         * No additional action is taken on permanent files.
         */
        None,
        /**
         * Transient files will be zeroed-out before truncation, to mask their
         * contents from more thorough filesystem inspection.
         *
         * This option is not compatible with permanent files.
         */
        Low
        /+
        /**
         * Transient files will be overwritten first with zeroes, then with
         * ones, and then with a random 32- or 64-bit pattern (dependant on
         * which is most efficient.)  The file will then be truncated.
         *
         * This option is not compatible with permanent files.
         */
        Medium
        +/
    }+/

    /**************************************************************************
     * 
     * This structure is used to determine how the temporary files should be
     * opened and used.
     *
     **************************************************************************/
    struct Style
    {
        align(1):
        //Visibility visibility;      ///
        Transience transience;      ///
        //Sensitivity sensitivity;    ///
        //Share share;                ///
        //Cache cache;                ///
        int attempts = 10;          ///
    }

    /**
     * Style for creating a transient temporary file that only the current
     * user can access.
     */
    static const Style Transient = {Transience.Transient};
    /**
     * Style for creating a permanent temporary file that only the current
     * user can access.
     */
    static const Style Permanent = {Transience.Permanent};

    // Path to the temporary file
    private /*PathView*/ FilePath _path;

    // Style we've opened with
    private Style _style;

    // Have we been detatched?
    private bool detached = false;

    ///
    this(Style style = Style.init)
    {
        create(style);
    }

    ///
    this(char[] prefix, Style style = Style.init)
    {
        this(FilePath(prefix), style);
    }

    ///
    this(FilePath prefix, Style style = Style.init)
    {
        create(prefix.dup, style);
    }

    ~this()
    {
        if( !detached ) this.detach();
    }

    /**************************************************************************
     *
     * Returns a PathView to the temporary file.  Please note that depending
     * on your platform, the returned path may or may not actually exist if
     * you specified a transient file.
     *
     **************************************************************************/
    /*PathView*/ FilePath path()
    {
        return _path;
    }

    /**************************************************************************
     *
     * Indicates the style that this TempFile was created with.
     *
     **************************************************************************/
    Style style()
    {
        return _style;
    }

    override char[] toString()
    {
        if( path.toString.length > 0 )
            return path.toString;
        else
            return "<TempFile>";
    }

    /**************************************************************************
     *
     * Returns the current cursor position within the file.
     *
     **************************************************************************/
    long position()
    {
        return seek(0, Seek.Anchor.Current);
    }

    /**************************************************************************
     *
     * Returns the total length, in bytes, of the temporary file.
     *
     **************************************************************************/
    long length()
    {
        long pos, ret;
        pos = position;
        ret = seek(0, Seek.Anchor.End);
        seek(pos);
        return ret;
    }

    /*
     * Creates a new temporary file with the given style.
     */
    private void create(Style style)
    {
        create(FilePath(tempPath).dup, style);
    }

    private void create(FilePath prefix, Style style)
    {
        for( size_t i=0; i<style.attempts; ++i )
        {
            if( create_path(prefix.dup.append(randomName), style) )
                return;
        }

        error("could not create temporary file");
    }

    version( Win32 )
    {
        private static const DEFAULT_LENGTH = 6;
        private static const DEFAULT_PREFIX = "~t";
        private static const DEFAULT_SUFFIX = ".tmp";

        private static const JUNK_CHARS = 
            "abcdefghijklmnopqrstuvwxyz0123456789";

        /*
         * Returns the path to the temporary directory.
         */
        private char[] tempPath()
        {
            return GetTempPath();
        }

        /*
         * Creates a new temporary file at the given path, with the specified
         * style.
         */
        private bool create_path(FilePath path, Style style)
        {
            // TODO: Check permissions directly and throw an exception;
            // otherwise, we could spin trying to make a file when it's
            // actually not possible.

            // This code is largely stolen from FileConduit
            DWORD attr, share, access, create;

            alias DWORD[] Flags;

            // Basic stuff
            access = GENERIC_READ | GENERIC_WRITE;
            share = 0; // No sharing
            create = CREATE_NEW;

            // Set up flags
            attr = FILE_ATTRIBUTE_NORMAL;
            attr |= reparseSupported ? FILE_FLAG_OPEN_REPARSE_POINT : 0;
            if( style.transience == Transience.Transient )
                attr |= FILE_FLAG_DELETE_ON_CLOSE;

            handle = CreateFile(
                    /*lpFileName*/ path,
                    /*dwDesiredAccess*/ access,
                    /*dwShareMode*/ share,
                    /*lpSecurityAttributes*/ null,
                    /*dwCreationDisposition*/ CREATE_NEW,
                    /*dwFlagsAndAttributes*/ attr,
                    /*hTemplateFile*/ null
                    );

            if( handle is INVALID_HANDLE_VALUE )
                return false;

            _path = path;
            _style = style;
            return true;
        }

        // See DDoc version
        long seek(long offset, Seek.Anchor anchor = Seek.Anchor.Begin)
        {
            LONG high = cast(LONG) (offset >> 32);
            long result = SetFilePointer (handle, cast(LONG) offset, 
                                          &high, anchor);

            if (result is -1 && 
                GetLastError() != ERROR_SUCCESS)
                error();

            return result + (cast(long) high << 32);
        }
    }
    else version( Posix )
    {
        private static const DEFAULT_LENGTH = 6;
        private static const DEFAULT_PREFIX = ".tmp";

        // Use "~" to work around a bug in DMD where it elides empty constants
        private static const DEFAULT_SUFFIX = "~";

        private static const JUNK_CHARS = 
            "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
            "abcdefghijklmnopqrstuvwxyz0123456789";

        /*
         * Returns the path to the temporary directory.
         */
        private char[] tempPath()
        {
            // Check for TMPDIR; failing that, use /tmp
            if( auto tmpdir = Environment.get("TMPDIR") )
                return tmpdir;
            else
                return "/tmp/";
        }

        /*
         * Creates a new temporary file at the given path, with the specified
         * style.
         */
        private bool create_path(FilePath path, Style style)
        {
            // Check suitability
            {
                auto parent = path.path;
                auto parentz = toStringz(parent);

                // Make sure we have write access
                if( access(parentz, W_OK) == -1 )
                    error("do not have write access to temporary directory");

                // Get info on directory
                stat_t sb;
                if( stat(parentz, &sb) == -1 )
                    error("could not stat temporary directory");

                // Get root's UID
                auto pwe = getpwnam("root");
                if( pwe is null ) error("could not get root's uid");
                auto root_uid = pwe.pw_uid;
                
                // Make sure either we or root are the owner
                if( !(sb.st_uid == root_uid || sb.st_uid == getuid) )
                    error("temporary directory owned by neither root nor user");

                // Check to see if anyone other than us can write to the dir.
                if( (sb.st_mode & 022) != 0 && (sb.st_mode & 01000) == 0 )
                    error("sticky bit not set on world-writable directory");
            }

            // Create file
            {
                auto flags = O_LARGEFILE | O_CREAT | O_EXCL
                    | O_NOFOLLOW | O_RDWR;

                auto pathz = path.cString.ptr;

                handle = open(pathz, flags, 0600);
                if( handle is -1 )
                    return false;

                if( style.transience == Transience.Transient )
                {
                    // BUG TODO: check to make sure the path still points
                    // to the file we opened.  Pity you can't unlink a file
                    // descriptor...

                    // NOTE: This should be an exception and not simply
                    // returning false, since this is a violation of our
                    // guarantees.
                    if( unlink(pathz) == -1 )
                        error("could not remove transient file");
                }

                _path = path;
                _style = style;

                return true;
            }
        }

        // See DDoc version
        long seek(long offset, Seek.Anchor anchor = Seek.Anchor.Begin)
        {
            long result = lseek(handle, offset, anchor);
            if (result is -1)
                error();
            return result;
        }
    }
    else version( DDoc )
    {
        /**********************************************************************
         * 
         * Seeks the temporary file's cursor to the given location.
         *
         **********************************************************************/
        long seek(long offset, Seek.Anchor anchor = Seek.Anchor.Begin);
    }
    else
    {
        static assert(false, "Unsupported platform");
    }

    /*
     * Generates a new random file name, sans directory.
     */
    private char[] randomName(uint length=DEFAULT_LENGTH,
            char[] prefix=DEFAULT_PREFIX,
            char[] suffix=DEFAULT_SUFFIX)
    {
        auto junk = new char[length];
        scope(exit) delete junk;

        foreach( ref c ; junk )
            c = JUNK_CHARS[Random.shared.next($)];

        return prefix~junk~suffix;
    }
    
    override void detach()
    {
        static assert( !is(Sensitivity) );
        super.detach();
        detached = true;
    }
}

version( TempFile_SelfTest ):

import tango.io.Console : Cin;
import tango.io.Stdout : Stdout;

void main()
{
    Stdout(r"
Please ensure that the transient file no longer exists once the TempFile
object is destroyed, and that the permanent file does.  You should also check
the following on both:

 * the file should be owned by you,
 * the owner should have read and write permissions,
 * no other permissions should be set on the file.

For POSIX systems:

 * the temp directory should be owned by either root or you,
 * if anyone other than root or you can write to it, the sticky bit should be
   set,
 * if the directory is writable by anyone other than root or the user, and the
   sticky bit is *not* set, then creating the temporary file should fail.

You might want to delete the permanent one afterwards, too. :)")
    .newline;

    Stdout.formatln("Creating a transient file:");
    {
        scope tempFile = new TempFile(/*TempFile.UserPermanent*/);
        scope(exit) tempFile.detach;

        Stdout.formatln(" .. path: {}", tempFile);

        tempFile.output.write("Transient temp file.");

        char[] buffer = new char[1023];
        tempFile.seek(0);
        buffer = buffer[0..tempFile.input.read(buffer)];

        Stdout.formatln(" .. contents: \"{}\"", buffer);

        Stdout(" .. press Enter to destroy TempFile object.").newline;
        Cin.copyln();
    }

    Stdout.newline;

    Stdout.formatln("Creating a permanent file:");
    {
        scope tempFile = new TempFile(TempFile.UserPermanent);
        scope(exit) tempFile.detach;

        Stdout.formatln(" .. path: {}", tempFile);

        tempFile.output.write("Permanent temp file.");

        char[] buffer = new char[1023];
        tempFile.seek(0);
        buffer = buffer[0..tempFile.input.read(buffer)];

        Stdout.formatln(" .. contents: \"{}\"", buffer);

        Stdout(" .. press Enter to destroy TempFile object.").flush;
        Cin.copyln();
    }

    Stdout("\nDone.").newline;
}