// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build windows

package os_test

import (
	"errors"
	"fmt"
	"internal/strconv"
	"internal/syscall/windows"
	"internal/testenv"
	"os"
	"path/filepath"
	"syscall"
	"testing"
	"unsafe"
)

// Verify that Root.Open rejects Windows reserved names.
func TestRootWindowsDeviceNames(t *testing.T) {
	r, err := os.OpenRoot(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	defer r.Close()
	if f, err := r.Open("NUL"); err == nil {
		t.Errorf(`r.Open("NUL") succeeded; want error"`)
		f.Close()
	}
}

// Verify that Root.Open is case-insensitive.
// (The wrong options to NtOpenFile could make operations case-sensitive,
// so this is worth checking.)
func TestRootWindowsCaseInsensitivity(t *testing.T) {
	dir := t.TempDir()
	if err := os.WriteFile(filepath.Join(dir, "file"), nil, 0666); err != nil {
		t.Fatal(err)
	}
	r, err := os.OpenRoot(dir)
	if err != nil {
		t.Fatal(err)
	}
	defer r.Close()
	f, err := r.Open("FILE")
	if err != nil {
		t.Fatal(err)
	}
	f.Close()
	if err := r.Remove("FILE"); err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(filepath.Join(dir, "file")); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("os.Stat(file) after deletion: %v, want ErrNotFound", err)
	}
}

// TestRootSymlinkRelativity tests that symlinks created using Root.Symlink have the
// same SYMLINK_FLAG_RELATIVE value as ones creates using os.Symlink.
func TestRootSymlinkRelativity(t *testing.T) {
	testenv.MustHaveSymlink(t)

	dir := t.TempDir()
	root, err := os.OpenRoot(dir)
	if err != nil {
		t.Fatal(err)
	}
	defer root.Close()

	for i, test := range []struct {
		name   string
		target string
	}{{
		name:   "relative",
		target: `foo`,
	}, {
		name:   "absolute",
		target: `C:\foo`,
	}, {
		name:   "current working directory-relative",
		target: `C:foo`,
	}, {
		name:   "root-relative",
		target: `\foo`,
	}, {
		name:   "question prefix",
		target: `\\?\foo`,
	}, {
		name:   "relative with dot dot",
		target: `a\..\b`, // could be cleaned (but isn't)
	}} {
		t.Run(test.name, func(t *testing.T) {
			name := fmt.Sprintf("symlink_%v", i)
			if err := os.Symlink(test.target, filepath.Join(dir, name)); err != nil {
				t.Fatal(err)
			}
			if err := root.Symlink(test.target, name+"_at"); err != nil {
				t.Fatal(err)
			}

			osRDB, err := readSymlinkReparseData(filepath.Join(dir, name))
			if err != nil {
				t.Fatal(err)
			}
			rootRDB, err := readSymlinkReparseData(filepath.Join(dir, name+"_at"))
			if err != nil {
				t.Fatal(err)
			}
			if osRDB.Flags != rootRDB.Flags {
				t.Errorf("symlink target %q: Symlink flags = %x, Root.Symlink flags = %x", test.target, osRDB.Flags, rootRDB.Flags)
			}

			// Compare the link target.
			// os.Symlink converts current working directory-relative links
			// such as c:foo into absolute links.
			osTarget, err := os.Readlink(filepath.Join(dir, name))
			if err != nil {
				t.Fatal(err)
			}
			rootTarget, err := os.Readlink(filepath.Join(dir, name+"_at"))
			if err != nil {
				t.Fatal(err)
			}
			if osTarget != rootTarget {
				t.Errorf("symlink created with target %q: Symlink target = %q, Root.Symlink target = %q", test.target, osTarget, rootTarget)
			}
		})
	}
}

func readSymlinkReparseData(name string) (*windows.SymbolicLinkReparseBuffer, error) {
	nameu16, err := syscall.UTF16FromString(name)
	if err != nil {
		return nil, err
	}
	h, err := syscall.CreateFile(&nameu16[0], syscall.GENERIC_READ, 0, nil, syscall.OPEN_EXISTING,
		syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
	if err != nil {
		return nil, err
	}
	defer syscall.CloseHandle(h)

	var rdbbuf [syscall.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]byte
	var bytesReturned uint32
	err = syscall.DeviceIoControl(h, syscall.FSCTL_GET_REPARSE_POINT, nil, 0, &rdbbuf[0], uint32(len(rdbbuf)), &bytesReturned, nil)
	if err != nil {
		return nil, err
	}

	rdb := (*windows.REPARSE_DATA_BUFFER)(unsafe.Pointer(&rdbbuf[0]))
	if rdb.ReparseTag != syscall.IO_REPARSE_TAG_SYMLINK {
		return nil, fmt.Errorf("%q: not a symlink", name)
	}

	bufoff := unsafe.Offsetof(rdb.DUMMYUNIONNAME)
	symlinkBuf := (*windows.SymbolicLinkReparseBuffer)(unsafe.Pointer(&rdbbuf[bufoff]))

	return symlinkBuf, nil
}

// TestRootSymlinkToDirectory tests that Root.Symlink creates directory links
// when the target is a directory contained within the root.
func TestRootSymlinkToDirectory(t *testing.T) {
	testenv.MustHaveSymlink(t)

	dir := t.TempDir()
	root, err := os.OpenRoot(dir)
	if err != nil {
		t.Fatal(err)
	}
	defer root.Close()

	if err := os.Mkdir(filepath.Join(dir, "dir"), 0777); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dir, "file"), nil, 0666); err != nil {
		t.Fatal(err)
	}

	dir2 := t.TempDir()

	for i, test := range []struct {
		name    string
		target  string
		wantDir bool
	}{{
		name:    "directory outside root",
		target:  dir2,
		wantDir: false,
	}, {
		name:    "directory inside root",
		target:  "dir",
		wantDir: true,
	}, {
		name:    "file inside root",
		target:  "file",
		wantDir: false,
	}, {
		name:    "nonexistent inside root",
		target:  "nonexistent",
		wantDir: false,
	}} {
		t.Run(test.name, func(t *testing.T) {
			name := fmt.Sprintf("symlink_%v", i)
			if err := root.Symlink(test.target, name); err != nil {
				t.Fatal(err)
			}

			// Lstat strips the directory mode bit from reparse points,
			// so we need to use GetFileInformationByHandle directly to
			// determine if this is a directory link.
			nameu16, err := syscall.UTF16PtrFromString(filepath.Join(dir, name))
			if err != nil {
				t.Fatal(err)
			}
			h, err := syscall.CreateFile(nameu16, 0, 0, nil, syscall.OPEN_EXISTING,
				syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
			if err != nil {
				t.Fatal(err)
			}
			defer syscall.CloseHandle(h)
			var fi syscall.ByHandleFileInformation
			if err := syscall.GetFileInformationByHandle(h, &fi); err != nil {
				t.Fatal(err)
			}
			gotDir := fi.FileAttributes&syscall.FILE_ATTRIBUTE_DIRECTORY != 0

			if got, want := gotDir, test.wantDir; got != want {
				t.Errorf("link target %q: isDir = %v, want %v", test.target, got, want)
			}
		})
	}
}

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()
	pipe := newBytePipe(t, name, false)
	defer pipe.Close()

	root, err := os.OpenRoot(filepath.Dir(name))
	if err != nil {
		t.Fatal(err)
	}
	defer root.Close()

	f, err := root.OpenFile(filepath.Base(name), os.O_TRUNC|os.O_RDWR|os.O_CREATE, 0666)
	if err != nil {
		t.Fatal(err)
	}
	f.Close()
}

func TestRootOpenFileFlags(t *testing.T) {
	t.Parallel()

	dir := t.TempDir()
	root, err := os.OpenRoot(dir)
	if err != nil {
		t.Fatal(err)
	}
	defer root.Close()

	// The only way to retrieve some of the flags passed in CreateFile
	// is using NtQueryInformationFile, which returns the file flags
	// NT equivalent. Note that FILE_SYNCHRONOUS_IO_NONALERT is always
	// set when FILE_FLAG_OVERLAPPED is not passed.
	// The flags that can't be retrieved using NtQueryInformationFile won't
	// be tested in here, but we at least know that the logic to handle them is correct.
	tests := []struct {
		flag     uint32
		wantMode uint32
	}{
		{0, windows.FILE_SYNCHRONOUS_IO_NONALERT},
		{windows.O_FILE_FLAG_OVERLAPPED, 0},
		{windows.O_FILE_FLAG_NO_BUFFERING, windows.FILE_NO_INTERMEDIATE_BUFFERING | windows.FILE_SYNCHRONOUS_IO_NONALERT},
		{windows.O_FILE_FLAG_NO_BUFFERING | windows.O_FILE_FLAG_OVERLAPPED, windows.FILE_NO_INTERMEDIATE_BUFFERING},
		{windows.O_FILE_FLAG_SEQUENTIAL_SCAN, windows.FILE_SEQUENTIAL_ONLY | windows.FILE_SYNCHRONOUS_IO_NONALERT},
		{windows.O_FILE_FLAG_WRITE_THROUGH, windows.FILE_WRITE_THROUGH | windows.FILE_SYNCHRONOUS_IO_NONALERT},
	}
	for i, tt := range tests {
		t.Run(strconv.Itoa(i), func(t *testing.T) {
			f, err := root.OpenFile(strconv.Itoa(i)+".txt", syscall.O_RDWR|syscall.O_CREAT|int(tt.flag), 0666)
			if err != nil {
				t.Fatal(err)
			}
			defer f.Close()
			var info windows.FILE_MODE_INFORMATION
			if err := windows.NtQueryInformationFile(syscall.Handle(f.Fd()), &windows.IO_STATUS_BLOCK{},
				unsafe.Pointer(&info), uint32(unsafe.Sizeof(info)), windows.FileModeInformation); err != nil {
				t.Fatal(err)
			}
			if info.Mode != tt.wantMode {
				t.Errorf("file mode = 0x%x; want 0x%x", info.Mode, tt.wantMode)
			}
		})
	}
}

func TestRootOpenFileDeleteOnClose(t *testing.T) {
	t.Parallel()
	dir := t.TempDir()
	root, err := os.OpenRoot(dir)
	if err != nil {
		t.Fatal(err)
	}
	defer root.Close()
	const name = "test.txt"
	f, err := root.OpenFile(name, syscall.O_RDWR|syscall.O_CREAT|windows.O_FILE_FLAG_DELETE_ON_CLOSE, 0666)
	if err != nil {
		t.Fatal(err)
	}
	if err := f.Close(); err != nil {
		t.Fatal(err)
	}
	// The file should be deleted after closing.
	if _, err := os.Stat(filepath.Join(dir, name)); !errors.Is(err, os.ErrNotExist) {
		t.Errorf("expected file to be deleted, got %v", err)
	}
}

func TestRootOpenFileFlagInvalid(t *testing.T) {
	t.Parallel()
	dir := t.TempDir()
	root, err := os.OpenRoot(dir)
	if err != nil {
		t.Fatal(err)
	}
	defer root.Close()
	// invalidFileFlag is the only value in the file flag range that is not supported,
	// as it is not defined in the Windows API.
	const invalidFileFlag = 0x00400000
	f, err := root.OpenFile("test.txt", syscall.O_RDWR|syscall.O_CREAT|invalidFileFlag, 0666)
	if !errors.Is(err, os.ErrInvalid) {
		t.Fatalf("expected os.ErrInvalid, got %v", err)
	}
	f.Close()
}
