zipファイルを調べてみた

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

zipファイルを作る

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

  1. using System.IO;
  2. using System.IO.Compression;
  3.  
  4. File.Delete(zipFilePath);
  5. 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を追加します。

  1. File.Delete(zipFilePath);
  2. byte[] a = { 0x50, 0x4b, 0x05, 0x06, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
  3. File.WriteAllBytes(zipFilePath, a);
  4.  
  5. var sh = new Shell32.Shell();
  6. Shell32.Folder zip = sh.NameSpace(zipFilePath);
  7. Shell32.Folder d = sh.NameSpace(dataFolderPath);
  8. zip.CopyHere(d);

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

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



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

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

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


  1.         public class zipEOCD
  2.         {
  3.             [StructLayout(LayoutKind.Sequential, Pack = 2)]
  4.             public struct HEADER
  5.             {
  6.                 public UInt32 sig;
  7.                 public Int16 diskCount;
  8.                 public UInt16 diskStart;
  9.                 public UInt16 diskRecordCount;
  10.                 public UInt16 count;
  11.                 public UInt32 size;
  12.                 public UInt32 pos;
  13.                 public UInt16 n;
  14.             };
  15.  
  16.             internal HEADER hd;
  17.  
  18.             internal zipEOCD(string path)
  19.             {
  20.                 int getSigPos(byte[] bytes)
  21.                 {
  22.                     for (int i = 0; i < 100; i++)
  23.                     {
  24.                         if (bytes[i] == 0x50 && bytes[i + 1] == 0x4b && bytes[i + 2] == 0x05 && bytes[i + 3] == 0x06)
  25.                         {
  26.                             return i;
  27.                         }
  28.                     }
  29.                     return 0;
  30.                 }
  31.  
  32.                 int pos = 0;
  33.                 using (FileStream st = File.OpenRead(path))
  34.                 {
  35.                     byte[] a = new byte[100];
  36.                     long n = st.Seek(-100, System.IO.SeekOrigin.End);
  37.                     st.Read(a, 0, 100);
  38.                     pos = getSigPos(a) - 100;
  39.                 }
  40.  
  41.                 var size = Marshal.SizeOf(typeof(HEADER));
  42.                 var ptr = Marshal.AllocHGlobal(size);
  43.  
  44.                 using (FileStream st = File.OpenRead(path))
  45.                 {
  46.                     st.Seek(pos, SeekOrigin.End);
  47.                         //byte[] a = new byte[100];
  48.                         //st.Read(a, 0, 100);
  49.  
  50.                     BinaryReader br = new BinaryReader(st);
  51.                     Marshal.Copy(br.ReadBytes(size), 0, ptr, size);
  52.                     HEADER hd = (HEADER)Marshal.PtrToStructure(ptr, typeof(HEADER));
  53.  
  54.                     this.hd = hd;
  55.                 }
  1.                 Marshal.FreeHGlobal(ptr);
  2.             }
  3.         }


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

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







コメント

このブログの人気の投稿

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