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