/* NSC -- new Scala compiler * Copyright 2005-2009 LAMP/EPFL * @author Martin Odersky */ // $Id: ZipArchive.scala 18621 2009-09-01 03:27:19Z extempore $ package scala.tools.nsc package io import scala.io.{ File, Path } import java.net.URL import java.util.Enumeration import java.io.{ File => JFile, IOException, InputStream, BufferedInputStream, ByteArrayInputStream } import java.util.zip.{ ZipEntry, ZipFile, ZipInputStream } import PartialFunction._ import scala.collection.Traversable import scala.collection.mutable.{ Map, HashMap } import scala.collection.immutable.{ StringVector => SV } import scala.collection.JavaConversions.asIterator /** * @author Philippe Altherr * @version 1.0, 23/03/2004 */ object ZipArchive { def fromPath(path: Path): ZipArchive = fromFile(path.toFile) /** * If the specified file <code>file</code> exists and is a readable * zip archive, returns an abstract file backed by it. Otherwise, * returns <code>null</code>. * * @param file ... * @return ... */ def fromFile(file: File): ZipArchive = try new ZipArchive(file, new ZipFile(file.jfile)) catch { case _: IOException => null } /** * Returns an abstract directory backed by the specified archive. */ def fromArchive(archive: ZipFile): ZipArchive = new ZipArchive(File(archive.getName()), archive) /** * Returns an abstract directory backed by the specified archive. */ def fromURL(url: URL): AbstractFile = new URLZipArchive(url) private[io] class ZipEntryTraversableClass(in: InputStream) extends Traversable[ZipEntry] { val zis = new ZipInputStream(in) def foreach[U](f: ZipEntry => U) = { def loop(x: ZipEntry): Unit = if (x != null) { f(x) zis.closeEntry() loop(zis.getNextEntry()) } loop(zis.getNextEntry()) } } } /** This abstraction aims to factor out the common code between * ZipArchive (backed by a zip file) and URLZipArchive (backed * by an InputStream.) */ private[io] trait ZipContainer extends AbstractFile { /** Abstract types */ type SourceType // InputStream or AbstractFile type CreationType // InputStream or ZipFile type ZipTrav = Traversable[ZipEntry] { def zis: ZipInputStream } /** Abstract values */ protected val creationSource: CreationType protected val root: DirEntryInterface protected def DirEntryConstructor: (AbstractFile, String, String) => DirEntryInterface protected def FileEntryConstructor: (SourceType, String, String, ZipEntry) => FileEntryInterface protected def ZipTravConstructor: CreationType => ZipTrav protected[io] trait EntryInterface extends VirtualFile { def name: String def path: String } protected[io] trait DirEntryInterface extends EntryInterface { def source: SourceType val entries: Map[String, EntryInterface] = new HashMap() var entry: ZipEntry = _ override def input = throw new Error("cannot read directories") override def lastModified: Long = if (entry ne null) entry.getTime() else super.lastModified override def isDirectory = true override def iterator: Iterator[AbstractFile] = entries.valuesIterator override def lookupName(name: String, directory: Boolean): AbstractFile = { def slashName = if (directory) name + "/" else name entries.getOrElse(slashName, null) } } protected[io] trait FileEntryInterface extends EntryInterface { def entry: ZipEntry override def lastModified: Long = entry.getTime() override def sizeOption = Some(entry.getSize().toInt) } class ZipRootCreator(f: ZipRootCreator => SourceType) { val root = DirEntryConstructor(ZipContainer.this, "<root>", "/") // Map from paths to DirEntries val dirs = HashMap[String, DirEntryInterface]("/" -> root) val traverser = ZipTravConstructor(creationSource) private[this] var _parent: DirEntryInterface = _ def parent = _parent def addEntry(entry: ZipEntry) { val path = entry.getName if (entry.isDirectory) { val dir: DirEntryInterface = getDir(dirs, path) if (dir.entry == null) dir.entry = entry } else { val (home, name) = splitPath(path) _parent = getDir(dirs, home) _parent.entries(name) = FileEntryConstructor(f(this), name, path, entry) } } def apply() = { traverser foreach addEntry root } } protected def splitPath(path: String): (String, String) = { (path lastIndexOf '/') match { case -1 => ("/", path) case idx => SV.splitAt(path, idx + 1) } } /** * Returns the abstract file in this abstract directory with the * specified name. If there is no such file, returns null. The * argument "directory" tells whether to look for a directory or * or a regular file. */ override def lookupName(name: String, directory: Boolean): AbstractFile = root.lookupName(name, directory) /** Returns an abstract file with the given name. It does not * check that it exists. */ override def lookupNameUnchecked(name: String, directory: Boolean): AbstractFile = throw new UnsupportedOperationException() /** Returns all abstract subfiles of this abstract directory. */ override def iterator: Iterator[AbstractFile] = root.iterator /** * Looks up the path in the given map and returns if found. * If not present, creates a new DirEntry, adds to both given * map and parent.entries, and returns it. */ protected def getDir(dirs: Map[String, DirEntryInterface], path: String): DirEntryInterface = dirs.getOrElseUpdate(path, { val (home, name) = splitPath(SV init path) val parent = getDir(dirs, home) val dir = DirEntryConstructor(parent, name, path) parent.entries(name + path.last) = dir dir }) override def isDirectory = true } /** * This class implements an abstract directory backed by a zip * archive. We let the encoding be <code>null</code>, because we behave like * a directory. * * @author Philippe Altherr * @version 1.0, 23/03/2004 */ final class ZipArchive(file: File, val archive: ZipFile) extends PlainFile(file) with ZipContainer { self => type SourceType = AbstractFile type CreationType = ZipFile protected val creationSource = archive protected lazy val root = new ZipRootCreator(_.parent)() protected def DirEntryConstructor = new DirEntry(_, _, _) protected def FileEntryConstructor = new FileEntry(_, _, _, _) protected def ZipTravConstructor = zipTraversableFromZipFile _ abstract class Entry( override val container: AbstractFile, name: String, path: String ) extends VirtualFile(name, path) { final override def path = "%s(%s)".format(self, pathInArchive) final def getArchive = self.archive def pathInArchive = super.path override def hashCode = super.hashCode + container.hashCode override def equals(that : Any) = super.equals(that) && (cond(that) { case e: Entry => container == e.container }) } final class DirEntry( container: AbstractFile, name: String, path: String ) extends Entry(container, name, path) with DirEntryInterface { def source = container } final class FileEntry( container: AbstractFile, name: String, path: String, val entry: ZipEntry ) extends Entry(container, name, path) with FileEntryInterface { def archive = self.archive override def input = archive getInputStream entry } private def zipTraversableFromZipFile(z: ZipFile): ZipTrav = new Traversable[ZipEntry] { def zis: ZipInputStream = null // not valid for this type val itStream = asIterator(z.entries()).toStream def foreach[U](f: ZipEntry => U) = itStream foreach f } } /** * This class implements an abstract directory backed by a specified * zip archive. * * @author Stephane Micheloud * @version 1.0, 29/05/2007 */ final class URLZipArchive(url: URL) extends AbstractFile with ZipContainer { type SourceType = InputStream type CreationType = InputStream protected lazy val creationSource = input protected lazy val root = new ZipRootCreator(x => byteInputStream(x.traverser.zis))() protected def DirEntryConstructor = (_, name, path) => new DirEntry(name, path) protected def FileEntryConstructor = new FileEntry(_, _, _, _) protected def ZipTravConstructor = new ZipArchive.ZipEntryTraversableClass(_) def name: String = url.getFile() def path: String = url.getPath() def input: InputStream = url.openStream() def absolute: AbstractFile = this def lastModified: Long = try url.openConnection().getLastModified() catch { case _: IOException => 0 } /** Methods we don't support but have to implement because of the design */ def file: JFile = null def create: Unit = throw new UnsupportedOperationException def delete: Unit = throw new UnsupportedOperationException def output = throw new Error("unsupported") def container = throw new Error("unsupported") abstract class Entry(name: String, path: String) extends VirtualFile(name, path) { final override def path = "%s(%s)".format(URLZipArchive.this, super.path) } final class DirEntry(name: String, path: String) extends Entry(name, path) with DirEntryInterface { def source = input } final class FileEntry( val in: InputStream, name: String, path: String, val entry: ZipEntry ) extends Entry(name, path) with FileEntryInterface { override def input = in } /** Private methods **/ private def byteInputStream(in: InputStream): InputStream = { val buf = new BufferedInputStream(in) val bytes = Iterator continually in.read().toByte takeWhile (_ != -1) new ByteArrayInputStream(bytes.toSequence.toArray) } }