/// import JVMTypes = require('../../includes/JVMTypes'); import * as DoppioJVM from '../doppiojvm'; import JVMThread = DoppioJVM.VM.Threading.JVMThread; import ReferenceClassData = DoppioJVM.VM.ClassFile.ReferenceClassData; import logging = DoppioJVM.Debug.Logging; import util = DoppioJVM.VM.Util; import Long = DoppioJVM.VM.Long; import AbstractClasspathJar = DoppioJVM.VM.ClassFile.AbstractClasspathJar; import BrowserFS = require('browserfs'); import path = require('path'); import fs = require('fs'); import ThreadStatus = DoppioJVM.VM.Enums.ThreadStatus; import ArrayClassData = DoppioJVM.VM.ClassFile.ArrayClassData; import PrimitiveClassData = DoppioJVM.VM.ClassFile.PrimitiveClassData; import assert = DoppioJVM.Debug.Assert; import deflate = require('pako/lib/zlib/deflate'); import inflate = require('pako/lib/zlib/inflate'); import crc32 = require('pako/lib/zlib/crc32'); import adler32 = require('pako/lib/zlib/adler32'); import ZStreamCons = require('pako/lib/zlib/zstream'); import GZHeader = require('pako/lib/zlib/gzheader'); import i82u8 = util.i82u8; import isInt8Array = util.isInt8Array; import buff2i8 = util.buff2i8; import ZStream = Pako.ZStream; import ZlibReturnCode = Pako.ZlibReturnCode; import ZlibFlushValue = Pako.ZlibFlushValue; let BFSUtils = BrowserFS.BFSRequire('bfs_utils'); const MAX_WBITS = 15; // For type information only. import {default as TZipFS, CentralDirectory as TCentralDirectory} from 'browserfs/dist/node/backend/ZipFS'; declare var registerNatives: (defs: any) => void; let CanUseCopyFastPath = false; if (typeof Int8Array !== "undefined") { let i8arr = new Int8Array(1); let b = new Buffer( i8arr.buffer); i8arr[0] = 100; CanUseCopyFastPath = i8arr[0] == b.readInt8(0); } let ZipFiles: {[id: number]: TZipFS} = {}; let ZipEntries: {[id: number]: TCentralDirectory} = {}; let ZStreams: {[id: number]: ZStream} = {}; // Start at 1, as 0 is interpreted as an error. let NextId: number = 1; function OpenItem(item: T, map: {[id: number]: T}): number { let id = NextId++; map[id] = item; return id; } function GetItem(thread: JVMThread, id: number, map: {[id: number]: T}, errMsg: string): T { let item = map[id]; if (!item) { thread.throwNewException("Ljava/lang/IllegalStateException;", errMsg); return null; } else { return item; } } function CloseItem(id: number, map: {[id: number]: T}): void { delete map[id]; } function OpenZipFile(zfile: TZipFS): number { return OpenItem(zfile, ZipFiles); } function CloseZipFile(id: number): void { CloseItem(id, ZipFiles); } /** * Returns the zip file, if it exists. * Otherwise, throws an IllegalStateException. */ function GetZipFile(thread: JVMThread, id: number): TZipFS { return GetItem(thread, id, ZipFiles, `ZipFile not found.`); } function OpenZipEntry(zentry: TCentralDirectory): number { return OpenItem(zentry, ZipEntries); } function CloseZipEntry(id: number): void { CloseItem(id, ZipEntries); } /** * Returns the zip entry, if it exists. * Otherwise, throws an IllegalStateException. */ function GetZipEntry(thread: JVMThread, id: number): TCentralDirectory { return GetItem(thread, id, ZipEntries, `Invalid ZipEntry.`); } function OpenZStream(inflaterState: ZStream): number { return OpenItem(inflaterState, ZStreams); } function CloseZStream(id: number): void { CloseItem(id, ZStreams); } function GetZStream(thread: JVMThread, id: number): ZStream { return GetItem(thread, id, ZStreams, `Inflater not found.`); } /** * The type of a JZEntry field. Copied from java.util.zip.ZipFile. */ const enum JZEntryType { JZENTRY_NAME = 0, JZENTRY_EXTRA = 1, JZENTRY_COMMENT = 2, } class java_util_concurrent_atomic_AtomicLong { public static 'VMSupportsCS8()Z'(thread: JVMThread): boolean { return true; } } class java_util_jar_JarFile { /** * Returns an array of strings representing the names of all entries * that begin with "META-INF/" (case ignored). This native method is * used in JarFile as an optimization when looking up manifest and * signature file entries. Returns null if no entries were found. */ public static 'getMetaInfEntryNames()[Ljava/lang/String;'(thread: JVMThread, javaThis: JVMTypes.java_util_jar_JarFile): JVMTypes.JVMArray { let zip = GetZipFile(thread, javaThis['java/util/zip/ZipFile/jzfile'].toNumber()); if (zip) { if (!zip.existsSync('/META-INF')) { return null; } let explorePath: string[] = ['/META-INF']; let bsCl = thread.getBsCl(); let foundFiles: JVMTypes.java_lang_String[] = [util.initString(bsCl, 'META-INF/')]; while (explorePath.length > 0) { let p = explorePath.pop(); let dirListing = zip.readdirSync(p); for (let i = 0; i < dirListing.length; i++) { let newP = `${p}/${dirListing[i]}`; if (zip.statSync(newP, false).isDirectory()) { explorePath.push(newP); // Add a final /, and strip off first /. foundFiles.push(util.initString(bsCl, `${newP.slice(1)}/`)); } else { // Strip off first /. foundFiles.push(util.initString(bsCl, newP.slice(1))); } } return util.newArrayFromData(thread, bsCl, "[Ljava/lang/String;", foundFiles); } } } } class java_util_logging_FileHandler { public static 'isSetUID()Z'(thread: JVMThread): boolean { // Our FS does not support setUID. return false; } } class java_util_TimeZone { public static 'getSystemTimeZoneID(Ljava/lang/String;)Ljava/lang/String;'(thread: JVMThread, arg0: JVMTypes.java_lang_String): JVMTypes.java_lang_String { // NOTE: Can be half of an hour (e.g. Newfoundland is GMT-3.5) // NOTE: Is positive for negative offset. let offset = new Date().getTimezoneOffset() / 60; return thread.getJVM().internString(`GMT${offset > 0 ? '-' : '+'}${offset}`); } public static 'getSystemGMTOffsetID()Ljava/lang/String;'(thread: JVMThread): JVMTypes.java_lang_String { // XXX may not be correct return null; } } class java_util_zip_Adler32 { public static 'update(II)I'(thread: JVMThread, adler: number, byte: number): number { return adler32(adler, [byte & 0xFF], 1, 0); } public static 'updateBytes(I[BII)I'(thread: JVMThread, adler: number, b: JVMTypes.JVMArray, off: number, len: number): number { return adler32(adler, i82u8(b.array, off, len), len, 0); } public static 'updateByteBuffer(IJII)I'(thread: JVMThread, adler: number, addr: Long, off: number, len: number): number { let heap = thread.getJVM().getHeap(); let buff = BFSUtils.buffer2Arrayish(heap.get_buffer(addr.toNumber() + off, len)); return adler32(adler, buff, buff.length, 0); } } class java_util_zip_CRC32 { public static 'update(II)I'(thread: JVMThread, crc: number, byte: number): number { return crc32(crc, [byte & 0xFF], 1, 0); } public static 'updateBytes(I[BII)I'(thread: JVMThread, crc: number, b: JVMTypes.JVMArray, off: number, len: number): number { return crc32(crc, i82u8(b.array, off, len), len, 0); } public static 'updateByteBuffer(IJII)I'(thread: JVMThread, crc: number, addr: Long, off: number, len: number): number { let heap = thread.getJVM().getHeap(); let buff = BFSUtils.buffer2Arrayish(heap.get_buffer(addr.toNumber() + off, len)); return crc32(crc, buff, buff.length, 0); } } class java_util_zip_Deflater { public static 'initIDs()V'(thread: JVMThread): void {} /** * Initialize a new deflater. Using the zlib recommended default values. */ public static 'init(IIZ)J'(thread: JVMThread, level: number, strategy: number, nowrap: number): Long { let DEF_MEM_LEVEL = 8; // Zlib recommended default let Z_DEFLATED = 8; // This value is in the js version of pako under pako.Z_DEFLATED. // Possibly it is set to private in the Typescript version. The default value is 8, so this should work fine let strm = new ZStreamCons(); let ret = deflate.deflateInit2(strm, level, Z_DEFLATED, nowrap ? -MAX_WBITS : MAX_WBITS, DEF_MEM_LEVEL, strategy); if (ret != ZlibReturnCode.Z_OK) { let msg = ((strm.msg) ? strm.msg : (ret == ZlibReturnCode.Z_STREAM_ERROR) ? "inflateInit2 returned Z_STREAM_ERROR" : "unknown error initializing zlib library"); thread.throwNewException("Ljava/lang/InternalError;", msg); } else { let num = OpenZStream(strm); return Long.fromNumber(num); } } /** * Apparently this is explicitly not supported by pako. * @see Notes at http://nodeca.github.io/pako/ */ public static 'setDictionary(J[BII)V'(thread: JVMThread, arg0: Long, arg1: JVMTypes.JVMArray, arg2: number, arg3: number): void { thread.throwNewException('Ljava/lang/UnsatisfiedLinkError;', 'Native method not implemented.'); } public static 'deflateBytes(J[BIII)I'(thread: JVMThread, javaThis: JVMTypes.java_util_zip_Deflater, addr: Long, b: JVMTypes.JVMArray, off: number, len: number, flush: number): number { let strm = GetZStream(thread, addr.toNumber()); if (!strm) return; let thisBuf = javaThis['java/util/zip/Deflater/buf']; let thisOff = javaThis['java/util/zip/Deflater/off']; let thisLen = javaThis['java/util/zip/Deflater/len']; let inBuf = thisBuf.array; let outBuf = b.array; strm.input = i82u8(inBuf, 0, inBuf.length); strm.next_in = thisOff; strm.avail_in = thisLen; strm.output = i82u8(outBuf, 0, outBuf.length); strm.next_out = off; strm.avail_out = len; if (javaThis['java/util/zip/Deflater/setParams']) { let level = javaThis['java/util/zip/Deflater/level']; let strategy = javaThis['java/util/zip/Deflater/level']; //deflateParams is not yet supported by pako. We'll open a new ZStream with the new parameters instead. // res = deflate.deflateParams(strm, level, strategy); let newStream = new ZStreamCons(); let res = deflate.deflateInit2(newStream, level, strm.state.method, strm.state.windowBits, strm.state.memLevel, strategy); ZStreams[addr.toNumber()] = newStream; switch (res) { case ZlibReturnCode.Z_OK: javaThis['java/util/zip/Deflater/setParams'] = 0; thisOff += thisLen - strm.avail_in; javaThis['java/util/zip/Deflater/off'] = thisOff; javaThis['java/util/zip/Deflater/len'] = strm.avail_in; return len - strm.avail_out; case ZlibReturnCode.Z_BUF_ERROR: javaThis['java/util/zip/Deflater/setParams'] = 0; return 0; default: thread.throwNewException("Ljava/lang/InternalError;", strm.msg); } } else { let finish = javaThis['java/util/zip/Deflater/finish']; let res = deflate.deflate(strm, finish ? ZlibFlushValue.Z_FINISH : flush); switch (res) { case ZlibReturnCode.Z_STREAM_END: javaThis['java/util/zip/Deflater/finished'] = 1; // intentionally fall through case ZlibReturnCode.Z_OK: thisOff += thisLen - strm.avail_in; javaThis['java/util/zip/Deflater/off'] = thisOff; javaThis['java/util/zip/Deflater/len'] = strm.avail_in; return len - strm.avail_out; case ZlibReturnCode.Z_BUF_ERROR: return 0; default: thread.throwNewException('Ljava/lang/InternalError;', strm.msg); } } } public static 'getAdler(J)I'(thread: JVMThread, addr: Long): number { let strm = GetZStream(thread, addr.toNumber()); if (strm) { return strm.adler; } } public static 'reset(J)V'(thread: JVMThread, addr: Long): void { let strm = GetZStream(thread, addr.toNumber()); if (strm) { if (deflate.deflateReset(strm) !== ZlibReturnCode.Z_OK) { thread.throwNewException('Ljava/lang/InternalError;', strm.msg); } } } public static 'end(J)V'(thread: JVMThread, addr: Long): void { let strm = GetZStream(thread, addr.toNumber()); if (strm) { if (deflate.deflateEnd(strm) === ZlibReturnCode.Z_STREAM_ERROR) { thread.throwNewException('Ljava/lang/InternalError;', strm.msg); } else { CloseZStream(addr.toNumber()); } } } } class java_util_zip_Inflater { public static 'initIDs()V'(thread: JVMThread): void { // NOP. } public static 'init(Z)J'(thread: JVMThread, nowrap: number): Long { // Copying logic exactly from Java's native. let strm = new ZStreamCons(); let ret = inflate.inflateInit2(strm, nowrap ? -MAX_WBITS : MAX_WBITS); switch(ret) { case ZlibReturnCode.Z_OK: let num = OpenZStream(strm); return Long.fromNumber(num); default: let msg = (strm.msg) ? strm.msg : (ret == ZlibReturnCode.Z_STREAM_ERROR) ? "inflateInit2 returned Z_STREAM_ERROR" : "unknown error initializing zlib library"; thread.throwNewException("Ljava/lang/InternalError;", msg); break; } } /** * Note: This function is explicitly not supported by pako, the library we use * for inflation. * @see Notes at http://nodeca.github.io/pako/ */ public static 'setDictionary(J[BII)V'(thread: JVMThread, arg0: Long, arg1: JVMTypes.JVMArray, arg2: number, arg3: number): void { thread.throwNewException('Ljava/lang/UnsatisfiedLinkError;', 'Native method not implemented.'); } /** * NOTE: inflateBytes modifies the following properties on the Inflate object: * * - off * - len * - needDict * - finished */ public static 'inflateBytes(J[BII)I'(thread: JVMThread, javaThis: JVMTypes.java_util_zip_Inflater, addr: Long, b: JVMTypes.JVMArray, off: number, len: number): number { let strm = GetZStream(thread, addr.toNumber()); if (!strm) { return; } let thisBuf = javaThis['java/util/zip/Inflater/buf']; let thisOff = javaThis['java/util/zip/Inflater/off']; let thisLen = javaThis['java/util/zip/Inflater/len']; // Return 0 when the buffer is empty, which tells Java to refill its buffer. if (thisLen === 0 || len === 0) { return 0; } let inBuf = thisBuf.array; let outBuf = b.array; // Set up the zstream. strm.input = i82u8(inBuf, 0, inBuf.length); strm.next_in = thisOff; strm.avail_in = thisLen; strm.output = i82u8(outBuf, 0, outBuf.length); strm.next_out = off; strm.avail_out = len; // NOTE: JVM code does a partial flush, but Pako doesn't support it. // Thus, we do a sync flush instead. let ret = inflate.inflate(strm, ZlibFlushValue.Z_SYNC_FLUSH); let lenRead = len - strm.avail_out; if (!isInt8Array(outBuf)) { // Slow path: No typed arrays. Copy decompressed data. // u8 -> i8 let result = strm.output; for (let i = 0; i < lenRead; i++) { let byte = result[i + off]; if (byte > 127) { // Sign extend. byte |= 0xFFFFFF80 } outBuf[i + off] = byte; } } switch(ret) { case ZlibReturnCode.Z_STREAM_END: javaThis['java/util/zip/Inflater/finished'] = 1; /* fall through */ case ZlibReturnCode.Z_OK: thisOff += thisLen - strm.avail_in; javaThis['java/util/zip/Inflater/off'] = thisOff; javaThis['java/util/zip/Inflater/len'] = strm.avail_in; return lenRead; case ZlibReturnCode.Z_NEED_DICT: javaThis['java/util/zip/Inflater/needDict'] = 1; /* Might have consumed some input here! */ thisOff += thisLen - strm.avail_in; javaThis['java/util/zip/Inflater/off'] = thisOff; javaThis['java/util/zip/Inflater/len'] = strm.avail_in; return 0; case ZlibReturnCode.Z_BUF_ERROR: return 0; case ZlibReturnCode.Z_DATA_ERROR: thread.throwNewException('Ljava/util/zip/DataFormatException;', strm.msg); return; default: thread.throwNewException('Ljava/lang/InternalError;', strm.msg); return; } } public static 'getAdler(J)I'(thread: JVMThread, addr: Long): number { let strm = GetZStream(thread, addr.toNumber()); if (strm) { return strm.adler; } } public static 'reset(J)V'(thread: JVMThread, addr: Long): void { let addrNum = addr.toNumber(); let strm = GetZStream(thread, addrNum); if (strm) { /* There's a bug in Pako that prevents reset from working. if (inflate.inflateReset(strm) !== ZlibReturnCode.Z_OK) { thread.throwNewException('Ljava/lang/InternalError;', ''); } */ // Allocate a new stream, instead. let newStrm = new ZStreamCons(); let ret = inflate.inflateInit2(newStrm, strm.state.wrap ? MAX_WBITS : -MAX_WBITS); ZStreams[addrNum] = newStrm; } } public static 'end(J)V'(thread: JVMThread, addr: Long): void { let strm = GetZStream(thread, addr.toNumber()); if (strm) { if (inflate.inflateEnd(strm) === ZlibReturnCode.Z_STREAM_ERROR) { thread.throwNewException('Ljava/lang/InternalError;', strm.msg); } else { CloseZStream(addr.toNumber()); } } } } class java_util_zip_ZipFile { public static 'initIDs()V'(thread: JVMThread): void { // NOP. } /** * Note: Returns 0 when entry does not exist. */ public static 'getEntry(J[BZ)J'(thread: JVMThread, jzfile: Long, nameBytes: JVMTypes.JVMArray, addSlash: number): Long { // ASSUMPTION: Name is UTF-8. // Should actually compare the raw bytes. let zipfs = GetZipFile(thread, jzfile.toNumber()); if (zipfs) { let name = new Buffer(nameBytes.array).toString('utf8'); if (name[0] !== '/') { name = `/${name}`; } name = path.resolve(name); try { return Long.fromNumber(OpenZipEntry(zipfs.getCentralDirectoryEntry(name))); } catch (e) { return Long.ZERO; } } } public static 'freeEntry(JJ)V'(thread: JVMThread, jzfile: Long, jzentry: Long): void { CloseZipEntry(jzentry.toNumber()); } public static 'getNextEntry(JI)J'(thread: JVMThread, jzfile: Long, index: number): Long { let zipfs = GetZipFile(thread, jzfile.toNumber()); if (zipfs) { try { return Long.fromNumber(OpenZipEntry(zipfs.getCentralDirectoryEntryAt(index))); } catch (e) { return Long.ZERO; } } } public static 'close(J)V'(thread: JVMThread, jzfile: Long): void { CloseZipFile(jzfile.toNumber()); } public static 'open(Ljava/lang/String;IJZ)J'(thread: JVMThread, nameObj: JVMTypes.java_lang_String, mode: number, modified: Long, usemmap: number): Long { // Ignore mmap option. let name = nameObj.toString(); // Optimization: Check if this is a JAR file on the classpath. let cpath = thread.getBsCl().getClassPathItems(); for (let i = 0; i < cpath.length; i++) { let cpathItem = cpath[i]; if (cpathItem instanceof AbstractClasspathJar) { if (path.resolve(cpathItem.getPath()) === path.resolve(name)) { return Long.fromNumber(OpenZipFile(( cpathItem).getFS())); } } } // Async path. thread.setStatus(ThreadStatus.ASYNC_WAITING); fs.readFile(name, (err, data) => { if (err) { thread.throwNewException("Ljava/io/IOException;", err.message); } else { thread.asyncReturn(Long.fromNumber(OpenZipFile(new BrowserFS.FileSystem.ZipFS(data, name))), null); } }); } public static 'getTotal(J)I'(thread: JVMThread, jzfile: Long): number { let zipfs = GetZipFile(thread, jzfile.toNumber()); if (zipfs) { return zipfs.getNumberOfCentralDirectoryEntries(); } } public static 'startsWithLOC(J)Z'(thread: JVMThread, arg0: Long): number { // We do not support any zip files that do not begin with the proper signature. // Boolean, so 1 === true. return 1; } public static 'read(JJJ[BII)I'(thread: JVMThread, jzfile: Long, jzentry: Long, pos: Long, b: JVMTypes.JVMArray, off: number, len: number): number { let zipentry = GetZipEntry(thread, jzentry.toNumber()); let posNum = pos.toNumber(); if (zipentry) { if (len <= 0) { return 0; } let data = zipentry.getRawData(); // Sanity check: Will likely never happen, as Java code ensures that this method is // called in a sane manner. if (posNum >= data.length) { thread.throwNewException("Ljava/io/IOException;", "End of zip file."); return; } if (posNum + len > data.length) { len = data.length - posNum; } let arr = b.array; if (CanUseCopyFastPath) { let i8arr: Int8Array = arr; // XXX: DefinitelyTyped typings are out of date. let b = new Buffer( i8arr.buffer); return data.copy(b, off + i8arr.byteOffset, posNum, posNum + len); } else { for (let i = 0; i < len; i++) { arr[off + i] = data.readInt8(posNum + i); } return len; } } } public static 'getEntryTime(J)J'(thread: JVMThread, jzentry: Long): Long { let zipentry = GetZipEntry(thread, jzentry.toNumber()); if (zipentry) { return Long.fromNumber(zipentry.rawLastModFileTime()); } } public static 'getEntryCrc(J)J'(thread: JVMThread, jzentry: Long): Long { let zipentry = GetZipEntry(thread, jzentry.toNumber()); if (zipentry) { return Long.fromNumber(zipentry.crc32()); } } public static 'getEntryCSize(J)J'(thread: JVMThread, jzentry: Long): Long { let zipentry = GetZipEntry(thread, jzentry.toNumber()); if (zipentry) { return Long.fromNumber(zipentry.compressedSize()); } } public static 'getEntrySize(J)J'(thread: JVMThread, jzentry: Long): Long { let zipentry = GetZipEntry(thread, jzentry.toNumber()); if (zipentry) { return Long.fromNumber(zipentry.uncompressedSize()); } } public static 'getEntryMethod(J)I'(thread: JVMThread, jzentry: Long): number { let zipentry = GetZipEntry(thread, jzentry.toNumber()); if (zipentry) { return zipentry.compressionMethod(); } } public static 'getEntryFlag(J)I'(thread: JVMThread, jzentry: Long): number { let zipentry = GetZipEntry(thread, jzentry.toNumber()); if (zipentry) { return zipentry.flag(); } } public static 'getCommentBytes(J)[B'(thread: JVMThread, jzfile: Long): JVMTypes.JVMArray { let zipfile = GetZipFile(thread, jzfile.toNumber()); if (zipfile) { let eocd = zipfile.getEndOfCentralDirectory(); let comment = eocd.rawCdZipComment(); // Should be zero-copy in most situations. return util.newArrayFromDataWithClass(thread, > thread.getBsCl().getInitializedClass(thread, '[B'), buff2i8(comment)); } } public static 'getEntryBytes(JI)[B'(thread: JVMThread, jzentry: Long, type: JZEntryType): JVMTypes.JVMArray { let zipentry = GetZipEntry(thread, jzentry.toNumber()); if (zipentry) { switch(type) { case JZEntryType.JZENTRY_COMMENT: return util.newArrayFromDataWithClass(thread, > thread.getBsCl().getInitializedClass(thread, '[B'), buff2i8(zipentry.rawFileComment())); case JZEntryType.JZENTRY_EXTRA: return util.newArrayFromDataWithClass(thread, > thread.getBsCl().getInitializedClass(thread, '[B'), buff2i8(zipentry.extraField())); case JZEntryType.JZENTRY_NAME: return util.newArrayFromDataWithClass(thread, > thread.getBsCl().getInitializedClass(thread, '[B'), buff2i8(zipentry.rawFileName())); default: return null; } } } /** * Called to get an exception message. Should never really need to be called. */ public static 'getZipMessage(J)Ljava/lang/String;'(thread: JVMThread, jzfile: Long): JVMTypes.java_lang_String { return util.initString(thread.getBsCl(), "Something bad happened."); } } registerNatives({ 'java/util/concurrent/atomic/AtomicLong': java_util_concurrent_atomic_AtomicLong, 'java/util/jar/JarFile': java_util_jar_JarFile, 'java/util/logging/FileHandler': java_util_logging_FileHandler, 'java/util/TimeZone': java_util_TimeZone, 'java/util/zip/Adler32': java_util_zip_Adler32, 'java/util/zip/CRC32': java_util_zip_CRC32, 'java/util/zip/Deflater': java_util_zip_Deflater, 'java/util/zip/Inflater': java_util_zip_Inflater, 'java/util/zip/ZipFile': java_util_zip_ZipFile });