blob: 2fa0a1373e3d062081eb0b6fc64135c74fdf7327 [file] [log] [blame]
// Copyright 2014 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Copied from bazel core and there is some code in other branches which will use the some of the unused elements. Fix
// this later on.
@file:Suppress("unused","MemberVisibilityCanBePrivate")
package io.bazel.kotlin.builder.utils.jars
import java.io.FileNotFoundException
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.*
import java.util.jar.Attributes
import java.util.jar.JarEntry
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.CRC32
/**
* A simple helper class for creating Jar files. All Jar entries are sorted alphabetically. Allows
* normalization of Jar entries by setting the timestamp of non-.class files to the DOS epoch.
* Timestamps of .class files are set to the DOS epoch + 2 seconds (The zip timestamp granularity)
* Adjusting the timestamp for .class files is necessary since otherwise javac will recompile java
* files if both the java file and its .class file are present.
*/
open class JarHelper internal constructor (
// The path to the Jar we want to create
protected val jarPath: Path,
// The properties to describe how to create the Jar
protected val normalize: Boolean = true,
protected val verbose: Boolean = false,
compression: Boolean = true
) {
private var storageMethod: Int = JarEntry.DEFLATED
// The state needed to create the Jar
private val names: MutableSet<String> = HashSet()
init {
setCompression(compression)
}
/**
* Enables or disables compression for the Jar file entries.
*
* @param compression if true enables compressions for the Jar file entries.
*/
private fun setCompression(compression: Boolean) {
storageMethod = if (compression) JarEntry.DEFLATED else JarEntry.STORED
}
/**
* Returns the normalized timestamp for a jar entry based on its name. This is necessary since
* javac will, when loading a class X, prefer a source file to a class file, if both files have
* the same timestamp. Therefore, we need to adjust the timestamp for class files to slightly
* after the normalized time.
*
* @param name The name of the file for which we should return the normalized timestamp.
* @return the time for a new Jar file entry in milliseconds since the epoch.
*/
private fun normalizedTimestamp(name: String): Long {
return if (name.endsWith(".class")) {
DEFAULT_TIMESTAMP + MINIMUM_TIMESTAMP_INCREMENT
} else {
DEFAULT_TIMESTAMP
}
}
/**
* Returns the time for a new Jar file entry in milliseconds since the epoch. Uses JarCreator.DEFAULT_TIMESTAMP]
* for normalized entries, [System.currentTimeMillis] otherwise.
*
* @param filename The name of the file for which we are entering the time
* @return the time for a new Jar file entry in milliseconds since the epoch.
*/
private fun newEntryTimeMillis(filename: String): Long {
return if (normalize) normalizedTimestamp(filename) else System.currentTimeMillis()
}
/**
* Writes an entry with specific contents to the jar. Directory entries must include the trailing
* '/'.
*/
@Throws(IOException::class)
private fun writeEntry(out: JarOutputStream, name: String, content: ByteArray) {
if (names.add(name)) {
// Create a new entry
val entry = JarEntry(name)
entry.time = newEntryTimeMillis(name)
val size = content.size
entry.size = size.toLong()
if (size == 0) {
entry.method = JarEntry.STORED
entry.crc = 0
out.putNextEntry(entry)
} else {
entry.method = storageMethod
if (storageMethod == JarEntry.STORED) {
val crc = CRC32()
crc.update(content)
entry.crc = crc.value
}
out.putNextEntry(entry)
out.write(content)
}
out.closeEntry()
}
}
/**
* Writes a standard Java manifest entry into the JarOutputStream. This includes the directory
* entry for the "META-INF" directory
*
* @param content the Manifest content to write to the manifest entry.
* @throws IOException
*/
@Throws(IOException::class)
protected fun writeManifestEntry(out: JarOutputStream, content: ByteArray) {
val oldStorageMethod = storageMethod
// Do not compress small manifest files, the compressed one is frequently
// larger than the original. The threshold of 256 bytes is somewhat arbitrary.
if (content.size < 256) {
storageMethod = JarEntry.STORED
}
try {
writeEntry(out, MANIFEST_DIR, byteArrayOf())
writeEntry(out, MANIFEST_NAME, content)
} finally {
storageMethod = oldStorageMethod
}
}
/**
* Copies file or directory entries from the file system into the jar. Directory entries will be
* detected and their names automatically '/' suffixed.
*/
@Throws(IOException::class)
protected fun JarOutputStream.copyEntry(name: String, path: Path) {
var normalizedName = name
if (!names.contains(normalizedName)) {
if (!Files.exists(path)) {
throw FileNotFoundException("${path.toAbsolutePath()} (No such file or directory)")
}
val isDirectory = Files.isDirectory(path)
if (isDirectory && !normalizedName.endsWith("/")) {
normalizedName = "$normalizedName/" // always normalize directory names before checking set
}
if (names.add(normalizedName)) {
if (verbose) {
System.err.println("adding $path")
}
// Create a new entry
val size = if (isDirectory) 0 else Files.size(path)
val outEntry = JarEntry(normalizedName)
val newtime = if (normalize) normalizedTimestamp(normalizedName) else Files.getLastModifiedTime(path).toMillis()
outEntry.time = newtime
outEntry.size = size
if (size == 0L) {
outEntry.method = JarEntry.STORED
outEntry.crc = 0
putNextEntry(outEntry)
} else {
outEntry.method = storageMethod
if (storageMethod == JarEntry.STORED) {
// ZipFile requires us to calculate the CRC-32 for any STORED entry.
// It would be nicer to do this via DigestInputStream, but
// the architecture of ZipOutputStream requires us to know the CRC-32
// before we write the data to the stream.
val bytes = Files.readAllBytes(path)
val crc = CRC32()
crc.update(bytes)
outEntry.crc = crc.value
putNextEntry(outEntry)
write(bytes)
} else {
putNextEntry(outEntry)
Files.copy(path, this)
}
}
closeEntry()
}
}
}
/**
* Copies a a single entry into the jar. This variant differs from the other [copyEntry] in two ways. Firstly the
* jar contents are already loaded in memory and Secondly the [name] and [path] entries don't necessarily have a
* correspondence.
*
* @param path the path used to retrieve the timestamp in case normalize is disabled.
* @param data if this is empty array then the entry is a directory.
*/
protected fun JarOutputStream.copyEntry(name: String, path: Path? = null, data: ByteArray = EMPTY_BYTEARRAY) {
val outEntry = JarEntry(name)
outEntry.time = when {
normalize -> normalizedTimestamp(name)
else -> Files.getLastModifiedTime(checkNotNull(path)).toMillis()
}
outEntry.size = data.size.toLong()
if (data.isEmpty()) {
outEntry.method = JarEntry.STORED
outEntry.crc = 0
putNextEntry(outEntry)
} else {
outEntry.method = storageMethod
if (storageMethod == JarEntry.STORED) {
val crc = CRC32()
crc.update(data)
outEntry.crc = crc.value
putNextEntry(outEntry)
write(data)
} else {
putNextEntry(outEntry)
write(data)
}
}
closeEntry()
}
companion object {
const val MANIFEST_DIR = "META-INF/"
const val MANIFEST_NAME = JarFile.MANIFEST_NAME
const val SERVICES_DIR = "META-INF/services/"
internal val EMPTY_BYTEARRAY = ByteArray(0)
/** Normalize timestamps. */
val DEFAULT_TIMESTAMP = LocalDateTime.of(1980, 1, 1, 0, 0, 0)
.atZone(ZoneId.systemDefault())
.toInstant()
.toEpochMilli()
// These attributes are used by JavaBuilder, Turbine, and ijar.
// They must all be kept in sync.
val TARGET_LABEL = Attributes.Name("Target-Label")
val INJECTING_RULE_KIND = Attributes.Name("Injecting-Rule-Kind")
// ZIP timestamps have a resolution of 2 seconds.
// see http://www.info-zip.org/FAQ.html#limits
const val MINIMUM_TIMESTAMP_INCREMENT = 2000L
}
}