# HG changeset patch # User clanger # Date 1537884584 -3600 # Tue Sep 25 15:09:44 2018 +0100 # Node ID d68320c9662e8af423d1ec8f1dbc148154a4bb36 # Parent eb954a4b60836a16c9db9e22544d9d02fdde4cc9 6194856: Zip Files lose ALL ownership and permissions of the files diff --git a/src/java.base/share/classes/java/nio/file/attribute/PosixFilePermissions.java b/src/java.base/share/classes/java/nio/file/attribute/PosixFilePermissions.java --- a/src/java.base/share/classes/java/nio/file/attribute/PosixFilePermissions.java +++ b/src/java.base/share/classes/java/nio/file/attribute/PosixFilePermissions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2007, 2018, 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 @@ -26,7 +26,13 @@ package java.nio.file.attribute; import static java.nio.file.attribute.PosixFilePermission.*; -import java.util.*; + +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * This class consists exclusively of static methods that operate on sets of @@ -34,10 +40,119 @@ * * @since 1.7 */ - public final class PosixFilePermissions { private PosixFilePermissions() { } + /** + * The bit flag used to specify read permission by the owner. + */ + public static final int POSIX_USER_READ = 0400; + + /** + * The bit flag used to specify write permission by the owner. + */ + public static final int POSIX_USER_WRITE = 0200; + + /** + * The bit flag used to specify execute permission by the owner. + */ + public static final int POSIX_USER_EXECUTE = 0100; + + /** + * The bit flag used to specify read permission by the group. + */ + public static final int POSIX_GROUP_READ = 040; + + /** + * The bit flag used to specify write permission by the group. + */ + public static final int POSIX_GROUP_WRITE = 020; + + /** + * The bit flag used to specify execute permission by the group. + */ + public static final int POSIX_GROUP_EXECUTE = 010; + + /** + * The bit flag used to specify read permission by others. + */ + public static final int POSIX_OTHER_READ = 04; + + /** + * The bit flag used to specify write permission by others. + */ + public static final int POSIX_OTHER_WRITE = 02; + + /** + * The bit flag used to specify execute permission by others. + */ + public 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. + */ + private 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. + */ + public static int toFlags(Set perms) { + return perms + .stream() + .mapToInt(PosixFilePermissions::permToFlag) + .reduce(0, (p1, p2)-> p1 | p2); + } + + /** + * Converts a bit mask of Posix file permissions into a set of + * {@link PosixFilePermission} objects. + * + * @param flags The bit mask containing the flags. + * + * @return A set of {@link PosixFilePermission} objects matching the input + * flags. + */ + public static Set fromFlags(int flags) { + if (flags == 0) { + return Collections.emptySet(); + } + return Stream.of(PosixFilePermission.values()) + .filter(perm -> 0 != (flags & permToFlag(perm))) + .collect(Collectors.toSet()); + } + // Write string representation of permission bits to {@code sb}. private static void writeBits(StringBuilder sb, boolean r, boolean w, boolean x) { if (r) { diff --git a/src/java.base/share/classes/java/util/zip/ZipEntry.java b/src/java.base/share/classes/java/util/zip/ZipEntry.java --- a/src/java.base/share/classes/java/util/zip/ZipEntry.java +++ b/src/java.base/share/classes/java/util/zip/ZipEntry.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1995, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1995, 2018, 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,15 +25,19 @@ package java.util.zip; +import static java.util.zip.ZipConstants64.*; import static java.util.zip.ZipUtils.*; + import java.nio.file.attribute.FileTime; -import java.util.Objects; -import java.util.concurrent.TimeUnit; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.ZonedDateTime; -import java.time.ZoneId; - -import static java.util.zip.ZipConstants64.*; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; /** * This class is used to represent a ZIP file entry. @@ -56,6 +60,7 @@ long csize = -1; // compressed size of entry data int method = -1; // compression method int flag = 0; // general purpose flag + int posixPerms = -1; // posix permissions byte[] extra; // optional extra field data for entry String comment; // optional comment string for entry @@ -656,6 +661,43 @@ } /** + * Returns the set of Posix file permissions that are associated with + * this ZipEntry. This information might not be present all times, + * e.g. it is platform dependend. Also, when reading a zip file via + * {@link ZipInputStream}, the posix permissions will be null + * because data is stored in the CEN of the zip file which is not + * evaluated then. If you rely on Posix permissions, you need to access + * the zip file via {@link ZipFile}. + * + * @return The set of Posix File permissions, in case they are associated. + * + * @since 12 + */ + public Optional> getPosixPermissions() { + if (posixPerms == -1) { + return Optional.empty(); + } + return Optional.of(PosixFilePermissions.fromFlags(posixPerms)); + } + + /** + * Sets the set of Posix file permissions for this ZipEntry. + * + * @param permissions A set of PosixFilePermissions. If the value is null, + * no permission information will be stored in the zip + * file. + * + * @since 12 + */ + public void setPosixPermissions(Set permissions) { + if (permissions == null) { + posixPerms = -1; + return; + } + posixPerms = PosixFilePermissions.toFlags(permissions); + } + + /** * Returns a string representation of the ZIP entry. */ public String toString() { diff --git a/src/java.base/share/classes/java/util/zip/ZipFile.java b/src/java.base/share/classes/java/util/zip/ZipFile.java --- a/src/java.base/share/classes/java/util/zip/ZipFile.java +++ b/src/java.base/share/classes/java/util/zip/ZipFile.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1995, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1995, 2018, 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 java.util.zip; +import static java.util.zip.ZipConstants64.*; +import static java.util.zip.ZipUtils.*; + import java.io.Closeable; -import java.io.InputStream; -import java.io.IOException; import java.io.EOFException; import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.io.RandomAccessFile; import java.io.UncheckedIOException; import java.lang.ref.Cleaner.Cleanable; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.Files; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; @@ -45,8 +48,8 @@ import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; +import java.util.NoSuchElementException; import java.util.Objects; -import java.util.NoSuchElementException; import java.util.Set; import java.util.Spliterator; import java.util.Spliterators; @@ -58,6 +61,7 @@ import java.util.jar.JarFile; import java.util.stream.Stream; import java.util.stream.StreamSupport; + import jdk.internal.misc.JavaLangAccess; import jdk.internal.misc.JavaUtilZipFileAccess; import jdk.internal.misc.SharedSecrets; @@ -66,9 +70,6 @@ import jdk.internal.ref.CleanerFactory; import jdk.internal.vm.annotation.Stable; -import static java.util.zip.ZipConstants64.*; -import static java.util.zip.ZipUtils.*; - /** * This class is used to read entries from a zip file. * @@ -111,6 +112,8 @@ private static final int STORED = ZipEntry.STORED; private static final int DEFLATED = ZipEntry.DEFLATED; + static final int FILE_ATTRIBUTES_UNIX = 3; + /** * Mode flag to open a zip file for reading. */ @@ -678,6 +681,9 @@ e.size = CENLEN(cen, pos); e.csize = CENSIZ(cen, pos); e.method = CENHOW(cen, pos); + if (CENVEM_FA(cen, pos) == FILE_ATTRIBUTES_UNIX) { + e.posixPerms = CENATX_PERMS(cen, pos) & 0xFFF; // 12 bits for setuid, setgid, sticky + perms + } if (elen != 0) { int start = pos + CENHDR + nlen; e.setExtra0(Arrays.copyOfRange(cen, start, start + elen), true); diff --git a/src/java.base/share/classes/java/util/zip/ZipOutputStream.java b/src/java.base/share/classes/java/util/zip/ZipOutputStream.java --- a/src/java.base/share/classes/java/util/zip/ZipOutputStream.java +++ b/src/java.base/share/classes/java/util/zip/ZipOutputStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2018, 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,14 +25,16 @@ package java.util.zip; +import static java.util.zip.ZipConstants64.*; +import static java.util.zip.ZipUtils.*; + +import java.io.IOException; import java.io.OutputStream; -import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.HashSet; import java.util.Vector; -import java.util.HashSet; -import static java.util.zip.ZipConstants64.*; -import static java.util.zip.ZipUtils.*; + import sun.security.action.GetPropertyAction; /** @@ -67,6 +69,8 @@ } } + private static int VERSION_BASE_UNIX = ZipFile.FILE_ATTRIBUTES_UNIX << 8; + private XEntry current; private Vector xentries = new Vector<>(); private HashSet names = new HashSet<>(); @@ -81,7 +85,12 @@ private final ZipCoder zc; - private static int version(ZipEntry e) throws ZipException { + private static int version(ZipEntry e, boolean zip64) + throws ZipException + { + if (zip64) { + return 45; + } switch (e.method) { case DEFLATED: return 20; case STORED: return 10; @@ -90,6 +99,15 @@ } /** + * Adds information about compatibility of file attribute information + * to a version value. + */ + private static int versionMadeBy(ZipEntry e, int version) { + return (e.posixPerms < 0) ? version : + VERSION_BASE_UNIX | (version & 0xff); + } + + /** * Checks to make sure that this stream has not been closed. */ private void ensureOpen() throws IOException { @@ -386,12 +404,12 @@ private void writeLOC(XEntry xentry) throws IOException { ZipEntry e = xentry.entry; int flag = e.flag; - boolean hasZip64 = false; + boolean zip64 = false; int elen = getExtraLen(e.extra); writeInt(LOCSIG); // LOC header signature if ((flag & 8) == 8) { - writeShort(version(e)); // version needed to extract + writeShort(version(e, zip64)); // version needed to extract writeShort(flag); // general purpose bit flag writeShort(e.method); // compression method writeInt(e.xdostime); // last modification time @@ -402,16 +420,14 @@ writeInt(0); } else { if (e.csize >= ZIP64_MAGICVAL || e.size >= ZIP64_MAGICVAL) { - hasZip64 = true; - writeShort(45); // ver 4.5 for zip64 - } else { - writeShort(version(e)); // version needed to extract + zip64 = true; } + writeShort(version(e, zip64)); // version needed to extract writeShort(flag); // general purpose bit flag writeShort(e.method); // compression method writeInt(e.xdostime); // last modification time writeInt(e.crc); // crc-32 - if (hasZip64) { + if (zip64) { writeInt(ZIP64_MAGICVAL); writeInt(ZIP64_MAGICVAL); elen += 20; //headid(2) + size(2) + size(8) + csize(8) @@ -455,7 +471,7 @@ } writeShort(elen); writeBytes(nameBytes, 0, nameBytes.length); - if (hasZip64) { + if (zip64) { writeShort(ZIP64_EXTID); writeShort(16); writeLong(e.size); @@ -512,38 +528,33 @@ * REMIND: add support for file attributes */ private void writeCEN(XEntry xentry) throws IOException { - ZipEntry e = xentry.entry; + ZipEntry e = xentry.entry; int flag = e.flag; - int version = version(e); long csize = e.csize; long size = e.size; long offset = xentry.offset; int elenZIP64 = 0; - boolean hasZip64 = false; - + boolean zip64 = false; if (e.csize >= ZIP64_MAGICVAL) { csize = ZIP64_MAGICVAL; - elenZIP64 += 8; // csize(8) - hasZip64 = true; + elenZIP64 += 8; // csize(8) + zip64 = true; } if (e.size >= ZIP64_MAGICVAL) { size = ZIP64_MAGICVAL; // size(8) elenZIP64 += 8; - hasZip64 = true; + zip64 = true; } if (xentry.offset >= ZIP64_MAGICVAL) { offset = ZIP64_MAGICVAL; - elenZIP64 += 8; // offset(8) - hasZip64 = true; + elenZIP64 += 8; // offset(8) + zip64 = true; } + int version = version(e, zip64); + writeInt(CENSIG); // CEN header signature - if (hasZip64) { - writeShort(45); // ver 4.5 for zip64 - writeShort(45); - } else { - writeShort(version); // version made by - writeShort(version); // version needed to extract - } + writeShort(versionMadeBy(e, version)); // version made by + writeShort(version); // version needed to extract writeShort(flag); // general purpose bit flag writeShort(e.method); // compression method writeInt(e.xdostime); // last modification time @@ -554,8 +565,8 @@ writeShort(nameBytes.length); int elen = getExtraLen(e.extra); - if (hasZip64) { - elen += (elenZIP64 + 4);// + headid(2) + datasize(2) + if (zip64) { + elen += (elenZIP64 + 4); // + headid(2) + datasize(2) } // cen info-zip extended timestamp only outputs mtime // but set the flag for a/ctime, if present in loc @@ -598,12 +609,14 @@ } writeShort(0); // starting disk number writeShort(0); // internal file attributes (unused) - writeInt(0); // external file attributes (unused) + writeInt(e.posixPerms > 0 ? e.posixPerms << 16 : 0); // external file + // attributes, used for storing posix + // permissions writeInt(offset); // relative offset of local header writeBytes(nameBytes, 0, nameBytes.length); // take care of EXTID_ZIP64 and EXTID_EXTT - if (hasZip64) { + if (zip64) { writeShort(ZIP64_EXTID);// Zip64 extra writeShort(elenZIP64); if (size == ZIP64_MAGICVAL) @@ -650,25 +663,25 @@ * Writes end of central directory (END) header. */ private void writeEND(long off, long len) throws IOException { - boolean hasZip64 = false; + boolean zip64 = false; long xlen = len; long xoff = off; if (xlen >= ZIP64_MAGICVAL) { xlen = ZIP64_MAGICVAL; - hasZip64 = true; + zip64 = true; } if (xoff >= ZIP64_MAGICVAL) { xoff = ZIP64_MAGICVAL; - hasZip64 = true; + zip64 = true; } int count = xentries.size(); if (count >= ZIP64_MAGICCOUNT) { - hasZip64 |= !inhibitZip64; - if (hasZip64) { + zip64 |= !inhibitZip64; + if (zip64) { count = ZIP64_MAGICCOUNT; } } - if (hasZip64) { + if (zip64) { long off64 = written; //zip64 end of central directory record writeInt(ZIP64_ENDSIG); // zip64 END record signature diff --git a/src/java.base/share/classes/java/util/zip/ZipUtils.java b/src/java.base/share/classes/java/util/zip/ZipUtils.java --- a/src/java.base/share/classes/java/util/zip/ZipUtils.java +++ b/src/java.base/share/classes/java/util/zip/ZipUtils.java @@ -25,7 +25,8 @@ package java.util.zip; -import java.nio.Buffer; +import static java.util.zip.ZipConstants.ENDHDR; + import java.nio.ByteBuffer; import java.nio.file.attribute.FileTime; import java.security.AccessController; @@ -37,10 +38,7 @@ import java.util.Date; import java.util.concurrent.TimeUnit; -import static java.util.zip.ZipConstants.ENDHDR; - import jdk.internal.misc.Unsafe; -import sun.nio.ch.DirectBuffer; class ZipUtils { @@ -257,6 +255,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);} @@ -270,6 +269,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.jartool/share/classes/sun/tools/jar/GNUStyleOptions.java b/src/jdk.jartool/share/classes/sun/tools/jar/GNUStyleOptions.java --- a/src/jdk.jartool/share/classes/sun/tools/jar/GNUStyleOptions.java +++ b/src/jdk.jartool/share/classes/sun/tools/jar/GNUStyleOptions.java @@ -28,6 +28,7 @@ import java.io.File; import java.io.PrintWriter; import java.lang.module.ModuleDescriptor.Version; +import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; import java.util.regex.Pattern; @@ -187,6 +188,11 @@ jartool.flag0 = true; } }, + new Option(true, OptionType.ANY, "--preserve-posix", "-o") { + void process(Main jartool, String opt, String arg) { + jartool.oflag = FileSystems.getDefault().supportedFileAttributeViews().contains("posix"); + } + }, // Hidden options new Option(false, OptionType.OTHER, "-P") { diff --git a/src/jdk.jartool/share/classes/sun/tools/jar/Main.java b/src/jdk.jartool/share/classes/sun/tools/jar/Main.java --- a/src/jdk.jartool/share/classes/sun/tools/jar/Main.java +++ b/src/jdk.jartool/share/classes/sun/tools/jar/Main.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2018, 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,6 +25,11 @@ package sun.tools.jar; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static java.util.jar.JarFile.MANIFEST_NAME; +import static java.util.stream.Collectors.joining; +import static jdk.internal.util.jar.JarIndex.INDEX_NAME; + import java.io.*; import java.lang.module.Configuration; import java.lang.module.FindException; @@ -44,6 +49,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermission; import java.text.MessageFormat; import java.util.*; import java.util.function.Consumer; @@ -59,6 +65,7 @@ import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; + import jdk.internal.module.Checks; import jdk.internal.module.ModuleHashes; import jdk.internal.module.ModuleHashesBuilder; @@ -68,11 +75,6 @@ import jdk.internal.module.ModuleTarget; import jdk.internal.util.jar.JarIndex; -import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; -import static java.util.jar.JarFile.MANIFEST_NAME; -import static java.util.stream.Collectors.joining; -import static jdk.internal.util.jar.JarIndex.INDEX_NAME; - /** * This class implements a simple utility for creating files in the JAR * (Java Archive) file format. The JAR format is based on the ZIP file @@ -149,8 +151,9 @@ * nflag: Perform jar normalization at the end * pflag: preserve/don't strip leading slash and .. component from file name * dflag: print module descriptor + * oflag: preserve Posix file attributes */ - boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag, nflag, pflag, dflag; + boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag, nflag, pflag, dflag, oflag; boolean suppressDeprecateMsg = false; @@ -399,7 +402,10 @@ // latter can handle it. String[] files = filesMapToFiles(filesMap); - if (fname != null && files != null) { + // if we need to restore posix permissions (-o flag), we need to use + // the ZipFile approach because permissions are stored in the CEN + // which is not read when using a ZipInputStream. + if (fname != null && (files != null || oflag)) { extract(fname, files); } else { InputStream in = (fname == null) @@ -1206,6 +1212,9 @@ } else if (flag0) { crc32File(e, file); } + if (oflag) { + e.setPosixPermissions(Files.getPosixFilePermissions(file.toPath())); + } zos.putNextEntry(e); if (!isDir) { copy(file, zos); @@ -1472,6 +1481,16 @@ f.setLastModified(lastModified); } } + if (oflag) { + Optional> permissions = e.getPosixPermissions(); + if (permissions.isPresent()) { + try { + Files.setPosixFilePermissions(f.toPath(), permissions.get()); + } catch (UnsupportedOperationException exc) { + // Ignore the exception + } + } + } return rc; } diff --git a/src/jdk.jartool/share/classes/sun/tools/jar/resources/jar.properties b/src/jdk.jartool/share/classes/sun/tools/jar/resources/jar.properties --- a/src/jdk.jartool/share/classes/sun/tools/jar/resources/jar.properties +++ b/src/jdk.jartool/share/classes/sun/tools/jar/resources/jar.properties @@ -249,6 +249,9 @@ \ of the jar (i.e. META-INF/versions/VERSION/) main.help.opt.any.verbose=\ \ -v, --verbose Generate verbose output on standard output +main.help.opt.any.preserve-posix=\ +\ -o, --preserve-posix Preserve Posix file permissions when working on a\n\ +\ Posix file system main.help.opt.create=\ \ Operation modifiers valid only in create mode:\n main.help.opt.create.normalize=\ 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, 2014, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2018, 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 @@ -26,10 +26,8 @@ package jdk.nio.zipfs; /** - * * @author Xueming Shen */ - class ZipConstants { /* * Compression methods @@ -232,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);} @@ -245,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, 2014, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2018, 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,17 +25,22 @@ package jdk.nio.zipfs; -import java.nio.file.attribute.*; import java.io.IOException; +import java.nio.file.attribute.BasicFileAttributeView; +import java.nio.file.attribute.FileAttributeView; +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 +/** + * @author Xueming Shen, Rajendra Gutupalli, Jaya Hangal */ - -class ZipFileAttributeView implements BasicFileAttributeView -{ +class ZipFileAttributeView implements PosixFileAttributeView { private static enum AttrID { size, creationTime, @@ -48,15 +53,22 @@ fileKey, compressedSize, crc, - method + method, + permissions }; + private static enum ViewType { + zip, + posix, + basic + } + 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 @@ -64,9 +76,11 @@ if (type == null) throw new NullPointerException(); if (type == BasicFileAttributeView.class) - return (V)new ZipFileAttributeView(path, false); + return (V)new ZipFileAttributeView(path, ViewType.basic); + if (type == PosixFileAttributeView.class) + return (V)new ZipFileAttributeView(path, ViewType.posix); if (type == ZipFileAttributeView.class) - return (V)new ZipFileAttributeView(path, true); + return (V)new ZipFileAttributeView(path, ViewType.zip); return null; } @@ -74,19 +88,28 @@ if (type == null) throw new NullPointerException(); if (type.equals("basic")) - return new ZipFileAttributeView(path, false); + return new ZipFileAttributeView(path, ViewType.basic); + if (type.equals("posix")) + return new ZipFileAttributeView(path, ViewType.posix); if (type.equals("zip")) - return new ZipFileAttributeView(path, true); + return new ZipFileAttributeView(path, ViewType.zip); return null; } @Override public String name() { - return isZipView ? "zip" : "basic"; + switch (type) { + case zip: + return "zip"; + case posix: + return "posix"; + case basic: + default: + return "basic"; + } } - public ZipFileAttributes readAttributes() throws IOException - { + public ZipFileAttributes readAttributes() throws IOException { return path.getAttributes(); } @@ -104,11 +127,11 @@ { try { if (AttrID.valueOf(attribute) == AttrID.lastModifiedTime) - setTimes ((FileTime)value, null, null); + setTimes((FileTime)value, null, null); if (AttrID.valueOf(attribute) == AttrID.lastAccessTime) - setTimes (null, (FileTime)value, null); + setTimes(null, (FileTime)value, null); if (AttrID.valueOf(attribute) == AttrID.creationTime) - setTimes (null, null, (FileTime)value); + setTimes(null, null, (FileTime)value); return; } catch (IllegalArgumentException x) {} throw new UnsupportedOperationException("'" + attribute + @@ -158,18 +181,47 @@ case fileKey: return zfas.fileKey(); 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 permissions: + if (type == ViewType.zip || type == ViewType.posix) { + try { + return zfas.permissions(); + } catch (UnsupportedOperationException e) { + return null; + } + } + break; } return null; } + + @Override + public UserPrincipal getOwner() throws IOException { + throw new UnsupportedOperationException("ZipFileSystem does not support getOwner."); + } + + @Override + public void setOwner(UserPrincipal owner) throws IOException { + throw new UnsupportedOperationException("ZipFileSystem does not support setOwner."); + } + + @Override + public void setPermissions(Set perms) throws IOException { + readAttributes().setPermissions(perms); + } + + @Override + public void setGroup(GroupPrincipal group) throws IOException { + throw new UnsupportedOperationException("ZipFileSystem does not support setGroup."); + } } 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, 2014, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2018, 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.Set; /** + * The attributes of a file stored in a zip file. * - * @author Xueming Shen, Rajendra Gutupalli,Jaya Hangal + * @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 void setPermissions(Set perms); } 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, 2014, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2018, 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 @@ -26,21 +26,18 @@ package jdk.nio.zipfs; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.FileStore; import java.nio.file.FileSystems; +import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.BasicFileAttributeView; import java.nio.file.attribute.FileAttributeView; import java.nio.file.attribute.FileStoreAttributeView; -import java.nio.file.attribute.BasicFileAttributeView; -import java.util.Formatter; +import java.nio.file.attribute.PosixFileAttributeView; -/* - * - * @author Xueming Shen, Rajendra Gutupalli, Jaya Hangal +/** + * @author Xueming Shen, Rajendra Gutupalli, Jaya Hangal */ - class ZipFileStore extends FileStore { private final ZipFileSystem zfs; @@ -67,16 +64,16 @@ @Override public boolean supportsFileAttributeView(Class type) { return (type == BasicFileAttributeView.class || + type == PosixFileAttributeView.class || type == ZipFileAttributeView.class); } @Override public boolean supportsFileAttributeView(String name) { - return name.equals("basic") || name.equals("zip"); + return name.equals("basic") || name.equals("posix") || name.equals("zip"); } @Override - @SuppressWarnings("unchecked") public V getFileStoreAttributeView(Class type) { if (type == null) throw new NullPointerException(); 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2017, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2018, 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,49 +25,69 @@ package jdk.nio.zipfs; +import static java.lang.Boolean.TRUE; +import static jdk.nio.zipfs.ZipConstants.*; +import static jdk.nio.zipfs.ZipUtils.*; +import static java.nio.file.StandardOpenOption.*; +import static java.nio.file.StandardCopyOption.*; + import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.EOFException; -import java.io.File; import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; -import java.nio.channels.*; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; import java.nio.file.*; -import java.nio.file.attribute.*; -import java.nio.file.spi.*; +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; import java.security.PrivilegedExceptionAction; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Formatter; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.regex.Pattern; import java.util.zip.CRC32; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; import java.util.zip.Inflater; -import java.util.zip.Deflater; import java.util.zip.InflaterInputStream; -import java.util.zip.DeflaterOutputStream; import java.util.zip.ZipException; -import static java.lang.Boolean.*; -import static jdk.nio.zipfs.ZipConstants.*; -import static jdk.nio.zipfs.ZipUtils.*; -import static java.nio.file.StandardOpenOption.*; -import static java.nio.file.StandardCopyOption.*; /** * A FileSystem built on a zip file * * @author Xueming Shen */ - class ZipFileSystem extends FileSystem { - + private static final int FILE_ATTRIBUTES_UNIX = 3; + private static final int VERSION_BASE_UNIX = FILE_ATTRIBUTES_UNIX << 8; private final ZipFileSystemProvider provider; private final Path zfpath; final ZipCoder zc; @@ -435,7 +455,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(); @@ -657,7 +677,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(); @@ -722,7 +742,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); @@ -1468,7 +1488,7 @@ // TBD: wrap to hook close() // streams.add(eis); return eis; - } else { // untouced CEN or COPY + } else { // untouched CEN or COPY eis = new EntryInputStream(e, ch); } if (e.method == METHOD_DEFLATED) { @@ -1530,14 +1550,12 @@ // point to a new channel after sync() private long pos; // current position within entry data protected long rem; // number of remaining bytes within entry - protected final long size; // uncompressed size of this entry EntryInputStream(Entry e, SeekableByteChannel zfch) throws IOException { this.zfch = zfch; rem = e.csize; - size = e.size; pos = e.locoff; if (pos == -1) { Entry e2 = getEntry(e.name); @@ -1604,10 +1622,6 @@ return rem > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) rem; } - public long size() { - return size; - } - public void close() { rem = 0; streams.remove(this); @@ -1663,7 +1677,7 @@ // List of available Deflater objects for compression private final List deflaters = new ArrayList<>(); - // Gets an deflater from the list of available deflaters or allocates + // Gets a deflater from the list of available deflaters or allocates // a new one. private Deflater getDeflater() { synchronized (deflaters) { @@ -1677,18 +1691,6 @@ } } - // Releases the specified inflater to the list of available inflaters. - private void releaseDeflater(Deflater def) { - synchronized (deflaters) { - if (inflaters.size() < MAX_FLATER) { - def.reset(); - deflaters.add(def); - } else { - def.end(); - } - } - } - // End of central directory record static class END { // these 2 fields are not used by anyone and write() uses "0" @@ -1856,6 +1858,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 @@ -1867,7 +1870,7 @@ // cen - // these fields are not used by anyone and writeCEN uses "0" + // these fields are not used // int versionMade; // int disk; // int attrs; @@ -1887,12 +1890,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") || attrName.equals("unix:permissions")) { + posixPerms = PosixFilePermissions.toFlags((Set)attr.value()); + } + } } - Entry (Entry e, int type) { + Entry(Entry e, int type) { name(e.name); this.isdir = e.isdir; this.version = e.version; @@ -1912,15 +1922,26 @@ */ this.locoff = e.locoff; this.comment = e.comment; + this.posixPerms = e.posixPerms; 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") || attrName.equals("unix:permissions")) { + posixPerms = PosixFilePermissions.toFlags((Set)attr.value()); + } + } } - int version() throws ZipException { + int version(boolean zip64) throws ZipException { + if (zip64) { + return 45; + } if (method == METHOD_DEFLATED) return 20; else if (method == METHOD_STORED) @@ -1928,6 +1949,15 @@ 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 ////////////////////// static Entry readCEN(ZipFileSystem zipfs, IndexNode inode) throws IOException @@ -1958,6 +1988,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; @@ -1976,10 +2009,7 @@ return this; } - int writeCEN(OutputStream os) throws IOException - { - int written = CENHDR; - int version0 = version(); + int writeCEN(OutputStream os) throws IOException { long csize0 = csize; long size0 = size; long locoff0 = locoff; @@ -2010,6 +2040,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); @@ -2026,13 +2058,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 @@ -2050,10 +2077,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) @@ -2093,18 +2122,17 @@ int writeLOC(OutputStream os) throws IOException { writeInt(os, LOCSIG); // LOC header signature - int version = 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; if ((flag & FLAG_DATADESCR) != 0) { - writeShort(os, version()); // 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 @@ -2117,16 +2145,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, version()); // 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 { @@ -2156,7 +2183,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); @@ -2409,9 +2436,46 @@ 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(); } + + @Override + public UserPrincipal owner() { + throw new UnsupportedOperationException( + "ZipFileSystem does not support owner."); + } + + @Override + public GroupPrincipal group() { + throw new UnsupportedOperationException( + "ZipFileSystem does not support group."); + } + + @Override + public Set permissions() { + if (posixPerms == -1) { + // in case there are no Posix permissions associated with the + // entry, we should not return an empty set of permissions + // because that would be an explicit set of permissions meaning + // no permissions for anyone + throw new UnsupportedOperationException( + "No posix permissions associated with zip entry."); + } + return PosixFilePermissions.fromFlags(posixPerms); + } + + @Override + public void setPermissions(Set perms) { + if (perms == null) { + posixPerms = -1; + return; + } + posixPerms = PosixFilePermissions.toFlags(perms); + } } // ZIP directory has two issues: @@ -2421,7 +2485,6 @@ // structure. // A possible solution is to build the node tree ourself as // implemented below. - private IndexNode root; // default time stamp for pseudo entries private long zfsDefaultTimeStamp = System.currentTimeMillis(); 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 @@ -25,28 +25,44 @@ package jdk.nio.zipfs; -import java.io.*; -import java.nio.channels.*; -import java.nio.file.*; -import java.nio.file.DirectoryStream.Filter; -import java.nio.file.attribute.*; -import java.nio.file.spi.FileSystemProvider; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; +import java.nio.channels.AsynchronousFileChannel; +import java.nio.channels.FileChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.AccessMode; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryStream; +import java.nio.file.DirectoryStream.Filter; +import java.nio.file.FileStore; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.ProviderMismatchException; +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.util.HashMap; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutorService; import java.util.zip.ZipException; -import java.util.concurrent.ExecutorService; -/* - * - * @author Xueming Shen, Rajendra Gutupalli, Jaya Hangal +/** + * @author Xueming Shen, Rajendra Gutupalli, Jaya Hangal */ - public class ZipFileSystemProvider extends FileSystemProvider { - private final Map filesystems = new HashMap<>(); public ZipFileSystemProvider() {} @@ -202,7 +218,6 @@ } @Override - @SuppressWarnings("unchecked") public V getFileAttributeView(Path path, Class type, LinkOption... options) { @@ -286,7 +301,9 @@ readAttributes(Path path, Class type, LinkOption... options) throws IOException { - if (type == BasicFileAttributes.class || type == ZipFileAttributes.class) + if (type == BasicFileAttributes.class || + type == PosixFileAttributes.class || + type == ZipFileAttributes.class) return (A)toZipPath(path).getAttributes(); return null; } diff --git a/test/jdk/java/util/zip/ZipFile/TestPosixPerms.java b/test/jdk/java/util/zip/ZipFile/TestPosixPerms.java new file mode 100644 --- /dev/null +++ b/test/jdk/java/util/zip/ZipFile/TestPosixPerms.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2018, 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. + */ + +/** + * @test + * @run testng TestPosixPerms + * @summary Test zip file operations handling posix permissions. + */ + +import static java.nio.file.attribute.PosixFilePermission.*; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Collections; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +import org.testng.annotations.Test; + +public class TestPosixPerms { + private static final int NUMBER_OF_ENTRIES_IN_POSIXTEST_ZIP = 10; + + private int entries; + + private static void checkPermissionsOfEntry(String name, ZipEntry ze, boolean directory, Set expected) { + System.out.print("Checking " + name + "..."); + assertEquals(ze.isDirectory(), directory, "Unexpected directory attribute."); + Set permissions = ze.getPosixPermissions().orElse(null); + if (expected == null) { + assertEquals(permissions, null, "Nonempty posix permissions associated with entry."); + System.out.println(); + } else { + assertNotEquals(permissions, null, "No posix permissions associated with entry."); + System.out.println("[" + PosixFilePermissions.toString(permissions) + "]"); + assertEquals(permissions.size(), expected.size(), "Unexpected number of permissions."); + for (PosixFilePermission p : expected) { + assertTrue(permissions.contains(p), "Posix permission " + p + " missing."); + } + } + } + + private void putEntry(ZipOutputStream zos, String name, Set perms) throws IOException { + ZipEntry e = new ZipEntry(name); + if (perms != null) { + e.setPosixPermissions(perms); + } + zos.putNextEntry(e); + entries++; + } + + @Test + public void readCheckedInArchiveWithPosixPerms() throws Exception { + File zipFile = + new File(System.getProperty("test.src", "."), "posixtest.zip"); + System.out.println("Testing " + zipFile.getAbsolutePath() + "..."); + try (ZipFile zf = new ZipFile(zipFile)) { + int size = zf.size(); + System.out.println("Number of entries: " + size + "..."); + assertEquals(size, NUMBER_OF_ENTRIES_IN_POSIXTEST_ZIP, "File contained wrong number of entries."); + zf.stream().forEach((ze)->{ + String name = ze.getName(); + if (name.startsWith("dir")) { + checkPermissionsOfEntry(name, ze, true, Set.of( + OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, + GROUP_READ, GROUP_WRITE, GROUP_EXECUTE, + OTHERS_READ, OTHERS_WRITE, OTHERS_EXECUTE)); + } else if (name.equals("uread")) { + checkPermissionsOfEntry(name, ze, false, Set.of(OWNER_READ)); + } else if (name.equals("uwrite")) { + checkPermissionsOfEntry(name, ze, false, Set.of(OWNER_WRITE)); + } else if (name.equals("uexec")) { + checkPermissionsOfEntry(name, ze, false, Set.of(OWNER_EXECUTE)); + } else if (name.equals("gread")) { + checkPermissionsOfEntry(name, ze, false, Set.of(GROUP_READ)); + } else if (name.equals("gwrite")) { + checkPermissionsOfEntry(name, ze, false, Set.of(GROUP_WRITE)); + } else if (name.equals("gexec")) { + checkPermissionsOfEntry(name, ze, false, Set.of(GROUP_EXECUTE)); + } else if (name.equals("oread")) { + checkPermissionsOfEntry(name, ze, false, Set.of(OTHERS_READ)); + } else if (name.equals("owrite")) { + checkPermissionsOfEntry(name, ze, false, Set.of(OTHERS_WRITE)); + } else if (name.equals("oexec")) { + checkPermissionsOfEntry(name, ze, false, Set.of(OTHERS_EXECUTE)); + } else { + fail("Found unknown entry " + name + "."); + } + }); + } + } + + @Test + public void testWriteAndReadArchiveWithPosixPerms() throws Exception { + File zfile = Paths.get(System.getProperty("test.dir", "."), "testPosixPerms.zip").toFile(); + System.out.println("Create " + zfile.getAbsolutePath() + "..."); + if (zfile.exists()) { + zfile.delete(); + } + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zfile)); + entries = 0; + putEntry(zos, "dir/", Set.of( + OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, + GROUP_READ, GROUP_WRITE, GROUP_EXECUTE, + OTHERS_READ, OTHERS_WRITE, OTHERS_EXECUTE)); + putEntry(zos, "uread", Set.of(OWNER_READ)); + putEntry(zos, "uwrite", Set.of(OWNER_WRITE)); + putEntry(zos, "uexec", Set.of(OWNER_EXECUTE)); + putEntry(zos, "gread", Set.of(GROUP_READ)); + putEntry(zos, "gwrite", Set.of(GROUP_WRITE)); + putEntry(zos, "gexec", Set.of(GROUP_EXECUTE)); + putEntry(zos, "oread", Set.of(OTHERS_READ)); + putEntry(zos, "owrite", Set.of(OTHERS_WRITE)); + putEntry(zos, "oexec", Set.of(OTHERS_EXECUTE)); + putEntry(zos, "emptyperms", Collections.emptySet()); + putEntry(zos, "noperms", null); + zos.close(); + + System.out.println("Test reading " + zfile.getAbsolutePath() + "..."); + try (ZipFile zf = new ZipFile(zfile)) { + int size = zf.size(); + System.out.println("Number of entries: " + size + "..."); + assertEquals(size, entries, "File contained wrong number of entries."); + zf.stream().forEach((ze)->{ + String name = ze.getName(); + if (name.startsWith("dir")) { + checkPermissionsOfEntry(name, ze, true, Set.of( + OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, + GROUP_READ, GROUP_WRITE, GROUP_EXECUTE, + OTHERS_READ, OTHERS_WRITE, OTHERS_EXECUTE)); + } else if (name.equals("uread")) { + checkPermissionsOfEntry(name, ze, false, Set.of(OWNER_READ)); + } else if (name.equals("uwrite")) { + checkPermissionsOfEntry(name, ze, false, Set.of(OWNER_WRITE)); + } else if (name.equals("uexec")) { + checkPermissionsOfEntry(name, ze, false, Set.of(OWNER_EXECUTE)); + } else if (name.equals("gread")) { + checkPermissionsOfEntry(name, ze, false, Set.of(GROUP_READ)); + } else if (name.equals("gwrite")) { + checkPermissionsOfEntry(name, ze, false, Set.of(GROUP_WRITE)); + } else if (name.equals("gexec")) { + checkPermissionsOfEntry(name, ze, false, Set.of(GROUP_EXECUTE)); + } else if (name.equals("oread")) { + checkPermissionsOfEntry(name, ze, false, Set.of(OTHERS_READ)); + } else if (name.equals("owrite")) { + checkPermissionsOfEntry(name, ze, false, Set.of(OTHERS_WRITE)); + } else if (name.equals("oexec")) { + checkPermissionsOfEntry(name, ze, false, Set.of(OTHERS_EXECUTE)); + } else if (name.equals("emptyperms")) { + checkPermissionsOfEntry(name, ze, false, Collections.emptySet()); + } else if (name.equals("noperms")) { + checkPermissionsOfEntry(name, ze, false, null); + } else { + fail("Found unknown entry " + name + "."); + } + }); + } + } +} diff --git a/test/jdk/java/util/zip/ZipFile/posixtest.zip b/test/jdk/java/util/zip/ZipFile/posixtest.zip new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..81c358077cea2d3a581925450f84aa47e3e03c3c GIT binary patch literal 1406 zc$|HbO-sWt7=V+u89FcXB0GqB_9BP}LGa@&g~D`~!f?1CI~9v%MGzwX1dqE2{tNN% zC<+RC6a;6`lf`QuzB6-q83cA703c?*ZGR z_$SZynVmicG^X%pA8w~l08J?T#WQ_|(|3UFP(0t;bha<-^!e>ws_&Wjr)T=YPA_3+ zN$tKgwY#3a!0E@Z_1L#P4_3li<+bal7W+BStLadf4hfT6;l#w*oZ5x8Zc>;g36)zU zs@Bw%R+(=kOrM0xtrJx(^{7?#_Y$U6!sV8VF0Q^|&Fl%i&7#V!7FChj;cU{dFbxwb zw_sES>Qbxlgx;3X<<^X@ef0pY!k*CEHLBdQQN^ymw91~)+c>J+%G>r?sqKK-=7nkA Rafyb0w+X)wc)JYrzh9!D(nJ6N diff --git a/test/jdk/jdk/nio/zipfs/TestPosixPerms.java b/test/jdk/jdk/nio/zipfs/TestPosixPerms.java new file mode 100644 --- /dev/null +++ b/test/jdk/jdk/nio/zipfs/TestPosixPerms.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2018, 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. + */ + +/** + * @test + * @modules jdk.zipfs + * @run testng TestPosixPerms + * @summary Test zip file operations handling posix permissions. + */ + +import static java.nio.file.attribute.PosixFilePermission.*; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.nio.file.spi.FileSystemProvider; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.testng.annotations.Test; + +public class TestPosixPerms { + + private static final int NUMBER_OF_ENTRIES_IN_POSIXTEST_ZIP = 10; + + private int entries; + + private static FileSystemProvider getZipFSProvider() { + for (FileSystemProvider provider : FileSystemProvider.installedProviders()) { + if ("jar".equals(provider.getScheme())) + return provider; + } + return null; + } + + private void checkPermissionsOfEntry(Path file, boolean directory, Set expected) { + System.out.println("Checking " + file + "..."); + assertEquals(Files.isDirectory(file), directory, "Unexpected directory attribute."); + try { + System.out.println(Files.readAttributes(file, PosixFileAttributes.class).toString()); + } catch (IOException e) { + fail("Failed to list file attributes (posix) for entry.", e); + } + try { + Set permissions = Files.getPosixFilePermissions(file); + assertNotEquals(permissions, null, "No posix permissions associated with entry."); + assertNotEquals(expected, null, "Got a set of " + permissions.size() + + " permissions but expected null/UnsupportedOperationException."); + assertEquals(permissions.size(), expected.size(), "Unexpected number of permissions."); + for (PosixFilePermission p : expected) { + assertTrue(permissions.contains(p), "Posix permission " + p + " missing."); + } + } catch (UnsupportedOperationException e) { + if (expected != null) { + fail("Unexpected: No posix permissions associated with entry."); + } + } catch (IOException e) { + fail("Caught unexpected exception obtaining posix file permissions.", e); + } + } + + private void putFile(FileSystem fs, String name, Set perms) throws IOException { + if (perms == null) { + Files.createFile(fs.getPath(name)); + } else { + Files.createFile(fs.getPath(name), PosixFilePermissions.asFileAttribute(perms)); + } + entries++; + } + + private void putDirectory(FileSystem fs, String name, Set perms) throws IOException { + if (perms == null) { + Files.createDirectory(fs.getPath(name)); + } else { + Files.createDirectory(fs.getPath(name), PosixFilePermissions.asFileAttribute(perms)); + } + entries++; + } + + @Test + public void readCheckedInArchiveWithPosixPerms() throws Exception { + FileSystemProvider provider = getZipFSProvider(); + assertNotNull(provider, "ZIP filesystem provider is not installed"); + try (FileSystem fs = provider.newFileSystem(Paths.get(System.getProperty("test.src", "."), "posixtest.zip"), + Collections.emptyMap())) { + System.out.println("Testing " + fs + "..."); + entries = 0; + try (DirectoryStream paths = Files.newDirectoryStream(fs.getPath("/"))) { + paths.forEach((file)->{ + entries++; + String name = file.getFileName().toString(); + if (name.startsWith("dir")) { + checkPermissionsOfEntry(file, true, Set.of( + OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, + GROUP_READ, GROUP_WRITE, GROUP_EXECUTE, + OTHERS_READ, OTHERS_WRITE, OTHERS_EXECUTE)); + } else if (name.equals("uread")) { + checkPermissionsOfEntry(file, false, Set.of(OWNER_READ)); + } else if (name.equals("uwrite")) { + checkPermissionsOfEntry(file, false, Set.of(OWNER_WRITE)); + } else if (name.equals("uexec")) { + checkPermissionsOfEntry(file, false, Set.of(OWNER_EXECUTE)); + } else if (name.equals("gread")) { + checkPermissionsOfEntry(file, false, Set.of(GROUP_READ)); + } else if (name.equals("gwrite")) { + checkPermissionsOfEntry(file, false, Set.of(GROUP_WRITE)); + } else if (name.equals("gexec")) { + checkPermissionsOfEntry(file, false, Set.of(GROUP_EXECUTE)); + } else if (name.equals("oread")) { + checkPermissionsOfEntry(file, false, Set.of(OTHERS_READ)); + } else if (name.equals("owrite")) { + checkPermissionsOfEntry(file, false, Set.of(OTHERS_WRITE)); + } else if (name.equals("oexec")) { + checkPermissionsOfEntry(file, false, Set.of(OTHERS_EXECUTE)); + } else { + fail("Found unknown entry " + name + "."); + } + }); + } + System.out.println("Number of entries: " + entries + "."); + assertEquals(entries, NUMBER_OF_ENTRIES_IN_POSIXTEST_ZIP, "File contained wrong number of entries."); + } + } + + @Test + public void testWriteAndReadArchiveWithPosixPerms() throws Exception { + FileSystemProvider provider = getZipFSProvider(); + assertNotNull(provider, "ZIP filesystem provider is not installed"); + Path zpath = Paths.get(System.getProperty("test.dir", "."), "testPosixPerms.zip"); + System.out.println("Create " + zpath + "..."); + if (Files.exists(zpath)) { + Files.delete(zpath); + } + Map env = new HashMap<>(); + env.put("create", "true"); + try (FileSystem fs = provider.newFileSystem(zpath, env)) { + entries = 0; + putDirectory(fs, "dir", Set.of( + OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, + GROUP_READ, GROUP_WRITE, GROUP_EXECUTE, + OTHERS_READ, OTHERS_WRITE, OTHERS_EXECUTE)); + putFile(fs, "uread", Set.of(OWNER_READ)); + putFile(fs, "uwrite", Set.of(OWNER_WRITE)); + putFile(fs, "uexec", Set.of(OWNER_EXECUTE)); + putFile(fs, "gread", Set.of(GROUP_READ)); + putFile(fs, "gwrite", Set.of(GROUP_WRITE)); + putFile(fs, "gexec", Set.of(GROUP_EXECUTE)); + putFile(fs, "oread", Set.of(OTHERS_READ)); + putFile(fs, "owrite", Set.of(OTHERS_WRITE)); + putFile(fs, "oexec", Set.of(OTHERS_EXECUTE)); + putFile(fs, "emptyperms", Collections.emptySet()); + putFile(fs, "noperms", null); + putFile(fs, "permsaddedlater", null); + Files.setPosixFilePermissions(fs.getPath("permsaddedlater"), Set.of(OWNER_READ)); + } + int entriesCreated = entries; + + System.out.println("Test reading " + zpath + "..."); + env.clear(); + try (FileSystem fs = provider.newFileSystem(zpath, env)) { + entries = 0; + try (DirectoryStream paths = Files.newDirectoryStream(fs.getPath("/"))) { + paths.forEach((file)->{ + entries++; + String name = file.getFileName().toString(); + if (name.startsWith("dir")) { + checkPermissionsOfEntry(file, true, Set.of( + OWNER_READ, OWNER_WRITE, OWNER_EXECUTE, + GROUP_READ, GROUP_WRITE, GROUP_EXECUTE, + OTHERS_READ, OTHERS_WRITE, OTHERS_EXECUTE)); + } else if (name.equals("uread")) { + checkPermissionsOfEntry(file, false, Set.of(OWNER_READ)); + } else if (name.equals("uwrite")) { + checkPermissionsOfEntry(file, false, Set.of(OWNER_WRITE)); + } else if (name.equals("uexec")) { + checkPermissionsOfEntry(file, false, Set.of(OWNER_EXECUTE)); + } else if (name.equals("gread")) { + checkPermissionsOfEntry(file, false, Set.of(GROUP_READ)); + } else if (name.equals("gwrite")) { + checkPermissionsOfEntry(file, false, Set.of(GROUP_WRITE)); + } else if (name.equals("gexec")) { + checkPermissionsOfEntry(file, false, Set.of(GROUP_EXECUTE)); + } else if (name.equals("oread")) { + checkPermissionsOfEntry(file, false, Set.of(OTHERS_READ)); + } else if (name.equals("owrite")) { + checkPermissionsOfEntry(file, false, Set.of(OTHERS_WRITE)); + } else if (name.equals("oexec")) { + checkPermissionsOfEntry(file, false, Set.of(OTHERS_EXECUTE)); + } else if (name.equals("emptyperms")) { + checkPermissionsOfEntry(file, false, Collections.emptySet()); + } else if (name.equals("noperms")) { + checkPermissionsOfEntry(file, false, null); + } else if (name.equals("permsaddedlater")) { + checkPermissionsOfEntry(file, false, Set.of(OWNER_READ)); + } else { + fail("Found unknown entry " + name + "."); + } + }); + } + } + System.out.println("Number of entries: " + entries + "."); + assertEquals(entries, entriesCreated, "File contained wrong number of entries."); + } +} diff --git a/test/jdk/jdk/nio/zipfs/posixtest.zip b/test/jdk/jdk/nio/zipfs/posixtest.zip new file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..81c358077cea2d3a581925450f84aa47e3e03c3c GIT binary patch literal 1406 zc$|HbO-sWt7=V+u89FcXB0GqB_9BP}LGa@&g~D`~!f?1CI~9v%MGzwX1dqE2{tNN% zC<+RC6a;6`lf`QuzB6-q83cA703c?*ZGR z_$SZynVmicG^X%pA8w~l08J?T#WQ_|(|3UFP(0t;bha<-^!e>ws_&Wjr)T=YPA_3+ zN$tKgwY#3a!0E@Z_1L#P4_3li<+bal7W+BStLadf4hfT6;l#w*oZ5x8Zc>;g36)zU zs@Bw%R+(=kOrM0xtrJx(^{7?#_Y$U6!sV8VF0Q^|&Fl%i&7#V!7FChj;cU{dFbxwb zw_sES>Qbxlgx;3X<<^X@ef0pY!k*CEHLBdQQN^ymw91~)+c>J+%G>r?sqKK-=7nkA Rafyb0w+X)wc)JYrzh9!D(nJ6N