//go:build windows

/*
Copyright 2023 The Kubernetes 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 filesystem

import (
	"fmt"
	"net"
	"os"
	"path/filepath"
	"strings"
	"time"

	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/klog/v2"

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

const (
	// Amount of time to wait between attempting to use a Unix domain socket.
	// As detailed in https://github.com/kubernetes/kubernetes/issues/104584
	// the first attempt will most likely fail, hence the need to retry
	socketDialRetryPeriod = 1 * time.Second
	// Overall timeout value to dial a Unix domain socket, including retries
	socketDialTimeout = 4 * time.Second
)

// IsUnixDomainSocket returns whether a given file is a AF_UNIX socket file
// Note that due to the retry logic inside, it could take up to 4 seconds
// to determine whether or not the file path supplied is a Unix domain socket
func IsUnixDomainSocket(filePath string) (bool, error) {
	// Note that querrying for the Reparse Points (https://docs.microsoft.com/en-us/windows/win32/fileio/reparse-points)
	// for the file (using FSCTL_GET_REPARSE_POINT) and checking for reparse tag: reparseTagSocket
	// does NOT work in 1809 if the socket file is created within a bind mounted directory by a container
	// and the FSCTL is issued in the host by the kubelet.

	// If the file does not exist, it cannot be a Unix domain socket.
	if info, err := os.Stat(filePath); os.IsNotExist(err) {
		return false, fmt.Errorf("File %s not found. Err: %v", filePath, err)
	} else if err == nil && info.Mode()&os.ModeSocket != 0 { // Use os.ModeSocket (introduced in Go 1.23 on Windows)
		klog.V(6).InfoS("File identified as a Unix domain socket", "filePath", filePath)
		return true, nil
	}
	klog.V(6).InfoS("Function IsUnixDomainSocket starts", "filePath", filePath)
	// Due to the absence of golang support for os.ModeSocket in Windows (https://github.com/golang/go/issues/33357)
	// we need to dial the file and check if we receive an error to determine if a file is Unix Domain Socket file.
	// As detailed in https://github.com/kubernetes/kubernetes/issues/104584 we cannot rely
	// on the Unix Domain socket working on the very first try, hence the potential need to
	// dial multiple times
	var lastSocketErr error
	err := wait.PollImmediate(socketDialRetryPeriod, socketDialTimeout,
		func() (bool, error) {
			klog.V(6).InfoS("Dialing the socket", "filePath", filePath)
			var c net.Conn
			c, lastSocketErr = net.Dial("unix", filePath)
			if lastSocketErr == nil {
				c.Close()
				klog.V(6).InfoS("Socket dialed successfully", "filePath", filePath)
				return true, nil
			}
			klog.V(6).InfoS("Failed the current attempt to dial the socket, so pausing before retry",
				"filePath", filePath, "err", lastSocketErr, "socketDialRetryPeriod",
				socketDialRetryPeriod)
			return false, nil
		})

	// PollImmediate will return "timed out waiting for the condition" if the function it
	// invokes never returns true
	if err != nil {
		klog.V(2).InfoS("Failed all attempts to dial the socket so marking it as a non-Unix Domain socket. Last socket error along with the error from PollImmediate follow",
			"filePath", filePath, "lastSocketErr", lastSocketErr, "err", err)
		return false, nil
	}
	return true, nil
}

// On Windows os.Mkdir all doesn't set any permissions so call the Chown function below to set
// permissions once the directory is created.
func MkdirAll(path string, perm os.FileMode) error {
	klog.V(6).InfoS("Function MkdirAll starts", "path", path, "perm", perm)
	if _, err := os.Stat(path); err == nil {
		// Path already exists: nothing to do.
		return nil
	} else if !os.IsNotExist(err) {
		return fmt.Errorf("error checking path %s: %w", path, err)
	}

	err := os.MkdirAll(path, perm)
	if err != nil {
		return fmt.Errorf("error creating directory %s: %w", path, err)
	}

	err = Chmod(path, perm)
	if err != nil {
		return fmt.Errorf("error setting permissions for directory %s: %w", path, err)
	}

	return nil
}

const (
	// These aren't defined in the syscall package for Windows :(
	USER_READ      = 0x100
	USER_WRITE     = 0x80
	USER_EXECUTE   = 0x40
	GROUP_READ     = 0x20
	GROUP_WRITE    = 0x10
	GROUP_EXECUTE  = 0x8
	OTHERS_READ    = 0x4
	OTHERS_WRITE   = 0x2
	OTHERS_EXECUTE = 0x1
	USER_ALL       = USER_READ | USER_WRITE | USER_EXECUTE
	GROUP_ALL      = GROUP_READ | GROUP_WRITE | GROUP_EXECUTE
	OTHERS_ALL     = OTHERS_READ | OTHERS_WRITE | OTHERS_EXECUTE
)

// On Windows os.Chmod only sets the read-only flag on files, so we need to use Windows APIs to set the desired access on files / directories.
// The OWNER mode will set file permissions for the file owner SID, the GROUP mode will set file permissions for the file group SID,
// and the OTHERS mode will set file permissions for BUILTIN\Users.
// Please note that Windows containers can be run as one of two user accounts; ContainerUser or ContainerAdministrator.
// Containers run as ContainerAdministrator will inherit permissions from BUILTIN\Administrators,
// while containers run as ContainerUser will inherit permissions from BUILTIN\Users.
// Windows containers do not have the ability to run as a custom user account that is known to the host so the OTHERS group mode
// is used to grant / deny permissions of files on the hosts to the ContainerUser account.
func Chmod(path string, filemode os.FileMode) error {
	klog.V(6).InfoS("Function Chmod starts", "path", path, "filemode", filemode)
	// Get security descriptor for the file
	sd, err := windows.GetNamedSecurityInfo(
		path,
		windows.SE_FILE_OBJECT,
		windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION|windows.OWNER_SECURITY_INFORMATION|windows.GROUP_SECURITY_INFORMATION)
	if err != nil {
		return fmt.Errorf("Error getting security descriptor for file %s: %v", path, err)
	}

	// Get owner SID from the security descriptor for assigning USER permissions
	owner, _, err := sd.Owner()
	if err != nil {
		return fmt.Errorf("Error getting owner SID for file %s: %v", path, err)
	}
	ownerString := owner.String()

	// Get the group SID from the security descriptor for assigning GROUP permissions
	group, _, err := sd.Group()
	if err != nil {
		return fmt.Errorf("Error getting group SID for file %s: %v", path, err)
	}
	groupString := group.String()

	mask := uint32(windows.ACCESS_MASK(filemode))

	// Build a new Discretionary Access Control List (DACL) with the desired permissions using
	//the Security Descriptor Definition Language (SDDL) format.
	// https://learn.microsoft.com/windows/win32/secauthz/security-descriptor-definition-language
	// the DACL is a list of Access Control Entries (ACEs) where each ACE represents the permissions (Allow or Deny) for a specific SID.
	// Each ACE has the following format:
	//  (AceType;AceFlags;Rights;ObjectGuid;InheritObjectGuid;AccountSid)
	// We can leave ObjectGuid and InheritObjectGuid empty for our purposes.

	dacl := "D:"

	// build the owner ACE
	dacl += "(A;OICI;"
	if mask&USER_ALL == USER_ALL {
		dacl += "FA"
	} else {
		if mask&USER_READ == USER_READ {
			dacl += "FR"
		}
		if mask&USER_WRITE == USER_WRITE {
			dacl += "FW"
		}
		if mask&USER_EXECUTE == USER_EXECUTE {
			dacl += "FX"
		}
	}
	dacl += ";;;" + ownerString + ")"

	// Build the group ACE
	dacl += "(A;OICI;"
	if mask&GROUP_ALL == GROUP_ALL {
		dacl += "FA"
	} else {
		if mask&GROUP_READ == GROUP_READ {
			dacl += "FR"
		}
		if mask&GROUP_WRITE == GROUP_WRITE {
			dacl += "FW"
		}
		if mask&GROUP_EXECUTE == GROUP_EXECUTE {
			dacl += "FX"
		}
	}
	dacl += ";;;" + groupString + ")"

	// Build the others ACE
	dacl += "(A;OICI;"
	if mask&OTHERS_ALL == OTHERS_ALL {
		dacl += "FA"
	} else {
		if mask&OTHERS_READ == OTHERS_READ {
			dacl += "FR"
		}
		if mask&OTHERS_WRITE == OTHERS_WRITE {
			dacl += "FW"
		}
		if mask&OTHERS_EXECUTE == OTHERS_EXECUTE {
			dacl += "FX"
		}
	}
	dacl += ";;;BU)"

	klog.V(6).InfoS("Setting new DACL for path", "path", path, "dacl", dacl)

	// create a new security descriptor from the DACL string
	newSD, err := windows.SecurityDescriptorFromString(dacl)
	if err != nil {
		return fmt.Errorf("Error creating new security descriptor from DACL string: %v", err)
	}

	// get the DACL in binary format from the newly created security descriptor
	newDACL, _, err := newSD.DACL()
	if err != nil {
		return fmt.Errorf("Error getting DACL from new security descriptor: %v", err)
	}

	// Write the new security descriptor to the file
	return windows.SetNamedSecurityInfo(
		path,
		windows.SE_FILE_OBJECT,
		windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION,
		nil, // owner SID
		nil, // group SID
		newDACL,
		nil) // SACL
}

// IsAbs returns whether the given path is absolute or not.
// On Windows, filepath.IsAbs will not return True for paths prefixed with a slash, even
// though they can be used as absolute paths (https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats).
//
// WARN: It isn't safe to use this for API values which will propagate across systems (e.g. REST API values
// that get validated on Unix, persisted, then consumed by Windows, etc).
func IsAbs(path string) bool {
	return filepath.IsAbs(path) || strings.HasPrefix(path, `\`) || strings.HasPrefix(path, `/`)
}
