Skip to content

Commit e70cb4e

Browse files
author
Eirik Bjørsnøs
committed
8322565: (zipfs) Files.setPosixPermissions should preserve 'external file attributes' bits
Reviewed-by: clanger, lancea
1 parent d89602a commit e70cb4e

File tree

2 files changed

+136
-36
lines changed

2 files changed

+136
-36
lines changed

src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2009, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2009, 2024, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -644,7 +644,12 @@ void setPermissions(byte[] path, Set<PosixFilePermission> perms) throws IOExcept
644644
if (e.type == Entry.CEN) {
645645
e.type = Entry.COPY; // copy e
646646
}
647-
e.posixPerms = perms == null ? -1 : ZipUtils.permsToFlags(perms);
647+
if (perms == null) {
648+
e.posixPerms = -1;
649+
} else {
650+
e.posixPerms = ZipUtils.permsToFlags(perms) |
651+
(e.posixPerms & 0xFE00); // Preserve unrelated bits
652+
}
648653
update(e);
649654
} finally {
650655
endWrite();
@@ -3008,7 +3013,7 @@ private void readCEN(ZipFileSystem zipfs, IndexNode inode) throws IOException {
30083013
attrsEx = CENATX(cen, pos);
30093014
*/
30103015
if (CENVEM_FA(cen, pos) == FILE_ATTRIBUTES_UNIX) {
3011-
posixPerms = CENATX_PERMS(cen, pos) & 0xFFF; // 12 bits for setuid, setgid, sticky + perms
3016+
posixPerms = (CENATX_PERMS(cen, pos) & 0xFFFF); // 16 bits for file type, setuid, setgid, sticky + perms
30123017
}
30133018
locoff = CENOFF(cen, pos);
30143019
pos += CENHDR;

test/jdk/jdk/nio/zipfs/TestPosix.java

Lines changed: 128 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2019, 2022, SAP SE. All rights reserved.
2+
* Copyright (c) 2019, 2024, SAP SE. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -25,31 +25,24 @@
2525
import java.io.FileOutputStream;
2626
import java.io.IOException;
2727
import java.io.InputStream;
28+
import java.nio.ByteBuffer;
29+
import java.nio.ByteOrder;
2830
import java.nio.file.*;
29-
import java.nio.file.attribute.BasicFileAttributes;
30-
import java.nio.file.attribute.GroupPrincipal;
31-
import java.nio.file.attribute.PosixFileAttributeView;
32-
import java.nio.file.attribute.PosixFileAttributes;
33-
import java.nio.file.attribute.PosixFilePermission;
34-
import java.nio.file.attribute.PosixFilePermissions;
35-
import java.nio.file.attribute.UserPrincipal;
31+
import java.nio.file.attribute.*;
3632
import java.security.AccessController;
3733
import java.security.PrivilegedAction;
3834
import java.security.PrivilegedActionException;
3935
import java.security.PrivilegedExceptionAction;
40-
import java.util.Collections;
41-
import java.util.Enumeration;
42-
import java.util.HashMap;
43-
import java.util.Map;
44-
import java.util.Set;
36+
import java.time.Instant;
37+
import java.util.*;
4538
import java.util.concurrent.atomic.AtomicInteger;
4639
import java.util.jar.JarEntry;
4740
import java.util.jar.JarFile;
4841
import java.util.spi.ToolProvider;
4942
import java.util.zip.ZipEntry;
5043
import java.util.zip.ZipFile;
5144

52-
import org.testng.annotations.Test;
45+
import org.junit.jupiter.api.Test;
5346

5447
import static java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE;
5548
import static java.nio.file.attribute.PosixFilePermission.GROUP_READ;
@@ -60,20 +53,20 @@
6053
import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE;
6154
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
6255
import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
63-
import static org.testng.Assert.assertEquals;
64-
import static org.testng.Assert.assertNotNull;
65-
import static org.testng.Assert.assertNull;
66-
import static org.testng.Assert.assertTrue;
67-
import static org.testng.Assert.fail;
56+
import static org.junit.jupiter.api.Assertions.assertEquals;
57+
import static org.junit.jupiter.api.Assertions.assertNotNull;
58+
import static org.junit.jupiter.api.Assertions.assertNull;
59+
import static org.junit.jupiter.api.Assertions.assertTrue;
60+
import static org.junit.jupiter.api.Assertions.fail;
6861

6962
/**
7063
* @test
7164
* @bug 8213031 8273935
7265
* @summary Test POSIX ZIP file operations.
7366
* @modules jdk.zipfs
7467
* jdk.jartool
75-
* @run testng TestPosix
76-
* @run testng/othervm/java.security.policy=test.policy.posix TestPosix
68+
* @run junit TestPosix
69+
* @run junit/othervm/java.security.policy=test.policy.posix TestPosix
7770
*/
7871
public class TestPosix {
7972
private static final ToolProvider JAR_TOOL = ToolProvider.findFirst("jar")
@@ -364,7 +357,7 @@ private void checkEntry(Path file, checkExpects expected) {
364357
fail("Caught IOException reading file attributes (basic) " + name + ": " + e.getMessage());
365358
}
366359
}
367-
assertEquals(Files.isDirectory(file), ei.isDir, "Unexpected directory attribute for:" + System.lineSeparator() + attrs);
360+
assertEquals(ei.isDir, Files.isDirectory(file), "Unexpected directory attribute for:" + System.lineSeparator() + attrs);
368361

369362
if (expected == checkExpects.contentOnly) {
370363
return;
@@ -402,7 +395,7 @@ private void doCheckEntries(Path path, checkExpects expected) throws IOException
402395
});
403396
}
404397
System.out.println("Number of entries: " + entries.get() + ".");
405-
assertEquals(entries.get(), entriesCreated, "File contained wrong number of entries.");
398+
assertEquals(entriesCreated, entries.get(), "File contained wrong number of entries.");
406399
}
407400

408401
private void checkEntries(FileSystem fs, checkExpects expected) throws IOException {
@@ -429,7 +422,7 @@ private void comparePermissions(Set<PosixFilePermission> expected, Set<PosixFile
429422
assertNull(actual, "Permissions are not null");
430423
} else {
431424
assertNotNull(actual, "Permissions are null.");
432-
assertEquals(actual.size(), expected.size(), "Unexpected number of permissions (" +
425+
assertEquals(expected.size(), actual.size(), "Unexpected number of permissions (" +
433426
actual.size() + " received vs " + expected.size() + " expected).");
434427
for (PosixFilePermission p : expected) {
435428
assertTrue(actual.contains(p), "Posix permission " + p + " missing.");
@@ -609,18 +602,18 @@ public void testPosixDefaults() throws IOException {
609602
var owner = Files.getOwner(entry);
610603
assertNotNull(owner, "owner should not be null");
611604
if (defaultOwner != null) {
612-
assertEquals(owner.getName(), defaultOwner);
605+
assertEquals(defaultOwner, owner.getName());
613606
}
614607
Files.setOwner(entry, DUMMY_USER);
615-
assertEquals(Files.getOwner(entry), DUMMY_USER);
608+
assertEquals(DUMMY_USER, Files.getOwner(entry));
616609
var view = Files.getFileAttributeView(entry, PosixFileAttributeView.class);
617610
var group = view.readAttributes().group();
618611
assertNotNull(group, "group must not be null");
619612
if (defaultGroup != null) {
620-
assertEquals(group.getName(), defaultGroup);
613+
assertEquals(defaultGroup, group.getName());
621614
}
622615
view.setGroup(DUMMY_GROUP);
623-
assertEquals(view.readAttributes().group(), DUMMY_GROUP);
616+
assertEquals(DUMMY_GROUP, view.readAttributes().group());
624617
entry = zipIn.getPath("/uexec");
625618
Files.setPosixFilePermissions(entry, GR); // will be persisted
626619
comparePermissions(GR, Files.getPosixFilePermissions(entry));
@@ -632,9 +625,9 @@ public void testPosixDefaults() throws IOException {
632625
{
633626
var entry = zipIn.getPath("/noperms");
634627
comparePermissions(UR, Files.getPosixFilePermissions(entry));
635-
assertEquals(Files.getOwner(entry).getName(), "auser");
628+
assertEquals("auser", Files.getOwner(entry).getName());
636629
var view = Files.getFileAttributeView(entry, PosixFileAttributeView.class);
637-
assertEquals(view.readAttributes().group().getName(), "agroup");
630+
assertEquals("agroup", view.readAttributes().group().getName());
638631
// check if the change to permissions of /uexec was persisted
639632
comparePermissions(GR, Files.getPosixFilePermissions(zipIn.getPath("/uexec")));
640633
}
@@ -645,9 +638,9 @@ public void testPosixDefaults() throws IOException {
645638
{
646639
var entry = zipIn.getPath("/noperms");
647640
comparePermissions(UR, Files.getPosixFilePermissions(entry));
648-
assertEquals(Files.getOwner(entry), DUMMY_USER);
641+
assertEquals(DUMMY_USER, Files.getOwner(entry));
649642
var view = Files.getFileAttributeView(entry, PosixFileAttributeView.class);
650-
assertEquals(view.readAttributes().group(), DUMMY_GROUP);
643+
assertEquals(DUMMY_GROUP, view.readAttributes().group());
651644
}
652645
}
653646

@@ -722,6 +715,108 @@ public void testJarFile() throws IOException {
722715

723716
// the run method catches IOExceptions, we need to expose them
724717
int rc = JAR_TOOL.run(System.out, System.err, "xvf", JAR_FILE.toString());
725-
assertEquals(rc, 0, "Return code of jar call is " + rc + " but expected 0");
718+
assertEquals(0, rc, "Return code of jar call is " + rc + " but expected 0");
719+
}
720+
721+
/**
722+
* Verify that calling Files.setPosixPermissions with the current
723+
* permission set does not change the 'external file attributes' field.
724+
*
725+
* @throws IOException if an unexpected IOException occurs
726+
*/
727+
@Test
728+
public void setPermissionsShouldPreserveRemainingBits() throws IOException {
729+
assertExternalFileAttributeUnchanged(fs -> {
730+
Path path = fs.getPath("hello.txt");
731+
// Set permissions to their current value
732+
Files.setPosixFilePermissions(path, Files.getPosixFilePermissions(path));
733+
});
734+
}
735+
736+
/**
737+
* Verify that a non-POSIX operation such as Files.setLastModifiedTime
738+
* does not change the 'external file attributes' field.
739+
*
740+
* @throws IOException if an unexpected IOException occurs
741+
*/
742+
@Test
743+
public void setLastModifiedTimeShouldNotChangeExternalFileAttribute() throws IOException {
744+
assertExternalFileAttributeUnchanged(fs -> {
745+
Path path = fs.getPath("hello.txt");
746+
Files.setLastModifiedTime(path, FileTime.from(Instant.now()));
747+
});
748+
}
749+
750+
// Represents an operation performed on a FileSystem
751+
static interface FileSystemOperation {
752+
void accept(FileSystem fileSystem) throws IOException;
753+
}
754+
755+
/**
756+
* Assert that running the given operation on a ZipFileSystem does not
757+
* change the 'external file attributes' value of the 'hello.txt' entry
758+
* @param action the action to run on the file system
759+
*
760+
* @throws IOException if an unexpected IOException occurs
761+
*/
762+
private void assertExternalFileAttributeUnchanged(FileSystemOperation action) throws IOException {
763+
/*
764+
* The ZIP test vector used here is created using:
765+
* % touch hello.txt
766+
* % chmod u+s hello.txt # setuid
767+
* % chmod g+s hello.txt # setgid
768+
* % chmod +t hello.txt # sticky
769+
* % zip hello.zip hello.txt
770+
* % cat hello.zip | xxd -ps
771+
*/
772+
byte[] zip = HexFormat.of().parseHex("""
773+
504b03040a0000000000d994945700000000000000000000000009001c00
774+
68656c6c6f2e7478745554090003aa268365aa26836575780b000104f501
775+
00000414000000504b01021e030a0000000000d994945700000000000000
776+
0000000000090018000000000000000000a48f0000000068656c6c6f2e74
777+
78745554050003aa26836575780b000104f50100000414000000504b0506
778+
00000000010001004f000000430000000000
779+
""".replaceAll("\n",""));
780+
781+
// Expected bit values of the 'external file attributes' CEN field in the ZIP above
782+
String expectedBits = "1000111110100100";
783+
// ^^^^ file type: 1000 (regular file)
784+
// ^ setuid: ON
785+
// ^ setgid: ON
786+
// ^ sticky: ON
787+
// ^^^^^^^^^ rwxr--r-- (9 bits)
788+
789+
// Sanity check that 'external file attributes' has the expected value
790+
verifyExternalFileAttribute(zip, expectedBits);
791+
792+
793+
Path zipFile = Path.of("preserve-external-file-attrs.zip");
794+
Files.write(zipFile, zip);
795+
796+
// Run the provided action on the ZipFileSystem
797+
try (FileSystem fs = FileSystems.newFileSystem(zipFile, ENV_POSIX)) {
798+
action.accept(fs);
799+
}
800+
// Running the action should not change the 'external file attributes' value
801+
verifyExternalFileAttribute(Files.readAllBytes(zipFile), expectedBits);
802+
}
803+
804+
/**
805+
* Verify that the first 16 bits of the CEN field 'external file attributes' matches
806+
* a given bit string
807+
* @param zip the ZIP file to parse
808+
* @param expectedBits a string of '0' or '1' representing the expected bits
809+
*/
810+
private void verifyExternalFileAttribute(byte[] zip, String expectedBits) {
811+
// Buffer to help parse the ZIP
812+
ByteBuffer buffer = ByteBuffer.wrap(zip).order(ByteOrder.LITTLE_ENDIAN);
813+
// Look up offset of first CEN header from the END header
814+
int cenOff = buffer.getInt(buffer.capacity() - ZipFile.ENDHDR + ZipFile.ENDOFF);
815+
// We're interested in the first 16 'unix' bits of the 32-bit 'external file attributes' field
816+
int externalFileAttr = (buffer.getInt(cenOff + ZipFile.CENATX) >> 16) & 0xFFFF;
817+
818+
// Verify that the expected bits are set
819+
assertEquals(expectedBits, Integer.toBinaryString(externalFileAttr),
820+
"The 'external file attributes' field does not match the expected value:");
726821
}
727822
}

0 commit comments

Comments
 (0)