ShellのCopyHereを使わずに素の.netでZIPファイルを扱う
.net 3.0のプライベートクラスを利用したZIP圧縮・展開の記事を書いた。
素の.net 3.0以降でZIPファイルを扱う - じゅんじゅんのきまぐれ
しかし、更新日付問題があった。
これを解決してみた。
多分、、、.net 2.0以降で動くんじゃなかろうか。
原理
汎用的に作るとどんどん肥大化していくので、超割切り仕様です。
ZIPファイル作成は、
- ZipArchiveMinをインスタンス化する(引数は出力先Stream)
- ZipArchiveMin.Entryをインスタンス化し以下の設定をする
- ZipArchiveMin.Entry.Nameにファイル名(パス付可)
- ZipArchiveMin.Entry.Bodyに対象データのStream
- ZipArchiveMin.Entry.UncompressedSizeにファイルサイズ
- 必要なら、ZipArchiveMin.Entry.DateTimeに更新日付
- ZipArchiveMinのインスタンスメソッド、AddにEntryのインスタンスを渡す
- 必要数分上記を行った後、CloseまたはDisposeすると、ZIPファイルが完成します
属性等は保持できません。
圧縮自体は、System.IO.Compression.DeflateStreamに任せています。
メモリーはじゃぶじゃぶ使う方式なので、あまり大きいファイル・多数のファイルには向きません。
(対象ファイルの圧縮効率のため、ファイル全体をメモリーに載せます
ファイル情報(本体以外)はCloseまで保持します)
ZIPファイル展開は、
- ZipArchiveMinをインスタンス化する(引数は読込Stream)
- ZipArchiveMinのインスタンスメソッドReadで、ZipArchiveMin.Entryを受け取り好きにする
- ZipArchiveMin.Entryは不要になったらDisposeする
- 全ファイル処理するなら、nullが返るまでReadする
- 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()