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
2525import java .io .FileOutputStream ;
2626import java .io .IOException ;
2727import java .io .InputStream ;
28+ import java .nio .ByteBuffer ;
29+ import java .nio .ByteOrder ;
2830import 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 .*;
3632import java .security .AccessController ;
3733import java .security .PrivilegedAction ;
3834import java .security .PrivilegedActionException ;
3935import 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 .*;
4538import java .util .concurrent .atomic .AtomicInteger ;
4639import java .util .jar .JarEntry ;
4740import java .util .jar .JarFile ;
4841import java .util .spi .ToolProvider ;
4942import java .util .zip .ZipEntry ;
5043import java .util .zip .ZipFile ;
5144
52- import org .testng . annotations .Test ;
45+ import org .junit . jupiter . api .Test ;
5346
5447import static java .nio .file .attribute .PosixFilePermission .GROUP_EXECUTE ;
5548import static java .nio .file .attribute .PosixFilePermission .GROUP_READ ;
6053import static java .nio .file .attribute .PosixFilePermission .OWNER_EXECUTE ;
6154import static java .nio .file .attribute .PosixFilePermission .OWNER_READ ;
6255import 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 */
7871public 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