1 /*******************************************************************************
2 
3     copyright:  Copyright © 2007 Daniel Keep.  All rights reserved.
4 
5     license:    BSD style: $(LICENSE)
6 
7     version:    The Great Namechange: February 2008$(BR)
8 
9                 Initial release: December 2007
10 
11     author:     Daniel Keep
12 
13 *******************************************************************************/
14 
15 module tango.io.vfs.ZipFolder;
16 
17 import Path = tango.io.Path;
18 import tango.io.device.File : File;
19 import tango.io.FilePath : FilePath;
20 import tango.io.device.TempFile : TempFile;
21 import tango.util.compress.Zip : ZipReader, ZipBlockReader,
22        ZipWriter, ZipBlockWriter, ZipEntry, ZipEntryInfo, Method;
23 import tango.io.model.IConduit : IConduit, InputStream, OutputStream;
24 import tango.io.vfs.model.Vfs : VfsFolder, VfsFolderEntry, VfsFile,
25        VfsFolders, VfsFiles, VfsFilter, VfsStats, VfsFilterInfo,
26        VfsInfo, VfsSync;
27 import tango.time.Time : Time;
28 
29 debug( ZipFolder )
30 {
31     import tango.io.Stdout : Stderr;
32 }
33 
34 // This disables code that is causing heap corruption in Tango 0.99.3
35 version = Bug_HeapCorruption;
36 
37 // ************************************************************************ //
38 // ************************************************************************ //
39 
40 private
41 {
42     enum EntryType { Dir, File }
43 
44     /*
45      * Entries are what make up the internal tree that describes the
46      * filesystem of the archive.  Each Entry is either a directory or a file.
47      */
48     struct Entry
49     {
50         EntryType type;
51 
52         union
53         {
54             DirEntry dir;
55             FileEntry file;
56         }
57 
58         invariant()
59         {
60             if(type == EntryType.File)
61             {
62                 auto zn = file.zipEntry is null;
63                 auto tn = file.tempFile is null;
64                 assert( (zn && tn)
65                         /* zn xor tn */ || (!(zn&&tn)&&(zn||tn)) );
66             }
67         }
68 
69         const(char)[] fullname;
70         const(char)[] name;
71 
72         /+
73         invariant()
74         {
75             assert( (type == EntryType.Dir)
76                  || (type == EntryType.File) );
77 
78             assert( fullname.nz() );
79             assert( name.nz() );
80         }
81         +/
82 
83         VfsFilterInfo vfsFilterInfo;
84 
85         VfsInfo vfsInfo()
86         {
87             return &vfsFilterInfo;
88         }
89 
90         /*
91          * Updates the VfsInfo structure for this entry.
92          */
93         void makeVfsInfo()
94         {
95             with( vfsFilterInfo )
96             {
97                 // Cheat horribly here
98                 name = this.name;
99                 path = this.fullname[0..($-name.length+"/".length)];
100 
101                 folder = isDir;
102                 bytes = folder ? 0 : fileSize;
103             }
104         }
105 
106         @property bool isDir()
107         {
108             return (type == EntryType.Dir);
109         }
110 
111         @property bool isFile()
112         {
113             return (type == EntryType.File);
114         }
115 
116         @property ulong fileSize()
117         in
118         {
119             assert( type == EntryType.File );
120         }
121         body
122         {
123             if( file.zipEntry !is null )
124                 return file.zipEntry.size();
125 
126             else if( file.tempFile !is null )
127             {
128                 assert( file.tempFile.length >= 0 );
129                 return cast(ulong) file.tempFile.length;
130             }
131             else
132                 return 0;
133         }
134 
135         /*
136          * Opens a File Entry for reading.
137          *
138          * BUG: Currently, if a user opens a new or unmodified file for input,
139          * and then opens it for output, the two streams will be working with
140          * different underlying conduits.  This means that the result of
141          * openInput should probably be wrapped in some kind of switching
142          * stream that can update when the backing store for the file changes.
143          */
144         InputStream openInput()
145         in
146         {
147             assert( type == EntryType.File );
148         }
149         body
150         {
151             if( file.zipEntry !is null )
152             {
153                 file.zipEntry.verify();
154                 return file.zipEntry.open();
155             }
156             else if( file.tempFile !is null )
157                 return new WrapSeekInputStream(file.tempFile, 0);
158 
159             else
160                {
161                throw new Exception ("cannot open input stream for '"~fullname.idup~"'");
162                //return new DummyInputStream;
163                }
164         }
165 
166         /*
167          * Opens a file entry for output.
168          */
169         OutputStream openOutput()
170         in
171         {
172             assert( type == EntryType.File );
173         }
174         body
175         {
176             if( file.tempFile !is null )
177                 return new WrapSeekOutputStream(file.tempFile);
178 
179             else
180             {
181                 // Ok; we need to make a temporary file to store output in.
182                 // If we already have a zip entry, we need to dump that into
183                 // the temp. file and remove the zipEntry.
184                 if( file.zipEntry !is null )
185                 {
186                     {
187                         auto zi = file.zipEntry.open();
188                         scope(exit) zi.close();
189 
190                         file.tempFile = new TempFile;
191                         file.tempFile.copy(zi).close();
192 
193                         debug( ZipFolder )
194                             Stderr.formatln("Entry.openOutput: duplicated" ~
195                                     " temp file {} for {}",
196                                     file.tempFile, this.fullname);
197                     }
198 
199                     // TODO: Copy file info if available
200 
201                     file.zipEntry = null;
202                 }
203                 else
204                 {
205                     // Otherwise, just make a new, blank temp file
206                     file.tempFile = new TempFile;
207 
208                     debug( ZipFolder )
209                         Stderr.formatln("Entry.openOutput: created" ~
210                                 " temp file {} for {}",
211                                 file.tempFile, this.fullname);
212                 }
213 
214                 assert( file.tempFile !is null );
215                 return openOutput();
216             }
217         }
218 
219         void dispose()
220         {
221             fullname = name = null;
222 
223             with( vfsFilterInfo )
224             {
225                 name = path = null;
226             }
227 
228             dispose_children();
229         }
230 
231         void dispose_children()
232         {
233             switch( type )
234             {
235                 case EntryType.Dir:
236                     auto keys = dir.children.keys;
237                     scope(exit) keys.destroy;
238                     foreach( k ; keys )
239                     {
240                         auto child = dir.children[k];
241                         child.dispose();
242                         dir.children.remove(k);
243                         child.destroy;
244                     }
245                     dir.children = dir.children.init;
246                     break;
247 
248                 case EntryType.File:
249                     if( file.zipEntry !is null )
250                     {
251                         // Don't really need to do anything here
252                         file.zipEntry = null;
253                     }
254                     else if( file.tempFile !is null )
255                     {
256                         // Detatch to destroy the physical file itself
257                         file.tempFile.detach();
258                         file.tempFile = null;
259                     }
260                     break;
261 
262                 default:
263                     debug( ZipFolder ) Stderr.formatln(
264                             "Entry.dispose_children: unknown type {}",
265                             type);
266                     assert(false);
267             }
268         }
269     }
270 
271     struct DirEntry
272     {
273         Entry*[const(char)[]] children;
274     }
275 
276     struct FileEntry
277     {
278         ZipEntry zipEntry;
279         TempFile tempFile;
280     }
281 }
282 
283 // ************************************************************************ //
284 // ************************************************************************ //
285 
286 /**
287  * This class represents a folder in an archive.  In addition to supporting
288  * the sync operation, you can also use the archive member to get a reference
289  * to the underlying ZipFolder instance.
290  */
291 class ZipSubFolder : VfsFolder, VfsSync
292 {
293     ///
294     @property final const(char)[] name()
295     in { assert( valid ); }
296     body
297     {
298         return entry.name;
299     }
300 
301     ///
302     final override string toString()
303     {
304         assert( valid );
305         return entry.fullname.idup;
306     }
307 
308     ///
309     @property final VfsFile file(const(char)[] path)
310     in
311     {
312         assert( valid );
313         assert( !Path.parse(path).isAbsolute );
314     }
315     body
316     {
317         auto fp = Path.parse(path);
318         auto dir = fp.path;
319         auto name = fp.file;
320 
321         if (dir.length > 0 && '/' == dir[$-1]) {
322             dir = dir[0..$-1];
323         }
324 
325         // If the file is in another directory, then we need to look up that
326         // up first.
327         if( dir.nz() )
328         {
329             auto dir_ent = this.folder(dir);
330             auto dir_obj = dir_ent.open();
331             return dir_obj.file(name);
332         }
333         else
334         {
335             // Otherwise, we need to check and see whether the file is in our
336             // entry list.
337             if( auto file_entry = (name in this.entry.dir.children) )
338             {
339                 // It is; create a new object for it.
340                 return new ZipFile(archive, this.entry, *file_entry);
341             }
342             else
343             {
344                 // Oh dear... return a holding object.
345                 return new ZipFile(archive, this.entry, name);
346             }
347         }
348     }
349 
350     ///
351     @property final VfsFolderEntry folder(const(char)[] path)
352     in
353     {
354         assert( valid );
355         assert( !Path.parse(path).isAbsolute );
356     }
357     body
358     {
359         // Locate the folder in question.  We do this by "walking" the
360         // path components.  If we find a component that doesn't exist,
361         // then we create a ZipSubFolderEntry for the remainder.
362         Entry* curent = this.entry;
363 
364         // h is the "head" of the path, t is the remainder.  ht is both
365         // joined together.
366         const(char)[] h,t,ht;
367         ht = path;
368 
369         do
370         {
371             // Split ht at the first path separator.
372             assert( ht.nz() );
373             headTail(ht,h,t);
374 
375             // Look for a pre-existing subentry
376             auto subent = (h in curent.dir.children);
377             if( t.nz() && !!subent )
378             {
379                 // Move to the subentry, and split the tail on the next
380                 // iteration.
381                 curent = *subent;
382                 ht = t;
383             }
384             else
385                 // If the next component doesn't exist, return a folder entry.
386                 // If the tail is empty, return a folder entry as well (let
387                 // the ZipSubFolderEntry do the last lookup.)
388                 return new ZipSubFolderEntry(archive, curent, ht);
389         }
390         while( true );
391         //assert(false);
392     }
393 
394     ///
395     @property final VfsFolders self()
396     in { assert( valid ); }
397     body
398     {
399         return new ZipSubFolderGroup(archive, this, false);
400     }
401 
402     ///
403     @property final VfsFolders tree()
404     in { assert( valid ); }
405     body
406     {
407         return new ZipSubFolderGroup(archive, this, true);
408     }
409 
410     ///
411     final int opApply(scope int delegate(ref VfsFolder) dg)
412     in { assert( valid ); }
413     body
414     {
415         int result = 0;
416 
417         foreach( _,childEntry ; this.entry.dir.children )
418         {
419             if( childEntry.isDir )
420             {
421                 VfsFolder childFolder = new ZipSubFolder(archive, childEntry);
422                 if( (result = dg(childFolder)) != 0 )
423                     break;
424             }
425         }
426 
427         return result;
428     }
429 
430     ///
431     final VfsFolder clear()
432     in { assert( valid ); }
433     body
434     {
435 version( ZipFolder_NonMutating )
436 {
437         mutate_error("VfsFolder.clear");
438         assert(false);
439 }
440 else
441 {
442         // MUTATE
443         enforce_mutable();
444 
445         // Disposing of the underlying entry subtree should do our job for us.
446         entry.dispose_children();
447         mutate();
448         return this;
449 }
450     }
451 
452     ///
453     @property final bool writable()
454     in { assert( valid ); }
455     body
456     {
457         return !archive.readonly;
458     }
459 
460     /**
461      * Closes this folder object.  If commit is true, then the folder is
462      * sync'ed before being closed.
463      */
464     VfsFolder close(bool commit = true)
465     in { assert( valid ); }
466     body
467     {
468         // MUTATE
469         if( commit ) sync();
470 
471         // Just clean up our pointers
472         archive = null;
473         entry = null;
474         return this;
475     }
476 
477     /**
478      * This will flush any changes to the archive to disk.  Note that this
479      * applies to the entire archive, not just this folder and its contents.
480      */
481     override VfsFolder sync()
482     {
483         assert( valid );
484 
485         // MUTATE
486         archive.sync();
487         return this;
488     }
489 
490     ///
491     final void verify(VfsFolder folder, bool mounting)
492     in { assert( valid ); }
493     body
494     {
495         auto zipfolder = cast(ZipSubFolder) folder;
496 
497         if( mounting
498                 && zipfolder !is null
499                 && zipfolder.archive is archive )
500         {
501             auto src = this.toString();
502             auto dst = zipfolder.toString();
503 
504             auto len = src.length > dst.length ? dst.length : src.length;
505 
506             if( src[0..len] == dst[0..len] )
507                 error(`folders "`~dst~`" and "`~src~`" in archive "`
508                         ~archive.path~`" overlap`);
509         }
510     }
511 
512     /**
513      * Returns a reference to the underlying ZipFolder instance.
514      */
515     @property final ZipFolder archive() { return _archive; }
516 
517 private:
518     ZipFolder _archive;
519     Entry* entry;
520     VfsStats stats;
521 
522     @property final ZipFolder archive(ZipFolder v) { return _archive = v; }
523 
524     this(ZipFolder archive, Entry* entry)
525     {
526         this.reset(archive, entry);
527     }
528 
529     final void reset(ZipFolder archive, Entry* entry)
530     in
531     {
532         assert( archive !is null );
533         assert( entry.isDir );
534     }
535     out { assert( valid ); }
536     body
537     {
538         this.archive = archive;
539         this.entry = entry;
540     }
541 
542     @property final bool valid()
543     {
544         return( (archive !is null) && !archive.closed );
545     }
546 
547     final void enforce_mutable()
548     in { assert( valid ); }
549     body
550     {
551         if( archive.readonly )
552             // TODO: exception
553             throw new Exception("cannot mutate a read-only Zip archive");
554     }
555 
556     final void mutate()
557     in { assert( valid ); }
558     body
559     {
560         enforce_mutable();
561         archive.modified = true;
562     }
563 
564     final ZipSubFolder[] folders(bool collect)
565     in { assert( valid ); }
566     body
567     {
568         ZipSubFolder[] folders;
569         stats = stats.init;
570 
571         foreach( _,childEntry ; entry.dir.children )
572         {
573             if( childEntry.isDir )
574             {
575                 if( collect ) folders ~= new ZipSubFolder(archive, childEntry);
576                 ++ stats.folders;
577             }
578             else
579             {
580                 assert( childEntry.isFile );
581                 stats.bytes += childEntry.fileSize;
582                 ++ stats.files;
583             }
584         }
585 
586         return folders;
587     }
588 
589     final Entry*[] files(ref VfsStats stats, VfsFilter filter = null)
590     in { assert( valid ); }
591     body
592     {
593         Entry*[] files;
594 
595         foreach( _,childEntry ; entry.dir.children )
596         {
597             if( childEntry.isFile )
598                 if( filter is null || filter(childEntry.vfsInfo()) )
599                 {
600                     files ~= childEntry;
601                     stats.bytes += childEntry.fileSize;
602                     ++stats.files;
603                 }
604         }
605 
606         return files;
607     }
608 }
609 
610 // ************************************************************************ //
611 // ************************************************************************ //
612 
613 /**
614  * ZipFolder serves as the root object for all Zip archives in the VFS.
615  * Presently, it can only open archives on the local filesystem.
616  */
617 class ZipFolder : ZipSubFolder
618 {
619     /**
620      * Opens an archive from the local filesystem.  If the readonly argument
621      * is specified as true, then modification of the archive will be
622      * explicitly disallowed.
623      */
624     this(const(char)[] path, bool readonly=false)
625     out { assert( valid ); }
626     body
627     {
628         debug( ZipFolder )
629             Stderr.formatln(`ZipFolder("{}", {})`, path, readonly);
630         this.resetArchive(path, readonly);
631         super(this, root);
632     }
633 
634     /**
635      * Closes the archive, and releases all internal resources.  If the commit
636      * argument is true (the default), then changes to the archive will be
637      * flushed out to disk.  If false, changes will simply be discarded.
638      */
639     final override VfsFolder close(bool commit = true)
640     {
641         assert (valid);
642 
643         debug( ZipFolder )
644             Stderr.formatln("ZipFolder.close({})",commit);
645 
646         // MUTATE
647         if( commit ) sync();
648 
649         // Close ZipReader
650         if( zr !is null )
651         {
652             zr.close();
653             zr.destroy;
654         }
655 
656         // Destroy entries
657         root.dispose();
658         version( Bug_HeapCorruption )
659             root = null;
660         else
661             root.destroy;
662 
663         return this;
664     }
665 
666     /**
667      * Flushes all changes to the archive out to disk.
668      */
669     final override VfsFolder sync()
670     out
671     {
672         assert( valid );
673         assert( !modified );
674     }
675     body
676     {
677         assert( valid );
678 
679         debug( ZipFolder )
680             Stderr("ZipFolder.sync()").newline;
681 
682         if( !modified )
683             return this;
684 
685 version( ZipFolder_NonMutating )
686 {
687         mutate_error("ZipFolder.sync");
688         assert(false);
689 }
690 else
691 {
692         enforce_mutable();
693 
694         // First, we need to determine if we have any zip entries.  If we
695         // don't, then we can write directly to the path.  If there *are*
696         // zip entries, then we'll need to write to a temporary path instead.
697         OutputStream os;
698         TempFile tempFile;
699         scope(exit) if( tempFile !is null ) tempFile.destroy;
700 
701         auto p = Path.parse (path);
702         foreach( file ; this.tree.catalog )
703         {
704             if( auto zf = cast(ZipFile) file )
705                 if( zf.entry.file.zipEntry !is null )
706                 {
707                     tempFile = new TempFile(p.path, TempFile.Permanent);
708                     os = tempFile;
709                     debug( ZipFolder )
710                         Stderr.formatln(" sync: created temp file {}",
711                                 tempFile.path);
712                     break;
713                 }
714         }
715 
716         if( tempFile is null )
717         {
718             // Kill the current zip reader so we can re-open the file it's
719             // using.
720             if( zr !is null )
721             {
722                 zr.close();
723                 zr.destroy;
724             }
725 
726             os = new File(path, File.WriteCreate);
727         }
728 
729         // Now, we can create the archive.
730         {
731             scope zw = new ZipBlockWriter(os);
732             foreach( file ; this.tree.catalog )
733             {
734                 auto zei = ZipEntryInfo(file.toString()[1..$]);
735                 // BUG: Passthru doesn't maintain compression for some
736                 // reason...
737                 if( auto zf = cast(ZipFile) file )
738                 {
739                     if( zf.entry.file.zipEntry !is null )
740                         zw.putEntry(zei, zf.entry.file.zipEntry);
741                     else
742                         zw.putStream(zei, file.input);
743                 }
744                 else
745                     zw.putStream(zei, file.input);
746             }
747             zw.finish();
748         }
749 
750         // With that done, we can free all our handles, etc.
751         debug( ZipFolder )
752             Stderr(" sync: close").newline;
753         this.close(/*commit*/ false);
754         os.close();
755 
756         // If we wrote the archive into a temporary file, move that over the
757         // top of the old archive.
758         if( tempFile !is null )
759         {
760             debug( ZipFolder )
761                 Stderr(" sync: destroying temp file").newline;
762 
763             debug( ZipFolder )
764                 Stderr.formatln(" sync: renaming {} to {}",
765                         tempFile, path);
766 
767             Path.rename (tempFile.toString(), path);
768         }
769 
770         // Finally, re-open the archive so that we have all the nicely
771         // compressed files.
772         debug( ZipFolder )
773             Stderr(" sync: reset archive").newline;
774         this.resetArchive(path, readonly);
775 
776         debug( ZipFolder )
777             Stderr(" sync: reset folder").newline;
778         this.reset(this, root);
779 
780         debug( ZipFolder )
781             Stderr(" sync: done").newline;
782 
783         return this;
784 }
785     }
786 
787     /**
788      * Indicates whether the archive was opened for read-only access.  Note
789      * that in addition to the readonly constructor flag, this is also
790      * influenced by whether the file itself is read-only or not.
791      */
792     @property final bool readonly() { return _readonly; }
793 
794     /**
795      * Allows you to read and specify the path to the archive.  The effect of
796      * setting this is to change where the archive will be written to when
797      * flushed to disk.
798      */
799     @property final const(char)[] path() { return _path; }
800     @property final const(char)[] path(const(char)[] v) { return _path = v; } /// ditto
801 
802 private:
803     ZipReader zr;
804     Entry* root;
805     const(char)[] _path;
806     bool _readonly;
807     bool modified = false;
808 
809     @property final bool readonly(bool v) { return _readonly = v; }
810 
811     @property final bool closed()
812     {
813         debug( ZipFolder )
814             Stderr("ZipFolder.closed()").newline;
815         return (root is null);
816     }
817 
818     @property final bool valid()
819     {
820         debug( ZipFolder )
821             Stderr("ZipFolder.valid()").newline;
822         return !closed;
823     }
824 
825     final OutputStream mutateStream(OutputStream source)
826     {
827         return new EventSeekOutputStream(source,
828                 EventSeekOutputStream.Callbacks(
829                     null,
830                     null,
831                     &mutate_write,
832                     null));
833     }
834 
835     void mutate_write(size_t bytes, const(void)[] src)
836     {
837         if( !(bytes == 0 || bytes == IConduit.Eof) )
838             this.modified = true;
839     }
840 
841     void resetArchive(const(char)[] path, bool readonly=false)
842     out { assert( valid ); }
843     body
844     {
845         debug( ZipFolder )
846             Stderr.formatln(`ZipFolder.resetArchive("{}", {})`, path, readonly);
847 
848         debug( ZipFolder )
849             Stderr.formatln(" .. size of Entry: {0}, {0:x} bytes", Entry.sizeof);
850 
851         this.path = path;
852         this.readonly = readonly;
853 
854         // Make sure the modified flag is set appropriately
855         scope(exit) modified = false;
856 
857         // First, create a root entry
858         root = new Entry;
859         root.type = EntryType.Dir;
860         root.fullname = root.name = "/";
861 
862         // If the user allowed writing, also allow creating a new archive.
863         // Note that we MUST drop out here if the archive DOES NOT exist,
864         // since Path.isWriteable will throw an exception if called on a
865         // non-existent path.
866         if( !this.readonly && !Path.exists(path) )
867             return;
868 
869         // Update readonly to reflect the write-protected status of the
870         // archive.
871         this.readonly = this.readonly || !Path.isWritable(path);
872 
873         zr = new ZipBlockReader(path);
874 
875         // Parse the contents of the archive
876         foreach( zipEntry ; zr )
877         {
878             // Normalise name
879             auto name = FilePath(zipEntry.info.name.dup).standard.toString();
880 
881             // If the last character is '/', treat as a directory and skip
882             // TODO: is there a better way of detecting this?
883             if( name[$-1] == '/' )
884                 continue;
885 
886             // Now, we need to locate the right spot to insert this entry.
887             {
888                 // That's CURrent ENTity, not current OR currant...
889                 Entry* curent = root;
890                 const(char)[] h,t;
891                 headTail(name,h,t);
892                 while( t.nz() )
893                 {
894                     assert( curent.isDir );
895                     if( auto nextent = (h in curent.dir.children) )
896                         curent = *nextent;
897 
898                     else
899                     {
900                         // Create new directory entry
901                         Entry* dirent = new Entry;
902                         dirent.type = EntryType.Dir;
903                         if( curent.fullname != "/" )
904                             dirent.fullname = curent.fullname ~ "/" ~ h;
905                         else
906                             dirent.fullname = "/" ~ h;
907                         dirent.name = dirent.fullname[$-h.length..$];
908 
909                         // Insert into current entry
910                         curent.dir.children[dirent.name] = dirent;
911 
912                         // Make it the new current entry
913                         curent = dirent;
914                     }
915 
916                     headTail(t,h,t);
917                 }
918 
919                 // Getting here means that t is empty, which means the final
920                 // component of the path--the file name--is in h.  The entry
921                 // of the containing directory is in curent.
922 
923                 // Make sure the file isn't already there (you never know!)
924                 assert( !(h in curent.dir.children) );
925 
926                 // Create a new file entry for it.
927                 {
928                     // BUG: Bug_HeapCorruption
929                     // with ZipTest, on the resetArchive operation, on
930                     // the second time through this next line, it erroneously
931                     // allocates filent 16 bytes lower than curent.  Entry
932                     // is *way* larger than 16 bytes, and this causes it to
933                     // zero-out the existing root element, which leads to
934                     // segfaults later on at line +12:
935                     //
936                     //      // Insert
937                     //      curent.dir.children[filent.name] = filent;
938 
939                     Entry* filent = new Entry;
940                     filent.type = EntryType.File;
941                     if( curent.fullname != "/" )
942                         filent.fullname = curent.fullname ~ "/" ~ h;
943                     else
944                         filent.fullname = "/" ~ h;
945                     filent.name = filent.fullname[$-h.length..$];
946                     filent.file.zipEntry = zipEntry.dup();
947 
948                     filent.makeVfsInfo();
949 
950                     // Insert
951                     curent.dir.children[filent.name] = filent;
952                 }
953             }
954         }
955     }
956 }
957 
958 // ************************************************************************ //
959 // ************************************************************************ //
960 
961 /**
962  * This class represents a file within an archive.
963  */
964 class ZipFile : VfsFile
965 {
966     ///
967     @property final const(char)[] name()
968     in { assert( valid ); }
969     body
970     {
971         if( entry ) return entry.name;
972         else        return name_;
973     }
974 
975     ///
976     final override string toString()
977     {
978         assert( valid );
979         if( entry ) return entry.fullname.idup;
980         else        return (parent.fullname ~ "/" ~ name_).idup;
981     }
982 
983     ///
984     @property final bool exists()
985     in { assert( valid ); }
986     body
987     {
988         // If we've only got a parent and a name, this means we don't actually
989         // exist; EXISTENTIAL CRISIS TEIM!!!
990         return !!entry;
991     }
992 
993     ///
994     @property final ulong size()
995     in { assert( valid ); }
996     body
997     {
998         if( exists )
999             return entry.fileSize;
1000         else
1001             error("ZipFile.size: cannot reliably determine size of a " ~
1002                     "non-existent file");
1003 
1004         assert(false);
1005     }
1006 
1007     ///
1008     final VfsFile copy(VfsFile source)
1009     in { assert( valid ); }
1010     body
1011     {
1012 version( ZipFolder_NonMutating )
1013 {
1014         mutate_error("ZipFile.copy");
1015         assert(false);
1016 }
1017 else
1018 {
1019         // MUTATE
1020         enforce_mutable();
1021 
1022         if( !exists ) this.create();
1023         this.output.copy(source.input);
1024 
1025         return this;
1026 }
1027     }
1028 
1029     ///
1030     final VfsFile move(VfsFile source)
1031     in { assert( valid ); }
1032     body
1033     {
1034 version( ZipFolder_NonMutating )
1035 {
1036         mutate_error("ZipFile.move");
1037         assert(false);
1038 }
1039 else
1040 {
1041         // MUTATE
1042         enforce_mutable();
1043 
1044         this.copy(source);
1045         source.remove();
1046 
1047         return this;
1048 }
1049     }
1050 
1051     ///
1052     final VfsFile create()
1053     in { assert( valid ); }
1054     out { assert( valid ); }
1055     body
1056     {
1057 version( ZipFolder_NonMutating )
1058 {
1059         mutate_error("ZipFile.create");
1060         assert(false);
1061 }
1062 else
1063 {
1064         if( exists )
1065             error("ZipFile.create: cannot create already existing file: " ~
1066                     "this folder ain't big enough for the both of 'em");
1067 
1068         // MUTATE
1069         enforce_mutable();
1070 
1071         auto entry = new Entry;
1072         entry.type = EntryType.File;
1073         entry.fullname = parent.fullname.dir_app(name);
1074         entry.name = entry.fullname[$-name.length..$];
1075         entry.makeVfsInfo();
1076 
1077         assert( !(entry.name in parent.dir.children) );
1078         parent.dir.children[entry.name] = entry;
1079         this.reset(archive, parent, entry);
1080         mutate();
1081 
1082         // Done
1083         return this;
1084 }
1085     }
1086 
1087     ///
1088     final VfsFile create(InputStream stream)
1089     in { assert( valid ); }
1090     body
1091     {
1092 version( ZipFolder_NonMutating )
1093 {
1094         mutate_error("ZipFile.create");
1095         assert(false);
1096 }
1097 else
1098 {
1099         create();
1100         output.copy(stream).close();
1101         return this;
1102 }
1103     }
1104 
1105     ///
1106     final VfsFile remove()
1107     in{ assert( valid ); }
1108     out { assert( valid ); }
1109     body
1110     {
1111 version( ZipFolder_NonMutating )
1112 {
1113         mutate_error("ZipFile.remove");
1114         assert(false);
1115 }
1116 else
1117 {
1118         if( !exists )
1119             error("ZipFile.remove: cannot remove non-existent file; " ~
1120                     "rather redundant, really");
1121 
1122         // MUTATE
1123         enforce_mutable();
1124 
1125         // Save the old name
1126         auto old_name = name;
1127 
1128         // Do the removal
1129         assert( !!(name in parent.dir.children) );
1130         parent.dir.children.remove(name);
1131         entry.dispose();
1132         entry = null;
1133         mutate();
1134 
1135         // Swap out our now empty entry for the name, so the file can be
1136         // directly recreated.
1137         this.reset(archive, parent, old_name);
1138 
1139         return this;
1140 }
1141     }
1142 
1143     ///
1144     @property final InputStream input()
1145     in { assert( valid ); }
1146     body
1147     {
1148         if( exists )
1149             return entry.openInput();
1150 
1151         else
1152             error("ZipFile.input: cannot open non-existent file for input; " ~
1153                     "results would not be very useful");
1154 
1155         assert(false);
1156     }
1157 
1158     ///
1159     @property final OutputStream output()
1160     in { assert( valid ); }
1161     body
1162     {
1163 version( ZipFolder_NonMutable )
1164 {
1165         mutate_error("ZipFile.output");
1166         assert(false);
1167 }
1168 else
1169 {
1170         // MUTATE
1171         enforce_mutable();
1172 
1173         // Don't call mutate(); defer that until the user actually writes to or
1174         // modifies the underlying stream.
1175         return archive.mutateStream(entry.openOutput());
1176 }
1177     }
1178 
1179     ///
1180     @property final VfsFile dup()
1181     in { assert( valid ); }
1182     body
1183     {
1184         if( entry )
1185             return new ZipFile(archive, parent, entry);
1186         else
1187             return new ZipFile(archive, parent, name);
1188     }
1189 
1190     ///
1191     @property final Time modified()
1192     {
1193         return entry.file.zipEntry.info.modified;
1194     }
1195 
1196     private:
1197     ZipFolder archive;
1198     Entry* entry;
1199 
1200     Entry* parent;
1201     const(char)[] name_;
1202 
1203     this()
1204     out { assert( !valid ); }
1205     body
1206     {
1207     }
1208 
1209     this(ZipFolder archive, Entry* parent, Entry* entry)
1210     in
1211     {
1212         assert( archive !is null );
1213         assert( parent );
1214         assert( parent.isDir );
1215         assert( entry );
1216         assert( entry.isFile );
1217         assert( parent.dir.children[entry.name] is entry );
1218     }
1219     out { assert( valid ); }
1220     body
1221     {
1222         this.reset(archive, parent, entry);
1223     }
1224 
1225     this(ZipFolder archive, Entry* parent, const(char)[] name)
1226     in
1227     {
1228         assert( archive !is null );
1229         assert( parent );
1230         assert( parent.isDir );
1231         assert( name.nz() );
1232         assert( !(name in parent.dir.children) );
1233     }
1234     out { assert( valid ); }
1235     body
1236     {
1237         this.reset(archive, parent, name);
1238     }
1239 
1240     @property final bool valid()
1241     {
1242         return( (archive !is null) && !archive.closed );
1243     }
1244 
1245     final void enforce_mutable()
1246     in { assert( valid ); }
1247     body
1248     {
1249         if( archive.readonly )
1250             // TODO: exception
1251             throw new Exception("cannot mutate a read-only Zip archive");
1252     }
1253 
1254     final void mutate()
1255     in { assert( valid ); }
1256     body
1257     {
1258         enforce_mutable();
1259         archive.modified = true;
1260     }
1261 
1262     final void reset(ZipFolder archive, Entry* parent, Entry* entry)
1263     in
1264     {
1265         assert( archive !is null );
1266         assert( parent );
1267         assert( parent.isDir );
1268         assert( entry );
1269         assert( entry.isFile );
1270         assert( parent.dir.children[entry.name] is entry );
1271     }
1272     out { assert( valid ); }
1273     body
1274     {
1275         this.parent = parent;
1276         this.archive = archive;
1277         this.entry = entry;
1278         this.name_ = null;
1279     }
1280 
1281     final void reset(ZipFolder archive, Entry* parent, const(char)[] name)
1282     in
1283     {
1284         assert( archive !is null );
1285         assert( parent );
1286         assert( parent.isDir );
1287         assert( name.nz() );
1288         assert( !(name in parent.dir.children) );
1289     }
1290     out { assert( valid ); }
1291     body
1292     {
1293         this.archive = archive;
1294         this.parent = parent;
1295         this.entry = null;
1296         this.name_ = name;
1297     }
1298 
1299     final void close()
1300     in { assert( valid ); }
1301     out { assert( !valid ); }
1302     body
1303     {
1304         archive = null;
1305         parent = null;
1306         entry = null;
1307         name_ = null;
1308     }
1309 }
1310 
1311 // ************************************************************************ //
1312 // ************************************************************************ //
1313 
1314 class ZipSubFolderEntry : VfsFolderEntry
1315 {
1316     final VfsFolder open()
1317     in { assert( valid ); }
1318     body
1319     {
1320         auto entry = (name in parent.dir.children);
1321         if( entry )
1322             return new ZipSubFolder(archive, *entry);
1323 
1324         else
1325         {
1326             // NOTE: this can be called with a multi-part path.
1327             error("ZipSubFolderEntry.open: \""
1328                     ~ parent.fullname ~ "/" ~ name
1329                     ~ "\" does not exist");
1330 
1331             assert(false);
1332         }
1333     }
1334 
1335     final VfsFolder create()
1336     in { assert( valid ); }
1337     body
1338     {
1339 version( ZipFolder_NonMutating )
1340 {
1341         // TODO: different exception if folder exists (this operation is
1342         // currently invalid either way...)
1343         mutate_error("ZipSubFolderEntry.create");
1344         assert(false);
1345 }
1346 else
1347 {
1348         // MUTATE
1349         enforce_mutable();
1350 
1351         // If the folder exists, we can't really create it, now can we?
1352         if( this.exists )
1353             error("ZipSubFolderEntry.create: cannot create folder that already " ~
1354                     "exists, and believe me, I *tried*");
1355 
1356         // Ok, I suppose I can do this for ya...
1357         auto entry = new Entry;
1358         entry.type = EntryType.Dir;
1359         entry.fullname = parent.fullname.dir_app(name);
1360         entry.name = entry.fullname[$-name.length..$];
1361         entry.makeVfsInfo();
1362 
1363         assert( !(entry.name in parent.dir.children) );
1364         parent.dir.children[entry.name] = entry;
1365         mutate();
1366 
1367         // Done
1368         return new ZipSubFolder(archive, entry);
1369 }
1370     }
1371 
1372     @property final bool exists()
1373     in { assert( valid ); }
1374     body
1375     {
1376         return !!(name in parent.dir.children);
1377     }
1378 
1379 private:
1380     ZipFolder archive;
1381     Entry* parent;
1382     const(char)[] name;
1383 
1384     this(ZipFolder archive, Entry* parent, const(char)[] name)
1385     in
1386     {
1387         assert( archive !is null );
1388         assert( parent.isDir );
1389         assert( name.nz() );
1390         assert( name.single_path_part() );
1391     }
1392     out { assert( valid ); }
1393     body
1394     {
1395         this.archive = archive;
1396         this.parent = parent;
1397         this.name = name;
1398     }
1399 
1400     @property final bool valid()
1401     {
1402         return (archive !is null) && !archive.closed;
1403     }
1404 
1405     final void enforce_mutable()
1406     in { assert( valid ); }
1407     body
1408     {
1409         if( archive.readonly )
1410             // TODO: exception
1411             throw new Exception("cannot mutate a read-only Zip archive");
1412     }
1413 
1414     final void mutate()
1415     in { assert( valid ); }
1416     body
1417     {
1418         enforce_mutable();
1419         archive.modified = true;
1420     }
1421 }
1422 
1423 // ************************************************************************ //
1424 // ************************************************************************ //
1425 
1426 class ZipSubFolderGroup : VfsFolders
1427 {
1428     final int opApply(scope int delegate(ref VfsFolder) dg)
1429     in { assert( valid ); }
1430     body
1431     {
1432         int result = 0;
1433 
1434         foreach( folder ; members )
1435         {
1436             VfsFolder x = folder;
1437             if( (result = dg(x)) != 0 )
1438                 break;
1439         }
1440 
1441         return result;
1442     }
1443 
1444     @property final size_t files()
1445     in { assert( valid ); }
1446     body
1447     {
1448         uint files = 0;
1449 
1450         foreach( folder ; members )
1451             files += folder.stats.files;
1452 
1453         return files;
1454     }
1455 
1456     @property final size_t folders()
1457     in { assert( valid ); }
1458     body
1459     {
1460         return members.length;
1461     }
1462 
1463     @property final size_t entries()
1464     in { assert( valid ); }
1465     body
1466     {
1467         return files + folders;
1468     }
1469 
1470     @property final ulong bytes()
1471     in { assert( valid ); }
1472     body
1473     {
1474         ulong bytes = 0;
1475 
1476         foreach( folder ; members )
1477             bytes += folder.stats.bytes;
1478 
1479         return bytes;
1480     }
1481 
1482     final VfsFolders subset(const(char)[] pattern)
1483     in { assert( valid ); }
1484     body
1485     {
1486         ZipSubFolder[] set;
1487 
1488         foreach( folder ; members )
1489             if( Path.patternMatch(folder.name, pattern) )
1490                 set ~= folder;
1491 
1492         return new ZipSubFolderGroup(archive, set);
1493     }
1494 
1495     @property final VfsFiles catalog(const(char)[] pattern)
1496     in { assert( valid ); }
1497     body
1498     {
1499         bool filter (VfsInfo info)
1500         {
1501                 return Path.patternMatch(info.name, pattern);
1502         }
1503 
1504         return catalog (&filter);
1505     }
1506 
1507     @property final VfsFiles catalog(VfsFilter filter = null)
1508     in { assert( valid ); }
1509     body
1510     {
1511         return new ZipFileGroup(archive, this, filter);
1512     }
1513 
1514 private:
1515     ZipFolder archive;
1516     ZipSubFolder[] members;
1517 
1518     this(ZipFolder archive, ZipSubFolder root, bool recurse)
1519     out { assert( valid ); }
1520     body
1521     {
1522         this.archive = archive;
1523         members = root ~ scan(root, recurse);
1524     }
1525 
1526     this(ZipFolder archive, ZipSubFolder[] members)
1527     out { assert( valid ); }
1528     body
1529     {
1530         this.archive = archive;
1531         this.members = members;
1532     }
1533 
1534     @property final bool valid()
1535     {
1536         return (archive !is null) && !archive.closed;
1537     }
1538 
1539     final ZipSubFolder[] scan(ZipSubFolder root, bool recurse)
1540     in { assert( valid ); }
1541     body
1542     {
1543         auto folders = root.folders(recurse);
1544 
1545         if( recurse )
1546             foreach( child ; folders )
1547                 folders ~= scan(child, recurse);
1548 
1549         return folders;
1550     }
1551 }
1552 
1553 // ************************************************************************ //
1554 // ************************************************************************ //
1555 
1556 class ZipFileGroup : VfsFiles
1557 {
1558     final int opApply(scope int delegate(ref VfsFile) dg)
1559     in { assert( valid ); }
1560     body
1561     {
1562         int result = 0;
1563         auto file = new ZipFile;
1564 
1565         foreach( entry ; group )
1566         {
1567             file.reset(archive,entry.parent,entry.entry);
1568             VfsFile x = file;
1569             if( (result = dg(x)) != 0 )
1570                 break;
1571         }
1572 
1573         return result;
1574     }
1575 
1576     @property final size_t files()
1577     in { assert( valid ); }
1578     body
1579     {
1580         return group.length;
1581     }
1582 
1583     @property final ulong bytes()
1584     in { assert( valid ); }
1585     body
1586     {
1587         return stats.bytes;
1588     }
1589 
1590 private:
1591     ZipFolder archive;
1592     FileEntry[] group;
1593     VfsStats stats;
1594 
1595     struct FileEntry
1596     {
1597         Entry* parent;
1598         Entry* entry;
1599     }
1600 
1601     this(ZipFolder archive, ZipSubFolderGroup host, VfsFilter filter)
1602     out { assert( valid ); }
1603     body
1604     {
1605         this.archive = archive;
1606         foreach( folder ; host.members )
1607             foreach( file ; folder.files(stats, filter) )
1608                 group ~= FileEntry(folder.entry, file);
1609     }
1610 
1611     @property final bool valid()
1612     {
1613         return (archive !is null) && !archive.closed;
1614     }
1615 }
1616 
1617 // ************************************************************************ //
1618 // ************************************************************************ //
1619 
1620 private:
1621 
1622 void error(const(char)[] msg)
1623 {
1624     throw new Exception(msg.idup);
1625 }
1626 
1627 void mutate_error(const(char)[] method)
1628 {
1629     error(method ~ ": mutating the contents of a ZipFolder " ~
1630             "is not supported yet; terribly sorry");
1631 }
1632 
1633 bool nz(const(char)[] s)
1634 {
1635     return s.length > 0;
1636 }
1637 
1638 bool zero(const(char)[] s)
1639 {
1640     return s.length == 0;
1641 }
1642 
1643 bool single_path_part(const(char)[] s)
1644 {
1645     foreach( c ; s )
1646         if( c == '/' ) return false;
1647     return true;
1648 }
1649 
1650 char[] dir_app(const(char)[] dir, const(char)[] name)
1651 {
1652     return dir ~ (dir[$-1]!='/' ? "/" : "") ~ name;
1653 }
1654 
1655 inout(char)[] headTail(inout(char)[] path, out inout(char)[] head, out inout(char)[] tail)
1656 {
1657     foreach( i,dchar c ; path[1..$] )
1658         if( c == '/' )
1659         {
1660             head = path[0..i+1];
1661             tail = path[i+2..$];
1662             return head;
1663         }
1664 
1665     head = path;
1666     tail = null;
1667     return head;
1668 }
1669 
1670 debug (UnitTest)
1671 {
1672 unittest
1673 {
1674     const(char)[] h,t;
1675 
1676     headTail("/a/b/c", h, t);
1677     assert( h == "/a" );
1678     assert( t == "b/c" );
1679 
1680     headTail("a/b/c", h, t);
1681     assert( h == "a" );
1682     assert( t == "b/c" );
1683 
1684     headTail("a/", h, t);
1685     assert( h == "a" );
1686     assert( t == "" );
1687 
1688     headTail("a", h, t);
1689     assert( h == "a" );
1690     assert( t == "" );
1691 }
1692 }
1693 
1694 // ************************************************************************** //
1695 // ************************************************************************** //
1696 // ************************************************************************** //
1697 
1698 // Dependencies
1699 private:
1700 import tango.io.device.Conduit : Conduit;
1701 
1702 /*******************************************************************************
1703 
1704     copyright:  Copyright © 2007 Daniel Keep.  All rights reserved.
1705 
1706     license:    BSD style: $(LICENSE)
1707 
1708     version:    Prerelease
1709 
1710     author:     Daniel Keep
1711 
1712 *******************************************************************************/
1713 
1714 //module tangox.io.stream.DummyStream;
1715 
1716 
1717 
1718 //import tango.io.device.Conduit : Conduit;
1719 //import tango.io.model.IConduit : IConduit, InputStream, OutputStream;
1720 
1721 /**
1722  * The dummy stream classes are used to provide simple, empty stream objects
1723  * where one is required, but none is available.
1724  *
1725  * Note that, currently, these classes return 'null' for the underlying
1726  * conduit, which will likely break code which expects streams to have an
1727  * underlying conduit.
1728  */
1729 private deprecated class DummyInputStream : InputStream // IConduit.Seek
1730 {
1731     //alias IConduit.Seek.Anchor Anchor;
1732 
1733     override InputStream input() {return null;}
1734     override IConduit conduit() { return null; }
1735     override void close() {}
1736     override size_t read(void[] dst) { return IConduit.Eof; }
1737     override InputStream flush() { return this; }
1738     override void[] load(size_t max=-1)
1739     {
1740         return Conduit.load(this, max);
1741     }
1742     override long seek(long offset, Anchor anchor = cast(Anchor)0) { return 0; }
1743 }
1744 
1745 /// ditto
1746 private deprecated class DummyOutputStream : OutputStream //, IConduit.Seek
1747 {
1748     //alias IConduit.Seek.Anchor Anchor;
1749 
1750     override OutputStream output() {return null;}
1751     override IConduit conduit() { return null; }
1752     override void close() {}
1753     override size_t write(const(void)[] src) { return IConduit.Eof; }
1754     override OutputStream copy(InputStream src, size_t max=-1)
1755     {
1756         Conduit.transfer(src, this, max);
1757         return this;
1758     }
1759     override OutputStream flush() { return this; }
1760     override long seek(long offset, Anchor anchor = cast(Anchor)0) { return 0; }
1761 }
1762 
1763 /*******************************************************************************
1764 
1765     copyright:  Copyright © 2007 Daniel Keep.  All rights reserved.
1766 
1767     license:    BSD style: $(LICENSE)
1768 
1769     version:    Prerelease
1770 
1771     author:     Daniel Keep
1772 
1773 *******************************************************************************/
1774 
1775 //module tangox.io.stream.EventStream;
1776 
1777 //import tango.io.device.Conduit : Conduit;
1778 //import tango.io.model.IConduit : IConduit, InputStream, OutputStream;
1779 
1780 /**
1781  * The event stream classes are designed to allow you to receive feedback on
1782  * how a stream chain is being used.  This is done through the use of
1783  * delegate callbacks which are invoked just before the associated method is
1784  * complete.
1785  */
1786 class EventSeekInputStream : InputStream //, IConduit.Seek
1787 {
1788     ///
1789     struct Callbacks
1790     {
1791         void delegate()                     close; ///
1792         void delegate()                     clear; ///
1793         void delegate(size_t, void[])       read; ///
1794         void delegate(long, long, Anchor)   seek; ///
1795     }
1796 
1797     //alias IConduit.Seek.Anchor Anchor;
1798 
1799     ///
1800     this(InputStream source, Callbacks callbacks)
1801     in
1802     {
1803         assert( source !is null );
1804         assert( (cast(IConduit.Seek) source.conduit) !is null );
1805     }
1806     body
1807     {
1808         this.source = source;
1809         this.seeker = source; //cast(IConduit.Seek) source;
1810         this.callbacks = callbacks;
1811     }
1812 
1813     override IConduit conduit()
1814     {
1815         return source.conduit;
1816     }
1817 
1818     InputStream input()
1819     {
1820         return source;
1821     }
1822 
1823     override void close()
1824     {
1825         source.close();
1826         source = null;
1827         seeker = null;
1828         if( callbacks.close ) callbacks.close();
1829     }
1830 
1831     override size_t read(void[] dst)
1832     {
1833         auto result = source.read(dst);
1834         if( callbacks.read ) callbacks.read(result, dst);
1835         return result;
1836     }
1837 
1838     override InputStream flush()
1839     {
1840         source.flush();
1841         if( callbacks.clear ) callbacks.clear();
1842         return this;
1843     }
1844 
1845     override void[] load(size_t max=-1)
1846     {
1847         return Conduit.load(this, max);
1848     }
1849 
1850     override long seek(long offset, Anchor anchor = cast(Anchor)0)
1851     {
1852         auto result = seeker.seek(offset, anchor);
1853         if( callbacks.seek ) callbacks.seek(result, offset, anchor);
1854         return result;
1855     }
1856 
1857 private:
1858     InputStream source;
1859     InputStream seeker; //IConduit.Seek seeker;
1860     Callbacks callbacks;
1861 
1862     invariant()
1863     {
1864         assert( cast(Object) source is cast(Object) seeker );
1865     }
1866 }
1867 
1868 /// ditto
1869 class EventSeekOutputStream : OutputStream //, IConduit.Seek
1870 {
1871     ///
1872     struct Callbacks
1873     {
1874         void delegate()                      close; ///
1875         void delegate()                      flush; ///
1876         void delegate(size_t, const(void)[]) write; ///
1877         void delegate(long, long, Anchor)    seek; ///
1878     }
1879 
1880     //alias IConduit.Seek.Anchor Anchor;
1881 
1882     ///
1883     this(OutputStream source, Callbacks callbacks)
1884     in
1885     {
1886         assert( source !is null );
1887         assert( (cast(IConduit.Seek) source.conduit) !is null );
1888     }
1889     body
1890     {
1891         this.source = source;
1892         this.seeker = source; //cast(IConduit.Seek) source;
1893         this.callbacks = callbacks;
1894     }
1895 
1896     override IConduit conduit()
1897     {
1898         return source.conduit;
1899     }
1900 
1901     override OutputStream output()
1902     {
1903         return source;
1904     }
1905 
1906     override void close()
1907     {
1908         source.close();
1909         source = null;
1910         seeker = null;
1911         if( callbacks.close ) callbacks.close();
1912     }
1913 
1914     override size_t write(const(void)[] dst)
1915     {
1916         auto result = source.write(dst);
1917         if( callbacks.write ) callbacks.write(result, dst);
1918         return result;
1919     }
1920 
1921     override OutputStream flush()
1922     {
1923         source.flush();
1924         if( callbacks.flush ) callbacks.flush();
1925         return this;
1926     }
1927 
1928     override long seek(long offset, Anchor anchor = cast(Anchor)0)
1929     {
1930         auto result = seeker.seek(offset, anchor);
1931         if( callbacks.seek ) callbacks.seek(result, offset, anchor);
1932         return result;
1933     }
1934 
1935     override OutputStream copy(InputStream src, size_t max=-1)
1936     {
1937         Conduit.transfer(src, this, max);
1938         return this;
1939     }
1940 
1941 private:
1942     OutputStream source;
1943     OutputStream seeker; //IConduit.Seek seeker;
1944     Callbacks callbacks;
1945 
1946     invariant()
1947     {
1948         assert( cast(Object) source is cast(Object) seeker );
1949     }
1950 }
1951 
1952 /*******************************************************************************
1953 
1954     copyright:  Copyright © 2007 Daniel Keep.  All rights reserved.
1955 
1956     license:    BSD style: $(LICENSE)
1957 
1958     version:    Prerelease
1959 
1960     author:     Daniel Keep
1961 *******************************************************************************/
1962 
1963 
1964 //module tangox.io.stream.WrapStream;
1965 
1966 
1967 
1968 //import tango.io.device.Conduit : Conduit;
1969 //import tango.io.model.IConduit : IConduit, InputStream, OutputStream;
1970 
1971 /**
1972  * This stream can be used to provide access to another stream.
1973  * Its distinguishing feature is that users cannot.close() the underlying
1974  * stream.
1975  *
1976  * This stream fully supports seeking, and as such requires that the
1977  * underlying stream also support seeking.
1978  */
1979 class WrapSeekInputStream : InputStream //, IConduit.Seek
1980 {
1981     //alias IConduit.Seek.Anchor Anchor;
1982 
1983     /**
1984      * Create a new wrap stream from the given source.
1985      */
1986     this(InputStream source)
1987     in
1988     {
1989         assert( source !is null );
1990         assert( (cast(IConduit.Seek) source.conduit) !is null );
1991     }
1992     body
1993     {
1994         this.source = source;
1995         this.seeker = source; //cast(IConduit.Seek) source;
1996         this._position = seeker.seek(0, Anchor.Current);
1997     }
1998 
1999     /// ditto
2000     this(InputStream source, long position)
2001     in
2002     {
2003         assert( position >= 0 );
2004     }
2005     body
2006     {
2007         this(source);
2008         this._position = position;
2009     }
2010 
2011     override IConduit conduit()
2012     {
2013         return source.conduit;
2014     }
2015 
2016     InputStream input()
2017     {
2018         return source;
2019     }
2020 
2021     override void close()
2022     {
2023         source = null;
2024         seeker = null;
2025     }
2026 
2027     override size_t read(void[] dst)
2028     {
2029         if( seeker.seek(0, Anchor.Current) != _position )
2030             seeker.seek(_position, Anchor.Begin);
2031 
2032         auto read = source.read(dst);
2033         if( read != IConduit.Eof )
2034             _position += read;
2035 
2036         return read;
2037     }
2038 
2039     override InputStream flush()
2040     {
2041         source.flush();
2042         return this;
2043     }
2044 
2045     override void[] load(size_t max=-1)
2046     {
2047         return Conduit.load(this, max);
2048     }
2049 
2050     override long seek(long offset, Anchor anchor = cast(Anchor)0)
2051     {
2052         seeker.seek(_position, Anchor.Begin);
2053         return (_position = seeker.seek(offset, anchor));
2054     }
2055 
2056 private:
2057     InputStream source;
2058     InputStream seeker; //IConduit.Seek seeker;
2059     long _position;
2060 
2061     invariant()
2062     {
2063         assert( cast(Object) source is cast(Object) seeker );
2064         assert( _position >= 0 );
2065     }
2066 }
2067 
2068 /**
2069  * This stream can be used to provide access to another stream.
2070  * Its distinguishing feature is that the users cannot.close() the underlying
2071  * stream.
2072  *
2073  * This stream fully supports seeking, and as such requires that the
2074  * underlying stream also support seeking.
2075  */
2076 class WrapSeekOutputStream : OutputStream//, IConduit.Seek
2077 {
2078     //alias IConduit.Seek.Anchor Anchor;
2079 
2080     /**
2081      * Create a new wrap stream from the given source.
2082      */
2083     this(OutputStream source)
2084     in
2085     {
2086         assert( (cast(IConduit.Seek) source.conduit) !is null );
2087     }
2088     body
2089     {
2090         this.source = source;
2091         this.seeker = source; //cast(IConduit.Seek) source;
2092         this._position = seeker.seek(0, Anchor.Current);
2093     }
2094 
2095     /// ditto
2096     this(OutputStream source, long position)
2097     in
2098     {
2099         assert( position >= 0 );
2100     }
2101     body
2102     {
2103         this(source);
2104         this._position = position;
2105     }
2106 
2107     override IConduit conduit()
2108     {
2109         return source.conduit;
2110     }
2111 
2112     override OutputStream output()
2113     {
2114         return source;
2115     }
2116 
2117     override void close()
2118     {
2119         source = null;
2120         seeker = null;
2121     }
2122 
2123     size_t write(const(void)[] src)
2124     {
2125         if( seeker.seek(0, Anchor.Current) != _position )
2126             seeker.seek(_position, Anchor.Begin);
2127 
2128         auto wrote = source.write(src);
2129         if( wrote != IConduit.Eof )
2130             _position += wrote;
2131         return wrote;
2132     }
2133 
2134     override OutputStream copy(InputStream src, size_t max=-1)
2135     {
2136         Conduit.transfer(src, this, max);
2137         return this;
2138     }
2139 
2140     override OutputStream flush()
2141     {
2142         source.flush();
2143         return this;
2144     }
2145 
2146     override long seek(long offset, Anchor anchor = cast(Anchor)0)
2147     {
2148         seeker.seek(_position, Anchor.Begin);
2149         return (_position = seeker.seek(offset, anchor));
2150     }
2151 
2152 private:
2153     OutputStream source;
2154     OutputStream seeker; //IConduit.Seek seeker;
2155     long _position;
2156 
2157     invariant()
2158     {
2159         assert( cast(Object) source is cast(Object) seeker );
2160         assert( _position >= 0 );
2161     }
2162 }
2163 
2164