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
0 commit comments