修正ZipFile错误,XCode在NET40下基本测试通过
大石头 authored at 2019-07-06 17:51:36
18.88 KiB
X
#if NET4
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Xml.Serialization;
using NewLife;
using NewLife.Reflection;
using NewLife.Security;
using NewLife.Serialization;

namespace System.IO.Compression
{
    /// <summary>Zip实体。包含文件头信息和文件位置</summary>
    public class ZipEntry : IDisposable
    {
        #region 数据属性
#pragma warning disable 0169, 0649

        /// <summary>签名</summary>
        public UInt32 Signature;

        // ZipDirEntry成员
        /// <summary>系统类型</summary>
        readonly HostSystem VersionMadeBy;

        /// <summary>解压缩所需要的版本</summary>
        public UInt16 VersionNeeded;

        /// <summary>标识位</summary>
        readonly GeneralBitFlags BitField;

        /// <summary>压缩方法</summary>
        public CompressionMethod CompressionMethod;

        private Int32 _LastModified;
        /// <summary>最后修改时间</summary>
        [XmlIgnore]
        public DateTime LastModified { get => ZipFile.DosDateTimeToFileTime(_LastModified); set => _LastModified = ZipFile.FileTimeToDosDateTime(value); }

        /// <summary>CRC校验</summary>
        public UInt32 Crc;

        /// <summary>压缩后大小</summary>
        public UInt32 CompressedSize;

        /// <summary>原始大小</summary>
        public UInt32 UncompressedSize;

        /// <summary>文件名长度</summary>
        private readonly UInt16 FileNameLength;

        /// <summary>扩展数据长度</summary>
        private readonly UInt16 ExtraFieldLength;

        // ZipDirEntry成员
        /// <summary>注释长度</summary>
        private readonly UInt16 CommentLength;

        // ZipDirEntry成员
        /// <summary>分卷号。</summary>
        public UInt16 DiskNumber;

        // ZipDirEntry成员
        /// <summary>内部文件属性</summary>
        public UInt16 InternalFileAttrs;

        // ZipDirEntry成员
        /// <summary>扩展文件属性</summary>
        public UInt32 ExternalFileAttrs;

        // ZipDirEntry成员
        /// <summary>文件头相对位移</summary>
        public UInt32 RelativeOffsetOfLocalHeader;

        /// <summary>文件名,如果是目录,则以/结束</summary>
        [FieldSize("FileNameLength")]
        public String FileName;

        /// <summary>扩展字段</summary>
        [FieldSize("ExtraFieldLength")]
        public Byte[] ExtraField;

        // ZipDirEntry成员
        /// <summary>注释</summary>
        [FieldSize("CommentLength")]
        public String Comment;

#pragma warning restore 0169, 0649
        #endregion

        #region 属性
        /// <summary>是否目录</summary>
        [XmlIgnore]
        public Boolean IsDirectory => ("" + FileName).EndsWith(ZipFile.DirSeparator);

        /// <summary>数据源</summary>
        [NonSerialized]
        private IDataSource DataSource;

        /// <summary>全名</summary>
        public String FullName => FileName;

        /// <summary>长度</summary>
        public Int32 Length => ExtraFieldLength;
        #endregion

        #region 构造
        /// <summary>实例化Zip实体</summary>
        public ZipEntry()
        {
            Signature = ZipConstants.ZipEntrySignature;
            VersionNeeded = 20;
        }

        /// <summary>释放资源</summary>
        public void Dispose()
        {
            if (DataSource != null)
            {
                try
                {
                    DataSource.Dispose();
                    DataSource = null;
                }
                catch { }
            }
        }
        #endregion

        #region 读取核心
        internal static ZipEntry ReadEntry(ZipFile zipfile, Stream stream, Boolean first, Boolean embedFileData)
        {
            var reader = zipfile.CreateReader(stream);
            // 读取文件头时忽略掉这些字段,这些都是DirEntry的字段
            //reader.Settings.IgnoreMembers = dirMembers;

            var bn = (reader as Binary);
            if (bn != null) bn.IgnoreMembers = dirMembers;

            // 有时候Zip文件以PK00开头
            if (first)
            {
                if (reader.Read<UInt32>() != ZipConstants.PackedToRemovableMedia) reader.Stream.Position -= 4;
            }

            // 验证头部
            var v = reader.Read<UInt32>();
            if (v != ZipConstants.ZipEntrySignature)
            {
                if (v != ZipConstants.ZipDirEntrySignature && v != ZipConstants.EndOfCentralDirectorySignature)
                    throw new ZipException("0x{0:X8}处签名错误!", stream.Position);

                return null;
            }
            reader.Stream.Position -= 4;

            var entry = reader.Read<ZipEntry>();
            if (entry.IsDirectory) return entry;

            // 0长度的实体不要设置数据源
            if (entry.CompressedSize > 0)
            {
                // 是否内嵌文件数据
                entry.DataSource = embedFileData ? new ArrayDataSource(stream, (Int32)entry.CompressedSize) : new StreamDataSource(stream);
                entry.DataSource.IsCompressed = entry.CompressionMethod != CompressionMethod.Stored;

                // 移到文件数据之后,可能是文件头
                if (!embedFileData) stream.Seek(entry.CompressedSize, SeekOrigin.Current);
            }

            // 如果有扩展,则跳过
            if (entry.BitField.Has(GeneralBitFlags.Descriptor))
            {
                //stream.Seek(20, SeekOrigin.Current);

                // 在某些只读流中,可能无法回头设置校验和大小,此时可通过描述符在文件内容之后设置
                entry.Crc = reader.Read<UInt32>();
                entry.CompressedSize = reader.Read<UInt32>();
                entry.UncompressedSize = reader.Read<UInt32>();
            }

            return entry;
        }

        internal static ZipEntry ReadDirEntry(ZipFile zipfile, Stream stream)
        {
            var reader = zipfile.CreateReader(stream);

            var v = reader.Read<UInt32>();
            if (v != ZipConstants.ZipDirEntrySignature)
            {
                if (v != ZipConstants.EndOfCentralDirectorySignature && v != ZipConstants.ZipEntrySignature)
                {
                    throw new ZipException("0x{0:X8}处签名错误!", stream.Position);
                }
                return null;
            }
            reader.Stream.Position -= 4;

            var entry = reader.Read<ZipEntry>();
            return entry;
        }
        #endregion

        #region 写入核心
        internal void Write(IFormatterX writer)
        {
            Signature = ZipConstants.ZipEntrySignature;

            // 取得数据流位置
            RelativeOffsetOfLocalHeader = (UInt32)writer.Stream.Position;

            if (IsDirectory)
            {
                // 写入头部
                writer.Write(this);

                return;
            }

            var dsLen = (Int32)DataSource.Length;
            //if (dsLen <= 0) CompressionMethod = CompressionMethod.Stored;

            // 计算签名和大小
            if (Crc == 0) Crc = DataSource.GetCRC();
            if (UncompressedSize == 0) UncompressedSize = (UInt32)dsLen;
            if (CompressionMethod == CompressionMethod.Stored) CompressedSize = UncompressedSize;
            if (DataSource.IsCompressed) CompressedSize = (UInt32)dsLen;

            // 写入头部
            writer.Write(this);

            // 没有数据,直接跳过
            if (dsLen <= 0) return;

            #region 写入文件数据
            // 数据源。只能用一次,因为GetData的时候把数据流移到了合适位置
            var source = DataSource.GetData();
            if (DataSource.IsCompressed)
            {
                // 可能数据源是曾经被压缩过了的,比如刚解压的实体
                source.CopyTo(writer.Stream, 0, dsLen);
                return;
            }

            switch (CompressionMethod)
            {
                case CompressionMethod.Stored:
                    // 原始数据流直接拷贝到目标。必须指定大小,否则可能读过界
                    source.CopyTo(writer.Stream, 0, dsLen);
                    break;
                case CompressionMethod.Deflated:
                    {
                        // 记录数据流位置,待会用来计算已压缩大小
                        var p = writer.Stream.Position;
                        using (var stream = new DeflateStream(writer.Stream, CompressionMode.Compress, true))
                        {
                            source.CopyTo(stream);
                            stream.Close();
                        }
                        CompressedSize = (UInt32)(writer.Stream.Position - p);

                        // 回头重新修正压缩后大小CompressedSize
                        p = writer.Stream.Position;
                        // 计算好压缩大小字段所在位置
                        writer.Stream.Seek(RelativeOffsetOfLocalHeader + 18, SeekOrigin.Begin);
                        //var wr = writer as IWriter2;
                        //wr.Write(CompressedSize);
                        writer.Write(CompressedSize);
                        writer.Stream.Seek(p, SeekOrigin.Begin);
                    }

                    break;
                default:
                    throw new XException("无法处理的压缩算法{0}!", CompressionMethod);
            }
            #endregion
        }

        internal void WriteDir(IFormatterX writer)
        {
            Signature = ZipConstants.ZipDirEntrySignature;

            // 写入头部
            writer.Write(this);
        }
        #endregion

        #region 解压缩
        /// <summary>解压缩</summary>
        /// <param name="destinationFileName">目标路径</param>
        /// <param name="overrideExisting">是否覆盖已有文件</param>
        public void ExtractToFile(String destinationFileName, Boolean overrideExisting = true)
        {
            if (destinationFileName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(destinationFileName));

            if (!IsDirectory)
            {
                //if (DataSource == null) throw new ZipException("文件数据不正确!");
                //if (CompressedSize <= 0) throw new ZipException("文件大小不正确!");

                var file = destinationFileName.GetFullPath();
                if (!overrideExisting && File.Exists(file)) return;

                file.EnsureDirectory(true);

                using (var stream = File.Create(file))
                {
                    // 没有数据直接跳过。放到这里,保证已经创建文件
                    if (DataSource != null && DataSource.Length > 0 && CompressedSize > 0) Extract(stream);
                }

                // 修正时间
                if (LastModified > ZipFile.MinDateTime)
                {
                    var fi = new FileInfo(file)
                    {
                        LastWriteTime = LastModified
                    };
                }
            }
            else
            {
                destinationFileName = Path.Combine(destinationFileName, FileName);
                if (!Directory.Exists(destinationFileName)) Directory.CreateDirectory(destinationFileName);
            }
        }

        /// <summary>解压缩</summary>
        /// <param name="outStream">目标数据流</param>
        public void Extract(Stream outStream)
        {
            if (outStream == null || !outStream.CanWrite) throw new ArgumentNullException(nameof(outStream));
            //if (DataSource == null) throw new ZipException("文件数据不正确!");

            // 没有数据直接跳过
            if (DataSource == null || DataSource.Length <= 0) return;

            if (CompressedSize <= 0) throw new ZipException("文件大小不正确!");

            try
            {
                switch (CompressionMethod)
                {
                    case CompressionMethod.Stored:
                        DataSource.GetData().CopyTo(outStream);
                        break;
                    case CompressionMethod.Deflated:
                        using (var stream = new DeflateStream(DataSource.GetData(), CompressionMode.Decompress, true))
                        {
                            stream.CopyTo(outStream);
                            stream.Close();
                        }
                        break;
                    default:
                        throw new XException("无法处理的压缩算法{0}!", CompressionMethod);
                }
            }
            catch (Exception ex) { throw new ZipException(String.Format("解压缩{0}时出错!", FileName), ex); }
        }
        #endregion

        #region 压缩
        /// <summary>从文件创建实体</summary>
        /// <param name="fileName"></param>
        /// <param name="entryName"></param>
        /// <param name="stored"></param>
        /// <returns></returns>
        public static ZipEntry Create(String fileName, String entryName = null, Boolean? stored = false)
        {
            if (entryName.IsNullOrEmpty())
            {
                if (fileName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(fileName));

                entryName = Path.GetFileName(fileName);
            }

            IDataSource ds = null;
            if (!entryName.EndsWith(ZipFile.DirSeparator))
            {
                if (fileName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(fileName));

                ds = new FileDataSource(fileName);
            }

            var entry = Create(entryName, ds, stored);

            // 读取最后修改时间
            if (entry.LastModified <= ZipFile.MinDateTime)
            {
                var fi = new FileInfo(fileName);
                entry.LastModified = fi.LastWriteTime;
            }

            return entry;
        }

        /// <summary>从数据流创建实体</summary>
        /// <param name="stream"></param>
        /// <param name="entryName"></param>
        /// <param name="stored"></param>
        /// <param name="embedFileData"></param>
        /// <returns></returns>
        public static ZipEntry Create(Stream stream, String entryName, Boolean stored = false, Boolean embedFileData = false)
        {
            //if (stream == null) throw new ArgumentNullException("stream");

            // 有可能从文件流中获取文件名
            String fileName = null;
            if (String.IsNullOrEmpty(entryName) && stream != null && stream is FileStream)
                entryName = fileName = Path.GetFileName((stream as FileStream).Name);

            if (entryName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(entryName));

            // 是否内嵌文件数据
            IDataSource ds = null;
            if (stream != null) ds = embedFileData ? new ArrayDataSource(stream, -1) : new StreamDataSource(stream);

            var entry = Create(entryName, ds, stored);

            // 读取最后修改时间
            if (!String.IsNullOrEmpty(fileName) && entry.LastModified <= ZipFile.MinDateTime)
            {
                var fi = new FileInfo(fileName);
                entry.LastModified = fi.LastWriteTime;
            }

            return entry;
        }

        private static ZipEntry Create(String entryName, IDataSource datasource, Boolean? stored)
        {
            if (entryName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(entryName));
            entryName = entryName.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);

            var entry = new ZipEntry
            {
                FileName = entryName,
                CompressionMethod = stored ?? IsZip(entryName) ? CompressionMethod.Stored : CompressionMethod.Deflated,
                DataSource = datasource
            };

            return entry;
        }
        #endregion

        #region 辅助
        internal static readonly ICollection<String> dirMembers = new HashSet<String>(new String[] {
            "VersionMadeBy", "CommentLength", "DiskNumber", "InternalFileAttrs", "ExternalFileAttrs", "RelativeOffsetOfLocalHeader", "Comment" }, StringComparer.OrdinalIgnoreCase);

        /// <summary>复制DirEntry专属的字段</summary>
        /// <param name="entry"></param>
        internal void CopyFromDirEntry(ZipEntry entry)
        {
            foreach (var item in dirMembers)
            {
                this.SetValue(item, entry.GetValue(item));
            }
        }

        /// <summary>已重载。</summary>
        /// <returns></returns>
        public override String ToString() => FileName;

        static readonly String[] zips = new String[] { ".zip", ".rar", ".iso" };
        static Boolean IsZip(String name)
        {
            var ext = Path.GetExtension(name);
            if (String.IsNullOrEmpty(ext)) return false;

            ext = ext.ToLower();
            return Array.IndexOf(zips, ext) >= 0;
        }
        #endregion

        #region 数据源
        interface IDataSource : IDisposable
        {
            Stream GetData();

            UInt32 GetCRC();

            Int64 Length { get; }

            Boolean IsCompressed { get; set; }
        }

        #region 数据流
        class StreamDataSource : IDataSource
        {
            /// <summary>数据流</summary>
            public virtual Stream Stream { get; protected set; }

            /// <summary>位移</summary>
            public Int64 Offset { get; }

            /// <summary>长度</summary>
            public Int64 Length { get; }

            /// <summary>是否被压缩</summary>
            public Boolean IsCompressed { get; set; }

            public StreamDataSource(Stream stream)
            {
                Stream = stream;
                Offset = stream.Position;
                Length = stream.Length;
            }

            public void Dispose() => Stream.TryDispose();

            #region IDataSource 成员
            public Stream GetData()
            {
                Stream.Seek(Offset, SeekOrigin.Begin);
                return Stream;
            }

            public UInt32 GetCRC()
            {
                Stream.Seek(Offset, SeekOrigin.Begin);
                return new Crc32().Update(Stream, Length).Value;
            }
            #endregion
        }
        #endregion

        #region 字节数组
        class ArrayDataSource : StreamDataSource
        {
            public ArrayDataSource(Stream stream, Int32 length) : base(new MemoryStream(stream.ReadBytes(length))) { }
        }
        #endregion

        #region 文件
        class FileDataSource : StreamDataSource
        {
            /// <summary>文件名</summary>
            public String FileName { get; set; }

            public FileDataSource(String fileName) : base(new FileStream(fileName, FileMode.Open, FileAccess.Read)) => FileName = fileName;
        }
        #endregion
        #endregion
    }
}
#endif