SubtitleEdit/libse/ZipExtractor.cs
2019-09-29 14:33:26 +02:00

468 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ZipStorer, by Jaime Olivares
// Website: zipstorer.codeplex.com
// Version: 2.35 (March 14, 2010)
// Simplified to extract-only by Nikse - August 18, 2010
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;
namespace Nikse.SubtitleEdit.Core
{
/// <summary>
/// Zip archive decompression. Represents a Zip file.
/// </summary>
public class ZipExtractor : IDisposable
{
/// <summary>
/// Compression method enumeration
/// </summary>
public enum Compression : ushort
{
/// <summary>Uncompressed storage</summary>
Store = 0,
/// <summary>Deflate compression method</summary>
Deflate = 8
}
/// <summary>
/// Represents an entry in Zip file directory
/// </summary>
public struct ZipFileEntry
{
/// <summary>Compression method</summary>
public Compression Method;
/// <summary>Full path and filename as stored in Zip</summary>
public string FilenameInZip;
/// <summary>Original file size</summary>
public uint FileSize;
/// <summary>Compressed file size</summary>
public uint CompressedSize;
/// <summary>Offset of header information inside Zip storage</summary>
public uint HeaderOffset;
/// <summary>Offset of file inside Zip storage</summary>
public uint FileOffset;
/// <summary>Size of header information</summary>
public uint HeaderSize;
/// <summary>32-bit checksum of entire file</summary>
public uint Crc32;
/// <summary>Last modification time of file</summary>
public DateTime ModifyTime;
/// <summary>User comment for file</summary>
public string Comment;
/// <summary>Overriden method</summary>
/// <returns>Filename in Zip</returns>
public override string ToString()
{
return FilenameInZip;
}
}
#region Public fields
#endregion Public fields
#region Private fields
// Stream object of storage file
private Stream ZipFileStream;
// Central dir image
private byte[] CentralDirImage;
// Static CRC32 Table
private static UInt32[] CrcTable;
// Default filename encoder
private static Encoding DefaultEncoding = Encoding.GetEncoding(437);
#endregion Private fields
#region Public methods
// Static constructor. Just invoked once in order to create the CRC32 lookup table.
static ZipExtractor()
{
// Generate CRC32 table
CrcTable = new UInt32[256];
for (int i = 0; i < CrcTable.Length; i++)
{
UInt32 c = (UInt32)i;
for (int j = 0; j < 8; j++)
{
if ((c & 1) != 0)
{
c = 3988292384 ^ (c >> 1);
}
else
{
c >>= 1;
}
}
CrcTable[i] = c;
}
}
/// <summary>
/// Method to open an existing storage file
/// </summary>
/// <param name="filename">Full path of Zip file to open</param>
/// <returns>A valid ZipStorer object</returns>
public static ZipExtractor Open(string filename)
{
Stream stream = new FileStream(filename, FileMode.Open, FileAccess.Read);
ZipExtractor zip = Open(stream);
return zip;
}
/// <summary>
/// Method to open an existing storage from stream
/// </summary>
/// <param name="stream">Already opened stream with zip contents</param>
/// <returns>A valid ZipStorer object</returns>
public static ZipExtractor Open(Stream stream)
{
ZipExtractor zip = new ZipExtractor();
zip.ZipFileStream = stream;
if (zip.ReadFileInfo())
{
return zip;
}
throw new InvalidDataException();
}
/// <summary>
/// Close the Zip storage
/// </summary>
/// <remarks>This is a required step, unless automatic dispose is used</remarks>
public void Close()
{
if (this.ZipFileStream != null)
{
this.ZipFileStream.Flush();
this.ZipFileStream.Dispose();
this.ZipFileStream = null;
}
}
/// <summary>
/// Read all the file records in the central directory
/// </summary>
/// <returns>List of all entries in directory</returns>
public List<ZipFileEntry> ReadCentralDir()
{
if (this.CentralDirImage == null)
{
throw new InvalidOperationException("Central directory currently does not exist");
}
List<ZipFileEntry> result = new List<ZipFileEntry>();
for (int pointer = 0; pointer < this.CentralDirImage.Length;)
{
uint signature = BitConverter.ToUInt32(CentralDirImage, pointer);
if (signature != 0x02014b50)
{
break;
}
bool encodeUtf8 = (BitConverter.ToUInt16(CentralDirImage, pointer + 8) & 0x0800) != 0;
ushort method = BitConverter.ToUInt16(CentralDirImage, pointer + 10);
uint modifyTime = BitConverter.ToUInt32(CentralDirImage, pointer + 12);
uint crc32 = BitConverter.ToUInt32(CentralDirImage, pointer + 16);
uint comprSize = BitConverter.ToUInt32(CentralDirImage, pointer + 20);
uint fileSize = BitConverter.ToUInt32(CentralDirImage, pointer + 24);
ushort filenameSize = BitConverter.ToUInt16(CentralDirImage, pointer + 28);
ushort extraSize = BitConverter.ToUInt16(CentralDirImage, pointer + 30);
ushort commentSize = BitConverter.ToUInt16(CentralDirImage, pointer + 32);
uint headerOffset = BitConverter.ToUInt32(CentralDirImage, pointer + 42);
uint headerSize = (uint)(46 + filenameSize + extraSize + commentSize);
Encoding encoder = encodeUtf8 ? Encoding.UTF8 : DefaultEncoding;
ZipFileEntry zfe = new ZipFileEntry();
zfe.Method = (Compression)method;
zfe.FilenameInZip = encoder.GetString(CentralDirImage, pointer + 46, filenameSize);
zfe.FileOffset = GetFileOffset(headerOffset);
zfe.FileSize = fileSize;
zfe.CompressedSize = comprSize;
zfe.HeaderOffset = headerOffset;
zfe.HeaderSize = headerSize;
zfe.Crc32 = crc32;
zfe.ModifyTime = DosTimeToDateTime(modifyTime);
if (commentSize > 0)
{
zfe.Comment = encoder.GetString(CentralDirImage, pointer + 46 + filenameSize + extraSize, commentSize);
}
result.Add(zfe);
pointer += (46 + filenameSize + extraSize + commentSize);
}
return result;
}
/// <summary>
/// Copy the contents of a stored file into a physical file
/// </summary>
/// <param name="zfe">Entry information of file to extract</param>
/// <param name="filename">Name of file to store uncompressed data</param>
/// <returns>True if success, false if not.</returns>
/// <remarks>Unique compression methods are Store and Deflate</remarks>
public bool ExtractFile(ZipFileEntry zfe, string filename)
{
// Make sure the parent directory exist
string path = Path.GetDirectoryName(filename);
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
// Check it is directory. If so, do nothing
if (Directory.Exists(filename))
{
return true;
}
bool result;
using (Stream output = new FileStream(filename, FileMode.Create, FileAccess.Write))
{
result = ExtractFile(zfe, output);
}
File.SetCreationTime(filename, zfe.ModifyTime);
File.SetLastWriteTime(filename, zfe.ModifyTime);
return result;
}
/// <summary>
/// Copy the contents of a stored file into an opened stream
/// </summary>
/// <param name="zfe">Entry information of file to extract</param>
/// <param name="stream">Stream to store the uncompressed data</param>
/// <returns>True if success, false if not.</returns>
/// <remarks>Unique compression methods are Store and Deflate</remarks>
public bool ExtractFile(ZipFileEntry zfe, Stream stream)
{
if (!stream.CanWrite)
{
throw new InvalidOperationException("Stream cannot be written");
}
// check signature
byte[] signature = new byte[4];
this.ZipFileStream.Seek(zfe.HeaderOffset, SeekOrigin.Begin);
this.ZipFileStream.Read(signature, 0, 4);
if (BitConverter.ToUInt32(signature, 0) != 0x04034b50)
{
return false;
}
// Select input stream for inflating or just reading
Stream inStream;
if (zfe.Method == Compression.Store)
{
inStream = this.ZipFileStream;
}
else if (zfe.Method == Compression.Deflate)
{
inStream = new DeflateStream(this.ZipFileStream, CompressionMode.Decompress, true);
}
else
{
return false;
}
// Buffered copy
byte[] buffer = new byte[16384];
this.ZipFileStream.Seek(zfe.FileOffset, SeekOrigin.Begin);
uint bytesPending = zfe.FileSize;
while (bytesPending > 0)
{
int bytesRead = inStream.Read(buffer, 0, (int)Math.Min(bytesPending, buffer.Length));
stream.Write(buffer, 0, bytesRead);
bytesPending -= (uint)bytesRead;
}
stream.Flush();
if (zfe.Method == Compression.Deflate)
{
inStream.Dispose();
}
return true;
}
#endregion Public methods
#region Private methods
/// <summary>
/// Calculate the file offset by reading the corresponding local header
/// </summary>
private uint GetFileOffset(uint headerOffset)
{
byte[] buffer = new byte[2];
ZipFileStream.Seek(headerOffset + 26, SeekOrigin.Begin);
ZipFileStream.Read(buffer, 0, 2);
ushort filenameSize = BitConverter.ToUInt16(buffer, 0);
ZipFileStream.Read(buffer, 0, 2);
ushort extraSize = BitConverter.ToUInt16(buffer, 0);
return (uint)(30 + filenameSize + extraSize + headerOffset);
}
/* Local file header:
local file header signature 4 bytes (0x04034b50)
version needed to extract 2 bytes
general purpose bit flag 2 bytes
compression method 2 bytes
last mod file time 2 bytes
last mod file date 2 bytes
crc-32 4 bytes
compressed size 4 bytes
uncompressed size 4 bytes
filename length 2 bytes
extra field length 2 bytes
filename (variable size)
extra field (variable size)
*/
/* Central directory's File header:
central file header signature 4 bytes (0x02014b50)
version made by 2 bytes
version needed to extract 2 bytes
general purpose bit flag 2 bytes
compression method 2 bytes
last mod file time 2 bytes
last mod file date 2 bytes
crc-32 4 bytes
compressed size 4 bytes
uncompressed size 4 bytes
filename length 2 bytes
extra field length 2 bytes
file comment length 2 bytes
disk number start 2 bytes
internal file attributes 2 bytes
external file attributes 4 bytes
relative offset of local header 4 bytes
filename (variable size)
extra field (variable size)
file comment (variable size)
*
/* End of central dir record:
end of central dir signature 4 bytes (0x06054b50)
number of this disk 2 bytes
number of the disk with the
start of the central directory 2 bytes
total number of entries in
the central dir on this disk 2 bytes
total number of entries in
the central dir 2 bytes
size of the central directory 4 bytes
offset of start of central
directory with respect to
the starting disk number 4 bytes
zipfile comment length 2 bytes
zipfile comment (variable size)
*/
/* DOS Date and time:
MS-DOS date. The date is a packed value with the following format. Bits Description
0-4 Day of the month (131)
5-8 Month (1 = January, 2 = February, and so on)
9-15 Year offset from 1980 (add 1980 to get actual year)
MS-DOS time. The time is a packed value with the following format. Bits Description
0-4 Second divided by 2
5-10 Minute (059)
11-15 Hour (023 on a 24-hour clock)
*/
private static DateTime DosTimeToDateTime(uint dt)
{
return new DateTime(
(int)(dt >> 25) + 1980,
(int)(dt >> 21) & 15,
(int)(dt >> 16) & 31,
(int)(dt >> 11) & 31,
(int)(dt >> 5) & 63,
(int)(dt & 31) * 2);
}
// Reads the end-of-central-directory record
private bool ReadFileInfo()
{
if (ZipFileStream.Length < 22)
{
return false;
}
try
{
ZipFileStream.Seek(-17, SeekOrigin.End);
var br = new BinaryReader(this.ZipFileStream);
do
{
ZipFileStream.Seek(-5, SeekOrigin.Current);
var sig = br.ReadUInt32();
if (sig == 0x06054b50)
{
ZipFileStream.Seek(6, SeekOrigin.Current);
br.ReadUInt16();
var centralSize = br.ReadInt32();
var centralDirOffset = br.ReadUInt32();
var commentSize = br.ReadUInt16();
// check if comment field is the very last data in file
if (ZipFileStream.Position + commentSize != this.ZipFileStream.Length)
{
return false;
}
// Copy entire central directory to a memory buffer
CentralDirImage = new byte[centralSize];
ZipFileStream.Seek(centralDirOffset, SeekOrigin.Begin);
ZipFileStream.Read(this.CentralDirImage, 0, centralSize);
// Leave the pointer at the beginning of central dir, to append new files
ZipFileStream.Seek(centralDirOffset, SeekOrigin.Begin);
return true;
}
} while (ZipFileStream.Position > 0);
}
catch
{
// ignored
}
return false;
}
#endregion Private methods
#region IDisposable Members
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
Close();
}
}
#endregion IDisposable Members
}
}