Skip to content

Commit 57700dd

Browse files
authored
Add support for permissions and symlinks in os.zip/unzip with vendored zip source code from Apache Ant (#374)
### Vendored code Follow the discussion in #356, this adds support for permissions and symlinks in `os.zip`/`unzip` with vendored zip source code from Apache Ant. The vendored source code is generated by the `os.zip.apacheAntZipSource` task and put in `os/zip`. It's shaded with the package renamed from `org.apache.tools.zip` to `os.shaded_org_apache_tools_zip`. `scala-steward.conf` was added and configured to run `os.zip.apacheAntZipSource` on `org.apache.ant:ant` updates. ### Features This brings support for permissions and symlinks to `zip` (for creating new zips, not modifying existing ones), `zip.stream` and `unzip`. As for modifying existing zips, we would still have to rely on `jdk.zipfs` which does not support symlinks. | | file permissions | symlinks | | --- | --- | --- | | `os.zip.open` | if Java Runtime Version >= 14 | | | `os.zip` (create new) | ✅ | ✅ | | `os.zip` (modify existing) | if Java Runtime Version >= 14 | | | `os.zip.stream` | ✅ | ✅ | | `os.unzip` | ✅ | ✅ | | `os.unzip.stream` | | | ### TODO - [ ] **(Advice needed)** make sure we comply with Apache Ant's license to include the code here. Would appreciate opinions on this as I'm not an expert. - [ ] **(Advice needed)** make `ZipOps` JVM only - [x] tests - [x] make sure things don't break on Windows - [x] add permission support to modifying existing zips with `jdk.zipfs` like what @sake92 did in #371
1 parent 341e86b commit 57700dd

File tree

5 files changed

+635
-38
lines changed

5 files changed

+635
-38
lines changed

Readme.adoc

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,7 +1233,8 @@ def apply(dest: os.Path,
12331233
includePatterns: Seq[Regex] = List(),
12341234
preserveMtimes: Boolean = false,
12351235
deletePatterns: Seq[Regex] = List(),
1236-
compressionLevel: Int = -1 /* 0-9 */): os.Path
1236+
compressionLevel: Int = -1, /* 0-9 */
1237+
followLinks: Boolean = true): os.Path
12371238
----
12381239

12391240
The zip object provides functionality to create or modify zip archives. It supports:
@@ -1243,14 +1244,19 @@ The zip object provides functionality to create or modify zip archives. It suppo
12431244
- Exclude Patterns (-x): You can specify files or patterns to exclude while zipping.
12441245
- Include Patterns (-i): You can include specific files or patterns while zipping.
12451246
- Delete Patterns (-d): You can delete specific files from an existing zip archive.
1246-
- Configuring whether or not to preserve filesyste mtimes and permissions
1247+
- Symbolic Links (-y): You can configure to zip symbolic links as symbolic links on Linux/Unix by setting `followLinks = false`. Symbolic links are zipped as the referenced files by default on Linux/Unix, and always on Windows.
1248+
- Configuring whether or not to preserve filesyste mtimes.
1249+
- Preserving Unix file permissions.
12471250

12481251
This will create a new zip archive at `dest` containing `file1.txt` and everything
12491252
inside `sources`. If `dest` already exists as a zip, the files will be appended to the
12501253
existing zip, and any existing zip entries matching `deletePatterns` will be removed.
12511254

1252-
Note that `os.zip` doesn't support creating/unpacking symlinks or filesystem permissions
1253-
in Zip files, because the underlying `java.util.zip.Zip*Stream` doesn't support them.
1255+
When modifying an existing zip file,
1256+
- Unix file permissions will be preserved if Java Runtime Version >= 14.
1257+
- If using Java Runtime Version < 14, Unix file permissions are not preserved, even for existing zip entries.
1258+
- Symbolics links will always be stored as the referenced files.
1259+
- Existing symbolic links stored in the zip might lose their symbolic link file type field and become broken.
12541260

12551261
===== Zipping Files and Folders
12561262

@@ -1375,6 +1381,8 @@ assert(paths == Seq(unzippedFolder / "File.txt"))
13751381
This can be useful for streaming the zipped data to places which are not files:
13761382
over the network, over a pipe, etc.
13771383

1384+
File permissions will be preserved. Symbolic links will be zipped as the referenced files by default on Linux/Unix, and always on Windows. To zip them as symbolic links on Linux/Unix, set `followLinks = false`.
1385+
13781386
==== `os.unzip`
13791387

13801388
===== Unzipping Files
@@ -1384,7 +1392,7 @@ over the network, over a pipe, etc.
13841392
os.unzip(os.Path("/path/to/archive.zip"), Some(os.Path("/path/to/destination")))
13851393
----
13861394

1387-
This extracts the contents of `archive.zip` to the specified destination.
1395+
This extracts the contents of `archive.zip` to the specified destination. It supports preserving file permissions and symbolic links.
13881396

13891397

13901398
===== Excluding Files While Unzipping
@@ -1407,7 +1415,7 @@ You can list the contents of the zip file without extracting them:
14071415
os.unzip.list(os.Path("/path/to/archive.zip"))
14081416
----
14091417

1410-
This will print all the file paths contained in the zip archive.
1418+
This will print all the file paths contained in the zip archive. File permissions and symbolic links will not be preserved.
14111419

14121420
==== `os.unzip.stream`
14131421

@@ -1427,6 +1435,9 @@ os.unzip.stream(
14271435
This can be useful if the zip file does not exist on disk, e.g. if it is received over the network
14281436
or produced in-memory by application logic.
14291437

1438+
File permissions and symbolic links are not supported since permissions and symlink mode are stored as external attributes which might reside in the central directory located at the end of the zip archive.
1439+
For more a more detailed explanation see the `ZipArchiveInputStream` vs `ZipFile` section at https://commons.apache.org/proper/commons-compress/zip.html.
1440+
14301441
OS-Lib also provides the `os.unzip.streamRaw` API, which is a lower level API used internally
14311442
within `os.unzip.stream` but can also be used directly if lower-level control is necessary.
14321443

@@ -1464,6 +1475,8 @@ finally zipFile3.close()
14641475
of the zip file rather than a bare path on the filesystem. Note that you need to call `ZipRoot#close()`
14651476
when you are done with it to avoid leaking filesystem resources.
14661477

1478+
File permissions are only supported for Java Runtime Version >= 14. Symbolic links are not supported. Using `os.zip.open` on a zip archive that contains symbolic links might break the links.
1479+
14671480
=== Filesystem Metadata
14681481

14691482
==== `os.stat`
@@ -1793,7 +1806,7 @@ is run:
17931806

17941807
* `cwd`: the working directory of the subprocess
17951808
* `env`: any additional environment variables you wish to set in the subprocess
1796-
in addition to those passed via `propagateEnv`. You can also set their values
1809+
in addition to those passed via `propagateEnv`. You can also set their values
17971810
to `null` to remove specific variables.
17981811
* `stdin`: any data you wish to pass to the subprocess's standard input
17991812
* `stdout`/`stderr`: these are ``os.Redirect``s that let you configure how the

build.mill

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ object Deps {
2323
val acyclic = ivy"com.lihaoyi:::acyclic:0.3.18"
2424
val jna = ivy"net.java.dev.jna:jna:5.15.0"
2525
val geny = ivy"com.lihaoyi::geny::1.1.1"
26+
val ant = ivy"org.apache.ant:ant:1.10.15"
2627
val sourcecode = ivy"com.lihaoyi::sourcecode::0.4.2"
2728
val utest = ivy"com.lihaoyi::utest::0.8.4"
2829
val expecty = ivy"com.eed3si9n.expecty::expecty::0.16.0"
@@ -78,13 +79,7 @@ trait MiMaChecks extends Mima {
7879
)
7980
}
8081

81-
trait OsLibModule
82-
extends CrossScalaModule
83-
with PublishModule
84-
with AcyclicModule
85-
with SafeDeps
86-
with PlatformScalaModule { outer =>
87-
82+
trait OsLibPublishModule extends PublishModule {
8883
def publishVersion = VcsVersion.vcsState().format()
8984
def pomSettings = PomSettings(
9085
description = artifactName(),
@@ -99,6 +94,14 @@ trait OsLibModule
9994
Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi")
10095
)
10196
)
97+
}
98+
99+
trait OsLibModule
100+
extends OsLibPublishModule
101+
with CrossScalaModule
102+
with AcyclicModule
103+
with SafeDeps
104+
with PlatformScalaModule { outer =>
102105

103106
trait OsLibTestModule extends ScalaModule with TestModule.Utest with SafeDeps {
104107
def ivyDeps = Agg(Deps.utest, Deps.sourcecode)
@@ -169,6 +172,8 @@ object os extends Module {
169172

170173
object jvm extends Cross[OsJvmModule](scalaVersions)
171174
trait OsJvmModule extends OsModule with MiMaChecks {
175+
def moduleDeps = super.moduleDeps ++ Seq(os.zip)
176+
172177
object test extends ScalaTests with OsLibTestModule {
173178
override def ivyDeps = T { super.ivyDeps() ++ Agg(Deps.expecty) }
174179

@@ -194,6 +199,55 @@ object os extends Module {
194199
object nohometest extends ScalaTests with OsLibTestModule
195200
}
196201

202+
object zip extends JavaModule with OsLibPublishModule {
203+
def apacheAntZipOriginalSource: T[PathRef] = Task(persistent = true) {
204+
if (!_root_.os.exists(Task.dest / "unzipped")) {
205+
val antVersion = Deps.ant.version
206+
_root_.os.unzip.stream(
207+
requests.get.stream(
208+
s"https://repo1.maven.org/maven2/org/apache/ant/ant/$antVersion/ant-$antVersion-sources.jar"
209+
),
210+
Task.dest / "unzipped"
211+
)
212+
}
213+
214+
PathRef(Task.dest / "unzipped" / "org/apache/tools/zip")
215+
}
216+
217+
/**
218+
* Shades Apache Ant
219+
* [[`org.apache.tools.zip` https://ant.apache.org/manual/api/org/apache/tools/zip/package-summary.html package]] to
220+
* provide Unix file permission and symbolic link support for `os.zip` and `os.unzip`
221+
*
222+
* A third party dependency is needed since JDK's own
223+
* [[`jdk.zipfs` https://docs.oracle.com/en/java/javase/14/docs/api/jdk.zipfs/module-summary.html]] does not support
224+
* symbolic links and only supports file permissions since JDK 14.
225+
*
226+
* Apache Ant `org.apache.tools.zip` was chosen over Apache Commons Compress due to the former not having any
227+
* third party dependency, only depending on Java core libraries while the later also depends on Apache Commons IO.
228+
*
229+
* To avoid classpath conflicts, the dependency is shaded and compiled from source. Only the `org.apache.tools.zip`
230+
* package, not the entire Ant codebase, is needed. This only adds < 100kb to Os-Lib jar size.
231+
*/
232+
def generatedSources = T {
233+
val pkg = "os.shaded_org_apache_tools_zip"
234+
val zipSrc = T.dest / "os/shaded_org_apache_tools_zip"
235+
_root_.os.makeDir.all(zipSrc)
236+
237+
// Move from "package org.apache.tools.zip" to "package os.shaded_org_apache_tools_zip"
238+
// Make all classes package private (private [os]) by removing any `public` access modifier
239+
_root_.os.walk.stream(apacheAntZipOriginalSource().path)
240+
.filter(_.ext == "java")
241+
.foreach { p =>
242+
val content = _root_.os.read(p)
243+
.replaceAll("org.apache.tools.zip", pkg)
244+
_root_.os.write(zipSrc / p.last, content)
245+
}
246+
247+
Seq(PathRef(T.dest))
248+
}
249+
}
250+
197251
/*object native extends Cross[OsNativeModule](scalaVersions)
198252
trait OsNativeModule extends OsModule with ScalaNativeModule {
199253
def scalaNativeVersion = "0.5.2"

0 commit comments

Comments
 (0)