Skip to content

Commit 538d77d

Browse files
committed
Add support for zip/unzip permissions and symlinks
via Apache Ant zip tools
1 parent 60d334d commit 538d77d

File tree

3 files changed

+219
-23
lines changed

3 files changed

+219
-23
lines changed

build.mill

Lines changed: 2 additions & 1 deletion
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 compress = 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"
@@ -112,7 +113,7 @@ trait OsLibModule
112113
}
113114

114115
trait OsModule extends OsLibModule { outer =>
115-
def ivyDeps = Agg(Deps.geny)
116+
def ivyDeps = Agg(Deps.geny, Deps.compress)
116117
override def compileIvyDeps = T {
117118
val scalaReflectOpt = Option.when(!ZincWorkerUtil.isDottyOrScala3(scalaVersion()))(
118119
Deps.scalaReflect(scalaVersion())

os/src/PermissionUtils.java

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
// taken from https://github.com/apache/ant/blob/ecfca50b0133c576021dd088f855ee878a6b8e66/src/main/org/apache/tools/ant/util/PermissionUtils.java
20+
package os;
21+
22+
import java.io.IOException;
23+
import java.nio.file.Files;
24+
import java.nio.file.Path;
25+
import java.nio.file.attribute.BasicFileAttributes;
26+
import java.nio.file.attribute.PosixFileAttributeView;
27+
import java.nio.file.attribute.PosixFilePermission;
28+
import java.util.EnumSet;
29+
import java.util.Set;
30+
31+
/**
32+
* Contains helper methods for dealing with {@link
33+
* PosixFilePermission} or the traditional Unix mode representation of
34+
* permissions.
35+
*/
36+
class PermissionUtils {
37+
38+
private PermissionUtils() { }
39+
40+
/**
41+
* Translates a set of permissions into a Unix stat(2) {@code
42+
* st_mode} result.
43+
* @param permissions the permissions
44+
* @param type the file type
45+
* @return the "mode"
46+
*/
47+
public static int modeFromPermissions(Set<PosixFilePermission> permissions,
48+
FileType type) {
49+
int mode;
50+
switch (type) {
51+
case SYMLINK:
52+
mode = 012;
53+
break;
54+
case REGULAR_FILE:
55+
mode = 010;
56+
break;
57+
case DIR:
58+
mode = 004;
59+
break;
60+
default:
61+
// OTHER could be a character or block device, a socket or a FIFO - so don't set anything
62+
mode = 0;
63+
break;
64+
}
65+
mode <<= 3;
66+
mode <<= 3; // we don't support sticky, setuid, setgid
67+
mode |= modeFromPermissions(permissions, "OWNER");
68+
mode <<= 3;
69+
mode |= modeFromPermissions(permissions, "GROUP");
70+
mode <<= 3;
71+
mode |= modeFromPermissions(permissions, "OTHERS");
72+
return mode;
73+
}
74+
75+
/**
76+
* Translates a Unix stat(2) {@code st_mode} compatible value into
77+
* a set of permissions.
78+
* @param mode the "mode"
79+
* @return set of permissions
80+
*/
81+
public static Set<PosixFilePermission> permissionsFromMode(int mode) {
82+
Set<PosixFilePermission> permissions = EnumSet.noneOf(PosixFilePermission.class);
83+
addPermissions(permissions, "OTHERS", mode);
84+
addPermissions(permissions, "GROUP", mode >> 3);
85+
addPermissions(permissions, "OWNER", mode >> 6);
86+
return permissions;
87+
}
88+
89+
private static long modeFromPermissions(Set<PosixFilePermission> permissions,
90+
String prefix) {
91+
long mode = 0;
92+
if (permissions.contains(PosixFilePermission.valueOf(prefix + "_READ"))) {
93+
mode |= 4;
94+
}
95+
if (permissions.contains(PosixFilePermission.valueOf(prefix + "_WRITE"))) {
96+
mode |= 2;
97+
}
98+
if (permissions.contains(PosixFilePermission.valueOf(prefix + "_EXECUTE"))) {
99+
mode |= 1;
100+
}
101+
return mode;
102+
}
103+
104+
private static void addPermissions(Set<PosixFilePermission> permissions,
105+
String prefix, long mode) {
106+
if ((mode & 1) == 1) {
107+
permissions.add(PosixFilePermission.valueOf(prefix + "_EXECUTE"));
108+
}
109+
if ((mode & 2) == 2) {
110+
permissions.add(PosixFilePermission.valueOf(prefix + "_WRITE"));
111+
}
112+
if ((mode & 4) == 4) {
113+
permissions.add(PosixFilePermission.valueOf(prefix + "_READ"));
114+
}
115+
}
116+
117+
/**
118+
* The supported types of files, maps to the {@code isFoo} methods
119+
* in {@link java.nio.file.attribute.BasicFileAttributes}.
120+
*/
121+
public enum FileType {
122+
/** A regular file. */
123+
REGULAR_FILE,
124+
/** A directory. */
125+
DIR,
126+
/** A symbolic link. */
127+
SYMLINK,
128+
/** Something that is neither a regular file nor a directory nor a symbolic link. */
129+
OTHER;
130+
131+
/**
132+
* Determines the file type of a {@link Path}.
133+
*
134+
* @param p Path
135+
* @return FileType
136+
* @throws IOException if file attributes cannot be read
137+
*/
138+
public static FileType of(Path p) throws IOException {
139+
BasicFileAttributes attrs =
140+
Files.readAttributes(p, BasicFileAttributes.class);
141+
if (attrs.isRegularFile()) {
142+
return FileType.REGULAR_FILE;
143+
} else if (attrs.isDirectory()) {
144+
return FileType.DIR;
145+
} else if (attrs.isSymbolicLink()) {
146+
return FileType.SYMLINK;
147+
}
148+
return FileType.OTHER;
149+
}
150+
}
151+
152+
public static int FILE_TYPE_FLAG = 0170000;
153+
}

os/src/ZipOps.scala

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package os
22

3+
import org.apache.tools.{zip => ant}
4+
35
import java.net.URI
46
import java.nio.file.{FileSystem, FileSystems, Files}
57
import java.nio.file.attribute.{BasicFileAttributeView, FileTime, PosixFilePermissions}
@@ -115,7 +117,7 @@ object zip {
115117
compressionLevel: Int,
116118
out: java.io.OutputStream
117119
): Unit = {
118-
val zipOut = new ZipOutputStream(out)
120+
val zipOut = new ant.ZipOutputStream(out)
119121
zipOut.setLevel(compressionLevel)
120122

121123
try {
@@ -125,6 +127,7 @@ object zip {
125127
includePatterns,
126128
(path, sub) => makeZipEntry(path, sub, preserveMtimes, zipOut)
127129
)
130+
zipOut.finish()
128131
} finally {
129132
zipOut.close()
130133
}
@@ -143,35 +146,38 @@ object zip {
143146
!isExcluded && isIncluded
144147
}
145148

149+
private def toFileType(file: os.Path): PermissionUtils.FileType = {
150+
if (os.isLink(file)) PermissionUtils.FileType.SYMLINK
151+
else if (os.isFile(file)) PermissionUtils.FileType.REGULAR_FILE
152+
else if (os.isDir(file)) PermissionUtils.FileType.DIR
153+
else PermissionUtils.FileType.OTHER
154+
}
155+
146156
private def makeZipEntry(
147157
file: os.Path,
148158
sub: os.SubPath,
149159
preserveMtimes: Boolean,
150-
zipOut: ZipOutputStream
160+
zipOut: ant.ZipOutputStream
151161
) = {
162+
val zipEntry = new ant.ZipEntry(sub.toString)
152163

153-
val mtimeOpt = if (preserveMtimes) Some(os.mtime(file)) else None
154-
155-
val fis = if (os.isFile(file)) Some(os.read.inputStream(file)) else None
156-
try makeZipEntry0(sub, fis, mtimeOpt, zipOut)
157-
finally fis.foreach(_.close())
158-
}
164+
val mtime = if (preserveMtimes) os.mtime(file) else 0
165+
zipEntry.setTime(mtime)
159166

160-
private def makeZipEntry0(
161-
sub: os.SubPath,
162-
is: Option[java.io.InputStream],
163-
preserveMtimes: Option[Long],
164-
zipOut: ZipOutputStream
165-
) = {
166-
val zipEntry = new ZipEntry(sub.toString)
167+
val mode = PermissionUtils.modeFromPermissions(os.perms(file).toSet(), toFileType(file))
168+
zipEntry.setUnixMode(mode)
167169

168-
preserveMtimes match {
169-
case Some(mtime) => zipEntry.setTime(mtime)
170-
case None => zipEntry.setTime(0)
171-
}
170+
val fis =
171+
if (os.isLink(file))
172+
Some(new java.io.ByteArrayInputStream(os.readLink(file).toString().getBytes()))
173+
else if (os.isFile(file)) Some(os.read.inputStream(file))
174+
else None
172175

173-
zipOut.putNextEntry(zipEntry)
174-
is.foreach(os.Internals.transfer(_, zipOut, close = false))
176+
try {
177+
zipOut.putNextEntry(zipEntry)
178+
fis.foreach(os.Internals.transfer(_, zipOut, close = false))
179+
zipOut.closeEntry()
180+
} finally fis.foreach(_.close())
175181
}
176182

177183
/**
@@ -241,6 +247,10 @@ object unzip {
241247
} yield os.SubPath(zipEntry.getName)
242248
}
243249

250+
private lazy val S_IFMT: Int = java.lang.Integer.parseInt("0170000", 8)
251+
private def isSymLink(mode: Int): Boolean =
252+
(mode & S_IFMT) == ant.UnixStat.LINK_FLAG
253+
244254
/**
245255
* Extract the given zip file into the destination directory
246256
*
@@ -254,7 +264,39 @@ object unzip {
254264
excludePatterns: Seq[Regex] = List(),
255265
includePatterns: Seq[Regex] = List()
256266
): os.Path = {
257-
stream(os.read.stream(source), dest, excludePatterns, includePatterns)
267+
checker.value.onWrite(dest)
268+
269+
val zipFile = new ant.ZipFile(source.toIO)
270+
val zipEntryInputStreams = zipFile.getEntries.asScala
271+
.filter(ze => os.zip.shouldInclude(ze.getName, excludePatterns, includePatterns))
272+
.map(ze => (ze, zipFile.getInputStream(ze)))
273+
274+
try {
275+
for ((zipEntry, zipInputStream) <- zipEntryInputStreams) {
276+
val newFile = dest / os.SubPath(zipEntry.getName)
277+
val mode = zipEntry.getUnixMode
278+
val perms = if (mode > 0) {
279+
os.PermSet.fromSet(PermissionUtils.permissionsFromMode(mode))
280+
} else null
281+
282+
if (zipEntry.isDirectory) {
283+
os.makeDir.all(newFile, perms = perms)
284+
} else if (isSymLink(mode)) {
285+
val target = scala.io.Source.fromInputStream(zipInputStream).mkString
286+
val path = java.nio.file.Paths.get(target)
287+
val dest = if (path.isAbsolute) os.Path(path) else os.RelPath(path)
288+
os.symlink(newFile, dest)
289+
} else {
290+
val outputStream = os.write.outputStream(newFile, createFolders = true)
291+
os.Internals.transfer(zipInputStream, outputStream, close = false)
292+
outputStream.close()
293+
if (perms != null) os.perms.set(newFile, perms)
294+
}
295+
}
296+
} finally {
297+
zipFile.close()
298+
}
299+
258300
dest
259301
}
260302

0 commit comments

Comments
 (0)