Mercurial > projects > ldc
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 |