1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977 |
|
/*******************************************************************************
*
* copyright: Copyright (c) 2007 Daniel Keep. All rights reserved.
*
* license: BSD style: $(LICENSE)
*
* version: Initial release: December 2007
*
* author: Daniel Keep
*
******************************************************************************/
module tango.io.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 tango.io.FilePath : FilePath, PathView;
import tango.io.device.FileMap : FileMap;
import tango.io.compress.ZlibStream : 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 Path = tango.io.Path;
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
{
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
{
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 );
char[] file_name;
ubyte[] extra_field;
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;
char[] function(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
{
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, char[] path);
void putFile(ZipEntryInfo info, char[] path);
void putStream(ZipEntryInfo info, InputStream source);
void putEntry(ZipEntryInfo info, ZipEntry entry);
void putData(ZipEntryInfo info, 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(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(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, 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, void[] data)
{
//scope mc = new MemoryConduit(data);
scope mc = new Array(data);
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.
*/
Method method() { return _method; }
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;
char[] filename;
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,
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.
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.
char[] comment;
}
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
//
// Exceptions
//
/**
* This is the base class from which all exceptions generated by this module
* derive from.
*/
class ZipException : Exception
{
this(char[] msg) { super(msg); }
private:
alias typeof(this) thisT;
static void opCall(char[] msg) { throw new ZipException(msg); }
static void badsig()
{
thisT("corrupt signature or unexpected section found");
}
static void badsig(char[] type)
{
thisT("corrupt "~type~" signature or unexpected section found");
}
static void incons(char[] name)
{
thisT("inconsistent headers for file \""~name~"\"; "
"archive is likely corrupted");
}
static void missingdir()
{
thisT("could not locate central archive directory; "
"file is corrupt or possibly not a Zip archive");
}
static void toomanyentries()
{
thisT("too many archive entries");
}
static void toolong()
{
thisT("archive is too long; limited to 4GB total");
}
static void cdtoolong()
{
thisT("central directory is too long; limited to 4GB total");
}
static void fntoolong()
{
thisT("file name too long; limited to 65,535 characters");
}
static void eftoolong()
{
thisT("extra field too long; limited to 65,535 characters");
}
static void cotoolong()
{
thisT("extra field too long; limited to 65,535 characters");
}
static void fnencode()
{
thisT("could not encode filename into codepage 437");
}
static void coencode()
{
thisT("could not encode comment into codepage 437");
}
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(char[] name)
{
super("checksum failed on zip entry \""~name~"\"");
}
private:
static void opCall(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(char[] msg) { super(msg); }
private:
alias ZipNotSupportedException thisT;
static void opCall(char[] msg)
{
throw new thisT(msg ~ " 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)
~"."
~Integer.toString(ver % 10)
~" not supported; maximum of version "
~Integer.toString(MAX_EXTRACT_VERSION / 10)
~"."
~Integer.toString(MAX_EXTRACT_VERSION % 10)
~" 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
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(char[] archive, Method method, char[][] files...)
{
scope zw = new ZipBlockWriter(archive);
zw.method = method;
foreach( file ; files )
{
scope fp = new FilePath(file);
ZipEntryInfo zi;
zi.name = file;
zi.modified = fp.modified;
zw.putFile(zi, file);
}
zw.finish;
}
void extractArchive(char[] archive, 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, 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"
];
char[] cp437_to_utf8(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 )
{
char[] repl = cp437_to_utf8_map_low[d];
r[k..k+repl.length] = repl[];
k += repl.length;
}
else if( d == 127 )
{
char[] repl = "\u2302";
r[k..k+repl.length] = repl[];
k += repl.length;
}
else if( d > 127 )
{
char[] repl = cp437_to_utf8_map_high[d-128];
r[k..k+repl.length] = repl[];
k += repl.length;
}
else
assert(false);
}
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(char[]) s;
}
debug( UnitTest )
{
unittest
{
char[] c(char[] s) { return cp437_to_utf8(cast(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~"\"" );
}
}
const char[dchar] utf8_to_cp437_map;
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'
];
}
ubyte[] utf8_to_cp437(char[] s)
{
foreach( i,dchar c ; s )
{
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[s.length];
r[0..i] = cast(ubyte[]) s[0..i];
size_t k=i;
foreach( dchar d ; s[i..$] )
{
if( 32 <= d && d <= 126 || d == 0 )
r[k++] = 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)
~ "\" in codepage 437.");
}
}
return r[0..k];
}
}
// If we got here, then the entire string is printable ASCII, which just
// happens to *also* be valid CP 437! Huzzah!
return cast(ubyte[]) s;
}
debug( UnitTest )
{
unittest
{
alias cp437_to_utf8 x;
alias utf8_to_cp437 y;
ubyte[256] s;
foreach( i,ref c ; s )
c = 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.
*/
char[] utf8_to_utf8(ubyte[] s) { return cast(char[]) 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(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(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(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 );
}
}
|