2583 lines
111 KiB
C#
2583 lines
111 KiB
C#
//#define Trace
|
|
|
|
// ZipEntry.Write.cs
|
|
// ------------------------------------------------------------------
|
|
//
|
|
// Copyright (c) 2009-2011 Dino Chiesa
|
|
// All rights reserved.
|
|
//
|
|
// This code module is part of DotNetZip, a zipfile class library.
|
|
//
|
|
// ------------------------------------------------------------------
|
|
//
|
|
// This code is licensed under the Microsoft Public License.
|
|
// See the file License.txt for the license details.
|
|
// More info on: http://dotnetzip.codeplex.com
|
|
//
|
|
// ------------------------------------------------------------------
|
|
//
|
|
// Last Saved: <2011-July-30 14:55:47>
|
|
//
|
|
// ------------------------------------------------------------------
|
|
//
|
|
// This module defines logic for writing (saving) the ZipEntry into a
|
|
// zip file.
|
|
//
|
|
// ------------------------------------------------------------------
|
|
|
|
|
|
using System;
|
|
using System.IO;
|
|
using RE = System.Text.RegularExpressions;
|
|
|
|
namespace Ionic.Zip
|
|
{
|
|
public partial class ZipEntry
|
|
{
|
|
internal void WriteCentralDirectoryEntry(Stream s)
|
|
{
|
|
byte[] bytes = new byte[4096];
|
|
int i = 0;
|
|
// signature
|
|
bytes[i++] = (byte)(ZipConstants.ZipDirEntrySignature & 0x000000FF);
|
|
bytes[i++] = (byte)((ZipConstants.ZipDirEntrySignature & 0x0000FF00) >> 8);
|
|
bytes[i++] = (byte)((ZipConstants.ZipDirEntrySignature & 0x00FF0000) >> 16);
|
|
bytes[i++] = (byte)((ZipConstants.ZipDirEntrySignature & 0xFF000000) >> 24);
|
|
|
|
// Version Made By
|
|
// workitem 7071
|
|
// We must not overwrite the VersionMadeBy field when writing out a zip
|
|
// archive. The VersionMadeBy tells the zip reader the meaning of the
|
|
// File attributes. Overwriting the VersionMadeBy will result in
|
|
// inconsistent metadata. Consider the scenario where the application
|
|
// opens and reads a zip file that had been created on Linux. Then the
|
|
// app adds one file to the Zip archive, and saves it. The file
|
|
// attributes for all the entries added on Linux will be significant for
|
|
// Linux. Therefore the VersionMadeBy for those entries must not be
|
|
// changed. Only the entries that are actually created on Windows NTFS
|
|
// should get the VersionMadeBy indicating Windows/NTFS.
|
|
bytes[i++] = (byte)(_VersionMadeBy & 0x00FF);
|
|
bytes[i++] = (byte)((_VersionMadeBy & 0xFF00) >> 8);
|
|
|
|
// Apparently we want to duplicate the extra field here; we cannot
|
|
// simply zero it out and assume tools and apps will use the right one.
|
|
|
|
////Int16 extraFieldLengthSave = (short)(_EntryHeader[28] + _EntryHeader[29] * 256);
|
|
////_EntryHeader[28] = 0;
|
|
////_EntryHeader[29] = 0;
|
|
|
|
// Version Needed, Bitfield, compression method, lastmod,
|
|
// crc, compressed and uncompressed sizes, filename length and extra field length.
|
|
// These are all present in the local file header, but they may be zero values there.
|
|
// So we cannot just copy them.
|
|
|
|
// workitem 11969: Version Needed To Extract in central directory must be
|
|
// the same as the local entry or MS .NET System.IO.Zip fails read.
|
|
Int16 vNeeded = (Int16)(VersionNeeded != 0 ? VersionNeeded : 20);
|
|
// workitem 12964
|
|
if (_OutputUsesZip64==null)
|
|
{
|
|
// a zipentry in a zipoutputstream, with zero bytes written
|
|
_OutputUsesZip64 = new Nullable<bool>(_container.Zip64 == Zip64Option.Always);
|
|
}
|
|
|
|
Int16 versionNeededToExtract = (Int16)(_OutputUsesZip64.Value ? 45 : vNeeded);
|
|
#if BZIP
|
|
if (this.CompressionMethod == Ionic.Zip.CompressionMethod.BZip2)
|
|
versionNeededToExtract = 46;
|
|
#endif
|
|
|
|
bytes[i++] = (byte)(versionNeededToExtract & 0x00FF);
|
|
bytes[i++] = (byte)((versionNeededToExtract & 0xFF00) >> 8);
|
|
|
|
bytes[i++] = (byte)(_BitField & 0x00FF);
|
|
bytes[i++] = (byte)((_BitField & 0xFF00) >> 8);
|
|
|
|
bytes[i++] = (byte)(_CompressionMethod & 0x00FF);
|
|
bytes[i++] = (byte)((_CompressionMethod & 0xFF00) >> 8);
|
|
|
|
#if AESCRYPTO
|
|
if (Encryption == EncryptionAlgorithm.WinZipAes128 ||
|
|
Encryption == EncryptionAlgorithm.WinZipAes256)
|
|
{
|
|
i -= 2;
|
|
bytes[i++] = 0x63;
|
|
bytes[i++] = 0;
|
|
}
|
|
#endif
|
|
|
|
bytes[i++] = (byte)(_TimeBlob & 0x000000FF);
|
|
bytes[i++] = (byte)((_TimeBlob & 0x0000FF00) >> 8);
|
|
bytes[i++] = (byte)((_TimeBlob & 0x00FF0000) >> 16);
|
|
bytes[i++] = (byte)((_TimeBlob & 0xFF000000) >> 24);
|
|
bytes[i++] = (byte)(_Crc32 & 0x000000FF);
|
|
bytes[i++] = (byte)((_Crc32 & 0x0000FF00) >> 8);
|
|
bytes[i++] = (byte)((_Crc32 & 0x00FF0000) >> 16);
|
|
bytes[i++] = (byte)((_Crc32 & 0xFF000000) >> 24);
|
|
|
|
int j = 0;
|
|
if (_OutputUsesZip64.Value)
|
|
{
|
|
// CompressedSize (Int32) and UncompressedSize - all 0xFF
|
|
for (j = 0; j < 8; j++)
|
|
bytes[i++] = 0xFF;
|
|
}
|
|
else
|
|
{
|
|
bytes[i++] = (byte)(_CompressedSize & 0x000000FF);
|
|
bytes[i++] = (byte)((_CompressedSize & 0x0000FF00) >> 8);
|
|
bytes[i++] = (byte)((_CompressedSize & 0x00FF0000) >> 16);
|
|
bytes[i++] = (byte)((_CompressedSize & 0xFF000000) >> 24);
|
|
|
|
bytes[i++] = (byte)(_UncompressedSize & 0x000000FF);
|
|
bytes[i++] = (byte)((_UncompressedSize & 0x0000FF00) >> 8);
|
|
bytes[i++] = (byte)((_UncompressedSize & 0x00FF0000) >> 16);
|
|
bytes[i++] = (byte)((_UncompressedSize & 0xFF000000) >> 24);
|
|
}
|
|
|
|
byte[] fileNameBytes = GetEncodedFileNameBytes();
|
|
Int16 filenameLength = (Int16)fileNameBytes.Length;
|
|
bytes[i++] = (byte)(filenameLength & 0x00FF);
|
|
bytes[i++] = (byte)((filenameLength & 0xFF00) >> 8);
|
|
|
|
// do this again because now we have real data
|
|
_presumeZip64 = _OutputUsesZip64.Value;
|
|
|
|
// workitem 11131
|
|
//
|
|
// cannot generate the extra field again, here's why: In the case of a
|
|
// zero-byte entry, which uses encryption, DotNetZip will "remove" the
|
|
// encryption from the entry. It does this in PostProcessOutput; it
|
|
// modifies the entry header, and rewrites it, resetting the Bitfield
|
|
// (one bit indicates encryption), and potentially resetting the
|
|
// compression method - for AES the Compression method is 0x63, and it
|
|
// would get reset to zero (no compression). It then calls SetLength()
|
|
// to truncate the stream to remove the encryption header (12 bytes for
|
|
// AES256). But, it leaves the previously-generated "Extra Field"
|
|
// metadata (11 bytes) for AES in the entry header. This extra field
|
|
// data is now "orphaned" - it refers to AES encryption when in fact no
|
|
// AES encryption is used. But no problem, the PKWARE spec says that
|
|
// unrecognized extra fields can just be ignored. ok. After "removal"
|
|
// of AES encryption, the length of the Extra Field can remains the
|
|
// same; it's just that there will be 11 bytes in there that previously
|
|
// pertained to AES which are now unused. Even the field code is still
|
|
// there, but it will be unused by readers, as the encryption bit is not
|
|
// set.
|
|
//
|
|
// Re-calculating the Extra field now would produce a block that is 11
|
|
// bytes shorter, and that mismatch - between the extra field in the
|
|
// local header and the extra field in the Central Directory - would
|
|
// cause problems. (where? why? what problems?) So we can't do
|
|
// that. It's all good though, because though the content may have
|
|
// changed, the length definitely has not. Also, the _EntryHeader
|
|
// contains the "updated" extra field (after PostProcessOutput) at
|
|
// offset (30 + filenameLength).
|
|
|
|
_Extra = ConstructExtraField(true);
|
|
|
|
Int16 extraFieldLength = (Int16)((_Extra == null) ? 0 : _Extra.Length);
|
|
bytes[i++] = (byte)(extraFieldLength & 0x00FF);
|
|
bytes[i++] = (byte)((extraFieldLength & 0xFF00) >> 8);
|
|
|
|
// File (entry) Comment Length
|
|
// the _CommentBytes private field was set during WriteHeader()
|
|
int commentLength = (_CommentBytes == null) ? 0 : _CommentBytes.Length;
|
|
|
|
// the size of our buffer defines the max length of the comment we can write
|
|
if (commentLength + i > bytes.Length) commentLength = bytes.Length - i;
|
|
bytes[i++] = (byte)(commentLength & 0x00FF);
|
|
bytes[i++] = (byte)((commentLength & 0xFF00) >> 8);
|
|
|
|
// Disk number start
|
|
bool segmented = (this._container.ZipFile != null) &&
|
|
(this._container.ZipFile.MaxOutputSegmentSize != 0);
|
|
if (segmented) // workitem 13915
|
|
{
|
|
// Emit nonzero disknumber only if saving segmented archive.
|
|
bytes[i++] = (byte)(_diskNumber & 0x00FF);
|
|
bytes[i++] = (byte)((_diskNumber & 0xFF00) >> 8);
|
|
}
|
|
else
|
|
{
|
|
// If reading a segmneted archive and saving to a regular archive,
|
|
// ZipEntry._diskNumber will be non-zero but it should be saved as
|
|
// zero.
|
|
bytes[i++] = 0;
|
|
bytes[i++] = 0;
|
|
}
|
|
|
|
// internal file attrs
|
|
// workitem 7801
|
|
bytes[i++] = (byte)((_IsText) ? 1 : 0); // lo bit: filetype hint. 0=bin, 1=txt.
|
|
bytes[i++] = 0;
|
|
|
|
// external file attrs
|
|
// workitem 7071
|
|
bytes[i++] = (byte)(_ExternalFileAttrs & 0x000000FF);
|
|
bytes[i++] = (byte)((_ExternalFileAttrs & 0x0000FF00) >> 8);
|
|
bytes[i++] = (byte)((_ExternalFileAttrs & 0x00FF0000) >> 16);
|
|
bytes[i++] = (byte)((_ExternalFileAttrs & 0xFF000000) >> 24);
|
|
|
|
// workitem 11131
|
|
// relative offset of local header.
|
|
//
|
|
// If necessary to go to 64-bit value, then emit 0xFFFFFFFF,
|
|
// else write out the value.
|
|
//
|
|
// Even if zip64 is required for other reasons - number of the entry
|
|
// > 65534, or uncompressed size of the entry > MAX_INT32, the ROLH
|
|
// need not be stored in a 64-bit field .
|
|
if (_RelativeOffsetOfLocalHeader > 0xFFFFFFFFL) // _OutputUsesZip64.Value
|
|
{
|
|
bytes[i++] = 0xFF;
|
|
bytes[i++] = 0xFF;
|
|
bytes[i++] = 0xFF;
|
|
bytes[i++] = 0xFF;
|
|
}
|
|
else
|
|
{
|
|
bytes[i++] = (byte)(_RelativeOffsetOfLocalHeader & 0x000000FF);
|
|
bytes[i++] = (byte)((_RelativeOffsetOfLocalHeader & 0x0000FF00) >> 8);
|
|
bytes[i++] = (byte)((_RelativeOffsetOfLocalHeader & 0x00FF0000) >> 16);
|
|
bytes[i++] = (byte)((_RelativeOffsetOfLocalHeader & 0xFF000000) >> 24);
|
|
}
|
|
|
|
// actual filename
|
|
Buffer.BlockCopy(fileNameBytes, 0, bytes, i, filenameLength);
|
|
i += filenameLength;
|
|
|
|
// "Extra field"
|
|
if (_Extra != null)
|
|
{
|
|
// workitem 11131
|
|
//
|
|
// copy from EntryHeader if available - it may have been updated.
|
|
// if not, copy from Extra. This would be unnecessary if I just
|
|
// updated the Extra field when updating EntryHeader, in
|
|
// PostProcessOutput.
|
|
|
|
//?? I don't understand why I wouldn't want to just use
|
|
// the recalculated Extra field. ??
|
|
|
|
// byte[] h = _EntryHeader ?? _Extra;
|
|
// int offx = (h == _EntryHeader) ? 30 + filenameLength : 0;
|
|
// Buffer.BlockCopy(h, offx, bytes, i, extraFieldLength);
|
|
// i += extraFieldLength;
|
|
|
|
byte[] h = _Extra;
|
|
int offx = 0;
|
|
Buffer.BlockCopy(h, offx, bytes, i, extraFieldLength);
|
|
i += extraFieldLength;
|
|
}
|
|
|
|
// file (entry) comment
|
|
if (commentLength != 0)
|
|
{
|
|
// now actually write the comment itself into the byte buffer
|
|
Buffer.BlockCopy(_CommentBytes, 0, bytes, i, commentLength);
|
|
// for (j = 0; (j < commentLength) && (i + j < bytes.Length); j++)
|
|
// bytes[i + j] = _CommentBytes[j];
|
|
i += commentLength;
|
|
}
|
|
|
|
s.Write(bytes, 0, i);
|
|
}
|
|
|
|
|
|
#if INFOZIP_UTF8
|
|
static private bool FileNameIsUtf8(char[] FileNameChars)
|
|
{
|
|
bool isUTF8 = false;
|
|
bool isUnicode = false;
|
|
for (int j = 0; j < FileNameChars.Length; j++)
|
|
{
|
|
byte[] b = System.BitConverter.GetBytes(FileNameChars[j]);
|
|
isUnicode |= (b.Length != 2);
|
|
isUnicode |= (b[1] != 0);
|
|
isUTF8 |= ((b[0] & 0x80) != 0);
|
|
}
|
|
|
|
return isUTF8;
|
|
}
|
|
#endif
|
|
|
|
|
|
private byte[] ConstructExtraField(bool forCentralDirectory)
|
|
{
|
|
var listOfBlocks = new System.Collections.Generic.List<byte[]>();
|
|
byte[] block;
|
|
|
|
// Conditionally emit an extra field with Zip64 information. If the
|
|
// Zip64 option is Always, we emit the field, before knowing that it's
|
|
// necessary. Later, if it turns out this entry does not need zip64,
|
|
// we'll set the header ID to rubbish and the data will be ignored.
|
|
// This results in additional overhead metadata in the zip file, but
|
|
// it will be small in comparison to the entry data.
|
|
//
|
|
// On the other hand if the Zip64 option is AsNecessary and it's NOT
|
|
// for the central directory, then we do the same thing. Or, if the
|
|
// Zip64 option is AsNecessary and it IS for the central directory,
|
|
// and the entry requires zip64, then emit the header.
|
|
if (_container.Zip64 == Zip64Option.Always ||
|
|
(_container.Zip64 == Zip64Option.AsNecessary &&
|
|
(!forCentralDirectory || _entryRequiresZip64.Value)))
|
|
{
|
|
// add extra field for zip64 here
|
|
// workitem 7924
|
|
int sz = 4 + (forCentralDirectory ? 28 : 16);
|
|
block = new byte[sz];
|
|
int i = 0;
|
|
|
|
if (_presumeZip64 || forCentralDirectory)
|
|
{
|
|
// HeaderId = always use zip64 extensions.
|
|
block[i++] = 0x01;
|
|
block[i++] = 0x00;
|
|
}
|
|
else
|
|
{
|
|
// HeaderId = dummy data now, maybe set to 0x0001 (ZIP64) later.
|
|
block[i++] = 0x99;
|
|
block[i++] = 0x99;
|
|
}
|
|
|
|
// DataSize
|
|
block[i++] = (byte)(sz - 4); // decimal 28 or 16 (workitem 7924)
|
|
block[i++] = 0x00;
|
|
|
|
// The actual metadata - we may or may not have real values yet...
|
|
|
|
// uncompressed size
|
|
Array.Copy(BitConverter.GetBytes(_UncompressedSize), 0, block, i, 8);
|
|
i += 8;
|
|
// compressed size
|
|
Array.Copy(BitConverter.GetBytes(_CompressedSize), 0, block, i, 8);
|
|
i += 8;
|
|
|
|
// workitem 7924 - only include this if the "extra" field is for
|
|
// use in the central directory. It is unnecessary and not useful
|
|
// for local header; makes WinZip choke.
|
|
if (forCentralDirectory)
|
|
{
|
|
// relative offset
|
|
Array.Copy(BitConverter.GetBytes(_RelativeOffsetOfLocalHeader), 0, block, i, 8);
|
|
i += 8;
|
|
|
|
// starting disk number
|
|
Array.Copy(BitConverter.GetBytes(0), 0, block, i, 4);
|
|
}
|
|
listOfBlocks.Add(block);
|
|
}
|
|
|
|
|
|
#if AESCRYPTO
|
|
if (Encryption == EncryptionAlgorithm.WinZipAes128 ||
|
|
Encryption == EncryptionAlgorithm.WinZipAes256)
|
|
{
|
|
block = new byte[4 + 7];
|
|
int i = 0;
|
|
// extra field for WinZip AES
|
|
// header id
|
|
block[i++] = 0x01;
|
|
block[i++] = 0x99;
|
|
|
|
// data size
|
|
block[i++] = 0x07;
|
|
block[i++] = 0x00;
|
|
|
|
// vendor number
|
|
block[i++] = 0x01; // AE-1 - means "Verify CRC"
|
|
block[i++] = 0x00;
|
|
|
|
// vendor id "AE"
|
|
block[i++] = 0x41;
|
|
block[i++] = 0x45;
|
|
|
|
// key strength
|
|
int keystrength = GetKeyStrengthInBits(Encryption);
|
|
if (keystrength == 128)
|
|
block[i] = 1;
|
|
else if (keystrength == 256)
|
|
block[i] = 3;
|
|
else
|
|
block[i] = 0xFF;
|
|
i++;
|
|
|
|
// actual compression method
|
|
block[i++] = (byte)(_CompressionMethod & 0x00FF);
|
|
block[i++] = (byte)(_CompressionMethod & 0xFF00);
|
|
|
|
listOfBlocks.Add(block);
|
|
}
|
|
#endif
|
|
|
|
if (_ntfsTimesAreSet && _emitNtfsTimes)
|
|
{
|
|
block = new byte[32 + 4];
|
|
// HeaderId 2 bytes 0x000a == NTFS times
|
|
// Datasize 2 bytes 32
|
|
// reserved 4 bytes ?? don't care
|
|
// timetag 2 bytes 0x0001 == NTFS time
|
|
// size 2 bytes 24 == 8 bytes each for ctime, mtime, atime
|
|
// mtime 8 bytes win32 ticks since win32epoch
|
|
// atime 8 bytes win32 ticks since win32epoch
|
|
// ctime 8 bytes win32 ticks since win32epoch
|
|
int i = 0;
|
|
// extra field for NTFS times
|
|
// header id
|
|
block[i++] = 0x0a;
|
|
block[i++] = 0x00;
|
|
|
|
// data size
|
|
block[i++] = 32;
|
|
block[i++] = 0;
|
|
|
|
i += 4; // reserved
|
|
|
|
// time tag
|
|
block[i++] = 0x01;
|
|
block[i++] = 0x00;
|
|
|
|
// data size (again)
|
|
block[i++] = 24;
|
|
block[i++] = 0;
|
|
|
|
Int64 z = _Mtime.ToFileTime();
|
|
Array.Copy(BitConverter.GetBytes(z), 0, block, i, 8);
|
|
i += 8;
|
|
z = _Atime.ToFileTime();
|
|
Array.Copy(BitConverter.GetBytes(z), 0, block, i, 8);
|
|
i += 8;
|
|
z = _Ctime.ToFileTime();
|
|
Array.Copy(BitConverter.GetBytes(z), 0, block, i, 8);
|
|
i += 8;
|
|
|
|
listOfBlocks.Add(block);
|
|
}
|
|
|
|
if (_ntfsTimesAreSet && _emitUnixTimes)
|
|
{
|
|
int len = 5 + 4;
|
|
if (!forCentralDirectory) len += 8;
|
|
|
|
block = new byte[len];
|
|
// local form:
|
|
// --------------
|
|
// HeaderId 2 bytes 0x5455 == unix timestamp
|
|
// Datasize 2 bytes 13
|
|
// flags 1 byte 7 (low three bits all set)
|
|
// mtime 4 bytes seconds since unix epoch
|
|
// atime 4 bytes seconds since unix epoch
|
|
// ctime 4 bytes seconds since unix epoch
|
|
//
|
|
// central directory form:
|
|
//---------------------------------
|
|
// HeaderId 2 bytes 0x5455 == unix timestamp
|
|
// Datasize 2 bytes 5
|
|
// flags 1 byte 7 (low three bits all set)
|
|
// mtime 4 bytes seconds since unix epoch
|
|
//
|
|
int i = 0;
|
|
// extra field for "unix" times
|
|
// header id
|
|
block[i++] = 0x55;
|
|
block[i++] = 0x54;
|
|
|
|
// data size
|
|
block[i++] = unchecked((byte)(len - 4));
|
|
block[i++] = 0;
|
|
|
|
// flags
|
|
block[i++] = 0x07;
|
|
|
|
Int32 z = unchecked((int)((_Mtime - _unixEpoch).TotalSeconds));
|
|
Array.Copy(BitConverter.GetBytes(z), 0, block, i, 4);
|
|
i += 4;
|
|
if (!forCentralDirectory)
|
|
{
|
|
z = unchecked((int)((_Atime - _unixEpoch).TotalSeconds));
|
|
Array.Copy(BitConverter.GetBytes(z), 0, block, i, 4);
|
|
i += 4;
|
|
z = unchecked((int)((_Ctime - _unixEpoch).TotalSeconds));
|
|
Array.Copy(BitConverter.GetBytes(z), 0, block, i, 4);
|
|
i += 4;
|
|
}
|
|
listOfBlocks.Add(block);
|
|
}
|
|
|
|
|
|
// inject other blocks here...
|
|
|
|
|
|
// concatenate any blocks we've got:
|
|
byte[] aggregateBlock = null;
|
|
if (listOfBlocks.Count > 0)
|
|
{
|
|
int totalLength = 0;
|
|
int i, current = 0;
|
|
for (i = 0; i < listOfBlocks.Count; i++)
|
|
totalLength += listOfBlocks[i].Length;
|
|
aggregateBlock = new byte[totalLength];
|
|
for (i = 0; i < listOfBlocks.Count; i++)
|
|
{
|
|
System.Array.Copy(listOfBlocks[i], 0, aggregateBlock, current, listOfBlocks[i].Length);
|
|
current += listOfBlocks[i].Length;
|
|
}
|
|
}
|
|
|
|
return aggregateBlock;
|
|
}
|
|
|
|
|
|
|
|
// private System.Text.Encoding GenerateCommentBytes()
|
|
// {
|
|
// var getEncoding = new Func<System.Text.Encoding>({
|
|
// switch (AlternateEncodingUsage)
|
|
// {
|
|
// case ZipOption.Always:
|
|
// return AlternateEncoding;
|
|
// case ZipOption.Never:
|
|
// return ibm437;
|
|
// }
|
|
// var cb = ibm437.GetBytes(_Comment);
|
|
// // need to use this form of GetString() for .NET CF
|
|
// string s1 = ibm437.GetString(cb, 0, cb.Length);
|
|
// if (s1 == _Comment)
|
|
// return ibm437;
|
|
// return AlternateEncoding;
|
|
// });
|
|
//
|
|
// var encoding = getEncoding();
|
|
// _CommentBytes = encoding.GetBytes(_Comment);
|
|
// return encoding;
|
|
// }
|
|
|
|
|
|
private string NormalizeFileName()
|
|
{
|
|
// here, we need to flip the backslashes to forward-slashes,
|
|
// also, we need to trim the \\server\share syntax from any UNC path.
|
|
// and finally, we need to remove any leading .\
|
|
|
|
string SlashFixed = FileName.Replace("\\", "/");
|
|
string s1 = null;
|
|
if ((_TrimVolumeFromFullyQualifiedPaths) && (FileName.Length >= 3)
|
|
&& (FileName[1] == ':') && (SlashFixed[2] == '/'))
|
|
{
|
|
// trim off volume letter, colon, and slash
|
|
s1 = SlashFixed.Substring(3);
|
|
}
|
|
else if ((FileName.Length >= 4)
|
|
&& ((SlashFixed[0] == '/') && (SlashFixed[1] == '/')))
|
|
{
|
|
int n = SlashFixed.IndexOf('/', 2);
|
|
if (n == -1)
|
|
throw new ArgumentException("The path for that entry appears to be badly formatted");
|
|
s1 = SlashFixed.Substring(n + 1);
|
|
}
|
|
else if ((FileName.Length >= 3)
|
|
&& ((SlashFixed[0] == '.') && (SlashFixed[1] == '/')))
|
|
{
|
|
// trim off dot and slash
|
|
s1 = SlashFixed.Substring(2);
|
|
}
|
|
else
|
|
{
|
|
s1 = SlashFixed;
|
|
}
|
|
return s1;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// generate and return a byte array that encodes the filename
|
|
/// for the entry.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// side effects: generate and store into _CommentBytes the
|
|
/// byte array for any comment attached to the entry. Also
|
|
/// sets _actualEncoding to indicate the actual encoding
|
|
/// used. The same encoding is used for both filename and
|
|
/// comment.
|
|
/// </para>
|
|
/// </remarks>
|
|
private byte[] GetEncodedFileNameBytes()
|
|
{
|
|
// workitem 6513
|
|
var s1 = NormalizeFileName();
|
|
|
|
switch(AlternateEncodingUsage)
|
|
{
|
|
case ZipOption.Always:
|
|
if (!(_Comment == null || _Comment.Length == 0))
|
|
_CommentBytes = AlternateEncoding.GetBytes(_Comment);
|
|
_actualEncoding = AlternateEncoding;
|
|
return AlternateEncoding.GetBytes(s1);
|
|
|
|
case ZipOption.Never:
|
|
if (!(_Comment == null || _Comment.Length == 0))
|
|
_CommentBytes = ibm437.GetBytes(_Comment);
|
|
_actualEncoding = ibm437;
|
|
return ibm437.GetBytes(s1);
|
|
}
|
|
|
|
// arriving here means AlternateEncodingUsage is "AsNecessary"
|
|
|
|
// case ZipOption.AsNecessary:
|
|
// workitem 6513: when writing, use the alternative encoding
|
|
// only when _actualEncoding is not yet set (it can be set
|
|
// during Read), and when ibm437 will not do.
|
|
|
|
byte[] result = ibm437.GetBytes(s1);
|
|
// need to use this form of GetString() for .NET CF
|
|
string s2 = ibm437.GetString(result, 0, result.Length);
|
|
_CommentBytes = null;
|
|
if (s2 != s1)
|
|
{
|
|
// Encoding the filename with ibm437 does not allow round-trips.
|
|
// Therefore, use the alternate encoding. Assume it will work,
|
|
// no checking of round trips here.
|
|
result = AlternateEncoding.GetBytes(s1);
|
|
if (_Comment != null && _Comment.Length != 0)
|
|
_CommentBytes = AlternateEncoding.GetBytes(_Comment);
|
|
_actualEncoding = AlternateEncoding;
|
|
return result;
|
|
}
|
|
|
|
_actualEncoding = ibm437;
|
|
|
|
// Using ibm437, FileName can be encoded without information
|
|
// loss; now try the Comment.
|
|
|
|
// if there is no comment, use ibm437.
|
|
if (_Comment == null || _Comment.Length == 0)
|
|
return result;
|
|
|
|
// there is a comment. Get the encoded form.
|
|
byte[] cbytes = ibm437.GetBytes(_Comment);
|
|
string c2 = ibm437.GetString(cbytes,0,cbytes.Length);
|
|
|
|
// Check for round-trip.
|
|
if (c2 != Comment)
|
|
{
|
|
// Comment cannot correctly be encoded with ibm437. Use
|
|
// the alternate encoding.
|
|
|
|
result = AlternateEncoding.GetBytes(s1);
|
|
_CommentBytes = AlternateEncoding.GetBytes(_Comment);
|
|
_actualEncoding = AlternateEncoding;
|
|
return result;
|
|
}
|
|
|
|
// use IBM437
|
|
_CommentBytes = cbytes;
|
|
return result;
|
|
}
|
|
|
|
|
|
|
|
private bool WantReadAgain()
|
|
{
|
|
if (_UncompressedSize < 0x10) return false;
|
|
if (_CompressionMethod == 0x00) return false;
|
|
if (CompressionLevel == Ionic.Zlib.CompressionLevel.None) return false;
|
|
if (_CompressedSize < _UncompressedSize) return false;
|
|
|
|
if (this._Source == ZipEntrySource.Stream && !this._sourceStream.CanSeek) return false;
|
|
|
|
#if AESCRYPTO
|
|
if (_aesCrypto_forWrite != null && (CompressedSize - _aesCrypto_forWrite.SizeOfEncryptionMetadata) <= UncompressedSize + 0x10) return false;
|
|
#endif
|
|
|
|
if (_zipCrypto_forWrite != null && (CompressedSize - 12) <= UncompressedSize) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
private void MaybeUnsetCompressionMethodForWriting(int cycle)
|
|
{
|
|
// if we've already tried with compression... turn it off this time
|
|
if (cycle > 1)
|
|
{
|
|
_CompressionMethod = 0x0;
|
|
return;
|
|
}
|
|
// compression for directories = 0x00 (No Compression)
|
|
if (IsDirectory)
|
|
{
|
|
_CompressionMethod = 0x0;
|
|
return;
|
|
}
|
|
|
|
if (this._Source == ZipEntrySource.ZipFile)
|
|
{
|
|
return; // do nothing
|
|
}
|
|
|
|
// If __FileDataPosition is zero, then that means we will get the data
|
|
// from a file or stream.
|
|
|
|
// It is never possible to compress a zero-length file, so we check for
|
|
// this condition.
|
|
|
|
if (this._Source == ZipEntrySource.Stream)
|
|
{
|
|
// workitem 7742
|
|
if (_sourceStream != null && _sourceStream.CanSeek)
|
|
{
|
|
// Length prop will throw if CanSeek is false
|
|
long fileLength = _sourceStream.Length;
|
|
if (fileLength == 0)
|
|
{
|
|
_CompressionMethod = 0x00;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else if ((this._Source == ZipEntrySource.FileSystem) && (SharedUtilities.GetFileLength(LocalFileName) == 0L))
|
|
{
|
|
_CompressionMethod = 0x00;
|
|
return;
|
|
}
|
|
|
|
// Ok, we're getting the data to be compressed from a
|
|
// non-zero-length file or stream, or a file or stream of
|
|
// unknown length, and we presume that it is non-zero. In
|
|
// that case we check the callback to see if the app wants
|
|
// to tell us whether to compress or not.
|
|
if (SetCompression != null)
|
|
CompressionLevel = SetCompression(LocalFileName, _FileNameInArchive);
|
|
|
|
// finally, set CompressionMethod to None if CompressionLevel is None
|
|
if (CompressionLevel == (short)Ionic.Zlib.CompressionLevel.None &&
|
|
CompressionMethod == Ionic.Zip.CompressionMethod.Deflate)
|
|
_CompressionMethod = 0x00;
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
// write the header info for an entry
|
|
internal void WriteHeader(Stream s, int cycle)
|
|
{
|
|
// Must remember the offset, within the output stream, of this particular
|
|
// entry header.
|
|
//
|
|
// This is for 2 reasons:
|
|
//
|
|
// 1. so we can determine the RelativeOffsetOfLocalHeader (ROLH) for
|
|
// use in the central directory.
|
|
// 2. so we can seek backward in case there is an error opening or reading
|
|
// the file, and the application decides to skip the file. In this case,
|
|
// we need to seek backward in the output stream to allow the next entry
|
|
// to be added to the zipfile output stream.
|
|
//
|
|
// Normally you would just store the offset before writing to the output
|
|
// stream and be done with it. But the possibility to use split archives
|
|
// makes this approach ineffective. In split archives, each file or segment
|
|
// is bound to a max size limit, and each local file header must not span a
|
|
// segment boundary; it must be written contiguously. If it will fit in the
|
|
// current segment, then the ROLH is just the current Position in the output
|
|
// stream. If it won't fit, then we need a new file (segment) and the ROLH
|
|
// is zero.
|
|
//
|
|
// But we only can know if it is possible to write a header contiguously
|
|
// after we know the size of the local header, a size that varies with
|
|
// things like filename length, comments, and extra fields. We have to
|
|
// compute the header fully before knowing whether it will fit.
|
|
//
|
|
// That takes care of item #1 above. Now, regarding #2. If an error occurs
|
|
// while computing the local header, we want to just seek backward. The
|
|
// exception handling logic (in the caller of WriteHeader) uses ROLH to
|
|
// scroll back.
|
|
//
|
|
// All this means we have to preserve the starting offset before computing
|
|
// the header, and also we have to compute the offset later, to handle the
|
|
// case of split archives.
|
|
|
|
var counter = s as CountingStream;
|
|
|
|
// workitem 8098: ok (output)
|
|
// This may change later, for split archives
|
|
|
|
// Don't set _RelativeOffsetOfLocalHeader. Instead, set a temp variable.
|
|
// This allows for re-streaming, where a zip entry might be read from a
|
|
// zip archive (and maybe decrypted, and maybe decompressed) and then
|
|
// written to another zip archive, with different settings for
|
|
// compression method, compression level, or encryption algorithm.
|
|
_future_ROLH = (counter != null)
|
|
? counter.ComputedPosition
|
|
: s.Position;
|
|
|
|
int j = 0, i = 0;
|
|
|
|
byte[] block = new byte[30];
|
|
|
|
// signature
|
|
block[i++] = (byte)(ZipConstants.ZipEntrySignature & 0x000000FF);
|
|
block[i++] = (byte)((ZipConstants.ZipEntrySignature & 0x0000FF00) >> 8);
|
|
block[i++] = (byte)((ZipConstants.ZipEntrySignature & 0x00FF0000) >> 16);
|
|
block[i++] = (byte)((ZipConstants.ZipEntrySignature & 0xFF000000) >> 24);
|
|
|
|
// Design notes for ZIP64:
|
|
//
|
|
// The specification says that the header must include the Compressed
|
|
// and Uncompressed sizes, as well as the CRC32 value. When creating
|
|
// a zip via streamed processing, these quantities are not known until
|
|
// after the compression is done. Thus, a typical way to do it is to
|
|
// insert zeroes for these quantities, then do the compression, then
|
|
// seek back to insert the appropriate values, then seek forward to
|
|
// the end of the file data.
|
|
//
|
|
// There is also the option of using bit 3 in the GP bitfield - to
|
|
// specify that there is a data descriptor after the file data
|
|
// containing these three quantities.
|
|
//
|
|
// This works when the size of the quantities is known, either 32-bits
|
|
// or 64 bits as with the ZIP64 extensions.
|
|
//
|
|
// With Zip64, the 4-byte fields are set to 0xffffffff, and there is a
|
|
// corresponding data block in the "extra field" that contains the
|
|
// actual Compressed, uncompressed sizes. (As well as an additional
|
|
// field, the "Relative Offset of Local Header")
|
|
//
|
|
// The problem is when the app desires to use ZIP64 extensions
|
|
// optionally, only when necessary. Suppose the library assumes no
|
|
// zip64 extensions when writing the header, then after compression
|
|
// finds that the size of the data requires zip64. At this point, the
|
|
// header, already written to the file, won't have the necessary data
|
|
// block in the "extra field". The size of the entry header is fixed,
|
|
// so it is not possible to just "add on" the zip64 data block after
|
|
// compressing the file. On the other hand, always using zip64 will
|
|
// break interoperability with many other systems and apps.
|
|
//
|
|
// The approach we take is to insert a 32-byte dummy data block in the
|
|
// extra field, whenever zip64 is to be used "as necessary". This data
|
|
// block will get the actual zip64 HeaderId and zip64 metadata if
|
|
// necessary. If not necessary, the data block will get a meaningless
|
|
// HeaderId (0x1111), and will be filled with zeroes.
|
|
//
|
|
// When zip64 is actually in use, we also need to set the
|
|
// VersionNeededToExtract field to 45.
|
|
//
|
|
// There is one additional wrinkle: using zip64 as necessary conflicts
|
|
// with output to non-seekable devices. The header is emitted and
|
|
// must indicate whether zip64 is in use, before we know if zip64 is
|
|
// necessary. Because there is no seeking, the header can never be
|
|
// changed. Therefore, on non-seekable devices,
|
|
// Zip64Option.AsNecessary is the same as Zip64Option.Always.
|
|
//
|
|
|
|
|
|
// version needed- see AppNote.txt.
|
|
//
|
|
// need v5.1 for PKZIP strong encryption, or v2.0 for no encryption or
|
|
// for PK encryption, 4.5 for zip64. We may reset this later, as
|
|
// necessary or zip64.
|
|
|
|
_presumeZip64 = (_container.Zip64 == Zip64Option.Always ||
|
|
(_container.Zip64 == Zip64Option.AsNecessary && !s.CanSeek));
|
|
Int16 VersionNeededToExtract = (Int16)(_presumeZip64 ? 45 : 20);
|
|
#if BZIP
|
|
if (this.CompressionMethod == Ionic.Zip.CompressionMethod.BZip2)
|
|
VersionNeededToExtract = 46;
|
|
#endif
|
|
|
|
// (i==4)
|
|
block[i++] = (byte)(VersionNeededToExtract & 0x00FF);
|
|
block[i++] = (byte)((VersionNeededToExtract & 0xFF00) >> 8);
|
|
|
|
// Get byte array. Side effect: sets ActualEncoding.
|
|
// Must determine encoding before setting the bitfield.
|
|
// workitem 6513
|
|
byte[] fileNameBytes = GetEncodedFileNameBytes();
|
|
Int16 filenameLength = (Int16)fileNameBytes.Length;
|
|
|
|
// general purpose bitfield
|
|
// In the current implementation, this library uses only these bits
|
|
// in the GP bitfield:
|
|
// bit 0 = if set, indicates the entry is encrypted
|
|
// bit 3 = if set, indicates the CRC, C and UC sizes follow the file data.
|
|
// bit 6 = strong encryption - for pkware's meaning of strong encryption
|
|
// bit 11 = UTF-8 encoding is used in the comment and filename
|
|
|
|
|
|
// Here we set or unset the encryption bit.
|
|
// _BitField may already be set, as with a ZipEntry added into ZipOutputStream, which
|
|
// has bit 3 always set. We only want to set one bit
|
|
if (_Encryption == EncryptionAlgorithm.None)
|
|
_BitField &= ~1; // encryption bit OFF
|
|
else
|
|
_BitField |= 1; // encryption bit ON
|
|
|
|
|
|
// workitem 7941: WinZip does not the "strong encryption" bit when using AES.
|
|
// This "Strong Encryption" is a PKWare Strong encryption thing.
|
|
// _BitField |= 0x0020;
|
|
|
|
// set the UTF8 bit if necessary
|
|
#if SILVERLIGHT
|
|
if (_actualEncoding.WebName == "utf-8")
|
|
#else
|
|
if (_actualEncoding.CodePage == System.Text.Encoding.UTF8.CodePage)
|
|
#endif
|
|
_BitField |= 0x0800;
|
|
|
|
// The PKZIP spec says that if bit 3 is set (0x0008) in the General
|
|
// Purpose BitField, then the CRC, Compressed size, and uncompressed
|
|
// size are written directly after the file data.
|
|
//
|
|
// These 3 quantities are normally present in the regular zip entry
|
|
// header. But, they are not knowable until after the compression is
|
|
// done. So, in the normal case, we
|
|
//
|
|
// - write the header, using zeros for these quantities
|
|
// - compress the data, and incidentally compute these quantities.
|
|
// - seek back and write the correct values them into the header.
|
|
//
|
|
// This is nice because, while it is more complicated to write the zip
|
|
// file, it is simpler and less error prone to read the zip file, and
|
|
// as a result more applications can read zip files produced this way,
|
|
// with those 3 quantities in the header.
|
|
//
|
|
// But if seeking in the output stream is not possible, then we need
|
|
// to set the appropriate bitfield and emit these quantities after the
|
|
// compressed file data in the output.
|
|
//
|
|
// workitem 7216 - having trouble formatting a zip64 file that is
|
|
// readable by WinZip. not sure why! What I found is that setting
|
|
// bit 3 and following all the implications, the zip64 file is
|
|
// readable by WinZip 12. and Perl's IO::Compress::Zip . Perl takes
|
|
// an interesting approach - it always sets bit 3 if ZIP64 in use.
|
|
// DotNetZip now does the same; this gives better compatibility with
|
|
// WinZip 12.
|
|
|
|
if (IsDirectory || cycle == 99)
|
|
{
|
|
// (cycle == 99) indicates a zero-length entry written by ZipOutputStream
|
|
|
|
_BitField &= ~0x0008; // unset bit 3 - no "data descriptor" - ever
|
|
_BitField &= ~0x0001; // unset bit 1 - no encryption - ever
|
|
Encryption = EncryptionAlgorithm.None;
|
|
Password = null;
|
|
}
|
|
else if (!s.CanSeek)
|
|
_BitField |= 0x0008;
|
|
|
|
#if DONT_GO_THERE
|
|
else if (this.Encryption == EncryptionAlgorithm.PkzipWeak &&
|
|
this._Source != ZipEntrySource.ZipFile)
|
|
{
|
|
// Set bit 3 to avoid the double-read perf issue.
|
|
//
|
|
// When PKZIP encryption is used, byte 11 of the encryption header is
|
|
// used as a consistency check. It is normally set to the MSByte of the
|
|
// CRC. But this means the cRC must be known ebfore compression and
|
|
// encryption, which means the entire stream has to be read twice. To
|
|
// avoid that, the high-byte of the time blob (when in DOS format) can
|
|
// be used for the consistency check (byte 11 in the encryption header).
|
|
// But this means the entry must have bit 3 set.
|
|
//
|
|
// Previously I used a more complex arrangement - using the methods like
|
|
// FigureCrc32(), PrepOutputStream() and others, in order to manage the
|
|
// seek-back in the source stream. Why? Because bit 3 is not always
|
|
// friendly with third-party zip tools, like those on the Mac.
|
|
//
|
|
// This is why this code is still ifdef'd out.
|
|
//
|
|
// Might consider making this yet another programmable option -
|
|
// AlwaysUseBit3ForPkzip. But that's for another day.
|
|
//
|
|
_BitField |= 0x0008;
|
|
}
|
|
#endif
|
|
|
|
// (i==6)
|
|
block[i++] = (byte)(_BitField & 0x00FF);
|
|
block[i++] = (byte)((_BitField & 0xFF00) >> 8);
|
|
|
|
// Here, we want to set values for Compressed Size, Uncompressed Size,
|
|
// and CRC. If we have __FileDataPosition as not -1 (zero is a valid
|
|
// FDP), then that means we are reading this zip entry from a zip
|
|
// file, and we have good values for those quantities.
|
|
//
|
|
// If _FileDataPosition is -1, then we are constructing this Entry
|
|
// from nothing. We zero those quantities now, and we will compute
|
|
// actual values for the three quantities later, when we do the
|
|
// compression, and then seek back to write them into the appropriate
|
|
// place in the header.
|
|
if (this.__FileDataPosition == -1)
|
|
{
|
|
//_UncompressedSize = 0; // do not unset - may need this value for restream
|
|
// _Crc32 = 0; // ditto
|
|
_CompressedSize = 0;
|
|
_crcCalculated = false;
|
|
}
|
|
|
|
// set compression method here
|
|
MaybeUnsetCompressionMethodForWriting(cycle);
|
|
|
|
// (i==8) compression method
|
|
block[i++] = (byte)(_CompressionMethod & 0x00FF);
|
|
block[i++] = (byte)((_CompressionMethod & 0xFF00) >> 8);
|
|
|
|
if (cycle == 99)
|
|
{
|
|
// (cycle == 99) indicates a zero-length entry written by ZipOutputStream
|
|
SetZip64Flags();
|
|
}
|
|
|
|
#if AESCRYPTO
|
|
else if (Encryption == EncryptionAlgorithm.WinZipAes128 || Encryption == EncryptionAlgorithm.WinZipAes256)
|
|
{
|
|
i -= 2;
|
|
block[i++] = 0x63;
|
|
block[i++] = 0;
|
|
}
|
|
#endif
|
|
|
|
// LastMod
|
|
_TimeBlob = Ionic.Zip.SharedUtilities.DateTimeToPacked(LastModified);
|
|
|
|
// (i==10) time blob
|
|
block[i++] = (byte)(_TimeBlob & 0x000000FF);
|
|
block[i++] = (byte)((_TimeBlob & 0x0000FF00) >> 8);
|
|
block[i++] = (byte)((_TimeBlob & 0x00FF0000) >> 16);
|
|
block[i++] = (byte)((_TimeBlob & 0xFF000000) >> 24);
|
|
|
|
// (i==14) CRC - if source==filesystem, this is zero now, actual value
|
|
// will be calculated later. if source==archive, this is a bonafide
|
|
// value.
|
|
block[i++] = (byte)(_Crc32 & 0x000000FF);
|
|
block[i++] = (byte)((_Crc32 & 0x0000FF00) >> 8);
|
|
block[i++] = (byte)((_Crc32 & 0x00FF0000) >> 16);
|
|
block[i++] = (byte)((_Crc32 & 0xFF000000) >> 24);
|
|
|
|
if (_presumeZip64)
|
|
{
|
|
// (i==18) CompressedSize (Int32) and UncompressedSize - all 0xFF for now
|
|
for (j = 0; j < 8; j++)
|
|
block[i++] = 0xFF;
|
|
}
|
|
else
|
|
{
|
|
// (i==18) CompressedSize (Int32) - this value may or may not be
|
|
// bonafide. if source == filesystem, then it is zero, and we'll
|
|
// learn it after we compress. if source == archive, then it is
|
|
// bonafide data.
|
|
block[i++] = (byte)(_CompressedSize & 0x000000FF);
|
|
block[i++] = (byte)((_CompressedSize & 0x0000FF00) >> 8);
|
|
block[i++] = (byte)((_CompressedSize & 0x00FF0000) >> 16);
|
|
block[i++] = (byte)((_CompressedSize & 0xFF000000) >> 24);
|
|
|
|
// (i==22) UncompressedSize (Int32) - this value may or may not be
|
|
// bonafide.
|
|
block[i++] = (byte)(_UncompressedSize & 0x000000FF);
|
|
block[i++] = (byte)((_UncompressedSize & 0x0000FF00) >> 8);
|
|
block[i++] = (byte)((_UncompressedSize & 0x00FF0000) >> 16);
|
|
block[i++] = (byte)((_UncompressedSize & 0xFF000000) >> 24);
|
|
}
|
|
|
|
// (i==26) filename length (Int16)
|
|
block[i++] = (byte)(filenameLength & 0x00FF);
|
|
block[i++] = (byte)((filenameLength & 0xFF00) >> 8);
|
|
|
|
_Extra = ConstructExtraField(false);
|
|
|
|
// (i==28) extra field length (short)
|
|
Int16 extraFieldLength = (Int16)((_Extra == null) ? 0 : _Extra.Length);
|
|
block[i++] = (byte)(extraFieldLength & 0x00FF);
|
|
block[i++] = (byte)((extraFieldLength & 0xFF00) >> 8);
|
|
|
|
// workitem 13542
|
|
byte[] bytes = new byte[i + filenameLength + extraFieldLength];
|
|
|
|
// get the fixed portion
|
|
Buffer.BlockCopy(block, 0, bytes, 0, i);
|
|
//for (j = 0; j < i; j++) bytes[j] = block[j];
|
|
|
|
// The filename written to the archive.
|
|
Buffer.BlockCopy(fileNameBytes, 0, bytes, i, fileNameBytes.Length);
|
|
// for (j = 0; j < fileNameBytes.Length; j++)
|
|
// bytes[i + j] = fileNameBytes[j];
|
|
|
|
i += fileNameBytes.Length;
|
|
|
|
// "Extra field"
|
|
if (_Extra != null)
|
|
{
|
|
Buffer.BlockCopy(_Extra, 0, bytes, i, _Extra.Length);
|
|
// for (j = 0; j < _Extra.Length; j++)
|
|
// bytes[i + j] = _Extra[j];
|
|
i += _Extra.Length;
|
|
}
|
|
|
|
_LengthOfHeader = i;
|
|
|
|
// handle split archives
|
|
var zss = s as ZipSegmentedStream;
|
|
if (zss != null)
|
|
{
|
|
zss.ContiguousWrite = true;
|
|
UInt32 requiredSegment = zss.ComputeSegment(i);
|
|
if (requiredSegment != zss.CurrentSegment)
|
|
_future_ROLH = 0; // rollover!
|
|
else
|
|
_future_ROLH = zss.Position;
|
|
|
|
_diskNumber = requiredSegment;
|
|
}
|
|
|
|
// validate the ZIP64 usage
|
|
if (_container.Zip64 == Zip64Option.Never && (uint)_RelativeOffsetOfLocalHeader >= 0xFFFFFFFF)
|
|
throw new ZipException("Offset within the zip archive exceeds 0xFFFFFFFF. Consider setting the UseZip64WhenSaving property on the ZipFile instance.");
|
|
|
|
|
|
// finally, write the header to the stream
|
|
s.Write(bytes, 0, i);
|
|
|
|
// now that the header is written, we can turn off the contiguous write restriction.
|
|
if (zss != null)
|
|
zss.ContiguousWrite = false;
|
|
|
|
// Preserve this header data, we'll use it again later.
|
|
// ..when seeking backward, to write again, after we have the Crc, compressed
|
|
// and uncompressed sizes.
|
|
// ..and when writing the central directory structure.
|
|
_EntryHeader = bytes;
|
|
}
|
|
|
|
|
|
|
|
|
|
private Int32 FigureCrc32()
|
|
{
|
|
if (_crcCalculated == false)
|
|
{
|
|
Stream input = null;
|
|
// get the original stream:
|
|
if (this._Source == ZipEntrySource.WriteDelegate)
|
|
{
|
|
var output = new Ionic.Crc.CrcCalculatorStream(Stream.Null);
|
|
// allow the application to write the data
|
|
this._WriteDelegate(this.FileName, output);
|
|
_Crc32 = output.Crc;
|
|
}
|
|
else if (this._Source == ZipEntrySource.ZipFile)
|
|
{
|
|
// nothing to do - the CRC is already set
|
|
}
|
|
else
|
|
{
|
|
if (this._Source == ZipEntrySource.Stream)
|
|
{
|
|
PrepSourceStream();
|
|
input = this._sourceStream;
|
|
}
|
|
else if (this._Source == ZipEntrySource.JitStream)
|
|
{
|
|
// allow the application to open the stream
|
|
if (this._sourceStream == null)
|
|
_sourceStream = this._OpenDelegate(this.FileName);
|
|
PrepSourceStream();
|
|
input = this._sourceStream;
|
|
}
|
|
else if (this._Source == ZipEntrySource.ZipOutputStream)
|
|
{
|
|
}
|
|
else
|
|
{
|
|
//input = File.OpenRead(LocalFileName);
|
|
input = File.Open(LocalFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
}
|
|
|
|
var crc32 = new Ionic.Crc.CRC32();
|
|
_Crc32 = crc32.GetCrc32(input);
|
|
|
|
if (_sourceStream == null)
|
|
{
|
|
#if NETCF
|
|
input.Close();
|
|
#else
|
|
input.Dispose();
|
|
#endif
|
|
}
|
|
}
|
|
_crcCalculated = true;
|
|
}
|
|
return _Crc32;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Stores the position of the entry source stream, or, if the position is
|
|
/// already stored, seeks to that position.
|
|
/// </summary>
|
|
///
|
|
/// <remarks>
|
|
/// <para>
|
|
/// This method is called in prep for reading the source stream. If PKZIP
|
|
/// encryption is used, then we need to calc the CRC32 before doing the
|
|
/// encryption, because the CRC is used in the 12th byte of the PKZIP
|
|
/// encryption header. So, we need to be able to seek backward in the source
|
|
/// when saving the ZipEntry. This method is called from the place that
|
|
/// calculates the CRC, and also from the method that does the encryption of
|
|
/// the file data.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// The first time through, this method sets the _sourceStreamOriginalPosition
|
|
/// field. Subsequent calls to this method seek to that position.
|
|
/// </para>
|
|
/// </remarks>
|
|
private void PrepSourceStream()
|
|
{
|
|
if (_sourceStream == null)
|
|
throw new ZipException(String.Format("The input stream is null for entry '{0}'.", FileName));
|
|
|
|
if (this._sourceStreamOriginalPosition != null)
|
|
{
|
|
// this will happen the 2nd cycle through, if the stream is seekable
|
|
this._sourceStream.Position = this._sourceStreamOriginalPosition.Value;
|
|
}
|
|
else if (this._sourceStream.CanSeek)
|
|
{
|
|
// this will happen the first cycle through, if seekable
|
|
this._sourceStreamOriginalPosition = new Nullable<Int64>(this._sourceStream.Position);
|
|
}
|
|
else if (this.Encryption == EncryptionAlgorithm.PkzipWeak)
|
|
{
|
|
// In general, using PKZIP encryption on a a zip entry whose input
|
|
// comes from a non-seekable stream, is tricky. Here's why:
|
|
//
|
|
// Byte 11 of the PKZIP encryption header is used for password
|
|
// validation and consistency checknig.
|
|
//
|
|
// Normally, the highest byte of the CRC is used as the 11th (last) byte
|
|
// in the PKZIP encryption header. This means the CRC must be known
|
|
// before encryption is performed. Normally that means we read the full
|
|
// data stream, compute the CRC, then seek back and read it again for
|
|
// the compression+encryption phase. Obviously this is bad for
|
|
// performance with a large input file.
|
|
//
|
|
// There's a twist in the ZIP spec (actually documented only in infozip
|
|
// code, not in the spec itself) that allows the high-order byte of the
|
|
// last modified time for the entry, when the lastmod time is in packed
|
|
// (DOS) format, to be used for Byte 11 in the encryption header. In
|
|
// this case, the bit 3 "data descriptor" must be used.
|
|
//
|
|
// An intelligent implementation would therefore force the use of the
|
|
// bit 3 data descriptor when PKZIP encryption is in use, regardless.
|
|
// This avoids the double-read of the stream to be encrypted. So far,
|
|
// DotNetZip doesn't do that; it just punts when the input stream is
|
|
// non-seekable, and the output does not use Bit 3.
|
|
//
|
|
// The other option is to use the CRC when it is already available, eg,
|
|
// when the source for the data is a ZipEntry (when the zip file is
|
|
// being updated). In this case we already know the CRC and can just use
|
|
// what we know.
|
|
|
|
if (this._Source != ZipEntrySource.ZipFile && ((this._BitField & 0x0008) != 0x0008))
|
|
throw new ZipException("It is not possible to use PKZIP encryption on a non-seekable input stream");
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Copy metadata that may have been changed by the app. We do this when
|
|
/// resetting the zipFile instance. If the app calls Save() on a ZipFile, then
|
|
/// tries to party on that file some more, we may need to Reset() it , which
|
|
/// means re-reading the entries and then copying the metadata. I think.
|
|
/// </summary>
|
|
internal void CopyMetaData(ZipEntry source)
|
|
{
|
|
this.__FileDataPosition = source.__FileDataPosition;
|
|
this.CompressionMethod = source.CompressionMethod;
|
|
this._CompressionMethod_FromZipFile = source._CompressionMethod_FromZipFile;
|
|
this._CompressedFileDataSize = source._CompressedFileDataSize;
|
|
this._UncompressedSize = source._UncompressedSize;
|
|
this._BitField = source._BitField;
|
|
this._Source = source._Source;
|
|
this._LastModified = source._LastModified;
|
|
this._Mtime = source._Mtime;
|
|
this._Atime = source._Atime;
|
|
this._Ctime = source._Ctime;
|
|
this._ntfsTimesAreSet = source._ntfsTimesAreSet;
|
|
this._emitUnixTimes = source._emitUnixTimes;
|
|
this._emitNtfsTimes = source._emitNtfsTimes;
|
|
}
|
|
|
|
|
|
private void OnWriteBlock(Int64 bytesXferred, Int64 totalBytesToXfer)
|
|
{
|
|
if (_container.ZipFile != null)
|
|
_ioOperationCanceled = _container.ZipFile.OnSaveBlock(this, bytesXferred, totalBytesToXfer);
|
|
}
|
|
|
|
|
|
|
|
private void _WriteEntryData(Stream s)
|
|
{
|
|
// Read in the data from the input stream (often a file in the filesystem),
|
|
// and write it to the output stream, calculating a CRC on it as we go.
|
|
// We will also compress and encrypt as necessary.
|
|
|
|
Stream input = null;
|
|
long fdp = -1L;
|
|
try
|
|
{
|
|
// Want to record the position in the zip file of the zip entry
|
|
// data (as opposed to the metadata). s.Position may fail on some
|
|
// write-only streams, eg stdout or System.Web.HttpResponseStream.
|
|
// We swallow that exception, because we don't care, in that case.
|
|
// But, don't set __FileDataPosition directly. It may be needed
|
|
// to READ the zip entry from the zip file, if this is a
|
|
// "re-stream" situation. In other words if the zip entry has
|
|
// changed compression level, or compression method, or (maybe?)
|
|
// encryption algorithm. In that case if the original entry is
|
|
// encrypted, we need __FileDataPosition to be the value for the
|
|
// input zip file. This s.Position is for the output zipfile. So
|
|
// we copy fdp to __FileDataPosition after this entry has been
|
|
// (maybe) restreamed.
|
|
fdp = s.Position;
|
|
}
|
|
catch (Exception) { }
|
|
|
|
try
|
|
{
|
|
// Use fileLength for progress updates, and to decide whether we can
|
|
// skip encryption and compression altogether (in case of length==zero)
|
|
long fileLength = SetInputAndFigureFileLength(ref input);
|
|
|
|
// Wrap a counting stream around the raw output stream:
|
|
// This is the last thing that happens before the bits go to the
|
|
// application-provided stream.
|
|
//
|
|
// Sometimes s is a CountingStream. Doesn't matter. Wrap it with a
|
|
// counter anyway. We need to count at both levels.
|
|
|
|
CountingStream entryCounter = new CountingStream(s);
|
|
|
|
Stream encryptor;
|
|
Stream compressor;
|
|
|
|
if (fileLength != 0L)
|
|
{
|
|
// Maybe wrap an encrypting stream around the counter: This will
|
|
// happen BEFORE output counting, and AFTER compression, if encryption
|
|
// is used.
|
|
encryptor = MaybeApplyEncryption(entryCounter);
|
|
|
|
// Maybe wrap a compressing Stream around that.
|
|
// This will happen BEFORE encryption (if any) as we write data out.
|
|
compressor = MaybeApplyCompression(encryptor, fileLength);
|
|
}
|
|
else
|
|
{
|
|
encryptor = compressor = entryCounter;
|
|
}
|
|
|
|
// Wrap a CrcCalculatorStream around that.
|
|
// This will happen BEFORE compression (if any) as we write data out.
|
|
var output = new Ionic.Crc.CrcCalculatorStream(compressor, true);
|
|
|
|
// output.Write() causes this flow:
|
|
// calc-crc -> compress -> encrypt -> count -> actually write
|
|
|
|
if (this._Source == ZipEntrySource.WriteDelegate)
|
|
{
|
|
// allow the application to write the data
|
|
this._WriteDelegate(this.FileName, output);
|
|
}
|
|
else
|
|
{
|
|
// synchronously copy the input stream to the output stream-chain
|
|
byte[] buffer = new byte[BufferSize];
|
|
int n;
|
|
while ((n = SharedUtilities.ReadWithRetry(input, buffer, 0, buffer.Length, FileName)) != 0)
|
|
{
|
|
output.Write(buffer, 0, n);
|
|
OnWriteBlock(output.TotalBytesSlurped, fileLength);
|
|
if (_ioOperationCanceled)
|
|
break;
|
|
}
|
|
}
|
|
|
|
FinishOutputStream(s, entryCounter, encryptor, compressor, output);
|
|
}
|
|
finally
|
|
{
|
|
if (this._Source == ZipEntrySource.JitStream)
|
|
{
|
|
// allow the application to close the stream
|
|
if (this._CloseDelegate != null)
|
|
this._CloseDelegate(this.FileName, input);
|
|
}
|
|
else if ((input as FileStream) != null)
|
|
{
|
|
#if NETCF
|
|
input.Close();
|
|
#else
|
|
input.Dispose();
|
|
#endif
|
|
}
|
|
}
|
|
|
|
if (_ioOperationCanceled)
|
|
return;
|
|
|
|
// set FDP now, to allow for re-streaming
|
|
this.__FileDataPosition = fdp;
|
|
PostProcessOutput(s);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Set the input stream and get its length, if possible. The length is
|
|
/// used for progress updates, AND, to allow an optimization in case of
|
|
/// a stream/file of zero length. In that case we skip the Encrypt and
|
|
/// compression Stream. (like DeflateStream or BZip2OutputStream)
|
|
/// </summary>
|
|
private long SetInputAndFigureFileLength(ref Stream input)
|
|
{
|
|
long fileLength = -1L;
|
|
// get the original stream:
|
|
if (this._Source == ZipEntrySource.Stream)
|
|
{
|
|
PrepSourceStream();
|
|
input = this._sourceStream;
|
|
|
|
// Try to get the length, no big deal if not available.
|
|
try { fileLength = this._sourceStream.Length; }
|
|
catch (NotSupportedException) { }
|
|
}
|
|
else if (this._Source == ZipEntrySource.ZipFile)
|
|
{
|
|
// we are "re-streaming" the zip entry.
|
|
string pwd = (_Encryption_FromZipFile == EncryptionAlgorithm.None) ? null : (this._Password ?? this._container.Password);
|
|
this._sourceStream = InternalOpenReader(pwd);
|
|
PrepSourceStream();
|
|
input = this._sourceStream;
|
|
fileLength = this._sourceStream.Length;
|
|
}
|
|
else if (this._Source == ZipEntrySource.JitStream)
|
|
{
|
|
// allow the application to open the stream
|
|
if (this._sourceStream == null) _sourceStream = this._OpenDelegate(this.FileName);
|
|
PrepSourceStream();
|
|
input = this._sourceStream;
|
|
try { fileLength = this._sourceStream.Length; }
|
|
catch (NotSupportedException) { }
|
|
}
|
|
else if (this._Source == ZipEntrySource.FileSystem)
|
|
{
|
|
// workitem 7145
|
|
FileShare fs = FileShare.ReadWrite;
|
|
#if !NETCF
|
|
// FileShare.Delete is not defined for the Compact Framework
|
|
fs |= FileShare.Delete;
|
|
#endif
|
|
// workitem 8423
|
|
input = File.Open(LocalFileName, FileMode.Open, FileAccess.Read, fs);
|
|
fileLength = input.Length;
|
|
}
|
|
|
|
return fileLength;
|
|
}
|
|
|
|
|
|
|
|
internal void FinishOutputStream(Stream s,
|
|
CountingStream entryCounter,
|
|
Stream encryptor,
|
|
Stream compressor,
|
|
Ionic.Crc.CrcCalculatorStream output)
|
|
{
|
|
if (output == null) return;
|
|
|
|
output.Close();
|
|
|
|
// by calling Close() on the deflate stream, we write the footer bytes, as necessary.
|
|
if ((compressor as Ionic.Zlib.DeflateStream) != null)
|
|
compressor.Close();
|
|
#if BZIP
|
|
else if ((compressor as Ionic.BZip2.BZip2OutputStream) != null)
|
|
compressor.Close();
|
|
#if !NETCF
|
|
else if ((compressor as Ionic.BZip2.ParallelBZip2OutputStream) != null)
|
|
compressor.Close();
|
|
#endif
|
|
#endif
|
|
|
|
#if !NETCF
|
|
else if ((compressor as Ionic.Zlib.ParallelDeflateOutputStream) != null)
|
|
compressor.Close();
|
|
#endif
|
|
|
|
encryptor.Flush();
|
|
encryptor.Close();
|
|
|
|
_LengthOfTrailer = 0;
|
|
|
|
_UncompressedSize = output.TotalBytesSlurped;
|
|
|
|
#if AESCRYPTO
|
|
WinZipAesCipherStream wzacs = encryptor as WinZipAesCipherStream;
|
|
if (wzacs != null && _UncompressedSize > 0)
|
|
{
|
|
s.Write(wzacs.FinalAuthentication, 0, 10);
|
|
_LengthOfTrailer += 10;
|
|
}
|
|
#endif
|
|
_CompressedFileDataSize = entryCounter.BytesWritten;
|
|
_CompressedSize = _CompressedFileDataSize; // may be adjusted
|
|
_Crc32 = output.Crc;
|
|
|
|
// Set _RelativeOffsetOfLocalHeader now, to allow for re-streaming
|
|
StoreRelativeOffset();
|
|
}
|
|
|
|
|
|
|
|
|
|
internal void PostProcessOutput(Stream s)
|
|
{
|
|
var s1 = s as CountingStream;
|
|
|
|
// workitem 8931 - for WriteDelegate.
|
|
// The WriteDelegate changes things because there can be a zero-byte stream
|
|
// written. In all other cases DotNetZip knows the length of the stream
|
|
// before compressing and encrypting. In this case we have to circle back,
|
|
// and omit all the crypto stuff - the GP bitfield, and the crypto header.
|
|
if (_UncompressedSize == 0 && _CompressedSize == 0)
|
|
{
|
|
if (this._Source == ZipEntrySource.ZipOutputStream) return; // nothing to do...
|
|
|
|
if (_Password != null)
|
|
{
|
|
int headerBytesToRetract = 0;
|
|
if (Encryption == EncryptionAlgorithm.PkzipWeak)
|
|
headerBytesToRetract = 12;
|
|
#if AESCRYPTO
|
|
else if (Encryption == EncryptionAlgorithm.WinZipAes128 ||
|
|
Encryption == EncryptionAlgorithm.WinZipAes256)
|
|
{
|
|
headerBytesToRetract = _aesCrypto_forWrite._Salt.Length + _aesCrypto_forWrite.GeneratedPV.Length;
|
|
}
|
|
#endif
|
|
if (this._Source == ZipEntrySource.ZipOutputStream && !s.CanSeek)
|
|
throw new ZipException("Zero bytes written, encryption in use, and non-seekable output.");
|
|
|
|
if (Encryption != EncryptionAlgorithm.None)
|
|
{
|
|
// seek back in the stream to un-output the security metadata
|
|
s.Seek(-1 * headerBytesToRetract, SeekOrigin.Current);
|
|
s.SetLength(s.Position);
|
|
// workitem 10178
|
|
Ionic.Zip.SharedUtilities.Workaround_Ladybug318918(s);
|
|
|
|
// workitem 11131
|
|
// adjust the count on the CountingStream as necessary
|
|
if (s1 != null) s1.Adjust(headerBytesToRetract);
|
|
|
|
// subtract the size of the security header from the _LengthOfHeader
|
|
_LengthOfHeader -= headerBytesToRetract;
|
|
__FileDataPosition -= headerBytesToRetract;
|
|
}
|
|
_Password = null;
|
|
|
|
// turn off the encryption bit
|
|
_BitField &= ~(0x0001);
|
|
|
|
// copy the updated bitfield value into the header
|
|
int j = 6;
|
|
_EntryHeader[j++] = (byte)(_BitField & 0x00FF);
|
|
_EntryHeader[j++] = (byte)((_BitField & 0xFF00) >> 8);
|
|
|
|
#if AESCRYPTO
|
|
if (Encryption == EncryptionAlgorithm.WinZipAes128 ||
|
|
Encryption == EncryptionAlgorithm.WinZipAes256)
|
|
{
|
|
// Fix the extra field - overwrite the 0x9901 headerId
|
|
// with dummy data. (arbitrarily, 0x9999)
|
|
Int16 fnLength = (short)(_EntryHeader[26] + _EntryHeader[27] * 256);
|
|
int offx = 30 + fnLength;
|
|
int aesIndex = FindExtraFieldSegment(_EntryHeader, offx, 0x9901);
|
|
if (aesIndex >= 0)
|
|
{
|
|
_EntryHeader[aesIndex++] = 0x99;
|
|
_EntryHeader[aesIndex++] = 0x99;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
CompressionMethod = 0;
|
|
Encryption = EncryptionAlgorithm.None;
|
|
}
|
|
else if (_zipCrypto_forWrite != null
|
|
#if AESCRYPTO
|
|
|| _aesCrypto_forWrite != null
|
|
#endif
|
|
)
|
|
|
|
{
|
|
if (Encryption == EncryptionAlgorithm.PkzipWeak)
|
|
{
|
|
_CompressedSize += 12; // 12 extra bytes for the encryption header
|
|
}
|
|
#if AESCRYPTO
|
|
else if (Encryption == EncryptionAlgorithm.WinZipAes128 ||
|
|
Encryption == EncryptionAlgorithm.WinZipAes256)
|
|
{
|
|
// adjust the compressed size to include the variable (salt+pv)
|
|
// security header and 10-byte trailer. According to the winzip AES
|
|
// spec, that metadata is included in the "Compressed Size" figure
|
|
// when encoding the zip archive.
|
|
_CompressedSize += _aesCrypto_forWrite.SizeOfEncryptionMetadata;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
int i = 8;
|
|
_EntryHeader[i++] = (byte)(_CompressionMethod & 0x00FF);
|
|
_EntryHeader[i++] = (byte)((_CompressionMethod & 0xFF00) >> 8);
|
|
|
|
i = 14;
|
|
// CRC - the correct value now
|
|
_EntryHeader[i++] = (byte)(_Crc32 & 0x000000FF);
|
|
_EntryHeader[i++] = (byte)((_Crc32 & 0x0000FF00) >> 8);
|
|
_EntryHeader[i++] = (byte)((_Crc32 & 0x00FF0000) >> 16);
|
|
_EntryHeader[i++] = (byte)((_Crc32 & 0xFF000000) >> 24);
|
|
|
|
SetZip64Flags();
|
|
|
|
// (i==26) filename length (Int16)
|
|
Int16 filenameLength = (short)(_EntryHeader[26] + _EntryHeader[27] * 256);
|
|
Int16 extraFieldLength = (short)(_EntryHeader[28] + _EntryHeader[29] * 256);
|
|
|
|
if (_OutputUsesZip64.Value)
|
|
{
|
|
// VersionNeededToExtract - set to 45 to indicate zip64
|
|
_EntryHeader[4] = (byte)(45 & 0x00FF);
|
|
_EntryHeader[5] = 0x00;
|
|
|
|
// workitem 7924 - don't need bit 3
|
|
// // workitem 7917
|
|
// // set bit 3 for ZIP64 compatibility with WinZip12
|
|
// _BitField |= 0x0008;
|
|
// _EntryHeader[6] = (byte)(_BitField & 0x00FF);
|
|
|
|
// CompressedSize and UncompressedSize - 0xFF
|
|
for (int j = 0; j < 8; j++)
|
|
_EntryHeader[i++] = 0xff;
|
|
|
|
// At this point we need to find the "Extra field" that follows the
|
|
// filename. We had already emitted it, but the data (uncomp, comp,
|
|
// ROLH) was not available at the time we did so. Here, we emit it
|
|
// again, with final values.
|
|
|
|
i = 30 + filenameLength;
|
|
_EntryHeader[i++] = 0x01; // zip64
|
|
_EntryHeader[i++] = 0x00;
|
|
|
|
i += 2; // skip over data size, which is 16+4
|
|
|
|
Array.Copy(BitConverter.GetBytes(_UncompressedSize), 0, _EntryHeader, i, 8);
|
|
i += 8;
|
|
Array.Copy(BitConverter.GetBytes(_CompressedSize), 0, _EntryHeader, i, 8);
|
|
}
|
|
else
|
|
{
|
|
// VersionNeededToExtract - reset to 20 since no zip64
|
|
_EntryHeader[4] = (byte)(20 & 0x00FF);
|
|
_EntryHeader[5] = 0x00;
|
|
|
|
// CompressedSize - the correct value now
|
|
i = 18;
|
|
_EntryHeader[i++] = (byte)(_CompressedSize & 0x000000FF);
|
|
_EntryHeader[i++] = (byte)((_CompressedSize & 0x0000FF00) >> 8);
|
|
_EntryHeader[i++] = (byte)((_CompressedSize & 0x00FF0000) >> 16);
|
|
_EntryHeader[i++] = (byte)((_CompressedSize & 0xFF000000) >> 24);
|
|
|
|
// UncompressedSize - the correct value now
|
|
_EntryHeader[i++] = (byte)(_UncompressedSize & 0x000000FF);
|
|
_EntryHeader[i++] = (byte)((_UncompressedSize & 0x0000FF00) >> 8);
|
|
_EntryHeader[i++] = (byte)((_UncompressedSize & 0x00FF0000) >> 16);
|
|
_EntryHeader[i++] = (byte)((_UncompressedSize & 0xFF000000) >> 24);
|
|
|
|
// The HeaderId in the extra field header, is already dummied out.
|
|
if (extraFieldLength != 0)
|
|
{
|
|
i = 30 + filenameLength;
|
|
// For zip archives written by this library, if the zip64
|
|
// header exists, it is the first header. Because of the logic
|
|
// used when first writing the _EntryHeader bytes, the
|
|
// HeaderId is not guaranteed to be any particular value. So
|
|
// we determine if the first header is a putative zip64 header
|
|
// by examining the datasize. UInt16 HeaderId =
|
|
// (UInt16)(_EntryHeader[i] + _EntryHeader[i + 1] * 256);
|
|
Int16 DataSize = (short)(_EntryHeader[i + 2] + _EntryHeader[i + 3] * 256);
|
|
if (DataSize == 16)
|
|
{
|
|
// reset to Header Id to dummy value, effectively dummy-ing out the zip64 metadata
|
|
_EntryHeader[i++] = 0x99;
|
|
_EntryHeader[i++] = 0x99;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#if AESCRYPTO
|
|
|
|
if (Encryption == EncryptionAlgorithm.WinZipAes128 ||
|
|
Encryption == EncryptionAlgorithm.WinZipAes256)
|
|
{
|
|
// Must set compressionmethod to 0x0063 (decimal 99)
|
|
//
|
|
// and then set the compression method bytes inside the extra
|
|
// field to the actual compression method value.
|
|
|
|
i = 8;
|
|
_EntryHeader[i++] = 0x63;
|
|
_EntryHeader[i++] = 0;
|
|
|
|
i = 30 + filenameLength;
|
|
do
|
|
{
|
|
UInt16 HeaderId = (UInt16)(_EntryHeader[i] + _EntryHeader[i + 1] * 256);
|
|
Int16 DataSize = (short)(_EntryHeader[i + 2] + _EntryHeader[i + 3] * 256);
|
|
if (HeaderId != 0x9901)
|
|
{
|
|
// skip this header
|
|
i += DataSize + 4;
|
|
}
|
|
else
|
|
{
|
|
i += 9;
|
|
// actual compression method
|
|
_EntryHeader[i++] = (byte)(_CompressionMethod & 0x00FF);
|
|
_EntryHeader[i++] = (byte)(_CompressionMethod & 0xFF00);
|
|
}
|
|
} while (i < (extraFieldLength - 30 - filenameLength));
|
|
}
|
|
#endif
|
|
|
|
// finally, write the data.
|
|
|
|
// workitem 7216 - sometimes we don't seek even if we CAN. ASP.NET
|
|
// Response.OutputStream, or stdout are non-seekable. But we may also want
|
|
// to NOT seek in other cases, eg zip64. For all cases, we just check bit 3
|
|
// to see if we want to seek. There's one exception - if using a
|
|
// ZipOutputStream, and PKZip encryption is in use, then we set bit 3 even
|
|
// if the out is seekable. This is so the check on the last byte of the
|
|
// PKZip Encryption Header can be done on the current time, as opposed to
|
|
// the CRC, to prevent streaming the file twice. So, test for
|
|
// ZipOutputStream and seekable, and if so, seek back, even if bit 3 is set.
|
|
|
|
if ((_BitField & 0x0008) != 0x0008 ||
|
|
(this._Source == ZipEntrySource.ZipOutputStream && s.CanSeek))
|
|
{
|
|
// seek back and rewrite the entry header
|
|
var zss = s as ZipSegmentedStream;
|
|
if (zss != null && _diskNumber != zss.CurrentSegment)
|
|
{
|
|
// In this case the entry header is in a different file,
|
|
// which has already been closed. Need to re-open it.
|
|
using (Stream hseg = ZipSegmentedStream.ForUpdate(this._container.ZipFile.Name, _diskNumber))
|
|
{
|
|
hseg.Seek(this._RelativeOffsetOfLocalHeader, SeekOrigin.Begin);
|
|
hseg.Write(_EntryHeader, 0, _EntryHeader.Length);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// seek in the raw output stream, to the beginning of the header for
|
|
// this entry.
|
|
// workitem 8098: ok (output)
|
|
s.Seek(this._RelativeOffsetOfLocalHeader, SeekOrigin.Begin);
|
|
|
|
// write the updated header to the output stream
|
|
s.Write(_EntryHeader, 0, _EntryHeader.Length);
|
|
|
|
// adjust the count on the CountingStream as necessary
|
|
if (s1 != null) s1.Adjust(_EntryHeader.Length);
|
|
|
|
// seek in the raw output stream, to the end of the file data
|
|
// for this entry
|
|
s.Seek(_CompressedSize, SeekOrigin.Current);
|
|
}
|
|
}
|
|
|
|
// emit the descriptor - only if not a directory.
|
|
if (((_BitField & 0x0008) == 0x0008) && !IsDirectory)
|
|
{
|
|
byte[] Descriptor = new byte[16 + (_OutputUsesZip64.Value ? 8 : 0)];
|
|
i = 0;
|
|
|
|
// signature
|
|
Array.Copy(BitConverter.GetBytes(ZipConstants.ZipEntryDataDescriptorSignature), 0, Descriptor, i, 4);
|
|
i += 4;
|
|
|
|
// CRC - the correct value now
|
|
Array.Copy(BitConverter.GetBytes(_Crc32), 0, Descriptor, i, 4);
|
|
i += 4;
|
|
|
|
// workitem 7917
|
|
if (_OutputUsesZip64.Value)
|
|
{
|
|
// CompressedSize - the correct value now
|
|
Array.Copy(BitConverter.GetBytes(_CompressedSize), 0, Descriptor, i, 8);
|
|
i += 8;
|
|
|
|
// UncompressedSize - the correct value now
|
|
Array.Copy(BitConverter.GetBytes(_UncompressedSize), 0, Descriptor, i, 8);
|
|
i += 8;
|
|
}
|
|
else
|
|
{
|
|
// CompressedSize - (lower 32 bits) the correct value now
|
|
Descriptor[i++] = (byte)(_CompressedSize & 0x000000FF);
|
|
Descriptor[i++] = (byte)((_CompressedSize & 0x0000FF00) >> 8);
|
|
Descriptor[i++] = (byte)((_CompressedSize & 0x00FF0000) >> 16);
|
|
Descriptor[i++] = (byte)((_CompressedSize & 0xFF000000) >> 24);
|
|
|
|
// UncompressedSize - (lower 32 bits) the correct value now
|
|
Descriptor[i++] = (byte)(_UncompressedSize & 0x000000FF);
|
|
Descriptor[i++] = (byte)((_UncompressedSize & 0x0000FF00) >> 8);
|
|
Descriptor[i++] = (byte)((_UncompressedSize & 0x00FF0000) >> 16);
|
|
Descriptor[i++] = (byte)((_UncompressedSize & 0xFF000000) >> 24);
|
|
}
|
|
|
|
// finally, write the trailing descriptor to the output stream
|
|
s.Write(Descriptor, 0, Descriptor.Length);
|
|
|
|
_LengthOfTrailer += Descriptor.Length;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private void SetZip64Flags()
|
|
{
|
|
// zip64 housekeeping
|
|
_entryRequiresZip64 = new Nullable<bool>
|
|
(_CompressedSize >= 0xFFFFFFFF || _UncompressedSize >= 0xFFFFFFFF || _RelativeOffsetOfLocalHeader >= 0xFFFFFFFF);
|
|
|
|
// validate the ZIP64 usage
|
|
if (_container.Zip64 == Zip64Option.Never && _entryRequiresZip64.Value)
|
|
throw new ZipException("Compressed or Uncompressed size, or offset exceeds the maximum value. Consider setting the UseZip64WhenSaving property on the ZipFile instance.");
|
|
|
|
_OutputUsesZip64 = new Nullable<bool>(_container.Zip64 == Zip64Option.Always || _entryRequiresZip64.Value);
|
|
}
|
|
|
|
|
|
|
|
/// <summary>
|
|
/// Prepare the given stream for output - wrap it in a CountingStream, and
|
|
/// then in a CRC stream, and an encryptor and deflator as appropriate.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Previously this was used in ZipEntry.Write(), but in an effort to
|
|
/// introduce some efficiencies in that method I've refactored to put the
|
|
/// code inline. This method still gets called by ZipOutputStream.
|
|
/// </para>
|
|
/// </remarks>
|
|
internal void PrepOutputStream(Stream s,
|
|
long streamLength,
|
|
out CountingStream outputCounter,
|
|
out Stream encryptor,
|
|
out Stream compressor,
|
|
out Ionic.Crc.CrcCalculatorStream output)
|
|
{
|
|
TraceWriteLine("PrepOutputStream: e({0}) comp({1}) crypto({2}) zf({3})",
|
|
FileName,
|
|
CompressionLevel,
|
|
Encryption,
|
|
_container.Name);
|
|
|
|
// Wrap a counting stream around the raw output stream:
|
|
// This is the last thing that happens before the bits go to the
|
|
// application-provided stream.
|
|
outputCounter = new CountingStream(s);
|
|
|
|
// Sometimes the incoming "raw" output stream is already a CountingStream.
|
|
// Doesn't matter. Wrap it with a counter anyway. We need to count at both
|
|
// levels.
|
|
|
|
if (streamLength != 0L)
|
|
{
|
|
// Maybe wrap an encrypting stream around that:
|
|
// This will happen BEFORE output counting, and AFTER deflation, if encryption
|
|
// is used.
|
|
encryptor = MaybeApplyEncryption(outputCounter);
|
|
|
|
// Maybe wrap a compressing Stream around that.
|
|
// This will happen BEFORE encryption (if any) as we write data out.
|
|
compressor = MaybeApplyCompression(encryptor, streamLength);
|
|
}
|
|
else
|
|
{
|
|
encryptor = compressor = outputCounter;
|
|
}
|
|
// Wrap a CrcCalculatorStream around that.
|
|
// This will happen BEFORE compression (if any) as we write data out.
|
|
output = new Ionic.Crc.CrcCalculatorStream(compressor, true);
|
|
}
|
|
|
|
|
|
|
|
private Stream MaybeApplyCompression(Stream s, long streamLength)
|
|
{
|
|
if (_CompressionMethod == 0x08 && CompressionLevel != Ionic.Zlib.CompressionLevel.None)
|
|
{
|
|
#if !NETCF
|
|
// ParallelDeflateThreshold == 0 means ALWAYS use parallel deflate
|
|
// ParallelDeflateThreshold == -1L means NEVER use parallel deflate
|
|
// Other values specify the actual threshold.
|
|
if (_container.ParallelDeflateThreshold == 0L ||
|
|
(streamLength > _container.ParallelDeflateThreshold &&
|
|
_container.ParallelDeflateThreshold > 0L))
|
|
{
|
|
// This is sort of hacky.
|
|
//
|
|
// It's expensive to create a ParallelDeflateOutputStream, because
|
|
// of the large memory buffers. But the class is unlike most Stream
|
|
// classes in that it can be re-used, so the caller can compress
|
|
// multiple files with it, one file at a time. The key is to call
|
|
// Reset() on it, in between uses.
|
|
//
|
|
// The ParallelDeflateOutputStream is attached to the container
|
|
// itself - there is just one for the entire ZipFile or
|
|
// ZipOutputStream. So it gets created once, per save, and then
|
|
// re-used many times.
|
|
//
|
|
// This approach will break when we go to a "parallel save"
|
|
// approach, where multiple entries within the zip file are being
|
|
// compressed and saved at the same time. But for now it's ok.
|
|
//
|
|
|
|
// instantiate the ParallelDeflateOutputStream
|
|
if (_container.ParallelDeflater == null)
|
|
{
|
|
_container.ParallelDeflater =
|
|
new Ionic.Zlib.ParallelDeflateOutputStream(s,
|
|
CompressionLevel,
|
|
_container.Strategy,
|
|
true);
|
|
// can set the codec buffer size only before the first call to Write().
|
|
if (_container.CodecBufferSize > 0)
|
|
_container.ParallelDeflater.BufferSize = _container.CodecBufferSize;
|
|
if (_container.ParallelDeflateMaxBufferPairs > 0)
|
|
_container.ParallelDeflater.MaxBufferPairs =
|
|
_container.ParallelDeflateMaxBufferPairs;
|
|
}
|
|
// reset it with the new stream
|
|
Ionic.Zlib.ParallelDeflateOutputStream o1 = _container.ParallelDeflater;
|
|
o1.Reset(s);
|
|
return o1;
|
|
}
|
|
#endif
|
|
var o = new Ionic.Zlib.DeflateStream(s, Ionic.Zlib.CompressionMode.Compress,
|
|
CompressionLevel,
|
|
true);
|
|
if (_container.CodecBufferSize > 0)
|
|
o.BufferSize = _container.CodecBufferSize;
|
|
o.Strategy = _container.Strategy;
|
|
return o;
|
|
}
|
|
|
|
|
|
#if BZIP
|
|
if (_CompressionMethod == 0x0c)
|
|
{
|
|
#if !NETCF
|
|
if (_container.ParallelDeflateThreshold == 0L ||
|
|
(streamLength > _container.ParallelDeflateThreshold &&
|
|
_container.ParallelDeflateThreshold > 0L))
|
|
{
|
|
|
|
var o1 = new Ionic.BZip2.ParallelBZip2OutputStream(s, true);
|
|
return o1;
|
|
}
|
|
#endif
|
|
var o = new Ionic.BZip2.BZip2OutputStream(s, true);
|
|
return o;
|
|
}
|
|
#endif
|
|
|
|
return s;
|
|
}
|
|
|
|
|
|
|
|
private Stream MaybeApplyEncryption(Stream s)
|
|
{
|
|
if (Encryption == EncryptionAlgorithm.PkzipWeak)
|
|
{
|
|
TraceWriteLine("MaybeApplyEncryption: e({0}) PKZIP", FileName);
|
|
|
|
return new ZipCipherStream(s, _zipCrypto_forWrite, CryptoMode.Encrypt);
|
|
}
|
|
#if AESCRYPTO
|
|
if (Encryption == EncryptionAlgorithm.WinZipAes128 ||
|
|
Encryption == EncryptionAlgorithm.WinZipAes256)
|
|
{
|
|
TraceWriteLine("MaybeApplyEncryption: e({0}) AES", FileName);
|
|
|
|
return new WinZipAesCipherStream(s, _aesCrypto_forWrite, CryptoMode.Encrypt);
|
|
}
|
|
#endif
|
|
TraceWriteLine("MaybeApplyEncryption: e({0}) None", FileName);
|
|
|
|
return s;
|
|
}
|
|
|
|
|
|
|
|
private void OnZipErrorWhileSaving(Exception e)
|
|
{
|
|
if (_container.ZipFile != null)
|
|
_ioOperationCanceled = _container.ZipFile.OnZipErrorSaving(this, e);
|
|
}
|
|
|
|
|
|
|
|
internal void Write(Stream s)
|
|
{
|
|
var cs1 = s as CountingStream;
|
|
var zss1 = s as ZipSegmentedStream;
|
|
|
|
bool done = false;
|
|
do
|
|
{
|
|
try
|
|
{
|
|
// When the app is updating a zip file, it may be possible to
|
|
// just copy data for a ZipEntry from the source zipfile to the
|
|
// destination, as a block, without decompressing and
|
|
// recompressing, etc. But, in some cases the app modifies the
|
|
// properties on a ZipEntry prior to calling Save(). A change to
|
|
// any of the metadata - the FileName, CompressioLeve and so on,
|
|
// means DotNetZip cannot simply copy through the existing
|
|
// ZipEntry data unchanged.
|
|
//
|
|
// There are two cases:
|
|
//
|
|
// 1. Changes to only metadata, which means the header and
|
|
// central directory must be changed.
|
|
//
|
|
// 2. Changes to the properties that affect the compressed
|
|
// stream, such as CompressionMethod, CompressionLevel, or
|
|
// EncryptionAlgorithm. In this case, DotNetZip must
|
|
// "re-stream" the data: the old entry data must be maybe
|
|
// decrypted, maybe decompressed, then maybe re-compressed
|
|
// and maybe re-encrypted.
|
|
//
|
|
// This test checks if the source for the entry data is a zip file, and
|
|
// if a restream is necessary. If NOT, then it just copies through
|
|
// one entry, potentially changing the metadata.
|
|
|
|
if (_Source == ZipEntrySource.ZipFile && !_restreamRequiredOnSave)
|
|
{
|
|
CopyThroughOneEntry(s);
|
|
return;
|
|
}
|
|
|
|
// Is the entry a directory? If so, the write is relatively simple.
|
|
if (IsDirectory)
|
|
{
|
|
WriteHeader(s, 1);
|
|
StoreRelativeOffset();
|
|
_entryRequiresZip64 = new Nullable<bool>(_RelativeOffsetOfLocalHeader >= 0xFFFFFFFF);
|
|
_OutputUsesZip64 = new Nullable<bool>(_container.Zip64 == Zip64Option.Always || _entryRequiresZip64.Value);
|
|
// handle case for split archives
|
|
if (zss1 != null)
|
|
_diskNumber = zss1.CurrentSegment;
|
|
|
|
return;
|
|
}
|
|
|
|
// At this point, the source for this entry is not a directory, and
|
|
// not a previously created zip file, or the source for the entry IS
|
|
// a previously created zip but the settings whave changed in
|
|
// important ways and therefore we will need to process the
|
|
// bytestream (compute crc, maybe compress, maybe encrypt) in order
|
|
// to write the content into the new zip.
|
|
//
|
|
// We do this in potentially 2 passes: The first time we do it as
|
|
// requested, maybe with compression and maybe encryption. If that
|
|
// causes the bytestream to inflate in size, and if compression was
|
|
// on, then we turn off compression and do it again.
|
|
|
|
|
|
bool readAgain = true;
|
|
int nCycles = 0;
|
|
do
|
|
{
|
|
nCycles++;
|
|
|
|
WriteHeader(s, nCycles);
|
|
|
|
// write the encrypted header
|
|
WriteSecurityMetadata(s);
|
|
|
|
// write the (potentially compressed, potentially encrypted) file data
|
|
_WriteEntryData(s);
|
|
|
|
// track total entry size (including the trailing descriptor and MAC)
|
|
_TotalEntrySize = _LengthOfHeader + _CompressedFileDataSize + _LengthOfTrailer;
|
|
|
|
// The file data has now been written to the stream, and
|
|
// the file pointer is positioned directly after file data.
|
|
|
|
if (nCycles > 1) readAgain = false;
|
|
else if (!s.CanSeek) readAgain = false;
|
|
else readAgain = WantReadAgain();
|
|
|
|
if (readAgain)
|
|
{
|
|
// Seek back in the raw output stream, to the beginning of the file
|
|
// data for this entry.
|
|
|
|
// handle case for split archives
|
|
if (zss1 != null)
|
|
{
|
|
// Console.WriteLine("***_diskNumber/first: {0}", _diskNumber);
|
|
// Console.WriteLine("***_diskNumber/current: {0}", zss.CurrentSegment);
|
|
zss1.TruncateBackward(_diskNumber, _RelativeOffsetOfLocalHeader);
|
|
}
|
|
else
|
|
// workitem 8098: ok (output).
|
|
s.Seek(_RelativeOffsetOfLocalHeader, SeekOrigin.Begin);
|
|
|
|
// If the last entry expands, we read again; but here, we must
|
|
// truncate the stream to prevent garbage data after the
|
|
// end-of-central-directory.
|
|
|
|
// workitem 8098: ok (output).
|
|
s.SetLength(s.Position);
|
|
|
|
// Adjust the count on the CountingStream as necessary.
|
|
if (cs1 != null) cs1.Adjust(_TotalEntrySize);
|
|
}
|
|
}
|
|
while (readAgain);
|
|
_skippedDuringSave = false;
|
|
done = true;
|
|
}
|
|
catch (System.Exception exc1)
|
|
{
|
|
ZipErrorAction orig = this.ZipErrorAction;
|
|
int loop = 0;
|
|
do
|
|
{
|
|
if (ZipErrorAction == ZipErrorAction.Throw)
|
|
throw;
|
|
|
|
if (ZipErrorAction == ZipErrorAction.Skip ||
|
|
ZipErrorAction == ZipErrorAction.Retry)
|
|
{
|
|
// must reset file pointer here.
|
|
// workitem 13903 - seek back only when necessary
|
|
long p1 = (cs1 != null)
|
|
? cs1.ComputedPosition
|
|
: s.Position;
|
|
long delta = p1 - _future_ROLH;
|
|
if (delta > 0)
|
|
{
|
|
s.Seek(delta, SeekOrigin.Current); // may throw
|
|
long p2 = s.Position;
|
|
s.SetLength(s.Position); // to prevent garbage if this is the last entry
|
|
if (cs1 != null) cs1.Adjust(p1 - p2);
|
|
}
|
|
if (ZipErrorAction == ZipErrorAction.Skip)
|
|
{
|
|
WriteStatus("Skipping file {0} (exception: {1})", LocalFileName, exc1.ToString());
|
|
|
|
_skippedDuringSave = true;
|
|
done = true;
|
|
}
|
|
else
|
|
this.ZipErrorAction = orig;
|
|
break;
|
|
}
|
|
|
|
if (loop > 0) throw;
|
|
|
|
if (ZipErrorAction == ZipErrorAction.InvokeErrorEvent)
|
|
{
|
|
OnZipErrorWhileSaving(exc1);
|
|
if (_ioOperationCanceled)
|
|
{
|
|
done = true;
|
|
break;
|
|
}
|
|
}
|
|
loop++;
|
|
}
|
|
while (true);
|
|
}
|
|
}
|
|
while (!done);
|
|
}
|
|
|
|
|
|
internal void StoreRelativeOffset()
|
|
{
|
|
_RelativeOffsetOfLocalHeader = _future_ROLH;
|
|
}
|
|
|
|
|
|
|
|
internal void NotifySaveComplete()
|
|
{
|
|
// When updating a zip file, there are two contexts for properties
|
|
// like Encryption or CompressionMethod - the values read from the
|
|
// original zip file, and the values used in the updated zip file.
|
|
// The _FromZipFile versions are the originals. At the end of a save,
|
|
// these values are the same. So we need to update them. This takes
|
|
// care of the boundary case where a single zipfile instance can be
|
|
// saved multiple times, with distinct changes to the properties on
|
|
// the entries, in between each Save().
|
|
_Encryption_FromZipFile = _Encryption;
|
|
_CompressionMethod_FromZipFile = _CompressionMethod;
|
|
_restreamRequiredOnSave = false;
|
|
_metadataChanged = false;
|
|
//_Source = ZipEntrySource.None;
|
|
_Source = ZipEntrySource.ZipFile; // workitem 10694
|
|
}
|
|
|
|
|
|
internal void WriteSecurityMetadata(Stream outstream)
|
|
{
|
|
if (Encryption == EncryptionAlgorithm.None)
|
|
return;
|
|
|
|
string pwd = this._Password;
|
|
|
|
// special handling for source == ZipFile.
|
|
// Want to support the case where we re-stream an encrypted entry. This will involve,
|
|
// at runtime, reading, decrypting, and decompressing from the original zip file, then
|
|
// compressing, encrypting, and writing to the output zip file.
|
|
|
|
// If that's what we're doing, and the password hasn't been set on the entry,
|
|
// we use the container (ZipFile/ZipOutputStream) password to decrypt.
|
|
// This test here says to use the container password to re-encrypt, as well,
|
|
// with that password, if the entry password is null.
|
|
|
|
if (this._Source == ZipEntrySource.ZipFile && pwd == null)
|
|
pwd = this._container.Password;
|
|
|
|
if (pwd == null)
|
|
{
|
|
_zipCrypto_forWrite = null;
|
|
#if AESCRYPTO
|
|
_aesCrypto_forWrite = null;
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
TraceWriteLine("WriteSecurityMetadata: e({0}) crypto({1}) pw({2})",
|
|
FileName, Encryption.ToString(), pwd);
|
|
|
|
if (Encryption == EncryptionAlgorithm.PkzipWeak)
|
|
{
|
|
// If PKZip (weak) encryption is in use, then the encrypted entry data
|
|
// is preceded by 12-byte "encryption header" for the entry.
|
|
|
|
_zipCrypto_forWrite = ZipCrypto.ForWrite(pwd);
|
|
|
|
// generate the random 12-byte header:
|
|
var rnd = new System.Random();
|
|
byte[] encryptionHeader = new byte[12];
|
|
rnd.NextBytes(encryptionHeader);
|
|
|
|
// workitem 8271
|
|
if ((this._BitField & 0x0008) == 0x0008)
|
|
{
|
|
// In the case that bit 3 of the general purpose bit flag is set to
|
|
// indicate the presence of a 'data descriptor' (signature
|
|
// 0x08074b50), the last byte of the decrypted header is sometimes
|
|
// compared with the high-order byte of the lastmodified time,
|
|
// rather than the high-order byte of the CRC, to verify the
|
|
// password.
|
|
//
|
|
// This is not documented in the PKWare Appnote.txt.
|
|
// This was discovered this by analysis of the Crypt.c source file in the
|
|
// InfoZip library
|
|
// http://www.info-zip.org/pub/infozip/
|
|
|
|
// Also, winzip insists on this!
|
|
_TimeBlob = Ionic.Zip.SharedUtilities.DateTimeToPacked(LastModified);
|
|
encryptionHeader[11] = (byte)((this._TimeBlob >> 8) & 0xff);
|
|
}
|
|
else
|
|
{
|
|
// When bit 3 is not set, the CRC value is required before
|
|
// encryption of the file data begins. In this case there is no way
|
|
// around it: must read the stream in its entirety to compute the
|
|
// actual CRC before proceeding.
|
|
FigureCrc32();
|
|
encryptionHeader[11] = (byte)((this._Crc32 >> 24) & 0xff);
|
|
}
|
|
|
|
// Encrypt the random header, INCLUDING the final byte which is either
|
|
// the high-order byte of the CRC32, or the high-order byte of the
|
|
// _TimeBlob. Must do this BEFORE encrypting the file data. This
|
|
// step changes the state of the cipher, or in the words of the PKZIP
|
|
// spec, it "further initializes" the cipher keys.
|
|
|
|
byte[] cipherText = _zipCrypto_forWrite.EncryptMessage(encryptionHeader, encryptionHeader.Length);
|
|
|
|
// Write the ciphered bonafide encryption header.
|
|
outstream.Write(cipherText, 0, cipherText.Length);
|
|
_LengthOfHeader += cipherText.Length; // 12 bytes
|
|
}
|
|
|
|
#if AESCRYPTO
|
|
else if (Encryption == EncryptionAlgorithm.WinZipAes128 ||
|
|
Encryption == EncryptionAlgorithm.WinZipAes256)
|
|
{
|
|
// If WinZip AES encryption is in use, then the encrypted entry data is
|
|
// preceded by a variable-sized Salt and a 2-byte "password
|
|
// verification" value for the entry.
|
|
|
|
int keystrength = GetKeyStrengthInBits(Encryption);
|
|
_aesCrypto_forWrite = WinZipAesCrypto.Generate(pwd, keystrength);
|
|
outstream.Write(_aesCrypto_forWrite.Salt, 0, _aesCrypto_forWrite._Salt.Length);
|
|
outstream.Write(_aesCrypto_forWrite.GeneratedPV, 0, _aesCrypto_forWrite.GeneratedPV.Length);
|
|
_LengthOfHeader += _aesCrypto_forWrite._Salt.Length + _aesCrypto_forWrite.GeneratedPV.Length;
|
|
|
|
TraceWriteLine("WriteSecurityMetadata: AES e({0}) keybits({1}) _LOH({2})",
|
|
FileName, keystrength, _LengthOfHeader);
|
|
|
|
}
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
private void CopyThroughOneEntry(Stream outStream)
|
|
{
|
|
// Just read the entry from the existing input zipfile and write to the output.
|
|
// But, if metadata has changed (like file times or attributes), or if the ZIP64
|
|
// option has changed, we can re-stream the entry data but must recompute the
|
|
// metadata.
|
|
if (this.LengthOfHeader == 0)
|
|
throw new BadStateException("Bad header length.");
|
|
|
|
// is it necessary to re-constitute new metadata for this entry?
|
|
bool needRecompute = _metadataChanged ||
|
|
(this.ArchiveStream is ZipSegmentedStream) ||
|
|
(outStream is ZipSegmentedStream) ||
|
|
(_InputUsesZip64 && _container.UseZip64WhenSaving == Zip64Option.Never) ||
|
|
(!_InputUsesZip64 && _container.UseZip64WhenSaving == Zip64Option.Always);
|
|
|
|
if (needRecompute)
|
|
CopyThroughWithRecompute(outStream);
|
|
else
|
|
CopyThroughWithNoChange(outStream);
|
|
|
|
// zip64 housekeeping
|
|
_entryRequiresZip64 = new Nullable<bool>
|
|
(_CompressedSize >= 0xFFFFFFFF || _UncompressedSize >= 0xFFFFFFFF ||
|
|
_RelativeOffsetOfLocalHeader >= 0xFFFFFFFF
|
|
);
|
|
|
|
_OutputUsesZip64 = new Nullable<bool>(_container.Zip64 == Zip64Option.Always || _entryRequiresZip64.Value);
|
|
}
|
|
|
|
|
|
|
|
private void CopyThroughWithRecompute(Stream outstream)
|
|
{
|
|
int n;
|
|
byte[] bytes = new byte[BufferSize];
|
|
var input = new CountingStream(this.ArchiveStream);
|
|
|
|
long origRelativeOffsetOfHeader = _RelativeOffsetOfLocalHeader;
|
|
|
|
// The header length may change due to rename of file, add a comment, etc.
|
|
// We need to retain the original.
|
|
int origLengthOfHeader = LengthOfHeader; // including crypto bytes!
|
|
|
|
// WriteHeader() has the side effect of changing _RelativeOffsetOfLocalHeader
|
|
// and setting _LengthOfHeader. While ReadHeader() reads the crypto header if
|
|
// present, WriteHeader() does not write the crypto header.
|
|
WriteHeader(outstream, 0);
|
|
StoreRelativeOffset();
|
|
|
|
if (!this.FileName.EndsWith("/"))
|
|
{
|
|
// Not a directory; there is file data.
|
|
// Seek to the beginning of the entry data in the input stream.
|
|
|
|
long pos = origRelativeOffsetOfHeader + origLengthOfHeader;
|
|
int len = GetLengthOfCryptoHeaderBytes(_Encryption_FromZipFile);
|
|
pos -= len; // want to keep the crypto header
|
|
_LengthOfHeader += len;
|
|
|
|
input.Seek(pos, SeekOrigin.Begin);
|
|
|
|
// copy through everything after the header to the output stream
|
|
long remaining = this._CompressedSize;
|
|
|
|
while (remaining > 0)
|
|
{
|
|
len = (remaining > bytes.Length) ? bytes.Length : (int)remaining;
|
|
|
|
// read
|
|
n = input.Read(bytes, 0, len);
|
|
//_CheckRead(n);
|
|
|
|
// write
|
|
outstream.Write(bytes, 0, n);
|
|
remaining -= n;
|
|
OnWriteBlock(input.BytesRead, this._CompressedSize);
|
|
if (_ioOperationCanceled)
|
|
break;
|
|
}
|
|
|
|
// bit 3 descriptor
|
|
if ((this._BitField & 0x0008) == 0x0008)
|
|
{
|
|
int size = 16;
|
|
if (_InputUsesZip64) size += 8;
|
|
byte[] Descriptor = new byte[size];
|
|
input.Read(Descriptor, 0, size);
|
|
|
|
if (_InputUsesZip64 && _container.UseZip64WhenSaving == Zip64Option.Never)
|
|
{
|
|
// original descriptor was 24 bytes, now we need 16.
|
|
// Must check for underflow here.
|
|
// signature + CRC.
|
|
outstream.Write(Descriptor, 0, 8);
|
|
|
|
// Compressed
|
|
if (_CompressedSize > 0xFFFFFFFF)
|
|
throw new InvalidOperationException("ZIP64 is required");
|
|
outstream.Write(Descriptor, 8, 4);
|
|
|
|
// UnCompressed
|
|
if (_UncompressedSize > 0xFFFFFFFF)
|
|
throw new InvalidOperationException("ZIP64 is required");
|
|
outstream.Write(Descriptor, 16, 4);
|
|
_LengthOfTrailer -= 8;
|
|
}
|
|
else if (!_InputUsesZip64 && _container.UseZip64WhenSaving == Zip64Option.Always)
|
|
{
|
|
// original descriptor was 16 bytes, now we need 24
|
|
// signature + CRC
|
|
byte[] pad = new byte[4];
|
|
outstream.Write(Descriptor, 0, 8);
|
|
// Compressed
|
|
outstream.Write(Descriptor, 8, 4);
|
|
outstream.Write(pad, 0, 4);
|
|
// UnCompressed
|
|
outstream.Write(Descriptor, 12, 4);
|
|
outstream.Write(pad, 0, 4);
|
|
_LengthOfTrailer += 8;
|
|
}
|
|
else
|
|
{
|
|
// same descriptor on input and output. Copy it through.
|
|
outstream.Write(Descriptor, 0, size);
|
|
//_LengthOfTrailer += size;
|
|
}
|
|
}
|
|
}
|
|
|
|
_TotalEntrySize = _LengthOfHeader + _CompressedFileDataSize + _LengthOfTrailer;
|
|
}
|
|
|
|
|
|
private void CopyThroughWithNoChange(Stream outstream)
|
|
{
|
|
int n;
|
|
byte[] bytes = new byte[BufferSize];
|
|
var input = new CountingStream(this.ArchiveStream);
|
|
|
|
// seek to the beginning of the entry data in the input stream
|
|
input.Seek(this._RelativeOffsetOfLocalHeader, SeekOrigin.Begin);
|
|
|
|
if (this._TotalEntrySize == 0)
|
|
{
|
|
// We've never set the length of the entry.
|
|
// Set it here.
|
|
this._TotalEntrySize = this._LengthOfHeader + this._CompressedFileDataSize + _LengthOfTrailer;
|
|
|
|
// The CompressedSize includes all the leading metadata associated
|
|
// to encryption, if any, as well as the compressed data, or
|
|
// compressed-then-encrypted data, and the trailer in case of AES.
|
|
|
|
// The CompressedFileData size is the same, less the encryption
|
|
// framing data (12 bytes header for PKZip; 10/18 bytes header and
|
|
// 10 byte trailer for AES).
|
|
|
|
// The _LengthOfHeader includes all the zip entry header plus the
|
|
// crypto header, if any. The _LengthOfTrailer includes the
|
|
// 10-byte MAC for AES, where appropriate, and the bit-3
|
|
// Descriptor, where applicable.
|
|
}
|
|
|
|
|
|
// workitem 5616
|
|
// remember the offset, within the output stream, of this particular entry header.
|
|
// This may have changed if any of the other entries changed (eg, if a different
|
|
// entry was removed or added.)
|
|
var counter = outstream as CountingStream;
|
|
_RelativeOffsetOfLocalHeader = (counter != null)
|
|
? counter.ComputedPosition
|
|
: outstream.Position; // BytesWritten
|
|
|
|
// copy through the header, filedata, trailer, everything...
|
|
long remaining = this._TotalEntrySize;
|
|
while (remaining > 0)
|
|
{
|
|
int len = (remaining > bytes.Length) ? bytes.Length : (int)remaining;
|
|
|
|
// read
|
|
n = input.Read(bytes, 0, len);
|
|
//_CheckRead(n);
|
|
|
|
// write
|
|
outstream.Write(bytes, 0, n);
|
|
remaining -= n;
|
|
OnWriteBlock(input.BytesRead, this._TotalEntrySize);
|
|
if (_ioOperationCanceled)
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
[System.Diagnostics.ConditionalAttribute("Trace")]
|
|
private void TraceWriteLine(string format, params object[] varParams)
|
|
{
|
|
lock (_outputLock)
|
|
{
|
|
int tid = System.Threading.Thread.CurrentThread.GetHashCode();
|
|
#if ! (NETCF || SILVERLIGHT)
|
|
Console.ForegroundColor = (ConsoleColor)(tid % 8 + 8);
|
|
#endif
|
|
Console.Write("{0:000} ZipEntry.Write ", tid);
|
|
Console.WriteLine(format, varParams);
|
|
#if ! (NETCF || SILVERLIGHT)
|
|
Console.ResetColor();
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private object _outputLock = new Object();
|
|
}
|
|
}
|