Desugared library nio: fix FileChannel options
Bug: b/259869915
Change-Id: I95e0a64148219b0530e9fa77fc7e8c4672e87c3c
diff --git a/src/library_desugar/java/java/nio/channels/DesugarChannels.java b/src/library_desugar/java/java/nio/channels/DesugarChannels.java
index 63697a3..15040ef 100644
--- a/src/library_desugar/java/java/nio/channels/DesugarChannels.java
+++ b/src/library_desugar/java/java/nio/channels/DesugarChannels.java
@@ -22,17 +22,6 @@
public class DesugarChannels {
- /** Special conversion for Channel to answer a converted FileChannel if required. */
- public static Channel convertMaybeLegacyChannelFromLibrary(Channel raw) {
- if (raw == null) {
- return null;
- }
- if (raw instanceof FileChannel) {
- return convertMaybeLegacyFileChannelFromLibrary((FileChannel) raw);
- }
- return raw;
- }
-
/**
* Below Api 24 FileChannel does not implement SeekableByteChannel. When we get one from the
* library, we wrap it to implement the interface.
@@ -48,32 +37,41 @@
}
/**
- * We unwrap when going to the library since we cannot intercept the calls to final methods in the
- * library.
+ * All FileChannels below 24 are wrapped to support the new interface SeekableByteChannel.
+ * FileChannels between 24 and 26 are wrapped only to improve the emulation of program opened
+ * FileChannels, especially with the append and delete on close options.
*/
- public static FileChannel convertMaybeLegacyFileChannelToLibrary(FileChannel raw) {
- if (raw == null) {
- return null;
- }
- if (raw instanceof WrappedFileChannel) {
- return ((WrappedFileChannel) raw).delegate;
- }
- return raw;
- }
-
static class WrappedFileChannel extends FileChannel implements SeekableByteChannel {
final FileChannel delegate;
+ final boolean deleteOnClose;
+ final boolean appendMode;
+ final Path path;
public static FileChannel wrap(FileChannel channel) {
if (channel instanceof WrappedFileChannel) {
return channel;
}
- return new WrappedFileChannel(channel);
+ return new WrappedFileChannel(channel, false, false, null);
}
- private WrappedFileChannel(FileChannel delegate) {
+ public static FileChannel withExtraOptions(
+ FileChannel channel, Set<? extends OpenOption> options, Path path) {
+ FileChannel raw =
+ channel instanceof WrappedFileChannel ? ((WrappedFileChannel) channel).delegate : channel;
+ return new WrappedFileChannel(
+ raw,
+ options.contains(StandardOpenOption.DELETE_ON_CLOSE),
+ options.contains(StandardOpenOption.APPEND),
+ path);
+ }
+
+ private WrappedFileChannel(
+ FileChannel delegate, boolean deleteOnClose, boolean appendMode, Path path) {
this.delegate = delegate;
+ this.deleteOnClose = deleteOnClose;
+ this.appendMode = appendMode;
+ this.path = deleteOnClose ? path : null;
}
@Override
@@ -88,6 +86,9 @@
@Override
public int write(ByteBuffer src) throws IOException {
+ if (appendMode) {
+ return delegate.write(src, size());
+ }
return delegate.write(src);
}
@@ -148,20 +149,57 @@
return delegate.map(mode, position, size);
}
+ // When using lock or tryLock the extra options are lost.
@Override
public FileLock lock(long position, long size, boolean shared) throws IOException {
- return delegate.lock(position, size, shared);
+ return wrapLock(delegate.lock(position, size, shared));
}
@Override
public FileLock tryLock(long position, long size, boolean shared) throws IOException {
- return delegate.tryLock(position, size, shared);
+ return wrapLock(delegate.tryLock(position, size, shared));
+ }
+
+ private FileLock wrapLock(FileLock lock) {
+ if (lock == null) {
+ return null;
+ }
+ return new WrappedFileChannelFileLock(lock, this);
}
@Override
public void implCloseChannel() throws IOException {
// We cannot call the protected method, this should be effectively equivalent.
delegate.close();
+ if (deleteOnClose) {
+ Files.deleteIfExists(path);
+ }
+ }
+ }
+
+ /**
+ * The FileLock state is final and duplicated in the wrapper, besides the FileChannel where the
+ * wrapped file channel is used. All methods in FileLock, even channel(), use the duplicated and
+ * corrected state. Only 2 methods require to dispatch to the delegate which is effectively
+ * holding the lock.
+ */
+ static class WrappedFileChannelFileLock extends FileLock {
+
+ private final FileLock delegate;
+
+ WrappedFileChannelFileLock(FileLock delegate, WrappedFileChannel wrappedFileChannel) {
+ super(wrappedFileChannel, delegate.position(), delegate.size(), delegate.isShared());
+ this.delegate = delegate;
+ }
+
+ @Override
+ public boolean isValid() {
+ return delegate.isValid();
+ }
+
+ @Override
+ public void release() throws IOException {
+ delegate.release();
}
}
@@ -205,15 +243,13 @@
randomAccessFile.setLength(0);
}
- if (!openOptions.contains(StandardOpenOption.APPEND)) {
+ if (!openOptions.contains(StandardOpenOption.APPEND)
+ && !openOptions.contains(StandardOpenOption.DELETE_ON_CLOSE)) {
// This one may be retargeted, below 24, to support SeekableByteChannel.
return randomAccessFile.getChannel();
}
- // TODO(b/259056135): Consider subclassing UnsupportedOperationException for desugared library.
- // RandomAccessFile does not support APPEND.
- // We could hack a wrapper to support APPEND in simple cases such as Files.write().
- throw new UnsupportedOperationException("APPEND not supported below api 26.");
+ return WrappedFileChannel.withExtraOptions(randomAccessFile.getChannel(), openOptions, path);
}
private static void validateOpenOptions(Path path, Set<? extends OpenOption> openOptions)
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs_nio.json b/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
index f4a3aef..4bcf1a3 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
@@ -355,12 +355,9 @@
"java.util.stream.LongStream java.util.stream.Stream#flatMapToLong(java.util.function.Function)": [0, "java.util.function.Function java.util.stream.FlatMapApiFlips#flipFunctionReturningStream(java.util.function.Function)"],
"java.util.stream.LongStream java.util.stream.LongStream#flatMap(java.util.function.LongFunction)": [0, "java.util.function.LongFunction java.util.stream.FlatMapApiFlips#flipFunctionReturningStream(java.util.function.LongFunction)"],
"java.lang.Object java.lang.StackWalker#walk(java.util.function.Function)": [0, "java.util.function.Function java.util.stream.StackWalkerApiFlips#flipFunctionStream(java.util.function.Function)"],
- "java.nio.channels.FileChannel java.nio.channels.FileLock#channel()": [-1, "java.nio.channels.FileChannel java.nio.channels.DesugarChannels#convertMaybeLegacyFileChannelFromLibrary(java.nio.channels.FileChannel)"],
- "java.nio.channels.Channel java.nio.channels.FileLock#acquiredBy()": [-1, "java.nio.channels.Channel java.nio.channels.DesugarChannels#convertMaybeLegacyChannelFromLibrary(java.nio.channels.Channel)"],
"java.nio.channels.FileChannel java.io.RandomAccessFile#getChannel()": [-1, "java.nio.channels.FileChannel java.nio.channels.DesugarChannels#convertMaybeLegacyFileChannelFromLibrary(java.nio.channels.FileChannel)"],
"java.nio.channels.FileChannel java.io.FileInputStream#getChannel()": [-1, "java.nio.channels.FileChannel java.nio.channels.DesugarChannels#convertMaybeLegacyFileChannelFromLibrary(java.nio.channels.FileChannel)"],
- "java.nio.channels.FileChannel java.io.FileOutputStream#getChannel()": [-1, "java.nio.channels.FileChannel java.nio.channels.DesugarChannels#convertMaybeLegacyFileChannelFromLibrary(java.nio.channels.FileChannel)"],
- "void java.nio.channels.FileLock#<init>(java.nio.channels.FileChannel,long, long, boolean)": [0, "java.nio.channels.FileChannel java.nio.channels.DesugarChannels#convertMaybeLegacyFileChannelToLibrary(java.nio.channels.FileChannel)"]
+ "java.nio.channels.FileChannel java.io.FileOutputStream#getChannel()": [-1, "java.nio.channels.FileChannel java.nio.channels.DesugarChannels#convertMaybeLegacyFileChannelFromLibrary(java.nio.channels.FileChannel)"]
},
"never_outline_api": [
"java.lang.Object java.lang.StackWalker#walk(java.util.function.Function)"
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FileLockTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FileLockTest.java
new file mode 100644
index 0000000..ae36385
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FileLockTest.java
@@ -0,0 +1,163 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary.jdk11;
+
+import static com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification.DEFAULT_SPECIFICATIONS;
+import static com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification.JDK11_PATH;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
+import com.android.tools.r8.desugar.desugaredlibrary.test.CompilationSpecification;
+import com.android.tools.r8.desugar.desugaredlibrary.test.LibraryDesugaringSpecification;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.List;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class FileLockTest extends DesugaredLibraryTestBase {
+
+ private static final String EXPECTED_RESULT =
+ StringUtils.lines(
+ "true",
+ "true",
+ "pos:2;sz:7;sh:false",
+ "true",
+ "true",
+ "true",
+ "pos:2;sz:7;sh:false",
+ "false",
+ "true",
+ "true",
+ "pos:2;sz:7;sh:false",
+ "true");
+
+ private final TestParameters parameters;
+ private final LibraryDesugaringSpecification libraryDesugaringSpecification;
+ private final CompilationSpecification compilationSpecification;
+
+ @Parameters(name = "{0}, spec: {1}, {2}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ // Skip Android 4.4.4 due to missing libjavacrypto.
+ getTestParameters()
+ .withCfRuntime(CfVm.JDK11)
+ .withDexRuntime(Version.V4_0_4)
+ .withDexRuntimesStartingFromIncluding(Version.V5_1_1)
+ .withAllApiLevels()
+ .build(),
+ ImmutableList.of(JDK11_PATH),
+ DEFAULT_SPECIFICATIONS);
+ }
+
+ public FileLockTest(
+ TestParameters parameters,
+ LibraryDesugaringSpecification libraryDesugaringSpecification,
+ CompilationSpecification compilationSpecification) {
+ this.parameters = parameters;
+ this.libraryDesugaringSpecification = libraryDesugaringSpecification;
+ this.compilationSpecification = compilationSpecification;
+ }
+
+ @Test
+ public void test() throws Throwable {
+ if (parameters.isCfRuntime()) {
+ // Reference runtime, we use Jdk 11 since this is Jdk 11 desugared library, not that Jdk 8
+ // behaves differently on this test.
+ Assume.assumeTrue(parameters.isCfRuntime(CfVm.JDK11) && !ToolHelper.isWindows());
+ testForJvm()
+ .addInnerClasses(getClass())
+ .run(parameters.getRuntime(), TestClass.class, "10000")
+ .assertSuccessWithOutput(EXPECTED_RESULT);
+ return;
+ }
+ testForDesugaredLibrary(parameters, libraryDesugaringSpecification, compilationSpecification)
+ .addInnerClasses(getClass())
+ .addKeepMainRule(TestClass.class)
+ .compile()
+ .withArt6Plus64BitsLib()
+ .run(
+ parameters.getRuntime(),
+ TestClass.class,
+ String.valueOf(parameters.getApiLevel().getLevel()))
+ .assertSuccessWithOutput(EXPECTED_RESULT);
+ }
+
+ public static class TestClass {
+
+ private static int minApi;
+
+ public static void main(String[] args) throws Throwable {
+ minApi = Integer.parseInt(args[0]);
+ appendTest();
+ deleteTest();
+ fosTest();
+ }
+
+ private static void checkChannel(FileChannel channel) throws IOException {
+ FileLock lock = channel.lock(2, 7, false);
+ System.out.println(lock.channel() == channel);
+ checkAcquiredBy(lock, channel);
+ System.out.println(
+ "pos:" + lock.position() + ";sz:" + lock.size() + ";sh:" + lock.isShared());
+ lock.release();
+ channel.close();
+ }
+
+ private static void checkAcquiredBy(FileLock lock, FileChannel channel) {
+ // The method acquiredBy was introduced in 24, we do not backport or check below.
+ if (minApi >= 24) {
+ System.out.println((lock.acquiredBy() == channel));
+ } else {
+ System.out.println("true");
+ }
+ }
+
+ private static void appendTest() throws IOException {
+ Path tmpAppend = Files.createTempFile("tmp_append", ".txt");
+ Files.write(tmpAppend, "There will be dragons!".getBytes(StandardCharsets.UTF_8));
+ FileChannel appendChannel =
+ FileChannel.open(tmpAppend, StandardOpenOption.APPEND, StandardOpenOption.WRITE);
+ checkChannel(appendChannel);
+ System.out.println(Files.exists(tmpAppend));
+ Files.delete(tmpAppend);
+ }
+
+ private static void deleteTest() throws IOException {
+ Path tmpDelete = Files.createTempFile("tmp_delete", ".txt");
+ Files.write(tmpDelete, "There will be dragons!".getBytes(StandardCharsets.UTF_8));
+ FileChannel deleteChannel =
+ FileChannel.open(tmpDelete, StandardOpenOption.DELETE_ON_CLOSE, StandardOpenOption.WRITE);
+ checkChannel(deleteChannel);
+ System.out.println(Files.exists(tmpDelete));
+ Files.deleteIfExists(tmpDelete);
+ }
+
+ private static void fosTest() throws IOException {
+ Path tmpFOS = Files.createTempFile("tmp_fos", ".txt");
+ Files.write(tmpFOS, "There will be dragons!".getBytes(StandardCharsets.UTF_8));
+ FileOutputStream fileOutputStream = new FileOutputStream(tmpFOS.toFile());
+ FileChannel fosChannel = fileOutputStream.getChannel();
+ checkChannel(fosChannel);
+ System.out.println(Files.exists(tmpFOS));
+ Files.delete(tmpFOS);
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesInOutTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesInOutTest.java
index f813ac3..fe483f8 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesInOutTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdk11/FilesInOutTest.java
@@ -51,9 +51,9 @@
"buffWrite[WRITE]:cwHello!",
"newByte[WRITE]:c6",
"inStream[APPEND]:class java.lang.UnsupportedOperationException :: 'APPEND' not allowed",
- "outStream[APPEND]:%s",
- "buffWrite[APPEND]:%s",
- "newByte[APPEND]:%s",
+ "outStream[APPEND]:cwHello!Game over!",
+ "buffWrite[APPEND]:cwHello!",
+ "newByte[APPEND]:c6",
"inStream[TRUNCATE_EXISTING]:cHello",
"outStream[TRUNCATE_EXISTING]:cwGame over!",
"buffWrite[TRUNCATE_EXISTING]:cwclass java.lang.IndexOutOfBoundsException :: index 0,"
@@ -125,11 +125,9 @@
"outStream[DSYNC]:class java.nio.file.NoSuchFileException :: notExisting",
"buffWrite[DSYNC]:class java.nio.file.NoSuchFileException :: notExisting",
"newByte[DSYNC]:class java.nio.file.NoSuchFileException :: notExisting");
+
private static final String[] EXPECTED_RESULT_NO_DESUGARING =
new String[] {
- "cwHello!Game over!",
- "cwHello!",
- "c6",
"cwclass java.nio.file.NoSuchFileException :: example",
"cwclass java.nio.file.NoSuchFileException :: example",
"class java.nio.file.NoSuchFileException :: notExisting",
@@ -140,11 +138,6 @@
private static final String[] EXPECTED_RESULT_DESUGARING =
new String[] {
- // APPEND not supported.
- "class java.lang.UnsupportedOperationException :: APPEND not supported below api 26.",
- "class java.lang.UnsupportedOperationException :: APPEND not supported below api 26.",
- "class java.lang.UnsupportedOperationException :: APPEND not supported below api 26.",
- // DELETE_ON_CLOSE not supported.
"cwGame over!",
"cwHello!",
// In some cases the desugaring version raises FileNotFoundException instead of