• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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