Skip to content

Commit 832c5b0

Browse files
david-beaumontjaikiran
authored andcommitted
8350880: (zipfs) Add support for read-only zip file systems
Reviewed-by: lancea, alanb, jpai
1 parent 24edd3b commit 832c5b0

File tree

5 files changed

+363
-68
lines changed

5 files changed

+363
-68
lines changed

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

Lines changed: 94 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2009, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2009, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -81,18 +81,24 @@ class ZipFileSystem extends FileSystem {
8181
private static final boolean isWindows = System.getProperty("os.name")
8282
.startsWith("Windows");
8383
private static final byte[] ROOTPATH = new byte[] { '/' };
84+
85+
// Global access mode for "mounted" file system ("readOnly" or "readWrite").
86+
private static final String PROPERTY_ACCESS_MODE = "accessMode";
87+
88+
// Posix file permissions allow per-file access control in a posix-like fashion.
89+
// Using a "readOnly" access mode will change the posix permissions of any
90+
// underlying entries (they may still show as "writable", but will not be).
8491
private static final String PROPERTY_POSIX = "enablePosixFileAttributes";
8592
private static final String PROPERTY_DEFAULT_OWNER = "defaultOwner";
8693
private static final String PROPERTY_DEFAULT_GROUP = "defaultGroup";
8794
private static final String PROPERTY_DEFAULT_PERMISSIONS = "defaultPermissions";
8895
// Property used to specify the entry version to use for a multi-release JAR
8996
private static final String PROPERTY_RELEASE_VERSION = "releaseVersion";
97+
9098
// Original property used to specify the entry version to use for a
9199
// multi-release JAR which is kept for backwards compatibility.
92100
private static final String PROPERTY_MULTI_RELEASE = "multi-release";
93101

94-
private static final Set<PosixFilePermission> DEFAULT_PERMISSIONS =
95-
PosixFilePermissions.fromString("rwxrwxrwx");
96102
// Property used to specify the compression mode to use
97103
private static final String PROPERTY_COMPRESSION_METHOD = "compressionMethod";
98104
// Value specified for compressionMethod property to compress Zip entries
@@ -104,7 +110,8 @@ class ZipFileSystem extends FileSystem {
104110
private final Path zfpath;
105111
final ZipCoder zc;
106112
private final ZipPath rootdir;
107-
private boolean readOnly; // readonly file system, false by default
113+
// Starts in readOnly (safe mode), but might be reset at the end of initialization.
114+
private boolean readOnly = true;
108115

109116
// default time stamp for pseudo entries
110117
private final long zfsDefaultTimeStamp = System.currentTimeMillis();
@@ -129,10 +136,37 @@ class ZipFileSystem extends FileSystem {
129136
final boolean supportPosix;
130137
private final UserPrincipal defaultOwner;
131138
private final GroupPrincipal defaultGroup;
139+
// Unmodifiable set.
132140
private final Set<PosixFilePermission> defaultPermissions;
133141

134142
private final Set<String> supportedFileAttributeViews;
135143

144+
private enum ZipAccessMode {
145+
// Creates a file system for read-write access.
146+
READ_WRITE("readWrite"),
147+
// Creates a file system for read-only access.
148+
READ_ONLY("readOnly");
149+
150+
private final String label;
151+
152+
ZipAccessMode(String label) {
153+
this.label = label;
154+
}
155+
156+
// Parses the access mode from an environmental parameter.
157+
// Returns null for missing value to indicate default behavior.
158+
static ZipAccessMode from(Object value) {
159+
if (value == null) {
160+
return null;
161+
} else if (READ_WRITE.label.equals(value)) {
162+
return ZipAccessMode.READ_WRITE;
163+
} else if (READ_ONLY.label.equals(value)) {
164+
return ZipAccessMode.READ_ONLY;
165+
}
166+
throw new IllegalArgumentException("Unknown file system access mode: " + value);
167+
}
168+
}
169+
136170
ZipFileSystem(ZipFileSystemProvider provider,
137171
Path zfpath,
138172
Map<String, ?> env) throws IOException
@@ -144,28 +178,38 @@ class ZipFileSystem extends FileSystem {
144178
this.useTempFile = isTrue(env, "useTempFile");
145179
this.forceEnd64 = isTrue(env, "forceZIP64End");
146180
this.defaultCompressionMethod = getDefaultCompressionMethod(env);
181+
182+
ZipAccessMode accessMode = ZipAccessMode.from(env.get(PROPERTY_ACCESS_MODE));
183+
boolean forceReadOnly = (accessMode == ZipAccessMode.READ_ONLY);
184+
147185
this.supportPosix = isTrue(env, PROPERTY_POSIX);
148186
this.defaultOwner = supportPosix ? initOwner(zfpath, env) : null;
149187
this.defaultGroup = supportPosix ? initGroup(zfpath, env) : null;
150-
this.defaultPermissions = supportPosix ? initPermissions(env) : null;
188+
this.defaultPermissions = supportPosix ? Collections.unmodifiableSet(initPermissions(env)) : null;
151189
this.supportedFileAttributeViews = supportPosix ?
152-
Set.of("basic", "posix", "zip") : Set.of("basic", "zip");
190+
Set.of("basic", "posix", "zip") : Set.of("basic", "zip");
191+
192+
// 'create=true' is semantically the same as StandardOpenOption.CREATE,
193+
// and can only be used to create a writable file system (whether the
194+
// underlying ZIP file exists or not), and is always incompatible with
195+
// 'accessMode=readOnly').
196+
boolean shouldCreate = isTrue(env, "create");
197+
if (shouldCreate && forceReadOnly) {
198+
throw new IllegalArgumentException(
199+
"Specifying 'accessMode=readOnly' is incompatible with 'create=true'");
200+
}
153201
if (Files.notExists(zfpath)) {
154-
// create a new zip if it doesn't exist
155-
if (isTrue(env, "create")) {
202+
if (shouldCreate) {
156203
try (OutputStream os = Files.newOutputStream(zfpath, CREATE_NEW, WRITE)) {
157204
new END().write(os, 0, forceEnd64);
158205
}
159206
} else {
160207
throw new NoSuchFileException(zfpath.toString());
161208
}
162209
}
163-
// sm and existence check
210+
// Existence check
164211
zfpath.getFileSystem().provider().checkAccess(zfpath, AccessMode.READ);
165-
boolean writeable = Files.isWritable(zfpath);
166-
this.readOnly = !writeable;
167212
this.zc = ZipCoder.get(nameEncoding);
168-
this.rootdir = new ZipPath(this, new byte[]{'/'});
169213
this.ch = Files.newByteChannel(zfpath, READ);
170214
try {
171215
this.cen = initCEN();
@@ -179,13 +223,29 @@ class ZipFileSystem extends FileSystem {
179223
}
180224
this.provider = provider;
181225
this.zfpath = zfpath;
226+
this.rootdir = new ZipPath(this, new byte[]{'/'});
227+
228+
// Determining a release version uses 'this' instance to read paths etc.
229+
Optional<Integer> multiReleaseVersion = determineReleaseVersion(env);
230+
231+
// Set the version-based lookup function for multi-release JARs.
232+
this.entryLookup =
233+
multiReleaseVersion.map(this::createVersionedLinks).orElse(Function.identity());
182234

183-
initializeReleaseVersion(env);
235+
// We only allow read-write zip/jar files if they are not multi-release
236+
// JARs and the underlying file is writable.
237+
this.readOnly = forceReadOnly || multiReleaseVersion.isPresent() || !Files.isWritable(zfpath);
238+
if (readOnly && accessMode == ZipAccessMode.READ_WRITE) {
239+
String reason = multiReleaseVersion.isPresent()
240+
? "the multi-release JAR file is not writable"
241+
: "the ZIP file is not writable";
242+
throw new IOException(reason);
243+
}
184244
}
185245

186246
/**
187247
* Return the compression method to use (STORED or DEFLATED). If the
188-
* property {@code commpressionMethod} is set use its value to determine
248+
* property {@code compressionMethod} is set use its value to determine
189249
* the compression method to use. If the property is not set, then the
190250
* default compression is DEFLATED unless the property {@code noCompression}
191251
* is set which is supported for backwards compatibility.
@@ -293,12 +353,12 @@ private GroupPrincipal initGroup(Path zfpath, Map<String, ?> env) throws IOExcep
293353
" or " + GroupPrincipal.class);
294354
}
295355

296-
// Initialize the default permissions for files inside the zip archive.
356+
// Return the default permissions for files inside the zip archive.
297357
// If not specified in env, it will return 777.
298358
private Set<PosixFilePermission> initPermissions(Map<String, ?> env) {
299359
Object o = env.get(PROPERTY_DEFAULT_PERMISSIONS);
300360
if (o == null) {
301-
return DEFAULT_PERMISSIONS;
361+
return PosixFilePermissions.fromString("rwxrwxrwx");
302362
}
303363
if (o instanceof String) {
304364
return PosixFilePermissions.fromString((String)o);
@@ -346,10 +406,6 @@ private void checkWritable() {
346406
}
347407
}
348408

349-
void setReadOnly() {
350-
this.readOnly = true;
351-
}
352-
353409
@Override
354410
public Iterable<Path> getRootDirectories() {
355411
return List.of(rootdir);
@@ -1383,33 +1439,24 @@ private void removeFromTree(IndexNode inode) {
13831439
* Checks if the Zip File System property "releaseVersion" has been specified. If it has,
13841440
* use its value to determine the requested version. If not use the value of the "multi-release" property.
13851441
*/
1386-
private void initializeReleaseVersion(Map<String, ?> env) throws IOException {
1442+
private Optional<Integer> determineReleaseVersion(Map<String, ?> env) throws IOException {
13871443
Object o = env.containsKey(PROPERTY_RELEASE_VERSION) ?
13881444
env.get(PROPERTY_RELEASE_VERSION) :
13891445
env.get(PROPERTY_MULTI_RELEASE);
13901446

1391-
if (o != null && isMultiReleaseJar()) {
1392-
int version;
1393-
if (o instanceof String) {
1394-
String s = (String)o;
1395-
if (s.equals("runtime")) {
1396-
version = Runtime.version().feature();
1397-
} else if (s.matches("^[1-9][0-9]*$")) {
1398-
version = Version.parse(s).feature();
1399-
} else {
1400-
throw new IllegalArgumentException("Invalid runtime version");
1401-
}
1402-
} else if (o instanceof Integer) {
1403-
version = Version.parse(((Integer)o).toString()).feature();
1404-
} else if (o instanceof Version) {
1405-
version = ((Version)o).feature();
1406-
} else {
1407-
throw new IllegalArgumentException("env parameter must be String, " +
1408-
"Integer, or Version");
1409-
}
1410-
createVersionedLinks(version < 0 ? 0 : version);
1411-
setReadOnly();
1447+
if (o == null || !isMultiReleaseJar()) {
1448+
return Optional.empty();
14121449
}
1450+
int version = switch (o) {
1451+
case String s when s.equals("runtime") -> Runtime.version().feature();
1452+
case String s when s.matches("^[1-9][0-9]*$") -> Version.parse(s).feature();
1453+
case Integer i -> Version.parse(i.toString()).feature();
1454+
case Version v -> v.feature();
1455+
case String s -> throw new IllegalArgumentException("Invalid runtime version: " + s);
1456+
default -> throw new IllegalArgumentException("env parameter must be String, " +
1457+
"Integer, or Version");
1458+
};
1459+
return Optional.of(Math.max(version, 0));
14131460
}
14141461

14151462
/**
@@ -1435,11 +1482,11 @@ private boolean isMultiReleaseJar() throws IOException {
14351482
* Then wrap the map in a function that getEntry can use to override root
14361483
* entry lookup for entries that have corresponding versioned entries.
14371484
*/
1438-
private void createVersionedLinks(int version) {
1485+
private Function<byte[], byte[]> createVersionedLinks(int version) {
14391486
IndexNode verdir = getInode(getBytes("/META-INF/versions"));
14401487
// nothing to do, if no /META-INF/versions
14411488
if (verdir == null) {
1442-
return;
1489+
return Function.identity();
14431490
}
14441491
// otherwise, create a map and for each META-INF/versions/{n} directory
14451492
// put all the leaf inodes, i.e. entries, into the alias map
@@ -1451,10 +1498,7 @@ private void createVersionedLinks(int version) {
14511498
getOrCreateInode(getRootName(entryNode, versionNode), entryNode.isdir),
14521499
entryNode.name))
14531500
);
1454-
entryLookup = path -> {
1455-
byte[] entry = aliasMap.get(IndexNode.keyOf(path));
1456-
return entry == null ? path : entry;
1457-
};
1501+
return path -> aliasMap.getOrDefault(IndexNode.keyOf(path), path);
14581502
}
14591503

14601504
/**
@@ -3551,7 +3595,8 @@ public GroupPrincipal group() {
35513595

35523596
@Override
35533597
public Set<PosixFilePermission> permissions() {
3554-
return storedPermissions().orElse(Set.copyOf(defaultPermissions));
3598+
// supportPosix ==> (defaultPermissions != null)
3599+
return storedPermissions().orElse(defaultPermissions);
35553600
}
35563601
}
35573602

src/jdk.zipfs/share/classes/module-info.java

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -153,8 +153,8 @@
153153
* <td>{@link java.lang.String} or {@link java.lang.Boolean}</td>
154154
* <td>false</td>
155155
* <td>
156-
* If the value is {@code true}, the ZIP file system provider
157-
* creates a new ZIP or JAR file if it does not exist.
156+
* If the value is {@code true}, the ZIP file system provider creates a
157+
* new ZIP or JAR file if it does not exist.
158158
* </td>
159159
* </tr>
160160
* <tr>
@@ -225,8 +225,8 @@
225225
* </li>
226226
* <li>
227227
* If the value is not {@code "STORED"} or {@code "DEFLATED"}, an
228-
* {@code IllegalArgumentException} will be thrown when the Zip
229-
* filesystem is created.
228+
* {@code IllegalArgumentException} will be thrown when creating the
229+
* ZIP file system.
230230
* </li>
231231
* </ul>
232232
* </td>
@@ -260,12 +260,54 @@
260260
* <li>
261261
* If the value does not represent a valid
262262
* {@linkplain Runtime.Version Java SE Platform version number},
263-
* an {@code IllegalArgumentException} will be thrown.
263+
* an {@code IllegalArgumentException} will be thrown when creating
264+
* the ZIP file system.
264265
* </li>
265266
* </ul>
266267
* </td>
267268
* </tr>
268-
* </tbody>
269+
* <tr>
270+
* <th scope="row">accessMode</th>
271+
* <td>{@link java.lang.String}</td>
272+
* <td>null/unset</td>
273+
* <td>
274+
* A value defining the desired access mode of the file system.
275+
* ZIP file systems can be created to allow for <em>read-write</em> or
276+
* <em>read-only</em> access.
277+
* <ul>
278+
* <li>
279+
* If no value is set, the file system is created as <em>read-write</em>
280+
* if possible. Use {@link java.nio.file.FileSystem#isReadOnly()
281+
* isReadOnly()} to determine the actual access mode.
282+
* </li>
283+
* <li>
284+
* If the value is {@code "readOnly"}, the file system is created
285+
* <em>read-only</em>, and {@link java.nio.file.FileSystem#isReadOnly()
286+
* isReadOnly()} will always return {@code true}. Creating a
287+
* <em>read-only</em> file system requires the underlying ZIP file to
288+
* already exist.
289+
* Specifying the {@code create} property as {@code true} with the
290+
* {@code accessMode} as {@code readOnly} will cause an {@code
291+
* IllegalArgumentException} to be thrown when creating the ZIP file
292+
* system.
293+
* </li>
294+
* <li>
295+
* If the value is {@code "readWrite"}, the file system is created
296+
* <em>read-write</em>, and {@link java.nio.file.FileSystem#isReadOnly()
297+
* isReadOnly()} will always return {@code false}. If a writable file
298+
* system cannot be created, an {@code IOException} will be thrown
299+
* when creating the ZIP file system.
300+
* </li>
301+
* <li>
302+
* Any other values will cause an {@code IllegalArgumentException}
303+
* to be thrown when creating the ZIP file system.
304+
* </li>
305+
* </ul>
306+
* The {@code accessMode} property has no effect on reported POSIX file
307+
* permissions (in cases where POSIX support is enabled).
308+
* </td>
309+
* </tr>
310+
* </tbody>
269311
* </table>
270312
*
271313
* <h2>Examples:</h2>

0 commit comments

Comments
 (0)