mirror of
https://github.com/openjdk/jdk.git
synced 2026-01-28 12:09:14 +00:00
8350880: (zipfs) Add support for read-only zip file systems
Reviewed-by: lancea, alanb, jpai
This commit is contained in:
parent
24edd3b2c1
commit
832c5b06e8
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user