diff options
Diffstat (limited to 'tools/java/com/google/devtools/kotlin/srczip/SourceJarZipper.kt')
-rw-r--r-- | tools/java/com/google/devtools/kotlin/srczip/SourceJarZipper.kt | 291 |
1 files changed, 291 insertions, 0 deletions
diff --git a/tools/java/com/google/devtools/kotlin/srczip/SourceJarZipper.kt b/tools/java/com/google/devtools/kotlin/srczip/SourceJarZipper.kt new file mode 100644 index 0000000..c82f356 --- /dev/null +++ b/tools/java/com/google/devtools/kotlin/srczip/SourceJarZipper.kt @@ -0,0 +1,291 @@ +/* + * * Copyright 2022 Google LLC. 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. + */ + +package com.google.devtools.kotlin.srczip + +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardCopyOption +import java.time.LocalDateTime +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream +import kotlin.system.exitProcess +import picocli.CommandLine +import picocli.CommandLine.Command +import picocli.CommandLine.Model.CommandSpec +import picocli.CommandLine.Option +import picocli.CommandLine.ParameterException +import picocli.CommandLine.Parameters +import picocli.CommandLine.Spec + +@Command( + name = "source-jar-zipper", + subcommands = [Unzip::class, Zip::class, ZipResources::class], + description = ["A tool to pack and unpack srcjar files, and to zip resource files"], +) +class SourceJarZipper : Runnable { + @Spec private lateinit var spec: CommandSpec + + override fun run() { + throw ParameterException(spec.commandLine(), "Specify a command: zip, zip_resources or unzip") + } +} + +fun main(args: Array<String>) { + val exitCode = CommandLine(SourceJarZipper()).execute(*args) + exitProcess(exitCode) +} + +/** + * Checks for duplicates and adds an entry into [errors] if one is found, otherwise adds a pair of + * [zipPath] and [sourcePath] to the receiver + * + * @param[zipPath] relative path inside the jar, built either from package name (e.g. package + * com.google.foo -> com/google/foo/FileName.kt) or by resolving the file name relative to the + * directory it came from (e.g. foo/bar/1/2.txt came from foo/bar -> 1/2.txt) + * @param[sourcePath] full path of file into its file system + * @param[errors] list of strings describing catched errors + * @receiver a mutable map of path to path, where keys are relative paths of files inside the + * resulting .jar, and values are full paths of files + */ +fun MutableMap<Path, Path>.checkForDuplicatesAndSetFilePathToPathInsideJar( + zipPath: Path, + sourcePath: Path, + errors: MutableList<String>, +) { + val duplicatedSourcePath: Path? = this[zipPath] + if (duplicatedSourcePath == null) { + this[zipPath] = sourcePath + } else { + errors.add( + "${sourcePath} has the same path inside .jar as ${duplicatedSourcePath}! " + + "If it is intended behavior rename one or both of them." + ) + } +} + +private fun clearSingletonEmptyPath(list: MutableList<Path>) { + if (list.size == 1 && list[0].toString() == "") { + list.clear() + } +} + +// Normalize timestamps +val DEFAULT_TIMESTAMP = LocalDateTime.of(2010, 1, 1, 0, 0, 0) + +fun MutableMap<Path, Path>.writeToStream( + zipper: ZipOutputStream, + prefix: String = "", +) { + for ((zipPath, sourcePath) in this) { + BufferedInputStream(Files.newInputStream(sourcePath)).use { inputStream -> + val entry = ZipEntry(Paths.get(prefix).resolve(zipPath).toString()) + entry.timeLocal = DEFAULT_TIMESTAMP + zipper.putNextEntry(entry) + inputStream.copyTo(zipper, bufferSize = 1024) + } + } +} + +@Command(name = "zip", description = ["Zip source files into a source jar file"]) +class Zip : Runnable { + + @Parameters(index = "0", paramLabel = "outputJar", description = ["Output jar"]) + lateinit var outputJar: Path + + @Option( + names = ["-i", "--ignore_not_allowed_files"], + description = ["Ignore not .kt, .java or invalid file paths without raising an exception"], + ) + var ignoreNotAllowedFiles = false + + @Option( + names = ["--kotlin_srcs"], + split = ",", + description = ["Kotlin source files"], + ) + val kotlinSrcs = mutableListOf<Path>() + + @Option( + names = ["--common_srcs"], + split = ",", + description = ["Common source files"], + ) + val commonSrcs = mutableListOf<Path>() + + private companion object { + const val PACKAGE_SPACE = "package " + // can't start with digit and can't be all underscores + val IDENTIFIER_REGEX = Regex("(?:[a-zA-Z]|_+[a-zA-Z0-9])\\w*") + val PACKAGE_NAME_REGEX = Regex("$IDENTIFIER_REGEX(?:\\.$IDENTIFIER_REGEX)*") + } + + override fun run() { + clearSingletonEmptyPath(kotlinSrcs) + clearSingletonEmptyPath(commonSrcs) + + // Validating files and getting paths for resulting .jar in one cycle + // for each _srcs list + val ktZipPathToSourcePath = mutableMapOf<Path, Path>() + val commonZipPathToSourcePath = mutableMapOf<Path, Path>() + val errors = mutableListOf<String>() + + fun Path.getPackagePath(): Path { + this.toFile().bufferedReader().use { stream -> + while (true) { + val line = stream.readLine() ?: return this.fileName + + if (line.startsWith(PACKAGE_SPACE)) { + // Kotlin allows usage of reserved words in package names framing them + // with backquote symbol "`" + val packageName = + line + .removePrefix(PACKAGE_SPACE) + .substringBefore("//") + .trim() + .removeSuffix(";") + .replace(Regex("\\B`(.+?)`\\B"), "$1") + if (!PACKAGE_NAME_REGEX.matches(packageName)) { + errors.add("$this contains an invalid package name") + return this.fileName + } + return Paths.get(packageName.replace(".", "/")).resolve(this.fileName) + } + } + } + } + + fun Path.validateFile(): Boolean { + when { + !Files.isRegularFile(this) -> { + if (!ignoreNotAllowedFiles) errors.add("${this} is not a file") + return false + } + !this.toString().endsWith(".kt") && !this.toString().endsWith(".java") -> { + if (!ignoreNotAllowedFiles) errors.add("${this} is not a Kotlin file") + return false + } + else -> return true + } + } + + for (sourcePath in kotlinSrcs) { + if (sourcePath.validateFile()) { + ktZipPathToSourcePath.checkForDuplicatesAndSetFilePathToPathInsideJar( + sourcePath.getPackagePath(), + sourcePath, + errors, + ) + } + } + + for (sourcePath in commonSrcs) { + if (sourcePath.validateFile()) { + commonZipPathToSourcePath.checkForDuplicatesAndSetFilePathToPathInsideJar( + sourcePath.getPackagePath(), + sourcePath, + errors, + ) + } + } + + check(errors.isEmpty()) { errors.joinToString("\n") } + + ZipOutputStream(BufferedOutputStream(Files.newOutputStream(outputJar))).use { zipper -> + commonZipPathToSourcePath.writeToStream(zipper, "common-srcs") + ktZipPathToSourcePath.writeToStream(zipper) + } + } +} + +@Command(name = "unzip", description = ["Unzip a jar archive into a specified directory"]) +class Unzip : Runnable { + + @Parameters(index = "0", paramLabel = "inputJar", description = ["Jar archive to unzip"]) + lateinit var inputJar: Path + + @Parameters(index = "1", paramLabel = "outputDir", description = ["Output directory"]) + lateinit var outputDir: Path + + override fun run() { + ZipInputStream(Files.newInputStream(inputJar)).use { unzipper -> + while (true) { + val zipEntry: ZipEntry? = unzipper.nextEntry + if (zipEntry == null) return + + val entryName = zipEntry.name + check(!entryName.contains("./")) { "Cannot unpack srcjar with relative path ${entryName}" } + + if (!entryName.endsWith(".kt") && !entryName.endsWith(".java")) continue + + val entryPath = outputDir.resolve(entryName) + if (!Files.exists(entryPath.parent)) Files.createDirectories(entryPath.parent) + Files.copy(unzipper, entryPath, StandardCopyOption.REPLACE_EXISTING) + } + } + } +} + +@Command(name = "zip_resources", description = ["Zip resources"]) +class ZipResources : Runnable { + + @Parameters(index = "0", paramLabel = "outputJar", description = ["Output jar"]) + lateinit var outputJar: Path + + @Option( + names = ["--input_dirs"], + split = ",", + description = ["Input files directories"], + required = true, + ) + val inputDirs = mutableListOf<Path>() + + override fun run() { + clearSingletonEmptyPath(inputDirs) + + val filePathToOutputPath = mutableMapOf<Path, Path>() + val errors = mutableListOf<String>() + + // inputDirs has filter checking if the dir exists, because some empty dirs generated by blaze + // may not exist from Kotlin compiler's side. It turned out to be safer to apply a filter then + // to rely that generated directories are always directories, not just path names + for (dirPath in inputDirs.filter { curDirPath -> Files.exists(curDirPath) }) { + if (!Files.isDirectory(dirPath)) { + errors.add("${dirPath} is not a directory") + } else { + Files.walk(dirPath) + .filter { fileOrDir -> !Files.isDirectory(fileOrDir) } + .forEach { filePath -> + filePathToOutputPath.checkForDuplicatesAndSetFilePathToPathInsideJar( + dirPath.relativize(filePath), + filePath, + errors + ) + } + } + } + + check(errors.isEmpty()) { errors.joinToString("\n") } + + ZipOutputStream(BufferedOutputStream(Files.newOutputStream(outputJar))).use { zipper -> + filePathToOutputPath.writeToStream(zipper) + } + } +} |