Skip to content

Commit 641fbe6

Browse files
authored
Unzipping contents before the enclosing directories (#388)
to prevent crash in cases where directories is missing READ/EXECUTE permission Added a test that - creates a zip file with directories without READ/EXECUTE permissions - `os.unzip` everything successfully - re-adds READ and EXECUTE permissions to directories see my and @lefou's comments #387 (comment) com-lihaoyi/mill#5048 (comment) Tested with the android test. Here is the permission fix at the call site (`AndroidApModule.scala`) com-lihaoyi/mill@main...kiendang:mill:fix-android-classesjar-unzip#diff-49ebd9c68d1348785194e15a80d62f0845c26386e722d3f5d43429238bbf7616 @lihaoyi @vaslabs
1 parent e051c2c commit 641fbe6

File tree

2 files changed

+84
-23
lines changed

2 files changed

+84
-23
lines changed

os/src/ZipOps.scala

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -345,12 +345,11 @@ object unzip {
345345
)
346346
})
347347
.toList
348-
.sortBy { case (_, path, _, isSymLink, _) =>
349-
// Unzipping symbolic links last.
350-
// Enclosing directories are unzipped before their contents.
351-
// This makes sure directory permissions are applied correctly.
352-
(isSymLink, path)
353-
}
348+
.sortBy { case (_, path, _, _, _) =>
349+
// Unzipping children first before the enclosing directories
350+
// This prevents crash for the case where directories lack READ/EXECUTE permission
351+
path
352+
}(Ordering[os.SubPath].reverse)
354353

355354
try {
356355
for ((zipEntry, path, mode, isSymLink, zipInputStream) <- zipEntryInputStreams) {
@@ -360,10 +359,10 @@ object unzip {
360359
} else null
361360

362361
if (zipEntry.isDirectory) {
363-
os.makeDir.all(newFile)
364-
if (perms != null) {
365-
// make sure directories at least have OWNER_EXECUTE
366-
os.perms.set(newFile, perms + PosixFilePermission.OWNER_EXECUTE)
362+
os.makeDir.all(newFile, perms = perms)
363+
if (perms != null && os.perms(newFile) != perms) {
364+
// because of umask
365+
os.perms.set(newFile, perms)
367366
}
368367
} else if (isSymLink) {
369368
val target = scala.io.Source.fromInputStream(zipInputStream).mkString

os/test/src/ZipOpTests.scala

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -538,24 +538,86 @@ object ZipOpTests extends TestSuite {
538538
assert(file2Content == "Content of file2")
539539
}
540540

541-
test("unzipDirectoryEnsureExecutablePermission") - prep { wd =>
541+
test("unzipDirectoriesWithoutReadOrExecute") - prep { wd =>
542542
if (!scala.util.Properties.isWin) {
543-
val zipFileName = "zipDirExecutable"
544-
val source = wd / "folder1"
545-
val dir = source / "dir"
543+
def zip_(sources: Seq[(os.Path, os.PermSet)], root: os.Path, dest: os.Path): os.Path = {
544+
import os.{shaded_org_apache_tools_zip => apache}
546545

547-
os.makeDir(dir)
548-
val perms = os.perms(dir)
549-
os.perms.set(dir, perms - PosixFilePermission.OWNER_EXECUTE)
546+
val zipOut = new apache.ZipOutputStream(
547+
java.nio.file.Files.newOutputStream(dest.toNIO)
548+
)
550549

551-
val zipped = os.zip(
552-
dest = wd / s"$zipFileName.zip",
553-
sources = Seq(source)
550+
try {
551+
sources.foreach { case (p, perms) =>
552+
val name = p.subRelativeTo(root).toString + (if (os.isDir(p)) "/" else "")
553+
554+
val fileType = apache.PermissionUtils.FileType.of(p.toNIO)
555+
val mode = apache.PermissionUtils.modeFromPermissions(perms.toSet(), fileType)
556+
val fis = if (os.isDir(p))
557+
None
558+
else Some(os.read.inputStream(p))
559+
560+
val zipEntry = new apache.ZipEntry(name)
561+
zipEntry.setUnixMode(mode)
562+
563+
try {
564+
zipOut.putNextEntry(zipEntry)
565+
fis.foreach(os.Internals.transfer(_, zipOut, close = false))
566+
zipOut.closeEntry()
567+
} finally {
568+
fis.foreach(_.close())
569+
}
570+
}
571+
zipOut.finish()
572+
} finally {
573+
zipOut.close()
574+
}
575+
576+
dest
577+
}
578+
579+
def walk_(p: os.Path): geny.Generator[os.Path] = {
580+
if (os.isDir(p))
581+
os.list.stream(p) ++ os.list.stream(p).flatMap(walk_)
582+
else geny.Generator()
583+
}
584+
585+
import java.nio.file.attribute.PosixFilePermission._
586+
587+
val zipFileName = "zipDirNoReadExecute"
588+
val source = wd / "dirNoReadExecute"
589+
val dir = source / "dir"
590+
val nested = dir / "nested"
591+
val file = nested / "file.txt"
592+
593+
os.makeDir.all(nested)
594+
os.write(file, "Contents of file.txt")
595+
596+
val readAndExecute = os.PermSet.fromSet(java.util.Set.of(
597+
OWNER_READ,
598+
OWNER_EXECUTE,
599+
GROUP_READ,
600+
GROUP_EXECUTE,
601+
OTHERS_READ,
602+
OTHERS_EXECUTE
603+
))
604+
605+
val filesToZip: Seq[(os.Path, os.PermSet)] =
606+
Seq(dir, nested, file)
607+
.map(p => (p, if (os.isDir(p)) os.perms(p) -- readAndExecute else os.perms(p)))
608+
609+
val zipped = zip_(filesToZip, source, wd / s"$zipFileName.zip")
610+
val unzipped = os.unzip(
611+
zipped,
612+
dest = wd / zipFileName
554613
)
555614

556-
val unzipped = os.unzip(zipped, dest = wd / zipFileName)
557-
assert(os.perms(unzipped / "dir").contains(PosixFilePermission.OWNER_EXECUTE))
558-
assert(os.perms(unzipped / "dir") == perms)
615+
walk_(unzipped).foreach { p =>
616+
os.perms.set(p, os.perms(p) + OWNER_READ + OWNER_EXECUTE)
617+
}
618+
619+
assert(os.walk(unzipped).map(_.subRelativeTo(unzipped)) ==
620+
os.walk(source).map(_.subRelativeTo(source)))
559621
}
560622
}
561623
}

0 commit comments

Comments
 (0)