--- old/src/java.base/share/lib/security/default.policy 2019-05-21 17:12:45.829018500 +0200 +++ new/src/java.base/share/lib/security/default.policy 2019-05-21 17:12:44.266125700 +0200 @@ -201,8 +201,10 @@ grant codeBase "jrt:/jdk.zipfs" { permission java.io.FilePermission "<>", "read,write,delete"; permission java.lang.RuntimePermission "fileSystemProvider"; + permission java.lang.RuntimePermission "accessUserInformation"; permission java.util.PropertyPermission "os.name", "read"; permission java.util.PropertyPermission "user.dir", "read"; + permission java.util.PropertyPermission "user.name", "read"; }; // permissions needed by applications using java.desktop module --- old/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipConstants.java 2019-05-21 17:12:56.102531200 +0200 +++ new/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipConstants.java 2019-05-21 17:12:54.572862700 +0200 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2019, Oracle and/or its affiliates. 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 @@ -230,6 +230,7 @@ // central directory header (CEN) fields static final long CENSIG(byte[] b, int pos) { return LG(b, pos + 0); } static final int CENVEM(byte[] b, int pos) { return SH(b, pos + 4); } + static final int CENVEM_FA(byte[] b, int pos) { return CH(b, pos + 5); } static final int CENVER(byte[] b, int pos) { return SH(b, pos + 6); } static final int CENFLG(byte[] b, int pos) { return SH(b, pos + 8); } static final int CENHOW(byte[] b, int pos) { return SH(b, pos + 10);} @@ -243,6 +244,7 @@ static final int CENDSK(byte[] b, int pos) { return SH(b, pos + 34);} static final int CENATT(byte[] b, int pos) { return SH(b, pos + 36);} static final long CENATX(byte[] b, int pos) { return LG(b, pos + 38);} + static final int CENATX_PERMS(byte[] b, int pos) { return SH(b, pos + 40);} static final long CENOFF(byte[] b, int pos) { return LG(b, pos + 42);} /* The END header is followed by a variable length comment of size < 64k. */ --- old/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileAttributeView.java 2019-05-21 17:13:06.114533300 +0200 +++ new/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileAttributeView.java 2019-05-21 17:13:05.000348800 +0200 @@ -27,15 +27,18 @@ import java.io.IOException; import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.PosixFilePermission; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; /** * @author Xueming Shen, Rajendra Gutupalli, Jaya Hangal */ class ZipFileAttributeView implements BasicFileAttributeView { - private enum AttrID { + static enum AttrID { size, creationTime, lastAccessTime, @@ -47,10 +50,13 @@ fileKey, compressedSize, crc, - method + method, + owner, + group, + permissions } - private final ZipPath path; + final ZipPath path; private final boolean isZipView; ZipFileAttributeView(ZipPath path, boolean isZipView) { @@ -64,7 +70,7 @@ } @Override - public ZipFileAttributes readAttributes() throws IOException { + public BasicFileAttributes readAttributes() throws IOException { return path.readAttributes(); } @@ -77,6 +83,11 @@ path.setTimes(lastModifiedTime, lastAccessTime, createTime); } + public void setPermissions(Set perms) throws IOException { + path.setPermissions(perms); + } + + @SuppressWarnings("unchecked") void setAttribute(String attribute, Object value) throws IOException { @@ -87,6 +98,8 @@ setTimes(null, (FileTime)value, null); if (AttrID.valueOf(attribute) == AttrID.creationTime) setTimes(null, null, (FileTime)value); + if (AttrID.valueOf(attribute) == AttrID.permissions) + setPermissions((Set)value); } catch (IllegalArgumentException x) { throw new UnsupportedOperationException("'" + attribute + "' is unknown or read-only attribute"); @@ -96,7 +109,7 @@ Map readAttributes(String attributes) throws IOException { - ZipFileAttributes zfas = readAttributes(); + ZipFileAttributes zfas = (ZipFileAttributes)readAttributes(); LinkedHashMap map = new LinkedHashMap<>(); if ("*".equals(attributes)) { for (AttrID id : AttrID.values()) { @@ -115,7 +128,7 @@ return map; } - private Object attribute(AttrID id, ZipFileAttributes zfas) { + Object attribute(AttrID id, ZipFileAttributes zfas) { switch (id) { case size: return zfas.size(); @@ -147,6 +160,11 @@ if (isZipView) return zfas.method(); break; + case permissions: + if (isZipView) { + return zfas.storedPermissions().orElse(null); + } + break; default: break; } --- old/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileAttributes.java 2019-05-21 17:13:14.361204000 +0200 +++ new/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileAttributes.java 2019-05-21 17:13:13.052235600 +0200 @@ -26,6 +26,9 @@ package jdk.nio.zipfs; import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Optional; +import java.util.Set; /** * The attributes of a file stored in a zip file. @@ -38,4 +41,5 @@ int method(); byte[] extra(); byte[] comment(); + Optional> storedPermissions(); } --- old/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileStore.java 2019-05-21 17:13:22.682122300 +0200 +++ new/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileStore.java 2019-05-21 17:13:21.340723000 +0200 @@ -32,7 +32,9 @@ import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.FileAttributeView; +import java.nio.file.attribute.FileOwnerAttributeView; import java.nio.file.attribute.FileStoreAttributeView; +import java.nio.file.attribute.PosixFileAttributeView; /** * @author Xueming Shen, Rajendra Gutupalli, Jaya Hangal @@ -63,12 +65,15 @@ @Override public boolean supportsFileAttributeView(Class type) { return (type == BasicFileAttributeView.class || - type == ZipFileAttributeView.class); + type == ZipFileAttributeView.class || + ((type == FileOwnerAttributeView.class || + type == PosixFileAttributeView.class) && zfs.supportPosix)); } @Override public boolean supportsFileAttributeView(String name) { - return "basic".equals(name) || "zip".equals(name); + return "basic".equals(name) || "zip".equals(name) || + (("owner".equals(name) || "posix".equals(name)) && zfs.supportPosix); } @Override --- old/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java 2019-05-21 17:13:30.812734100 +0200 +++ new/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java 2019-05-21 17:13:29.617804600 +0200 @@ -41,9 +41,7 @@ import java.nio.channels.SeekableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.*; -import java.nio.file.attribute.FileAttribute; -import java.nio.file.attribute.FileTime; -import java.nio.file.attribute.UserPrincipalLookupService; +import java.nio.file.attribute.*; import java.nio.file.spi.FileSystemProvider; import java.security.AccessController; import java.security.PrivilegedAction; @@ -82,9 +80,14 @@ private static final boolean isWindows = AccessController.doPrivileged( (PrivilegedAction)()->System.getProperty("os.name") .startsWith("Windows")); - private static final Set supportedFileAttributeViews = - Set.of("basic", "zip"); private static final byte[] ROOTPATH = new byte[] { '/' }; + private static final String OPT_POSIX = "enablePosixFileAttributes"; + private static final String OPT_DEFAULT_OWNER = "defaultOwner"; + private static final String OPT_DEFAULT_GROUP = "defaultGroup"; + private static final String OPT_DEFAULT_PERMISSIONS = "defaultPermissions"; + + private static final Set DEFAULT_PERMISSIONS = + PosixFilePermissions.fromString("rwxrwxrwx"); private final ZipFileSystemProvider provider; private final Path zfpath; @@ -103,6 +106,14 @@ private final int defaultCompressionMethod; // METHOD_STORED if "noCompression=true" // METHOD_DEFLATED otherwise + // POSIX support + final boolean supportPosix; + private final UserPrincipal defaultOwner; + private final GroupPrincipal defaultGroup; + private final Set defaultPermissions; + + private final Set supportedFileAttributeViews; + ZipFileSystem(ZipFileSystemProvider provider, Path zfpath, Map env) throws IOException @@ -114,6 +125,12 @@ this.useTempFile = isTrue(env, "useTempFile"); this.forceEnd64 = isTrue(env, "forceZIP64End"); this.defaultCompressionMethod = isTrue(env, "noCompression") ? METHOD_STORED : METHOD_DEFLATED; + this.supportPosix = isTrue(env, OPT_POSIX); + this.defaultOwner = initOwner(zfpath, env); + this.defaultGroup = initGroup(zfpath, env); + this.defaultPermissions = initPermissions(env); + this.supportedFileAttributeViews = supportPosix ? + Set.of("basic", "posix", "zip") : Set.of("basic", "zip"); if (Files.notExists(zfpath)) { // create a new zip if it doesn't exist if (isTrue(env, "create")) { @@ -151,6 +168,107 @@ return "true".equals(env.get(name)) || TRUE.equals(env.get(name)); } + // Initialize the default owner for files inside the zip archive. + // If not specified in env, it is the owner of the archive. If no owner can + // be determined, we try to go with system property "user.name". If that's not + // accessible, we return "". + private UserPrincipal initOwner(Path zfpath, Map env) throws IOException { + Object o = env.get(OPT_DEFAULT_OWNER); + if (o == null) { + try { + PrivilegedExceptionAction pa = ()->Files.getOwner(zfpath); + return AccessController.doPrivileged(pa); + } catch (PrivilegedActionException e) { + if (e.getCause() instanceof UnsupportedOperationException || + e.getCause() instanceof NoSuchFileException) + { + PrivilegedAction pa = ()->System.getProperty("user.name"); + String userName = AccessController.doPrivileged(pa); + return ()->userName; + } else { + throw new IOException(e); + } + } + } + if (o instanceof String) { + if (((String)o).isEmpty()) { + throw new IllegalArgumentException("Value for property " + + OPT_DEFAULT_OWNER + " must not be empty."); + } + return ()->(String)o; + } + if (o instanceof UserPrincipal) { + return (UserPrincipal)o; + } + throw new IllegalArgumentException("Value for property " + + OPT_DEFAULT_OWNER + " must be of type " + String.class + + " or " + UserPrincipal.class); + } + + // Initialize the default group for files inside the zip archive. + // If not specified in env, we try to determine the group of the zip archive itself. + // If this is not possible/unsupported, we will return a group principal going by + // the same name as the default owner. + private GroupPrincipal initGroup(Path zfpath, Map env) throws IOException { + Object o = env.get(OPT_DEFAULT_GROUP); + if (o == null) { + PosixFileAttributeView zfpv = Files.getFileAttributeView(zfpath, PosixFileAttributeView.class); + if (zfpv == null) { + return defaultOwner::getName; + } + PrivilegedExceptionAction pa = ()->zfpv.readAttributes().group(); + try { + return AccessController.doPrivileged(pa); + } catch (PrivilegedActionException e) { + if (e.getCause() instanceof NoSuchFileException) { + return defaultOwner::getName; + } else { + throw new IOException(e); + } + } + } + if (o instanceof String) { + if (((String)o).isEmpty()) { + throw new IllegalArgumentException("Value for property " + + OPT_DEFAULT_GROUP + " must not be empty."); + } + return ()->(String)o; + } + if (o instanceof GroupPrincipal) { + return (GroupPrincipal)o; + } + throw new IllegalArgumentException("Value for property " + + OPT_DEFAULT_GROUP + " must be of type " + String.class + + " or " + GroupPrincipal.class); + } + + // Initialize the default permissions for files inside the zip archive. + // If not specified in env, it will return 777. + private Set initPermissions(Map env) { + Object o = env.get(OPT_DEFAULT_PERMISSIONS); + if (o == null) { + return DEFAULT_PERMISSIONS; + } + if (o instanceof String) { + return PosixFilePermissions.fromString((String)o); + } + if (!(o instanceof Set)) { + throw new IllegalArgumentException("Value for property " + + OPT_DEFAULT_PERMISSIONS + " must be of type " + String.class + + " or " + Set.class); + } + Set perms = new HashSet<>(); + for (Object o2 : (Set)o) { + if (o2 instanceof PosixFilePermission) { + perms.add((PosixFilePermission)o2); + } else { + throw new IllegalArgumentException(OPT_DEFAULT_PERMISSIONS + + " must only contain objects of type " + PosixFilePermission.class); + } + } + return perms; + } + @Override public FileSystemProvider provider() { return provider; @@ -338,11 +456,13 @@ return (Entry)inode; } else if (inode.pos == -1) { // pseudo directory, uses METHOD_STORED - Entry e = new Entry(inode.name, inode.isdir, METHOD_STORED); + Entry e = supportPosix ? + new PosixEntry(inode.name, inode.isdir, METHOD_STORED) : + new Entry(inode.name, inode.isdir, METHOD_STORED); e.mtime = e.atime = e.ctime = zfsDefaultTimeStamp; return e; } else { - return new Entry(this, inode); + return supportPosix ? new PosixEntry(this, inode) : new Entry(this, inode); } } finally { endRead(); @@ -387,6 +507,65 @@ } } + void setOwner(byte[] path, UserPrincipal owner) throws IOException { + checkWritable(); + beginWrite(); + try { + ensureOpen(); + Entry e = getEntry(path); // ensureOpen checked + if (e == null) { + throw new NoSuchFileException(getString(path)); + } + // as the owner information is not persistent, we don't need to + // change e.type to Entry.COPY + if (e instanceof PosixEntry) { + ((PosixEntry)e).owner = owner; + update(e); + } + } finally { + endWrite(); + } + } + + void setGroup(byte[] path, GroupPrincipal group) throws IOException { + checkWritable(); + beginWrite(); + try { + ensureOpen(); + Entry e = getEntry(path); // ensureOpen checked + if (e == null) { + throw new NoSuchFileException(getString(path)); + } + // as the group information is not persistent, we don't need to + // change e.type to Entry.COPY + if (e instanceof PosixEntry) { + ((PosixEntry)e).group = group; + update(e); + } + } finally { + endWrite(); + } + } + + void setPermissions(byte[] path, Set perms) throws IOException { + checkWritable(); + beginWrite(); + try { + ensureOpen(); + Entry e = getEntry(path); // ensureOpen checked + if (e == null) { + throw new NoSuchFileException(getString(path)); + } + if (e.type == Entry.CEN) { + e.type = Entry.COPY; // copy e + } + e.posixPerms = perms == null ? -1 : ZipUtils.permsToFlags(perms); + update(e); + } finally { + endWrite(); + } + } + boolean exists(byte[] path) { beginRead(); try { @@ -448,7 +627,9 @@ if (dir.length == 0 || exists(dir)) // root dir, or existing dir throw new FileAlreadyExistsException(getString(dir)); checkParents(dir); - Entry e = new Entry(dir, Entry.NEW, true, METHOD_STORED); + Entry e = supportPosix ? + new PosixEntry(dir, Entry.NEW, true, METHOD_STORED, attrs) : + new Entry(dir, Entry.NEW, true, METHOD_STORED, attrs); update(e); } finally { endWrite(); @@ -489,7 +670,9 @@ checkParents(dst); } // copy eSrc entry and change name - Entry u = new Entry(eSrc, Entry.COPY); + Entry u = supportPosix ? + new PosixEntry((PosixEntry)eSrc, Entry.COPY) : + new Entry(eSrc, Entry.COPY); u.name(dst); if (eSrc.type == Entry.NEW || eSrc.type == Entry.FILECH) { u.type = eSrc.type; // make it the same type @@ -553,12 +736,15 @@ } return os; } - return getOutputStream(new Entry(e, Entry.NEW)); + return getOutputStream(supportPosix ? + new PosixEntry((PosixEntry)e, Entry.NEW) : new Entry(e, Entry.NEW)); } else { if (!hasCreate && !hasCreateNew) throw new NoSuchFileException(getString(path)); checkParents(path); - return getOutputStream(new Entry(path, Entry.NEW, false, defaultCompressionMethod)); + return getOutputStream(supportPosix ? + new PosixEntry(path, Entry.NEW, false, defaultCompressionMethod) : + new Entry(path, Entry.NEW, false, defaultCompressionMethod)); } } finally { endRead(); @@ -645,7 +831,9 @@ if (e.isDir() || options.contains(CREATE_NEW)) throw new FileAlreadyExistsException(getString(path)); SeekableByteChannel sbc = - new EntryOutputChannel(new Entry(e, Entry.NEW)); + new EntryOutputChannel(supportPosix ? + new PosixEntry((PosixEntry)e, Entry.NEW) : + new Entry(e, Entry.NEW)); if (options.contains(APPEND)) { try (InputStream is = getInputStream(e)) { // copyover byte[] buf = new byte[8192]; @@ -664,7 +852,9 @@ throw new NoSuchFileException(getString(path)); checkParents(path); return new EntryOutputChannel( - new Entry(path, Entry.NEW, false, defaultCompressionMethod)); + supportPosix ? + new PosixEntry(path, Entry.NEW, false, defaultCompressionMethod, attrs) : + new Entry(path, Entry.NEW, false, defaultCompressionMethod, attrs)); } finally { endRead(); } @@ -728,7 +918,10 @@ final FileChannel fch = tmpfile.getFileSystem() .provider() .newFileChannel(tmpfile, options, attrs); - final Entry u = isFCH ? e : new Entry(path, tmpfile, Entry.FILECH); + final Entry u = isFCH ? e : ( + supportPosix ? + new PosixEntry(path, tmpfile, Entry.FILECH, attrs) : + new Entry(path, tmpfile, Entry.FILECH, attrs)); if (forWrite) { u.flag = FLAG_DATADESCR; u.method = defaultCompressionMethod; @@ -1343,7 +1536,7 @@ continue; // no root '/' directory even if it // exists in original zip/jar file. } - e = new Entry(this, inode); + e = supportPosix ? new PosixEntry(this, inode) : new Entry(this, inode); try { if (buf == null) buf = new byte[8192]; @@ -1417,7 +1610,7 @@ return (Entry)inode; if (inode == null || inode.pos == -1) return null; - return new Entry(this, inode); + return supportPosix ? new PosixEntry(this, inode): new Entry(this, inode); } public void deleteFile(byte[] path, boolean failIfNotExists) @@ -2053,6 +2246,7 @@ // entry attributes int version; int flag; + int posixPerms = -1; // posix permissions int method = -1; // compression method long mtime = -1; // last modification time (in DOS time) long atime = -1; // last access time @@ -2081,13 +2275,20 @@ this.method = method; } - Entry(byte[] name, int type, boolean isdir, int method) { + @SuppressWarnings("unchecked") + Entry(byte[] name, int type, boolean isdir, int method, FileAttribute... attrs) { this(name, isdir, method); this.type = type; + for (FileAttribute attr : attrs) { + String attrName = attr.name(); + if (attrName.equals("posix:permissions")) { + posixPerms = ZipUtils.permsToFlags((Set)attr.value()); + } + } } - Entry(byte[] name, Path file, int type) { - this(name, type, false, METHOD_STORED); + Entry(byte[] name, Path file, int type, FileAttribute... attrs) { + this(name, type, false, METHOD_STORED, attrs); this.file = file; } @@ -2111,6 +2312,7 @@ */ this.locoff = e.locoff; this.comment = e.comment; + this.posixPerms = e.posixPerms; this.type = type; } @@ -2135,6 +2337,15 @@ throw new ZipException("unsupported compression method"); } + /** + * Adds information about compatibility of file attribute information + * to a version value. + */ + private int versionMadeBy(int version) { + return (posixPerms < 0) ? version : + VERSION_BASE_UNIX | (version & 0xff); + } + ///////////////////// CEN ////////////////////// private void readCEN(ZipFileSystem zipfs, IndexNode inode) throws IOException { byte[] cen = zipfs.cen; @@ -2157,6 +2368,9 @@ attrs = CENATT(cen, pos); attrsEx = CENATX(cen, pos); */ + if (CENVEM_FA(cen, pos) == FILE_ATTRIBUTES_UNIX) { + posixPerms = CENATX_PERMS(cen, pos) & 0xFFF; // 12 bits for setuid, setgid, sticky + perms + } locoff = CENOFF(cen, pos); pos += CENHDR; this.name = inode.name; @@ -2223,7 +2437,7 @@ } } writeInt(os, CENSIG); // CEN header signature - writeShort(os, version0); // version made by + writeShort(os, versionMadeBy(version0)); // version made by writeShort(os, version0); // version needed to extract writeShort(os, flag); // general purpose bit flag writeShort(os, method); // compression method @@ -2242,7 +2456,9 @@ } writeShort(os, 0); // starting disk number writeShort(os, 0); // internal file attributes (unused) - writeInt(os, 0); // external file attributes (unused) + writeInt(os, posixPerms > 0 ? posixPerms << 16 : 0); // external file + // attributes, used for storing posix + // permissions writeInt(os, locoff0); // relative offset of local header writeBytes(os, zname, 1, nlen); if (zip64) { @@ -2527,6 +2743,10 @@ fm.format(" compressedSize : %d%n", compressedSize()); fm.format(" crc : %x%n", crc()); fm.format(" method : %d%n", method()); + Set permissions = storedPermissions().orElse(null); + if (permissions != null) { + fm.format(" permissions : %s%n", permissions); + } fm.close(); return sb.toString(); } @@ -2607,6 +2827,62 @@ return Arrays.copyOf(comment, comment.length); return null; } + + @Override + public Optional> storedPermissions() { + Set perms = null; + if (posixPerms != -1) { + perms = new HashSet<>(PosixFilePermission.values().length); + for (PosixFilePermission perm : PosixFilePermission.values()) { + if ((posixPerms & ZipUtils.permToFlag(perm)) != 0) { + perms.add(perm); + } + } + } + return Optional.ofNullable(perms); + } + } + + final class PosixEntry extends Entry implements PosixFileAttributes { + private UserPrincipal owner = defaultOwner; + private GroupPrincipal group = defaultGroup; + + PosixEntry(byte[] name, boolean isdir, int method) { + super(name, isdir, method); + } + + PosixEntry(byte[] name, int type, boolean isdir, int method, FileAttribute... attrs) { + super(name, type, isdir, method, attrs); + } + + PosixEntry(byte[] name, Path file, int type, FileAttribute... attrs) { + super(name, file, type, attrs); + } + + PosixEntry(PosixEntry e, int type) { + super(e, type); + this.owner = e.owner; + this.group = e.group; + } + + PosixEntry(ZipFileSystem zipfs, IndexNode inode) throws IOException { + super(zipfs, inode); + } + + @Override + public UserPrincipal owner() { + return owner; + } + + @Override + public GroupPrincipal group() { + return group; + } + + @Override + public Set permissions() { + return storedPermissions().orElse(Set.copyOf(defaultPermissions)); + } } private static class ExistingChannelCloser { --- old/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipPath.java 2019-05-21 17:13:39.732554500 +0200 +++ new/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipPath.java 2019-05-21 17:13:38.534424200 +0200 @@ -34,11 +34,7 @@ import java.nio.channels.SeekableByteChannel; import java.nio.file.*; import java.nio.file.DirectoryStream.Filter; -import java.nio.file.attribute.BasicFileAttributeView; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.FileAttribute; -import java.nio.file.attribute.FileAttributeView; -import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.*; import java.util.Arrays; import java.util.Iterator; import java.util.Map; @@ -711,6 +707,12 @@ return (V)new ZipFileAttributeView(this, false); if (type == ZipFileAttributeView.class) return (V)new ZipFileAttributeView(this, true); + if (zfs.supportPosix) { + if (type == PosixFileAttributeView.class) + return (V)new ZipPosixFileAttributeView(this, false); + if (type == FileOwnerAttributeView.class) + return (V)new ZipPosixFileAttributeView(this,true); + } throw new UnsupportedOperationException("view <" + type + "> is not supported"); } @@ -721,6 +723,12 @@ return new ZipFileAttributeView(this, false); if ("zip".equals(type)) return new ZipFileAttributeView(this, true); + if (zfs.supportPosix) { + if ("posix".equals(type)) + return new ZipPosixFileAttributeView(this, false); + if ("owner".equals(type)) + return new ZipPosixFileAttributeView(this, true); + } throw new UnsupportedOperationException("view <" + type + "> is not supported"); } @@ -764,10 +772,16 @@ @SuppressWarnings("unchecked") // Cast to A A readAttributes(Class type) throws IOException { + // unconditionally support BasicFileAttributes and ZipFileAttributes if (type == BasicFileAttributes.class || type == ZipFileAttributes.class) { return (A)readAttributes(); } + // support PosixFileAttributes when activated + if (type == PosixFileAttributes.class && zfs.supportPosix) { + return (A)readAttributes(); + } + throw new UnsupportedOperationException("Attributes of type " + type.getName() + " not supported"); } @@ -794,9 +808,22 @@ zfs.setTimes(getResolvedPath(), mtime, atime, ctime); } - Map readAttributes(String attributes, LinkOption... options) + void setOwner(UserPrincipal owner) throws IOException { + zfs.setOwner(getResolvedPath(), owner); + } + + void setPermissions(Set perms) throws IOException + { + zfs.setPermissions(getResolvedPath(), perms); + } + void setGroup(GroupPrincipal group) throws IOException { + zfs.setGroup(getResolvedPath(), group); + } + + Map readAttributes(String attributes, LinkOption... options) + throws IOException { String view; String attrs; @@ -948,12 +975,14 @@ } } if (copyAttrs) { - BasicFileAttributeView view = - target.getFileAttributeView(BasicFileAttributeView.class); + ZipFileAttributeView view = + target.getFileAttributeView(ZipFileAttributeView.class); try { view.setTimes(zfas.lastModifiedTime(), zfas.lastAccessTime(), zfas.creationTime()); + // copy permissions + view.setPermissions(zfas.storedPermissions().orElse(null)); } catch (IOException x) { // rollback? try { --- old/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipUtils.java 2019-05-21 17:13:47.849651600 +0200 +++ new/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipUtils.java 2019-05-21 17:13:46.683458800 +0200 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2019, Oracle and/or its affiliates. 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 @@ -27,12 +27,14 @@ import java.io.IOException; import java.io.OutputStream; +import java.nio.file.attribute.PosixFilePermission; import java.time.DateTimeException; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Arrays; import java.util.Date; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.regex.PatternSyntaxException; @@ -41,6 +43,112 @@ */ class ZipUtils { + /** + * The value indicating unix file attributes in CEN field "version made by". + */ + static final int FILE_ATTRIBUTES_UNIX = 3; + + /** + * Constant used to calculate "version made by". + */ + static final int VERSION_BASE_UNIX = FILE_ATTRIBUTES_UNIX << 8; + + /** + * The bit flag used to specify read permission by the owner. + */ + static final int POSIX_USER_READ = 0400; + + /** + * The bit flag used to specify write permission by the owner. + */ + static final int POSIX_USER_WRITE = 0200; + + /** + * The bit flag used to specify execute permission by the owner. + */ + static final int POSIX_USER_EXECUTE = 0100; + + /** + * The bit flag used to specify read permission by the group. + */ + static final int POSIX_GROUP_READ = 040; + + /** + * The bit flag used to specify write permission by the group. + */ + static final int POSIX_GROUP_WRITE = 020; + + /** + * The bit flag used to specify execute permission by the group. + */ + static final int POSIX_GROUP_EXECUTE = 010; + + /** + * The bit flag used to specify read permission by others. + */ + static final int POSIX_OTHER_READ = 04; + + /** + * The bit flag used to specify write permission by others. + */ + static final int POSIX_OTHER_WRITE = 02; + + /** + * The bit flag used to specify execute permission by others. + */ + static final int POSIX_OTHER_EXECUTE = 01; + + /** + * Convert a {@link PosixFilePermission} object into the appropriate bit + * flag. + * + * @param perm The {@link PosixFilePermission} object. + * @return The bit flag as int. + */ + static int permToFlag(PosixFilePermission perm) { + switch(perm) { + case OWNER_READ: + return POSIX_USER_READ; + case OWNER_WRITE: + return POSIX_USER_WRITE; + case OWNER_EXECUTE: + return POSIX_USER_EXECUTE; + case GROUP_READ: + return POSIX_GROUP_READ; + case GROUP_WRITE: + return POSIX_GROUP_WRITE; + case GROUP_EXECUTE: + return POSIX_GROUP_EXECUTE; + case OTHERS_READ: + return POSIX_OTHER_READ; + case OTHERS_WRITE: + return POSIX_OTHER_WRITE; + case OTHERS_EXECUTE: + return POSIX_OTHER_EXECUTE; + default: + return 0; + } + } + + /** + * Converts a set of {@link PosixFilePermission}s into an int value where + * the according bits are set. + * + * @param perms A Set of {@link PosixFilePermission} objects. + * + * @return A bit mask representing the input Set. + */ + static int permsToFlags(Set perms) { + if (perms == null) { + return -1; + } + int flags = 0; + for (PosixFilePermission perm : perms) { + flags |= permToFlag(perm); + } + return flags; + } + /* * Writes a 16-bit short to the output stream in little-endian byte order. */ --- old/src/jdk.zipfs/share/classes/module-info.java 2019-05-21 17:13:55.776187700 +0200 +++ new/src/jdk.zipfs/share/classes/module-info.java 2019-05-21 17:13:54.642363100 +0200 @@ -41,6 +41,42 @@ * * The URI {@link java.net.URI#getScheme scheme} that identifies the ZIP file system is {@code jar}. * + *

Support for POSIX file permissions

+ * + * A Zip file system supports POSIX permissions.
+ *
+ * A Zip file system that was created with default properties supports the attribute "{@code permissions}". + * The value of the attribute will be of type + * {@link java.util.Set Set}<{@link java.nio.file.attribute.PosixFilePermission PosixFilePermission}>. + * As POSIX permission data is optional in Zip files, its value can be {@code null} for a file. + * This means, no permission information is stored in the corresponding Zip entry. Files that are newly + * created in a Zip file system will by default have no POSIX permission data.
+ *
+ * For extended POSIX support, allowing to use + * {@link java.nio.file.attribute.PosixFileAttributeView PosixFileAttributeView} and taking advantage + * of {@link java.nio.file.Files#setPosixFilePermissions Files::setPosixFilePermissions} + * or {@link java.nio.file.Files#getPosixFilePermissions Files::getPosixFilePermissions}, + * a Zip file system can be created with the property "{@code enablePosixFileAttributes}" + * set to {@code true}. Owner, group and permissions will then be initialized with default values. + * If the file system that hosts the Zip file supports retrieving file owners, the default owner of + * files inside the Zip file system will be the owner of the Zip file itself. Otherwise, + * the default owner will be a {@link java.nio.file.attribute.UserPrincipal UserPrincipal} + * with its name set to the value of {@code System.getProperty("user.name")}. + * Analogously, if the file system that hosts the Zip file supports retrieving a file's group, + * the default group of files inside the Zip file system will be the group of the Zip file itself. + * Otherwise, the default group will be a {@link java.nio.file.attribute.GroupPrincipal GroupPrincipal} + * with its name set to the value of the file owner's name. + * The default {@link java.util.Set Set} of permissions for cases when no permission data + * is associated with a Zip file entry will contain the permissions + * {@link java.nio.file.attribute.PosixFilePermission#OWNER_READ OWNER_READ}, + * {@link java.nio.file.attribute.PosixFilePermission#OWNER_WRITE OWNER_WRITE} and + * {@link java.nio.file.attribute.PosixFilePermission#GROUP_READ GROUP_READ}. + * It is possible to override these defaults using properties as specified below. + * Owner, group and permission attributes can be modified for files hosted by a Zip file system. However, + * owner and group information are not persisted. Files that are newly + * created in a Zip file system will by default have no POSIX permission data, although default permissions + * are returned for the attribute "{@code permissions}". + * *

Zip File System Properties

* * The following properties may be specified when creating a Zip @@ -79,6 +115,44 @@ * names of the entries in the Zip or JAR file. * * + * + * enablePosixFileAttributes + * java.lang.String + * false + * + * If the value is {@code true}, the created Zip file system will support + * the {@link java.nio.file.attribute.PosixFileAttributeView PosixFileAttributeView}. + * + * + * + * defaultOwner + * {@link java.nio.file.attribute.UserPrincipal UserPrincipal} or java.lang.String + * null/unset + * + * Override the default owner for entries in the Zip file system. + * The value can be a UserPrincipal or a String value that is used as the UserPrincipal's name. + * + * + * + * defaultGroup + * {@link java.nio.file.attribute.GroupPrincipal GroupPrincipal} or java.lang.String + * null/unset + * + * Override the the default group for entries in the Zip file system. + * The value can be a GroupPrincipal or a String value that is used as the GroupPrincipal's name. + * + * + * + * defaultPermissions + * {@link java.util.Set Set}<{@link java.nio.file.attribute.PosixFilePermission PosixFilePermission}> + * or java.lang.String + * null/unset + * + * Override the default Set of permissions for entries in the Zip file system. + * The value can be a Set<PosixFilePermission> or a String that is parsed by + * {@link java.nio.file.attribute.PosixFilePermissions#fromString PosixFilePermissions.fromString} + * + * * * * --- /dev/null 2019-05-21 17:14:05.000000000 +0200 +++ new/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipPosixFileAttributeView.java 2019-05-21 17:14:03.271091500 +0200 @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2009, 2019, Oracle and/or its affiliates. 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.nio.zipfs; + +import java.io.IOException; +import java.nio.file.attribute.GroupPrincipal; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.UserPrincipal; + +/** + * The zip file system attribute view with POSIX support. + */ +class ZipPosixFileAttributeView extends ZipFileAttributeView implements PosixFileAttributeView { + private final boolean isOwnerView; + + ZipPosixFileAttributeView(ZipPath path, boolean owner) { + super(path, true); + this.isOwnerView = owner; + } + + @Override + public String name() { + return isOwnerView ? "owner" : "posix"; + } + + @Override + public PosixFileAttributes readAttributes() throws IOException { + return (PosixFileAttributes)path.readAttributes(); + } + + @Override + public UserPrincipal getOwner() throws IOException { + return readAttributes().owner(); + } + + @Override + public void setOwner(UserPrincipal owner) throws IOException { + path.setOwner(owner); + } + + @Override + public void setGroup(GroupPrincipal group) throws IOException { + path.setGroup(group); + } + + Object attribute(AttrID id, ZipFileAttributes zfas) { + PosixFileAttributes pzfas = (PosixFileAttributes)zfas; + switch (id) { + case owner: + return pzfas.owner(); + case group: + return pzfas.group(); + case permissions: + if (!isOwnerView) { + return pzfas.permissions(); + } else { + return super.attribute(id, zfas); + } + default: + return super.attribute(id, zfas); + } + } +} --- /dev/null 2019-05-21 17:14:13.000000000 +0200 +++ new/test/jdk/jdk/nio/zipfs/TestPosix.java 2019-05-21 17:14:10.902637700 +0200 @@ -0,0 +1,637 @@ +/* + * 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); + } + } +}