/*
   Copyright The containerd Authors.

   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

       http://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 os

import (
	"os"
	"strings"
	"sync"
	"unicode/utf16"

	"golang.org/x/sys/windows"
)

// openPath takes a path, opens it, and returns the resulting handle.
// It works for both file and directory paths.
//
// We are not able to use builtin Go functionality for opening a directory path:
//   - os.Open on a directory returns a os.File where Fd() is a search handle from FindFirstFile.
//   - syscall.Open does not provide a way to specify FILE_FLAG_BACKUP_SEMANTICS, which is needed to
//     open a directory.
//
// We could use os.Open if the path is a file, but it's easier to just use the same code for both.
// Therefore, we call windows.CreateFile directly.
func openPath(path string) (windows.Handle, error) {
	u16, err := windows.UTF16PtrFromString(path)
	if err != nil {
		return 0, err
	}
	h, err := windows.CreateFile(
		u16,
		0,
		windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
		nil,
		windows.OPEN_EXISTING,
		windows.FILE_FLAG_BACKUP_SEMANTICS, // Needed to open a directory handle.
		0)
	if err != nil {
		return 0, &os.PathError{
			Op:   "CreateFile",
			Path: path,
			Err:  err,
		}
	}
	return h, nil
}

// GetFinalPathNameByHandle flags.
//
//nolint:revive // SNAKE_CASE is not idiomatic in Go, but aligned with Win32 API.
const (
	cFILE_NAME_OPENED = 0x8

	cVOLUME_NAME_DOS  = 0x0
	cVOLUME_NAME_GUID = 0x1
)

var pool = sync.Pool{
	New: func() interface{} {
		// Size of buffer chosen somewhat arbitrarily to accommodate a large number of path strings.
		// MAX_PATH (260) + size of volume GUID prefix (49) + null terminator = 310.
		b := make([]uint16, 310)
		return &b
	},
}

// getFinalPathNameByHandle facilitates calling the Windows API GetFinalPathNameByHandle
// with the given handle and flags. It transparently takes care of creating a buffer of the
// correct size for the call.
func getFinalPathNameByHandle(h windows.Handle, flags uint32) (string, error) {
	b := *(pool.Get().(*[]uint16))
	defer func() { pool.Put(&b) }()
	for {
		n, err := windows.GetFinalPathNameByHandle(h, &b[0], uint32(len(b)), flags)
		if err != nil {
			return "", err
		}
		// If the buffer wasn't large enough, n will be the total size needed (including null terminator).
		// Resize and try again.
		if n > uint32(len(b)) {
			b = make([]uint16, n)
			continue
		}
		// If the buffer is large enough, n will be the size not including the null terminator.
		// Convert to a Go string and return.
		return string(utf16.Decode(b[:n])), nil
	}
}

// resolvePath implements path resolution for Windows. It attempts to return the "real" path to the
// file or directory represented by the given path.
// The resolution works by using the Windows API GetFinalPathNameByHandle, which takes a handle and
// returns the final path to that file.
func resolvePath(path string) (string, error) {
	h, err := openPath(path)
	if err != nil {
		return "", err
	}
	defer windows.CloseHandle(h)

	// We use the Windows API GetFinalPathNameByHandle to handle path resolution. GetFinalPathNameByHandle
	// returns a resolved path name for a file or directory. The returned path can be in several different
	// formats, based on the flags passed. There are several goals behind the design here:
	// - Do as little manual path manipulation as possible. Since Windows path formatting can be quite
	//   complex, we try to just let the Windows APIs handle that for us.
	// - Retain as much compatibility with existing Go path functions as we can. In particular, we try to
	//   ensure paths returned from resolvePath can be passed to EvalSymlinks.
	//
	// First, we query for the VOLUME_NAME_GUID path of the file. This will return a path in the form
	// "\\?\Volume{8a25748f-cf34-4ac6-9ee2-c89400e886db}\dir\file.txt". If the path is a UNC share
	// (e.g. "\\server\share\dir\file.txt"), then the VOLUME_NAME_GUID query will fail with ERROR_PATH_NOT_FOUND.
	// In this case, we will next try a VOLUME_NAME_DOS query. This query will return a path for a UNC share
	// in the form "\\?\UNC\server\share\dir\file.txt". This path will work with most functions, but EvalSymlinks
	// fails on it. Therefore, we rewrite the path to the form "\\server\share\dir\file.txt" before returning it.
	// This path rewrite may not be valid in all cases (see the notes in the next paragraph), but those should
	// be very rare edge cases, and this case wouldn't have worked with EvalSymlinks anyways.
	//
	// The "\\?\" prefix indicates that no path parsing or normalization should be performed by Windows.
	// Instead the path is passed directly to the object manager. The lack of parsing means that "." and ".." are
	// interpreted literally and "\"" must be used as a path separator. Additionally, because normalization is
	// not done, certain paths can only be represented in this format. For instance, "\\?\C:\foo." (with a trailing .)
	// cannot be written as "C:\foo.", because path normalization will remove the trailing ".".
	//
	// We use FILE_NAME_OPENED instead of FILE_NAME_NORMALIZED, as FILE_NAME_NORMALIZED can fail on some
	// UNC paths based on access restrictions. The additional normalization done is also quite minimal in
	// most cases.
	//
	// Querying for VOLUME_NAME_DOS first instead of VOLUME_NAME_GUID would yield a "nicer looking" path in some cases.
	// For instance, it could return "\\?\C:\dir\file.txt" instead of "\\?\Volume{8a25748f-cf34-4ac6-9ee2-c89400e886db}\dir\file.txt".
	// However, we query for VOLUME_NAME_GUID first for two reasons:
	// - The volume GUID path is more stable. A volume's mount point can change when it is remounted, but its
	//   volume GUID should not change.
	// - If the volume is mounted at a non-drive letter path (e.g. mounted to "C:\mnt"), then VOLUME_NAME_DOS
	//   will return the mount path. EvalSymlinks fails on a path like this due to a bug.
	//
	// References:
	// - GetFinalPathNameByHandle: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfinalpathnamebyhandlea
	// - Naming Files, Paths, and Namespaces: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
	// - Naming a Volume: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-volume

	rPath, err := getFinalPathNameByHandle(h, cFILE_NAME_OPENED|cVOLUME_NAME_GUID)
	if err == windows.ERROR_PATH_NOT_FOUND {
		// ERROR_PATH_NOT_FOUND is returned from the VOLUME_NAME_GUID query if the path is a
		// network share (UNC path). In this case, query for the DOS name instead, then translate
		// the returned path to make it more palatable to other path functions.
		rPath, err = getFinalPathNameByHandle(h, cFILE_NAME_OPENED|cVOLUME_NAME_DOS)
		if err != nil {
			return "", err
		}
		if strings.HasPrefix(rPath, `\\?\UNC\`) {
			// Convert \\?\UNC\server\share -> \\server\share. The \\?\UNC syntax does not work with
			// some Go filepath functions such as EvalSymlinks. In the future if other components
			// move away from EvalSymlinks and use GetFinalPathNameByHandle instead, we could remove
			// this path munging.
			rPath = `\\` + rPath[len(`\\?\UNC\`):]
		}
	} else if err != nil {
		return "", err
	}
	return rPath, nil
}

// ResolveSymbolicLink will follow any symbolic links
func (RealOS) ResolveSymbolicLink(path string) (string, error) {
	// filepath.EvalSymlinks does not work very well on Windows, so instead we resolve the path
	// via resolvePath which uses GetFinalPathNameByHandle. This returns either a path prefixed with `\\?\`,
	// or a remote share path in the form \\server\share. These should work with most Go and Windows APIs.
	return resolvePath(path)
}
