# HG changeset patch # User clanger # Date 1550006768 0 # Tue Feb 12 21:26:08 2019 +0000 # Node ID 10253aa14343ae77f7fad4f95f8c4575b832f5dc # Parent 6c96d42ec3e701d5c7ca863b282a24107e1f6386 8213031: (zipfs) Add support for POSIX file permissions diff --git a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipConstants.java b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipConstants.java --- a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipConstants.java +++ b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipConstants.java @@ -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. */ diff --git a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileAttributeView.java b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileAttributeView.java --- a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileAttributeView.java +++ b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileAttributeView.java @@ -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 @@ -28,14 +28,20 @@ import java.io.IOException; import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.FileAttributeView; +import java.nio.file.attribute.FileOwnerAttributeView; import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.GroupPrincipal; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.UserPrincipal; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; /** * @author Xueming Shen, Rajendra Gutupalli, Jaya Hangal */ -class ZipFileAttributeView implements BasicFileAttributeView { +class ZipFileAttributeView implements PosixFileAttributeView { private static enum AttrID { size, creationTime, @@ -48,43 +54,67 @@ fileKey, compressedSize, crc, - method + method, + storedPermissions, + owner, + group, + permissions }; + private static enum ViewType { + basic, + owner, + posix, + zip + } + private final ZipPath path; - private final boolean isZipView; + private final ViewType type; - private ZipFileAttributeView(ZipPath path, boolean isZipView) { + private ZipFileAttributeView(ZipPath path, ViewType type) { this.path = path; - this.isZipView = isZipView; + this.type = type; } @SuppressWarnings("unchecked") // Cast to V static V get(ZipPath path, Class type) { + if (type == BasicFileAttributeView.class) + return (V)new ZipFileAttributeView(path, ViewType.basic); + if (type == ZipFileAttributeView.class) + return (V)new ZipFileAttributeView(path, ViewType.zip); + if (path.zfs.supportPosix) { + if (type == PosixFileAttributeView.class) + return (V)new ZipFileAttributeView(path, ViewType.posix); + if (type == FileOwnerAttributeView.class) + return (V)new ZipFileAttributeView(path, ViewType.owner); + } if (type == null) throw new NullPointerException(); - if (type == BasicFileAttributeView.class) - return (V)new ZipFileAttributeView(path, false); - if (type == ZipFileAttributeView.class) - return (V)new ZipFileAttributeView(path, true); - return null; + throw new UnsupportedOperationException(); } static ZipFileAttributeView get(ZipPath path, String type) { if (type == null) throw new NullPointerException(); - if (type.equals("basic")) - return new ZipFileAttributeView(path, false); - if (type.equals("zip")) - return new ZipFileAttributeView(path, true); - return null; + if (type.equals(ViewType.basic.name())) + return new ZipFileAttributeView(path, ViewType.basic); + if (type.equals(ViewType.zip.name())) + return new ZipFileAttributeView(path, ViewType.zip); + if (path.zfs.supportPosix) { + if (type.equals(ViewType.posix.name())) + return new ZipFileAttributeView(path, ViewType.posix); + if (type.equals(ViewType.owner.name())) + return new ZipFileAttributeView(path, ViewType.owner); + } + throw new UnsupportedOperationException("view <" + type + "> is not supported"); } @Override public String name() { - return isZipView ? "zip" : "basic"; + return type.name(); } + @Override public ZipFileAttributes readAttributes() throws IOException { return path.getAttributes(); } @@ -98,6 +128,27 @@ path.setTimes(lastModifiedTime, lastAccessTime, createTime); } + @Override + public UserPrincipal getOwner() throws IOException { + return readAttributes().owner(); + } + + @Override + public void setOwner(UserPrincipal owner) throws IOException { + path.setOwner(owner); + } + + @Override + public void setPermissions(Set perms) throws IOException { + path.setPermissions(perms); + } + + @Override + public void setGroup(GroupPrincipal group) throws IOException { + path.setGroup(group); + } + + @SuppressWarnings("unchecked") void setAttribute(String attribute, Object value) throws IOException { @@ -108,10 +159,14 @@ setTimes(null, (FileTime)value, null); if (AttrID.valueOf(attribute) == AttrID.creationTime) setTimes(null, null, (FileTime)value); + if (AttrID.valueOf(attribute) == AttrID.permissions || + AttrID.valueOf(attribute) == AttrID.storedPermissions) + setPermissions((Set)value); return; - } catch (IllegalArgumentException x) {} - throw new UnsupportedOperationException("'" + attribute + - "' is unknown or read-only attribute"); + } catch (IllegalArgumentException x) { + throw new UnsupportedOperationException("'" + attribute + + "' is unknown or read-only attribute"); + } } Map readAttributes(String attributes) @@ -136,7 +191,7 @@ return map; } - Object attribute(AttrID id, ZipFileAttributes zfas) { + private Object attribute(AttrID id, ZipFileAttributes zfas) { switch (id) { case size: return zfas.size(); @@ -156,17 +211,40 @@ return zfas.isOther(); case fileKey: return zfas.fileKey(); + case owner: + if (type== ViewType.owner || type == ViewType.posix || type == ViewType.zip) { + return zfas.owner(); + } + break; + case group: + if (type == ViewType.posix || type == ViewType.zip) { + return zfas.group(); + } + break; + case permissions: + if (type == ViewType.posix || type == ViewType.zip) { + return zfas.permissions(); + } + break; case compressedSize: - if (isZipView) + if (type == ViewType.zip) { return zfas.compressedSize(); + } break; case crc: - if (isZipView) + if (type == ViewType.zip) { return zfas.crc(); + } break; case method: - if (isZipView) + if (type == ViewType.zip) { return zfas.method(); + } + break; + case storedPermissions: + if (type == ViewType.zip) { + return zfas.storedPermissions().orElse(null); + } break; } return null; diff --git a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileAttributes.java b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileAttributes.java --- a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileAttributes.java +++ b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileAttributes.java @@ -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 @@ -25,18 +25,21 @@ package jdk.nio.zipfs; -import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Optional; +import java.util.Set; /** * The attributes of a file stored in a zip file. * * @author Xueming Shen, Rajendra Gutupalli,Jaya Hangal */ -interface ZipFileAttributes extends BasicFileAttributes { +interface ZipFileAttributes extends PosixFileAttributes { public long compressedSize(); public long crc(); public int method(); public byte[] extra(); public byte[] comment(); - public String toString(); + public Optional> storedPermissions(); } diff --git a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileStore.java b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileStore.java --- a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileStore.java +++ b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileStore.java @@ -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 @@ -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 name.equals("basic") || name.equals("zip"); + return "basic".equals(name) || "zip".equals(name) || + (("owner".equals(name) || "posix".equals(name)) && zfs.supportPosix); } @Override diff --git a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java --- a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java +++ b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java @@ -43,8 +43,12 @@ import java.nio.file.*; import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.GroupPrincipal; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.UserPrincipal; import java.nio.file.attribute.UserPrincipalLookupService; import java.nio.file.spi.FileSystemProvider; +import java.security.AccessControlException; import java.security.AccessController; import java.security.PrivilegedAction; import java.security.PrivilegedActionException; @@ -78,6 +82,23 @@ * @author Xueming Shen */ class ZipFileSystem extends FileSystem { + // statics + private static final boolean isWindows = AccessController.doPrivileged( + (PrivilegedAction)() -> System.getProperty("os.name") + .startsWith("Windows")); + private static final String POSIX_OPT = "posix"; + private static final String DEFAULT_OWNER_OPT = "defaultOwner"; + private static final String DEFAULT_GROUP_OPT = "defaultGroup"; + private static final String DEFAULT_PERMISSIONS_OPT = "defaultPermissions"; + private static final String DEFAULT_PRINCIPAL_NAME = ""; + + private static final UserPrincipal DEFAULT_OWNER = initDefaultOwner(); + private static final GroupPrincipal DEFAULT_GROUP = () -> DEFAULT_PRINCIPAL_NAME; + private static final Set DEFAULT_PERMISSIONS = Set.of( + PosixFilePermission.OWNER_READ, + PosixFilePermission.OWNER_WRITE, + PosixFilePermission.GROUP_READ); + private final ZipFileSystemProvider provider; private final Path zfpath; final ZipCoder zc; @@ -88,13 +109,31 @@ private final boolean noExtt; // see readExtra() private final boolean useTempFile; // use a temp file for newOS, default // is to use BAOS for better performance - private static final boolean isWindows = AccessController.doPrivileged( - (PrivilegedAction)() -> System.getProperty("os.name") - .startsWith("Windows")); + private final boolean forceEnd64; private final int defaultMethod; // 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; + + private static UserPrincipal initDefaultOwner() { + String userDotName; + try { + userDotName = AccessController.doPrivileged( + (PrivilegedAction)() -> System.getProperty("user.name")); + } catch (AccessControlException e) { + userDotName = null; + } + String defaultUserName = userDotName == null ? DEFAULT_PRINCIPAL_NAME : userDotName; + return () -> defaultUserName; + } + ZipFileSystem(ZipFileSystemProvider provider, Path zfpath, Map env) throws IOException @@ -105,6 +144,60 @@ this.noExtt = "false".equals(env.get("zipinfo-time")); this.useTempFile = isTrue(env, "useTempFile"); this.forceEnd64 = isTrue(env, "forceZIP64End"); + this.supportPosix = isTrue(env, POSIX_OPT); + if (supportPosix) { + Object o = env.get(DEFAULT_OWNER_OPT); + if (o == null) { + defaultOwner = DEFAULT_OWNER; + } else { + if (o instanceof UserPrincipal) { + defaultOwner = (UserPrincipal)o; + } else { + throw new IllegalArgumentException("Value for property " + + DEFAULT_OWNER_OPT + " must be of type " + + UserPrincipal.class); + } + } + o = env.get(DEFAULT_GROUP_OPT); + if (o == null) { + defaultGroup = DEFAULT_GROUP; + } else { + if (o instanceof GroupPrincipal) { + defaultGroup = (GroupPrincipal)o; + } else { + throw new IllegalArgumentException("Value for property " + + DEFAULT_GROUP_OPT + " must be of type " + + GroupPrincipal.class); + } + } + o = env.get(DEFAULT_PERMISSIONS_OPT); + if (o == null) { + defaultPermissions = DEFAULT_PERMISSIONS; + } else { + if (o instanceof Set) { + defaultPermissions = new HashSet(); + Set perms = (Set)o; + for (Object o2 : perms) { + if (o2 instanceof PosixFilePermission) { + defaultPermissions.add((PosixFilePermission)o2); + } else { + throw new IllegalArgumentException(DEFAULT_PERMISSIONS_OPT + + " must only contain objects of type " + + PosixFilePermission.class); + } + } + } else { + throw new IllegalArgumentException("Value for property " + + DEFAULT_PERMISSIONS_OPT + " must be of type " + Set.class); + } + } + supportedFileAttributeViews = Set.of("basic", "posix", "zip"); + } else { + defaultOwner = DEFAULT_OWNER; + defaultGroup = DEFAULT_GROUP; + defaultPermissions = DEFAULT_PERMISSIONS; + supportedFileAttributeViews = Set.of("basic", "zip"); + } this.defaultMethod = isTrue(env, "noCompression") ? METHOD_STORED: METHOD_DEFLATED; if (Files.notExists(zfpath)) { // create a new zip if not exists @@ -119,7 +212,7 @@ // sm and existence check zfpath.getFileSystem().provider().checkAccess(zfpath, AccessMode.READ); boolean writeable = AccessController.doPrivileged( - (PrivilegedAction) () -> Files.isWritable(zfpath)); + (PrivilegedAction)() -> Files.isWritable(zfpath)); this.readOnly = !writeable; this.zc = ZipCoder.get(nameEncoding); this.rootdir = new ZipPath(this, new byte[]{'/'}); @@ -218,9 +311,6 @@ return List.of(new ZipFileStore(rootdir)); } - private static final Set supportedFileAttributeViews = - Set.of("basic", "zip"); - @Override public Set supportedFileAttributeViews() { return supportedFileAttributeViews; @@ -371,7 +461,7 @@ if (e == null) throw new NoSuchFileException(getString(path)); if (e.type == Entry.CEN) - e.type = Entry.COPY; // copy e + e.type = Entry.COPY; // copy e if (mtime != null) e.mtime = mtime.toMillis(); if (atime != null) @@ -384,6 +474,63 @@ } } + 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 + e.owner = owner; + 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(); + } + } + + 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 + e.group = group; + update(e); + } finally { + endWrite(); + } + } + boolean exists(byte[] path) throws IOException { @@ -453,7 +600,7 @@ if (dir.length == 0 || exists(dir)) // root dir, or exiting dir throw new FileAlreadyExistsException(getString(dir)); checkParents(dir); - Entry e = new Entry(dir, Entry.NEW, true, METHOD_STORED); + Entry e = new Entry(dir, Entry.NEW, true, METHOD_STORED, attrs); update(e); } finally { endWrite(); @@ -675,7 +822,7 @@ throw new NoSuchFileException(getString(path)); checkParents(path); return new EntryOutputChannel( - new Entry(path, Entry.NEW, false, getCompressMethod(attrs))); + new Entry(path, Entry.NEW, false, getCompressMethod(attrs), attrs)); } finally { endRead(); @@ -740,7 +887,7 @@ 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 : new Entry(path, tmpfile, Entry.FILECH, attrs); if (forWrite) { u.flag = FLAG_DATADESCR; u.method = getCompressMethod(attrs); @@ -1269,10 +1416,10 @@ continue; // pseudo directory node } if (inode.name.length == 1 && inode.name[0] == '/') { - continue; // no root '/' directory even it - // exits in original zip/jar file. + continue; // no root '/' directory even if it + // exists in original zip/jar file. } - e = Entry.readCEN(this, inode); + e = new Entry(inode); try { written += copyLOCEntry(e, false, os, written, buf); elist.add(e); @@ -1310,7 +1457,7 @@ return (Entry)inode; if (inode == null || inode.pos == -1) return null; - return Entry.readCEN(this, inode); + return new Entry(inode); } public void deleteFile(byte[] path, boolean failIfNotExists) @@ -1778,8 +1925,8 @@ static class IndexNode { byte[] name; int hashcode; // node is hashable/hashed by its name - int pos = -1; // position in cen table, -1 menas the - // entry does not exists in zip file + int pos = -1; // position in cen table, -1 means the + // entry does not exist in zip file boolean isdir; IndexNode(byte[] name, boolean isdir) { @@ -1855,7 +2002,7 @@ IndexNode child; // 1st child } - static class Entry extends IndexNode implements ZipFileAttributes { + class Entry extends IndexNode implements ZipFileAttributes { static final int CEN = 1; // entry read from cen static final int NEW = 2; // updated contents in bytes or file @@ -1869,6 +2016,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 @@ -1888,6 +2036,10 @@ long locoff; byte[] comment; + // posix support + private UserPrincipal owner = defaultOwner; + private GroupPrincipal group = defaultGroup; + Entry() {} Entry(byte[] name, boolean isdir, int method) { @@ -1900,12 +2052,19 @@ 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 (Entry e, int type) { + Entry(Entry e, int type) { name(e.name); this.isdir = e.isdir; this.version = e.version; @@ -1925,33 +2084,26 @@ */ this.locoff = e.locoff; this.comment = e.comment; + this.posixPerms = e.posixPerms; + this.owner = e.owner; + this.group = e.group; this.type = type; } - Entry (byte[] name, Path file, int type) { + @SuppressWarnings("unchecked") + Entry(byte[] name, Path file, int type, FileAttribute... attrs) { this(name, type, false, METHOD_STORED); this.file = file; + for (FileAttribute attr : attrs) { + String attrName = attr.name(); + if (attrName.equals("posix:permissions")) { + posixPerms = ZipUtils.permsToFlags((Set)attr.value()); + } + } } - int version() throws ZipException { - if (method == METHOD_DEFLATED) - return 20; - else if (method == METHOD_STORED) - return 10; - throw new ZipException("unsupported compression method"); - } - - ///////////////////// CEN ////////////////////// - static Entry readCEN(ZipFileSystem zipfs, IndexNode inode) - throws IOException - { - return new Entry().cen(zipfs, inode); - } - - private Entry cen(ZipFileSystem zipfs, IndexNode inode) - throws IOException - { - byte[] cen = zipfs.cen; + // reads the full entry from an IndexNode + Entry(IndexNode inode) throws IOException { int pos = inode.pos; if (!cenSigAt(cen, pos)) zerror("invalid CEN header (bad signature)"); @@ -1971,6 +2123,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; @@ -1981,16 +2136,35 @@ if (elen > 0) { extra = Arrays.copyOfRange(cen, pos, pos + elen); pos += elen; - readExtra(zipfs); + readExtra(ZipFileSystem.this); } if (clen > 0) { comment = Arrays.copyOfRange(cen, pos, pos + clen); } - return this; } + int version(boolean zip64) throws ZipException { + if (zip64) { + return 45; + } + if (method == METHOD_DEFLATED) + return 20; + else if (method == METHOD_STORED) + return 10; + throw new ZipException("unsupported compression method"); + } + + /** + * Adds information about compatibility of file attribute information + * to a version value. + */ + int versionMadeBy(int version) { + return (posixPerms < 0) ? version : + VERSION_BASE_UNIX | (version & 0xff); + } + + ///////////////////// CEN ////////////////////// int writeCEN(OutputStream os) throws IOException { - int version0 = version(); long csize0 = csize; long size0 = size; long locoff0 = locoff; @@ -2021,6 +2195,8 @@ if (elen64 != 0) { elen64 += 4; // header and data sz 4 bytes } + boolean zip64 = (elen64 != 0); + int version0 = version(zip64); while (eoff + 4 < elen) { int tag = SH(extra, eoff); int sz = SH(extra, eoff + 2); @@ -2037,13 +2213,8 @@ } } writeInt(os, CENSIG); // CEN header signature - if (elen64 != 0) { - writeShort(os, 45); // ver 4.5 for zip64 - writeShort(os, 45); - } else { - writeShort(os, version0); // version made by - writeShort(os, version0); // version needed to extract - } + 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 // last modification time @@ -2061,10 +2232,12 @@ } 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 (elen64 != 0) { + if (zip64) { writeShort(os, EXTID_ZIP64);// Zip64 extra writeShort(os, elen64 - 4); // size of "this" extra block if (size0 == ZIP64_MINVAL) @@ -2103,18 +2276,18 @@ ///////////////////// LOC ////////////////////// int writeLOC(OutputStream os) throws IOException { - int version0 = version(); byte[] zname = isdir ? toDirectoryPath(name) : name; int nlen = (zname != null) ? zname.length - 1 : 0; // [0] is slash int elen = (extra != null) ? extra.length : 0; boolean foundExtraTime = false; // if extra timestamp present int eoff = 0; int elen64 = 0; + boolean zip64 = false; int elenEXTT = 0; int elenNTFS = 0; writeInt(os, LOCSIG); // LOC header signature if ((flag & FLAG_DATADESCR) != 0) { - writeShort(os, version0); // version needed to extract + writeShort(os, version(zip64)); // version needed to extract writeShort(os, flag); // general purpose bit flag writeShort(os, method); // compression method // last modification time @@ -2127,16 +2300,15 @@ } else { if (csize >= ZIP64_MINVAL || size >= ZIP64_MINVAL) { elen64 = 20; //headid(2) + size(2) + size(8) + csize(8) - writeShort(os, 45); // ver 4.5 for zip64 - } else { - writeShort(os, version0); // version needed to extract + zip64 = true; } + writeShort(os, version(zip64)); // version needed to extract writeShort(os, flag); // general purpose bit flag writeShort(os, method); // compression method // last modification time writeInt(os, (int)javaToDosTime(mtime)); writeInt(os, crc); // crc-32 - if (elen64 != 0) { + if (zip64) { writeInt(os, ZIP64_MINVAL); writeInt(os, ZIP64_MINVAL); } else { @@ -2166,7 +2338,7 @@ writeShort(os, nlen); writeShort(os, elen + elen64 + elenNTFS + elenEXTT); writeBytes(os, zname, 1, nlen); - if (elen64 != 0) { + if (zip64) { writeShort(os, EXTID_ZIP64); writeShort(os, 16); writeLong(os, size); @@ -2203,7 +2375,7 @@ return LOCHDR + nlen + elen + elen64 + elenNTFS + elenEXTT; } - // Data Descriptior + // Data Descriptor int writeEXT(OutputStream os) throws IOException { writeInt(os, EXTSIG); // EXT header signature writeInt(os, crc); // crc-32 @@ -2280,7 +2452,7 @@ break; } byte[] buf = new byte[LOCHDR]; - if (zipfs.readFullyAt(buf, 0, buf.length , locoff) + if (zipfs.readFullyAt(buf, 0, buf.length, locoff) != buf.length) throw new ZipException("loc: reading failed"); if (!locSigAt(buf, 0)) @@ -2291,7 +2463,7 @@ break; int locNlen = LOCNAM(buf); buf = new byte[locElen]; - if (zipfs.readFullyAt(buf, 0, buf.length , locoff + LOCHDR + locNlen) + if (zipfs.readFullyAt(buf, 0, buf.length, locoff + LOCHDR + locNlen) != buf.length) throw new ZipException("loc extra: reading failed"); int locPos = 0; @@ -2332,6 +2504,30 @@ extra = null; } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(1024); + Formatter fm = new Formatter(sb); + fm.format(" name : %s%n", new String(name)); + fm.format(" creationTime : %tc%n", creationTime().toMillis()); + fm.format(" lastAccessTime : %tc%n", lastAccessTime().toMillis()); + fm.format(" lastModifiedTime: %tc%n", lastModifiedTime().toMillis()); + fm.format(" isRegularFile : %b%n", isRegularFile()); + fm.format(" isDirectory : %b%n", isDirectory()); + fm.format(" isSymbolicLink : %b%n", isSymbolicLink()); + fm.format(" isOther : %b%n", isOther()); + fm.format(" fileKey : %s%n", fileKey()); + fm.format(" size : %d%n", size()); + fm.format(" compressedSize : %d%n", compressedSize()); + fm.format(" crc : %x%n", crc()); + fm.format(" method : %d%n", method()); + if (posixPerms != -1) { + fm.format(" permissions : %s%n", permissions()); + } + fm.close(); + return sb.toString(); + } + ///////// basic file attributes /////////// @Override public FileTime creationTime() { @@ -2378,49 +2574,66 @@ return null; } - ///////// zip entry attributes /////////// + ///////// posix file attributes /////////// + + @Override + public UserPrincipal owner() { + return owner; + } + + @Override + public GroupPrincipal group() { + return group; + } + + @Override + public Set permissions() { + return storedPermissions().orElse(Set.copyOf(defaultPermissions)); + } + + ///////// zip file attributes /////////// + + @Override public long compressedSize() { return csize; } + @Override public long crc() { return crc; } + @Override public int method() { return method; } + @Override public byte[] extra() { if (extra != null) return Arrays.copyOf(extra, extra.length); return null; } + @Override public byte[] comment() { if (comment != null) return Arrays.copyOf(comment, comment.length); return null; } - public String toString() { - StringBuilder sb = new StringBuilder(1024); - Formatter fm = new Formatter(sb); - fm.format(" name : %s%n", new String(name)); - fm.format(" creationTime : %tc%n", creationTime().toMillis()); - fm.format(" lastAccessTime : %tc%n", lastAccessTime().toMillis()); - fm.format(" lastModifiedTime: %tc%n", lastModifiedTime().toMillis()); - fm.format(" isRegularFile : %b%n", isRegularFile()); - fm.format(" isDirectory : %b%n", isDirectory()); - fm.format(" isSymbolicLink : %b%n", isSymbolicLink()); - fm.format(" isOther : %b%n", isOther()); - fm.format(" fileKey : %s%n", fileKey()); - fm.format(" size : %d%n", size()); - fm.format(" compressedSize : %d%n", compressedSize()); - fm.format(" crc : %x%n", crc()); - fm.format(" method : %d%n", method()); - fm.close(); - return sb.toString(); + @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); } } diff --git a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystemProvider.java b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystemProvider.java --- a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystemProvider.java +++ b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystemProvider.java @@ -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 @@ -38,6 +38,7 @@ import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileAttribute; import java.nio.file.attribute.FileAttributeView; +import java.nio.file.attribute.PosixFileAttributes; import java.nio.file.spi.FileSystemProvider; import java.security.AccessController; import java.security.PrivilegedActionException; @@ -131,13 +132,13 @@ throws IOException { ensureFile(path); - try { - ZipFileSystem zipfs; - if (env.containsKey("multi-release")) { - zipfs = new JarFileSystem(this, path, env); - } else { - zipfs = new ZipFileSystem(this, path, env); - } + try { + ZipFileSystem zipfs; + if (env.containsKey("multi-release")) { + zipfs = new JarFileSystem(this, path, env); + } else { + zipfs = new ZipFileSystem(this, path, env); + } return zipfs; } catch (ZipException ze) { String pname = path.toString(); @@ -291,17 +292,31 @@ readAttributes(Path path, Class type, LinkOption... options) throws IOException { - if (type == BasicFileAttributes.class || type == ZipFileAttributes.class) + // unconditionally support BasicFileAttributes and ZipFileAttributes + if (type == BasicFileAttributes.class || + type == ZipFileAttributes.class) + { return (A)toZipPath(path).getAttributes(); - return null; + } + + // support PosixFileAttributes when activated + if (type == PosixFileAttributes.class) { + ZipPath zpath = toZipPath(path); + if (zpath.zfs.supportPosix) { + return (A)zpath.getAttributes(); + } + } + + throw new UnsupportedOperationException("Attributes of type " + + type.getName() + " not supported"); } @Override public Map - readAttributes(Path path, String attribute, LinkOption... options) + readAttributes(Path path, String attributes, LinkOption... options) throws IOException { - return toZipPath(path).readAttributes(attribute, options); + return toZipPath(path).readAttributes(attributes, options); } @Override diff --git a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipPath.java b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipPath.java --- a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipPath.java +++ b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipPath.java @@ -34,9 +34,11 @@ 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.FileAttribute; import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.GroupPrincipal; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.UserPrincipal; import java.util.Arrays; import java.util.Iterator; import java.util.Map; @@ -57,7 +59,7 @@ */ final class ZipPath implements Path { - private final ZipFileSystem zfs; + final ZipFileSystem zfs; private final byte[] path; private volatile int[] offsets; private int hashcode = 0; // cached hashcode (created lazily) @@ -756,10 +758,7 @@ type = attribute.substring(0, colonPos++); attr = attribute.substring(colonPos); } - ZipFileAttributeView view = ZipFileAttributeView.get(this, type); - if (view == null) - throw new UnsupportedOperationException("view <" + view + "> is not supported"); - view.setAttribute(attr, value); + ZipFileAttributeView.get(this, type).setAttribute(attr, value); } void setTimes(FileTime mtime, FileTime atime, FileTime ctime) @@ -768,9 +767,22 @@ zfs.setTimes(getResolvedPath(), mtime, atime, ctime); } + 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 = null; String attrs = null; @@ -782,11 +794,7 @@ view = attributes.substring(0, colonPos++); attrs = attributes.substring(colonPos); } - ZipFileAttributeView zfv = ZipFileAttributeView.get(this, view); - if (zfv == null) { - throw new UnsupportedOperationException("view not supported"); - } - return zfv.readAttributes(attrs); + return ZipFileAttributeView.get(this, view).readAttributes(attrs); } FileStore getFileStore() throws IOException { @@ -935,12 +943,14 @@ } } if (copyAttrs) { - BasicFileAttributeView view = - ZipFileAttributeView.get(target, BasicFileAttributeView.class); + ZipFileAttributeView view = + ZipFileAttributeView.get(target, ZipFileAttributeView.class); try { view.setTimes(zfas.lastModifiedTime(), zfas.lastAccessTime(), zfas.creationTime()); + // copy permissions + view.setPermissions(zfas.storedPermissions().orElse(null)); } catch (IOException x) { // rollback? try { diff --git a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipUtils.java b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipUtils.java --- a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipUtils.java +++ b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipUtils.java @@ -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. */ diff --git a/src/jdk.zipfs/share/classes/module-info.java b/src/jdk.zipfs/share/classes/module-info.java --- a/src/jdk.zipfs/share/classes/module-info.java +++ b/src/jdk.zipfs/share/classes/module-info.java @@ -41,6 +41,34 @@ * * The URI {@link java.net.URI#getScheme scheme} that identifies the ZIP file system is {@code jar}. * + *

Posix Support

+ * + * File systems created by the Zip file system provider have a built in support + * for POSIX permissions. It is possible to query a + * {@link java.nio.file.Path Path} object from a Zip file system for the + * attribute {@code storedPermissions}. The value of the attribute + * will be of type + * {@link java.util.Set}<{@link java.nio.file.attribute.PosixFilePermission}>. + * As POSIX permission information is optional in Zip files, the attribute value + * will be {@code null} in case no permission information exists for a Zip entry.
+ * + * For extended POSIX support, allowing to use + * {@link java.nio.file.attribute.PosixFileAttributeView} and taking advantage + * of {@link java.nio.file.Files#setPosixFilePermissions Files::setPosixFilePermissions} + * or {@link java.nio.file.Files#getPosixFilePermissions Files::getPosixFilePermissions}, + * it is possible to create a Zip file system with the property {@code posix} + * set to {@code true}. + * In that case, certain defaults apply. The owner of a file will be set to a + * {@link java.nio.file.attribute.UserPrincipal} with its name set to the value of + * {@code System.getProperty("user.name")}. The group will be a + * {@link java.nio.file.attribute.GroupPrincipal} with the name "{@code }". + * The default {@link java.util.Set} of permissions for cases when no permission data + * is associated with a Zip file entry, will contain + * {@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 the defaults using properties as specified below. + * *

Zip File System Properties

* * The following properties may be specified when creating a Zip @@ -79,6 +107,45 @@ * names of the entries in the Zip or JAR file. * * + * + * posix + * java.lang.Boolean + * false + * + * If the value is {@code true}, the created Zip file system will support + * the {@link java.nio.file.attribute.PosixFileAttributeView}. + * + * + * + * defaultOwner + * java.nio.file.attribute.UserPrincipal + * A user principal with its name set to the value of System property + * "{@code user.name}" + * + * The value will be the default owner for entries in the Zip + * file system. + * + * + * + * defaultGroup + * java.nio.file.attribute.GroupPrincipal + * A group principal with its name set to "{@code }" + * + * The value will be the default group for entries in the Zip + * file system. + * + * + * + * defaultPermissions + * java.util.Set<{@link java.nio.file.attribute.PosixFilePermission}> + * A set of {@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} + * + * The value will be the default Set of permissions for entries in the Zip + * file system. + * + * * * * diff --git a/test/jdk/jdk/nio/zipfs/TestPosix.java b/test/jdk/jdk/nio/zipfs/TestPosix.java new file mode 100644 --- /dev/null +++ b/test/jdk/jdk/nio/zipfs/TestPosix.java @@ -0,0 +1,627 @@ +/* + * 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. 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 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.AccessControlException; +import java.security.AccessController; +import java.security.PrivilegedAction; +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(System.getProperty("test.dir", "."), "testPosix.zip"); + private static final Path ZIP_FILE_COPY = Paths.get(System.getProperty("test.dir", "."), "testPosixCopy.zip"); + private static final Path UNZIP_DIR = Paths.get(System.getProperty("test.dir", "."), "unzip/"); + + // permission sets + private static final Set ALLPERMS = Set.of( + OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, + GROUP_READ, GROUP_WRITE, GROUP_EXECUTE, + OTHERS_READ, OTHERS_WRITE, OTHERS_EXECUTE); + private static final Set DEFAULTPERMS = Set.of(OWNER_READ, OWNER_WRITE, GROUP_READ); + 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"; + private static final String DEFAULT_PRINCIPAL_NAME = ""; + private static final String DEFAULT_USER_NAME = expectedDefaultOwner(); + + // FS open options + private static final Map ENV_DEFAULT = Collections.emptyMap(); + private static final Map ENV_POSIX = Map.of("posix", 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, DEFAULTPERMS, false, false)); + ENTRIES.put("permslater", new ZipFileEntryInfo(null, UR, UR, UR, false, false)); + } + + private static String expectedDefaultOwner() { + String userDotName; + try { + userDotName = AccessController.doPrivileged( + (PrivilegedAction)() -> System.getProperty("user.name")); + } catch (AccessControlException e) { + userDotName = null; + } + return userDotName == null ? DEFAULT_PRINCIPAL_NAME : userDotName; + } + + private static FileSystemProvider getZipFSProvider() { + for (FileSystemProvider provider : FileSystemProvider.installedProviders()) { + if ("jar".equals(provider.getScheme())) + return provider; + } + return null; + } + + private boolean isPosixFs(Path p) throws IOException { + try { + Files.getPosixFilePermissions(p); + return true; + } catch (UnsupportedOperationException e) { + return false; + } + } + + 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:storedPermissions", 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:storedPermissions"); + } 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); + + if (!isPosixFs(UNZIP_DIR)) { + System.out.println("This can't be tested on non POSIX default file system."); + 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)) { + var entry = zipIn.getPath("/noperms"); + comparePermissions(DEFAULTPERMS, Files.getPosixFilePermissions(entry)); + var owner = Files.getOwner(entry); + assertNotNull(owner, "owner should not be null"); + assertEquals(owner.getName(), DEFAULT_USER_NAME); + 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"); + assertEquals(group.getName(), DEFAULT_PRINCIPAL_NAME); + 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 + try (FileSystem zipIn = zipFSP.newFileSystem(ZIP_FILE, Map.of("posix", 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); + comparePermissions(GR, Files.getPosixFilePermissions(zipIn.getPath("/uexec"))); + } + } +} diff --git a/test/jdk/jdk/nio/zipfs/test.policy b/test/jdk/jdk/nio/zipfs/test.policy --- a/test/jdk/jdk/nio/zipfs/test.policy +++ b/test/jdk/jdk/nio/zipfs/test.policy @@ -2,5 +2,7 @@ permission java.io.FilePermission "<>","read,write,delete"; permission java.util.PropertyPermission "test.jdk","read"; permission java.util.PropertyPermission "test.src","read"; + permission java.util.PropertyPermission "test.dir","read"; permission java.util.PropertyPermission "user.dir","read"; + permission java.lang.RuntimePermission "accessUserInformation"; };