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
toString()97         public String toString(){
98             return "ZipEntry[name=" + name +
99                          ",  length=" + length +
100                          ",  compSize=" + compSize +
101                          ",  offset=" + offset + "]";
102         }
103     }
104 
get16(byte[] b, int off)105     private static int get16(byte[] b, int off) {
106 	return  (b[off++] & 0xff) |
107                ((b[off]   & 0xff) << 8);
108     }
109 
get32(byte[] b, int off)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 
getu32(byte[] b, int off)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 
getUTF8String(byte[] b, int off, int len)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