Mercurial > projects > ldc
diff 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 diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tango/tango/io/TempFile.d Fri Jan 11 17:57:40 2008 +0100 @@ -0,0 +1,663 @@ +/****************************************************************************** + * + * 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; +} +