zipファイルを調べてみた

.NET Frameworkの圧縮クラスを使って作ったzipファイルが、別のシステムで「展開できない」ということが起きた(らしい)。どういうことかと調べてみた。

zipファイルを作る

.NET Framework 4からzipファイルを作るクラスが追加された。特にZipFileクラスのメソッドを使うと、フォルダを圧縮してzipファイルを作ることが1行でできる。超簡単。

using System.IO;
using System.IO.Compression;

File.Delete(zipFilePath);
ZipFile.CreateFromDirectory(dataFolderPath, zipFilePath, CompressionLevel.Optimal, true);

しかし、こうして作ったzipファイルが展開できないらしい。

エクスプローラで圧縮したzipファイル

ところが、エクスプローラでフォルダを選択して作ったzipファイルは別システムでも展開できると云う。なにか違うのだろうか。

2つのファイルを並べてみると、たしかにサイズが違う。exploer.zipが手で作ったZIPファイルだが、サイズが大きい(112Kと106K)。エクスプローラのZIPファイルを作成するコマンドは、.NET Framework 4 のクラスを使っていないのか。へえええ。


中身を見てみたいが、バイナリデータなので、メモ帳では見れない。こういうときはUNIXと云うことで、bashコマンドを起動して、odで生身を見てみた。


21世紀にもなってod使うとは

比較してみると2つのファイルの内容は違っていた。これは解析してみたいという欲望が湧いてきて(貴重な週末なのに)zipファイルを解析するソフトを作ってみた。

結果が下図。


左がエクスプローラで作ったzipファイル
右がフレームワークで作ったzipファイル

ファイル構造の違い

  • エクスプローラで作ったzipファイルにはフォルダ要素があるが、フレームワークで作ったzipファイルにはフォルダ要素が無い。つまりファイルだけ。
  • フレームワークで作ったzipファイルの方が圧縮率が高いようだ。
フォルダ要素が無いのが、違いとしては大きい。

別システムがエラーの詳細を教えてくれないので、原因はさっぱりわからないのだが、このどちらかが原因と思われる。

回避策

安易に考えると、エクスプローラで作ったzipファイルはOKなのだから、それをC#で作れば問題は回避できる。

C#にはエクスプローラの動作を実行する「Microsoft Shell Controls And Automation」というCOMがあるので、これを使うと同じzipファイルを作れる。プロジェクトの参照設定で上記のCOMを追加します。

File.Delete(zipFilePath);
byte[] a = { 0x50, 0x4b, 0x05, 0x06, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
File.WriteAllBytes(zipFilePath, a);

var sh = new Shell32.Shell();
Shell32.Folder zip = sh.NameSpace(zipFilePath);
Shell32.Folder d = sh.NameSpace(dataFolderPath);
zip.CopyHere(d);

中身の要素が無いzipファイルを作って、そこにCopyHereメソッドでフォルダを追加するという流れ。

ただしマイクロソフト的には推奨していないようす。COMはエラーが起きやすいからね。
CopyHere メソッドから Zip ファイルを処理することはできません



zipファイルを解析した方法は以下の通り。

ZIPファイルの構造については、あちこちに解説記事があるので、それらを参考に。

ここでは、ファイルの末尾にあるEOCDを読むクラスを載せてみる。


        public class zipEOCD
        {
            [StructLayout(LayoutKind.Sequential, Pack = 2)]
            public struct HEADER
            {
                public UInt32 sig;
                public Int16 diskCount;
                public UInt16 diskStart;
                public UInt16 diskRecordCount;
                public UInt16 count;
                public UInt32 size;
                public UInt32 pos;
                public UInt16 n;
            };

            internal HEADER hd;

            internal zipEOCD(string path)
            {
                int getSigPos(byte[] bytes)
                {
                    for (int i = 0; i < 100; i++)
                    {
                        if (bytes[i] == 0x50 && bytes[i + 1] == 0x4b && bytes[i + 2] == 0x05 && bytes[i + 3] == 0x06)
                        {
                            return i;
                        }
                    }
                    return 0;
                }

                int pos = 0;
                using (FileStream st = File.OpenRead(path))
                {
                    byte[] a = new byte[100];
                    long n = st.Seek(-100, System.IO.SeekOrigin.End);
                    st.Read(a, 0, 100);
                    pos = getSigPos(a) - 100;
                }

                var size = Marshal.SizeOf(typeof(HEADER));
                var ptr = Marshal.AllocHGlobal(size);

                using (FileStream st = File.OpenRead(path))
                {
                    st.Seek(pos, SeekOrigin.End);
                        //byte[] a = new byte[100];
                        //st.Read(a, 0, 100);

                    BinaryReader br = new BinaryReader(st);
                    Marshal.Copy(br.ReadBytes(size), 0, ptr, size);
                    HEADER hd = (HEADER)Marshal.PtrToStructure(ptr, typeof(HEADER));

                    this.hd = hd;
                }
                Marshal.FreeHGlobal(ptr);
            }         }


EOCDはzipファイルの末尾にあって「0x504b0506」というバイト列で始まる部分らしいので、以下の手順で読み込んでいる。

  1. zipファイルをオープンして、末尾から100バイトの位置にシークする
  2. 100バイトのデータを配列に読み込んで、「0x504b0506」を探す
  3. 見つかったら位置を記録する
  4. zipファイルを再度オープンして、見つかった位置へシークする
  5. バイナリリーダーを作って、EOCDの形式にあわせた構造体に読み込む。
調査のためのソフトなので、思い切り乱暴ですな。エラー処理もしていないし。C#でバイナリデータを読み込むのは、なかなか大変なのもよくわかった。Cだとreadに構造体のアドレスを渡せばOKだったよね(記憶があいまい)。







コメント

このブログの人気の投稿

varchar をデータ型 numeric に変換中に、算術オーバーフロー エラーが発生しました。