ISZ ファイルを読む
ISZ ファイルは CD/DVD イメージファイルのひとつ。ISO ファイルをそのまま圧縮した形式で、単一データトラック (CD の場合は MODE1) のみを扱える。また、複数個のファイル (セグメント) に分割して格納することを可能にしている。
ISO ファイルを圧縮しただけであるため、展開したらセクタデータの処理を ISO ファイルからセクタデータを読み込んだ場合と共通化することができる。
UltraISO の公式サイトに ISZ ファイルフォーマットについての文書が置かれている (http://www.ezbsystems.com/isz/iszspec.txt) が不完全である。当文書ではこれを元に補足しながら ISZ ファイルを読み込むまでを記す。
諸注意
当文書は単一セグメントで暗号化されていない ISZ ファイルを認識して読み込むまでを記述しているが、不正確な情報であるため文字通り参考程度にしかならないだろう。さらにコードは意図的に間違っていたり効率が悪かったりする。
当文書を元にして ISZ ファイルを構築することは出来ない。Windows のディスクデバイスエミュレータである Daemon-Tools で読み込ませようとしても弾かれる。具体的には ISZ ファイルにファイルフッタ (file footer) が必要とされるようだ。
ISZ ファイルの構造
ISZ ファイルは大きく分けて4つの構造からなる。
- ファイルヘッダ : ISZ ファイルの認識や全体情報などを記述する
- セグメント情報 : セグメントに関する情報を記述する / 第1ファイルのみに存在
- チャンク情報 : 各チャンクの大きさと格納方法を記述する / 第1ファイルのみに存在
- チャンク列 : チャンクデータがぎっしり
ISZ ファイルでは連続する一定量のセクタをまとめて扱う。このまとまったセクタのことを チャンク という単位にして管理している。ISZ ファイル中に存在するチャンクはすべて同じセクタ数であり、チャンクごとに数が不揃いということはない。
CISO ファイルとの違いをひとつ述べておくと、CISO はセクタごとに圧縮することで読み込み反応速度を重視しているが、ISZ は圧縮効率を重視しているわけだ。
バイトオーダーはリトルエンディアンが用いられているが、バイト境界を跨いだり24ビット整数値が用いられたりするので、その都度変換する必要がある。
ファイルヘッダ
ファイルを認識するための基本的な情報を記述している。
構造体定義
(http://www.ezbsystems.com/isz/iszspec.txt から引用)
typedef struct isz_file_header {
char signature[4]; // 'IsZ!'
unsigned char header_size; // header size in bytes
char ver; // version number
unsigned int vsn; // volume serial number
unsigned short sect_size; // sector size in bytes
unsigned int total_sectors; // total sectors of ISO image
char has_password; // is Password protected?
__int64 segment_size; // size of segments in bytes
unsigned int nblocks; // number of chunks in image
unsigned int block_size; // chunk size in bytes (must be multiple of sector_size)
unsigned char ptr_len; // chunk pointer length
char seg_no; // segment number of this segment file, max 99
unsigned int ptr_offs; // offset of chunk pointers, zero = none
unsigned int seg_offs; // offset of segment pointers, zero = none
unsigned int data_offs; // data offset
char reserved;
} isz_header;
メモリレイアウト
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+00h | signature |hs |ver| vsn |sectsiz| total_sectors |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+10h |hpw| segment_size | nblocks | block_size |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
+20h ////|cpl|sno| ptr_offs | seg_offs | data_offs |RES|
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
見てわかるように、いくつかのフィールドが 32 / 64 ビット境界に配置されていない。構造体をそのまま利用するのであれば、定義はそれを考慮した記述をする必要がある。あるいはいったんバッファに読み込んでフィールドを一つ一つ変換していく。
(ruby によるコード)
file = File.open(iszpath, "rb")
buf = file.read(48)
(sig, headersize, ver, vsn, sectorsize, totalsectors,
haspassword, segsize, nblocks, blksize,
cpl, sno, ptroffs, segoffs, dataoffs) = buf.unpack("a4CCVvVCQ<VVCCVVVx")
raise "not recognized as ISZ file - #{iszpath}" unless sig == "IsZ!"
# 本来であれば headersize と ver も確認する。
セグメント情報
当文書ではセグメントをないものとして扱うため、記載なし。
チャンク情報
チャンク情報は各チャンクのバイト数、格納方法が記述されている。
構造体定義
(http://www.ezbsystems.com/isz/iszspec.txt から引用)
typedef struct isz_chunk_st {
chunk_flag;
blk_len;
} isz_chunk;
chunk_flag、blk_len に型がないのは、isz_header.ptr_len によって変化するためとの記述がある。まあよくわからないので、取りあえず 24ビット整数値型として処理するものとする。※ チャンクデータが 4MiB 以上の場合は32ビット整数値型を用いるものと考えられる。さらにもしかしたらチャンクデータが14ビット範囲内 (8KiB 未満) の場合は、16ビット整数値になるのかもしれない。
この24ビットの上位2ビットがチャンクデータの格納方法、下位22ビットがチャンクデータの格納時バイト数を表している。
チャンク情報はスクランブル化されている
チャンク情報をファイルから読み込んでもそのまま使うことが出来ない。スクランブル化されているためだ。これを解除するには巡回文字列 "IsZ!" と排他的論理和をとってビット反転すればよい。この "IsZ!" はファイルヘッダの signature と等しい。
(ruby によるコード)
cdtsize = nblocks * 3
file.pos = ptroffs
buf = file.read(cdtsize)
str = "IsZ!"
cdtsize.times { |i|
buf.setbyte(i, buf.getbyte(i) ^ str.getbyte(i % 4) ^ 0xff)
}
各チャンク情報を得る
まずは24ビット整数値を構築することからはじめる。
また、チャンク情報は格納手段とチャンクデータのバイト数だけでデータの位置が記述されていないが、これは計算で求められる。下記のコードではそれもまとめて計算で得ている。
(ruby によるコード)
offset = isz_header.data_offs
cdt = nblocks.times.map { |i|
n = (buf.getbyte(i * 3 + 0) << 0) |
(buf.getbyte(i * 3 + 1) << 8) |
(buf.getbyte(i * 3 + 2) << 16)
chunk_flag = (n << 22) & 0x03
blk_len = n & 0x003fffff
offset1 = offset
offset += blk_len
ISZ::Chunk.new(chunk_flag, blk_len, offset1)
}
iszspec.txt の isz_chunk 構造体には offset フィールドが存在しないため、追加しておくといいだろう。
(C によるコード)
struct isz_chunk
{
int chunk_flag;
uint32_t blk_len;
uint64_t offset; // DVD イメージの場合は32ビットでは対応できないため uint64_t を用いる
};
こんな感じでもいいし、適当に。
チャンクデータ
準備が整った。順を追って実際にセクタを読み込んでみる。
-
指定セクタを読み込むには、まず対応するチャンクデータが何番なのかを求める。
(ruby によるコード)
# 目的とするセクタ番号。0以上の整数値 target_sector = ほにゃらら # この値はファイルヘッダ読み込み時に計算して保持しておく block_in_sectors = blksize / sectorsize # チャンク番号 chunk_number = target_sector / block_in_sectors # ついでにチャンク中のセクタ開始位置(バイト値) offset_in_chunk = (target_sector % block_in_sectors) * sectorsize
-
そして CDT から場所とチャンクバイト数を取得して、ファイルから読み込む。
(ruby によるコード)
file.pos = cdt[chunk_number].offset buf = file.read(cdt[chunk_number].blk_size)
-
チャンクデータを展開。
(ruby によるコード)
case cdt[chunk_number].chunk_flag when 0 # ADI_ZERO # なんと (2) でファイルから読む必要すらなかった。 buf = "\0" * block_size when 1 # ADI_DATA # 何もしなくていい when 2 # ADI_ZLIB # zlib の uncompress で伸張。 buf = Zlib.inflate(buf) when 3 # ADI_BZ2 # ISZ の bzip2 圧縮チャンクは先頭3バイトが細工してある。"BZh" で上書きしてしまおう。 # C であれば『memcpy(buf, "BZh", 3)』ができる。 buf.setbyte(0, 0x42) # "B" buf.setbyte(1, 0x5a) # "Z" buf.setbyte(2, 0x68) # "h" # あとは通常の bzip2 ストリームなので、BZ2_bzBuffToBuffDecompress を用いて伸張する。 buf = Bzip2.uncompress(buf) end
-
チャンクデータから必要なセクタの部分を切り出す。
(ruby によるコード)
sector_data = buf.byteslice(offset_in_chunk, sectorsize)
これでセクタを読み込める。
ここまでみてわかるように、セクタひとつを読み込むまでにはそれなりの処理が必要になる。素の ISOファイルと比べて数十倍から数百倍遅くなるだろう。これを低減するためにチャンクキャッシュを用いたり、非圧縮チャンクであれば必要なセクタ部分のみを読み込んだりするなどの最適化が求められる。
To the extent possible under law,
dearblue
has waived all copyright and related or neighboring rights to
this work.