Download Reference Manual
The Developer's Library for D
About Wiki Forums Source Search Contact

root/trunk/tango/io/TempFile.d

Revision 3856, 21.1 kB (checked in by kris, 4 months ago)

moved Conduit and friends into tango.io.device in an effort to bring further clarity into tango.io -- this requires adjusting imports such that, for example, tango.io.FileConduit? becomes tango.io.device.FileConduit?

  • Property svn:eol-style set to native
  • Property svn:keywords set to Date Rev Author URL Id
Line 
1 /******************************************************************************
2  *
3  * copyright:   Copyright © 2007 Daniel Keep.  All rights reserved.
4  * license:     BSD style: $(LICENSE)
5  * version:     Initial release: December 2007
6  * authors:     Daniel Keep
7  * credits:     Thanks to John Reimer for helping test this module under
8  *              Linux.
9  *
10  ******************************************************************************/
11
12 module tango.io.TempFile;
13
14 import Path = tango.io.Path;
15 import tango.math.random.Kiss : Kiss;
16 import tango.io.device.DeviceConduit : DeviceConduit;
17 import tango.io.device.FileConduit : FileConduit;
18 import tango.io.FilePath : FilePath;
19 import tango.stdc.stringz : toStringz, toString16z;
20
21 /******************************************************************************
22  ******************************************************************************/
23
24 version( Win32 )
25 {
26     import tango.sys.Common : DWORD, LONG, MAX_PATH, PCHAR, CP_UTF8;
27
28     enum : DWORD { FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 }
29
30     version( Win32SansUnicode )
31     {
32         import tango.sys.Common :
33             GetVersionExA, OSVERSIONINFO,
34             CreateFileA, GENERIC_READ, GENERIC_WRITE,
35             CREATE_NEW, FILE_ATTRIBUTE_NORMAL, FILE_FLAG_DELETE_ON_CLOSE,
36             FILE_SHARE_READ, FILE_SHARE_WRITE,
37             LPSECURITY_ATTRIBUTES,
38             HANDLE, INVALID_HANDLE_VALUE,
39             GetTempPathA, SetFilePointer, GetLastError, ERROR_SUCCESS;
40
41         HANDLE CreateFile(char[] fn, DWORD da, DWORD sm,
42                 LPSECURITY_ATTRIBUTES sa, DWORD cd, DWORD faa, HANDLE tf)
43         {
44             return CreateFileA(toStringz(fn), da, sm, sa, cd, faa, tf);
45         }
46
47         char[] GetTempPath()
48         {
49             auto len = GetTempPathA(0, null);
50             if( len == 0 )
51                 throw new Exception("could not obtain temporary path");
52
53             auto result = new char[len+1];
54             len = GetTempPathA(len+1, result.ptr);
55             if( len == 0 )
56                 throw new Exception("could not obtain temporary path");
57             return Path.standard(result[0..len]);
58         }
59     }
60     else
61     {
62         import tango.sys.Common :
63             MultiByteToWideChar, WideCharToMultiByte,
64             GetVersionExW, OSVERSIONINFO,
65             CreateFileW, GENERIC_READ, GENERIC_WRITE,
66             CREATE_NEW, FILE_ATTRIBUTE_NORMAL, FILE_FLAG_DELETE_ON_CLOSE,
67             FILE_SHARE_READ, FILE_SHARE_WRITE,
68             LPSECURITY_ATTRIBUTES,
69             HANDLE, INVALID_HANDLE_VALUE,
70             GetTempPathW, SetFilePointer, GetLastError, ERROR_SUCCESS;
71
72         HANDLE CreateFile(char[] fn, DWORD da, DWORD sm,
73                 LPSECURITY_ATTRIBUTES sa, DWORD cd, DWORD faa, HANDLE tf)
74         {
75                 // convert into output buffer
76                 wchar[MAX_PATH+1] tmp = void;
77                 assert (fn.length < tmp.length);
78                 auto i = MultiByteToWideChar (CP_UTF8, 0, cast(PCHAR) fn.ptr,
79                                               fn.length, tmp.ptr, tmp.length);
80                 tmp[i] = 0;
81                 return CreateFileW(tmp.ptr, da, sm, sa, cd, faa, tf);
82         }
83
84         char[] GetTempPath()
85         {
86             auto len = GetTempPathW(0, null);
87             if( len == 0 )
88                 throw new Exception("could not obtain temporary path");
89
90             auto result = new wchar[len+1];
91             len = GetTempPathW(len+1, result.ptr);
92             if( len == 0 )
93                 throw new Exception("could not obtain temporary path");
94
95             auto dir = new char [len * 3];
96             auto i = WideCharToMultiByte (CP_UTF8, 0, result.ptr, len,
97                                           cast(PCHAR) dir.ptr, dir.length, null, null);
98             return Path.standard (dir[0..i]);
99         }
100     }
101
102     // Determines if reparse points (aka: symlinks) are supported.  Support
103     // was introduced in Windows Vista.
104     bool reparseSupported()
105     {
106         OSVERSIONINFO versionInfo = void;
107         versionInfo.dwOSVersionInfoSize = versionInfo.sizeof;
108
109         void e(){throw new Exception("could not determine Windows version");};
110
111         version( Win32SansUnicode )
112         {
113             if( !GetVersionExA(&versionInfo) ) e();
114         }
115         else
116         {
117             if( !GetVersionExW(&versionInfo) ) e();
118         }
119
120         return (versionInfo.dwMajorVersion >= 6);
121     }
122 }
123
124 else version( Posix )
125 {
126     import tango.stdc.posix.fcntl : open, O_CREAT, O_EXCL, O_RDWR;
127     import tango.stdc.posix.pwd : getpwnam;
128     import tango.stdc.posix.unistd : access, getuid, lseek, unlink, W_OK;
129     import tango.stdc.posix.sys.stat : stat, stat_t;
130    
131     import tango.sys.Environment : Environment;
132
133     enum { O_LARGEFILE = 0x8000 }
134
135     version( linux )
136     {
137         // NOTE: This constant is actually platform-dependant for some
138         // God-only-knows reason.  *sigh*  It should be fine for 'generic'
139         // architectures, but other ones will need to be double-checked.
140         enum { O_NOFOLLOW = 00400000 }
141     }
142     else version( darwin )
143     {
144         enum { O_NOFOLLOW = 0x0100 }
145     }
146     else version( freebsd )
147     {
148         enum { O_NOFOLLOW = 0x0100 }
149     }
150     else
151     {
152         pragma(msg, "Cannot use TempFile: O_NOFOLLOW is not "
153                 "defined for this platform.");
154         static assert(false);
155     }
156 }
157
158 /******************************************************************************
159  *
160  * The TempFile class aims to provide a safe way of creating and destroying
161  * temporary files.  The TempFile class will automatically close temporary
162  * files when the object is destroyed, so it is recommended that you make
163  * appropriate use of scoped destruction.
164  *
165  * Temporary files can be created with one of several styles, much like normal
166  * FileConduits.  TempFile styles have the following properties:
167  *
168  * $(UL
169  * $(LI $(B Transience): this determines whether the file should be destroyed
170  * as soon as it is closed (transient,) or continue to persist even after the
171  * application has terminated (permanent.))
172  * )
173  *
174  * Eventually, this will be expanded to give you greater control over the
175  * temporary file's properties.
176  *
177  * For the typical use-case (creating a file to temporarily store data too
178  * large to fit into memory,) the following is sufficient:
179  *
180  * -----
181  *  {
182  *      scope temp = new TempFile;
183  *     
184  *      // Use temp as a normal conduit; it will be automatically closed when
185  *      // it goes out of scope.
186  *  }
187  * -----
188  *
189  * $(B Important):
190  * It is recommended that you $(I do not) use files created by this class to
191  * store sensitive information.  There are several known issues with the
192  * current implementation that could allow an attacker to access the contents
193  * of these temporary files.
194  *
195  * $(B Todo): Detail security properties and guarantees.
196  *
197  ******************************************************************************/
198
199 class TempFile : DeviceConduit, DeviceConduit.Seek
200 {
201     //alias FileConduit.Cache Cache;
202     //alias FileConduit.Share Share;
203
204     /+enum Visibility : ubyte
205     {
206         /**
207          * The temporary file will have read and write access to it restricted
208          * to the current user.
209          */
210         User,
211         /**
212          * The temporary file will have read and write access available to any
213          * user on the system.
214          */
215         World
216     }+/
217
218     /**************************************************************************
219      *
220      * This enumeration is used to control whether the temporary file should
221      * persist after the TempFile object has been destroyed.
222      *
223      **************************************************************************/
224
225     enum Transience : ubyte
226     {
227         /**
228          * The temporary file should be destroyed along with the owner object.
229          */
230         Transient,
231         /**
232          * The temporary file should persist after the object has been
233          * destroyed.
234          */
235         Permanent
236     }
237
238     /+enum Sensitivity : ubyte
239     {
240         /**
241          * Transient files will be truncated to zero length immediately
242          * before closure to prevent casual filesystem inspection to recover
243          * their contents.
244          *
245          * No additional action is taken on permanent files.
246          */
247         None,
248         /**
249          * Transient files will be zeroed-out before truncation, to mask their
250          * contents from more thorough filesystem inspection.
251          *
252          * This option is not compatible with permanent files.
253          */
254         Low
255         /+
256         /**
257          * Transient files will be overwritten first with zeroes, then with
258          * ones, and then with a random 32- or 64-bit pattern (dependant on
259          * which is most efficient.)  The file will then be truncated.
260          *
261          * This option is not compatible with permanent files.
262          */
263         Medium
264         +/
265     }+/
266
267     /**************************************************************************
268      *
269      * This structure is used to determine how the temporary files should be
270      * opened and used.
271      *
272      **************************************************************************/
273     struct Style
274     {
275         align(1):
276         //Visibility visibility;      ///
277         Transience transience;      ///
278         //Sensitivity sensitivity;    ///
279         //Share share;                ///
280         //Cache cache;                ///
281         int attempts = 10;          ///
282     }
283
284     /**
285      * Style for creating a transient temporary file that only the current
286      * user can access.
287      */
288     static const Style Transient = {Transience.Transient};
289     /**
290      * Style for creating a permanent temporary file that only the current
291      * user can access.
292      */
293     static const Style Permanent = {Transience.Permanent};
294
295     // Path to the temporary file
296     private char[] _path;
297
298     // Style we've opened with
299     private Style _style;
300
301     // Have we been detatched?
302     private bool detached = false;
303
304     ///
305     this(Style style = Style.init)
306     {
307         create(style);
308     }
309
310     ///
311     this(char[] prefix, Style style = Style.init)
312     {
313         create (prefix, style);
314     }
315
316     /// deprecated: please use char[] version instead
317     deprecated this(FilePath prefix, Style style = Style.init)
318     {
319         this (prefix.toString.dup, style);
320     }
321
322     ~this()
323     {
324         if( !detached ) this.detach();
325     }
326
327     /**************************************************************************
328      *
329      * Returns the path of the temporary file.  Please note that depending
330      * on your platform, the returned path may or may not actually exist if
331      * you specified a transient file.
332      *
333      **************************************************************************/
334     char[] path()
335     {
336         return _path;
337     }
338
339     /**************************************************************************
340      *
341      * Indicates the style that this TempFile was created with.
342      *
343      **************************************************************************/
344     Style style()
345     {
346         return _style;
347     }
348
349     override char[] toString()
350     {
351         if( path.length > 0 )
352             return path;
353         else
354             return "<TempFile>";
355     }
356
357     /**************************************************************************
358      *
359      * Returns the current cursor position within the file.
360      *
361      **************************************************************************/
362     long position()
363     {
364         return seek(0, Seek.Anchor.Current);
365     }
366
367     /**************************************************************************
368      *
369      * Returns the total length, in bytes, of the temporary file.
370      *
371      **************************************************************************/
372     long length()
373     {
374         long pos, ret;
375         pos = position;
376         ret = seek(0, Seek.Anchor.End);
377         seek(pos);
378         return ret;
379     }
380
381     /*
382      * Creates a new temporary file with the given style.
383      */
384     private void create(Style style)
385     {
386         create(tempPath, style);
387     }
388
389     private void create(char[] prefix, Style style)
390     {
391         for( size_t i=0; i<style.attempts; ++i )
392         {
393             if( create_path(Path.join(prefix, randomName), style) )
394                 return;
395         }
396
397         error("could not create temporary file");
398     }
399
400     version( Win32 )
401     {
402         private static const DEFAULT_LENGTH = 6;
403         private static const DEFAULT_PREFIX = "~t";
404         private static const DEFAULT_SUFFIX = ".tmp";
405
406         private static const JUNK_CHARS =
407             "abcdefghijklmnopqrstuvwxyz0123456789";
408
409         /*
410          * Returns the path to the temporary directory.
411          */
412         public static char[] tempPath()
413         {
414             return GetTempPath;
415         }
416
417         /*
418          * Creates a new temporary file at the given path, with the specified
419          * style.
420          */
421         private bool create_path(char[] path, Style style)
422         {
423             // TODO: Check permissions directly and throw an exception;
424             // otherwise, we could spin trying to make a file when it's
425             // actually not possible.
426
427             // This code is largely stolen from FileConduit
428             DWORD attr, share, access, create;
429
430             alias DWORD[] Flags;
431
432             // Basic stuff
433             access = GENERIC_READ | GENERIC_WRITE;
434             share = 0; // No sharing
435             create = CREATE_NEW;
436
437             // Set up flags
438             attr = FILE_ATTRIBUTE_NORMAL;
439             attr |= reparseSupported ? FILE_FLAG_OPEN_REPARSE_POINT : 0;
440             if( style.transience == Transience.Transient )
441                 attr |= FILE_FLAG_DELETE_ON_CLOSE;
442
443             handle = CreateFile(
444                     /*lpFileName*/ path,
445                     /*dwDesiredAccess*/ access,
446                     /*dwShareMode*/ share,
447                     /*lpSecurityAttributes*/ null,
448                     /*dwCreationDisposition*/ CREATE_NEW,
449                     /*dwFlagsAndAttributes*/ attr,
450                     /*hTemplateFile*/ null
451                     );
452
453             if( handle is INVALID_HANDLE_VALUE )
454                 return false;
455
456             _path = path;
457             _style = style;
458             return true;
459         }
460
461         // See DDoc version
462         long seek(long offset, Seek.Anchor anchor = Seek.Anchor.Begin)
463         {
464             LONG high = cast(LONG) (offset >> 32);
465             long result = SetFilePointer (handle, cast(LONG) offset,
466                                           &high, anchor);
467
468             if (result is -1 &&
469                 GetLastError() != ERROR_SUCCESS)
470                 error();
471
472             return result + (cast(long) high << 32);
473         }
474     }
475     else version( Posix )
476     {
477         private static const DEFAULT_LENGTH = 6;
478         private static const DEFAULT_PREFIX = ".tmp";
479
480         // Use "~" to work around a bug in DMD where it elides empty constants
481         private static const DEFAULT_SUFFIX = "~";
482
483         private static const JUNK_CHARS =
484             "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
485             "abcdefghijklmnopqrstuvwxyz0123456789";
486
487         /*
488          * Returns the path to the temporary directory.
489          */
490         public static char[] tempPath()
491         {
492             // Check for TMPDIR; failing that, use /tmp
493             if( auto tmpdir = Environment.get("TMPDIR") )
494                 return tmpdir.dup;
495             else
496                 return "/tmp/";
497         }
498
499         /*
500          * Creates a new temporary file at the given path, with the specified
501          * style.
502          */
503         private bool create_path(char[] path, Style style)
504         {
505             // Check suitability
506             {
507                 auto parent = Path.parse(path).path;
508                 auto parentz = toStringz(parent);
509
510                 // Make sure we have write access
511                 if( access(parentz, W_OK) == -1 )
512                     error("do not have write access to temporary directory");
513
514                 // Get info on directory
515                 stat_t sb;
516                 if( stat(parentz, &sb) == -1 )
517                     error("could not stat temporary directory");
518
519                 // Get root's UID
520                 auto pwe = getpwnam("root");
521                 if( pwe is null ) error("could not get root's uid");
522                 auto root_uid = pwe.pw_uid;
523                
524                 // Make sure either we or root are the owner
525                 if( !(sb.st_uid == root_uid || sb.st_uid == getuid) )
526                     error("temporary directory owned by neither root nor user");
527
528                 // Check to see if anyone other than us can write to the dir.
529                 if( (sb.st_mode & 022) != 0 && (sb.st_mode & 01000) == 0 )
530                     error("sticky bit not set on world-writable directory");
531             }
532
533             // Create file
534             {
535                 auto flags = O_LARGEFILE | O_CREAT | O_EXCL
536                     | O_NOFOLLOW | O_RDWR;
537
538                 auto pathz = toStringz(path);
539
540                 handle = open(pathz, flags, 0600);
541                 if( handle is -1 )
542                     return false;
543
544                 if( style.transience == Transience.Transient )
545                 {
546                     // BUG TODO: check to make sure the path still points
547                     // to the file we opened.  Pity you can't unlink a file
548                     // descriptor...
549
550                     // NOTE: This should be an exception and not simply
551                     // returning false, since this is a violation of our
552                     // guarantees.
553                     if( unlink(pathz) == -1 )
554                         error("could not remove transient file");
555                 }
556
557                 _path = path;
558                 _style = style;
559
560                 return true;
561             }
562         }
563
564         // See DDoc version
565         long seek(long offset, Seek.Anchor anchor = Seek.Anchor.Begin)
566         {
567             assert( offset <= int.sizeof );
568             long result = lseek(handle, cast(int) offset, anchor);
569             if (result is -1)
570                 error();
571             return result;
572         }
573     }
574     else version( DDoc )
575     {
576         /**********************************************************************
577          *
578          * Seeks the temporary file's cursor to the given location.
579          *
580          **********************************************************************/
581         long seek(long offset, Seek.Anchor anchor = Seek.Anchor.Begin);
582
583         /**********************************************************************
584          *
585          * Returns the path to the directory where temporary files will be
586          * created.  The returned path is safe to mutate.
587          *
588          **********************************************************************/
589         char[] tempPath();
590     }
591     else
592     {
593         static assert(false, "Unsupported platform");
594     }
595
596     /*
597      * Generates a new random file name, sans directory.
598      */
599     private char[] randomName(uint length=DEFAULT_LENGTH,
600             char[] prefix=DEFAULT_PREFIX,
601             char[] suffix=DEFAULT_SUFFIX)
602     {
603         auto junk = new char[length];
604         scope(exit) delete junk;
605
606         foreach( ref c ; junk )
607             c = JUNK_CHARS[Kiss.shared.toInt($)];
608
609         return prefix~junk~suffix;
610     }
611    
612     override void detach()
613     {
614         static assert( !is(Sensitivity) );
615         super.detach();
616         detached = true;
617     }
618 }
619
620 version( TempFile_SelfTest ):
621
622 import tango.io.Console : Cin;
623 import tango.io.Stdout : Stdout;
624
625 void main()
626 {
627     Stdout(r"
628 Please ensure that the transient file no longer exists once the TempFile
629 object is destroyed, and that the permanent file does.  You should also check
630 the following on both:
631
632  * the file should be owned by you,
633  * the owner should have read and write permissions,
634  * no other permissions should be set on the file.
635
636 For POSIX systems:
637
638  * the temp directory should be owned by either root or you,
639  * if anyone other than root or you can write to it, the sticky bit should be
640    set,
641  * if the directory is writable by anyone other than root or the user, and the
642    sticky bit is *not* set, then creating the temporary file should fail.
643
644 You might want to delete the permanent one afterwards, too. :)")
645     .newline;
646
647     Stdout.formatln("Creating a transient file:");
648     {
649         scope tempFile = new TempFile(/*TempFile.UserPermanent*/);
650         scope(exit) tempFile.detach;
651
652         Stdout.formatln(" .. path: {}", tempFile);
653
654         tempFile.output.write("Transient temp file.");
655
656         char[] buffer = new char[1023];
657         tempFile.seek(0);
658         buffer = buffer[0..tempFile.input.read(buffer)];
659
660         Stdout.formatln(" .. contents: \"{}\"", buffer);
661
662         Stdout(" .. press Enter to destroy TempFile object.").newline;
663         Cin.copyln();
664     }
665
666     Stdout.newline;
667
668     Stdout.formatln("Creating a permanent file:");
669     {
670         scope tempFile = new TempFile(TempFile.Permanent);
671         scope(exit) tempFile.detach;
672
673         Stdout.formatln(" .. path: {}", tempFile);</