1 /*
<lambda>null2 * * Copyright 2022 Google LLC. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the License);
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.google.devtools.kotlin
18
19 import java.io.BufferedInputStream
20 import java.io.BufferedOutputStream
21 import java.nio.file.Files
22 import java.nio.file.Path
23 import java.nio.file.Paths
24 import java.nio.file.StandardCopyOption
25 import java.util.zip.ZipEntry
26 import java.util.zip.ZipInputStream
27 import java.util.zip.ZipOutputStream
28 import kotlin.system.exitProcess
29 import picocli.CommandLine
30 import picocli.CommandLine.Command
31 import picocli.CommandLine.Model.CommandSpec
32 import picocli.CommandLine.Option
33 import picocli.CommandLine.ParameterException
34 import picocli.CommandLine.Parameters
35 import picocli.CommandLine.Spec
36
37 @Command(
38 name = "source-jar-zipper",
39 subcommands = [Unzip::class, Zip::class, ZipResources::class],
40 description = ["A tool to pack and unpack srcjar files, and to zip resource files"],
41 )
42 class SourceJarZipper : Runnable {
43 @Spec private lateinit var spec: CommandSpec
44 override fun run() {
45 throw ParameterException(spec.commandLine(), "Specify a command: zip, zip_resources or unzip")
46 }
47 }
48
mainnull49 fun main(args: Array<String>) {
50 val exitCode = CommandLine(SourceJarZipper()).execute(*args)
51 exitProcess(exitCode)
52 }
53
54 /**
55 * Checks for duplicates and adds an entry into [errors] if one is found, otherwise adds a pair of
56 * [zipPath] and [sourcePath] to the receiver
57 *
58 * @param[zipPath] relative path inside the jar, built either from package name (e.g. package
59 * com.google.foo -> com/google/foo/FileName.kt) or by resolving the file name relative to the
60 * directory it came from (e.g. foo/bar/1/2.txt came from foo/bar -> 1/2.txt)
61 * @param[sourcePath] full path of file into its file system
62 * @param[errors] list of strings describing catched errors
63 * @receiver a mutable map of path to path, where keys are relative paths of files inside the
64 * resulting .jar, and values are full paths of files
65 */
checkForDuplicatesAndSetFilePathToPathInsideJarnull66 fun MutableMap<Path, Path>.checkForDuplicatesAndSetFilePathToPathInsideJar(
67 zipPath: Path,
68 sourcePath: Path,
69 errors: MutableList<String>,
70 ) {
71 val duplicatedSourcePath: Path? = this[zipPath]
72 if (duplicatedSourcePath == null) {
73 this[zipPath] = sourcePath
74 } else {
75 errors.add(
76 "${sourcePath} has the same path inside .jar as ${duplicatedSourcePath}! " +
77 "If it is intended behavior rename one or both of them."
78 )
79 }
80 }
81
clearSingletonEmptyPathnull82 private fun clearSingletonEmptyPath(list: MutableList<Path>) {
83 if (list.size == 1 && list[0].toString() == "") {
84 list.clear()
85 }
86 }
87
writeToStreamnull88 fun MutableMap<Path, Path>.writeToStream(
89 zipper: ZipOutputStream,
90 prefix: String = "",
91 ) {
92 for ((zipPath, sourcePath) in this) {
93 BufferedInputStream(Files.newInputStream(sourcePath)).use { inputStream ->
94 val entry = ZipEntry(Paths.get(prefix).resolve(zipPath).toString())
95 entry.time = 0
96 zipper.putNextEntry(entry)
97 inputStream.copyTo(zipper, bufferSize = 1024)
98 }
99 }
100 }
101
102 @Command(name = "zip", description = ["Zip source files into a source jar file"])
103 class Zip : Runnable {
104
105 @Parameters(index = "0", paramLabel = "outputJar", description = ["Output jar"])
106 lateinit var outputJar: Path
107
108 @Option(
109 names = ["-i", "--ignore_not_allowed_files"],
110 description = ["Ignore not .kt, .java or invalid file paths without raising an exception"],
111 )
112 var ignoreNotAllowedFiles = false
113
114 @Option(
115 names = ["--kotlin_srcs"],
116 split = ",",
117 description = ["Kotlin source files"],
118 )
119 val kotlinSrcs = mutableListOf<Path>()
120
121 @Option(
122 names = ["--common_srcs"],
123 split = ",",
124 description = ["Common source files"],
125 )
126 val commonSrcs = mutableListOf<Path>()
127
128 companion object {
129 const val PACKAGE_SPACE = "package "
130 val PACKAGE_NAME_REGEX = "[a-zA-Z][a-zA-Z0-9_]*(\\.[a-zA-Z0-9_]+)*".toRegex()
131 }
132
runnull133 override fun run() {
134 clearSingletonEmptyPath(kotlinSrcs)
135 clearSingletonEmptyPath(commonSrcs)
136
137 // Validating files and getting paths for resulting .jar in one cycle
138 // for each _srcs list
139 val ktZipPathToSourcePath = mutableMapOf<Path, Path>()
140 val commonZipPathToSourcePath = mutableMapOf<Path, Path>()
141 val errors = mutableListOf<String>()
142
143 fun Path.getPackagePath(): Path {
144 this.toFile().bufferedReader().use { stream ->
145 while (true) {
146 val line = stream.readLine() ?: return this.fileName
147
148 if (line.startsWith(PACKAGE_SPACE)) {
149 // Kotlin allows usage of reserved words in package names framing them
150 // with backquote symbol "`"
151 val packageName =
152 line.substring(PACKAGE_SPACE.length).trim().replace(";", "").replace("`", "")
153 if (!PACKAGE_NAME_REGEX.matches(packageName)) {
154 errors.add("${this} contains an invalid package name")
155 return this.fileName
156 }
157 return Paths.get(packageName.replace(".", "/")).resolve(this.fileName)
158 }
159 }
160 }
161 }
162
163 fun Path.validateFile(): Boolean {
164 when {
165 !Files.isRegularFile(this) -> {
166 if (!ignoreNotAllowedFiles) errors.add("${this} is not a file")
167 return false
168 }
169 !this.toString().endsWith(".kt") && !this.toString().endsWith(".java") -> {
170 if (!ignoreNotAllowedFiles) errors.add("${this} is not a Kotlin file")
171 return false
172 }
173 else -> return true
174 }
175 }
176
177 for (sourcePath in kotlinSrcs) {
178 if (sourcePath.validateFile()) {
179 ktZipPathToSourcePath.checkForDuplicatesAndSetFilePathToPathInsideJar(
180 sourcePath.getPackagePath(),
181 sourcePath,
182 errors,
183 )
184 }
185 }
186
187 for (sourcePath in commonSrcs) {
188 if (sourcePath.validateFile()) {
189 commonZipPathToSourcePath.checkForDuplicatesAndSetFilePathToPathInsideJar(
190 sourcePath.getPackagePath(),
191 sourcePath,
192 errors,
193 )
194 }
195 }
196
197 check(errors.isEmpty()) { errors.joinToString("\n") }
198
199 ZipOutputStream(BufferedOutputStream(Files.newOutputStream(outputJar))).use { zipper ->
200 commonZipPathToSourcePath.writeToStream(zipper, "common-srcs")
201 ktZipPathToSourcePath.writeToStream(zipper)
202 }
203 }
204 }
205
206 @Command(name = "unzip", description = ["Unzip a jar archive into a specified directory"])
207 class Unzip : Runnable {
208
209 @Parameters(index = "0", paramLabel = "inputJar", description = ["Jar archive to unzip"])
210 lateinit var inputJar: Path
211
212 @Parameters(index = "1", paramLabel = "outputDir", description = ["Output directory"])
213 lateinit var outputDir: Path
214
runnull215 override fun run() {
216 ZipInputStream(Files.newInputStream(inputJar)).use { unzipper ->
217 while (true) {
218 val zipEntry: ZipEntry? = unzipper.nextEntry
219 if (zipEntry == null) return
220
221 val entryName = zipEntry.name
222 check(!entryName.contains("./")) { "Cannot unpack srcjar with relative path ${entryName}" }
223
224 if (!entryName.endsWith(".kt") && !entryName.endsWith(".java")) continue
225
226 val entryPath = outputDir.resolve(entryName)
227 if (!Files.exists(entryPath.parent)) Files.createDirectories(entryPath.parent)
228 Files.copy(unzipper, entryPath, StandardCopyOption.REPLACE_EXISTING)
229 }
230 }
231 }
232 }
233
234 @Command(name = "zip_resources", description = ["Zip resources"])
235 class ZipResources : Runnable {
236
237 @Parameters(index = "0", paramLabel = "outputJar", description = ["Output jar"])
238 lateinit var outputJar: Path
239
240 @Option(
241 names = ["--input_dirs"],
242 split = ",",
243 description = ["Input files directories"],
244 required = true,
245 )
246 val inputDirs = mutableListOf<Path>()
247
runnull248 override fun run() {
249 clearSingletonEmptyPath(inputDirs)
250
251 val filePathToOutputPath = mutableMapOf<Path, Path>()
252 val errors = mutableListOf<String>()
253
254 // inputDirs has filter checking if the dir exists, because some empty dirs generated by blaze
255 // may not exist from Kotlin compiler's side. It turned out to be safer to apply a filter then
256 // to rely that generated directories are always directories, not just path names
257 for (dirPath in inputDirs.filter { curDirPath -> Files.exists(curDirPath) }) {
258 if (!Files.isDirectory(dirPath)) {
259 errors.add("${dirPath} is not a directory")
260 } else {
261 Files.walk(dirPath)
262 .filter { fileOrDir -> !Files.isDirectory(fileOrDir) }
263 .forEach { filePath ->
264 filePathToOutputPath.checkForDuplicatesAndSetFilePathToPathInsideJar(
265 dirPath.relativize(filePath),
266 filePath,
267 errors
268 )
269 }
270 }
271 }
272
273 check(errors.isEmpty()) { errors.joinToString("\n") }
274
275 ZipOutputStream(BufferedOutputStream(Files.newOutputStream(outputJar))).use { zipper ->
276 filePathToOutputPath.writeToStream(zipper)
277 }
278 }
279 }
280