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