diff --git a/.golangci.yaml b/.golangci.yaml index dd9c05cc..e89da03d 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -52,3 +52,4 @@ linters: - varnamelen - wrapcheck - wsl + - modernize diff --git a/README.md b/README.md index cb5d4194..b21290ae 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ validate := validator.New(validator.WithRequiredStructEnabled()) | udp6_addr | User Datagram Protocol Address UDPv6 | | udp_addr | User Datagram Protocol Address UDP | | unix_addr | Unix domain socket end point Address | +| uds_exists | Unix domain socket exists (checks filesystem sockets and Linux abstract sockets) | | uri | URI String | | url | URL String | | http_url | HTTP(s) URL String | diff --git a/baked_in.go b/baked_in.go index 8fd55e77..34a2a3eb 100644 --- a/baked_in.go +++ b/baked_in.go @@ -1,6 +1,7 @@ package validator import ( + "bufio" "bytes" "cmp" "context" @@ -15,6 +16,7 @@ import ( "net/url" "os" "reflect" + "runtime" "strconv" "strings" "sync" @@ -205,6 +207,7 @@ var ( "ip6_addr": isIP6AddrResolvable, "ip_addr": isIPAddrResolvable, "unix_addr": isUnixAddrResolvable, + "uds_exists": isUnixDomainSocketExists, "mac": isMAC, "hostname": isHostnameRFC952, // RFC 952 "hostname_rfc1123": isHostnameRFC1123, // RFC 1123 @@ -2595,6 +2598,70 @@ func isUnixAddrResolvable(fl FieldLevel) bool { return err == nil } +// isUnixDomainSocketExists is the validation function for validating if the field's value is an existing Unix domain socket. +// It handles both filesystem-based sockets and Linux abstract sockets. +// It always returns false for Windows. +func isUnixDomainSocketExists(fl FieldLevel) bool { + if runtime.GOOS == "windows" { + return false + } + + sockpath := fl.Field().String() + + if sockpath == "" { + return false + } + + // On Linux, check for abstract sockets (prefixed with @) + if runtime.GOOS == "linux" && strings.HasPrefix(sockpath, "@") { + return isAbstractSocketExists(sockpath) + } + + // For filesystem-based sockets, check if the path exists and is a socket + stats, err := os.Stat(sockpath) + if err != nil { + return false + } + + return stats.Mode().Type() == fs.ModeSocket +} + +// isAbstractSocketExists checks if a Linux abstract socket exists by reading /proc/net/unix. +// Abstract sockets are identified by an @ prefix in human-readable form. +func isAbstractSocketExists(sockpath string) bool { + file, err := os.Open("/proc/net/unix") + if err != nil { + return false + } + defer func() { + _ = file.Close() + }() + + scanner := bufio.NewScanner(file) + + // Skip the header line + if !scanner.Scan() { + return false + } + + // Abstract sockets in /proc/net/unix are represented with @ prefix + // The socket path is the last field in each line + for scanner.Scan() { + line := scanner.Text() + fields := strings.Fields(line) + + // The path is the last field (8th field typically) + if len(fields) >= 8 { + path := fields[len(fields)-1] + if path == sockpath { + return true + } + } + } + + return false +} + func isIP4Addr(fl FieldLevel) bool { val := fl.Field().String() diff --git a/doc.go b/doc.go index cd6eefdc..1f588e6f 100644 --- a/doc.go +++ b/doc.go @@ -1263,6 +1263,15 @@ This validates that a string value contains a valid Unix Address. Usage: unix_addr +# Unix Domain Socket Exists + +This validates that a Unix domain socket file exists at the specified path. +It checks both filesystem-based sockets and Linux abstract sockets (prefixed with @). +For filesystem sockets, it verifies the path exists and is a socket file. +For abstract sockets on Linux, it checks /proc/net/unix. + + Usage: uds_exists + # Media Access Control Address MAC This validates that a string value contains a valid MAC Address. diff --git a/validator_test.go b/validator_test.go index 8e969d30..b4b0f040 100644 --- a/validator_test.go +++ b/validator_test.go @@ -3,7 +3,9 @@ package validator import ( "bytes" "context" + "net" "database/sql" + "runtime" "database/sql/driver" "encoding/base64" "encoding/json" @@ -2950,6 +2952,76 @@ func TestUnixAddrValidation(t *testing.T) { } } +func TestUnixDomainSocketExistsValidation(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Unix domain sockets are not supported on Windows") + } + + validate := New() + + errs := validate.Var("", "uds_exists") + NotEqual(t, errs, nil) + AssertError(t, errs, "", "", "", "", "uds_exists") + + errs = validate.Var("/tmp/nonexistent.sock", "uds_exists") + NotEqual(t, errs, nil) + AssertError(t, errs, "", "", "", "", "uds_exists") + + sockPath := "/tmp/test_validator.sock" + var lc net.ListenConfig + listener, err := lc.Listen(t.Context(), "unix", sockPath) + if err != nil { + t.Fatalf("Failed to create test socket: %v", err) + } + defer func() { + _ = listener.Close() + }() + defer func() { + _ = os.Remove(sockPath) + }() + errs = validate.Var(sockPath, "uds_exists") + Equal(t, errs, nil) + + regularFile := "/tmp/test_validator_regular.txt" + if err := os.WriteFile(regularFile, []byte("test"), 0644); err != nil { + t.Fatalf("Failed to create regular file: %v", err) + } + defer func() { + _ = os.Remove(regularFile) + }() + errs = validate.Var(regularFile, "uds_exists") + NotEqual(t, errs, nil) + AssertError(t, errs, "", "", "", "", "uds_exists") + + dirPath := "/tmp/test_validator_dir" + if err := os.Mkdir(dirPath, 0755); err != nil && !os.IsExist(err) { + t.Fatalf("Failed to create directory: %v", err) + } + defer func() { + _ = os.RemoveAll(dirPath) + }() + errs = validate.Var(dirPath, "uds_exists") + NotEqual(t, errs, nil) + AssertError(t, errs, "", "", "", "", "uds_exists") + + if runtime.GOOS == "linux" { + abstractSockName := "@test_abstract_socket_" + fmt.Sprintf("%d", time.Now().UnixNano()) + var lc net.ListenConfig + abstractListener, err := lc.Listen(t.Context(), "unix", "\x00"+abstractSockName[1:]) + if err != nil { + t.Fatalf("Failed to create abstract socket: %v", err) + } + defer func() { + _ = abstractListener.Close() + }() + errs = validate.Var(abstractSockName, "uds_exists") + Equal(t, errs, nil) + errs = validate.Var("@nonexistent_abstract_socket", "uds_exists") + NotEqual(t, errs, nil) + AssertError(t, errs, "", "", "", "", "uds_exists") + } +} + func TestSliceMapArrayChanFuncPtrInterfaceRequiredValidation(t *testing.T) { validate := New()