| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988 | /******************************************************************************* * * copyright: Copyright (c) 2007 Daniel Keep. All rights reserved. * * license: BSD style: $(LICENSE) * * version: Initial release: December 2007 * * author: Daniel Keep * ******************************************************************************/ module tango.util.compress.Zip; /* TODO ==== * Disable UTF encoding until I've worked out what version of Zip that's related to... (actually; it's entirely possible that's it's merely a *proposal* at the moment.) (*Done*) * Make ZipEntry safe: make them aware that their creating reader has been destroyed. */ import tango.core.ByteSwap : ByteSwap; import tango.io.device.Array : Array; import tango.io.device.File : File; import Path = tango.io.Path; import tango.io.device.FileMap : FileMap; import tango.io.stream.Zlib : ZlibInput, ZlibOutput; import tango.util.digest.Crc32 : Crc32; import tango.io.model.IConduit : IConduit, InputStream, OutputStream; import tango.io.stream.Digester : DigestInput; import tango.time.Time : Time, TimeSpan; import tango.time.WallClock : WallClock; import tango.time.chrono.Gregorian : Gregorian; import Integer = tango.text.convert.Integer; debug(Zip) import tango.io.Stdout : Stderr; ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // Implementation crap // // Why is this here, you ask? Because of bloody DMD forward reference bugs. // For pete's sake, Walter, FIX THEM, please! // // To skip to the actual user-visible stuff, search for "Shared stuff". private { ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // LocalFileHeader // align(1) struct LocalFileHeaderData { align(1): ushort extract_version = ushort.max; ushort general_flags = 0; ushort compression_method = 0; ushort modification_file_time = 0; ushort modification_file_date = 0; uint crc_32 = 0; // offsetof = 10 uint compressed_size = 0; uint uncompressed_size = 0; ushort file_name_length = 0; ushort extra_field_length = 0; debug(Zip) void dump() { Stderr ("LocalFileHeader.Data {")("\n") (" extract_version = ")(extract_version)("\n") (" general_flags = ")(general_flags)("\n") (" compression_method = ")(compression_method)("\n") (" modification_file_time = ")(modification_file_time)("\n") (" modification_file_date = ")(modification_file_date)("\n") (" crc_32 = ")(crc_32)("\n") (" compressed_size = ")(compressed_size)("\n") (" uncompressed_size = ")(uncompressed_size)("\n") (" file_name_length = ")(file_name_length)("\n") (" extra_field_length = ")(extra_field_length)("\n") ("}").newline; } } struct LocalFileHeader { const uint signature = 0x04034b50; alias LocalFileHeaderData Data; Data data; static assert( Data.sizeof == 26 ); char[] file_name; ubyte[] extra_field; void[] data_arr() { return (&data)[0..1]; } void put(OutputStream output) { // Make sure var-length fields will fit. if( file_name.length > ushort.max ) ZipException.fntoolong; if( extra_field.length > ushort.max ) ZipException.eftoolong; // Encode filename auto file_name = utf8_to_cp437(this.file_name); scope(exit) if( file_name !is cast(ubyte[])this.file_name ) delete file_name; if( file_name is null ) ZipException.fnencode; // Update lengths in data Data data = this.data; data.file_name_length = cast(ushort) file_name.length; data.extra_field_length = cast(ushort) extra_field.length; // Do it version( BigEndian ) swapAll(data); writeExact(output, (&data)[0..1]); writeExact(output, file_name); writeExact(output, extra_field); } void fill(InputStream src) { readExact(src, data_arr()); version( BigEndian ) swapAll(data); //debug(Zip) data.dump; auto tmp = new ubyte[data.file_name_length]; readExact(src, tmp); file_name = cp437_to_utf8(tmp); if( cast(char*) tmp.ptr !is file_name.ptr ) delete tmp; extra_field = new ubyte[data.extra_field_length]; readExact(src, extra_field); } /* * This method will check to make sure that the local and central headers * are the same; if they're not, then that indicates that the archive is * corrupt. */ bool agrees_with(FileHeader h) { // NOTE: extra_field used to be compared with h.extra_field, but this caused // an assertion in certain archives. I found a mention of these fields being // allowed to be different, so I think it in general is wrong to include in // this sanity check. larsivi 20081111 if( data.extract_version != h.data.extract_version || data.general_flags != h.data.general_flags || data.compression_method != h.data.compression_method || data.modification_file_time != h.data.modification_file_time || data.modification_file_date != h.data.modification_file_date || file_name != h.file_name ) return false; // We need a separate check for the sizes and crc32, since these will // be zero if a trailing descriptor was used. if( !h.usingDataDescriptor() && ( data.crc_32 != h.data.crc_32 || data.compressed_size != h.data.compressed_size || data.uncompressed_size != h.data.uncompressed_size ) ) return false; return true; } } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // FileHeader // align(1) struct FileHeaderData { align(1): ubyte zip_version; ubyte file_attribute_type; ushort extract_version; ushort general_flags; ushort compression_method; ushort modification_file_time; ushort modification_file_date; uint crc_32; uint compressed_size; uint uncompressed_size; ushort file_name_length; ushort extra_field_length; ushort file_comment_length; ushort disk_number_start; ushort internal_file_attributes = 0; uint external_file_attributes = 0; int relative_offset_of_local_header; debug(Zip) void dump() { Stderr ("FileHeader.Data {\n") (" zip_version = ")(zip_version)("\n") (" file_attribute_type = ")(file_attribute_type)("\n") (" extract_version = ")(extract_version)("\n") (" general_flags = ")(general_flags)("\n") (" compression_method = ")(compression_method)("\n") (" modification_file_time = ")(modification_file_time)("\n") (" modification_file_date = ")(modification_file_date)("\n") (" crc_32 = ")(crc_32)("\n") (" compressed_size = ")(compressed_size)("\n") (" uncompressed_size = ")(uncompressed_size)("\n") (" file_name_length = ")(file_name_length)("\n") (" extra_field_length = ")(extra_field_length)("\n") (" file_comment_length = ")(file_comment_length)("\n") (" disk_number_start = ")(disk_number_start)("\n") (" internal_file_attributes = ")(internal_file_attributes)("\n") (" external_file_attributes = ")(external_file_attributes)("\n") (" relative_offset_of_local_header = ")(relative_offset_of_local_header) ("\n") ("}").newline; } void fromLocal(LocalFileHeader.Data data) { extract_version = data.extract_version; general_flags = data.general_flags; compression_method = data.compression_method; modification_file_time = data.modification_file_time; modification_file_date = data.modification_file_date; crc_32 = data.crc_32; compressed_size = data.compressed_size; uncompressed_size = data.uncompressed_size; file_name_length = data.file_name_length; extra_field_length = data.extra_field_length; } } struct FileHeader { const uint signature = 0x02014b50; alias FileHeaderData Data; Data* data; static assert( Data.sizeof == 42 ); const(char)[] file_name; ubyte[] extra_field; const(char)[] file_comment; bool usingDataDescriptor() { return !!(data.general_flags & 1<<3); } uint compressionOptions() { return (data.general_flags >> 1) & 0b11; } bool usingUtf8() { //return !!(data.general_flags & 1<<11); return false; } void[] data_arr() { return (cast(void*)data)[0 .. Data.sizeof]; } void put(OutputStream output) { // Make sure the var-length fields will fit. if( file_name.length > ushort.max ) ZipException.fntoolong; if( extra_field.length > ushort.max ) ZipException.eftoolong; if( file_comment.length > ushort.max ) ZipException.cotoolong; // encode the filename and comment auto file_name = utf8_to_cp437(this.file_name); scope(exit) if( file_name !is cast(ubyte[])this.file_name ) delete file_name; auto file_comment = utf8_to_cp437(this.file_comment); scope(exit) if( file_comment !is cast(ubyte[])this.file_comment ) delete file_comment; if( file_name is null ) ZipException.fnencode; if( file_comment is null && this.file_comment !is null ) ZipException.coencode; // Update the lengths Data data = *(this.data); data.file_name_length = cast(ushort) file_name.length; data.extra_field_length = cast(ushort) extra_field.length; data.file_comment_length = cast(ushort) file_comment.length; // Ok; let's do this! version( BigEndian ) swapAll(data); writeExact(output, (&data)[0..1]); writeExact(output, file_name); writeExact(output, extra_field); writeExact(output, file_comment); } long map(void[] src) { //debug(Zip) Stderr.formatln("FileHeader.map([0..{}])",src.length); auto old_ptr = src.ptr; data = cast(Data*) src.ptr; src = src[Data.sizeof..$]; version( BigEndian ) swapAll(*data); //debug(Zip) data.dump; inout(char[]) function(inout(ubyte[])) conv_fn; if( usingUtf8() ) conv_fn = &cp437_to_utf8; else conv_fn = &utf8_to_utf8; file_name = conv_fn( cast(ubyte[]) src[0..data.file_name_length]); src = src[data.file_name_length..$]; extra_field = cast(ubyte[]) src[0..data.extra_field_length]; src = src[data.extra_field_length..$]; file_comment = conv_fn( cast(ubyte[]) src[0..data.file_comment_length]); src = src[data.file_comment_length..$]; // Return how many bytes we've eaten //debug(Zip) Stderr.formatln(" . used {} bytes", cast(long)(src.ptr - old_ptr)); return cast(long)(src.ptr - old_ptr); } } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // EndOfCDRecord // align(1) struct EndOfCDRecordData { align(1): ushort disk_number = 0; ushort disk_with_start_of_central_directory = 0; ushort central_directory_entries_on_this_disk; ushort central_directory_entries_total; uint size_of_central_directory; uint offset_of_start_of_cd_from_starting_disk; ushort file_comment_length; debug(Zip) void dump() { Stderr .formatln("EndOfCDRecord.Data {}","{") .formatln(" disk_number = {}", disk_number) .formatln(" disk_with_start_of_central_directory = {}", disk_with_start_of_central_directory) .formatln(" central_directory_entries_on_this_disk = {}", central_directory_entries_on_this_disk) .formatln(" central_directory_entries_total = {}", central_directory_entries_total) .formatln(" size_of_central_directory = {}", size_of_central_directory) .formatln(" offset_of_start_of_cd_from_starting_disk = {}", offset_of_start_of_cd_from_starting_disk) .formatln(" file_comment_length = {}", file_comment_length) .formatln("}"); } } struct EndOfCDRecord { const uint signature = 0x06054b50; alias EndOfCDRecordData Data; Data data; static assert( data.sizeof == 18 ); char[] file_comment; void[] data_arr() { return (cast(void*)&data)[0 .. data.sizeof]; } void put(OutputStream output) { // Set up the comment; check length, encode if( file_comment.length > ushort.max ) ZipException.cotoolong; auto file_comment = utf8_to_cp437(this.file_comment); scope(exit) if( file_comment !is cast(ubyte[])this.file_comment ) delete file_comment; // Set up data block Data data = this.data; data.file_comment_length = cast(ushort) file_comment.length; version( BigEndian ) swapAll(data); writeExact(output, (&data)[0..1]); } void fill(void[] src) { //Stderr.formatln("EndOfCDRecord.fill([0..{}])",src.length); auto _data = data_arr(); _data[] = src[0.._data.length]; src = src[_data.length..$]; version( BigEndian ) swapAll(data); //data.dump; file_comment = cast(char[]) src[0..data.file_comment_length].dup; } } // End of implementation crap } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // Shared stuff public { /** * This enumeration denotes the kind of compression used on a file. */ enum Method { /// No compression should be used. Store, /// Deflate compression. Deflate, /** * This is a special value used for unsupported or unrecognised * compression methods. This value is only used internally. */ Unsupported } } private { const ushort ZIP_VERSION = 20; const ushort MAX_EXTRACT_VERSION = 20; /* compression flags uses trailing descriptor | utf-8 encoding | | ^ ^ /\ */ const ushort SUPPORTED_FLAGS = 0b00_0_0_0_0000_0_0_0_1_11_0; const ushort UNSUPPORTED_FLAGS = ~SUPPORTED_FLAGS; Method toMethod(ushort method) { switch( method ) { case 0: return Method.Store; case 8: return Method.Deflate; default: return Method.Unsupported; } } ushort fromMethod(Method method) { switch( method ) { case Method.Store: return 0; case Method.Deflate: return 8; default: assert(false, "unsupported compression method"); } } /* NOTE: This doesn't actually appear to work. Using the default magic * number with Tango's Crc32 digest works, however. */ //const CRC_MAGIC = 0xdebb20e3u; } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // ZipReader interface ZipReader { bool streamed(); void close(); bool more(); ZipEntry get(); ZipEntry get(ZipEntry); int opApply(int delegate(ref ZipEntry)); } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // ZipWriter interface ZipWriter { void finish(); void putFile(ZipEntryInfo info, const(char)[] path); void putStream(ZipEntryInfo info, InputStream source); void putEntry(ZipEntryInfo info, ZipEntry entry); void putData(ZipEntryInfo info, const(void)[] data); Method method(); Method method(Method); } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // ZipBlockReader /** * The ZipBlockReader class is used to parse a Zip archive. It exposes the * contents of the archive via an iteration interface. For instance, to loop * over all files in an archive, one can use either * * ----- * foreach( entry ; reader ) * ... * ----- * * Or * * ----- * while( reader.more ) * { * auto entry = reader.get; * ... * } * ----- * * See the ZipEntry class for more information on the contents of entries. * * Note that this class can only be used with input sources which can be * freely seeked. Also note that you may open a ZipEntry instance produced by * this reader at any time until the ZipReader that created it is closed. */ class ZipBlockReader : ZipReader { /** * Creates a ZipBlockReader using the specified file on the local * filesystem. */ this(const(char)[] path) { file_source = new File(path); this(file_source); } /** * Creates a ZipBlockReader using the provided InputStream. Please note * that this InputStream must be attached to a conduit implementing the * IConduit.Seek interface. */ this(InputStream source) in { assert( cast(IConduit.Seek) source.conduit, "source stream must be seekable" ); } body { this.source = source; this.seeker = source; //cast(IConduit.Seek) source; } bool streamed() { return false; } /** * Closes the reader, and releases all resources. After this operation, * all ZipEntry instances created by this ZipReader are invalid and should * not be used. */ void close() { // NOTE: Originally more of the GC allocated data in this class were // explicitly deleted here, such as cd_data - this caused segfaults // and have been removed as they were not necessary from correctness // point of view, and the memory usage win is questionable. state = State.Done; source = null; seeker = null; delete headers; if( file_source !is null ) { file_source.close(); delete file_source; } } /** * Returns true if and only if there are additional files in the archive * which have not been read via the get method. This returns true before * the first call to get (assuming the opened archive is non-empty), and * false after the last file has been accessed. */ bool more() { switch( state ) { case State.Init: read_cd(); assert( state == State.Open ); return more(); case State.Open: return (current_index < headers.length); case State.Done: return false; default: assert(false); } } /** * Retrieves the next file from the archive. Note that although this does * perform IO operations, it will not read the contents of the file. * * The optional reuse argument can be used to instruct the reader to reuse * an existing ZipEntry instance. If passed a null reference, it will * create a new ZipEntry instance. */ ZipEntry get() { if( !more() ) ZipExhaustedException(); return new ZipEntry(headers[current_index++], &open_file); } /// ditto ZipEntry get(ZipEntry reuse) { if( !more() ) ZipExhaustedException(); if( reuse is null ) return new ZipEntry(headers[current_index++], &open_file); else return reuse.reset(headers[current_index++], &open_file); } /** * This is used to iterate over the contents of an archive using a foreach * loop. Please note that the iteration will reuse the ZipEntry instance * passed to your loop. If you wish to keep the instance and re-use it * later, you $(B must) use the dup member to create a copy. */ int opApply(int delegate(ref ZipEntry) dg) { int result = 0; ZipEntry entry; while( more() ) { entry = get(entry); result = dg(entry); if( result ) break; } if( entry !is null ) delete entry; return result; } private: InputStream source; InputStream seeker; //IConduit.Seek seeker; enum State { Init, Open, Done } State state; size_t current_index = 0; FileHeader[] headers; // These should be killed when the reader is closed. ubyte[] cd_data; File file_source = null; /* * This function will read the contents of the central directory. Split * or spanned archives aren't supported. */ void read_cd() in { assert( state == State.Init ); assert( headers is null ); assert( cd_data is null ); } out { assert( state == State.Open ); assert( headers !is null ); assert( cd_data !is null ); assert( current_index == 0 ); } body { //Stderr.formatln("ZipReader.read_cd()"); // First, we need to locate the end of cd record, so that we know // where the cd itself is, and how big it is. auto eocdr = read_eocd_record(); // Now, make sure the archive is all in one file. if( eocdr.data.disk_number != eocdr.data.disk_with_start_of_central_directory || eocdr.data.central_directory_entries_on_this_disk != eocdr.data.central_directory_entries_total ) ZipNotSupportedException.spanned(); // Ok, read the whole damn thing in one go. cd_data = new ubyte[eocdr.data.size_of_central_directory]; long cd_offset = eocdr.data.offset_of_start_of_cd_from_starting_disk; seeker.seek(cd_offset, seeker.Anchor.Begin); readExact(source, cd_data); // Cake. Now, we need to break it up into records. headers = new FileHeader[ eocdr.data.central_directory_entries_total]; long cdr_offset = cd_offset; // Ok, map the CD data into file headers. foreach( i,ref header ; headers ) { //Stderr.formatln(" . reading header {}...", i); // Check signature { uint sig = (cast(uint[])(cd_data[0..4]))[0]; version( BigEndian ) swap(sig); if( sig != FileHeader.signature ) ZipException.badsig("file header"); } auto used = header.map(cd_data[4..$]); assert( used <= (size_t.max-4) ); cd_data = cd_data[4+cast(size_t)used..$]; // Update offset for next record cdr_offset += 4 /* for sig. */ + used; } // Done! state = State.Open; } /* * This will locate the end of CD record in the open stream. * * This code sucks, but that's because Zip sucks. * * Basically, the EOCD record is stuffed somewhere at the end of the file. * In a brilliant move, the record is *variably sized*, which means we * have to do a linear backwards search to find it. * * The header itself (including the signature) is at minimum 22 bytes * long, plus anywhere between 0 and 2^16-1 bytes of comment. That means * we need to read the last 2^16-1 + 22 bytes from the file, and look for * the signature [0x50,0x4b,0x05,0x06] in [0 .. $-18]. * * If we find the EOCD record, we'll return its contents. If we couldn't * find it, we'll throw an exception. */ EndOfCDRecord read_eocd_record() in { assert( state == State.Init ); } body { //Stderr.formatln("read_eocd_record()"); // Signature + record + max. comment length const max_chunk_len = 4 + EndOfCDRecord.Data.sizeof + ushort.max; auto file_len = seeker.seek(0, seeker.Anchor.End); assert( file_len <= size_t.max ); // We're going to need min(max_chunk_len, file_len) bytes. size_t chunk_len = max_chunk_len; if( file_len < max_chunk_len ) chunk_len = cast(size_t) file_len; //Stderr.formatln(" . chunk_len = {}", chunk_len); // Seek back and read in the chunk. Don't forget to clean up after // ourselves. seeker.seek(-cast(long)chunk_len, seeker.Anchor.End); auto chunk_offset = seeker.seek(0, seeker.Anchor.Current); //Stderr.formatln(" . chunk_offset = {}", chunk_offset); auto chunk = new ubyte[chunk_len]; scope(exit) delete chunk; readExact(source, chunk); // Now look for our magic number. Don't forget that on big-endian // machines, we need to byteswap the value we're looking for. uint eocd_magic = EndOfCDRecord.signature; version( BigEndian ) swap(eocd_magic); size_t eocd_loc = -1; if( chunk_len >= 18 ) for( size_t i=chunk_len-18; i>=0; --i ) { if( *(cast(uint*)(chunk.ptr+i)) == eocd_magic ) { // Found the bugger! Make sure we skip the signature (forgot // to do that originally; talk about weird errors :P) eocd_loc = i+4; break; } } // If we didn't find it, then we'll assume that this is not a valid // archive. if( eocd_loc == -1 ) ZipException.missingdir; // Ok, so we found it; now what? Now we need to read the record // itself in. eocd_loc is the offset within the chunk where the eocd // record was found, so slice it out. EndOfCDRecord eocdr; eocdr.fill(chunk[eocd_loc..$]); // Excellent. We're done here. return eocdr; } /* * Opens the specified file for reading. If the raw argument passed is * true, then the file is *not* decompressed. */ InputStream open_file(FileHeader header, bool raw) { // Check to make sure that we actually *can* open this file. if( header.data.extract_version > MAX_EXTRACT_VERSION ) ZipNotSupportedException.zipver(header.data.extract_version); if( header.data.general_flags & UNSUPPORTED_FLAGS ) ZipNotSupportedException.flags(); if( toMethod(header.data.compression_method) == Method.Unsupported ) ZipNotSupportedException.method(header.data.compression_method); // Open a raw stream InputStream stream = open_file_raw(header); // If that's all they wanted, pass it back. if( raw ) return stream; // Next up, wrap in an appropriate decompression stream switch( toMethod(header.data.compression_method) ) { case Method.Store: // Do nothing: \o/ break; case Method.Deflate: // Wrap in a zlib stream. We want a raw deflate stream, // so force no encoding. stream = new ZlibInput(stream, ZlibInput.Encoding.None); break; default: assert(false); } // We done, yo! return stream; } /* * Opens a file's raw input stream. Basically, this returns a slice of * the archive's input stream. */ InputStream open_file_raw(FileHeader header) { // Seek to and parse the local file header seeker.seek(header.data.relative_offset_of_local_header, seeker.Anchor.Begin); { uint sig; readExact(source, (&sig)[0..1]); version( BigEndian ) swap(sig); if( sig != LocalFileHeader.signature ) ZipException.badsig("local file header"); } LocalFileHeader lheader; lheader.fill(source); if( !lheader.agrees_with(header) ) ZipException.incons(header.file_name); // Ok; get a slice stream for the file return new SliceSeekInputStream( source, seeker.seek(0, seeker.Anchor.Current), header.data.compressed_size); } } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // ZipBlockWriter /** * The ZipBlockWriter class is used to create a Zip archive. It uses a * writing iterator interface. * * Note that this class can only be used with output streams which can be * freely seeked. */ class ZipBlockWriter : ZipWriter { /** * Creates a ZipBlockWriter using the specified file on the local * filesystem. */ this(const(char)[] path) { file_output = new File(path, File.WriteCreate); this(file_output); } /** * Creates a ZipBlockWriter using the provided OutputStream. Please note * that this OutputStream must be attached to a conduit implementing the * IConduit.Seek interface. */ this(OutputStream output) in { assert( output !is null ); assert( (cast(IConduit.Seek) output.conduit) !is null ); } body { this.output = output; this.seeker = output; // cast(IConduit.Seek) output; // Default to Deflate compression method = Method.Deflate; } /** * Finalises the archive, writes out the central directory, and closes the * output stream. */ void finish() { put_cd(); output.close(); output = null; seeker = null; if( file_output !is null ) delete file_output; } /** * Adds a file from the local filesystem to the archive. */ void putFile(ZipEntryInfo info, const(char)[] path) { scope file = new File(path); scope(exit) file.close(); putStream(info, file); } /** * Adds a file using the contents of the given InputStream to the archive. */ void putStream(ZipEntryInfo info, InputStream source) { put_compressed(info, source); } /** * Transfers a file from another archive into this archive. Note that * this method will not perform any compression: whatever compression was * applied to the file originally will be preserved. */ void putEntry(ZipEntryInfo info, ZipEntry entry) { put_raw(info, entry); } /** * Adds a file using the contents of the given array to the archive. */ void putData(ZipEntryInfo info, const(void)[] data) { //scope mc = new MemoryConduit(data); scope mc = new Array(data.dup); scope(exit) mc.close(); put_compressed(info, mc); } /** * This property allows you to control what compression method should be * used for files being added to the archive. */ @property Method method() { return _method; } @property Method method(Method v) { return _method = v; } /// ditto private: OutputStream output; OutputStream seeker; File file_output; Method _method; struct Entry { FileHeaderData data; long header_position; const(char)[] filename; const(char)[] comment; ubyte[] extra; } Entry[] entries; void put_cd() { // check that there aren't too many CD entries if( entries.length > ushort.max ) ZipException.toomanyentries; auto cd_pos = seeker.seek(0, seeker.Anchor.Current); if( cd_pos > uint.max ) ZipException.toolong; foreach( entry ; entries ) { FileHeader header; header.data = &entry.data; header.file_name = entry.filename; header.extra_field = entry.extra; header.file_comment = entry.comment; write(output, FileHeader.signature); header.put(output); } auto cd_len = seeker.seek(0, seeker.Anchor.Current) - cd_pos; if( cd_len > uint.max ) ZipException.cdtoolong; { assert( entries.length < ushort.max ); assert( cd_len < uint.max ); assert( cd_pos < uint.max ); EndOfCDRecord eocdr; eocdr.data.central_directory_entries_on_this_disk = cast(ushort) entries.length; eocdr.data.central_directory_entries_total = cast(ushort) entries.length; eocdr.data.size_of_central_directory = cast(uint) cd_len; eocdr.data.offset_of_start_of_cd_from_starting_disk = cast(uint) cd_pos; write(output, EndOfCDRecord.signature); eocdr.put(output); } } void put_raw(ZipEntryInfo info, ZipEntry entry) { // Write out local file header LocalFileHeader.Data lhdata; auto chdata = entry.header.data; lhdata.extract_version = chdata.extract_version; // Note: we need to mask off the data descriptor bit because we aren't // going to write one. lhdata.general_flags = chdata.general_flags & ~(1<<3); lhdata.compression_method = chdata.compression_method; lhdata.crc_32 = chdata.crc_32; lhdata.compressed_size = chdata.compressed_size; lhdata.uncompressed_size = chdata.uncompressed_size; timeToDos(info.modified, lhdata.modification_file_time, lhdata.modification_file_date); put_local_header(lhdata, info.name); // Store comment entries[$-1].comment = info.comment; // Output file contents { auto input = entry.open_raw(); scope(exit) input.close(); output.copy(input).flush(); } } void put_compressed(ZipEntryInfo info, InputStream source) { debug(Zip) Stderr.formatln("ZipBlockWriter.put_compressed()"); // Write out partial local file header auto header_pos = seeker.seek(0, seeker.Anchor.Current); debug(Zip) Stderr.formatln(" . header for {} at {}", info.name, header_pos); put_local_header(info, _method); // Store comment entries[$-1].comment = info.comment; uint crc; uint compressed_size; uint uncompressed_size; // Output file contents { // Input/output chains InputStream in_chain = source; OutputStream out_chain = new WrapSeekOutputStream(output); // Count number of bytes coming in from the source file scope in_counter = new CounterInput(in_chain); in_chain = in_counter; assert( in_counter.count() <= typeof(uncompressed_size).max ); scope(success) uncompressed_size = cast(uint) in_counter.count(); // Count the number of bytes going out to the archive scope out_counter = new CounterOutput(out_chain); out_chain = out_counter; assert( out_counter.count() <= typeof(compressed_size).max ); scope(success) compressed_size = cast(uint) out_counter.count(); // Add crc scope crc_d = new Crc32(/*CRC_MAGIC*/); scope crc_s = new DigestInput(in_chain, crc_d); in_chain = crc_s; scope(success) { debug(Zip) Stderr.formatln(" . Success: storing CRC."); crc = crc_d.crc32Digest(); } // Add compression ZlibOutput compress; scope(exit) if( compress !is null ) delete compress; switch( _method ) { case Method.Store: break; case Method.Deflate: compress = new ZlibOutput(out_chain, ZlibOutput.Level.init, ZlibOutput.Encoding.None); out_chain = compress; break; default: assert(false); } // All done. scope(exit) in_chain.close(); scope(success) in_chain.flush(); scope(exit) out_chain.close(); out_chain.copy(in_chain).flush(); debug(Zip) if( compress !is null ) { Stderr.formatln(" . compressed to {} bytes", compress.written); } debug(Zip) Stderr.formatln(" . wrote {} bytes", out_counter.count); debug(Zip) Stderr.formatln(" . contents written"); } debug(Zip) Stderr.formatln(" . CRC for \"{}\": 0x{:x8}", info.name, crc); // Rewind, and patch the header auto final_pos = seeker.seek(0, seeker.Anchor.Current); seeker.seek(header_pos); patch_local_header(crc, compressed_size, uncompressed_size); // Seek back to the end of the file, and we're done! seeker.seek(final_pos); } /* * Patches the local file header starting at the current output location * with updated crc and size information. Also updates the current last * Entry. */ void patch_local_header(uint crc_32, uint compressed_size, uint uncompressed_size) { /* BUG: For some reason, this code won't compile. No idea why... if * you instantiate LFHD, it says that there is no "offsetof" property. */ /+ alias LocalFileHeaderData LFHD; static assert( LFHD.compressed_size.offsetof == LFHD.crc_32.offsetof + 4 ); static assert( LFHD.uncompressed_size.offsetof == LFHD.compressed_size.offsetof + 4 ); +/ // Don't forget we have to seek past the signature, too // BUG: .offsetof is broken here /+seeker.seek(LFHD.crc_32.offsetof+4, seeker.Anchor.Current);+/ seeker.seek(10+4, seeker.Anchor.Current); write(output, crc_32); write(output, compressed_size); write(output, uncompressed_size); with( entries[$-1] ) { data.crc_32 = crc_32; data.compressed_size = compressed_size; data.uncompressed_size = uncompressed_size; } } /* * Generates and outputs a local file header from the given info block and * compression method. Note that the crc_32, compressed_size and * uncompressed_size header fields will be set to zero, and must be * patched. */ void put_local_header(ZipEntryInfo info, Method method) { LocalFileHeader.Data data; data.compression_method = fromMethod(method); timeToDos(info.modified, data.modification_file_time, data.modification_file_date); put_local_header(data, info.name); } /* * Writes the given local file header data and filename out to the output * stream. It also appends a new Entry with the data and filename. */ void put_local_header(LocalFileHeaderData data, const(char)[] file_name) { auto f_name = Path.normalize(file_name); auto p = Path.parse(f_name); // Compute Zip version if( data.extract_version == data.extract_version.max ) { ushort zipver = 10; void minver(ushort v) { zipver = v>zipver ? v : zipver; } { // Compression method switch( data.compression_method ) { case 0: minver(10); break; case 8: minver(20); break; default: assert(false); } // File is a folder if( f_name.length > 0 && f_name[$-1] == '/' ) // Is a directory, not a real file minver(20); } data.extract_version = zipver; } /+// Encode filename auto file_name_437 = utf8_to_cp437(file_name); if( file_name_437 is null ) ZipException.fnencode;+/ /+// Set up file name length if( file_name_437.length > ushort.max ) ZipException.fntoolong; data.file_name_length = file_name_437.length;+/ LocalFileHeader header; header.data = data; if (p.isAbsolute) f_name = f_name[p.root.length+1..$]; header.file_name = f_name; // Write out the header and the filename auto header_pos = seeker.seek(0, seeker.Anchor.Current); write(output, LocalFileHeader.signature); header.put(output); // Save the header assert( header_pos <= int.max ); Entry entry; entry.data.fromLocal(header.data); entry.filename = header.file_name; entry.header_position = header_pos; entry.data.relative_offset_of_local_header = cast(int) header_pos; entries ~= entry; } } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // ZipEntry /** * This class is used to represent a single entry in an archive. * Specifically, it combines meta-data about the file (see the info field) * along with the two basic operations on an entry: open and verify. */ class ZipEntry { /** * Header information on the file. See the ZipEntryInfo structure for * more information. */ ZipEntryInfo info; /** * Size (in bytes) of the file's uncompressed contents. */ uint size() { return header.data.uncompressed_size; } /** * Opens a stream for reading from the file. The contents of this stream * represent the decompressed contents of the file stored in the archive. * * You should not assume that the returned stream is seekable. * * Note that the returned stream may be safely closed without affecting * the underlying archive stream. * * If the file has not yet been verified, then the stream will be checked * as you read from it. When the stream is either exhausted or closed, * then the integrity of the file's data will be checked. This means that * if the file is corrupt, an exception will be thrown only after you have * finished reading from the stream. If you wish to make sure the data is * valid before you read from the file, call the verify method. */ InputStream open() { // If we haven't verified yet, wrap the stream in the appropriate // decorators. if( !verified ) return new ZipEntryVerifier(this, open_dg(header, false)); else return open_dg(header, false); } /** * Verifies the contents of this file by computing the CRC32 checksum, * and comparing it against the stored one. Throws an exception if the * checksums do not match. * * Not valid on streamed Zip archives. */ void verify() { // If we haven't verified the contents yet, just read everything in // to trigger it. auto s = open(); auto buffer = new ubyte[s.conduit.bufferSize]; while( s.read(buffer) != s.Eof ) {/*Do nothing*/} s.close(); } /** * Creates a new, independent copy of this instance. */ ZipEntry dup() { return new ZipEntry(header, open_dg); } private: /* * Callback used to open the file. */ alias InputStream delegate(FileHeader, bool raw) open_dg_t; open_dg_t open_dg; /* * Raw ZIP header. */ FileHeader header; /* * The flag used to keep track of whether the file's contents have been * verified. */ bool verified = false; /* * Opens a stream that does not perform any decompression or * transformation of the file contents. This is used internally by * ZipWriter to perform fast zip to zip transfers without having to * decompress and then recompress the contents. * * Note that because zip stores CRCs for the *uncompressed* data, this * method currently does not do any verification. */ InputStream open_raw() { return open_dg(header, true); } /* * Creates a new ZipEntry from the FileHeader. */ this(FileHeader header, open_dg_t open_dg) { this.reset(header, open_dg); } /* * Resets the current instance with new values. */ ZipEntry reset(FileHeader header, open_dg_t open_dg) { this.header = header; this.open_dg = open_dg; with( info ) { name = Path.standard(header.file_name.dup); dosToTime(header.data.modification_file_time, header.data.modification_file_date, modified); comment = header.file_comment.dup; } this.verified = false; return this; } } /** * This structure contains various pieces of meta-data on a file. The * contents of this structure may be safely mutated. * * This structure is also used to specify meta-data about a file when adding * it to an archive. */ struct ZipEntryInfo { /// Full path and file name of this file. const(char)[] name; /// Modification timestamp. If this is left uninitialised when passed to /// a ZipWriter, it will be reset to the current system time. Time modified = Time.min; /// Comment on the file. const(char)[] comment; } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // Exceptions // /** * This is the base class from which all exceptions generated by this module * derive from. */ class ZipException : Exception { this(immutable(char)[] msg) { super(msg); } private: alias typeof(this) thisT; static void opCall(immutable(char)[] msg) { throw new ZipException(msg); } @property static void badsig() { thisT("corrupt signature or unexpected section found"); } @property static void badsig(const(char)[] type) { thisT("corrupt "~type.idup~" signature or unexpected section found"); } @property static void incons(const(char)[] name) { thisT("inconsistent headers for file \""~name.idup~"\"; " "archive is likely corrupted"); } @property static void missingdir() { thisT("could not locate central archive directory; " "file is corrupt or possibly not a Zip archive"); } @property static void toomanyentries() { thisT("too many archive entries"); } @property static void toolong() { thisT("archive is too long; limited to 4GB total"); } @property static void cdtoolong() { thisT("central directory is too long; limited to 4GB total"); } @property static void fntoolong() { thisT("file name too long; limited to 65,535 characters"); } @property static void eftoolong() { thisT("extra field too long; limited to 65,535 characters"); } @property static void cotoolong() { thisT("extra field too long; limited to 65,535 characters"); } @property static void fnencode() { thisT("could not encode filename into codepage 437"); } @property static void coencode() { thisT("could not encode comment into codepage 437"); } @property static void tooold() { thisT("cannot represent dates before January 1, 1980"); } } /** * This exception is thrown if a ZipReader detects that a file's contents do * not match the stored checksum. */ class ZipChecksumException : ZipException { this(const(char)[] name) { super("checksum failed on zip entry \""~name.idup~"\""); } private: static void opCall(const(char)[] name) { throw new ZipChecksumException(name); } } /** * This exception is thrown if you call get reader method when there are no * more files in the archive. */ class ZipExhaustedException : ZipException { this() { super("no more entries in archive"); } private: static void opCall() { throw new ZipExhaustedException; } } /** * This exception is thrown if you attempt to read an archive that uses * features not supported by the reader. */ class ZipNotSupportedException : ZipException { this(immutable(char)[] msg) { super(msg); } private: alias ZipNotSupportedException thisT; static void opCall(const(char)[] msg) { throw new thisT(msg.idup ~ " not supported"); } static void spanned() { thisT("split and multi-disk archives"); } static void zipver(ushort ver) { throw new thisT("zip format version " ~Integer.toString(ver / 10).idup ~"." ~Integer.toString(ver % 10).idup ~" not supported; maximum of version " ~Integer.toString(MAX_EXTRACT_VERSION / 10).idup ~"." ~Integer.toString(MAX_EXTRACT_VERSION % 10).idup ~" supported."); } static void flags() { throw new thisT("unknown or unsupported file flags enabled"); } static void method(ushort m) { // Cheat here and work out what the method *actually* is immutable(char)[] ms; switch( m ) { case 0: case 8: assert(false); // supported case 1: ms = "Shrink"; break; case 2: ms = "Reduce (factor 1)"; break; case 3: ms = "Reduce (factor 2)"; break; case 4: ms = "Reduce (factor 3)"; break; case 5: ms = "Reduce (factor 4)"; break; case 6: ms = "Implode"; break; case 9: ms = "Deflate64"; break; case 10: ms = "TERSE (old)"; break; case 12: ms = "Bzip2"; break; case 14: ms = "LZMA"; break; case 18: ms = "TERSE (new)"; break; case 19: ms = "LZ77"; break; case 97: ms = "WavPack"; break; case 98: ms = "PPMd"; break; default: ms = "unknown"; } thisT(ms ~ " compression method"); } } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // Convenience methods void createArchive(const(char)[] archive, Method method, const(char[])[] files...) { scope zw = new ZipBlockWriter(archive); zw.method = method; foreach( file ; files ) { ZipEntryInfo zi; zi.name = file; zi.modified = Path.modified(file); zw.putFile(zi, file); } zw.finish(); } void extractArchive(const(char)[] archive, const(char)[] dest) { scope zr = new ZipBlockReader(archive); foreach( entry ; zr ) { // Skip directories if( entry.info.name[$-1] == '/' || entry.info.name[$-1] == '\\') continue; auto path = Path.join(dest, entry.info.name); path = Path.normalize(path); // Create the parent directory if necessary. auto parent = Path.parse(path).parent; if( !Path.exists(parent) ) { Path.createPath(parent); } path = Path.native(path); // Write out the file scope fout = new File(path, File.WriteCreate); fout.copy(entry.open()); fout.close(); // Update timestamps auto oldTS = Path.timeStamps(path); Path.timeStamps(path, oldTS.accessed, entry.info.modified); } } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // Private implementation stuff // private: ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // Verification stuff /* * This class wraps an input stream, and computes the CRC as it passes * through. On the event of either a close or EOF, it checks the CRC against * the one in the provided ZipEntry. If they don't match, it throws an * exception. */ class ZipEntryVerifier : InputStream { this(ZipEntry entry, InputStream source) in { assert( entry !is null ); assert( source !is null ); } body { this.entry = entry; this.digest = new Crc32; this.source = new DigestInput(source, digest); } IConduit conduit() { return source.conduit; } InputStream input() { return source; } long seek (long ofs, Anchor anchor = Anchor.Begin) { return source.seek (ofs, anchor); } void close() { check(); this.source.close(); this.entry = null; this.digest = null; this.source = null; } size_t read(void[] dst) { auto bytes = source.read(dst); if( bytes == IConduit.Eof ) check(); return bytes; } override void[] load(size_t max=-1) { return Conduit.load(this, max); } override InputStream flush() { this.source.flush(); return this; } private: Crc32 digest; InputStream source; ZipEntry entry; void check() { if( digest is null ) return; auto crc = digest.crc32Digest(); delete digest; if( crc != entry.header.data.crc_32 ) ZipChecksumException(entry.info.name); else entry.verified = true; } } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // IO functions /* * Really, seriously, read some bytes without having to go through a sodding * buffer. */ void readExact(InputStream s, void[] dst) { //Stderr.formatln("readExact(s, [0..{}])", dst.length); while( dst.length > 0 ) { auto octets = s.read(dst); //Stderr.formatln(" . octets = {}", octets); if( octets == -1 ) // Beware the dangers of MAGICAL THINKING throw new Exception("unexpected end of stream"); dst = dst[octets..$]; } } /* * Really, seriously, write some bytes. */ void writeExact(OutputStream s, const(void)[] src) { while( src.length > 0 ) { auto octets = s.write(src); if( octets == -1 ) throw new Exception("unexpected end of stream"); src = src[octets..$]; } } void write(T)(OutputStream s, T value) { version( BigEndian ) swap(value); writeExact(s, (&value)[0..1]); } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // Endian garbage void swapAll(T)(ref T data) { static if( is(typeof(T.record_fields)) ) const fields = T.record_fields; else const fields = data.tupleof.length; foreach( i,_ ; data.tupleof ) { if( i == fields ) break; swap(data.tupleof[i]); } } void swap(T)(ref T data) { static if( T.sizeof == 1 ) {} else static if( T.sizeof == 2 ) ByteSwap.swap16(&data, 2); else static if( T.sizeof == 4 ) ByteSwap.swap32(&data, 4); else static if( T.sizeof == 8 ) ByteSwap.swap64(&data, 8); else static if( T.sizeof == 10 ) ByteSwap.swap80(&data, 10); else static assert(false, "Can't swap "~T.stringof~"s."); } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // IBM Code Page 437 stuff // const char[][] cp437_to_utf8_map_low = [ "\u0000"[], "\u263a", "\u263b", "\u2665", "\u2666", "\u2663", "\u2660", "\u2022", "\u25d8", "\u25cb", "\u25d9", "\u2642", "\u2640", "\u266a", "\u266b", "\u263c", "\u25b6", "\u25c0", "\u2195", "\u203c", "\u00b6", "\u00a7", "\u25ac", "\u21a8", "\u2191", "\u2193", "\u2192", "\u2190", "\u221f", "\u2194", "\u25b2", "\u25bc" ]; const char[][] cp437_to_utf8_map_high = [ "\u00c7"[], "\u00fc", "\u00e9", "\u00e2", "\u00e4", "\u00e0", "\u00e5", "\u00e7", "\u00ea", "\u00eb", "\u00e8", "\u00ef", "\u00ee", "\u00ec", "\u00c4", "\u00c5", "\u00c9", "\u00e6", "\u00c6", "\u00f4", "\u00f6", "\u00f2", "\u00fb", "\u00f9", "\u00ff", "\u00d6", "\u00dc", "\u00f8", "\u00a3", "\u00a5", "\u20a7", "\u0192", "\u00e1", "\u00ed", "\u00f3", "\u00fa", "\u00f1", "\u00d1", "\u00aa", "\u00ba", "\u00bf", "\u2310", "\u00ac", "\u00bd", "\u00bc", "\u00a1", "\u00ab", "\u00bb", "\u2591", "\u2592", "\u2593", "\u2502", "\u2524", "\u2561", "\u2562", "\u2556", "\u2555", "\u2563", "\u2551", "\u2557", "\u255d", "\u255c", "\u255b", "\u2510", "\u2514", "\u2534", "\u252c", "\u251c", "\u2500", "\u253c", "\u255e", "\u255f", "\u255a", "\u2554", "\u2569", "\u2566", "\u2560", "\u2550", "\u256c", "\u2567", "\u2568", "\u2564", "\u2565", "\u2559", "\u2558", "\u2552", "\u2553", "\u256b", "\u256a", "\u2518", "\u250c", "\u2588", "\u2584", "\u258c", "\u2590", "\u2580", "\u03b1", "\u00df", "\u0393", "\u03c0", "\u03a3", "\u03c3", "\u00b5", "\u03c4", "\u03a6", "\u0398", "\u03a9", "\u03b4", "\u221e", "\u03c6", "\u03b5", "\u2229", "\u2261", "\u00b1", "\u2265", "\u2264", "\u2320", "\u2321", "\u00f7", "\u2248", "\u00b0", "\u2219", "\u00b7", "\u221a", "\u207f", "\u00b2", "\u25a0", "\u00a0" ]; inout(char[]) cp437_to_utf8(inout(ubyte[]) s) { foreach( i,c ; s ) { if( (1 <= c && c <= 31) || c >= 127 ) { /* Damn; we got a character not in ASCII. Since this is the first * non-ASCII character we found, copy everything up to this point * into the output verbatim. We'll allocate twice as much space * as there are remaining characters to ensure we don't need to do * any further allocations. */ auto r = new char[i+2*(s.length-i)]; r[0..i] = cast(char[]) s[0..i]; size_t k=i; // current length // We insert new characters at r[i+j+k] foreach( d ; s[i..$] ) { if( 32 <= d && d <= 126 || d == 0 ) { r[k++] = d; } else if( 1 <= d && d <= 31 ) { const(char)[] repl = cp437_to_utf8_map_low[d]; r[k..k+repl.length] = repl[]; k += repl.length; } else if( d == 127 ) { const(char)[] repl = "\u2302"; r[k..k+repl.length] = repl[]; k += repl.length; } else if( d > 127 ) { const(char)[] repl = cp437_to_utf8_map_high[d-128]; r[k..k+repl.length] = repl[]; k += repl.length; } else assert(false); } return cast(typeof(return))r[0..k]; } } /* If we got here, then all the characters in s are also in ASCII, which * means it's also valid UTF-8; return the string unmodified. */ return cast(typeof(return)) s; } debug( UnitTest ) { unittest { const(char)[] c(const(char)[] s) { return cp437_to_utf8(cast(const(ubyte)[]) s); } auto s = c("Hi there \x01 old \x0c!"); assert( s == "Hi there \u263a old \u2640!", "\""~s~"\"" ); s = c("Marker \x7f and divide \xf6."); assert( s == "Marker \u2302 and divide \u00f7.", "\""~s~"\"" ); } } __gshared const char[dchar] utf8_to_cp437_map; shared static this() { utf8_to_cp437_map = [ '\u0000': '\x00', '\u263a': '\x01', '\u263b': '\x02', '\u2665': '\x03', '\u2666': '\x04', '\u2663': '\x05', '\u2660': '\x06', '\u2022': '\x07', '\u25d8': '\x08', '\u25cb': '\x09', '\u25d9': '\x0a', '\u2642': '\x0b', '\u2640': '\x0c', '\u266a': '\x0d', '\u266b': '\x0e', '\u263c': '\x0f', '\u25b6': '\x10', '\u25c0': '\x11', '\u2195': '\x12', '\u203c': '\x13', '\u00b6': '\x14', '\u00a7': '\x15', '\u25ac': '\x16', '\u21a8': '\x17', '\u2191': '\x18', '\u2193': '\x19', '\u2192': '\x1a', '\u2190': '\x1b', '\u221f': '\x1c', '\u2194': '\x1d', '\u25b2': '\x1e', '\u25bc': '\x1f', /* * Printable ASCII range (well, most of it) is handled specially. */ '\u00c7': '\x80', '\u00fc': '\x81', '\u00e9': '\x82', '\u00e2': '\x83', '\u00e4': '\x84', '\u00e0': '\x85', '\u00e5': '\x86', '\u00e7': '\x87', '\u00ea': '\x88', '\u00eb': '\x89', '\u00e8': '\x8a', '\u00ef': '\x8b', '\u00ee': '\x8c', '\u00ec': '\x8d', '\u00c4': '\x8e', '\u00c5': '\x8f', '\u00c9': '\x90', '\u00e6': '\x91', '\u00c6': '\x92', '\u00f4': '\x93', '\u00f6': '\x94', '\u00f2': '\x95', '\u00fb': '\x96', '\u00f9': '\x97', '\u00ff': '\x98', '\u00d6': '\x99', '\u00dc': '\x9a', '\u00f8': '\x9b', '\u00a3': '\x9c', '\u00a5': '\x9d', '\u20a7': '\x9e', '\u0192': '\x9f', '\u00e1': '\xa0', '\u00ed': '\xa1', '\u00f3': '\xa2', '\u00fa': '\xa3', '\u00f1': '\xa4', '\u00d1': '\xa5', '\u00aa': '\xa6', '\u00ba': '\xa7', '\u00bf': '\xa8', '\u2310': '\xa9', '\u00ac': '\xaa', '\u00bd': '\xab', '\u00bc': '\xac', '\u00a1': '\xad', '\u00ab': '\xae', '\u00bb': '\xaf', '\u2591': '\xb0', '\u2592': '\xb1', '\u2593': '\xb2', '\u2502': '\xb3', '\u2524': '\xb4', '\u2561': '\xb5', '\u2562': '\xb6', '\u2556': '\xb7', '\u2555': '\xb8', '\u2563': '\xb9', '\u2551': '\xba', '\u2557': '\xbb', '\u255d': '\xbc', '\u255c': '\xbd', '\u255b': '\xbe', '\u2510': '\xbf', '\u2514': '\xc0', '\u2534': '\xc1', '\u252c': '\xc2', '\u251c': '\xc3', '\u2500': '\xc4', '\u253c': '\xc5', '\u255e': '\xc6', '\u255f': '\xc7', '\u255a': '\xc8', '\u2554': '\xc9', '\u2569': '\xca', '\u2566': '\xcb', '\u2560': '\xcc', '\u2550': '\xcd', '\u256c': '\xce', '\u2567': '\xcf', '\u2568': '\xd0', '\u2564': '\xd1', '\u2565': '\xd2', '\u2559': '\xd3', '\u2558': '\xd4', '\u2552': '\xd5', '\u2553': '\xd6', '\u256b': '\xd7', '\u256a': '\xd8', '\u2518': '\xd9', '\u250c': '\xda', '\u2588': '\xdb', '\u2584': '\xdc', '\u258c': '\xdd', '\u2590': '\xde', '\u2580': '\xdf', '\u03b1': '\xe0', '\u00df': '\xe1', '\u0393': '\xe2', '\u03c0': '\xe3', '\u03a3': '\xe4', '\u03c3': '\xe5', '\u00b5': '\xe6', '\u03c4': '\xe7', '\u03a6': '\xe8', '\u0398': '\xe9', '\u03a9': '\xea', '\u03b4': '\xeb', '\u221e': '\xec', '\u03c6': '\xed', '\u03b5': '\xee', '\u2229': '\xef', '\u2261': '\xf0', '\u00b1': '\xf1', '\u2265': '\xf2', '\u2264': '\xf3', '\u2320': '\xf4', '\u2321': '\xf5', '\u00f7': '\xf6', '\u2248': '\xf7', '\u00b0': '\xf8', '\u2219': '\xf9', '\u00b7': '\xfa', '\u221a': '\xfb', '\u207f': '\xfc', '\u00b2': '\xfd', '\u25a0': '\xfe', '\u00a0': '\xff' ]; } inout(ubyte)[] utf8_to_cp437(inout(char)[] s) { alias typeof(return) ret_type; /* Some sort of strange bug here */ ubyte[] bug_6867(const(char)[] cs) { foreach( i,dchar c ; cs ) { if( !((32 <= c && c <= 126) || c == 0) ) { /* We got a character not in CP 437: we need to create a buffer to * hold the new string. Since UTF-8 is *always* larger than CP * 437, we need, at most, an array of the same number of elements. */ auto r = new ubyte[cs.length]; r[0..i] = cast(ubyte[]) cs[0..i]; size_t k=i; foreach( dchar d ; cs[i..$] ) { if( 32 <= d && d <= 126 || d == 0 ) r[k++] = cast(ubyte)d; else if( d == '\u2302' ) r[k++] = '\x7f'; else if( auto e_ptr = d in utf8_to_cp437_map ) r[k++] = *e_ptr; else { throw new Exception("cannot encode character \"" ~ Integer.toString(cast(uint)d).idup ~ "\" in codepage 437."); } } return r[0..k]; } } return null; } auto ret = bug_6867(s); if (ret !is null) return cast(ret_type)ret; // If we got here, then the entire string is printable ASCII, which just // happens to *also* be valid CP 437! Huzzah! return cast(typeof(return)) s; } debug( UnitTest ) { unittest { alias cp437_to_utf8 x; alias utf8_to_cp437 y; ubyte[256] s; foreach( i,ref c ; s ) c = cast(ubyte)i; auto a = x(s); auto b = y(a); if(!( b == s )) { // Display list of characters that failed to convert as expected, // and what value we got. auto hex = "0123456789abcdef"; auto msg = "".dup; foreach( i,ch ; b ) { if( ch != i ) { msg ~= hex[i>>4]; msg ~= hex[i&15]; msg ~= " ("; msg ~= hex[ch>>4]; msg ~= hex[ch&15]; msg ~= "), "; } } msg ~= "failed."; assert( false, msg ); } } } /* * This is here to simplify the code elsewhere. */ inout(char[]) utf8_to_utf8(inout(ubyte[]) s) { return cast(typeof(return)) s; } ubyte[] utf8_to_utf8(char[] s) { return cast(ubyte[]) s; } ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// // // Date/time stuff void dosToTime(ushort dostime, ushort dosdate, out Time time) { uint sec, min, hour, day, mon, year; sec = (dostime & 0b00000_000000_11111) * 2; min = (dostime & 0b00000_111111_00000) >> 5; hour= (dostime & 0b11111_000000_00000) >> 11; day = (dosdate & 0b0000000_0000_11111); mon = (dosdate & 0b0000000_1111_00000) >> 5; year=((dosdate & 0b1111111_0000_00000) >> 9) + 1980; // This code rules! time = Gregorian.generic.toTime(year, mon, day, hour, min, sec); } void timeToDos(Time time, out ushort dostime, out ushort dosdate) { // Treat Time.min specially if( time == Time.min ) time = WallClock.now; // *muttering happily* auto date = Gregorian.generic.toDate(time); if( date.year < 1980 ) ZipException.tooold; auto tod = time.time(); dostime = cast(ushort) ( (tod.seconds / 2) | (tod.minutes << 5) | (tod.hours << 11)); dosdate = cast(ushort) ( (date.day) | (date.month << 5) | ((date.year - 1980) << 9)); } // ************************************************************************** // // ************************************************************************** // // ************************************************************************** // // 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.CounterStream; //import tango.io.device.Conduit : Conduit; //import tango.io.model.IConduit : IConduit, InputStream, OutputStream; /** * The counter stream classes are used to keep track of how many bytes flow * through a stream. * * To use them, simply wrap it around an existing stream. The number of bytes * that have flowed through the wrapped stream may be accessed using the * count member. */ class CounterInput : InputStream { /// this(InputStream input) in { assert( input !is null ); } body { this.source = input; } override IConduit conduit() { return source.conduit; } InputStream input() { return source; } long seek (long ofs, Anchor anchor = Anchor.Begin) { return source.seek (ofs, anchor); } override void close() { source.close(); source = null; } override size_t read(void[] dst) { auto read = source.read(dst); if( read != IConduit.Eof ) _count += read; return read; } override void[] load(size_t max=-1) { return Conduit.load(this, max); } override InputStream flush() { source.flush(); return this; } /// long count() { return _count; } private: InputStream source; long _count; } /// ditto class CounterOutput : OutputStream { /// this(OutputStream output) in { assert( output !is null ); } body { this.sink = output; } override IConduit conduit() { return sink.conduit; } OutputStream output() { return sink; } long seek (long ofs, Anchor anchor = Anchor.Begin) { return sink.seek (ofs, anchor); } override void close() { sink.close(); sink = null; } override size_t write(const(void)[] dst) { auto wrote = sink.write(dst); if( wrote != IConduit.Eof ) _count += wrote; return wrote; } override OutputStream copy(InputStream src, size_t max=-1) { Conduit.transfer(src, this, max); return this; } override OutputStream flush() { sink.flush(); return this; } /// long count() { return _count; } private: OutputStream sink; long _count; } /******************************************************************************* copyright: Copyright © 2007 Daniel Keep. All rights reserved. license: BSD style: $(LICENSE) version: Prerelease author: Daniel Keep *******************************************************************************/ //module tangox.io.stream.SliceStream; //import tango.io.device.Conduit : Conduit; //import tango.io.model.IConduit : IConduit, InputStream, OutputStream; /** * This stream can be used to provide stream-based access to a subset of * another stream. It is akin to slicing an array. * * This stream fully supports seeking, and as such requires that the * underlying stream also support seeking. */ class SliceSeekInputStream : InputStream { //alias IConduit.Seek.Anchor Anchor; /** * Create a new slice stream from the given source, covering the content * starting at position begin, for length bytes. */ this(InputStream source, long begin, long length) in { assert( source !is null ); assert( (cast(IConduit.Seek) source.conduit) !is null ); assert( begin >= 0 ); assert( length >= 0 ); } body { this.source = source; this.seeker = source; //cast(IConduit.Seek) source; this.begin = begin; this.length = length; } override IConduit conduit() { return source.conduit; } override void close() { source = null; seeker = null; } override size_t read(void[] dst) { // If we're at the end of the slice, return eof if( _position >= length ) return IConduit.Eof; // Otherwise, make sure we don't try to read past the end of the slice if( _position+dst.length > length ) dst.length = cast(size_t) (length-_position); // Seek source stream to the appropriate location. if( seeker.seek(0, Anchor.Current) != begin+_position ) seeker.seek(begin+_position, Anchor.Begin); // Do the read auto read = source.read(dst); if( read == IConduit.Eof ) // If we got an Eof, we'll consider that a bug for the moment. // TODO: proper exception throw new Exception("unexpected end-of-stream"); _position += read; return read; } override void[] load(size_t max=-1) { return Conduit.load(this, max); } override InputStream flush() { source.flush(); return this; } InputStream input() { return source; } override long seek(long offset, Anchor anchor = cast(Anchor)0) { switch( anchor ) { case Anchor.Begin: _position = offset; break; case Anchor.Current: _position += offset; if( _position < 0 ) _position = 0; break; case Anchor.End: _position = length+offset; if( _position < 0 ) _position = 0; break; default: assert(false); } return _position; } private: InputStream source; InputStream seeker; long _position, begin, length; invariant() { assert( cast(Object) source is cast(Object) seeker ); assert( begin >= 0 ); assert( length >= 0 ); assert( _position >= 0 ); } } /** * This stream can be used to provide stream-based access to a subset of * another stream. It is akin to slicing an array. */ class SliceInputStream : InputStream { /** * Create a new slice stream from the given source, covering the content * starting at the current seek position for length bytes. */ this(InputStream source, long length) in { assert( source !is null ); assert( length >= 0 ); } body { this.source = source; this._length = length; } override IConduit conduit() { return source.conduit; } override void close() { source = null; } InputStream input() { return source; } long seek (long ofs, Anchor anchor = Anchor.Begin) { return source.seek (ofs, anchor); } override size_t read(void[] dst) { // If we're at the end of the slice, return eof if( _length <= 0 ) return IConduit.Eof; // Otherwise, make sure we don't try to read past the end of the slice if( dst.length > _length ) dst.length = cast(size_t) _length; // Do the read auto read = source.read(dst); if( read == IConduit.Eof ) // If we got an Eof, we'll consider that a bug for the moment. // TODO: proper exception throw new Exception("unexpected end-of-stream"); _length -= read; return read; } override void[] load(size_t max=-1) { return Conduit.load(this, max); } override InputStream flush() { source.flush(); return this; } private: InputStream source; long _length; invariant() { if( _length > 0 ) assert( source !is null ); } } /** * This stream can be used to provide stream-based access to a subset of * another stream. It is akin to slicing an array. * * This stream fully supports seeking, and as such requires that the * underlying stream also support seeking. */ class SliceSeekOutputStream : OutputStream { //alias IConduit.Seek.Anchor Anchor; /** * Create a new slice stream from the given source, covering the content * starting at position begin, for length bytes. */ this(OutputStream source, long begin, long length) in { assert( (cast(IConduit.Seek) source.conduit) !is null ); assert( begin >= 0 ); assert( length >= 0 ); } body { this.source = source; this.seeker = source; //cast(IConduit.Seek) source; this.begin = begin; this.length = length; } override IConduit conduit() { return source.conduit; } override void close() { source = null; seeker = null; } size_t write(const(void)[] src) { // If we're at the end of the slice, return eof if( _position >= length ) return IConduit.Eof; // Otherwise, make sure we don't try to write past the end of the // slice if( _position+src.length > length ) src.length = cast(size_t) (length-_position); // Seek source stream to the appropriate location. if( seeker.seek(0, Anchor.Current) != begin+_position ) seeker.seek(begin+_position, Anchor.Begin); // Do the write auto wrote = source.write(src); if( wrote == IConduit.Eof ) // If we got an Eof, we'll consider that a bug for the moment. // TODO: proper exception throw new Exception("unexpected end-of-stream"); _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 OutputStream output() { return source; } override long seek(long offset, Anchor anchor = cast(Anchor)0) { switch( anchor ) { case Anchor.Begin: _position = offset; break; case Anchor.Current: _position += offset; if( _position < 0 ) _position = 0; break; case Anchor.End: _position = length+offset; if( _position < 0 ) _position = 0; break; default: assert(false); } return _position; } private: OutputStream source; OutputStream seeker; long _position, begin, length; invariant() { assert( cast(Object) source is cast(Object) seeker ); assert( begin >= 0 ); assert( length >= 0 ); assert( _position >= 0 ); } } /******************************************************************************* 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 { //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; } 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 void[] load(size_t max=-1) { return Conduit.load(this, max); } override InputStream flush() { source.flush(); return this; } InputStream input() { return source; } 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; 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 { //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 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 OutputStream output() { return source; } 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; long _position; invariant() { assert( cast(Object) source is cast(Object) seeker ); assert( _position >= 0 ); } } |