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