ByteArray 範例:讀取 .zip 檔

Adobe AIR 1.0 以及更新的版本

本範例會說明如何讀取含有多個不同類型檔案的單純 .zip 檔。其做法是從每個檔案的中繼資料內擷取相關資料、將每個檔案解壓縮到 ByteArray,再將結果檔案輸出至桌面。

.zip 檔的一般結構是以 PKWARE Inc. 的規格為準,這項規格詳列於 http://www.pkware.com/documents/casestudies/APPNOTE.TXT 中。其開頭為 .zip 封存內第一個檔案的檔案檔頭和檔案資料,且後面接著其它每個檔案的檔案檔頭和檔案資料配對組合 (檔案檔頭的結構將於稍後說明)。接著,.zip 檔可能會包含資料描述項記錄 (通常是在輸出的 zip 檔建立於記憶體而非儲存至磁碟機時)。後續則是幾個其它的選擇性元素:封存解密檔頭、封存額外資料記錄、中央目錄結構、中央目錄記錄的 Zip64 尾端、中央目錄定位器的 Zip64 尾端,以及中央目錄記錄的尾端。

本範例的程式碼僅用來剖析不含資料夾的 zip 檔,同時假設並無資料描述項記錄。位於最終檔案資料後面的各項資訊將全數忽略。

每個檔案的檔案檔頭格式如下:

檔案檔頭簽名

4 個位元組

必要版本

2 個位元組

一般用途位元旗標

2 個位元組

壓縮方法

2 個位元組 (8=DEFLATE;0=未壓縮)

檔案上次修改時間

2 個位元組

檔案上次修改日期

2 個位元組

crc-32

4 個位元組

壓縮後大小

4 個位元組

解壓縮後大小

4 個位元組

檔案名稱長度

2 個位元組

額外欄位長度

2 個位元組

檔案名稱

變數

額外欄位

變數

接續在檔案檔頭後面的部分就是實際檔案資料,可能已壓縮或未壓縮 (視壓縮方法旗標而定)。旗標為 0 (零) 代表檔案資料未壓縮,8 代表資料使用 DEFLATE 演算法進行壓縮,其它值代表使用其它壓縮演算法。

本範例的使用者介面會包含一個標籤和一個文字區域 ( taFiles )。應用程式會將 .zip 檔中所找到每個檔案的下列資訊寫入文字區域:檔案名稱、壓縮後大小與解壓縮後大小。下列 MXML 文件定義 Flex 版本應用程式的使用者介面:

<?xml version="1.0" encoding="utf-8"?> 
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="init();"> 
    <mx:Script> 
        <![CDATA[ 
            // The application code goes here 
        ]]> 
    </mx:Script> 
    <mx:Form> 
        <mx:FormItem label="Output"> 
            <mx:TextArea id="taFiles" width="320" height="150"/> 
        </mx:FormItem> 
    </mx:Form> 
</mx:WindowedApplication>

本程式一開始會執行下列工作:

  • 匯入所需的類別

    import flash.filesystem.*; 
    import flash.utils.ByteArray; 
    import flash.events.Event;
  • 定義使用者介面 (Flash 適用)

    import fl.controls.*; 
     
    //requires TextArea and Label components in the Library 
    var taFiles = new TextArea(); 
    var output = new Label(); 
    taFiles.setSize(320, 150); 
    taFiles.move(10, 30); 
    output.move(10, 10); 
    output.width = 150; 
    output.text = "Contents of HelloAir.zip"; 
    addChild(taFiles); 
    addChild(output);
  • 定義 bytes ByteArray

    var bytes:ByteArray = new ByteArray(); 
  • 定義變數以儲存檔案檔頭提供的中繼資料

    // variables for reading fixed portion of file header 
    var fileName:String = new String(); 
    var flNameLength:uint; 
    var xfldLength:uint; 
    var offset:uint; 
    var compSize:uint; 
    var uncompSize:uint; 
    var compMethod:int; 
    var signature:int; 
  • 定義 File ( zfile ) 和 FileStream ( zStream ) 物件以代表 .zip 檔,並指定該 .zip 檔的位置 (即 "HelloAIR.zip" 檔案,位於桌面目錄) 以從中擷取檔案。

    // File variables for accessing .zip file 
    var zfile:File = File.desktopDirectory.resolvePath("HelloAIR.zip"); 
    var zStream:FileStream = new FileStream();

在 Flex 中,首先執行的程式碼是 init() 方法,此方法會做為 mx:WindowedApplication 根標籤的 creationComplete 處理常式來呼叫。

// for Flex 
private function init():void 
{

首先,程式會以 READ 模式開啟 .zip 檔。

    zStream.open(zfile, FileMode.READ); 

接著,將 bytes endian 屬性設為 LITTLE_ENDIAN ,以指示數值欄位的位元組順序為最低位元組在前。

    bytes.endian = Endian.LITTLE_ENDIAN; 

之後, while() 陳述式將起始迴圈反覆執行,直到檔案串流中的目前位置大於或等於檔案大小為止。

    while (zStream.position < zfile.size) 
    {

迴圈內的第一個陳述式會將檔案串流開頭的 30 個位元組讀入 ByteArray bytes 。這 30 個開頭位元組構成了第一個檔案檔頭的固定大小部分。

        // read fixed metadata portion of local file header 
        zStream.readBytes(bytes, 0, 30);

接著,程式碼自此 30 位元組檔頭的開頭位元組中讀取整數 ( signature )。依 ZIP 格式規格定義,每個檔案檔頭的簽名都是十六進位值 0x04034b50 ;如果簽名並非此值,即表示程式碼執行已超出 .zip 檔的檔案部分,也就是說檔案全都擷取完畢。在此情況下,程式碼會立刻結束 while 迴圈,而不再繼續讀到位元組陣列結尾。

        bytes.position = 0; 
        signature = bytes.readInt(); 
        // if no longer reading data files, quit 
        if (signature != 0x04034b50) 
        { 
            break; 
        }

程式碼的下一段落則指定偏移 8 個單位,以便讀取該位置的檔頭位元組,並將其值儲存在 compMethod 變數中。包含於此位元組的值指出用於壓縮該檔案的壓縮方法。允許的壓縮方法有很多種,但實際上幾乎所有 .zip 檔都會使用 DEFLATE 壓縮演算法。如果目前的檔案使用 DEFLATE 壓縮方法進行壓縮, compMethod 即為 8;若檔案未壓縮,則 compMethod 為 0。

        bytes.position = 8; 
        compMethod = bytes.readByte();  // store compression method (8 == Deflate)

接續在開頭 30 個位元組後面的是長度不定的檔頭部分,內含檔案名稱,甚至還有額外欄位。這個部分的大小會儲存在 offset 變數中。大小的計算方式則是將檔案名稱長度和額外欄位長度相加 (分別讀取自檔頭偏移位置 26 和 28)。

        offset = 0;    // stores length of variable portion of metadata  
        bytes.position = 26;  // offset to file name length 
        flNameLength = bytes.readShort();    // store file name 
        offset += flNameLength;     // add length of file name 
        bytes.position = 28;    // offset to extra field length 
        xfldLength = bytes.readShort(); 
        offset += xfldLength;    // add length of extra field

之後,程式會讀取長度不定的檔案檔頭部分,而該部分的位元組數目即等於 offset 變數的值。

        // read variable length bytes between fixed-length header and compressed file data 
        zStream.readBytes(bytes, 30, offset);

程式也將從長度不定的檔頭部分讀取檔案名稱,連同檔案壓縮後 (zipped) 與解壓縮後 (原始) 大小一併顯示在文字區域內。

// Flash version 
        bytes.position = 30;  
        fileName = bytes.readUTFBytes(flNameLength); // read file name 
        taFiles.appendText(fileName + "\n"); // write file name to text area 
        bytes.position = 18; 
        compSize = bytes.readUnsignedInt();  // store size of compressed portion 
        taFiles.appendText("\tCompressed size is: " + compSize + '\n'); 
        bytes.position = 22;  // offset to uncompressed size 
        uncompSize = bytes.readUnsignedInt();  // store uncompressed size 
        taFiles.appendText("\tUncompressed size is: " + uncompSize + '\n'); 
        
// Flex version 
        bytes.position = 30;  
        fileName = bytes.readUTFBytes(flNameLength); // read file name 
        taFiles.text += fileName + "\n"; // write file name to text area 
        bytes.position = 18; 
        compSize = bytes.readUnsignedInt();  // store size of compressed portion 
        taFiles.text += "\tCompressed size is: " + compSize + '\n'; 
        bytes.position = 22;  // offset to uncompressed size 
        uncompSize = bytes.readUnsignedInt();  // store uncompressed size 
        taFiles.text += "\tUncompressed size is: " + uncompSize + '\n'; 

本範例接著會將目前檔案仍在檔案串流中的其餘部分 (壓縮後大小指定此長度) 讀入 bytes 以覆寫開頭 30 個位元組的檔案檔頭。即使檔案未壓縮,壓縮後大小還是很準確,因為這類檔案在壓縮前後的大小會相等。

    // read compressed file to offset 0 of bytes; for uncompressed files 
    // the compressed and uncompressed size is the same 
    if (compSize == 0) continue; 
    zStream.readBytes(bytes, 0, compSize);

範例的下一步是將壓縮過的檔案進行解壓縮,然後呼叫 outfile() 函數,將結果檔案寫入輸出檔案串流。傳遞給 outfile() 的參數指定了檔案名稱,以及包含檔案資料的位元組陣列。

        if (compMethod == 8) // if file is compressed, uncompress 
        { 
            bytes.uncompress(CompressionAlgorithm.DEFLATE); 
        } 
        outFile(fileName, bytes);   // call outFile() to write out the file

在上述範例中, bytes.uncompress(CompressionAlgorithm.DEFLATE) 只能在 AIR 應用程式中使用。若要為 AIR 和 Flash Player 解壓縮採用 deflate 壓縮的資料,請叫用 ByteArray 的 inflate() 函數。

右大括弧用來表示 while 迴圈、 init() 方法以及 Flex 應用程式程式碼的結尾 ( outFile() 方法除外)。當返回 while 迴圈起始處再次執行迴圈時,將繼續處理 .zip 檔中的下一輪位元組;可能是擷取另一個檔案,或因檔案都已處理完畢而停止處理 .zip 檔。

    } // end of while loop 
} // for Flex version, end of init() method and application

outfile() 函數將在桌面上以 WRITE 模式開啟輸出檔,檔案名稱則是由 filename 參數提供。接著,再將 data 參數提供的檔案資料寫入輸出檔案串流 ( outStream ),最後關閉該檔案。

// Flash version 
function outFile(fileName:String, data:ByteArray):void 
{ 
    var outFile:File = File.desktopDirectory; // destination folder is desktop 
    outFile = outFile.resolvePath(fileName);  // name of file to write 
    var outStream:FileStream = new FileStream(); 
    // open output file stream in WRITE mode 
    outStream.open(outFile, FileMode.WRITE); 
    // write out the file 
    outStream.writeBytes(data, 0, data.length); 
    // close it 
    outStream.close(); 
} 
private function outFile(fileName:String, data:ByteArray):void 
{ 
    var outFile:File = File.desktopDirectory; // dest folder is desktop 
    outFile = outFile.resolvePath(fileName);  // name of file to write 
    var outStream:FileStream = new FileStream(); 
    // open output file stream in WRITE mode 
    outStream.open(outFile, FileMode.WRITE); 
    // write out the file 
    outStream.writeBytes(data, 0, data.length); 
    // close it 
    outStream.close(); 
}