Remove dep on safeopen (#1522)

This repo's readme recommends using the newer APIs from go itself for
this instead.
diff --git a/MODULE.bazel b/MODULE.bazel
index 4ce46ea..9aeecba 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -20,15 +20,10 @@
 
 go_deps = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps")
 go_deps.from_file(go_mod = "//:go.mod")
-go_deps.gazelle_override(
-    build_file_generation = "clean",
-    path = "github.com/google/safeopen",
-)
 use_repo(
     go_deps,
     "com_github_golang_protobuf",
     "com_github_google_go_cmp",
-    "com_github_google_safeopen",
     "net_starlark_go",
     "org_golang_google_protobuf",
     "org_golang_x_tools",
diff --git a/WORKSPACE b/WORKSPACE
index db09952..5383fc1 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -15,7 +15,7 @@
 
 go_rules_dependencies()
 
-go_register_toolchains(version = "1.20.3")
+go_register_toolchains(version = "1.24.0")
 
 http_archive(
     name = "bazel_gazelle",
diff --git a/file/BUILD.bazel b/file/BUILD.bazel
index edaafa2..21f5552 100644
--- a/file/BUILD.bazel
+++ b/file/BUILD.bazel
@@ -1,14 +1,11 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
 
 go_library(
     name = "file",
     srcs = ["file.go"],
     importpath = "github.com/bazel-contrib/buildtools/v10/file",
     visibility = ["//visibility:public"],
-    deps = [
-        "//wspace",
-        "@com_github_google_safeopen//:safeopen",
-    ],
+    deps = ["//wspace"],
 )
 
 alias(
@@ -16,3 +13,9 @@
     actual = ":file",
     visibility = ["//visibility:public"],
 )
+
+go_test(
+    name = "file_test",
+    srcs = ["file_test.go"],
+    embed = [":file"],
+)
diff --git a/file/file.go b/file/file.go
index cbf387a..5b2331f 100644
--- a/file/file.go
+++ b/file/file.go
@@ -25,7 +25,6 @@
 	"runtime"
 
 	"github.com/bazel-contrib/buildtools/v10/wspace"
-	"github.com/google/safeopen"
 )
 
 // ReadFile can be updated from the caller to change the API
@@ -74,7 +73,7 @@
 		if err != nil {
 			return err
 		}
-		return safeopen.WriteFileBeneath(wsRoot, relPath, data, mode)
+		return writeFileBeneath(wsRoot, relPath, data, mode)
 	}
 	dir, file := filepath.Split(name)
 	absDir, err := filepath.Abs(dir)
@@ -82,7 +81,26 @@
 		return err
 	}
 	// If we are not in a workspace, we only allow writes to the directory where the file is located.
-	return safeopen.WriteFileBeneath(absDir, file, data, mode)
+	return writeFileBeneath(absDir, file, data, mode)
+}
+
+func writeFileBeneath(dir, name string, data []byte, mode os.FileMode) error {
+	root, err := os.OpenRoot(dir)
+	if err != nil {
+		return err
+	}
+	defer root.Close()
+
+	f, err := root.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
+	if err != nil {
+		return err
+	}
+	_, err = f.Write(data)
+	if err != nil {
+		f.Close()
+		return err
+	}
+	return f.Close()
 }
 
 // openReadFile is like os.Open.
diff --git a/file/file_test.go b/file/file_test.go
new file mode 100644
index 0000000..531dd68
--- /dev/null
+++ b/file/file_test.go
@@ -0,0 +1,51 @@
+/*
+Copyright 2026 Google LLC
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package file
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+)
+
+func TestWriteFileBeneath(t *testing.T) {
+	root := t.TempDir()
+	outside := t.TempDir()
+	if err := os.Mkdir(filepath.Join(root, "subdir"), 0755); err != nil {
+		t.Fatal(err)
+	}
+	if err := os.Symlink("subdir", filepath.Join(root, "inside")); err != nil {
+		t.Fatal(err)
+	}
+	if err := os.Symlink(outside, filepath.Join(root, "outside")); err != nil {
+		t.Fatal(err)
+	}
+
+	if err := writeFileBeneath(root, "inside/file", []byte("data"), 0644); err != nil {
+		t.Fatalf("write through symlink inside root: %v", err)
+	}
+	if got, err := os.ReadFile(filepath.Join(root, "subdir", "file")); err != nil || string(got) != "data" {
+		t.Fatalf("read written file: got %q, %v", got, err)
+	}
+
+	if err := writeFileBeneath(root, "outside/file", []byte("data"), 0644); err == nil {
+		t.Fatal("write through symlink outside root succeeded")
+	}
+	if _, err := os.Stat(filepath.Join(outside, "file")); !os.IsNotExist(err) {
+		t.Fatalf("file was created outside root: %v", err)
+	}
+}
diff --git a/go.mod b/go.mod
index b925f56..de02dda 100644
--- a/go.mod
+++ b/go.mod
@@ -7,14 +7,10 @@
 require (
 	github.com/golang/protobuf v1.5.4
 	github.com/google/go-cmp v0.7.0
-	github.com/google/safeopen v0.0.0-20260327150837-43626d6f4685
 	go.starlark.net v0.0.0-20210223155950-e043a3d3c984
 	google.golang.org/protobuf v1.36.10
 )
 
-require (
-	golang.org/x/sys v0.48.0 // indirect
-	golang.org/x/tools v0.50.0 // indirect
-)
+require golang.org/x/tools v0.50.0 // indirect
 
 tool golang.org/x/tools/cmd/goyacc
diff --git a/go.sum b/go.sum
index eb4b975..5022688 100644
--- a/go.sum
+++ b/go.sum
@@ -27,8 +27,6 @@
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/google/safeopen v0.0.0-20260327150837-43626d6f4685 h1:NcJjfIYRDHuboRtptwjtdQflVDKgXSqTIfwu9PpE9uo=
-github.com/google/safeopen v0.0.0-20260327150837-43626d6f4685/go.mod h1:D59KewtQCiD2Avi8N/v2zb/xTYaefwJl+ux2ejB58GQ=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 go.starlark.net v0.0.0-20210223155950-e043a3d3c984 h1:xwwDQW5We85NaTk2APgoN9202w/l0DVGp+GZMfsrh7s=
 go.starlark.net v0.0.0-20210223155950-e043a3d3c984/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0=