mirror of
https://github.com/dlang/phobos.git
synced 2025-04-26 13:10:35 +03:00
1710 lines
62 KiB
D
1710 lines
62 KiB
D
// Written in the D programming language.
|
|
|
|
/**
|
|
Read and write data in the
|
|
$(LINK2 https://en.wikipedia.org/wiki/Zip_%28file_format%29, zip archive)
|
|
format.
|
|
|
|
Standards:
|
|
|
|
The current implementation mostly conforms to
|
|
$(LINK2 https://www.iso.org/standard/60101.html, ISO/IEC 21320-1:2015),
|
|
which means,
|
|
$(UL
|
|
$(LI that files can only be stored uncompressed or using the deflate mechanism,)
|
|
$(LI that encryption features are not used,)
|
|
$(LI that digital signature features are not used,)
|
|
$(LI that patched data features are not used, and)
|
|
$(LI that archives may not span multiple volumes.)
|
|
)
|
|
|
|
Additionally, archives are checked for malware attacks and rejected if detected.
|
|
This includes
|
|
$(UL
|
|
$(LI $(LINK2 https://news.ycombinator.com/item?id=20352439, zip bombs) which
|
|
generate gigantic amounts of unpacked data)
|
|
$(LI zip archives that contain overlapping records)
|
|
$(LI chameleon zip archives which generate different unpacked data, depending
|
|
on the implementation of the unpack algorithm)
|
|
)
|
|
|
|
The current implementation makes use of the zlib compression library.
|
|
|
|
Usage:
|
|
|
|
There are two main ways of usage: Extracting files from a zip archive
|
|
and storing files into a zip archive. These can be mixed though (e.g.
|
|
read an archive, remove some files, add others and write the new
|
|
archive).
|
|
|
|
Examples:
|
|
|
|
Example for reading an existing zip archive:
|
|
---
|
|
import std.stdio : writeln, writefln;
|
|
import std.file : read;
|
|
import std.zip;
|
|
|
|
void main(string[] args)
|
|
{
|
|
// read a zip file into memory
|
|
auto zip = new ZipArchive(read(args[1]));
|
|
|
|
// iterate over all zip members
|
|
writefln("%-10s %-8s Name", "Length", "CRC-32");
|
|
foreach (name, am; zip.directory)
|
|
{
|
|
// print some data about each member
|
|
writefln("%10s %08x %s", am.expandedSize, am.crc32, name);
|
|
assert(am.expandedData.length == 0);
|
|
|
|
// decompress the archive member
|
|
zip.expand(am);
|
|
assert(am.expandedData.length == am.expandedSize);
|
|
}
|
|
}
|
|
---
|
|
|
|
Example for writing files into a zip archive:
|
|
---
|
|
import std.file : write;
|
|
import std.string : representation;
|
|
import std.zip;
|
|
|
|
void main()
|
|
{
|
|
// Create an ArchiveMembers for each file.
|
|
ArchiveMember file1 = new ArchiveMember();
|
|
file1.name = "test1.txt";
|
|
file1.expandedData("Test data.\n".dup.representation);
|
|
file1.compressionMethod = CompressionMethod.none; // don't compress
|
|
|
|
ArchiveMember file2 = new ArchiveMember();
|
|
file2.name = "test2.txt";
|
|
file2.expandedData("More test data.\n".dup.representation);
|
|
file2.compressionMethod = CompressionMethod.deflate; // compress
|
|
|
|
// Create an archive and add the member.
|
|
ZipArchive zip = new ZipArchive();
|
|
|
|
// add ArchiveMembers
|
|
zip.addMember(file1);
|
|
zip.addMember(file2);
|
|
|
|
// Build the archive
|
|
void[] compressed_data = zip.build();
|
|
|
|
// Write to a file
|
|
write("test.zip", compressed_data);
|
|
}
|
|
---
|
|
|
|
* Copyright: Copyright The D Language Foundation 2000 - 2009.
|
|
* License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
|
|
* Authors: $(HTTP digitalmars.com, Walter Bright)
|
|
* Source: $(PHOBOSSRC std/zip.d)
|
|
*/
|
|
|
|
/* Copyright The D Language Foundation 2000 - 2009.
|
|
* Distributed under the Boost Software License, Version 1.0.
|
|
* (See accompanying file LICENSE_1_0.txt or copy at
|
|
* http://www.boost.org/LICENSE_1_0.txt)
|
|
*/
|
|
module std.zip;
|
|
|
|
import std.exception : enforce;
|
|
|
|
// Non-Android/Apple ARM POSIX-only, because we can't rely on the unzip
|
|
// command being available on Android, Apple ARM or Windows
|
|
version (Android) {}
|
|
else version (iOS) {}
|
|
else version (TVOS) {}
|
|
else version (WatchOS) {}
|
|
else version (Posix)
|
|
version = HasUnzip;
|
|
|
|
//debug=print;
|
|
|
|
/// Thrown on error.
|
|
class ZipException : Exception
|
|
{
|
|
import std.exception : basicExceptionCtors;
|
|
///
|
|
mixin basicExceptionCtors;
|
|
}
|
|
|
|
/// Compression method used by `ArchiveMember`.
|
|
enum CompressionMethod : ushort
|
|
{
|
|
none = 0, /// No compression, just archiving.
|
|
deflate = 8 /// Deflate algorithm. Use zlib library to compress.
|
|
}
|
|
|
|
/// A single file or directory inside the archive.
|
|
final class ArchiveMember
|
|
{
|
|
import std.conv : to, octal;
|
|
import std.datetime.systime : DosFileTime, SysTime, SysTimeToDosFileTime;
|
|
|
|
/**
|
|
* The name of the archive member; it is used to index the
|
|
* archive directory for the member. Each member must have a
|
|
* unique name. Do not change without removing member from the
|
|
* directory first.
|
|
*/
|
|
string name;
|
|
|
|
/**
|
|
* The content of the extra data field for this member. See
|
|
* $(LINK2 https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT,
|
|
* original documentation)
|
|
* for a description of the general format of this data. May contain
|
|
* undocumented 3rd-party data.
|
|
*/
|
|
ubyte[] extra;
|
|
|
|
string comment; /// Comment associated with this member.
|
|
|
|
private ubyte[] _compressedData;
|
|
private ubyte[] _expandedData;
|
|
private uint offset;
|
|
private uint _crc32;
|
|
private uint _compressedSize;
|
|
private uint _expandedSize;
|
|
private CompressionMethod _compressionMethod;
|
|
private ushort _madeVersion = 20;
|
|
private ushort _extractVersion = 20;
|
|
private uint _externalAttributes;
|
|
private DosFileTime _time;
|
|
// by default, no explicit order goes after explicit order
|
|
private uint _index = uint.max;
|
|
|
|
/**
|
|
* Contains some information on how to extract this archive. See
|
|
* $(LINK2 https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT,
|
|
* original documentation)
|
|
* for details.
|
|
*/
|
|
ushort flags;
|
|
|
|
/**
|
|
* Internal attributes. Bit 1 is set, if the member is apparently in binary format
|
|
* and bit 2 is set, if each record is preceded by the length of the record.
|
|
*/
|
|
ushort internalAttributes;
|
|
|
|
/**
|
|
* The zip file format version needed to extract this member.
|
|
*
|
|
* Returns: Format version needed to extract this member.
|
|
*/
|
|
@property @safe pure nothrow @nogc ushort extractVersion() const { return _extractVersion; }
|
|
|
|
/**
|
|
* Cyclic redundancy check (CRC) value.
|
|
*
|
|
* Returns: CRC32 value.
|
|
*/
|
|
@property @safe pure nothrow @nogc uint crc32() const { return _crc32; }
|
|
|
|
/**
|
|
* Size of data of member in compressed form.
|
|
*
|
|
* Returns: Size of the compressed archive.
|
|
*/
|
|
@property @safe pure nothrow @nogc uint compressedSize() const { return _compressedSize; }
|
|
|
|
/**
|
|
* Size of data of member in uncompressed form.
|
|
*
|
|
* Returns: Size of uncompressed archive.
|
|
*/
|
|
@property @safe pure nothrow @nogc uint expandedSize() const { return _expandedSize; }
|
|
|
|
/**
|
|
* Data of member in compressed form.
|
|
*
|
|
* Returns: The file data in compressed form.
|
|
*/
|
|
@property @safe pure nothrow @nogc ubyte[] compressedData() { return _compressedData; }
|
|
|
|
/**
|
|
* Get or set data of member in uncompressed form. When an existing archive is
|
|
* read `ZipArchive.expand` needs to be called before this can be accessed.
|
|
*
|
|
* Params:
|
|
* ed = Expanded Data.
|
|
*
|
|
* Returns: The file data.
|
|
*/
|
|
@property @safe pure nothrow @nogc ubyte[] expandedData() { return _expandedData; }
|
|
|
|
/// ditto
|
|
@property @safe void expandedData(ubyte[] ed)
|
|
{
|
|
_expandedData = ed;
|
|
_expandedSize = to!uint(_expandedData.length);
|
|
|
|
// Clean old compressed data, if any
|
|
_compressedData.length = 0;
|
|
_compressedSize = 0;
|
|
}
|
|
|
|
/**
|
|
* Get or set the OS specific file attributes for this archive member.
|
|
*
|
|
* Params:
|
|
* attr = Attributes as obtained by $(REF getAttributes, std,file) or
|
|
* $(REF DirEntry.attributes, std,file).
|
|
*
|
|
* Returns: The file attributes or 0 if the file attributes were
|
|
* encoded for an incompatible OS (Windows vs. POSIX).
|
|
*/
|
|
@property @safe void fileAttributes(uint attr)
|
|
{
|
|
version (Posix)
|
|
{
|
|
_externalAttributes = (attr & 0xFFFF) << 16;
|
|
_madeVersion &= 0x00FF;
|
|
_madeVersion |= 0x0300; // attributes are in UNIX format
|
|
}
|
|
else version (Windows)
|
|
{
|
|
_externalAttributes = attr;
|
|
_madeVersion &= 0x00FF; // attributes are in MS-DOS and OS/2 format
|
|
}
|
|
else
|
|
{
|
|
static assert(0, "Unimplemented platform");
|
|
}
|
|
}
|
|
|
|
version (Posix) @safe unittest
|
|
{
|
|
auto am = new ArchiveMember();
|
|
am.fileAttributes = octal!100644;
|
|
assert(am._externalAttributes == octal!100644 << 16);
|
|
assert((am._madeVersion & 0xFF00) == 0x0300);
|
|
}
|
|
|
|
/// ditto
|
|
@property @nogc nothrow uint fileAttributes() const
|
|
{
|
|
version (Posix)
|
|
{
|
|
if ((_madeVersion & 0xFF00) == 0x0300)
|
|
return _externalAttributes >> 16;
|
|
return 0;
|
|
}
|
|
else version (Windows)
|
|
{
|
|
if ((_madeVersion & 0xFF00) == 0x0000)
|
|
return _externalAttributes;
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
static assert(0, "Unimplemented platform");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get or set the last modification time for this member.
|
|
*
|
|
* Params:
|
|
* time = Time to set (will be saved as DosFileTime, which is less accurate).
|
|
*
|
|
* Returns:
|
|
* The last modification time in DosFileFormat.
|
|
*/
|
|
@property DosFileTime time() const @safe pure nothrow @nogc
|
|
{
|
|
return _time;
|
|
}
|
|
|
|
/// ditto
|
|
@property void time(SysTime time)
|
|
{
|
|
_time = SysTimeToDosFileTime(time);
|
|
}
|
|
|
|
/// ditto
|
|
@property void time(DosFileTime time) @safe pure nothrow @nogc
|
|
{
|
|
_time = time;
|
|
}
|
|
|
|
/**
|
|
* Get or set compression method used for this member.
|
|
*
|
|
* Params:
|
|
* cm = Compression method.
|
|
*
|
|
* Returns: Compression method.
|
|
*
|
|
* See_Also:
|
|
* $(LREF CompressionMethod)
|
|
**/
|
|
@property @safe @nogc pure nothrow CompressionMethod compressionMethod() const { return _compressionMethod; }
|
|
|
|
/// ditto
|
|
@property @safe pure void compressionMethod(CompressionMethod cm)
|
|
{
|
|
if (cm == _compressionMethod) return;
|
|
|
|
enforce!ZipException(_compressedSize == 0, "Can't change compression method for a compressed element");
|
|
|
|
_compressionMethod = cm;
|
|
}
|
|
|
|
/**
|
|
* The index of this archive member within the archive. Set this to a
|
|
* different value for reordering the members of an archive.
|
|
*
|
|
* Params:
|
|
* value = Index value to set.
|
|
*
|
|
* Returns: The index.
|
|
*/
|
|
@property uint index(uint value) @safe pure nothrow @nogc { return _index = value; }
|
|
@property uint index() const @safe pure nothrow @nogc { return _index; } /// ditto
|
|
|
|
debug(print)
|
|
{
|
|
void print()
|
|
{
|
|
printf("name = '%.*s'\n", cast(int) name.length, name.ptr);
|
|
printf("\tcomment = '%.*s'\n", cast(int) comment.length, comment.ptr);
|
|
printf("\tmadeVersion = x%04x\n", _madeVersion);
|
|
printf("\textractVersion = x%04x\n", extractVersion);
|
|
printf("\tflags = x%04x\n", flags);
|
|
printf("\tcompressionMethod = %d\n", compressionMethod);
|
|
printf("\ttime = %d\n", time);
|
|
printf("\tcrc32 = x%08x\n", crc32);
|
|
printf("\texpandedSize = %d\n", expandedSize);
|
|
printf("\tcompressedSize = %d\n", compressedSize);
|
|
printf("\tinternalAttributes = x%04x\n", internalAttributes);
|
|
printf("\texternalAttributes = x%08x\n", externalAttributes);
|
|
printf("\tindex = x%08x\n", index);
|
|
}
|
|
}
|
|
}
|
|
|
|
@safe pure unittest
|
|
{
|
|
import std.exception : assertThrown, assertNotThrown;
|
|
|
|
auto am = new ArchiveMember();
|
|
|
|
assertNotThrown(am.compressionMethod(CompressionMethod.deflate));
|
|
assertNotThrown(am.compressionMethod(CompressionMethod.none));
|
|
|
|
am._compressedData = [0x65]; // not strictly necessary, but for consistency
|
|
am._compressedSize = 1;
|
|
|
|
assertThrown!ZipException(am.compressionMethod(CompressionMethod.deflate));
|
|
}
|
|
|
|
/**
|
|
* Object representing the entire archive.
|
|
* ZipArchives are collections of ArchiveMembers.
|
|
*/
|
|
final class ZipArchive
|
|
{
|
|
import std.algorithm.comparison : max;
|
|
import std.bitmanip : littleEndianToNative, nativeToLittleEndian;
|
|
import std.conv : to;
|
|
import std.datetime.systime : DosFileTime;
|
|
|
|
private:
|
|
// names are taken directly from the specification
|
|
// https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
|
|
static immutable ubyte[] centralFileHeaderSignature = [ 0x50, 0x4b, 0x01, 0x02 ];
|
|
static immutable ubyte[] localFileHeaderSignature = [ 0x50, 0x4b, 0x03, 0x04 ];
|
|
static immutable ubyte[] endOfCentralDirSignature = [ 0x50, 0x4b, 0x05, 0x06 ];
|
|
static immutable ubyte[] archiveExtraDataSignature = [ 0x50, 0x4b, 0x06, 0x08 ];
|
|
static immutable ubyte[] digitalSignatureSignature = [ 0x50, 0x4b, 0x05, 0x05 ];
|
|
static immutable ubyte[] zip64EndOfCentralDirSignature = [ 0x50, 0x4b, 0x06, 0x06 ];
|
|
static immutable ubyte[] zip64EndOfCentralDirLocatorSignature = [ 0x50, 0x4b, 0x06, 0x07 ];
|
|
|
|
enum centralFileHeaderLength = 46;
|
|
enum localFileHeaderLength = 30;
|
|
enum endOfCentralDirLength = 22;
|
|
enum archiveExtraDataLength = 8;
|
|
enum digitalSignatureLength = 6;
|
|
enum zip64EndOfCentralDirLength = 56;
|
|
enum zip64EndOfCentralDirLocatorLength = 20;
|
|
enum dataDescriptorLength = 12;
|
|
|
|
public:
|
|
string comment; /// The archive comment. Must be less than 65536 bytes in length.
|
|
|
|
private ubyte[] _data;
|
|
|
|
private bool _isZip64;
|
|
static const ushort zip64ExtractVersion = 45;
|
|
|
|
private Segment[] _segs;
|
|
|
|
/**
|
|
* Array representing the entire contents of the archive.
|
|
*
|
|
* Returns: Data of the entire contents of the archive.
|
|
*/
|
|
@property @safe @nogc pure nothrow ubyte[] data() { return _data; }
|
|
|
|
/**
|
|
* Number of ArchiveMembers in the directory.
|
|
*
|
|
* Returns: The number of files in this archive.
|
|
*/
|
|
@property @safe @nogc pure nothrow uint totalEntries() const { return cast(uint) _directory.length; }
|
|
|
|
/**
|
|
* True when the archive is in Zip64 format. Set this to true to force building a Zip64 archive.
|
|
*
|
|
* Params:
|
|
* value = True, when the archive is forced to be build in Zip64 format.
|
|
*
|
|
* Returns: True, when the archive is in Zip64 format.
|
|
*/
|
|
@property @safe @nogc pure nothrow bool isZip64() const { return _isZip64; }
|
|
|
|
/// ditto
|
|
@property @safe @nogc pure nothrow void isZip64(bool value) { _isZip64 = value; }
|
|
|
|
/**
|
|
* Associative array indexed by the name of each member of the archive.
|
|
*
|
|
* All the members of the archive can be accessed with a foreach loop:
|
|
*
|
|
* Example:
|
|
* --------------------
|
|
* ZipArchive archive = new ZipArchive(data);
|
|
* foreach (ArchiveMember am; archive.directory)
|
|
* {
|
|
* writefln("member name is '%s'", am.name);
|
|
* }
|
|
* --------------------
|
|
*
|
|
* Returns: Associative array with all archive members.
|
|
*/
|
|
@property @safe @nogc pure nothrow ArchiveMember[string] directory() { return _directory; }
|
|
|
|
private ArchiveMember[string] _directory;
|
|
|
|
debug (print)
|
|
{
|
|
@safe void print()
|
|
{
|
|
printf("\tdiskNumber = %u\n", diskNumber);
|
|
printf("\tdiskStartDir = %u\n", diskStartDir);
|
|
printf("\tnumEntries = %u\n", numEntries);
|
|
printf("\ttotalEntries = %u\n", totalEntries);
|
|
printf("\tcomment = '%.*s'\n", cast(int) comment.length, comment.ptr);
|
|
}
|
|
}
|
|
|
|
/* ============ Creating a new archive =================== */
|
|
|
|
/**
|
|
* Constructor to use when creating a new archive.
|
|
*/
|
|
this() @safe @nogc pure nothrow
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Add a member to the archive. The file is compressed on the fly.
|
|
*
|
|
* Params:
|
|
* de = Member to be added.
|
|
*
|
|
* Throws: ZipException when an unsupported compression method is used or when
|
|
* compression failed.
|
|
*/
|
|
@safe void addMember(ArchiveMember de)
|
|
{
|
|
_directory[de.name] = de;
|
|
if (!de._compressedData.length)
|
|
{
|
|
switch (de.compressionMethod)
|
|
{
|
|
case CompressionMethod.none:
|
|
de._compressedData = de._expandedData;
|
|
break;
|
|
|
|
case CompressionMethod.deflate:
|
|
import std.zlib : compress;
|
|
() @trusted
|
|
{
|
|
de._compressedData = cast(ubyte[]) compress(cast(void[]) de._expandedData);
|
|
}();
|
|
de._compressedData = de._compressedData[2 .. de._compressedData.length - 4];
|
|
break;
|
|
|
|
default:
|
|
throw new ZipException("unsupported compression method");
|
|
}
|
|
|
|
de._compressedSize = to!uint(de._compressedData.length);
|
|
import std.zlib : crc32;
|
|
() @trusted { de._crc32 = crc32(0, cast(void[]) de._expandedData); }();
|
|
}
|
|
assert(de._compressedData.length == de._compressedSize, "Archive member compressed failed.");
|
|
}
|
|
|
|
@safe unittest
|
|
{
|
|
import std.exception : assertThrown;
|
|
|
|
ArchiveMember am = new ArchiveMember();
|
|
am.compressionMethod = cast(CompressionMethod) 3;
|
|
|
|
ZipArchive zip = new ZipArchive();
|
|
|
|
assertThrown!ZipException(zip.addMember(am));
|
|
}
|
|
|
|
/**
|
|
* Delete member `de` from the archive. Uses the name of the member
|
|
* to detect which element to delete.
|
|
*
|
|
* Params:
|
|
* de = Member to be deleted.
|
|
*/
|
|
@safe void deleteMember(ArchiveMember de)
|
|
{
|
|
_directory.remove(de.name);
|
|
}
|
|
|
|
// https://issues.dlang.org/show_bug.cgi?id=20398
|
|
@safe unittest
|
|
{
|
|
import std.string : representation;
|
|
|
|
ArchiveMember file1 = new ArchiveMember();
|
|
file1.name = "test1.txt";
|
|
file1.expandedData("Test data.\n".dup.representation);
|
|
|
|
ZipArchive zip = new ZipArchive();
|
|
|
|
zip.addMember(file1);
|
|
assert(zip.totalEntries == 1);
|
|
|
|
zip.deleteMember(file1);
|
|
assert(zip.totalEntries == 0);
|
|
}
|
|
|
|
/**
|
|
* Construct the entire contents of the current members of the archive.
|
|
*
|
|
* Fills in the properties data[], totalEntries, and directory[].
|
|
* For each ArchiveMember, fills in properties crc32, compressedSize,
|
|
* compressedData[].
|
|
*
|
|
* Returns: Array representing the entire archive.
|
|
*
|
|
* Throws: ZipException when the archive could not be build.
|
|
*/
|
|
void[] build() @safe pure
|
|
{
|
|
import std.array : array, uninitializedArray;
|
|
import std.algorithm.sorting : sort;
|
|
import std.string : representation;
|
|
|
|
uint i;
|
|
uint directoryOffset;
|
|
|
|
enforce!ZipException(comment.length <= 0xFFFF, "archive comment longer than 65535");
|
|
|
|
// Compress each member; compute size
|
|
uint archiveSize = 0;
|
|
uint directorySize = 0;
|
|
auto directory = _directory.byValue.array.sort!((x, y) => x.index < y.index).release;
|
|
foreach (ArchiveMember de; directory)
|
|
{
|
|
enforce!ZipException(to!ulong(archiveSize) + localFileHeaderLength + de.name.length
|
|
+ de.extra.length + de.compressedSize + directorySize
|
|
+ centralFileHeaderLength + de.name.length + de.extra.length
|
|
+ de.comment.length + endOfCentralDirLength + comment.length
|
|
+ zip64EndOfCentralDirLocatorLength + zip64EndOfCentralDirLength <= uint.max,
|
|
"zip files bigger than 4 GB are unsupported");
|
|
|
|
archiveSize += localFileHeaderLength + de.name.length +
|
|
de.extra.length +
|
|
de.compressedSize;
|
|
directorySize += centralFileHeaderLength + de.name.length +
|
|
de.extra.length +
|
|
de.comment.length;
|
|
}
|
|
|
|
if (!isZip64 && _directory.length > ushort.max)
|
|
_isZip64 = true;
|
|
uint dataSize = archiveSize + directorySize + endOfCentralDirLength + cast(uint) comment.length;
|
|
if (isZip64)
|
|
dataSize += zip64EndOfCentralDirLocatorLength + zip64EndOfCentralDirLength;
|
|
|
|
_data = uninitializedArray!(ubyte[])(dataSize);
|
|
|
|
// Populate the data[]
|
|
|
|
// Store each archive member
|
|
i = 0;
|
|
foreach (ArchiveMember de; directory)
|
|
{
|
|
de.offset = i;
|
|
_data[i .. i + 4] = localFileHeaderSignature;
|
|
putUshort(i + 4, de.extractVersion);
|
|
putUshort(i + 6, de.flags);
|
|
putUshort(i + 8, de._compressionMethod);
|
|
putUint (i + 10, cast(uint) de.time);
|
|
putUint (i + 14, de.crc32);
|
|
putUint (i + 18, de.compressedSize);
|
|
putUint (i + 22, to!uint(de.expandedSize));
|
|
putUshort(i + 26, cast(ushort) de.name.length);
|
|
putUshort(i + 28, cast(ushort) de.extra.length);
|
|
i += localFileHeaderLength;
|
|
|
|
_data[i .. i + de.name.length] = (de.name.representation)[];
|
|
i += de.name.length;
|
|
_data[i .. i + de.extra.length] = (cast(ubyte[]) de.extra)[];
|
|
i += de.extra.length;
|
|
_data[i .. i + de.compressedSize] = de.compressedData[];
|
|
i += de.compressedSize;
|
|
}
|
|
|
|
// Write directory
|
|
directoryOffset = i;
|
|
foreach (ArchiveMember de; directory)
|
|
{
|
|
_data[i .. i + 4] = centralFileHeaderSignature;
|
|
putUshort(i + 4, de._madeVersion);
|
|
putUshort(i + 6, de.extractVersion);
|
|
putUshort(i + 8, de.flags);
|
|
putUshort(i + 10, de._compressionMethod);
|
|
putUint (i + 12, cast(uint) de.time);
|
|
putUint (i + 16, de.crc32);
|
|
putUint (i + 20, de.compressedSize);
|
|
putUint (i + 24, de.expandedSize);
|
|
putUshort(i + 28, cast(ushort) de.name.length);
|
|
putUshort(i + 30, cast(ushort) de.extra.length);
|
|
putUshort(i + 32, cast(ushort) de.comment.length);
|
|
putUshort(i + 34, cast(ushort) 0);
|
|
putUshort(i + 36, de.internalAttributes);
|
|
putUint (i + 38, de._externalAttributes);
|
|
putUint (i + 42, de.offset);
|
|
i += centralFileHeaderLength;
|
|
|
|
_data[i .. i + de.name.length] = (de.name.representation)[];
|
|
i += de.name.length;
|
|
_data[i .. i + de.extra.length] = (cast(ubyte[]) de.extra)[];
|
|
i += de.extra.length;
|
|
_data[i .. i + de.comment.length] = (de.comment.representation)[];
|
|
i += de.comment.length;
|
|
}
|
|
|
|
if (isZip64)
|
|
{
|
|
// Write zip64 end of central directory record
|
|
uint eocd64Offset = i;
|
|
_data[i .. i + 4] = zip64EndOfCentralDirSignature;
|
|
putUlong (i + 4, zip64EndOfCentralDirLength - 12);
|
|
putUshort(i + 12, zip64ExtractVersion);
|
|
putUshort(i + 14, zip64ExtractVersion);
|
|
putUint (i + 16, cast(ushort) 0);
|
|
putUint (i + 20, cast(ushort) 0);
|
|
putUlong (i + 24, directory.length);
|
|
putUlong (i + 32, directory.length);
|
|
putUlong (i + 40, directorySize);
|
|
putUlong (i + 48, directoryOffset);
|
|
i += zip64EndOfCentralDirLength;
|
|
|
|
// Write zip64 end of central directory record locator
|
|
_data[i .. i + 4] = zip64EndOfCentralDirLocatorSignature;
|
|
putUint (i + 4, cast(ushort) 0);
|
|
putUlong (i + 8, eocd64Offset);
|
|
putUint (i + 16, 1);
|
|
i += zip64EndOfCentralDirLocatorLength;
|
|
}
|
|
|
|
// Write end record
|
|
_data[i .. i + 4] = endOfCentralDirSignature;
|
|
putUshort(i + 4, cast(ushort) 0);
|
|
putUshort(i + 6, cast(ushort) 0);
|
|
putUshort(i + 8, (totalEntries > ushort.max ? ushort.max : cast(ushort) totalEntries));
|
|
putUshort(i + 10, (totalEntries > ushort.max ? ushort.max : cast(ushort) totalEntries));
|
|
putUint (i + 12, directorySize);
|
|
putUint (i + 16, directoryOffset);
|
|
putUshort(i + 20, cast(ushort) comment.length);
|
|
i += endOfCentralDirLength;
|
|
|
|
// Write archive comment
|
|
assert(i + comment.length == data.length, "Writing the archive comment failed.");
|
|
_data[i .. data.length] = (comment.representation)[];
|
|
|
|
return cast(void[]) data;
|
|
}
|
|
|
|
@safe pure unittest
|
|
{
|
|
import std.exception : assertNotThrown;
|
|
|
|
ZipArchive zip = new ZipArchive();
|
|
zip.comment = "A";
|
|
assertNotThrown(zip.build());
|
|
}
|
|
|
|
@safe pure unittest
|
|
{
|
|
import std.range : repeat, array;
|
|
import std.exception : assertThrown;
|
|
|
|
ZipArchive zip = new ZipArchive();
|
|
zip.comment = 'A'.repeat(70_000).array;
|
|
assertThrown!ZipException(zip.build());
|
|
}
|
|
|
|
/* ============ Reading an existing archive =================== */
|
|
|
|
/**
|
|
* Constructor to use when reading an existing archive.
|
|
*
|
|
* Fills in the properties data[], totalEntries, comment[], and directory[].
|
|
* For each ArchiveMember, fills in
|
|
* properties madeVersion, extractVersion, flags, compressionMethod, time,
|
|
* crc32, compressedSize, expandedSize, compressedData[],
|
|
* internalAttributes, externalAttributes, name[], extra[], comment[].
|
|
* Use expand() to get the expanded data for each ArchiveMember.
|
|
*
|
|
* Params:
|
|
* buffer = The entire contents of the archive.
|
|
*
|
|
* Throws: ZipException when the archive was invalid or when malware was detected.
|
|
*/
|
|
this(void[] buffer)
|
|
{
|
|
this._data = cast(ubyte[]) buffer;
|
|
|
|
enforce!ZipException(data.length <= uint.max - 2, "zip files bigger than 4 GB are unsupported");
|
|
|
|
_segs = [Segment(0, cast(uint) data.length)];
|
|
|
|
uint i = findEndOfCentralDirRecord();
|
|
|
|
int endCommentLength = getUshort(i + 20);
|
|
comment = cast(string)(_data[i + endOfCentralDirLength .. i + endOfCentralDirLength + endCommentLength]);
|
|
|
|
// end of central dir record
|
|
removeSegment(i, i + endOfCentralDirLength + endCommentLength);
|
|
|
|
uint k = i - zip64EndOfCentralDirLocatorLength;
|
|
if (k < i && _data[k .. k + 4] == zip64EndOfCentralDirLocatorSignature)
|
|
{
|
|
_isZip64 = true;
|
|
i = k;
|
|
|
|
// zip64 end of central dir record locator
|
|
removeSegment(k, k + zip64EndOfCentralDirLocatorLength);
|
|
}
|
|
|
|
uint directorySize;
|
|
uint directoryOffset;
|
|
uint directoryCount;
|
|
|
|
if (isZip64)
|
|
{
|
|
// Read Zip64 record data
|
|
ulong eocdOffset = getUlong(i + 8);
|
|
enforce!ZipException(eocdOffset + zip64EndOfCentralDirLength <= _data.length,
|
|
"corrupted directory");
|
|
|
|
i = to!uint(eocdOffset);
|
|
enforce!ZipException(_data[i .. i + 4] == zip64EndOfCentralDirSignature,
|
|
"invalid Zip EOCD64 signature");
|
|
|
|
ulong eocd64Size = getUlong(i + 4);
|
|
enforce!ZipException(eocd64Size + i - 12 <= data.length,
|
|
"invalid Zip EOCD64 size");
|
|
|
|
// zip64 end of central dir record
|
|
removeSegment(i, cast(uint) (i + 12 + eocd64Size));
|
|
|
|
ulong numEntriesUlong = getUlong(i + 24);
|
|
ulong totalEntriesUlong = getUlong(i + 32);
|
|
ulong directorySizeUlong = getUlong(i + 40);
|
|
ulong directoryOffsetUlong = getUlong(i + 48);
|
|
|
|
enforce!ZipException(numEntriesUlong <= uint.max,
|
|
"supposedly more than 4294967296 files in archive");
|
|
|
|
enforce!ZipException(numEntriesUlong == totalEntriesUlong,
|
|
"multiple disk zips not supported");
|
|
|
|
enforce!ZipException(directorySizeUlong <= i && directoryOffsetUlong <= i
|
|
&& directorySizeUlong + directoryOffsetUlong <= i,
|
|
"corrupted directory");
|
|
|
|
directoryCount = to!uint(totalEntriesUlong);
|
|
directorySize = to!uint(directorySizeUlong);
|
|
directoryOffset = to!uint(directoryOffsetUlong);
|
|
}
|
|
else
|
|
{
|
|
// Read end record data
|
|
directoryCount = getUshort(i + 10);
|
|
directorySize = getUint(i + 12);
|
|
directoryOffset = getUint(i + 16);
|
|
}
|
|
|
|
i = directoryOffset;
|
|
for (int n = 0; n < directoryCount; n++)
|
|
{
|
|
/* The format of an entry is:
|
|
* 'PK' 1, 2
|
|
* directory info
|
|
* path
|
|
* extra data
|
|
* comment
|
|
*/
|
|
|
|
uint namelen;
|
|
uint extralen;
|
|
uint commentlen;
|
|
|
|
enforce!ZipException(_data[i .. i + 4] == centralFileHeaderSignature,
|
|
"wrong central file header signature found");
|
|
ArchiveMember de = new ArchiveMember();
|
|
de._index = n;
|
|
de._madeVersion = getUshort(i + 4);
|
|
de._extractVersion = getUshort(i + 6);
|
|
de.flags = getUshort(i + 8);
|
|
de._compressionMethod = cast(CompressionMethod) getUshort(i + 10);
|
|
de.time = cast(DosFileTime) getUint(i + 12);
|
|
de._crc32 = getUint(i + 16);
|
|
de._compressedSize = getUint(i + 20);
|
|
de._expandedSize = getUint(i + 24);
|
|
namelen = getUshort(i + 28);
|
|
extralen = getUshort(i + 30);
|
|
commentlen = getUshort(i + 32);
|
|
de.internalAttributes = getUshort(i + 36);
|
|
de._externalAttributes = getUint(i + 38);
|
|
de.offset = getUint(i + 42);
|
|
|
|
// central file header
|
|
removeSegment(i, i + centralFileHeaderLength + namelen + extralen + commentlen);
|
|
|
|
i += centralFileHeaderLength;
|
|
|
|
enforce!ZipException(i + namelen + extralen + commentlen <= directoryOffset + directorySize,
|
|
"invalid field lengths in file header found");
|
|
|
|
de.name = cast(string)(_data[i .. i + namelen]);
|
|
i += namelen;
|
|
de.extra = _data[i .. i + extralen];
|
|
i += extralen;
|
|
de.comment = cast(string)(_data[i .. i + commentlen]);
|
|
i += commentlen;
|
|
|
|
auto localFileHeaderNamelen = getUshort(de.offset + 26);
|
|
auto localFileHeaderExtralen = getUshort(de.offset + 28);
|
|
|
|
// file data
|
|
removeSegment(de.offset, de.offset + localFileHeaderLength + localFileHeaderNamelen
|
|
+ localFileHeaderExtralen + de._compressedSize);
|
|
|
|
immutable uint dataOffset = de.offset + localFileHeaderLength
|
|
+ localFileHeaderNamelen + localFileHeaderExtralen;
|
|
de._compressedData = _data[dataOffset .. dataOffset + de.compressedSize];
|
|
|
|
_directory[de.name] = de;
|
|
}
|
|
|
|
enforce!ZipException(i == directoryOffset + directorySize, "invalid directory entry 3");
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
import std.exception : assertThrown;
|
|
|
|
// contains wrong directorySize (extra byte 0xff)
|
|
auto file =
|
|
"\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~
|
|
"\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~
|
|
"\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
|
|
"\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
|
|
"\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
|
|
"\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
|
|
"\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
|
|
"\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
|
|
"\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\xff\x50\x4b\x05"~
|
|
"\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4b\x00\x00\x00\x43\x00\x00"~
|
|
"\x00\x00\x00";
|
|
|
|
assertThrown!ZipException(new ZipArchive(cast(void[]) file));
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
import std.exception : assertThrown;
|
|
|
|
// wrong eocdOffset
|
|
auto file =
|
|
"\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
|
|
"\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
|
|
"\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
|
|
"\x00\x00";
|
|
|
|
assertThrown!ZipException(new ZipArchive(cast(void[]) file));
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
import std.exception : assertThrown;
|
|
|
|
// wrong signature of zip64 end of central directory
|
|
auto file =
|
|
"\x50\x4b\x06\x07\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
|
|
"\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
|
|
"\x00\x00";
|
|
|
|
assertThrown!ZipException(new ZipArchive(cast(void[]) file));
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
import std.exception : assertThrown;
|
|
|
|
// wrong size of zip64 end of central directory
|
|
auto file =
|
|
"\x50\x4b\x06\x06\xff\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
|
|
"\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
|
|
"\x00\x00";
|
|
|
|
assertThrown!ZipException(new ZipArchive(cast(void[]) file));
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
import std.exception : assertThrown;
|
|
|
|
// too many entries in zip64 end of central directory
|
|
auto file =
|
|
"\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
|
|
"\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
|
|
"\x00\x00";
|
|
|
|
assertThrown!ZipException(new ZipArchive(cast(void[]) file));
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
import std.exception : assertThrown;
|
|
|
|
// zip64: numEntries and totalEntries differ
|
|
auto file =
|
|
"\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
|
|
"\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
|
|
"\x00\x00";
|
|
|
|
assertThrown!ZipException(new ZipArchive(cast(void[]) file));
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
import std.exception : assertThrown;
|
|
|
|
// zip64: directorySize too large
|
|
auto file =
|
|
"\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
|
|
"\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
|
|
"\x00\x00";
|
|
|
|
assertThrown!ZipException(new ZipArchive(cast(void[]) file));
|
|
|
|
// zip64: directoryOffset too large
|
|
file =
|
|
"\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\xff\xff\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
|
|
"\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
|
|
"\x00\x00";
|
|
|
|
assertThrown!ZipException(new ZipArchive(cast(void[]) file));
|
|
|
|
// zip64: directorySize + directoryOffset too large
|
|
// we need to add a useless byte at the beginning to avoid that one of the other two checks allready fires
|
|
file =
|
|
"\x00\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x01\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
|
|
"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
|
|
"\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
|
|
"\x00\x00";
|
|
|
|
assertThrown!ZipException(new ZipArchive(cast(void[]) file));
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
import std.exception : assertThrown;
|
|
|
|
// wrong central file header signature
|
|
auto file =
|
|
"\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~
|
|
"\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~
|
|
"\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
|
|
"\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
|
|
"\x6c\x6c\x6f\x50\x4b\x01\x03\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
|
|
"\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
|
|
"\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
|
|
"\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
|
|
"\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~
|
|
"\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~
|
|
"\x00\x00\x00";
|
|
|
|
assertThrown!ZipException(new ZipArchive(cast(void[]) file));
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
import std.exception : assertThrown;
|
|
|
|
// invalid field lengths in file header
|
|
auto file =
|
|
"\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~
|
|
"\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~
|
|
"\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
|
|
"\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
|
|
"\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
|
|
"\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
|
|
"\x00\x18\x00\x01\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
|
|
"\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
|
|
"\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\xff\x50\x4b\x05"~
|
|
"\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~
|
|
"\x00\x00\x00";
|
|
|
|
assertThrown!ZipException(new ZipArchive(cast(void[]) file));
|
|
}
|
|
|
|
private uint findEndOfCentralDirRecord()
|
|
{
|
|
// end of central dir record can be followed by a comment of up to 2^^16-1 bytes
|
|
// therefore we have to scan 2^^16 positions
|
|
|
|
uint endrecOffset = to!uint(data.length);
|
|
foreach (i; 0 .. 2 ^^ 16)
|
|
{
|
|
if (endOfCentralDirLength + i > data.length) break;
|
|
uint start = to!uint(data.length) - endOfCentralDirLength - i;
|
|
|
|
if (data[start .. start + 4] != endOfCentralDirSignature) continue;
|
|
|
|
auto numberOfThisDisc = getUshort(start + 4);
|
|
if (numberOfThisDisc != 0) continue; // no support for multiple volumes yet
|
|
|
|
auto numberOfStartOfCentralDirectory = getUshort(start + 6);
|
|
if (numberOfStartOfCentralDirectory != 0) continue; // dito
|
|
|
|
if (numberOfThisDisc < numberOfStartOfCentralDirectory) continue;
|
|
|
|
uint k = start - zip64EndOfCentralDirLocatorLength;
|
|
auto maybeZip64 = k < start && _data[k .. k + 4] == zip64EndOfCentralDirLocatorSignature;
|
|
|
|
auto totalNumberOfEntriesOnThisDisk = getUshort(start + 8);
|
|
auto totalNumberOfEntriesInCentralDir = getUshort(start + 10);
|
|
|
|
if (totalNumberOfEntriesOnThisDisk > totalNumberOfEntriesInCentralDir &&
|
|
(!maybeZip64 || totalNumberOfEntriesOnThisDisk < 0xffff)) continue;
|
|
|
|
auto sizeOfCentralDirectory = getUint(start + 12);
|
|
if (sizeOfCentralDirectory > start &&
|
|
(!maybeZip64 || sizeOfCentralDirectory < 0xffff)) continue;
|
|
|
|
auto offsetOfCentralDirectory = getUint(start + 16);
|
|
if (offsetOfCentralDirectory > start - sizeOfCentralDirectory &&
|
|
(!maybeZip64 || offsetOfCentralDirectory < 0xffff)) continue;
|
|
|
|
auto zipfileCommentLength = getUshort(start + 20);
|
|
if (start + zipfileCommentLength + endOfCentralDirLength != data.length) continue;
|
|
|
|
enforce!ZipException(endrecOffset == to!uint(data.length),
|
|
"found more than one valid 'end of central dir record'");
|
|
|
|
endrecOffset = start;
|
|
}
|
|
|
|
enforce!ZipException(endrecOffset != to!uint(data.length),
|
|
"found no valid 'end of central dir record'");
|
|
|
|
return endrecOffset;
|
|
}
|
|
|
|
/**
|
|
* Decompress the contents of a member.
|
|
*
|
|
* Fills in properties extractVersion, flags, compressionMethod, time,
|
|
* crc32, compressedSize, expandedSize, expandedData[], name[], extra[].
|
|
*
|
|
* Params:
|
|
* de = Member to be decompressed.
|
|
*
|
|
* Returns: The expanded data.
|
|
*
|
|
* Throws: ZipException when the entry is invalid or the compression method is not supported.
|
|
*/
|
|
ubyte[] expand(ArchiveMember de)
|
|
{
|
|
import std.string : representation;
|
|
|
|
uint namelen;
|
|
uint extralen;
|
|
|
|
enforce!ZipException(_data[de.offset .. de.offset + 4] == localFileHeaderSignature,
|
|
"wrong local file header signature found");
|
|
|
|
// These values should match what is in the main zip archive directory
|
|
de._extractVersion = getUshort(de.offset + 4);
|
|
de.flags = getUshort(de.offset + 6);
|
|
de._compressionMethod = cast(CompressionMethod) getUshort(de.offset + 8);
|
|
de.time = cast(DosFileTime) getUint(de.offset + 10);
|
|
de._crc32 = getUint(de.offset + 14);
|
|
de._compressedSize = max(getUint(de.offset + 18), de.compressedSize);
|
|
de._expandedSize = max(getUint(de.offset + 22), de.expandedSize);
|
|
namelen = getUshort(de.offset + 26);
|
|
extralen = getUshort(de.offset + 28);
|
|
|
|
debug(print)
|
|
{
|
|
printf("\t\texpandedSize = %d\n", de.expandedSize);
|
|
printf("\t\tcompressedSize = %d\n", de.compressedSize);
|
|
printf("\t\tnamelen = %d\n", namelen);
|
|
printf("\t\textralen = %d\n", extralen);
|
|
}
|
|
|
|
enforce!ZipException((de.flags & 1) == 0, "encryption not supported");
|
|
|
|
switch (de.compressionMethod)
|
|
{
|
|
case CompressionMethod.none:
|
|
de._expandedData = de.compressedData;
|
|
return de.expandedData;
|
|
|
|
case CompressionMethod.deflate:
|
|
// -15 is a magic value used to decompress zip files.
|
|
// It has the effect of not requiring the 2 byte header
|
|
// and 4 byte trailer.
|
|
import std.zlib : uncompress;
|
|
de._expandedData = cast(ubyte[]) uncompress(cast(void[]) de.compressedData, de.expandedSize, -15);
|
|
return de.expandedData;
|
|
|
|
default:
|
|
throw new ZipException("unsupported compression method");
|
|
}
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
import std.exception : assertThrown;
|
|
|
|
// check for correct local file header signature
|
|
auto file =
|
|
"\x50\x4b\x04\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~
|
|
"\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~
|
|
"\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
|
|
"\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
|
|
"\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
|
|
"\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
|
|
"\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
|
|
"\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
|
|
"\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~
|
|
"\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~
|
|
"\x00\x00\x00";
|
|
|
|
auto za = new ZipArchive(cast(void[]) file);
|
|
|
|
assertThrown!ZipException(za.expand(za._directory["file"]));
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
import std.exception : assertThrown;
|
|
|
|
// check for encryption flag
|
|
auto file =
|
|
"\x50\x4b\x03\x04\x0a\x00\x01\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~
|
|
"\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~
|
|
"\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
|
|
"\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
|
|
"\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
|
|
"\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
|
|
"\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
|
|
"\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
|
|
"\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~
|
|
"\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~
|
|
"\x00\x00\x00";
|
|
|
|
auto za = new ZipArchive(cast(void[]) file);
|
|
|
|
assertThrown!ZipException(za.expand(za._directory["file"]));
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
import std.exception : assertThrown;
|
|
|
|
// check for invalid compression method
|
|
auto file =
|
|
"\x50\x4b\x03\x04\x0a\x00\x00\x00\x03\x00\x8f\x72\x4a\x4f\x86\xa6"~
|
|
"\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~
|
|
"\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
|
|
"\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
|
|
"\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
|
|
"\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
|
|
"\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
|
|
"\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
|
|
"\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~
|
|
"\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~
|
|
"\x00\x00\x00";
|
|
|
|
auto za = new ZipArchive(cast(void[]) file);
|
|
|
|
assertThrown!ZipException(za.expand(za._directory["file"]));
|
|
}
|
|
|
|
/* ============ Utility =================== */
|
|
|
|
@safe @nogc pure nothrow ushort getUshort(uint i)
|
|
{
|
|
ubyte[2] result = data[i .. i + 2];
|
|
return littleEndianToNative!ushort(result);
|
|
}
|
|
|
|
@safe @nogc pure nothrow uint getUint(uint i)
|
|
{
|
|
ubyte[4] result = data[i .. i + 4];
|
|
return littleEndianToNative!uint(result);
|
|
}
|
|
|
|
@safe @nogc pure nothrow ulong getUlong(uint i)
|
|
{
|
|
ubyte[8] result = data[i .. i + 8];
|
|
return littleEndianToNative!ulong(result);
|
|
}
|
|
|
|
@safe @nogc pure nothrow void putUshort(uint i, ushort us)
|
|
{
|
|
data[i .. i + 2] = nativeToLittleEndian(us);
|
|
}
|
|
|
|
@safe @nogc pure nothrow void putUint(uint i, uint ui)
|
|
{
|
|
data[i .. i + 4] = nativeToLittleEndian(ui);
|
|
}
|
|
|
|
@safe @nogc pure nothrow void putUlong(uint i, ulong ul)
|
|
{
|
|
data[i .. i + 8] = nativeToLittleEndian(ul);
|
|
}
|
|
|
|
/* ============== for detecting overlaps =============== */
|
|
|
|
private:
|
|
|
|
// defines a segment of the zip file, including start, excluding end
|
|
struct Segment
|
|
{
|
|
uint start;
|
|
uint end;
|
|
}
|
|
|
|
// removes Segment start .. end from _segs
|
|
// throws zipException if start .. end is not completely available in _segs;
|
|
void removeSegment(uint start, uint end) pure @safe
|
|
in (start < end, "segment invalid")
|
|
{
|
|
auto found = false;
|
|
size_t pos;
|
|
foreach (i,seg;_segs)
|
|
if (seg.start <= start && seg.end >= end
|
|
&& (!found || seg.start > _segs[pos].start))
|
|
{
|
|
found = true;
|
|
pos = i;
|
|
}
|
|
|
|
enforce!ZipException(found, "overlapping data detected");
|
|
|
|
if (start>_segs[pos].start)
|
|
_segs ~= Segment(_segs[pos].start, start);
|
|
if (end<_segs[pos].end)
|
|
_segs ~= Segment(end, _segs[pos].end);
|
|
_segs = _segs[0 .. pos] ~ _segs[pos + 1 .. $];
|
|
}
|
|
|
|
pure @safe unittest
|
|
{
|
|
with (new ZipArchive())
|
|
{
|
|
_segs = [Segment(0,100)];
|
|
removeSegment(10,20);
|
|
assert(_segs == [Segment(0,10),Segment(20,100)]);
|
|
|
|
_segs = [Segment(0,100)];
|
|
removeSegment(0,20);
|
|
assert(_segs == [Segment(20,100)]);
|
|
|
|
_segs = [Segment(0,100)];
|
|
removeSegment(10,100);
|
|
assert(_segs == [Segment(0,10)]);
|
|
|
|
_segs = [Segment(0,100), Segment(200,300), Segment(400,500)];
|
|
removeSegment(220,230);
|
|
assert(_segs == [Segment(0,100),Segment(400,500),Segment(200,220),Segment(230,300)]);
|
|
|
|
_segs = [Segment(200,300), Segment(0,100), Segment(400,500)];
|
|
removeSegment(20,30);
|
|
assert(_segs == [Segment(200,300),Segment(400,500),Segment(0,20),Segment(30,100)]);
|
|
|
|
import std.exception : assertThrown;
|
|
|
|
_segs = [Segment(0,100), Segment(200,300), Segment(400,500)];
|
|
assertThrown(removeSegment(120,230));
|
|
|
|
_segs = [Segment(0,100), Segment(200,300), Segment(400,500)];
|
|
removeSegment(0,100);
|
|
assertThrown(removeSegment(0,100));
|
|
|
|
_segs = [Segment(0,100)];
|
|
removeSegment(0,100);
|
|
assertThrown(removeSegment(0,100));
|
|
}
|
|
}
|
|
}
|
|
|
|
debug(print)
|
|
{
|
|
@safe void arrayPrint(ubyte[] array)
|
|
{
|
|
printf("array %p,%d\n", cast(void*) array, array.length);
|
|
for (int i = 0; i < array.length; i++)
|
|
{
|
|
printf("%02x ", array[i]);
|
|
if (((i + 1) & 15) == 0)
|
|
printf("\n");
|
|
}
|
|
printf("\n");
|
|
}
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
// @system due to (at least) ZipArchive.build
|
|
auto zip1 = new ZipArchive();
|
|
auto zip2 = new ZipArchive();
|
|
auto am1 = new ArchiveMember();
|
|
am1.name = "foo";
|
|
am1.expandedData = new ubyte[](1024);
|
|
zip1.addMember(am1);
|
|
auto data1 = zip1.build();
|
|
zip2.addMember(zip1.directory["foo"]);
|
|
zip2.build();
|
|
auto am2 = zip2.directory["foo"];
|
|
zip2.expand(am2);
|
|
assert(am1.expandedData == am2.expandedData);
|
|
auto zip3 = new ZipArchive(data1);
|
|
zip3.build();
|
|
assert(zip3.directory["foo"].compressedSize == am1.compressedSize);
|
|
|
|
// Test if packing and unpacking produces the original data
|
|
import std.conv, std.stdio;
|
|
import std.random : uniform, MinstdRand0;
|
|
MinstdRand0 gen;
|
|
const uint itemCount = 20, minSize = 10, maxSize = 500;
|
|
foreach (variant; 0 .. 2)
|
|
{
|
|
bool useZip64 = !!variant;
|
|
zip1 = new ZipArchive();
|
|
zip1.isZip64 = useZip64;
|
|
ArchiveMember[itemCount] ams;
|
|
foreach (i; 0 .. itemCount)
|
|
{
|
|
ams[i] = new ArchiveMember();
|
|
ams[i].name = to!string(i);
|
|
ams[i].expandedData = new ubyte[](uniform(minSize, maxSize));
|
|
foreach (ref ubyte c; ams[i].expandedData)
|
|
c = cast(ubyte)(uniform(0, 256));
|
|
ams[i].compressionMethod = CompressionMethod.deflate;
|
|
zip1.addMember(ams[i]);
|
|
}
|
|
auto zippedData = zip1.build();
|
|
zip2 = new ZipArchive(zippedData);
|
|
assert(zip2.isZip64 == useZip64);
|
|
foreach (am; ams)
|
|
{
|
|
am2 = zip2.directory[am.name];
|
|
zip2.expand(am2);
|
|
assert(am.crc32 == am2.crc32);
|
|
assert(am.expandedData == am2.expandedData);
|
|
}
|
|
}
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
import std.conv : to;
|
|
import std.random : Mt19937, randomShuffle;
|
|
// Test if packing and unpacking preserves order.
|
|
auto rand = Mt19937(15966);
|
|
string[] names;
|
|
int value = 0;
|
|
// Generate a series of unique numbers as filenames.
|
|
foreach (i; 0 .. 20)
|
|
{
|
|
value += 1 + rand.front & 0xFFFF;
|
|
rand.popFront;
|
|
names ~= value.to!string;
|
|
}
|
|
// Insert them in a random order.
|
|
names.randomShuffle(rand);
|
|
auto zip1 = new ZipArchive();
|
|
foreach (i, name; names)
|
|
{
|
|
auto member = new ArchiveMember();
|
|
member.name = name;
|
|
member.expandedData = cast(ubyte[]) name;
|
|
member.index = cast(int) i;
|
|
zip1.addMember(member);
|
|
}
|
|
auto data = zip1.build();
|
|
|
|
// Ensure that they appear in the same order.
|
|
auto zip2 = new ZipArchive(data);
|
|
foreach (i, name; names)
|
|
{
|
|
const member = zip2.directory[name];
|
|
assert(member.index == i, "member " ~ name ~ " had index " ~
|
|
member.index.to!string ~ " but we expected index " ~ i.to!string ~
|
|
". The input array was " ~ names.to!string);
|
|
}
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
import std.zlib;
|
|
|
|
ubyte[] src = cast(ubyte[])
|
|
"the quick brown fox jumps over the lazy dog\r
|
|
the quick brown fox jumps over the lazy dog\r
|
|
";
|
|
auto dst = cast(ubyte[]) compress(cast(void[]) src);
|
|
auto after = cast(ubyte[]) uncompress(cast(void[]) dst);
|
|
assert(src == after);
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
// @system due to ZipArchive.build
|
|
import std.datetime;
|
|
ubyte[] buf = [1, 2, 3, 4, 5, 0, 7, 8, 9];
|
|
|
|
auto ar = new ZipArchive;
|
|
auto am = new ArchiveMember; // 10
|
|
am.name = "buf";
|
|
am.expandedData = buf;
|
|
am.compressionMethod = CompressionMethod.deflate;
|
|
am.time = SysTimeToDosFileTime(Clock.currTime());
|
|
ar.addMember(am); // 15
|
|
|
|
auto zip1 = ar.build();
|
|
auto arAfter = new ZipArchive(zip1);
|
|
assert(arAfter.directory.length == 1);
|
|
auto amAfter = arAfter.directory["buf"];
|
|
arAfter.expand(amAfter);
|
|
assert(amAfter.name == am.name);
|
|
assert(amAfter.expandedData == am.expandedData);
|
|
assert(amAfter.time == am.time);
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
// invalid format of end of central directory entry
|
|
import std.exception : assertThrown;
|
|
assertThrown!ZipException(new ZipArchive(cast(void[]) "\x50\x4B\x05\x06aaaaaaaaaaaaaaaaaaaa"));
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
// minimum (empty) archive should pass
|
|
auto za = new ZipArchive(cast(void[]) "\x50\x4B\x05\x06\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00");
|
|
assert(za.directory.length == 0);
|
|
|
|
// one byte too short or too long should not pass
|
|
import std.exception : assertThrown;
|
|
assertThrown!ZipException(new ZipArchive(cast(void[]) "\x50\x4B\x05\x06\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"));
|
|
assertThrown!ZipException(new ZipArchive(cast(void[]) "\x50\x4B\x05\x06\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"));
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
// https://issues.dlang.org/show_bug.cgi?id=20239
|
|
// chameleon file, containing two valid end of central directory entries
|
|
auto file =
|
|
"\x50\x4B\x03\x04\x0A\x00\x00\x00\x00\x00\x89\x36\x39\x4F\x04\x6A\xB3\xA3\x01\x00"~
|
|
"\x00\x00\x01\x00\x00\x00\x0D\x00\x1C\x00\x62\x65\x73\x74\x5F\x6C\x61\x6E\x67\x75"~
|
|
"\x61\x67\x65\x55\x54\x09\x00\x03\x82\xF2\x8A\x5D\x82\xF2\x8A\x5D\x75\x78\x0B\x00"~
|
|
"\x01\x04\xEB\x03\x00\x00\x04\xEB\x03\x00\x00\x44\x50\x4B\x01\x02\x1E\x03\x0A\x00"~
|
|
"\x00\x00\x00\x00\x89\x36\x39\x4F\x04\x6A\xB3\xA3\x01\x00\x00\x00\x01\x00\x00\x00"~
|
|
"\x0D\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xB0\x81\x00\x00\x00\x00\x62\x65"~
|
|
"\x73\x74\x5F\x6C\x61\x6E\x67\x75\x61\x67\x65\x55\x54\x05\x00\x03\x82\xF2\x8A\x5D"~
|
|
"\x75\x78\x0B\x00\x01\x04\xEB\x03\x00\x00\x04\xEB\x03\x00\x00\x50\x4B\x05\x06\x00"~
|
|
"\x00\x00\x00\x01\x00\x01\x00\x53\x00\x00\x00\x48\x00\x00\x00\xB7\x00\x50\x4B\x03"~
|
|
"\x04\x0A\x00\x00\x00\x00\x00\x94\x36\x39\x4F\xD7\xCB\x3B\x55\x07\x00\x00\x00\x07"~
|
|
"\x00\x00\x00\x0D\x00\x1C\x00\x62\x65\x73\x74\x5F\x6C\x61\x6E\x67\x75\x61\x67\x65"~
|
|
"\x55\x54\x09\x00\x03\x97\xF2\x8A\x5D\x8C\xF2\x8A\x5D\x75\x78\x0B\x00\x01\x04\xEB"~
|
|
"\x03\x00\x00\x04\xEB\x03\x00\x00\x46\x4F\x52\x54\x52\x41\x4E\x50\x4B\x01\x02\x1E"~
|
|
"\x03\x0A\x00\x00\x00\x00\x00\x94\x36\x39\x4F\xD7\xCB\x3B\x55\x07\x00\x00\x00\x07"~
|
|
"\x00\x00\x00\x0D\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xB0\x81\xB1\x00\x00"~
|
|
"\x00\x62\x65\x73\x74\x5F\x6C\x61\x6E\x67\x75\x61\x67\x65\x55\x54\x05\x00\x03\x97"~
|
|
"\xF2\x8A\x5D\x75\x78\x0B\x00\x01\x04\xEB\x03\x00\x00\x04\xEB\x03\x00\x00\x50\x4B"~
|
|
"\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00\x53\x00\x00\x00\xFF\x00\x00\x00\x00\x00";
|
|
|
|
import std.exception : assertThrown;
|
|
assertThrown!ZipException(new ZipArchive(cast(void[]) file));
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
// https://issues.dlang.org/show_bug.cgi?id=20287
|
|
// check for correct compressed data
|
|
auto file =
|
|
"\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~
|
|
"\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~
|
|
"\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
|
|
"\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
|
|
"\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
|
|
"\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
|
|
"\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
|
|
"\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
|
|
"\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~
|
|
"\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~
|
|
"\x00\x00\x00";
|
|
|
|
auto za = new ZipArchive(cast(void[]) file);
|
|
assert(za.directory["file"].compressedData == [104, 101, 108, 108, 111]);
|
|
}
|
|
|
|
// https://issues.dlang.org/show_bug.cgi?id=20027
|
|
@system unittest
|
|
{
|
|
// central file header overlaps end of central directory
|
|
auto file =
|
|
// lfh
|
|
"\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~
|
|
"\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1c\x00\x66\x69"~
|
|
"\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
|
|
"\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
|
|
"\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
|
|
"\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
|
|
"\x00\x18\x00\x04\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
|
|
"\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
|
|
"\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~
|
|
"\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~
|
|
"\x00\x00\x00";
|
|
|
|
import std.exception : assertThrown;
|
|
assertThrown!ZipException(new ZipArchive(cast(void[]) file));
|
|
|
|
// local file header and file data overlap second local file header and file data
|
|
file =
|
|
"\x50\x4b\x03\x04\x0a\x00\x00\x00\x00\x00\x8f\x72\x4a\x4f\x86\xa6"~
|
|
"\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04\x00\x1e\x00\x66\x69"~
|
|
"\x6c\x65\x55\x54\x09\x00\x03\x0d\x22\x9f\x5d\x12\x22\x9f\x5d\x75"~
|
|
"\x78\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x68\x65"~
|
|
"\x6c\x6c\x6f\x50\x4b\x01\x02\x1e\x03\x0a\x00\x00\x00\x00\x00\x8f"~
|
|
"\x72\x4a\x4f\x86\xa6\x10\x36\x05\x00\x00\x00\x05\x00\x00\x00\x04"~
|
|
"\x00\x18\x00\x04\x00\x00\x00\x01\x00\x00\x00\xb0\x81\x00\x00\x00"~
|
|
"\x00\x66\x69\x6c\x65\x55\x54\x05\x00\x03\x0d\x22\x9f\x5d\x75\x78"~
|
|
"\x0b\x00\x01\x04\xf0\x03\x00\x00\x04\xf0\x03\x00\x00\x50\x4b\x05"~
|
|
"\x06\x00\x00\x00\x00\x01\x00\x01\x00\x4a\x00\x00\x00\x43\x00\x00"~
|
|
"\x00\x00\x00";
|
|
|
|
assertThrown!ZipException(new ZipArchive(cast(void[]) file));
|
|
}
|
|
|
|
@system unittest
|
|
{
|
|
// https://issues.dlang.org/show_bug.cgi?id=20295
|
|
// zip64 with 0xff bytes in end of central dir record do not work
|
|
// minimum (empty zip64) archive should pass
|
|
auto file =
|
|
"\x50\x4b\x06\x06\x2c\x00\x00\x00\x00\x00\x00\x00\x1e\x03\x2d\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x06\x07\x00\x00\x00\x00"~
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x50\x4B\x05\x06"~
|
|
"\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"~
|
|
"\x00\x00";
|
|
|
|
auto za = new ZipArchive(cast(void[]) file);
|
|
assert(za.directory.length == 0);
|
|
}
|
|
|
|
version (HasUnzip)
|
|
@system unittest
|
|
{
|
|
import std.datetime, std.file, std.format, std.path, std.process, std.stdio;
|
|
|
|
if (executeShell("unzip").status != 0)
|
|
{
|
|
writeln("Can't run unzip, skipping unzip test");
|
|
return;
|
|
}
|
|
|
|
auto zr = new ZipArchive();
|
|
auto am = new ArchiveMember();
|
|
am.compressionMethod = CompressionMethod.deflate;
|
|
am.name = "foo.bar";
|
|
am.time = SysTimeToDosFileTime(Clock.currTime());
|
|
am.expandedData = cast(ubyte[])"We all live in a yellow submarine, a yellow submarine";
|
|
zr.addMember(am);
|
|
auto data2 = zr.build();
|
|
|
|
mkdirRecurse(deleteme);
|
|
scope(exit) rmdirRecurse(deleteme);
|
|
string zipFile = buildPath(deleteme, "foo.zip");
|
|
std.file.write(zipFile, cast(byte[]) data2);
|
|
|
|
auto result = executeShell(format("unzip -l %s", zipFile));
|
|
scope(failure) writeln(result.output);
|
|
assert(result.status == 0);
|
|
}
|