< prev index next >

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

Print this page
rev 54189 : 8213031: (zipfs) Add support for POSIX file permissions

@@ -41,10 +41,14 @@
 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.GroupPrincipal;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.nio.file.attribute.UserPrincipal;
 import java.nio.file.attribute.UserPrincipalLookupService;
 import java.nio.file.spi.FileSystemProvider;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.security.PrivilegedActionException;

@@ -76,40 +80,65 @@
  * A FileSystem built on a zip file
  *
  * @author Xueming Shen
  */
 class ZipFileSystem extends FileSystem {
+    // statics
+    private static final boolean isWindows = AccessController.doPrivileged(
+        (PrivilegedAction<Boolean>)()->System.getProperty("os.name")
+                                             .startsWith("Windows"));
+    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 String DEFAULT_PRINCIPAL_NAME = "<zipfs_default>";
+    private static final Set<PosixFilePermission> DEFAULT_PERMISSIONS =
+        PosixFilePermissions.fromString("rwxrwxrwx");
+
     private final ZipFileSystemProvider provider;
     private final Path zfpath;
     final ZipCoder zc;
     private final ZipPath rootdir;
     private boolean readOnly = false;    // readonly file system
 
     // configurable by env map
     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<Boolean>)() -> 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<PosixFilePermission> defaultPermissions;
+
+    private final Set<String> supportedFileAttributeViews;
+
     ZipFileSystem(ZipFileSystemProvider provider,
                   Path zfpath,
                   Map<String, ?> env) throws IOException
     {
         // default encoding for name/comment
         String nameEncoding = env.containsKey("encoding") ?
-                              (String)env.get("encoding") : "UTF-8";
+            (String)env.get("encoding") : "UTF-8";
         this.noExtt = "false".equals(env.get("zipinfo-time"));
         this.useTempFile  = isTrue(env, "useTempFile");
         this.forceEnd64 = isTrue(env, "forceZIP64End");
-        this.defaultMethod = isTrue(env, "noCompression") ? METHOD_STORED: METHOD_DEFLATED;
+        this.defaultMethod = isTrue(env, "noCompression") ? METHOD_STORED : METHOD_DEFLATED;
+        this.supportPosix = isTrue(env, OPT_POSIX);
+        this.defaultOwner = initOwner(zfpath, env);
+        this.defaultGroup = initGroup(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 not exists
+            // create a new zip if it doesn't exist
             if (isTrue(env, "create")) {
                 try (OutputStream os = Files.newOutputStream(zfpath, CREATE_NEW, WRITE)) {
                     new END().write(os, 0, forceEnd64);
                 }
             } else {

@@ -117,11 +146,11 @@
             }
         }
         // sm and existence check
         zfpath.getFileSystem().provider().checkAccess(zfpath, AccessMode.READ);
         boolean writeable = AccessController.doPrivileged(
-            (PrivilegedAction<Boolean>) () ->  Files.isWritable(zfpath));
+            (PrivilegedAction<Boolean>)()->Files.isWritable(zfpath));
         this.readOnly = !writeable;
         this.zc = ZipCoder.get(nameEncoding);
         this.rootdir = new ZipPath(this, new byte[]{'/'});
         this.ch = Files.newByteChannel(zfpath, READ);
         try {

@@ -141,10 +170,94 @@
     // returns true if there is a name=true/"true" setting in env
     private static boolean isTrue(Map<String, ?> env, String name) {
         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 "<zipfs_default>".
+    private UserPrincipal initOwner(Path zfpath, Map<String, ?> env) {
+        Object o = env.get(OPT_DEFAULT_OWNER);
+        if (o == null) {
+            try {
+                return Files.getOwner(zfpath);
+            } catch (Exception e) {
+                try {
+                    String userName = AccessController.doPrivileged(
+                        (PrivilegedAction<String>)()->System.getProperty("user.name"));
+                    return ()->userName;
+                } catch (Exception e2) {
+                    return ()->DEFAULT_PRINCIPAL_NAME;
+                }
+            }
+        }
+        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, it will return a group principal going
+    // by the same name as the default owner.
+    private GroupPrincipal initGroup(Map<String, ?> env) {
+        Object o = env.get(OPT_DEFAULT_GROUP);
+        if (o == null) {
+            return ()->defaultOwner.getName();
+        }
+        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<PosixFilePermission> initPermissions(Map<String, ?> 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<PosixFilePermission> perms = new HashSet<PosixFilePermission>();
+        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;
     }
 

@@ -216,13 +329,10 @@
     @Override
     public Iterable<FileStore> getFileStores() {
         return List.of(new ZipFileStore(rootdir));
     }
 
-    private static final Set<String> supportedFileAttributeViews =
-            Set.of("basic", "zip");
-
     @Override
     public Set<String> supportedFileAttributeViews() {
         return supportedFileAttributeViews;
     }
 

@@ -369,11 +479,11 @@
             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.type = Entry.COPY;     // copy e
             if (mtime != null)
                 e.mtime = mtime.toMillis();
             if (atime != null)
                 e.atime = atime.toMillis();
             if (ctime != null)

@@ -382,10 +492,67 @@
         } finally {
             endWrite();
         }
     }
 
+    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<PosixFilePermission> 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
     {
         beginRead();
         try {

@@ -451,11 +618,11 @@
         try {
             ensureOpen();
             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();
         }
     }

@@ -632,14 +799,13 @@
 
     private int getCompressMethod(FileAttribute<?>... attrs) {
          return defaultMethod;
     }
 
-    // Returns a Writable/ReadByteChannel for now. Might consdier to use
+    // Returns a Writable/ReadByteChannel for now. Might consider to use
     // newFileChannel() instead, which dump the entry data into a regular
-    // file on the default file system and create a FileChannel on top of
-    // it.
+    // file on the default file system and create a FileChannel on top of it.
     SeekableByteChannel newByteChannel(byte[] path,
                                        Set<? extends OpenOption> options,
                                        FileAttribute<?>... attrs)
         throws IOException
     {

@@ -673,11 +839,11 @@
                 }
                 if (!options.contains(CREATE) && !options.contains(CREATE_NEW))
                     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();
             }
         } else {

@@ -738,11 +904,11 @@
             final boolean isFCH = (e != null && e.type == Entry.FILECH);
             final Path tmpfile = isFCH ? e.file : getTempPathForEntry(path);
             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);
             }
             // is there a better way to hook into the FileChannel's close method?

@@ -1267,14 +1433,14 @@
                 } else {                        // unchanged inode
                     if (inode.pos == -1) {
                         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);
                     } catch (IOException x) {
                         x.printStackTrace();    // skip any wrong entry

@@ -1308,11 +1474,11 @@
         IndexNode inode = getInode(path);
         if (inode instanceof Entry)
             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)
         throws IOException
     {

@@ -1776,12 +1942,12 @@
     // The node itself can be used as a "key" to lookup itself in
     // the HashMap inodes.
     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) {
             name(name);
             this.isdir = isdir;

@@ -1853,11 +2019,11 @@
         IndexNode() {}
         IndexNode sibling;
         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
         static final int FILECH = 3;  // fch update in "file"
         static final int COPY   = 4;  // copy of a CEN entry

@@ -1867,10 +2033,11 @@
         int    type = CEN;            // default is the entry read from cen
 
         // 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
         long   ctime  = -1;    // create time
         long   crc    = -1;    // crc-32 of entry data

@@ -1886,10 +2053,14 @@
         // int    attrs;
         // long   attrsEx;
         long   locoff;
         byte[] comment;
 
+        // posix support
+        private UserPrincipal owner = defaultOwner;
+        private GroupPrincipal group = defaultGroup;
+
         Entry() {}
 
         Entry(byte[] name, boolean isdir, int method) {
             name(name);
             this.isdir = isdir;

@@ -1898,16 +2069,23 @@
             this.size   = 0;
             this.csize  = 0;
             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<PosixFilePermission>)attr.value());
+                }
+            }
         }
 
-        Entry (Entry e, int type) {
+        Entry(Entry e, int type) {
             name(e.name);
             this.isdir     = e.isdir;
             this.version   = e.version;
             this.ctime     = e.ctime;
             this.atime     = e.atime;

@@ -1923,37 +2101,30 @@
             this.attrs     = e.attrs;
             this.attrsEx   = e.attrsEx;
             */
             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<PosixFilePermission>)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)");
             version     = CENVER(cen, pos);
             flag        = CENFLG(cen, pos);

@@ -1969,30 +2140,52 @@
             versionMade = CENVEM(cen, pos);
             disk        = CENDSK(cen, pos);
             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;
             this.isdir = inode.isdir;
             this.hashcode = inode.hashcode;
 
             pos += nlen;
             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;
             int elen64   = 0;                // extra for ZIP64
             int elenNTFS = 0;                // extra for NTFS (a/c/mtime)

@@ -2019,10 +2212,12 @@
                 elen64 += 8;                 // offset(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);
                 if (tag == EXTID_EXTT || tag == EXTID_NTFS) {
                     foundExtraTime = true;

@@ -2035,17 +2230,12 @@
                 } else {                     // Extended Timestamp otherwise
                     elenEXTT = 9;            // only mtime in cen
                 }
             }
             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
             writeInt(os, (int)javaToDosTime(mtime));
             writeInt(os, crc);               // crc-32

@@ -2059,14 +2249,16 @@
             } else {
                 writeShort(os, 0);
             }
             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)
                     writeLong(os, size);
                 if (csize0 == ZIP64_MINVAL)

@@ -2101,22 +2293,22 @@
         }
 
         ///////////////////// 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
                 writeInt(os, (int)javaToDosTime(mtime));
                 // store size, uncompressed size, and crc-32 in data descriptor

@@ -2125,20 +2317,19 @@
                 writeInt(os, 0);
                 writeInt(os, 0);
             } 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 {
                     writeInt(os, csize);        // compressed size
                     writeInt(os, size);         // uncompressed size

@@ -2164,11 +2355,11 @@
                 }
             }
             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);
                 writeLong(os, csize);
             }

@@ -2201,11 +2392,11 @@
                 writeBytes(os, extra);
             }
             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
             if (csize >= ZIP64_MINVAL || size >= ZIP64_MINVAL) {
                 writeLong(os, csize);

@@ -2278,22 +2469,22 @@
                         if (sz == 5)
                             mtime = unixToJavaTime(LG(extra, pos + 1));
                          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))
                         throw new ZipException("loc: wrong sig ->"
                                            + Long.toString(getSig(buf, 0), 16));
                     int locElen = LOCEXT(buf);
                     if (locElen < 9)    // EXTT is at lease 9 bytes
                         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;
                     while (locPos + 4 < buf.length) {
                         int locTag = SH(buf, locPos);

@@ -2330,10 +2521,34 @@
                 extra = Arrays.copyOf(extra, newOff);
             else
                 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() {
             return FileTime.fromMillis(ctime == -1 ? mtime : ctime);
         }

@@ -2376,53 +2591,70 @@
         @Override
         public Object fileKey() {
             return null;
         }
 
-        ///////// zip entry attributes ///////////
+        ///////// posix file attributes ///////////
+
+        @Override
+        public UserPrincipal owner() {
+            return owner;
+        }
+
+        @Override
+        public GroupPrincipal group() {
+            return group;
+        }
+
+        @Override
+        public Set<PosixFilePermission> 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<Set<PosixFilePermission>> storedPermissions() {
+            Set<PosixFilePermission> 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);
         }
     }
 
     // ZIP directory has two issues:
     // (1) ZIP spec does not require the ZIP file to include
< prev index next >