Home | History | Annotate | Download | only in plugins
      1 /*
      2  * Copyright (c) 2009-2010 jMonkeyEngine
      3  * All rights reserved.
      4  *
      5  * Redistribution and use in source and binary forms, with or without
      6  * modification, are permitted provided that the following conditions are
      7  * met:
      8  *
      9  * * Redistributions of source code must retain the above copyright
     10  *   notice, this list of conditions and the following disclaimer.
     11  *
     12  * * Redistributions in binary form must reproduce the above copyright
     13  *   notice, this list of conditions and the following disclaimer in the
     14  *   documentation and/or other materials provided with the distribution.
     15  *
     16  * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
     17  *   may be used to endorse or promote products derived from this software
     18  *   without specific prior written permission.
     19  *
     20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
     22  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     23  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
     24  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     25  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     26  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     27  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
     28  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
     29  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     30  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     31  */
     32 
     33 package com.jme3.asset.plugins;
     34 
     35 import com.jme3.asset.AssetInfo;
     36 import com.jme3.asset.AssetKey;
     37 import com.jme3.asset.AssetLocator;
     38 import com.jme3.asset.AssetManager;
     39 import java.io.IOException;
     40 import java.io.InputStream;
     41 import java.net.HttpURLConnection;
     42 import java.net.URL;
     43 import java.nio.ByteBuffer;
     44 import java.nio.CharBuffer;
     45 import java.nio.charset.CharacterCodingException;
     46 import java.nio.charset.Charset;
     47 import java.nio.charset.CharsetDecoder;
     48 import java.nio.charset.CoderResult;
     49 import java.util.HashMap;
     50 import java.util.logging.Level;
     51 import java.util.logging.Logger;
     52 import java.util.zip.Inflater;
     53 import java.util.zip.InflaterInputStream;
     54 import java.util.zip.ZipEntry;
     55 
     56 public class HttpZipLocator implements AssetLocator {
     57 
     58     private static final Logger logger = Logger.getLogger(HttpZipLocator.class.getName());
     59 
     60     private URL zipUrl;
     61     private String rootPath = "";
     62     private int numEntries;
     63     private int tableOffset;
     64     private int tableLength;
     65     private HashMap<String, ZipEntry2> entries;
     66 
     67     private static final ByteBuffer byteBuf = ByteBuffer.allocate(250);
     68     private static final CharBuffer charBuf = CharBuffer.allocate(250);
     69     private static final CharsetDecoder utf8Decoder;
     70 
     71     public static final long LOCSIG = 0x4034b50, EXTSIG = 0x8074b50,
     72       CENSIG = 0x2014b50, ENDSIG = 0x6054b50;
     73 
     74     public static final int LOCHDR = 30, EXTHDR = 16, CENHDR = 46, ENDHDR = 22,
     75       LOCVER = 4, LOCFLG = 6, LOCHOW = 8, LOCTIM = 10, LOCCRC = 14,
     76       LOCSIZ = 18, LOCLEN = 22, LOCNAM = 26, LOCEXT = 28, EXTCRC = 4,
     77       EXTSIZ = 8, EXTLEN = 12, CENVEM = 4, CENVER = 6, CENFLG = 8,
     78       CENHOW = 10, CENTIM = 12, CENCRC = 16, CENSIZ = 20, CENLEN = 24,
     79       CENNAM = 28, CENEXT = 30, CENCOM = 32, CENDSK = 34, CENATT = 36,
     80       CENATX = 38, CENOFF = 42, ENDSUB = 8, ENDTOT = 10, ENDSIZ = 12,
     81       ENDOFF = 16, ENDCOM = 20;
     82 
     83     static {
     84         Charset utf8 = Charset.forName("UTF-8");
     85         utf8Decoder = utf8.newDecoder();
     86     }
     87 
     88     private static class ZipEntry2 {
     89         String name;
     90         int length;
     91         int offset;
     92         int compSize;
     93         long crc;
     94         boolean deflate;
     95 
     96         @Override
     97         public String toString(){
     98             return "ZipEntry[name=" + name +
     99                          ",  length=" + length +
    100                          ",  compSize=" + compSize +
    101                          ",  offset=" + offset + "]";
    102         }
    103     }
    104 
    105     private static int get16(byte[] b, int off) {
    106 	return  (b[off++] & 0xff) |
    107                ((b[off]   & 0xff) << 8);
    108     }
    109 
    110     private static int get32(byte[] b, int off) {
    111 	return  (b[off++] & 0xff) |
    112                ((b[off++] & 0xff) << 8) |
    113                ((b[off++] & 0xff) << 16) |
    114                ((b[off] & 0xff) << 24);
    115     }
    116 
    117     private static long getu32(byte[] b, int off) throws IOException{
    118         return (b[off++]&0xff) |
    119               ((b[off++]&0xff) << 8) |
    120               ((b[off++]&0xff) << 16) |
    121              (((long)(b[off]&0xff)) << 24);
    122     }
    123 
    124     private static String getUTF8String(byte[] b, int off, int len) throws CharacterCodingException {
    125         StringBuilder sb = new StringBuilder();
    126 
    127         int read = 0;
    128         while (read < len){
    129             // Either read n remaining bytes in b or 250 if n is higher.
    130             int toRead = Math.min(len - read, byteBuf.capacity());
    131 
    132             boolean endOfInput = toRead < byteBuf.capacity();
    133 
    134             // read 'toRead' bytes into byteBuf
    135             byteBuf.put(b, off + read, toRead);
    136 
    137             // set limit to position and set position to 0
    138             // so data can be decoded
    139             byteBuf.flip();
    140 
    141             // decode data in byteBuf
    142             CoderResult result = utf8Decoder.decode(byteBuf, charBuf, endOfInput);
    143 
    144             // if the result is not an underflow its an error
    145             // that cannot be handled.
    146             // if the error is an underflow and its the end of input
    147             // then the decoder expects more bytes but there are no more => error
    148             if (!result.isUnderflow() || !endOfInput){
    149                 result.throwException();
    150             }
    151 
    152             // flip the char buf to get the string just decoded
    153             charBuf.flip();
    154 
    155             // append the decoded data into the StringBuilder
    156             sb.append(charBuf.toString());
    157 
    158             // clear buffers for next use
    159             byteBuf.clear();
    160             charBuf.clear();
    161 
    162             read += toRead;
    163         }
    164 
    165         return sb.toString();
    166     }
    167 
    168     private InputStream readData(int offset, int length) throws IOException{
    169         HttpURLConnection conn = (HttpURLConnection) zipUrl.openConnection();
    170         conn.setDoOutput(false);
    171         conn.setUseCaches(false);
    172         conn.setInstanceFollowRedirects(false);
    173         String range = "-";
    174         if (offset != Integer.MAX_VALUE){
    175             range = offset + range;
    176         }
    177         if (length != Integer.MAX_VALUE){
    178             if (offset != Integer.MAX_VALUE){
    179                 range = range + (offset + length - 1);
    180             }else{
    181                 range = range + length;
    182             }
    183         }
    184 
    185         conn.setRequestProperty("Range", "bytes=" + range);
    186         conn.connect();
    187         if (conn.getResponseCode() == HttpURLConnection.HTTP_PARTIAL){
    188             return conn.getInputStream();
    189         }else if (conn.getResponseCode() == HttpURLConnection.HTTP_OK){
    190             throw new IOException("Your server does not support HTTP feature Content-Range. Please contact your server administrator.");
    191         }else{
    192             throw new IOException(conn.getResponseCode() + " " + conn.getResponseMessage());
    193         }
    194     }
    195 
    196     private int readTableEntry(byte[] table, int offset) throws IOException{
    197         if (get32(table, offset) != CENSIG){
    198             throw new IOException("Central directory error, expected 'PK12'");
    199         }
    200 
    201         int nameLen = get16(table, offset + CENNAM);
    202         int extraLen = get16(table, offset + CENEXT);
    203         int commentLen = get16(table, offset + CENCOM);
    204         int newOffset = offset + CENHDR + nameLen + extraLen + commentLen;
    205 
    206         int flags = get16(table, offset + CENFLG);
    207         if ((flags & 1) == 1){
    208             // ignore this entry, it uses encryption
    209             return newOffset;
    210         }
    211 
    212         int method = get16(table, offset + CENHOW);
    213         if (method != ZipEntry.DEFLATED && method != ZipEntry.STORED){
    214             // ignore this entry, it uses unknown compression method
    215             return newOffset;
    216         }
    217 
    218         String name = getUTF8String(table, offset + CENHDR, nameLen);
    219         if (name.charAt(name.length()-1) == '/'){
    220             // ignore this entry, it is directory node
    221             // or it has no name (?)
    222             return newOffset;
    223         }
    224 
    225         ZipEntry2 entry = new ZipEntry2();
    226         entry.name     = name;
    227         entry.deflate  = (method == ZipEntry.DEFLATED);
    228         entry.crc      = getu32(table, offset + CENCRC);
    229         entry.length   = get32(table, offset + CENLEN);
    230         entry.compSize = get32(table, offset + CENSIZ);
    231         entry.offset   = get32(table, offset + CENOFF);
    232 
    233         // we want offset directly into file data ..
    234         // move the offset forward to skip the LOC header
    235         entry.offset += LOCHDR + nameLen + extraLen;
    236 
    237         entries.put(entry.name, entry);
    238 
    239         return newOffset;
    240     }
    241 
    242     private void fillByteArray(byte[] array, InputStream source) throws IOException{
    243         int total = 0;
    244         int length = array.length;
    245 	while (total < length) {
    246 	    int read = source.read(array, total, length - total);
    247             if (read < 0)
    248                 throw new IOException("Failed to read entire array");
    249 
    250 	    total += read;
    251 	}
    252     }
    253 
    254     private void readCentralDirectory() throws IOException{
    255         InputStream in = readData(tableOffset, tableLength);
    256         byte[] header = new byte[tableLength];
    257 
    258         // Fix for "PK12 bug in town.zip": sometimes
    259         // not entire byte array will be read with InputStream.read()
    260         // (especially for big headers)
    261         fillByteArray(header, in);
    262 
    263 //        in.read(header);
    264         in.close();
    265 
    266         entries = new HashMap<String, ZipEntry2>(numEntries);
    267         int offset = 0;
    268         for (int i = 0; i < numEntries; i++){
    269             offset = readTableEntry(header, offset);
    270         }
    271     }
    272 
    273     private void readEndHeader() throws IOException{
    274 
    275 //        InputStream in = readData(Integer.MAX_VALUE, ENDHDR);
    276 //        byte[] header = new byte[ENDHDR];
    277 //        fillByteArray(header, in);
    278 //        in.close();
    279 //
    280 //        if (get32(header, 0) != ENDSIG){
    281 //            throw new IOException("End header error, expected 'PK56'");
    282 //        }
    283 
    284         // Fix for "PK56 bug in town.zip":
    285         // If there's a zip comment inside the end header,
    286         // PK56 won't appear in the -22 position relative to the end of the
    287         // file!
    288         // In that case, we have to search for it.
    289         // Increase search space to 200 bytes
    290 
    291         InputStream in = readData(Integer.MAX_VALUE, 200);
    292         byte[] header = new byte[200];
    293         fillByteArray(header, in);
    294         in.close();
    295 
    296         int offset = -1;
    297         for (int i = 200 - 22; i >= 0; i--){
    298             if (header[i] == (byte) (ENDSIG & 0xff)
    299               && get32(header, i) == ENDSIG){
    300                 // found location
    301                 offset = i;
    302                 break;
    303             }
    304         }
    305         if (offset == -1)
    306             throw new IOException("Cannot find Zip End Header in file!");
    307 
    308         numEntries  = get16(header, offset + ENDTOT);
    309         tableLength = get32(header, offset + ENDSIZ);
    310         tableOffset = get32(header, offset + ENDOFF);
    311     }
    312 
    313     public void load(URL url) throws IOException {
    314         if (!url.getProtocol().equals("http"))
    315             throw new UnsupportedOperationException();
    316 
    317         zipUrl = url;
    318         readEndHeader();
    319         readCentralDirectory();
    320     }
    321 
    322     private InputStream openStream(ZipEntry2 entry) throws IOException{
    323         InputStream in = readData(entry.offset, entry.compSize);
    324         if (entry.deflate){
    325             return new InflaterInputStream(in, new Inflater(true));
    326         }
    327         return in;
    328     }
    329 
    330     public InputStream openStream(String name) throws IOException{
    331         ZipEntry2 entry = entries.get(name);
    332         if (entry == null)
    333             throw new RuntimeException("Entry not found: "+name);
    334 
    335         return openStream(entry);
    336     }
    337 
    338     public void setRootPath(String path){
    339         if (!rootPath.equals(path)){
    340             rootPath = path;
    341             try {
    342                 load(new URL(path));
    343             } catch (IOException ex) {
    344                 logger.log(Level.WARNING, "Failed to set root path "+path, ex);
    345             }
    346         }
    347     }
    348 
    349     public AssetInfo locate(AssetManager manager, AssetKey key){
    350         final ZipEntry2 entry = entries.get(key.getName());
    351         if (entry == null)
    352             return null;
    353 
    354         return new AssetInfo(manager, key){
    355             @Override
    356             public InputStream openStream() {
    357                 try {
    358                     return HttpZipLocator.this.openStream(entry);
    359                 } catch (IOException ex) {
    360                     logger.log(Level.WARNING, "Error retrieving "+entry.name, ex);
    361                     return null;
    362                 }
    363             }
    364         };
    365     }
    366 
    367 }
    368