Repository: denoland/deno
Pull request: fix(ext/node): support O_RDONLY | O_CREAT in fs.openSync

## Description

POSIX allows `O_RDONLY | O_CREAT` to create a file if it doesn't exist and then open it for reading only. Node.js and other runtimes support this combination. However, Rust's `std::fs::OpenOptions` requires write or append access when `create` is set, causing Deno to throw:

```
error: Uncaught (in promise) TypeError: creating or truncating a file requires write or append access
```

## Changes

- **`ext/fs/std_fs.rs`**: Handle the `create && !write && !append` case in `open_with_checked_path` by first creating the file (if it doesn't exist) via a temporary write-mode open, then reopening for read-only access.
- **`ext/node/ops/fs.rs`**: Update `open_options_to_access_kind` to include `create` flag in the write access check, ensuring proper permissions are requested.
- **`tests/unit_node/_fs/_fs_open_test.ts`**: Add tests for `O_RDONLY | O_CREAT` with both new and existing files.

## Reproduction

```js
import { openSync, closeSync, constants } from "node:fs";
const fd = openSync("./new_file.txt", constants.O_RDONLY | constants.O_CREAT);
closeSync(fd);
```

This works in Node.js and Bun but throws in Deno.

Fixes #32107

Changed files:
filename: ext/fs/std_fs.rs
patch: @@ -1029,13 +1029,49 @@ fn open_options(options: OpenOptions) -> fs::OpenOptions {
   open_options
 }
 
-#[inline(always)]
 pub fn open_with_checked_path(
-  options: OpenOptions,
+  opts: OpenOptions,
   path: &CheckedPath,
 ) -> FsResult<std::fs::File> {
-  let opts = open_options_for_checked_path(options, path);
-  Ok(opts.open(path)?)
+  // Rust's std::fs::OpenOptions requires write or append when create is set.
+  // However, POSIX allows O_RDONLY | O_CREAT (create the file if it doesn't
+  // exist, then open for reading). Handle this by creating the file first
+  // if needed, then opening for read only.
+  if opts.create && !opts.write && !opts.append {
+    if !path.exists() {
+      let create_opts = open_options_for_checked_path(
+        OpenOptions {
+          read: false,
+          write: true,
+          create: true,
+          truncate: false,
+          append: false,
+          create_new: opts.create_new,
+          custom_flags: None,
+          mode: opts.mode,
+        },
+        path,
+      );
+      // Create and immediately close the file
+      drop(create_opts.open(path)?);
+    }
+    let read_opts = open_options_for_checked_path(
+      OpenOptions {
+        read: true,
+        write: false,
+        create: false,
+        truncate: false,
+        append: false,
+        create_new: false,
+        custom_flags: opts.custom_flags,
+        mode: None,
+      },
+      path,
+    );
+    return Ok(read_opts.open(path)?);
+  }
+  let std_opts = open_options_for_checked_path(opts, path);
+  Ok(std_opts.open(path)?)
 }
 
 #[inline(always)]

filename: ext/node/ops/fs.rs
patch: @@ -87,7 +87,7 @@ fn get_open_options(flags: i32, mode: Option<u32>) -> OpenOptions {
 
 fn open_options_to_access_kind(open_options: &OpenOptions) -> OpenAccessKind {
   let read = open_options.read;
-  let write = open_options.write || open_options.append;
+  let write = open_options.write || open_options.append || open_options.create;
   match (read, write) {
     (true, true) => OpenAccessKind::ReadWrite,
     (false, true) => OpenAccessKind::Write,

filename: tests/unit_node/_fs/_fs_open_test.ts
patch: @@ -566,3 +566,52 @@ Deno.test(
     await Deno.remove(path);
   },
 );
+
+Deno.test({
+  name:
+    "open with numeric flag `O_RDONLY | O_CREAT` creates file and opens for reading",
+  fn() {
+    const file = join(tempDir, "o_rdonly_o_creat_test");
+    // Ensure file doesn't exist
+    try {
+      Deno.removeSync(file);
+    } catch {
+      // ignore
+    }
+    assertEquals(existsSync(file), false);
+
+    // O_RDONLY | O_CREAT should create the file if it doesn't exist,
+    // then open it for reading only (matching POSIX / Node.js behavior)
+    const fd = openSync(file, O_RDONLY | O_CREAT);
+    assertEquals(typeof fd, "number");
+    assertEquals(existsSync(file), true);
+
+    // The file should be empty
+    const buf = new Uint8Array(10);
+    const bytesRead = readSync(fd, buf, 0, 10, 0);
+    assertEquals(bytesRead, 0);
+
+    closeSync(fd);
+    Deno.removeSync(file);
+  },
+});
+
+Deno.test({
+  name:
+    "open with numeric flag `O_RDONLY | O_CREAT` opens existing file for reading",
+  fn() {
+    const file = Deno.makeTempFileSync();
+    Deno.writeTextFileSync(file, "hello");
+
+    const fd = openSync(file, O_RDONLY | O_CREAT);
+    assertEquals(typeof fd, "number");
+
+    // Should be able to read the existing content
+    const buf = new Uint8Array(10);
+    const bytesRead = readSync(fd, buf, 0, 10, 0);
+    assertEquals(bytesRead, 5);
+
+    closeSync(fd);
+    Deno.removeSync(file);
+  },
+});