os: remap / to \ in Windows os.Root.Symlink targets

Matches os.Symlink behavior. Windows accepts forward and backward slash
as interchangeable in most contexts, but not in symlink targets.

Fixes #80073

Change-Id: I62fdf301c7a2e713faaa5fbe4654d06e6a6a6964
Reviewed-on: https://go-review.googlesource.com/c/go/+/793020
Reviewed-by: Quim Muntal <quimmuntal@gmail.com>
Auto-Submit: Damien Neil <dneil@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/src/os/root_windows.go b/src/os/root_windows.go
index ca3bea8..362963d 100644
--- a/src/os/root_windows.go
+++ b/src/os/root_windows.go
@@ -242,6 +242,10 @@
 		return syscall.EINVAL
 	}
 
+	// Windows treats / and \ as equivalent almost everywhere, but not in symlink targets.
+	// Match os.Symlink behavior and convert / to \.
+	oldname = filepathlite.FromSlash(oldname)
+
 	// CreateSymbolicLinkW converts volume-relative paths into absolute ones.
 	// Do the same.
 	if filepathlite.VolumeNameLen(oldname) > 0 && !filepathlite.IsAbs(oldname) {
diff --git a/src/os/root_windows_test.go b/src/os/root_windows_test.go
index f393ad4..ea604d1 100644
--- a/src/os/root_windows_test.go
+++ b/src/os/root_windows_test.go
@@ -235,6 +235,31 @@
 	}
 }
 
+func TestRootSymlinkNormalization(t *testing.T) {
+	if !testenv.HasSymlink() {
+		t.Skip("skipping test; no symlink support")
+	}
+	const content = "dir/target" // same as file name
+	dir := makefs(t, []string{
+		"dir/target",
+	})
+	root, err := os.OpenRoot(dir)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer root.Close()
+	if err := root.Symlink("dir/target", "link"); err != nil {
+		t.Fatal(err)
+	}
+	got, err := os.ReadFile(dir + "/link")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if string(got) != content {
+		t.Fatalf("read link contents %q, want %q", got, content)
+	}
+}
+
 func TestRootOpenFileTruncateNamedPipe(t *testing.T) {
 	t.Parallel()
 	name := pipeName()