| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164 | /******************************************************************************* copyright: Copyright © 2007 Daniel Keep. All rights reserved. license: BSD style: $(LICENSE) version: The Great Namechange: February 2008$(BR) Initial release: December 2007 author: Daniel Keep *******************************************************************************/ module tango.io.vfs.ZipFolder; import Path = tango.io.Path; import tango.io.device.File : File; import tango.io.FilePath : FilePath; import tango.io.device.TempFile : TempFile; import tango.util.compress.Zip : ZipReader, ZipBlockReader, ZipWriter, ZipBlockWriter, ZipEntry, ZipEntryInfo, Method; import tango.io.model.IConduit : IConduit, InputStream, OutputStream; import tango.io.vfs.model.Vfs : VfsFolder, VfsFolderEntry, VfsFile, VfsFolders, VfsFiles, VfsFilter, VfsStats, VfsFilterInfo, VfsInfo, VfsSync; import tango.time.Time : Time; debug( ZipFolder ) { import tango.io.Stdout : Stderr; } // This disables code that is causing heap corruption in Tango 0.99.3 version = Bug_HeapCorruption; // ************************************************************************ // // ************************************************************************ // private { enum EntryType { Dir, File } /* * Entries are what make up the internal tree that describes the * filesystem of the archive. Each Entry is either a directory or a file. */ struct Entry { EntryType type; union { DirEntry dir; FileEntry file; } const(char)[] fullname; const(char)[] name; /+ invariant() { assert( (type == EntryType.Dir) || (type == EntryType.File) ); assert( fullname.nz() ); assert( name.nz() ); } +/ VfsFilterInfo vfsFilterInfo; VfsInfo vfsInfo() { return &vfsFilterInfo; } /* * Updates the VfsInfo structure for this entry. */ void makeVfsInfo() { with( vfsFilterInfo ) { // Cheat horribly here name = this.name; path = this.fullname[0..($-name.length+"/".length)]; folder = isDir; bytes = folder ? 0 : fileSize; } } @property bool isDir() { return (type == EntryType.Dir); } @property bool isFile() { return (type == EntryType.File); } @property ulong fileSize() in { assert( type == EntryType.File ); } body { if( file.zipEntry !is null ) return file.zipEntry.size(); else if( file.tempFile !is null ) { assert( file.tempFile.length >= 0 ); return cast(ulong) file.tempFile.length; } else return 0; } /* * Opens a File Entry for reading. * * BUG: Currently, if a user opens a new or unmodified file for input, * and then opens it for output, the two streams will be working with * different underlying conduits. This means that the result of * openInput should probably be wrapped in some kind of switching * stream that can update when the backing store for the file changes. */ InputStream openInput() in { assert( type == EntryType.File ); } body { if( file.zipEntry !is null ) { file.zipEntry.verify(); return file.zipEntry.open(); } else if( file.tempFile !is null ) return new WrapSeekInputStream(file.tempFile, 0); else { throw new Exception ("cannot open input stream for '"~fullname.idup~"'"); //return new DummyInputStream; } } /* * Opens a file entry for output. */ OutputStream openOutput() in { assert( type == EntryType.File ); } body { if( file.tempFile !is null ) return new WrapSeekOutputStream(file.tempFile); else { // Ok; we need to make a temporary file to store output in. // If we already have a zip entry, we need to dump that into // the temp. file and remove the zipEntry. if( file.zipEntry !is null ) { { auto zi = file.zipEntry.open(); scope(exit) zi.close(); file.tempFile = new TempFile; file.tempFile.copy(zi).close(); debug( ZipFolder ) Stderr.formatln("Entry.openOutput: duplicated" " temp file {} for {}", file.tempFile, this.fullname); } // TODO: Copy file info if available file.zipEntry = null; } else { // Otherwise, just make a new, blank temp file file.tempFile = new TempFile; debug( ZipFolder ) Stderr.formatln("Entry.openOutput: created" " temp file {} for {}", file.tempFile, this.fullname); } assert( file.tempFile !is null ); return openOutput(); } } void dispose() { fullname = name = null; with( vfsFilterInfo ) { name = path = null; } dispose_children(); } void dispose_children() { switch( type ) { case EntryType.Dir: auto keys = dir.children.keys; scope(exit) delete keys; foreach( k ; keys ) { auto child = dir.children[k]; child.dispose(); dir.children.remove(k); delete child; } dir.children = dir.children.init; break; case EntryType.File: if( file.zipEntry !is null ) { // Don't really need to do anything here file.zipEntry = null; } else if( file.tempFile !is null ) { // Detatch to destroy the physical file itself file.tempFile.detach(); file.tempFile = null; } break; default: debug( ZipFolder ) Stderr.formatln( "Entry.dispose_children: unknown type {}", type); assert(false); } } } struct DirEntry { Entry*[const(char)[]] children; } struct FileEntry { ZipEntry zipEntry; TempFile tempFile; invariant() { auto zn = zipEntry is null; auto tn = tempFile is null; assert( (zn && tn) /* zn xor tn */ || (!(zn&&tn)&&(zn||tn)) ); } } } // ************************************************************************ // // ************************************************************************ // /** * This class represents a folder in an archive. In addition to supporting * the sync operation, you can also use the archive member to get a reference * to the underlying ZipFolder instance. */ class ZipSubFolder : VfsFolder, VfsSync { /// @property final const(char)[] name() in { assert( valid ); } body { return entry.name; } /// final override string toString() in { assert( valid ); } body { return entry.fullname.idup; } /// @property final VfsFile file(const(char)[] path) in { assert( valid ); assert( !Path.parse(path).isAbsolute ); } body { auto fp = Path.parse(path); auto dir = fp.path; auto name = fp.file; if (dir.length > 0 && '/' == dir[$-1]) { dir = dir[0..$-1]; } // If the file is in another directory, then we need to look up that // up first. if( dir.nz() ) { auto dir_ent = this.folder(dir); auto dir_obj = dir_ent.open(); return dir_obj.file(name); } else { // Otherwise, we need to check and see whether the file is in our // entry list. if( auto file_entry = (name in this.entry.dir.children) ) { // It is; create a new object for it. return new ZipFile(archive, this.entry, *file_entry); } else { // Oh dear... return a holding object. return new ZipFile(archive, this.entry, name); } } } /// @property final VfsFolderEntry folder(const(char)[] path) in { assert( valid ); assert( !Path.parse(path).isAbsolute ); } body { // Locate the folder in question. We do this by "walking" the // path components. If we find a component that doesn't exist, // then we create a ZipSubFolderEntry for the remainder. Entry* curent = this.entry; // h is the "head" of the path, t is the remainder. ht is both // joined together. const(char)[] h,t,ht; ht = path; do { // Split ht at the first path separator. assert( ht.nz() ); headTail(ht,h,t); // Look for a pre-existing subentry auto subent = (h in curent.dir.children); if( t.nz() && !!subent ) { // Move to the subentry, and split the tail on the next // iteration. curent = *subent; ht = t; } else // If the next component doesn't exist, return a folder entry. // If the tail is empty, return a folder entry as well (let // the ZipSubFolderEntry do the last lookup.) return new ZipSubFolderEntry(archive, curent, ht); } while( true ); //assert(false); } /// @property final VfsFolders self() in { assert( valid ); } body { return new ZipSubFolderGroup(archive, this, false); } /// @property final VfsFolders tree() in { assert( valid ); } body { return new ZipSubFolderGroup(archive, this, true); } /// final int opApply(scope int delegate(ref VfsFolder) dg) in { assert( valid ); } body { int result = 0; foreach( _,childEntry ; this.entry.dir.children ) { if( childEntry.isDir ) { VfsFolder childFolder = new ZipSubFolder(archive, childEntry); if( (result = dg(childFolder)) != 0 ) break; } } return result; } /// final VfsFolder clear() in { assert( valid ); } body { version( ZipFolder_NonMutating ) { mutate_error("VfsFolder.clear"); assert(false); } else { // MUTATE enforce_mutable(); // Disposing of the underlying entry subtree should do our job for us. entry.dispose_children(); mutate(); return this; } } /// @property final bool writable() in { assert( valid ); } body { return !archive.readonly; } /** * Closes this folder object. If commit is true, then the folder is * sync'ed before being closed. */ VfsFolder close(bool commit = true) in { assert( valid ); } body { // MUTATE if( commit ) sync(); // Just clean up our pointers archive = null; entry = null; return this; } /** * This will flush any changes to the archive to disk. Note that this * applies to the entire archive, not just this folder and its contents. */ override VfsFolder sync() { assert( valid ); // MUTATE archive.sync(); return this; } /// final void verify(VfsFolder folder, bool mounting) in { assert( valid ); } body { auto zipfolder = cast(ZipSubFolder) folder; if( mounting && zipfolder !is null && zipfolder.archive is archive ) { auto src = this.toString(); auto dst = zipfolder.toString(); auto len = src.length > dst.length ? dst.length : src.length; if( src[0..len] == dst[0..len] ) error(`folders "`~dst~`" and "`~src~`" in archive "` ~archive.path~`" overlap`); } } /** * Returns a reference to the underlying ZipFolder instance. */ @property final ZipFolder archive() { return _archive; } private: ZipFolder _archive; Entry* entry; VfsStats stats; @property final ZipFolder archive(ZipFolder v) { return _archive = v; } this(ZipFolder archive, Entry* entry) { this.reset(archive, entry); } final void reset(ZipFolder archive, Entry* entry) in { assert( archive !is null ); assert( entry.isDir ); } out { assert( valid ); } body { this.archive = archive; this.entry = entry; } @property final bool valid() { return( (archive !is null) && !archive.closed ); } final void enforce_mutable() in { assert( valid ); } body { if( archive.readonly ) // TODO: exception throw new Exception("cannot mutate a read-only Zip archive"); } final void mutate() in { assert( valid ); } body { enforce_mutable(); archive.modified = true; } final ZipSubFolder[] folders(bool collect) in { assert( valid ); } body { ZipSubFolder[] folders; stats = stats.init; foreach( _,childEntry ; entry.dir.children ) { if( childEntry.isDir ) { if( collect ) folders ~= new ZipSubFolder(archive, childEntry); ++ stats.folders; } else { assert( childEntry.isFile ); stats.bytes += childEntry.fileSize; ++ stats.files; } } return folders; } final Entry*[] files(ref VfsStats stats, VfsFilter filter = null) in { assert( valid ); } body { Entry*[] files; foreach( _,childEntry ; entry.dir.children ) { if( childEntry.isFile ) if( filter is null || filter(childEntry.vfsInfo()) ) { files ~= childEntry; stats.bytes += childEntry.fileSize; ++stats.files; } } return files; } } // ************************************************************************ // // ************************************************************************ // /** * ZipFolder serves as the root object for all Zip archives in the VFS. * Presently, it can only open archives on the local filesystem. */ class ZipFolder : ZipSubFolder { /** * Opens an archive from the local filesystem. If the readonly argument * is specified as true, then modification of the archive will be * explicitly disallowed. */ this(const(char)[] path, bool readonly=false) out { assert( valid ); } body { debug( ZipFolder ) Stderr.formatln(`ZipFolder("{}", {})`, path, readonly); this.resetArchive(path, readonly); super(this, root); } /** * Closes the archive, and releases all internal resources. If the commit * argument is true (the default), then changes to the archive will be * flushed out to disk. If false, changes will simply be discarded. */ final override VfsFolder close(bool commit = true) { assert (valid); debug( ZipFolder ) Stderr.formatln("ZipFolder.close({})",commit); // MUTATE if( commit ) sync(); // Close ZipReader if( zr !is null ) { zr.close(); delete zr; } // Destroy entries root.dispose(); version( Bug_HeapCorruption ) root = null; else delete root; return this; } /** * Flushes all changes to the archive out to disk. */ final override VfsFolder sync() out { assert( valid ); assert( !modified ); } body { assert( valid ); debug( ZipFolder ) Stderr("ZipFolder.sync()").newline; if( !modified ) return this; version( ZipFolder_NonMutating ) { mutate_error("ZipFolder.sync"); assert(false); } else { enforce_mutable(); // First, we need to determine if we have any zip entries. If we // don't, then we can write directly to the path. If there *are* // zip entries, then we'll need to write to a temporary path instead. OutputStream os; TempFile tempFile; scope(exit) if( tempFile !is null ) delete tempFile; auto p = Path.parse (path); foreach( file ; this.tree.catalog ) { if( auto zf = cast(ZipFile) file ) if( zf.entry.file.zipEntry !is null ) { tempFile = new TempFile(p.path, TempFile.Permanent); os = tempFile; debug( ZipFolder ) Stderr.formatln(" sync: created temp file {}", tempFile.path); break; } } if( tempFile is null ) { // Kill the current zip reader so we can re-open the file it's // using. if( zr !is null ) { zr.close(); delete zr; } os = new File(path, File.WriteCreate); } // Now, we can create the archive. { scope zw = new ZipBlockWriter(os); foreach( file ; this.tree.catalog ) { auto zei = ZipEntryInfo(file.toString()[1..$]); // BUG: Passthru doesn't maintain compression for some // reason... if( auto zf = cast(ZipFile) file ) { if( zf.entry.file.zipEntry !is null ) zw.putEntry(zei, zf.entry.file.zipEntry); else zw.putStream(zei, file.input); } else zw.putStream(zei, file.input); } zw.finish(); } // With that done, we can free all our handles, etc. debug( ZipFolder ) Stderr(" sync: close").newline; this.close(/*commit*/ false); os.close(); // If we wrote the archive into a temporary file, move that over the // top of the old archive. if( tempFile !is null ) { debug( ZipFolder ) Stderr(" sync: destroying temp file").newline; debug( ZipFolder ) Stderr.formatln(" sync: renaming {} to {}", tempFile, path); Path.rename (tempFile.toString(), path); } // Finally, re-open the archive so that we have all the nicely // compressed files. debug( ZipFolder ) Stderr(" sync: reset archive").newline; this.resetArchive(path, readonly); debug( ZipFolder ) Stderr(" sync: reset folder").newline; this.reset(this, root); debug( ZipFolder ) Stderr(" sync: done").newline; return this; } } /** * Indicates whether the archive was opened for read-only access. Note * that in addition to the readonly constructor flag, this is also * influenced by whether the file itself is read-only or not. */ @property final bool readonly() { return _readonly; } /** * Allows you to read and specify the path to the archive. The effect of * setting this is to change where the archive will be written to when * flushed to disk. */ @property final const(char)[] path() { return _path; } @property final const(char)[] path(const(char)[] v) { return _path = v; } /// ditto private: ZipReader zr; Entry* root; const(char)[] _path; bool _readonly; bool modified = false; @property final bool readonly(bool v) { return _readonly = v; } @property final bool closed() { debug( ZipFolder ) Stderr("ZipFolder.closed()").newline; return (root is null); } @property final bool valid() { debug( ZipFolder ) Stderr("ZipFolder.valid()").newline; return !closed; } final OutputStream mutateStream(OutputStream source) { return new EventSeekOutputStream(source, EventSeekOutputStream.Callbacks( null, null, &mutate_write, null)); } void mutate_write(size_t bytes, const(void)[] src) { if( !(bytes == 0 || bytes == IConduit.Eof) ) this.modified = true; } void resetArchive(const(char)[] path, bool readonly=false) out { assert( valid ); } body { debug( ZipFolder ) Stderr.formatln(`ZipFolder.resetArchive("{}", {})`, path, readonly); debug( ZipFolder ) Stderr.formatln(" .. size of Entry: {0}, {0:x} bytes", Entry.sizeof); this.path = path; this.readonly = readonly; // Make sure the modified flag is set appropriately scope(exit) modified = false; // First, create a root entry root = new Entry; root.type = EntryType.Dir; root.fullname = root.name = "/"; // If the user allowed writing, also allow creating a new archive. // Note that we MUST drop out here if the archive DOES NOT exist, // since Path.isWriteable will throw an exception if called on a // non-existent path. if( !this.readonly && !Path.exists(path) ) return; // Update readonly to reflect the write-protected status of the // archive. this.readonly = this.readonly || !Path.isWritable(path); zr = new ZipBlockReader(path); // Parse the contents of the archive foreach( zipEntry ; zr ) { // Normalise name auto name = FilePath(zipEntry.info.name.dup).standard.toString(); // If the last character is '/', treat as a directory and skip // TODO: is there a better way of detecting this? if( name[$-1] == '/' ) continue; // Now, we need to locate the right spot to insert this entry. { // That's CURrent ENTity, not current OR currant... Entry* curent = root; const(char)[] h,t; headTail(name,h,t); while( t.nz() ) { assert( curent.isDir ); if( auto nextent = (h in curent.dir.children) ) curent = *nextent; else { // Create new directory entry Entry* dirent = new Entry; dirent.type = EntryType.Dir; if( curent.fullname != "/" ) dirent.fullname = curent.fullname ~ "/" ~ h; else dirent.fullname = "/" ~ h; dirent.name = dirent.fullname[$-h.length..$]; // Insert into current entry curent.dir.children[dirent.name] = dirent; // Make it the new current entry curent = dirent; } headTail(t,h,t); } // Getting here means that t is empty, which means the final // component of the path--the file name--is in h. The entry // of the containing directory is in curent. // Make sure the file isn't already there (you never know!) assert( !(h in curent.dir.children) ); // Create a new file entry for it. { // BUG: Bug_HeapCorruption // with ZipTest, on the resetArchive operation, on // the second time through this next line, it erroneously // allocates filent 16 bytes lower than curent. Entry // is *way* larger than 16 bytes, and this causes it to // zero-out the existing root element, which leads to // segfaults later on at line +12: // // // Insert // curent.dir.children[filent.name] = filent; Entry* filent = new Entry; filent.type = EntryType.File; if( curent.fullname != "/" ) filent.fullname = curent.fullname ~ "/" ~ h; else filent.fullname = "/" ~ h; filent.name = filent.fullname[$-h.length..$]; filent.file.zipEntry = zipEntry.dup(); filent.makeVfsInfo(); // Insert curent.dir.children[filent.name] = filent; } } } } } // ************************************************************************ // // ************************************************************************ // /** * This class represents a file within an archive. */ class ZipFile : VfsFile { /// @property final const(char)[] name() in { assert( valid ); } body { if( entry ) return entry.name; else return name_; } /// final override string toString() in { assert( valid ); } body { if( entry ) return entry.fullname.idup; else return (parent.fullname ~ "/" ~ name_).idup; } /// @property final bool exists() in { assert( valid ); } body { // If we've only got a parent and a name, this means we don't actually // exist; EXISTENTIAL CRISIS TEIM!!! return !!entry; } /// @property final ulong size() in { assert( valid ); } body { if( exists ) return entry.fileSize; else error("ZipFile.size: cannot reliably determine size of a " "non-existent file"); assert(false); } /// final VfsFile copy(VfsFile source) in { assert( valid ); } body { version( ZipFolder_NonMutating ) { mutate_error("ZipFile.copy"); assert(false); } else { // MUTATE enforce_mutable(); if( !exists ) this.create(); this.output.copy(source.input); return this; } } /// final VfsFile move(VfsFile source) in { assert( valid ); } body { version( ZipFolder_NonMutating ) { mutate_error("ZipFile.move"); assert(false); } else { // MUTATE enforce_mutable(); this.copy(source); source.remove(); return this; } } /// final VfsFile create() in { assert( valid ); } out { assert( valid ); } body { version( ZipFolder_NonMutating ) { mutate_error("ZipFile.create"); assert(false); } else { if( exists ) error("ZipFile.create: cannot create already existing file: " "this folder ain't big enough for the both of 'em"); // MUTATE enforce_mutable(); auto entry = new Entry; entry.type = EntryType.File; entry.fullname = parent.fullname.dir_app(name); entry.name = entry.fullname[$-name.length..$]; entry.makeVfsInfo(); assert( !(entry.name in parent.dir.children) ); parent.dir.children[entry.name] = entry; this.reset(archive, parent, entry); mutate(); // Done return this; } } /// final VfsFile create(InputStream stream) in { assert( valid ); } body { version( ZipFolder_NonMutating ) { mutate_error("ZipFile.create"); assert(false); } else { create(); output.copy(stream).close(); return this; } } /// final VfsFile remove() in{ assert( valid ); } out { assert( valid ); } body { version( ZipFolder_NonMutating ) { mutate_error("ZipFile.remove"); assert(false); } else { if( !exists ) error("ZipFile.remove: cannot remove non-existent file; " "rather redundant, really"); // MUTATE enforce_mutable(); // Save the old name auto old_name = name; // Do the removal assert( !!(name in parent.dir.children) ); parent.dir.children.remove(name); entry.dispose(); entry = null; mutate(); // Swap out our now empty entry for the name, so the file can be // directly recreated. this.reset(archive, parent, old_name); return this; } } /// @property final InputStream input() in { assert( valid ); } body { if( exists ) return entry.openInput(); else error("ZipFile.input: cannot open non-existent file for input; " "results would not be very useful"); assert(false); } /// @property final OutputStream output() in { assert( valid ); } body { version( ZipFolder_NonMutable ) { mutate_error("ZipFile.output"); assert(false); } else { // MUTATE enforce_mutable(); // Don't call mutate(); defer that until the user actually writes to or // modifies the underlying stream. return archive.mutateStream(entry.openOutput()); } } /// @property final VfsFile dup() in { assert( valid ); } body { if( entry ) return new ZipFile(archive, parent, entry); else return new ZipFile(archive, parent, name); } /// @property final Time modified() { return entry.file.zipEntry.info.modified; } private: ZipFolder archive; Entry* entry; Entry* parent; const(char)[] name_; this() out { assert( !valid ); } body { } this(ZipFolder archive, Entry* parent, Entry* entry) in { assert( archive !is null ); assert( parent ); assert( parent.isDir ); assert( entry ); assert( entry.isFile ); assert( parent.dir.children[entry.name] is entry ); } out { assert( valid ); } body { this.reset(archive, parent, entry); } this(ZipFolder archive, Entry* parent, const(char)[] name) in { assert( archive !is null ); assert( parent ); assert( parent.isDir ); assert( name.nz() ); assert( !(name in parent.dir.children) ); } out { assert( valid ); } body { this.reset(archive, parent, name); } @property final bool valid() { return( (archive !is null) && !archive.closed ); } final void enforce_mutable() in { assert( valid ); } body { if( archive.readonly ) // TODO: exception throw new Exception("cannot mutate a read-only Zip archive"); } final void mutate() in { assert( valid ); } body { enforce_mutable(); archive.modified = true; } final void reset(ZipFolder archive, Entry* parent, Entry* entry) in { assert( archive !is null ); assert( parent ); assert( parent.isDir ); assert( entry ); assert( entry.isFile ); assert( parent.dir.children[entry.name] is entry ); } out { assert( valid ); } body { this.parent = parent; this.archive = archive; this.entry = entry; this.name_ = null; } final void reset(ZipFolder archive, Entry* parent, const(char)[] name) in { assert( archive !is null ); assert( parent ); assert( parent.isDir ); assert( name.nz() ); assert( !(name in parent.dir.children) ); } out { assert( valid ); } body { this.archive = archive; this.parent = parent; this.entry = null; this.name_ = name; } final void close() in { assert( valid ); } out { assert( !valid ); } body { archive = null; parent = null; entry = null; name_ = null; } } // ************************************************************************ // // ************************************************************************ // class ZipSubFolderEntry : VfsFolderEntry { final VfsFolder open() in { assert( valid ); } body { auto entry = (name in parent.dir.children); if( entry ) return new ZipSubFolder(archive, *entry); else { // NOTE: this can be called with a multi-part path. error("ZipSubFolderEntry.open: \"" ~ parent.fullname ~ "/" ~ name ~ "\" does not exist"); assert(false); } } final VfsFolder create() in { assert( valid ); } body { version( ZipFolder_NonMutating ) { // TODO: different exception if folder exists (this operation is // currently invalid either way...) mutate_error("ZipSubFolderEntry.create"); assert(false); } else { // MUTATE enforce_mutable(); // If the folder exists, we can't really create it, now can we? if( this.exists ) error("ZipSubFolderEntry.create: cannot create folder that already " "exists, and believe me, I *tried*"); // Ok, I suppose I can do this for ya... auto entry = new Entry; entry.type = EntryType.Dir; entry.fullname = parent.fullname.dir_app(name); entry.name = entry.fullname[$-name.length..$]; entry.makeVfsInfo(); assert( !(entry.name in parent.dir.children) ); parent.dir.children[entry.name] = entry; mutate(); // Done return new ZipSubFolder(archive, entry); } } @property final bool exists() in { assert( valid ); } body { return !!(name in parent.dir.children); } private: ZipFolder archive; Entry* parent; const(char)[] name; this(ZipFolder archive, Entry* parent, const(char)[] name) in { assert( archive !is null ); assert( parent.isDir ); assert( name.nz() ); assert( name.single_path_part() ); } out { assert( valid ); } body { this.archive = archive; this.parent = parent; this.name = name; } @property final bool valid() { return (archive !is null) && !archive.closed; } final void enforce_mutable() in { assert( valid ); } body { if( archive.readonly ) // TODO: exception throw new Exception("cannot mutate a read-only Zip archive"); } final void mutate() in { assert( valid ); } body { enforce_mutable(); archive.modified = true; } } // ************************************************************************ // // ************************************************************************ // class ZipSubFolderGroup : VfsFolders { final int opApply(scope int delegate(ref VfsFolder) dg) in { assert( valid ); } body { int result = 0; foreach( folder ; members ) { VfsFolder x = folder; if( (result = dg(x)) != 0 ) break; } return result; } @property final size_t files() in { assert( valid ); } body { uint files = 0; foreach( folder ; members ) files += folder.stats.files; return files; } @property final size_t folders() in { assert( valid ); } body { return members.length; } @property final size_t entries() in { assert( valid ); } body { return files + folders; } @property final ulong bytes() in { assert( valid ); } body { ulong bytes = 0; foreach( folder ; members ) bytes += folder.stats.bytes; return bytes; } final VfsFolders subset(const(char)[] pattern) in { assert( valid ); } body { ZipSubFolder[] set; foreach( folder ; members ) if( Path.patternMatch(folder.name, pattern) ) set ~= folder; return new ZipSubFolderGroup(archive, set); } @property final VfsFiles catalog(const(char)[] pattern) in { assert( valid ); } body { bool filter (VfsInfo info) { return Path.patternMatch(info.name, pattern); } return catalog (&filter); } @property final VfsFiles catalog(VfsFilter filter = null) in { assert( valid ); } body { return new ZipFileGroup(archive, this, filter); } private: ZipFolder archive; ZipSubFolder[] members; this(ZipFolder archive, ZipSubFolder root, bool recurse) out { assert( valid ); } body { this.archive = archive; members = root ~ scan(root, recurse); } this(ZipFolder archive, ZipSubFolder[] members) out { assert( valid ); } body { this.archive = archive; this.members = members; } @property final bool valid() { return (archive !is null) && !archive.closed; } final ZipSubFolder[] scan(ZipSubFolder root, bool recurse) in { assert( valid ); } body { auto folders = root.folders(recurse); if( recurse ) foreach( child ; folders ) folders ~= scan(child, recurse); return folders; } } // ************************************************************************ // // ************************************************************************ // class ZipFileGroup : VfsFiles { final int opApply(scope int delegate(ref VfsFile) dg) in { assert( valid ); } body { int result = 0; auto file = new ZipFile; foreach( entry ; group ) { file.reset(archive,entry.parent,entry.entry); VfsFile x = file; if( (result = dg(x)) != 0 ) break; } return result; } @property final size_t files() in { assert( valid ); } body { return group.length; } @property final ulong bytes() in { assert( valid ); } body { return stats.bytes; } private: ZipFolder archive; FileEntry[] group; VfsStats stats; struct FileEntry { Entry* parent; Entry* entry; } this(ZipFolder archive, ZipSubFolderGroup host, VfsFilter filter) out { assert( valid ); } body { this.archive = archive; foreach( folder ; host.members ) foreach( file ; folder.files(stats, filter) ) group ~= FileEntry(folder.entry, file); } @property final bool valid() { return (archive !is null) && !archive.closed; } } // ************************************************************************ // // ************************************************************************ // private: void error(const(char)[] msg) { throw new Exception(msg.idup); } void mutate_error(const(char)[] method) { error(method ~ ": mutating the contents of a ZipFolder " "is not supported yet; terribly sorry"); } bool nz(const(char)[] s) { return s.length > 0; } bool zero(const(char)[] s) { return s.length == 0; } bool single_path_part(const(char)[] s) { foreach( c ; s ) if( c == '/' ) return false; return true; } char[] dir_app(const(char)[] dir, const(char)[] name) { return dir ~ (dir[$-1]!='/' ? "/" : "") ~ name; } inout(char)[] headTail(inout(char)[] path, out inout(char)[] head, out inout(char)[] tail) { foreach( i,dchar c ; path[1..$] ) if( c == '/' ) { head = path[0..i+1]; tail = path[i+2..$]; return head; } head = path; tail = null; return head; } debug (UnitTest) { unittest { const(char)[] h,t; headTail("/a/b/c", h, t); assert( h == "/a" ); assert( t == "b/c" ); headTail("a/b/c", h, t); assert( h == "a" ); assert( t == "b/c" ); headTail("a/", h, t); assert( h == "a" ); assert( t == "" ); headTail("a", h, t); assert( h == "a" ); assert( t == "" ); } } // ************************************************************************** // // ************************************************************************** // // ************************************************************************** // // Dependencies private: import tango.io.device.Conduit : Conduit; /******************************************************************************* copyright: Copyright © 2007 Daniel Keep. All rights reserved. license: BSD style: $(LICENSE) version: Prerelease author: Daniel Keep *******************************************************************************/ //module tangox.io.stream.DummyStream; //import tango.io.device.Conduit : Conduit; //import tango.io.model.IConduit : IConduit, InputStream, OutputStream; /** * The dummy stream classes are used to provide simple, empty stream objects * where one is required, but none is available. * * Note that, currently, these classes return 'null' for the underlying * conduit, which will likely break code which expects streams to have an * underlying conduit. */ private deprecated class DummyInputStream : InputStream // IConduit.Seek { //alias IConduit.Seek.Anchor Anchor; override InputStream input() {return null;} override IConduit conduit() { return null; } override void close() {} override size_t read(void[] dst) { return IConduit.Eof; } override InputStream flush() { return this; } override void[] load(size_t max=-1) { return Conduit.load(this, max); } override long seek(long offset, Anchor anchor = cast(Anchor)0) { return 0; } } /// ditto private deprecated class DummyOutputStream : OutputStream //, IConduit.Seek { //alias IConduit.Seek.Anchor Anchor; override OutputStream output() {return null;} override IConduit conduit() { return null; } override void close() {} override size_t write(const(void)[] src) { return IConduit.Eof; } override OutputStream copy(InputStream src, size_t max=-1) { Conduit.transfer(src, this, max); return this; } override OutputStream flush() { return this; } override long seek(long offset, Anchor anchor = cast(Anchor)0) { return 0; } } /******************************************************************************* copyright: Copyright © 2007 Daniel Keep. All rights reserved. license: BSD style: $(LICENSE) version: Prerelease author: Daniel Keep *******************************************************************************/ //module tangox.io.stream.EventStream; //import tango.io.device.Conduit : Conduit; //import tango.io.model.IConduit : IConduit, InputStream, OutputStream; /** * The event stream classes are designed to allow you to receive feedback on * how a stream chain is being used. This is done through the use of * delegate callbacks which are invoked just before the associated method is * complete. */ class EventSeekInputStream : InputStream //, IConduit.Seek { /// struct Callbacks { void delegate() close; /// void delegate() clear; /// void delegate(size_t, void[]) read; /// void delegate(long, long, Anchor) seek; /// } //alias IConduit.Seek.Anchor Anchor; /// this(InputStream source, Callbacks callbacks) in { assert( source !is null ); assert( (cast(IConduit.Seek) source.conduit) !is null ); } body { this.source = source; this.seeker = source; //cast(IConduit.Seek) source; this.callbacks = callbacks; } override IConduit conduit() { return source.conduit; } InputStream input() { return source; } override void close() { source.close(); source = null; seeker = null; if( callbacks.close ) callbacks.close(); } override size_t read(void[] dst) { auto result = source.read(dst); if( callbacks.read ) callbacks.read(result, dst); return result; } override InputStream flush() { source.flush(); if( callbacks.clear ) callbacks.clear(); return this; } override void[] load(size_t max=-1) { return Conduit.load(this, max); } override long seek(long offset, Anchor anchor = cast(Anchor)0) { auto result = seeker.seek(offset, anchor); if( callbacks.seek ) callbacks.seek(result, offset, anchor); return result; } private: InputStream source; InputStream seeker; //IConduit.Seek seeker; Callbacks callbacks; invariant() { assert( cast(Object) source is cast(Object) seeker ); } } /// ditto class EventSeekOutputStream : OutputStream //, IConduit.Seek { /// struct Callbacks { void delegate() close; /// void delegate() flush; /// void delegate(size_t, const(void)[]) write; /// void delegate(long, long, Anchor) seek; /// } //alias IConduit.Seek.Anchor Anchor; /// this(OutputStream source, Callbacks callbacks) in { assert( source !is null ); assert( (cast(IConduit.Seek) source.conduit) !is null ); } body { this.source = source; this.seeker = source; //cast(IConduit.Seek) source; this.callbacks = callbacks; } override IConduit conduit() { return source.conduit; } override OutputStream output() { return source; } override void close() { source.close(); source = null; seeker = null; if( callbacks.close ) callbacks.close(); } override size_t write(const(void)[] dst) { auto result = source.write(dst); if( callbacks.write ) callbacks.write(result, dst); return result; } override OutputStream flush() { source.flush(); if( callbacks.flush ) callbacks.flush(); return this; } override long seek(long offset, Anchor anchor = cast(Anchor)0) { auto result = seeker.seek(offset, anchor); if( callbacks.seek ) callbacks.seek(result, offset, anchor); return result; } override OutputStream copy(InputStream src, size_t max=-1) { Conduit.transfer(src, this, max); return this; } private: OutputStream source; OutputStream seeker; //IConduit.Seek seeker; Callbacks callbacks; invariant() { assert( cast(Object) source is cast(Object) seeker ); } } /******************************************************************************* copyright: Copyright © 2007 Daniel Keep. All rights reserved. license: BSD style: $(LICENSE) version: Prerelease author: Daniel Keep *******************************************************************************/ //module tangox.io.stream.WrapStream; //import tango.io.device.Conduit : Conduit; //import tango.io.model.IConduit : IConduit, InputStream, OutputStream; /** * This stream can be used to provide access to another stream. * Its distinguishing feature is that users cannot.close() the underlying * stream. * * This stream fully supports seeking, and as such requires that the * underlying stream also support seeking. */ class WrapSeekInputStream : InputStream //, IConduit.Seek { //alias IConduit.Seek.Anchor Anchor; /** * Create a new wrap stream from the given source. */ this(InputStream source) in { assert( source !is null ); assert( (cast(IConduit.Seek) source.conduit) !is null ); } body { this.source = source; this.seeker = source; //cast(IConduit.Seek) source; this._position = seeker.seek(0, Anchor.Current); } /// ditto this(InputStream source, long position) in { assert( position >= 0 ); } body { this(source); this._position = position; } override IConduit conduit() { return source.conduit; } InputStream input() { return source; } override void close() { source = null; seeker = null; } override size_t read(void[] dst) { if( seeker.seek(0, Anchor.Current) != _position ) seeker.seek(_position, Anchor.Begin); auto read = source.read(dst); if( read != IConduit.Eof ) _position += read; return read; } override InputStream flush() { source.flush(); return this; } override void[] load(size_t max=-1) { return Conduit.load(this, max); } override long seek(long offset, Anchor anchor = cast(Anchor)0) { seeker.seek(_position, Anchor.Begin); return (_position = seeker.seek(offset, anchor)); } private: InputStream source; InputStream seeker; //IConduit.Seek seeker; long _position; invariant() { assert( cast(Object) source is cast(Object) seeker ); assert( _position >= 0 ); } } /** * This stream can be used to provide access to another stream. * Its distinguishing feature is that the users cannot.close() the underlying * stream. * * This stream fully supports seeking, and as such requires that the * underlying stream also support seeking. */ class WrapSeekOutputStream : OutputStream//, IConduit.Seek { //alias IConduit.Seek.Anchor Anchor; /** * Create a new wrap stream from the given source. */ this(OutputStream source) in { assert( (cast(IConduit.Seek) source.conduit) !is null ); } body { this.source = source; this.seeker = source; //cast(IConduit.Seek) source; this._position = seeker.seek(0, Anchor.Current); } /// ditto this(OutputStream source, long position) in { assert( position >= 0 ); } body { this(source); this._position = position; } override IConduit conduit() { return source.conduit; } override OutputStream output() { return source; } override void close() { source = null; seeker = null; } size_t write(const(void)[] src) { if( seeker.seek(0, Anchor.Current) != _position ) seeker.seek(_position, Anchor.Begin); auto wrote = source.write(src); if( wrote != IConduit.Eof ) _position += wrote; return wrote; } override OutputStream copy(InputStream src, size_t max=-1) { Conduit.transfer(src, this, max); return this; } override OutputStream flush() { source.flush(); return this; } override long seek(long offset, Anchor anchor = cast(Anchor)0) { seeker.seek(_position, Anchor.Begin); return (_position = seeker.seek(offset, anchor)); } private: OutputStream source; OutputStream seeker; //IConduit.Seek seeker; long _position; invariant() { assert( cast(Object) source is cast(Object) seeker ); assert( _position >= 0 ); } } |