/* * Copyright (c) 2018, 2019 SAP SE. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact SAP SE, Dietmar-Hopp-Allee 16, 69190 Walldorf, Germany * or visit www.sap.com if you need additional information or have any * questions. */ import java.io.IOException; import java.nio.file.CopyOption; import java.nio.file.DirectoryStream; import java.nio.file.FileSystem; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.GroupPrincipal; import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFileAttributes; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.nio.file.attribute.UserPrincipal; import java.nio.file.spi.FileSystemProvider; import java.security.AccessController; import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import org.testng.annotations.Test; import static java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE; import static java.nio.file.attribute.PosixFilePermission.GROUP_READ; import static java.nio.file.attribute.PosixFilePermission.GROUP_WRITE; import static java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE; import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ; import static java.nio.file.attribute.PosixFilePermission.OTHERS_WRITE; import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; /** * @test * @bug 8213031 * @modules jdk.zipfs * @run testng TestPosix * @run testng/othervm/java.security.policy=test.policy TestPosix * @summary Test POSIX zip file operations. */ public class TestPosix { // files and directories private static final Path ZIP_FILE = Paths.get("testPosix.zip"); private static final Path ZIP_FILE_COPY = Paths.get("testPosixCopy.zip"); private static final Path UNZIP_DIR = Paths.get("unzip/"); // permission sets private static final Set ALLPERMS = PosixFilePermissions.fromString("rwxrwxrwx"); private static final Set EMPTYPERMS = Collections.emptySet(); private static final Set UR = Set.of(OWNER_READ); private static final Set UW = Set.of(OWNER_WRITE); private static final Set UE = Set.of(OWNER_EXECUTE); private static final Set GR = Set.of(GROUP_READ); private static final Set GW = Set.of(GROUP_WRITE); private static final Set GE = Set.of(GROUP_EXECUTE); private static final Set OR = Set.of(OTHERS_READ); private static final Set OW = Set.of(OTHERS_WRITE); private static final Set OE = Set.of(OTHERS_EXECUTE); // principals private static final UserPrincipal DUMMY_USER = ()->"defusr"; private static final GroupPrincipal DUMMY_GROUP = ()->"defgrp"; // FS open options private static final Map ENV_DEFAULT = Collections.emptyMap(); private static final Map ENV_POSIX = Map.of("enablePosixFileAttributes", true); // misc private static final FileSystemProvider zipFSP; private static final CopyOption[] COPY_ATTRIBUTES = {StandardCopyOption.COPY_ATTRIBUTES}; private static final Map ENTRIES = new HashMap<>(); private int entriesCreated; static enum checkExpects { contentOnly, noPermDataInZip, permsInZip, permsPosix } static class ZipFileEntryInfo { // permissions to set initially private final Set intialPerms; // permissions to set in a later call private final Set laterPerms; // permissions that should be effective in the zip file private final Set permsInZip; // permissions that should be returned by zipfs w/Posix support private final Set permsPosix; // entry is a directory private final boolean isDir; // need additional read flag in copy test private final boolean setReadFlag; private ZipFileEntryInfo(Set initialPerms, Set laterPerms, Set permsInZip, Set permsZipPosix, boolean isDir, boolean setReadFlag) { this.intialPerms = initialPerms; this.laterPerms = laterPerms; this.permsInZip = permsInZip; this.permsPosix = permsZipPosix; this.isDir = isDir; this.setReadFlag = setReadFlag; } } static class CopyVisitor extends SimpleFileVisitor { private Path from, to; private boolean copyPerms; CopyVisitor(Path from, Path to) { this.from = from; this.to = to; } CopyVisitor(Path from, Path to, boolean copyPerms) { this.from = from; this.to = to; this.copyPerms = copyPerms; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { FileVisitResult rc = super.preVisitDirectory(dir, attrs); Path target = to.resolve(from.relativize(dir).toString()); if (!Files.exists(target)) { Files.copy(dir, target, COPY_ATTRIBUTES); if (copyPerms) { Files.setPosixFilePermissions(target, Files.getPosixFilePermissions(dir)); } } return rc; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { FileVisitResult rc = super.visitFile(file, attrs); Path target = to.resolve(from.relativize(file).toString()); Files.copy(file, target, COPY_ATTRIBUTES); if (copyPerms) { Files.setPosixFilePermissions(target, Files.getPosixFilePermissions(file)); } return rc; } } static class DeleteVisitor extends SimpleFileVisitor { @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { FileVisitResult rc = super.postVisitDirectory(dir, exc); Files.delete(dir); return rc; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { FileVisitResult rc = super.visitFile(file, attrs); Files.delete(file); return rc; } } @FunctionalInterface static interface Executor { void doIt() throws IOException; } static { zipFSP = getZipFSProvider(); assertNotNull(zipFSP, "ZIP filesystem provider is not installed"); ENTRIES.put("dir", new ZipFileEntryInfo(ALLPERMS, null, ALLPERMS, ALLPERMS, true, false)); ENTRIES.put("uread", new ZipFileEntryInfo(UR, null, UR, UR, false, false)); ENTRIES.put("uwrite", new ZipFileEntryInfo(UW, null, UW, UW, false, true)); ENTRIES.put("uexec", new ZipFileEntryInfo(UE, null, UE, UE, false, true)); ENTRIES.put("gread", new ZipFileEntryInfo(GR, null, GR, GR, false, true)); ENTRIES.put("gwrite", new ZipFileEntryInfo(GW, null, GW, GW, false, true)); ENTRIES.put("gexec", new ZipFileEntryInfo(GE, null, GE, GE, false, true)); ENTRIES.put("oread", new ZipFileEntryInfo(OR, null, OR, OR, false, true)); ENTRIES.put("owrite", new ZipFileEntryInfo(OW, null, OW, OW, false, true)); ENTRIES.put("oexec", new ZipFileEntryInfo(OE, null, OE, OE, false, true)); ENTRIES.put("emptyperms", new ZipFileEntryInfo(EMPTYPERMS, null, EMPTYPERMS, EMPTYPERMS, false, true)); ENTRIES.put("noperms", new ZipFileEntryInfo(null, null, null, ALLPERMS, false, false)); ENTRIES.put("permslater", new ZipFileEntryInfo(null, UR, UR, UR, false, false)); } private static String expectedDefaultOwner(Path zf) { try { try { return AccessController.doPrivileged( (PrivilegedExceptionAction)()->Files.getOwner(zf).getName()); } catch (UnsupportedOperationException e) { // if we can't get the owner of the file, we fall back to system property user.name return AccessController.doPrivileged( (PrivilegedAction)()->System.getProperty("user.name")); } } catch (PrivilegedActionException | SecurityException e) { System.out.println("Caught an exception when running a privileged operation to get the default owner."); e.printStackTrace(); return null; } } private static FileSystemProvider getZipFSProvider() { for (FileSystemProvider provider : FileSystemProvider.installedProviders()) { if ("jar".equals(provider.getScheme())) return provider; } return null; } private void putEntry(FileSystem fs, String name, ZipFileEntryInfo entry) throws IOException { if (entry.isDir) { if (entry.intialPerms == null) { Files.createDirectory(fs.getPath(name)); } else { Files.createDirectory(fs.getPath(name), PosixFilePermissions.asFileAttribute(entry.intialPerms)); } } else { if (entry.intialPerms == null) { Files.createFile(fs.getPath(name)); } else { Files.createFile(fs.getPath(name), PosixFilePermissions.asFileAttribute(entry.intialPerms)); } } if (entry.laterPerms != null) { Files.setAttribute(fs.getPath(name), "zip:permissions", entry.laterPerms); } entriesCreated++; } private FileSystem createTestZipFile(Path zpath, Map env) throws IOException { if (Files.exists(zpath)) { System.out.println("Deleting old " + zpath + "..."); Files.delete(zpath); } System.out.println("Creating " + zpath + "..."); entriesCreated = 0; var opts = new HashMap(); opts.putAll(env); opts.put("create", true); FileSystem fs = zipFSP.newFileSystem(zpath, opts); for (String name : ENTRIES.keySet()) { putEntry(fs, name, ENTRIES.get(name)); } return fs; } private FileSystem createEmptyZipFile(Path zpath, Map env) throws IOException { if (Files.exists(zpath)) { System.out.println("Deleting old " + zpath + "..."); Files.delete(zpath); } System.out.println("Creating " + zpath + "..."); var opts = new HashMap(); opts.putAll(env); opts.put("create", true); return zipFSP.newFileSystem(zpath, opts); } private void delTree(Path p) throws IOException { if (Files.exists(p)) { Files.walkFileTree(p, new DeleteVisitor()); } } private void addOwnerRead(Path root) throws IOException { for (String name : ENTRIES.keySet()) { ZipFileEntryInfo ei = ENTRIES.get(name); if (!ei.setReadFlag) { continue; } Path setReadOn = root.resolve(name); Set perms = Files.getPosixFilePermissions(setReadOn); perms.add(OWNER_READ); Files.setPosixFilePermissions(setReadOn, perms); } } private void removeOwnerRead(Path root) throws IOException { for (String name : ENTRIES.keySet()) { ZipFileEntryInfo ei = ENTRIES.get(name); if (!ei.setReadFlag) { continue; } Path removeReadFrom = root.resolve(name); Set perms = Files.getPosixFilePermissions(removeReadFrom); perms.remove(OWNER_READ); Files.setPosixFilePermissions(removeReadFrom, perms); } } @SuppressWarnings("unchecked") private void checkEntry(Path file, checkExpects expected) { System.out.println("Checking " + file + "..."); String name = file.getFileName().toString(); ZipFileEntryInfo ei = ENTRIES.get(name); assertNotNull(ei, "Found unknown entry " + name + "."); BasicFileAttributes attrs = null; if (expected == checkExpects.permsPosix) { try { attrs = Files.readAttributes(file, PosixFileAttributes.class); } catch (IOException e) { e.printStackTrace(); fail("Caught IOException reading file attributes (posix) for " + name + ": " + e.getMessage()); } } else { try { attrs = Files.readAttributes(file, BasicFileAttributes.class); } catch (IOException e) { e.printStackTrace(); fail("Caught IOException reading file attributes (basic) " + name + ": " + e.getMessage()); } } assertEquals(Files.isDirectory(file), ei.isDir, "Unexpected directory attribute for:" + System.lineSeparator() + attrs); if (expected == checkExpects.contentOnly) { return; } Set permissions; if (expected == checkExpects.permsPosix) { try { permissions = Files.getPosixFilePermissions(file); } catch (IOException e) { e.printStackTrace(); fail("Caught IOException getting permission attribute for:" + System.lineSeparator() + attrs); return; } comparePermissions(ei.permsPosix, permissions); } else if (expected == checkExpects.permsInZip || expected == checkExpects.noPermDataInZip) { try { permissions = (Set)Files.getAttribute(file, "zip:permissions"); } catch (IOException e) { e.printStackTrace(); fail("Caught IOException getting permission attribute for:" + System.lineSeparator() + attrs); return; } comparePermissions(expected == checkExpects.noPermDataInZip ? null : ei.permsInZip, permissions); } } private void doCheckEntries(Path path, checkExpects expected) throws IOException { AtomicInteger entries = new AtomicInteger(); try (DirectoryStream paths = Files.newDirectoryStream(path)) { paths.forEach(file -> { entries.getAndIncrement(); checkEntry(file, expected); }); } System.out.println("Number of entries: " + entries.get() + "."); assertEquals(entries.get(), entriesCreated, "File contained wrong number of entries."); } private void checkEntries(FileSystem fs, checkExpects expected) throws IOException { System.out.println("Checking permissions on file system " + fs + "..."); doCheckEntries(fs.getPath("/"), expected); } private void checkEntries(Path path, checkExpects expected) throws IOException { System.out.println("Checking permissions on path " + path + "..."); doCheckEntries(path, expected); } private boolean throwsUOE(Executor e) throws IOException { try { e.doIt(); return false; } catch (UnsupportedOperationException exc) { return true; } } private void comparePermissions(Set expected, Set actual) { if (expected == null) { assertNull(actual, "Permissions are not null"); } else { assertNotNull(actual, "Permissions are null."); assertEquals(actual.size(), expected.size(), "Unexpected number of permissions (" + actual.size() + " received vs " + expected.size() + " expected)."); for (PosixFilePermission p : expected) { assertTrue(actual.contains(p), "Posix permission " + p + " missing."); } } } /** * This tests whether the entries in a zip file created w/o * Posix support are correct. * * @throws IOException */ @Test public void testDefault() throws IOException { // create zip file using zipfs with default options createTestZipFile(ZIP_FILE, ENV_DEFAULT).close(); // check entries on zipfs with default options try (FileSystem zip = zipFSP.newFileSystem(ZIP_FILE, ENV_DEFAULT)) { checkEntries(zip, checkExpects.permsInZip); } // check entries on zipfs with posix options try (FileSystem zip = zipFSP.newFileSystem(ZIP_FILE, ENV_POSIX)) { checkEntries(zip, checkExpects.permsPosix); } } /** * This tests whether the entries in a zip file created w/ * Posix support are correct. * * @throws IOException */ @Test public void testPosix() throws IOException { // create zip file using zipfs with posix option createTestZipFile(ZIP_FILE, ENV_POSIX).close(); // check entries on zipfs with default options try (FileSystem zip = zipFSP.newFileSystem(ZIP_FILE, ENV_DEFAULT)) { checkEntries(zip, checkExpects.permsInZip); } // check entries on zipfs with posix options try (FileSystem zip = zipFSP.newFileSystem(ZIP_FILE, ENV_POSIX)) { checkEntries(zip, checkExpects.permsPosix); } } /** * This tests whether the entries in a zip file copied from another * are correct. * * @throws IOException */ @Test public void testCopy() throws IOException { // copy zip to zip with default options try (FileSystem zipIn = createTestZipFile(ZIP_FILE, ENV_DEFAULT); FileSystem zipOut = createEmptyZipFile(ZIP_FILE_COPY, ENV_DEFAULT)) { Path from = zipIn.getPath("/"); Files.walkFileTree(from, new CopyVisitor(from, zipOut.getPath("/"))); } // check entries on copied zipfs with default options try (FileSystem zip = zipFSP.newFileSystem(ZIP_FILE_COPY, ENV_DEFAULT)) { checkEntries(zip, checkExpects.permsInZip); } // check entries on copied zipfs with posix options try (FileSystem zip = zipFSP.newFileSystem(ZIP_FILE_COPY, ENV_POSIX)) { checkEntries(zip, checkExpects.permsPosix); } } /** * This tests whether the entries of a zip file look correct after extraction * and re-packing. When not using zipfs with Posix support, we expect the * effective permissions in the resulting zip file to be empty. * * @throws IOException */ @Test public void testUnzipDefault() throws IOException { delTree(UNZIP_DIR); Files.createDirectory(UNZIP_DIR); try (FileSystem srcZip = createTestZipFile(ZIP_FILE, ENV_DEFAULT)) { Path from = srcZip.getPath("/"); Files.walkFileTree(from, new CopyVisitor(from, UNZIP_DIR)); } // we just check that the entries got extracted to file system checkEntries(UNZIP_DIR, checkExpects.contentOnly); // the target zip file is opened with Posix support // but we expect no permission data to be copied using the default copy method try (FileSystem tgtZip = createEmptyZipFile(ZIP_FILE_COPY, ENV_POSIX)) { Files.walkFileTree(UNZIP_DIR, new CopyVisitor(UNZIP_DIR, tgtZip.getPath("/"))); } // check entries on copied zipfs - no permission data should exist try (FileSystem zip = zipFSP.newFileSystem(ZIP_FILE_COPY, ENV_DEFAULT)) { checkEntries(zip, checkExpects.noPermDataInZip); } } /** * This tests whether the entries of a zip file look correct after extraction * and re-packing. If the default file system supports Posix, we test whether we * correctly carry the Posix permissions. Otherwise there's not much to test in * this method. * * @throws IOException */ @Test public void testUnzipPosix() throws IOException { delTree(UNZIP_DIR); Files.createDirectory(UNZIP_DIR); try { Files.getPosixFilePermissions(UNZIP_DIR); } catch (Exception e) { // if we run into any exception here, be it because of the fact that the file system // is not Posix or if we have insufficient security permissions, we can't do this test. System.out.println("This can't be tested here because of " + e); return; } try (FileSystem srcZip = createTestZipFile(ZIP_FILE, ENV_POSIX)) { Path from = srcZip.getPath("/"); // copy permissions as well Files.walkFileTree(from, new CopyVisitor(from, UNZIP_DIR, true)); } // permissions should have been propagated to file system checkEntries(UNZIP_DIR, checkExpects.permsPosix); try (FileSystem tgtZip = createEmptyZipFile(ZIP_FILE_COPY, ENV_POSIX)) { // Make some files owner readable to be able to copy them into the zipfs addOwnerRead(UNZIP_DIR); // copy permissions as well Files.walkFileTree(UNZIP_DIR, new CopyVisitor(UNZIP_DIR, tgtZip.getPath("/"), true)); // Fix back all the files in the target zip file which have been made readable before removeOwnerRead(tgtZip.getPath("/")); } // check entries on copied zipfs - permission data should have been propagated try (FileSystem zip = zipFSP.newFileSystem(ZIP_FILE_COPY, ENV_POSIX)) { checkEntries(zip, checkExpects.permsPosix); } } /** * Tests POSIX default behavior. * * @throws IOException */ @Test public void testPosixDefaults() throws IOException { // test with posix = false, expect UnsupportedOperationException try (FileSystem zipIn = createTestZipFile(ZIP_FILE, ENV_DEFAULT)) { var entry = zipIn.getPath("/dir"); assertTrue(throwsUOE(()->Files.getPosixFilePermissions(entry))); assertTrue(throwsUOE(()->Files.setPosixFilePermissions(entry, UW))); assertTrue(throwsUOE(()->Files.getOwner(entry))); assertTrue(throwsUOE(()->Files.setOwner(entry, DUMMY_USER))); assertTrue(throwsUOE(()->Files.getFileAttributeView(entry, PosixFileAttributeView.class))); } // test with posix = true -> default values try (FileSystem zipIn = zipFSP.newFileSystem(ZIP_FILE, ENV_POSIX)) { String defaultOwner = expectedDefaultOwner(ZIP_FILE); var entry = zipIn.getPath("/noperms"); comparePermissions(ALLPERMS, Files.getPosixFilePermissions(entry)); var owner = Files.getOwner(entry); assertNotNull(owner, "owner should not be null"); if (defaultOwner != null) { assertEquals(owner.getName(), defaultOwner); } Files.setOwner(entry, DUMMY_USER); assertEquals(Files.getOwner(entry), DUMMY_USER); var view = Files.getFileAttributeView(entry, PosixFileAttributeView.class); var group = view.readAttributes().group(); assertNotNull(group, "group must not be null"); if (defaultOwner != null) { assertEquals(group.getName(), defaultOwner); } view.setGroup(DUMMY_GROUP); assertEquals(view.readAttributes().group(), DUMMY_GROUP); entry = zipIn.getPath("/uexec"); Files.setPosixFilePermissions(entry, GR); // will be persisted comparePermissions(GR, Files.getPosixFilePermissions(entry)); } // test with posix = true + custom defaults of type String try (FileSystem zipIn = zipFSP.newFileSystem(ZIP_FILE, Map.of("enablePosixFileAttributes", true, "defaultOwner", "auser", "defaultGroup", "agroup", "defaultPermissions", "r--------"))) { var entry = zipIn.getPath("/noperms"); comparePermissions(UR, Files.getPosixFilePermissions(entry)); assertEquals(Files.getOwner(entry).getName(), "auser"); var view = Files.getFileAttributeView(entry, PosixFileAttributeView.class); assertEquals(view.readAttributes().group().getName(), "agroup"); // check if the change to permissions of /uexec was persisted comparePermissions(GR, Files.getPosixFilePermissions(zipIn.getPath("/uexec"))); } // test with posix = true + custom defaults as Objects try (FileSystem zipIn = zipFSP.newFileSystem(ZIP_FILE, Map.of("enablePosixFileAttributes", true, "defaultOwner", DUMMY_USER, "defaultGroup", DUMMY_GROUP, "defaultPermissions", UR))) { var entry = zipIn.getPath("/noperms"); comparePermissions(UR, Files.getPosixFilePermissions(entry)); assertEquals(Files.getOwner(entry), DUMMY_USER); var view = Files.getFileAttributeView(entry, PosixFileAttributeView.class); assertEquals(view.readAttributes().group(), DUMMY_GROUP); } } }