ShellのCopyHereを使わずに素の.netでZIPファイルを扱う

.net 3.0のプライベートクラスを利用したZIP圧縮・展開の記事を書いた。
素の.net 3.0以降でZIPファイルを扱う - じゅんじゅんのきまぐれ
しかし、更新日付問題があった。
これを解決してみた。
多分、、、.net 2.0以降で動くんじゃなかろうか。



原理

追記 2014/10/02
展開時のメモリー使用量を少しだけ削減

汎用的に作るとどんどん肥大化していくので、超割切り仕様です。


ZIPファイル作成は、

  1. ZipArchiveMinをインスタンス化する(引数は出力先Stream)
  2. ZipArchiveMin.Entryをインスタンス化し以下の設定をする
    1. ZipArchiveMin.Entry.Nameにファイル名(パス付可)
    2. ZipArchiveMin.Entry.Bodyに対象データのStream
    3. ZipArchiveMin.Entry.UncompressedSizeにファイルサイズ
    4. 必要なら、ZipArchiveMin.Entry.DateTimeに更新日付
  3. ZipArchiveMinのインスタンスメソッド、AddにEntryのインスタンスを渡す
  4. 必要数分上記を行った後、CloseまたはDisposeすると、ZIPファイルが完成します

属性等は保持できません。
圧縮自体は、System.IO.Compression.DeflateStreamに任せています。
モリーはじゃぶじゃぶ使う方式なので、あまり大きいファイル・多数のファイルには向きません。
(対象ファイルの圧縮効率のため、ファイル全体をメモリーに載せます
 ファイル情報(本体以外)はCloseまで保持します)


ZIPファイル展開は、

  1. ZipArchiveMinをインスタンス化する(引数は読込Stream)
  2. ZipArchiveMinのインスタンスメソッドReadで、ZipArchiveMin.Entryを受け取り好きにする
  3. ZipArchiveMin.Entryは不要になったらDisposeする
  4. 全ファイル処理するなら、nullが返るまでReadする
  5. ZipArchiveMinをDisposeする

各ファイルの内容をRead毎にメモリーに載せます。(EntryのDisposeまで解放されません)
また、ZIPの管理情報はろくに読まないので、ZipArchiveMinで作成した以外のZIPファイルでは、上手くいかないケースがあります。


ZIPファイルに詳しい人向け。

  • ファイル名はUTF-8決め打ち
  • Data Descriptorには未対応。あると正常に読めません(対応は難しくないけど)
  • 拡張フィールドやコメントは無視です(ない想定)
  • Readメソッドは、LocalFileHeaderしか利用しませんし、先頭から始まっている想定です
    • Versionはチェックしてません
    • BitFlagもチェックしません
    • CRC32チェックもしません
    • 属性やディスク分割も知ったことではありません
  • Addメソッドは、なんとなく最低限は埋めています

ソース

Powershellスクリプトにしています。
C#等で使うなら、クラス部分を抜き出して使ってください。


スクリプトを流すと、カレントディレクトリーに、電卓を圧縮したtest.zipを作成します。
それからtestディレクトリーを作成し、その下に階層入れて展開します。

$source = @"
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Text;

public class ZipArchiveMin : IDisposable
{
    private Stream _stream;

    public ZipArchiveMin(Stream stream)
    {
        _stream = stream;
    }

    public void Dispose()
    {
        Close();
        if (_stream != null) _stream.Dispose();
    }


    private static uint[] _table = null;
    private static uint CalcHash(byte[] array, int ibStart, int cbSize)
    {
        uint crcValue = 0xffffffff;
        if (_table == null)
        {
            _table = new uint[256];
            uint magic = 0xedb88320, num;
            for (uint i = 0; i < _table.Length; i++)
            {
                num = i;
                for (int j = 0; j < 8; j++)
                {
                    num = (num >> 1 & 0x7fffffff) ^ ((num & 1) * magic);
                }
                _table[i] = num;
            }
        }
        while (--cbSize >= 0)
        {
            crcValue = _table[(crcValue ^ array[ibStart++]) & 0xFF] ^ (crcValue >> 8);
        }
        return crcValue ^ 0xffffffff;
    }


    // 超割切り仕様とする。パラメーターチェックしない
    // 先頭に他データはないものとする
    // メモリーは使いたい放題とする

    // Addすると、LocalFileHeader&Dataを_streamに直接書き、CentralDirectoryHeaderを保持
    // AddはEntryを受け取る
    //  必須項目:UncompressedSize,Name,Body
    // Closeすると、CDHとEndofCentralDirectoryを書き込む
    // Dispose時はCloseもする
    private List<Entry> _cdh = new List<Entry>();
    public void Add(Entry entryIn)
    {
        Entry entry = entryIn.Clone();
        byte[] buf = new byte[entry.UncompressedSize];
        int len = entry.Body.Read(buf, 0, buf.Length);
        entry.Body.Close();
        entry.CRC32 = CalcHash(buf, 0, len);
        if (entry.Method == Entry.MethodEnum.Deflate)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                using (DeflateStream ds = new DeflateStream(ms, CompressionMode.Compress, true))
                {
                    ds.Write(buf, 0, len);
                }
                len = (int)ms.Length;
                buf = ms.GetBuffer();
            }
        }

        entry.CompressedSize = (uint)len;
        entry.LFHOffset = 0;
        if (_cdh.Count > 0)
        {
            Entry pre = _cdh[_cdh.Count - 1];
            entry.LFHOffset = (uint)(pre.LFHOffset + pre.GetLFHSize() + pre.CompressedSize);
        }

        byte[] lfh = entry.GetBytes(false);
        _stream.Write(lfh, 0, lfh.Length);
        _stream.Write(buf, 0, len);
        _cdh.Add(entry);
    }
    public void Close()
    {
        uint cdhSize = 0;
        for (int i = 0; i < _cdh.Count; i++)
        {
            byte[] buf = _cdh[i].GetBytes(true);
            _stream.Write(buf, 0, buf.Length);
            cdhSize += (uint)buf.Length;
        }
        if (_cdh.Count > 0)
        {
            byte[] ecd = _cdh[_cdh.Count - 1].GetEndofCentralDirectory((ushort)_cdh.Count, cdhSize);
            _stream.Write(ecd, 0, ecd.Length);
        }
        _cdh.Clear();
        if (_stream != null) _stream.Close();
    }

    // Readも頭から読む。LFHのみが対象。DataDescriptor未対応
    // 実体も読み込んでMemoryStream使ってDeflateStreamに渡しEntryとして返す
    public Entry Read()
    {
        return Entry.Create(_stream);
    }


    public class Entry : IDisposable
    {
        public enum SignatureEnum : uint
        {
            LocalFileHeader = 0x04034b50,
            CentralDirectoryHeader = 0x02014b50,
        }
        public enum VersionEnum : ushort
        {
            Non = 10,
            FolderDeflatePassword = 20,
            ZIP64 = 45,
            AES = 51,
            LZMA = 63,
        }
        [Flags]
        public enum BitFlagEnum : ushort
        {
            Password = 1,
            DataDescriptor = 8,
            Utf8 = 0x0800,
        }
        public enum MethodEnum : ushort
        {
            Deflate = 8,
            Store = 0,
        }

        public VersionEnum ReqVersion;
        public BitFlagEnum BitFlag;
        public MethodEnum Method;
        public DateTime DateTime;
        public uint CRC32;
        public uint CompressedSize;
        public uint UncompressedSize;
        public string Name;

        public VersionEnum MakeVersion;
        public uint LFHOffset;

        public Stream Body;
        public Encoding Encoding;
        private byte[] NameBytes;


        public Entry()
        {
            ReqVersion = VersionEnum.FolderDeflatePassword;
            BitFlag = BitFlagEnum.Utf8;
            Method = MethodEnum.Deflate;
            DateTime = DateTime.Now;
            MakeVersion = VersionEnum.FolderDeflatePassword;
            Encoding = Encoding.UTF8;
        }
        public Entry Clone()
        {
            return (Entry)this.MemberwiseClone();
        }
        public void Dispose()
        {
            if (Body != null) Body.Dispose();
        }

        public int GetLFHSize()
        {
            int ret = 30;
            if (Name != null)
            {
                NameBytes = Encoding.GetBytes(Name);
                ret += NameBytes.Length;
            }
            return ret;
        }
        public byte[] GetBytes(bool cdh)
        {
            int offset = 0, size = 4, bsize = GetLFHSize();
            byte[] name = NameBytes;
            if (cdh) bsize += 16;
            byte[] ret = new byte[bsize];
            if (cdh)
            {
                Array.Copy(BitConverter.GetBytes((uint)SignatureEnum.CentralDirectoryHeader), 0, ret, offset, size);
            }
            else
            {
                Array.Copy(BitConverter.GetBytes((uint)SignatureEnum.LocalFileHeader), 0, ret, offset, size);
            }
            offset += size;
            size = 2;
            if (cdh)
            {
                Array.Copy(BitConverter.GetBytes((ushort)MakeVersion), 0, ret, offset, size);
                offset += size;
            }
            Array.Copy(BitConverter.GetBytes((ushort)ReqVersion), 0, ret, offset, size);
            offset += size;
            Array.Copy(BitConverter.GetBytes((ushort)BitFlag), 0, ret, offset, size);
            offset += size;
            Array.Copy(BitConverter.GetBytes((ushort)Method), 0, ret, offset, size);
            offset += size;
            size = 4;
            Array.Copy(BitConverter.GetBytes((uint)(((DateTime.Year - 1980) << 25) | (DateTime.Month << 21) | (DateTime.Day << 16) |
                    (DateTime.Hour << 11) | (DateTime.Minute << 5) | (DateTime.Second / 2))), 0, ret, offset, size);
            offset += size;
            Array.Copy(BitConverter.GetBytes(CRC32), 0, ret, offset, size);
            offset += size;
            Array.Copy(BitConverter.GetBytes(CompressedSize), 0, ret, offset, size);
            offset += size;
            Array.Copy(BitConverter.GetBytes(UncompressedSize), 0, ret, offset, size);
            offset += size;
            size = 2;
            Array.Copy(BitConverter.GetBytes((ushort)name.Length), 0, ret, offset, size);
            offset += size;
            Array.Copy(BitConverter.GetBytes((ushort)0), 0, ret, offset, size);
            offset += size;
            if (cdh)
            {
                Array.Copy(BitConverter.GetBytes((ushort)0), 0, ret, offset, size);
                offset += size;
                Array.Copy(BitConverter.GetBytes((ushort)0), 0, ret, offset, size);
                offset += size;
                Array.Copy(BitConverter.GetBytes((ushort)0), 0, ret, offset, size);
                offset += size;
                size = 4;
                Array.Copy(BitConverter.GetBytes((uint)0), 0, ret, offset, size);
                offset += size;
                Array.Copy(BitConverter.GetBytes(LFHOffset), 0, ret, offset, size);
                offset += size;
            }
            Array.Copy(name, 0, ret, offset, name.Length);
            return ret;
        }
        public byte[] GetEndofCentralDirectory(ushort cdhNum, uint cdhSize)
        {
            byte[] ret = new byte[22];
            int offset = 0, size = 4;
            Array.Copy(BitConverter.GetBytes(0x06054b50), 0, ret, offset, size);
            offset += size;
            size = 2;
            Array.Copy(BitConverter.GetBytes((ushort)0), 0, ret, offset, size);
            offset += size;
            Array.Copy(BitConverter.GetBytes((ushort)0), 0, ret, offset, size);
            offset += size;
            Array.Copy(BitConverter.GetBytes(cdhNum), 0, ret, offset, size);
            offset += size;
            Array.Copy(BitConverter.GetBytes(cdhNum), 0, ret, offset, size);
            offset += size;
            size = 4;
            Array.Copy(BitConverter.GetBytes(cdhSize), 0, ret, offset, size);
            offset += size;
            Array.Copy(BitConverter.GetBytes(LFHOffset + GetLFHSize() + CompressedSize), 0, ret, offset, size);
            offset += size;
            size = 2;
            Array.Copy(BitConverter.GetBytes((ushort)0), 0, ret, offset, size);
            return ret;
        }

        public static Entry Create(Stream stream)
        {
            Entry ret = new Entry();
            byte[] data = new byte[ret.GetLFHSize()];
            int size = 4, offset = 0, read = stream.Read(data, 0, data.Length);
            if (read < ret.GetLFHSize() || BitConverter.ToUInt32(data, offset) != (uint)SignatureEnum.LocalFileHeader) return null;
            offset += size;
            size = 2;
            ret.ReqVersion = (VersionEnum)BitConverter.ToUInt16(data, offset);
            offset += size;
            ret.BitFlag = (BitFlagEnum)BitConverter.ToUInt16(data, offset);
            offset += size;
            ret.Method = (MethodEnum)BitConverter.ToUInt16(data, offset);
            offset += size;
            size = 4;
            uint date = BitConverter.ToUInt32(data, offset);
            ret.DateTime = new DateTime((int)((date >> 25) + 1980), (int)(date >> 21 & 0xf), (int)(date >> 16 & 0x1f),
                    (int)(date >> 11 & 0x1f), (int)(date >> 5 & 0x3f), (int)(date & 0x1f) * 2);
            offset += size;
            ret.CRC32 = BitConverter.ToUInt32(data, offset);
            offset += size;
            ret.CompressedSize = BitConverter.ToUInt32(data, offset);
            offset += size;
            ret.UncompressedSize = BitConverter.ToUInt32(data, offset);
            offset += size;
            size = 2;
            ushort nameLen = BitConverter.ToUInt16(data, offset);
            offset += size;
            offset += size;
            data = new byte[nameLen];
            read = stream.Read(data, 0, data.Length);
            if (read < nameLen) return null;
            ret.Encoding = Encoding.UTF8;
            if ((ret.BitFlag & BitFlagEnum.Utf8) != BitFlagEnum.Utf8) ret.Encoding = Encoding.Default;
            ret.Name = ret.Encoding.GetString(data, 0, nameLen);
            offset += nameLen;
            byte[] buf = new byte[ret.CompressedSize];
            read = stream.Read(buf, 0, buf.Length);
            if (read < ret.CompressedSize) return null;
            ret.Body = new MemoryStream(buf);
            if (ret.Method == MethodEnum.Deflate)
            {
                ret.Body = new DeflateStream(ret.Body, CompressionMode.Decompress);
            }
            return ret;
        }
    }
}
"@
Add-Type -Language CSharp -TypeDefinition $source
#([AppDomain]::CurrentDomain).GetAssemblies() | Out-Gridview

# 圧縮試験
$testTarget = 'c:\windows\system32\calc.exe'
$za = New-Object ZipArchiveMin(New-Object System.IO.FileStream('test.zip', 'Create'))
$ze = New-Object ZipArchiveMin+Entry
$ze.Name = 'test/1/calc.exe'
$ze.Body = New-Object System.IO.FileStream($testTarget, 'Open', 'Read', 'Read')
$ze.UncompressedSize = $ze.Body.Length
$ze.DateTime = (Get-ItemProperty $testTarget).LastWriteTime
$za.Add($ze)
# 未圧縮も可
$ze.Name = 'test/2/calc.exe'
$ze.Body = New-Object System.IO.FileStream($testTarget, 'Open', 'Read', 'Read')
$ze.Method = 'Store'
$za.Add($ze)
$ze.Dispose()
$za.Dispose()

# 展開試験
$za = New-Object ZipArchiveMin(New-Object System.IO.FileStream('test.zip', 'Open'))
while(($ze = $za.Read()) -ne $null) {
	$dir = Split-Path $ze.Name -Parent
	if(($dir.Length -gt 0) -and !(Test-Path $dir)) { [void](mkdir $dir) }
	$fs = New-Object System.IO.FileStream($ze.Name, 'Create')
	$buf = New-Object byte[] $ze.UncompressedSize
	$fs.Write($buf, 0, $ze.Body.Read($buf, 0, $buf.Length))
	$fs.Dispose()
	Set-ItemProperty $ze.Name -Name LastWriteTime -Value $ze.DateTime
	$ze.Dispose()
}
$za.Dispose()