-
Notifications
You must be signed in to change notification settings - Fork 3.1k
/
ZipArchive.scala
430 lines (385 loc) · 14.7 KB
/
ZipArchive.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
/*
* Scala (https://www.scala-lang.org)
*
* Copyright EPFL and Lightbend, Inc.
*
* Licensed under Apache License 2.0
* (http://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/
package scala
package reflect
package io
import java.net.URL
import java.io.{ByteArrayInputStream, FilterInputStream, IOException, InputStream}
import java.io.{File => JFile}
import java.util.concurrent.{ArrayBlockingQueue, TimeUnit}
import java.util.zip.{ZipEntry, ZipFile, ZipInputStream}
import java.util.jar.Manifest
import scala.collection.mutable
import scala.collection.JavaConverters._
import scala.annotation.tailrec
import scala.reflect.internal.JDK9Reflectors
import ZipArchive._
/** 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 {
private[io] val closeZipFile = sys.props.get("scala.classpath.closeZip").map(_.toBoolean).getOrElse(false)
// The maximum number of entries retained in the pool associated with each FileZipArchive. FileZipArchive
// instances are shared across compiler threads (unless -YdisableFlatCpCaching), but to actually enable
// concurrent access to the data per-thread instance of the underlying j.u.ZipFile must be created. These
// are pooled for later usage
private[io] val zipFilePoolCapacity = {
val default = Runtime.getRuntime.availableProcessors().max(4)
sys.props.get("scala.classpath.zipFilePool.capacity").map(_.toInt).getOrElse(default)
}
private[io] final val RootEntry = "/"
/**
* @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) RootEntry
else path
else
if (front) path.substring(0, idx + 1)
else path.substring(idx + 1)
}
}
/** ''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, release: Option[String]) extends AbstractFile with Equals {
self =>
def this(file: JFile) = this(file, None)
override lazy val canonicalPath = super.canonicalPath
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 + ")"
override def unsafeToByteArray: Array[Byte] = toByteArray
}
/** ''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.get(name + "/").orNull
else entries.get(name).orNull
}
}
private def ensureDir(dirs: java.util.Map[String, DirEntry], path: String): DirEntry = {
dirs get path match {
case null =>
val parent = ensureDir(dirs, dirName(path))
val dir = new DirEntry(path)
parent.entries(baseName(path)) = dir
dirs.put(path, dir)
dir
case v => v
}
}
@volatile private[this] var lastDirName: String = RootEntry
private def dirNameUsingLast(name: String): String = {
val last = lastDirName
if (name.length > last.length + 1 && name.startsWith(last) && name.charAt(last.length) == '/' && name.indexOf('/', last.length + 1) == -1) {
// OPT: Avoid string allocation when reading successive entries in a zip index from the same directory.
lastDirName
} else {
val result = dirName(name)
lastDirName = result
result
}
}
protected def getDir(dirs: java.util.Map[String, DirEntry], entry: ZipEntry): DirEntry = {
if (entry.isDirectory) ensureDir(dirs, entry.getName)
else ensureDir(dirs, dirNameUsingLast(entry.getName))
}
def close(): Unit
}
/** ''Note: This library is considered experimental and should not be used unless you know what you are doing.'' */
final class FileZipArchive(file: JFile, release: Option[String]) extends ZipArchive(file, release) {
def this(file: JFile) = this(file, None)
private object zipFilePool {
private[this] val zipFiles = new ArrayBlockingQueue[ZipFile](ZipArchive.zipFilePoolCapacity)
def acquire: ZipFile = {
val zf = zipFiles.poll(0, TimeUnit.MILLISECONDS)
zf match {
case null =>
openZipFile()
case _ =>
zf
}
}
def release(zf: ZipFile): Unit = {
if (!zipFiles.offer(zf, 0, TimeUnit.MILLISECONDS))
zf.close()
}
def close(): Unit = {
val zipFilesToClose = new java.util.ArrayList[ZipFile]
zipFiles.drainTo(zipFilesToClose)
zipFilesToClose.iterator().forEachRemaining(_.close())
}
}
private[this] def openZipFile(): ZipFile = try {
release match {
case Some(r) if file.getName.endsWith(".jar") =>
val releaseVersion = JDK9Reflectors.runtimeVersionParse(r)
JDK9Reflectors.newJarFile(file, true, ZipFile.OPEN_READ, releaseVersion)
case _ =>
new ZipFile(file)
}
} catch {
case ioe: IOException => throw new IOException("Error accessing " + file.getPath, ioe)
}
private[this] class LazyEntry(
name: String,
time: Long,
size: Int
) extends Entry(name) {
override def lastModified: Long = time // could be stale
override def input: InputStream = {
val zipFile = openZipFile()
val entry = zipFile.getEntry(name) // with `-release`, returns the correct version under META-INF/versions
val delegate = zipFile.getInputStream(entry)
new FilterInputStream(delegate) {
override def close(): Unit = { zipFile.close() }
}
}
override def sizeOption: Option[Int] = Some(size) // could be stale
}
// keeps file handle(s) open to ZipFile in the pool this.zipFiles,
// which forbids file mutation on Windows, and leaks memory on all OS (typically by stopping
// classloaders from being garbage collected). But is slightly faster than LazyEntry.
//
// Note: scala/scala#7366 / scala/scala#7644, LeakyEntry _does_ close the file when `Global.close` is called,
// or after a short delay specified by FileBasedCache.deferCloseMs if classpath caching is enabled.
// So the file handle "leak" is far less a problem than it used do be.
private[this] class LeakyEntry(
name: String,
time: Long,
size: Int
) extends Entry(name) {
override def lastModified: Long = time // could be stale
override def input: InputStream = {
val zipFile = zipFilePool.acquire
val entry = zipFile.getEntry(name) // with `-release`, returns the correct version under META-INF/versions
val delegate = zipFile.getInputStream(entry)
new FilterInputStream(delegate) {
override def close(): Unit = { zipFilePool.release(zipFile) }
}
}
override def sizeOption: Option[Int] = Some(size)
}
private[this] val dirs = new java.util.HashMap[String, DirEntry]()
lazy val root: DirEntry = {
val root = new DirEntry(RootEntry)
dirs.put(RootEntry, root)
val zipFile = openZipFile()
val enum = zipFile.entries()
try {
while (enum.hasMoreElements) {
val zipEntry = enum.nextElement
if (!zipEntry.getName.startsWith("META-INF/versions/")) {
if (!zipEntry.isDirectory) {
val dir = getDir(dirs, zipEntry)
val mrEntry = if (release.isDefined) {
zipFile.getEntry(zipEntry.getName)
} else zipEntry
val f =
if (ZipArchive.closeZipFile)
new LazyEntry(
zipEntry.getName,
mrEntry.getTime,
mrEntry.getSize.toInt)
else
new LeakyEntry(zipEntry.getName,
mrEntry.getTime,
mrEntry.getSize.toInt)
dir.entries(f.name) = f
}
}
}
} finally {
if (!ZipArchive.closeZipFile)
zipFilePool.release(zipFile)
}
root
}
lazy val allDirs: java.util.Map[String, DirEntry] = { 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
}
override def close(): Unit = {
zipFilePool.close()
}
}
/** ''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(RootEntry)
val dirs = new java.util.HashMap[String, DirEntry]()
dirs.put(RootEntry, root)
val in = new ZipInputStream(new ByteArrayInputStream(Streamable.bytes(input)))
closeables ::= in
@tailrec def loop() {
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() {
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
}
private[this] var closeables: List[java.io.Closeable] = Nil
def close(): Unit = {
closeables.foreach(_.close())
}
}
final class ManifestResources(val url: URL) extends ZipArchive(null) {
def iterator = {
val root = new DirEntry(RootEntry)
val dirs = new java.util.HashMap[String, DirEntry]
dirs.put(RootEntry, root)
val manifest = new Manifest(input)
closeables ::= input
val iter = manifest.getEntries().keySet().iterator().asScala.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
}
}
}
private[this] var closeables: List[java.io.Closeable] = Nil
override def close(): Unit = {
closeables.foreach(_.close())
}
}