diff --git a/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java b/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java index 9ea935551b8..ab471694890 100644 --- 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, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, 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 @@ -81,18 +81,24 @@ class ZipFileSystem extends FileSystem { private static final boolean isWindows = System.getProperty("os.name") .startsWith("Windows"); private static final byte[] ROOTPATH = new byte[] { '/' }; + + // Global access mode for "mounted" file system ("readOnly" or "readWrite"). + private static final String PROPERTY_ACCESS_MODE = "accessMode"; + + // Posix file permissions allow per-file access control in a posix-like fashion. + // Using a "readOnly" access mode will change the posix permissions of any + // underlying entries (they may still show as "writable", but will not be). private static final String PROPERTY_POSIX = "enablePosixFileAttributes"; private static final String PROPERTY_DEFAULT_OWNER = "defaultOwner"; private static final String PROPERTY_DEFAULT_GROUP = "defaultGroup"; private static final String PROPERTY_DEFAULT_PERMISSIONS = "defaultPermissions"; // Property used to specify the entry version to use for a multi-release JAR private static final String PROPERTY_RELEASE_VERSION = "releaseVersion"; + // Original property used to specify the entry version to use for a // multi-release JAR which is kept for backwards compatibility. private static final String PROPERTY_MULTI_RELEASE = "multi-release"; - private static final Set DEFAULT_PERMISSIONS = - PosixFilePermissions.fromString("rwxrwxrwx"); // Property used to specify the compression mode to use private static final String PROPERTY_COMPRESSION_METHOD = "compressionMethod"; // Value specified for compressionMethod property to compress Zip entries @@ -104,7 +110,8 @@ class ZipFileSystem extends FileSystem { private final Path zfpath; final ZipCoder zc; private final ZipPath rootdir; - private boolean readOnly; // readonly file system, false by default + // Starts in readOnly (safe mode), but might be reset at the end of initialization. + private boolean readOnly = true; // default time stamp for pseudo entries private final long zfsDefaultTimeStamp = System.currentTimeMillis(); @@ -129,10 +136,37 @@ class ZipFileSystem extends FileSystem { final boolean supportPosix; private final UserPrincipal defaultOwner; private final GroupPrincipal defaultGroup; + // Unmodifiable set. private final Set defaultPermissions; private final Set supportedFileAttributeViews; + private enum ZipAccessMode { + // Creates a file system for read-write access. + READ_WRITE("readWrite"), + // Creates a file system for read-only access. + READ_ONLY("readOnly"); + + private final String label; + + ZipAccessMode(String label) { + this.label = label; + } + + // Parses the access mode from an environmental parameter. + // Returns null for missing value to indicate default behavior. + static ZipAccessMode from(Object value) { + if (value == null) { + return null; + } else if (READ_WRITE.label.equals(value)) { + return ZipAccessMode.READ_WRITE; + } else if (READ_ONLY.label.equals(value)) { + return ZipAccessMode.READ_ONLY; + } + throw new IllegalArgumentException("Unknown file system access mode: " + value); + } + } + ZipFileSystem(ZipFileSystemProvider provider, Path zfpath, Map env) throws IOException @@ -144,15 +178,28 @@ class ZipFileSystem extends FileSystem { this.useTempFile = isTrue(env, "useTempFile"); this.forceEnd64 = isTrue(env, "forceZIP64End"); this.defaultCompressionMethod = getDefaultCompressionMethod(env); + + ZipAccessMode accessMode = ZipAccessMode.from(env.get(PROPERTY_ACCESS_MODE)); + boolean forceReadOnly = (accessMode == ZipAccessMode.READ_ONLY); + this.supportPosix = isTrue(env, PROPERTY_POSIX); this.defaultOwner = supportPosix ? initOwner(zfpath, env) : null; this.defaultGroup = supportPosix ? initGroup(zfpath, env) : null; - this.defaultPermissions = supportPosix ? initPermissions(env) : null; + this.defaultPermissions = supportPosix ? Collections.unmodifiableSet(initPermissions(env)) : null; this.supportedFileAttributeViews = supportPosix ? - Set.of("basic", "posix", "zip") : Set.of("basic", "zip"); + Set.of("basic", "posix", "zip") : Set.of("basic", "zip"); + + // 'create=true' is semantically the same as StandardOpenOption.CREATE, + // and can only be used to create a writable file system (whether the + // underlying ZIP file exists or not), and is always incompatible with + // 'accessMode=readOnly'). + boolean shouldCreate = isTrue(env, "create"); + if (shouldCreate && forceReadOnly) { + throw new IllegalArgumentException( + "Specifying 'accessMode=readOnly' is incompatible with 'create=true'"); + } if (Files.notExists(zfpath)) { - // create a new zip if it doesn't exist - if (isTrue(env, "create")) { + if (shouldCreate) { try (OutputStream os = Files.newOutputStream(zfpath, CREATE_NEW, WRITE)) { new END().write(os, 0, forceEnd64); } @@ -160,12 +207,9 @@ class ZipFileSystem extends FileSystem { throw new NoSuchFileException(zfpath.toString()); } } - // sm and existence check + // Existence check zfpath.getFileSystem().provider().checkAccess(zfpath, AccessMode.READ); - boolean writeable = 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 { this.cen = initCEN(); @@ -179,13 +223,29 @@ class ZipFileSystem extends FileSystem { } this.provider = provider; this.zfpath = zfpath; + this.rootdir = new ZipPath(this, new byte[]{'/'}); - initializeReleaseVersion(env); + // Determining a release version uses 'this' instance to read paths etc. + Optional multiReleaseVersion = determineReleaseVersion(env); + + // Set the version-based lookup function for multi-release JARs. + this.entryLookup = + multiReleaseVersion.map(this::createVersionedLinks).orElse(Function.identity()); + + // We only allow read-write zip/jar files if they are not multi-release + // JARs and the underlying file is writable. + this.readOnly = forceReadOnly || multiReleaseVersion.isPresent() || !Files.isWritable(zfpath); + if (readOnly && accessMode == ZipAccessMode.READ_WRITE) { + String reason = multiReleaseVersion.isPresent() + ? "the multi-release JAR file is not writable" + : "the ZIP file is not writable"; + throw new IOException(reason); + } } /** * Return the compression method to use (STORED or DEFLATED). If the - * property {@code commpressionMethod} is set use its value to determine + * property {@code compressionMethod} is set use its value to determine * the compression method to use. If the property is not set, then the * default compression is DEFLATED unless the property {@code noCompression} * is set which is supported for backwards compatibility. @@ -293,12 +353,12 @@ class ZipFileSystem extends FileSystem { " or " + GroupPrincipal.class); } - // Initialize the default permissions for files inside the zip archive. + // Return the default permissions for files inside the zip archive. // If not specified in env, it will return 777. private Set initPermissions(Map env) { Object o = env.get(PROPERTY_DEFAULT_PERMISSIONS); if (o == null) { - return DEFAULT_PERMISSIONS; + return PosixFilePermissions.fromString("rwxrwxrwx"); } if (o instanceof String) { return PosixFilePermissions.fromString((String)o); @@ -346,10 +406,6 @@ class ZipFileSystem extends FileSystem { } } - void setReadOnly() { - this.readOnly = true; - } - @Override public Iterable getRootDirectories() { return List.of(rootdir); @@ -1383,33 +1439,24 @@ class ZipFileSystem extends FileSystem { * Checks if the Zip File System property "releaseVersion" has been specified. If it has, * use its value to determine the requested version. If not use the value of the "multi-release" property. */ - private void initializeReleaseVersion(Map env) throws IOException { + private Optional determineReleaseVersion(Map env) throws IOException { Object o = env.containsKey(PROPERTY_RELEASE_VERSION) ? env.get(PROPERTY_RELEASE_VERSION) : env.get(PROPERTY_MULTI_RELEASE); - if (o != null && isMultiReleaseJar()) { - int version; - if (o instanceof String) { - String s = (String)o; - if (s.equals("runtime")) { - version = Runtime.version().feature(); - } else if (s.matches("^[1-9][0-9]*$")) { - version = Version.parse(s).feature(); - } else { - throw new IllegalArgumentException("Invalid runtime version"); - } - } else if (o instanceof Integer) { - version = Version.parse(((Integer)o).toString()).feature(); - } else if (o instanceof Version) { - version = ((Version)o).feature(); - } else { - throw new IllegalArgumentException("env parameter must be String, " + - "Integer, or Version"); - } - createVersionedLinks(version < 0 ? 0 : version); - setReadOnly(); + if (o == null || !isMultiReleaseJar()) { + return Optional.empty(); } + int version = switch (o) { + case String s when s.equals("runtime") -> Runtime.version().feature(); + case String s when s.matches("^[1-9][0-9]*$") -> Version.parse(s).feature(); + case Integer i -> Version.parse(i.toString()).feature(); + case Version v -> v.feature(); + case String s -> throw new IllegalArgumentException("Invalid runtime version: " + s); + default -> throw new IllegalArgumentException("env parameter must be String, " + + "Integer, or Version"); + }; + return Optional.of(Math.max(version, 0)); } /** @@ -1435,11 +1482,11 @@ class ZipFileSystem extends FileSystem { * Then wrap the map in a function that getEntry can use to override root * entry lookup for entries that have corresponding versioned entries. */ - private void createVersionedLinks(int version) { + private Function createVersionedLinks(int version) { IndexNode verdir = getInode(getBytes("/META-INF/versions")); // nothing to do, if no /META-INF/versions if (verdir == null) { - return; + return Function.identity(); } // otherwise, create a map and for each META-INF/versions/{n} directory // put all the leaf inodes, i.e. entries, into the alias map @@ -1451,10 +1498,7 @@ class ZipFileSystem extends FileSystem { getOrCreateInode(getRootName(entryNode, versionNode), entryNode.isdir), entryNode.name)) ); - entryLookup = path -> { - byte[] entry = aliasMap.get(IndexNode.keyOf(path)); - return entry == null ? path : entry; - }; + return path -> aliasMap.getOrDefault(IndexNode.keyOf(path), path); } /** @@ -3551,7 +3595,8 @@ class ZipFileSystem extends FileSystem { @Override public Set permissions() { - return storedPermissions().orElse(Set.copyOf(defaultPermissions)); + // supportPosix ==> (defaultPermissions != null) + return storedPermissions().orElse(defaultPermissions); } } diff --git a/src/jdk.zipfs/share/classes/module-info.java b/src/jdk.zipfs/share/classes/module-info.java index 52a5aed98f9..db7ae3eec81 100644 --- a/src/jdk.zipfs/share/classes/module-info.java +++ b/src/jdk.zipfs/share/classes/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2025, 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 @@ -153,8 +153,8 @@ import java.util.Set; * {@link java.lang.String} or {@link java.lang.Boolean} * false * - * If the value is {@code true}, the ZIP file system provider - * creates a new ZIP or JAR file if it does not exist. + * If the value is {@code true}, the ZIP file system provider creates a + * new ZIP or JAR file if it does not exist. * * * @@ -225,8 +225,8 @@ import java.util.Set; * *
  • * If the value is not {@code "STORED"} or {@code "DEFLATED"}, an - * {@code IllegalArgumentException} will be thrown when the Zip - * filesystem is created. + * {@code IllegalArgumentException} will be thrown when creating the + * ZIP file system. *
  • * * @@ -260,12 +260,54 @@ import java.util.Set; *
  • * If the value does not represent a valid * {@linkplain Runtime.Version Java SE Platform version number}, - * an {@code IllegalArgumentException} will be thrown. + * an {@code IllegalArgumentException} will be thrown when creating + * the ZIP file system. *
  • * * * - * + * + * accessMode + * {@link java.lang.String} + * null/unset + * + * A value defining the desired access mode of the file system. + * ZIP file systems can be created to allow for read-write or + * read-only access. + *
      + *
    • + * If no value is set, the file system is created as read-write + * if possible. Use {@link java.nio.file.FileSystem#isReadOnly() + * isReadOnly()} to determine the actual access mode. + *
    • + *
    • + * If the value is {@code "readOnly"}, the file system is created + * read-only, and {@link java.nio.file.FileSystem#isReadOnly() + * isReadOnly()} will always return {@code true}. Creating a + * read-only file system requires the underlying ZIP file to + * already exist. + * Specifying the {@code create} property as {@code true} with the + * {@code accessMode} as {@code readOnly} will cause an {@code + * IllegalArgumentException} to be thrown when creating the ZIP file + * system. + *
    • + *
    • + * If the value is {@code "readWrite"}, the file system is created + * read-write, and {@link java.nio.file.FileSystem#isReadOnly() + * isReadOnly()} will always return {@code false}. If a writable file + * system cannot be created, an {@code IOException} will be thrown + * when creating the ZIP file system. + *
    • + *
    • + * Any other values will cause an {@code IllegalArgumentException} + * to be thrown when creating the ZIP file system. + *
    • + *
    + * The {@code accessMode} property has no effect on reported POSIX file + * permissions (in cases where POSIX support is enabled). + * + * + * * * *

    Examples:

    diff --git a/test/jdk/jdk/nio/zipfs/NewFileSystemTests.java b/test/jdk/jdk/nio/zipfs/NewFileSystemTests.java index 44fe2686aba..d15988176e3 100644 --- a/test/jdk/jdk/nio/zipfs/NewFileSystemTests.java +++ b/test/jdk/jdk/nio/zipfs/NewFileSystemTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, 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 @@ -31,11 +31,17 @@ import java.net.URI; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.Iterator; import java.util.Map; -import static org.testng.Assert.*; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertThrows; +import static org.testng.Assert.assertTrue; /** * @test @@ -170,6 +176,96 @@ public class NewFileSystemTests { FileSystems.newFileSystem(Path.of("basic.jar"), nullMap)); } + /** + * Validate that without {@code "create" = true}, a ZIP file system cannot be + * opened if the underlying file is missing, but even with this set, a ZIP + * file system cannot be opened for conflicting or invalid access modes. + */ + @DataProvider(name = "badEnvMap") + protected Object[][] badEnvMap() { + return new Object[][]{ + {Map.of(), NoSuchFileException.class}, + {Map.of("accessMode", "readOnly"), NoSuchFileException.class}, + {Map.of("accessMode", "readWrite"), NoSuchFileException.class}, + {Map.of("create", true, "accessMode", "readOnly"), IllegalArgumentException.class}, + {Map.of("create", true, "accessMode", "badValue"), IllegalArgumentException.class}, + }; + } + @Test(dataProvider = "badEnvMap") + public void badArgumentsFailure(Map env, Class exception) throws IOException { + assertThrows(exception, () -> FileSystems.newFileSystem(Path.of("no_such.zip"), env)); + } + + /** + * Validate that multi-release JARs can be opened read-write if no release + * version is specified. + */ + @Test + public void multiReleaseJarReadWriteSuccess() throws IOException { + // Multi-release JARs, when opened with a specified version are inherently read-only. + Path multiReleaseJar = createMultiReleaseJar(); + try (FileSystem fs = FileSystems.newFileSystem(multiReleaseJar, Map.of("accessMode", "readWrite"))) { + assertFalse(fs.isReadOnly()); + assertEquals( + Files.readString(fs.getPath("file.txt"), UTF_8), + "Default version", + "unexpected file content"); + } + } + + /** + * Validate that when the underlying file is read-only, it cannot be opened in + * read-write mode. + */ + @Test + public void readOnlyZipFileFailure() throws IOException { + // Underlying file is read-only. + Path readOnlyZip = Utils.createJarFile("read_only.zip", Map.of("file.txt", "Hello World")); + // In theory this can fail, and we should avoid unwanted false-negatives. + if (readOnlyZip.toFile().setReadOnly()) { + assertThrows(IOException.class, + () -> FileSystems.newFileSystem(readOnlyZip, Map.of("accessMode", "readWrite"))); + } + } + + /** + * Validate that multi-release JAR is opened read-only by default if a release + * version is specified. + */ + @Test + public void multiReleaseJarDefaultReadOnly() throws IOException { + Path multiReleaseJar = createMultiReleaseJar(); + try (FileSystem fs = FileSystems.newFileSystem(multiReleaseJar, Map.of("releaseVersion", "1"))) { + assertTrue(fs.isReadOnly()); + assertEquals( + Files.readString(fs.getPath("file.txt"), UTF_8), + "First version", + "unexpected file content"); + } + } + + /** + * Validate that multi-release JARs cannot be opened read-write if a release + * version is specified. + */ + @Test + public void multiReleaseJarReadWriteFailure() throws IOException { + Path multiReleaseJar = createMultiReleaseJar(); + assertThrows(IOException.class, + () -> FileSystems.newFileSystem( + multiReleaseJar, + Map.of("accessMode", "readWrite", "releaseVersion", "1"))); + } + + private static Path createMultiReleaseJar() throws IOException { + return Utils.createJarFile("multi_release.jar", Map.of( + // Newline required for attribute to be read from Manifest file. + "META-INF/MANIFEST.MF", "Multi-Release: true\n", + "META-INF/versions/1/file.txt", "First version", + "META-INF/versions/2/file.txt", "Second version", + "file.txt", "Default version")); + } + /* * DataProvider used to verify that a Zip file system may be returned * when specifying a class loader diff --git a/test/jdk/jdk/nio/zipfs/TestPosix.java b/test/jdk/jdk/nio/zipfs/TestPosix.java index 420c1618e12..ff6f9c4920f 100644 --- a/test/jdk/jdk/nio/zipfs/TestPosix.java +++ b/test/jdk/jdk/nio/zipfs/TestPosix.java @@ -1,5 +1,6 @@ /* * Copyright (c) 2019, 2024, SAP SE. All rights reserved. + * Copyright (c) 2025, 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 @@ -35,6 +36,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.spi.ToolProvider; +import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -50,8 +52,11 @@ import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -96,6 +101,8 @@ public class TestPosix { // FS open options private static final Map ENV_DEFAULT = Collections.emptyMap(); private static final Map ENV_POSIX = Map.of("enablePosixFileAttributes", true); + private static final Map ENV_READ_ONLY = Map.of("accessMode", "readOnly"); + private static final Map ENV_POSIX_READ_ONLY = Map.of("enablePosixFileAttributes", true, "accessMode", "readOnly"); // misc private static final CopyOption[] COPY_ATTRIBUTES = {StandardCopyOption.COPY_ATTRIBUTES}; @@ -398,6 +405,37 @@ public class TestPosix { doCheckEntries(path, expected); } + private void checkReadOnlyFileSystem(FileSystem fs) throws IOException { + assertTrue(fs.isReadOnly(), "File system should be read-only"); + Path root = fs.getPath("/"); + + // Rather than calling something like "addOwnerRead(root)", we walk all + // files to ensure that all operations fail, not some arbitrary first one. + Set badPerms = Set.of(OTHERS_EXECUTE, OTHERS_WRITE); + FileTime anyTime = FileTime.from(Instant.now()); + try (Stream paths = Files.walk(root)) { + paths.forEach(p -> { + assertFalse(Files.isWritable(p), "File should not be writable: " + p); + assertSame(fs, p.getFileSystem()); + assertThrows( + AccessDeniedException.class, + () -> fs.provider().checkAccess(p, AccessMode.WRITE)); + assertThrows( + ReadOnlyFileSystemException.class, + () -> fs.provider().setAttribute(p, "zip:permissions", badPerms)); + + // These fail because there is not corresponding File for a zip path (they will + // currently fail for read-write ZIP file systems too, but we sanity-check here). + assertThrows(UnsupportedOperationException.class, + () -> Files.setLastModifiedTime(p, anyTime)); + assertThrows(UnsupportedOperationException.class, + () -> Files.setAttribute(p, "zip:permissions", badPerms)); + assertThrows(UnsupportedOperationException.class, + () -> Files.setPosixFilePermissions(p, badPerms)); + }); + } + } + private boolean throwsUOE(Executor e) throws IOException { try { e.doIt(); @@ -440,6 +478,25 @@ public class TestPosix { } } + /** + * As {@code testDefault()} but with {@code "accessMode"="readOnly"}. + */ + @Test + public void testDefaultReadOnly() throws IOException { + // create zip file using zipfs with default option + createTestZipFile(ZIP_FILE, ENV_DEFAULT).close(); + // check entries on zipfs with read-only options + try (FileSystem zip = FileSystems.newFileSystem(ZIP_FILE, ENV_READ_ONLY)) { + checkEntries(zip, checkExpects.permsInZip); + checkReadOnlyFileSystem(zip); + } + // check entries on zipfs with posix and read-only options + try (FileSystem zip = FileSystems.newFileSystem(ZIP_FILE, ENV_POSIX_READ_ONLY)) { + checkEntries(zip, checkExpects.permsPosix); + checkReadOnlyFileSystem(zip); + } + } + /** * This tests whether the entries in a zip file created w/ * Posix support are correct. @@ -460,6 +517,25 @@ public class TestPosix { } } + /** + * As {@code testPosix()} but with {@code "accessMode"="readOnly"}. + */ + @Test + public void testPosixReadOnly() throws IOException { + // create zip file using zipfs with posix option + createTestZipFile(ZIP_FILE, ENV_POSIX).close(); + // check entries on zipfs with read-only options + try (FileSystem zip = FileSystems.newFileSystem(ZIP_FILE, ENV_READ_ONLY)) { + checkEntries(zip, checkExpects.permsInZip); + checkReadOnlyFileSystem(zip); + } + // check entries on zipfs with posix and read-only options + try (FileSystem zip = FileSystems.newFileSystem(ZIP_FILE, ENV_POSIX_READ_ONLY)) { + checkEntries(zip, checkExpects.permsPosix); + checkReadOnlyFileSystem(zip); + } + } + /** * This tests whether the entries in a zip file copied from another * are correct. diff --git a/test/jdk/jdk/nio/zipfs/Utils.java b/test/jdk/jdk/nio/zipfs/Utils.java index a561535dc5a..71cc1117a2f 100644 --- a/test/jdk/jdk/nio/zipfs/Utils.java +++ b/test/jdk/jdk/nio/zipfs/Utils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2025, 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 @@ -23,25 +23,35 @@ import java.io.IOException; import java.io.OutputStream; -import java.nio.file.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; import java.util.Random; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; -/** - * Utility class for zipfs tests. - */ +import static java.nio.charset.StandardCharsets.UTF_8; -class Utils { - private Utils() { } +/** + * Utility class for {@code ZipFileSystem} tests. + */ +final class Utils { + private Utils() {} /** - * Creates a JAR file of the given name with 0 or more named entries. + * Creates a JAR file of the given name with 0 or more named entries with + * random content. * - * @return Path to the newly created JAR file + *

    If an existing file of the same name already exists, it is silently + * overwritten. + * + * @param name the file name of the jar file to create in the working directory. + * @param entries entries JAR file entry names, whose content will be populated + * with random bytes + * @return the absolute path to the newly created JAR file. */ static Path createJarFile(String name, String... entries) throws IOException { - Path jarFile = Paths.get("basic.jar"); + Path jarFile = Path.of(name); Random rand = new Random(); try (OutputStream out = Files.newOutputStream(jarFile); JarOutputStream jout = new JarOutputStream(out)) { @@ -56,6 +66,32 @@ class Utils { len += 1024; } } - return jarFile; + return jarFile.toAbsolutePath(); + } + + /** + * Creates a JAR file of the given name with 0 or more entries with specified + * content. + * + *

    If an existing file of the same name already exists, it is silently + * overwritten. + * + * @param name the file name of the jar file to create in the working directory. + * @param entries a map of JAR file entry names to entry content (stored as + * UTF-8 encoded bytes). + * @return the absolute path to the newly created JAR file. + */ + static Path createJarFile(String name, Map entries) throws IOException { + Path jarFile = Path.of(name); + try (OutputStream out = Files.newOutputStream(jarFile); + JarOutputStream jout = new JarOutputStream(out)) { + for (var entry : entries.entrySet()) { + JarEntry je = new JarEntry(entry.getKey()); + jout.putNextEntry(je); + jout.write(entry.getValue().getBytes(UTF_8)); + jout.closeEntry(); + } + } + return jarFile.toAbsolutePath(); } }