8350880: (zipfs) Add support for read-only zip file systems

Reviewed-by: lancea, alanb, jpai
This commit is contained in:
David Beaumont 2025-06-03 04:01:09 +00:00 committed by Jaikiran Pai
parent 24edd3b2c1
commit 832c5b06e8
5 changed files with 364 additions and 69 deletions

View File

@ -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<PosixFilePermission> 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<PosixFilePermission> defaultPermissions;
private final Set<String> 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<String, ?> 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<Integer> 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<PosixFilePermission> initPermissions(Map<String, ?> 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<Path> 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<String, ?> env) throws IOException {
private Optional<Integer> determineReleaseVersion(Map<String, ?> 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<byte[], byte[]> 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<PosixFilePermission> permissions() {
return storedPermissions().orElse(Set.copyOf(defaultPermissions));
// supportPosix ==> (defaultPermissions != null)
return storedPermissions().orElse(defaultPermissions);
}
}

View File

@ -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;
* <td>{@link java.lang.String} or {@link java.lang.Boolean}</td>
* <td>false</td>
* <td>
* 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.
* </td>
* </tr>
* <tr>
@ -225,8 +225,8 @@ import java.util.Set;
* </li>
* <li>
* 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.
* </li>
* </ul>
* </td>
@ -260,12 +260,54 @@ import java.util.Set;
* <li>
* 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.
* </li>
* </ul>
* </td>
* </tr>
* </tbody>
* <tr>
* <th scope="row">accessMode</th>
* <td>{@link java.lang.String}</td>
* <td>null/unset</td>
* <td>
* A value defining the desired access mode of the file system.
* ZIP file systems can be created to allow for <em>read-write</em> or
* <em>read-only</em> access.
* <ul>
* <li>
* If no value is set, the file system is created as <em>read-write</em>
* if possible. Use {@link java.nio.file.FileSystem#isReadOnly()
* isReadOnly()} to determine the actual access mode.
* </li>
* <li>
* If the value is {@code "readOnly"}, the file system is created
* <em>read-only</em>, and {@link java.nio.file.FileSystem#isReadOnly()
* isReadOnly()} will always return {@code true}. Creating a
* <em>read-only</em> 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.
* </li>
* <li>
* If the value is {@code "readWrite"}, the file system is created
* <em>read-write</em>, 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.
* </li>
* <li>
* Any other values will cause an {@code IllegalArgumentException}
* to be thrown when creating the ZIP file system.
* </li>
* </ul>
* The {@code accessMode} property has no effect on reported POSIX file
* permissions (in cases where POSIX support is enabled).
* </td>
* </tr>
* </tbody>
* </table>
*
* <h2>Examples:</h2>

View File

@ -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<String, String> env, Class<? extends Throwable> 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

View File

@ -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<String, Object> ENV_DEFAULT = Collections.<String, Object>emptyMap();
private static final Map<String, Object> ENV_POSIX = Map.of("enablePosixFileAttributes", true);
private static final Map<String, Object> ENV_READ_ONLY = Map.of("accessMode", "readOnly");
private static final Map<String, Object> 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<PosixFilePermission> badPerms = Set.of(OTHERS_EXECUTE, OTHERS_WRITE);
FileTime anyTime = FileTime.from(Instant.now());
try (Stream<Path> 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.

View File

@ -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
* <p>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.
*
* <p>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<String, String> 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();
}
}