Skip to content

Instantly share code, notes, and snippets.

@fommil
Created November 18, 2016 09:36
Show Gist options
  • Save fommil/5709abc1678b346b6d927a13cd30b844 to your computer and use it in GitHub Desktop.
Save fommil/5709abc1678b346b6d927a13cd30b844 to your computer and use it in GitHub Desktop.
// Copyright: 2010 - 2016 https://github.com/ensime/ensime-server/graphs
// License: http://www.gnu.org/licenses/gpl-3.0.en.html
/* NSC -- new Scala compiler
* Copyright 2005-2013 LAMP/EPFL
* @author Paul Phillips
*/
package scala
package reflect
package io
import java.io.{ ByteArrayOutputStream, OutputStream }
import java.lang.reflect.Constructor
import java.net.URL
import java.io.{ IOException, InputStream, ByteArrayInputStream, FilterInputStream }
import java.io.{ File => JFile }
import java.util.zip._
import java.util.jar.Manifest
import scala.collection.mutable
import scala.collection.convert.WrapAsScala.asScalaIterator
import scala.annotation.tailrec
import scala.util.Try
/**
* An abstraction for zip files and streams. Everything is written the way
* it is for performance: we come through here a lot on every run. Be careful
* about changing it.
*
* @author Philippe Altherr (original version)
* @author Paul Phillips (this one)
* @version 2.0,
*
* ''Note: This library is considered experimental and should not be used unless you know what you are doing.''
*/
object ZipArchive {
/**
* @param file a File
* @return A ZipArchive if `file` is a readable zip file, otherwise null.
*/
def fromFile(file: File): FileZipArchive = fromFile(file.jfile)
def fromFile(file: JFile): FileZipArchive =
try { new FileZipArchive(file) }
catch { case _: IOException => null }
/**
* @param url the url of a zip file
* @return A ZipArchive backed by the given url.
*/
def fromURL(url: URL): URLZipArchive = new URLZipArchive(url)
def fromManifestURL(url: URL): AbstractFile = new ManifestResources(url)
private def dirName(path: String) = splitPath(path, front = true)
private def baseName(path: String) = splitPath(path, front = false)
private def splitPath(path0: String, front: Boolean): String = {
val isDir = path0.charAt(path0.length - 1) == '/'
val path = if (isDir) path0.substring(0, path0.length - 1) else path0
val idx = path.lastIndexOf('/')
if (idx < 0)
if (front) "/"
else path
else if (front) path.substring(0, idx + 1)
else path.substring(idx + 1)
}
private val classmonkey: Option[Constructor[sun.misc.URLClassPath]] = Try(Class.forName("fommil.URLClassPath")).toOption.map {
klass => klass.getDeclaredConstructors.find(_.getGenericParameterTypes.length == 1).get.asInstanceOf[Constructor[sun.misc.URLClassPath]]
}
private[io] def ucp(url: URL): sun.misc.URLClassPath = classmonkey match {
case Some(monkey) => monkey.newInstance(Array(url))
case None =>
System.err.println("[scala.reflect.io.ZipArchive] scalac must run with the class-monkey agent")
throw new UnsupportedOperationException("must be running with class-monkey")
//new sun.misc.URLClassPath(Array(url))
}
}
import ZipArchive._
/** ''Note: This library is considered experimental and should not be used unless you know what you are doing.'' */
abstract class ZipArchive(override val file: JFile) extends AbstractFile with Equals {
self =>
override def underlyingSource = Some(this)
def isDirectory = true
def lookupName(name: String, directory: Boolean) = unsupported()
def lookupNameUnchecked(name: String, directory: Boolean) = unsupported()
def create() = unsupported()
def delete() = unsupported()
def output = unsupported()
def container = unsupported()
def absolute = unsupported()
/** ''Note: This library is considered experimental and should not be used unless you know what you are doing.'' */
sealed abstract class Entry(path: String) extends VirtualFile(baseName(path), path) {
// have to keep this name for compat with sbt's compiler-interface
def getArchive: ZipFile = null
override def underlyingSource = Some(self)
override def toString = self.path + "(" + path + ")"
}
/** ''Note: This library is considered experimental and should not be used unless you know what you are doing.'' */
class DirEntry(path: String) extends Entry(path) {
val entries = mutable.HashMap[String, Entry]()
override def isDirectory = true
override def iterator: Iterator[Entry] = entries.valuesIterator
override def lookupName(name: String, directory: Boolean): Entry = {
if (directory) entries(name + "/")
else entries(name)
}
}
private def ensureDir(dirs: mutable.Map[String, DirEntry], path: String, zipEntry: ZipEntry): DirEntry =
//OPT inlined from getOrElseUpdate; saves ~50K closures on test run.
// was:
// dirs.getOrElseUpdate(path, {
// val parent = ensureDir(dirs, dirName(path), null)
// val dir = new DirEntry(path)
// parent.entries(baseName(path)) = dir
// dir
// })
dirs get path match {
case Some(v) => v
case None =>
val parent = ensureDir(dirs, dirName(path), null)
val dir = new DirEntry(path)
parent.entries(baseName(path)) = dir
dirs(path) = dir
dir
}
protected def getDir(dirs: mutable.Map[String, DirEntry], entry: ZipEntry): DirEntry = {
if (entry.isDirectory) ensureDir(dirs, entry.getName, entry)
else ensureDir(dirs, dirName(entry.getName), null)
}
}
/** ''Note: This library is considered experimental and should not be used unless you know what you are doing.'' */
final class FileZipArchive(file: JFile) extends ZipArchive(file) {
private class FileEntry(
time: Long,
contents: sun.misc.Resource
) extends Entry(contents.getName) {
override def getArchive: ZipFile = {
println("[error] ZipArchive.getArchive called, leaking files")
null
}
override def lastModified: Long = time
override def input: InputStream = contents.getInputStream
override def sizeOption: Option[Int] = Some(contents.getContentLength)
}
lazy val (root: DirEntry, allDirs: mutable.HashMap[String, DirEntry]) = {
val root = new DirEntry("/")
val dirs = mutable.HashMap[String, DirEntry]("/" -> root)
// NOTES on SI-9632
//
// The original scalac implementation of this class opens the zip
// file and retains the file handle, relying on the GC to call the
// finalizers (which reliably doesn't happen). On UNIX-like OS the
// file handle will refer to the file at the point when it was
// created (causing a huge PermGen memory leak), on Windows it is
// impossible for any process to delete the file.
//
// The changes here are to aggressively load archives, storing
// in-memory, so that the file handle does not leak and ensuring
// that the jar looks identical to the original implementation
// even if it changes on disk (i.e. changes are ignored).
//
// If using class-monkey to load the contents of the jars, which
// is cached in-memory across the entire JVM (unless the jar
// changed since the last time getResource was called). If
// class-monkey is *not* used then the jar will be accessed for
// every read and will probably break in lots of ways.
//
// Note that a caveat of the scalac behaviour is that it assumes
// that jars do not change.
val zipFile = try {
new ZipFile(file)
} catch {
case ioe: IOException => throw new IOException("Error accessing " + file.getPath, ioe)
}
val loader = ZipArchive.ucp(file.toURI.toURL)
try {
val enum = zipFile.entries()
while (enum.hasMoreElements) {
val entry = enum.nextElement
val dir = getDir(dirs, entry)
if (entry.isDirectory) dir
else {
val name = entry.getName
if (name.endsWith(".class")) {
// only classes are needed
val time = entry.getTime
val contents = loader.getResource(name)
dir.entries(name) = new FileEntry(time, contents)
}
}
}
} finally {
Try(zipFile.close())
Try(loader.closeLoaders())
}
(root, dirs)
}
def iterator: Iterator[Entry] = root.iterator
def name = file.getName
def path = file.getPath
def input = File(file).inputStream()
def lastModified = file.lastModified
override def sizeOption = Some(file.length.toInt)
override def canEqual(other: Any) = other.isInstanceOf[FileZipArchive]
override def hashCode() = file.hashCode
override def equals(that: Any) = that match {
case x: FileZipArchive => file.getAbsoluteFile == x.file.getAbsoluteFile
case _ => false
}
}
/** ''Note: This library is considered experimental and should not be used unless you know what you are doing.'' */
final class URLZipArchive(val url: URL) extends ZipArchive(null) {
def iterator: Iterator[Entry] = {
val root = new DirEntry("/")
val dirs = mutable.HashMap[String, DirEntry]("/" -> root)
val in = new ZipInputStream(new ByteArrayInputStream(Streamable.bytes(input)))
@tailrec def loop(): Unit = {
val zipEntry = in.getNextEntry()
class EmptyFileEntry() extends Entry(zipEntry.getName) {
override def toByteArray: Array[Byte] = null
override def sizeOption = Some(0)
}
class FileEntry() extends Entry(zipEntry.getName) {
override val toByteArray: Array[Byte] = {
val len = zipEntry.getSize().toInt
val arr = if (len == 0) Array.emptyByteArray else new Array[Byte](len)
var offset = 0
def loop(): Unit = {
if (offset < len) {
val read = in.read(arr, offset, len - offset)
if (read >= 0) {
offset += read
loop()
}
}
}
loop()
if (offset == arr.length) arr
else throw new IOException("Input stream truncated: read %d of %d bytes".format(offset, len))
}
override def sizeOption = Some(zipEntry.getSize().toInt)
}
if (zipEntry != null) {
val dir = getDir(dirs, zipEntry)
if (zipEntry.isDirectory)
dir
else {
val f = if (zipEntry.getSize() == 0) new EmptyFileEntry() else new FileEntry()
dir.entries(f.name) = f
}
in.closeEntry()
loop()
}
}
loop()
try root.iterator
finally dirs.clear()
}
def name = url.getFile()
def path = url.getPath()
def input = url.openStream()
def lastModified =
try url.openConnection().getLastModified()
catch { case _: IOException => 0 }
override def canEqual(other: Any) = other.isInstanceOf[URLZipArchive]
override def hashCode() = url.hashCode
override def equals(that: Any) = that match {
case x: URLZipArchive => url == x.url
case _ => false
}
}
final class ManifestResources(val url: URL) extends ZipArchive(null) {
def iterator = {
val root = new DirEntry("/")
val dirs = mutable.HashMap[String, DirEntry]("/" -> root)
val manifest = new Manifest(input)
val iter = manifest.getEntries().keySet().iterator().filter(_.endsWith(".class")).map(new ZipEntry(_))
for (zipEntry <- iter) {
val dir = getDir(dirs, zipEntry)
if (!zipEntry.isDirectory) {
class FileEntry() extends Entry(zipEntry.getName) {
override def lastModified = zipEntry.getTime()
override def input = resourceInputStream(path)
override def sizeOption = None
}
val f = new FileEntry()
dir.entries(f.name) = f
}
}
try root.iterator
finally dirs.clear()
}
def name = path
def path: String = {
val s = url.getPath
val n = s.lastIndexOf('!')
s.substring(0, n)
}
def input = url.openStream()
def lastModified =
try url.openConnection().getLastModified()
catch { case _: IOException => 0 }
override def canEqual(other: Any) = other.isInstanceOf[ManifestResources]
override def hashCode() = url.hashCode
override def equals(that: Any) = that match {
case x: ManifestResources => url == x.url
case _ => false
}
private def resourceInputStream(path: String): InputStream = {
new FilterInputStream(null) {
override def read(): Int = {
if (in == null) in = Thread.currentThread().getContextClassLoader().getResourceAsStream(path)
if (in == null) throw new RuntimeException(path + " not found")
super.read()
}
override def close(): Unit = {
super.close()
in = null
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment