Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
java-version: [11, 17]
java-version: [17]

runs-on: ${{ matrix.os }}

Expand All @@ -31,7 +31,7 @@ jobs:
run: curl -Lo mill.bat "https://raw.githubusercontent.com/lefou/millw/main/millw.bat"
if: matrix.os == 'windows-latest'

- run: ./mill -i -k __.test
- run: sudo ./mill -i -k __.test
if: matrix.os != 'windows-latest'
- run: ./mill.bat -i -k __.jvm.__.test
if: matrix.os == 'windows-latest'
Expand All @@ -46,7 +46,7 @@ jobs:
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 11
java-version: 17

- run: ./mill -i -k __.mimaReportBinaryIssues

Expand Down
57 changes: 57 additions & 0 deletions os/src/ZipOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,40 @@ object zip {
f
)
finally f.close()
// preserve permissions
if (!scala.util.Properties.isWin) {
val zipFS = FileSystems.newFileSystem(
new URI("jar", dest.wrapped.toUri.toString, null),
Map(
"create" -> "true",
"enablePosixFileAttributes" -> "true",
"defaultPermissions" -> Set.empty[String].asJava
).asJava
)
try {
sources.foreach { source =>
if (os.isDir(source.src)) {
for (path <- os.walk(source.src)) {
if (
os.isFile(path) && shouldInclude(path.toString, excludePatterns, includePatterns)
) {
val sourcePerms = os.perms(path)
val entry = zipFS.getPath(
(source.dest.getOrElse(os.sub) / path.subRelativeTo(source.src)).toString
)
Files.setPosixFilePermissions(entry, sourcePerms.toSet())
}
}
} else if (shouldInclude(source.src.last, excludePatterns, includePatterns)) {
val sourcePerms = os.perms(source.src)
val entry = zipFS.getPath(source.dest.getOrElse(os.sub / source.src.last).toString)
Files.setPosixFilePermissions(entry, sourcePerms.toSet())
}
}
} finally {
zipFS.close()
}
}
}
dest
}
Expand Down Expand Up @@ -255,6 +289,29 @@ object unzip {
includePatterns: Seq[Regex] = List()
): os.Path = {
stream(os.read.stream(source), dest, excludePatterns, includePatterns)
// preserve permissions
if (!scala.util.Properties.isWin) {
val zipFS = FileSystems.newFileSystem(
new URI("jar", source.wrapped.toUri.toString, null),
Map(
"create" -> "true",
"enablePosixFileAttributes" -> "true",
"defaultPermissions" -> Set.empty[String].asJava
).asJava
)
try {
os.walk(dest).foreach { path =>
val relPath = path.subRelativeTo(dest).toString
if (os.zip.shouldInclude(relPath, excludePatterns, includePatterns)) {
val entry = zipFS.getPath(relPath)
val permissions = Files.getPosixFilePermissions(entry)
Copy link
Contributor

@kiendang kiendang Mar 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should use the more low-level Files.getAttribute(entry, "zip:permissions") here.
In case the zip entry does not have permission information, Files.getAttribute(entry, "zip:permissions") would be empty and we could skip os.perms.set.

If using Files.getPosixFilePermissions, according to the doc,

The "permissions" attribute is not optional in the "posix" view so a default set of permissions are used for entries that do not have access permissions stored in the Zip file. The default set of permissions are
OWNER_READ
OWNER_WRITE
GROUP_READ

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the unzipped file would get some default permissions even if we dont set them, right?
With this approach we at least can set it with the "defaultPermissions" property.
Currently I set it to empty

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When there's no permission information stored with the zip entry, we want it to have the default permissions set by the user's os/file system/umask when unzipped right, not the zipfs "defaultPermissions"? Also the zipfs "defaultPermissions" is never empty. When it's null/unset it's set to [OWNER_READ, OWNER_WRITE, GROUP_READ]. So we should check with Files.getAttribute(entry, "zip:permissions") instead, and skip the os.perms.set part if it's empty.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you are right, I'm not super familiar with *nix permissions.
Fixed now, looks good?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I didn't have a correct understanding of umask either. There're no default permissions in Unix. Applications set permissions to files they create. Umask will then mask some of the permissions. What you did before was totally fine. https://askubuntu.com/questions/44542/what-is-umask-and-how-does-it-work

os.perms.set(path, permissions)
}
}
} finally {
zipFS.close()
}
}
dest
}

Expand Down
23 changes: 23 additions & 0 deletions os/test/src-jvm/ZipOpJvmTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,29 @@ object ZipOpJvmTests extends TestSuite {
test("append") - prep { wd => zipAndUnzipDontPreserveMtimes(wd, true) }
}

test("zipAndUnzipPreservePermissions") - prep { wd =>
if (Unix()) {
// Create a file and set its permissions
val testFile = wd / "FileWithPerms"
os.write(testFile, "Test content")
os.perms.set(testFile, "rwxr-xr-x")
// Zip the file with permissions
val zipFile = os.zip(
dest = wd / "zipWithPermsPreservation.zip",
sources = List(testFile)
)
// Unzip it
val unzippedFolder = wd / "unzippedWithPerms"
os.unzip(
source = zipFile,
dest = unzippedFolder
)
// Compare the original and actual permissions after unzip
val unzippedFilePerms = os.perms(unzippedFolder / "FileWithPerms")
assert(unzippedFilePerms.toString() == "rwxr-xr-x")
}
}

test("deletePatterns") - prep { wd =>
val amxFile = "File.amx"
os.copy(wd / "File.txt", wd / amxFile)
Expand Down