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 );
}
}
|