Skip to content

Commit 65b1bcc

Browse files
barash-asenovBarash Asenov
andauthored
Feat: uds_exists validator (#1482)
See: #1348 --------- Co-authored-by: Barash Asenov <[email protected]>
1 parent e9b900c commit 65b1bcc

File tree

4 files changed

+171
-0
lines changed

4 files changed

+171
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ validate := validator.New(validator.WithRequiredStructEnabled())
123123
| udp6_addr | User Datagram Protocol Address UDPv6 |
124124
| udp_addr | User Datagram Protocol Address UDP |
125125
| unix_addr | Unix domain socket end point Address |
126+
| uds_exists | Unix domain socket exists (checks filesystem sockets and Linux abstract sockets) |
126127
| uri | URI String |
127128
| url | URL String |
128129
| http_url | HTTP(s) URL String |

baked_in.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package validator
22

33
import (
4+
"bufio"
45
"bytes"
56
"cmp"
67
"context"
@@ -15,6 +16,7 @@ import (
1516
"net/url"
1617
"os"
1718
"reflect"
19+
"runtime"
1820
"strconv"
1921
"strings"
2022
"sync"
@@ -206,6 +208,7 @@ var (
206208
"ip6_addr": isIP6AddrResolvable,
207209
"ip_addr": isIPAddrResolvable,
208210
"unix_addr": isUnixAddrResolvable,
211+
"uds_exists": isUnixDomainSocketExists,
209212
"mac": isMAC,
210213
"hostname": isHostnameRFC952, // RFC 952
211214
"hostname_rfc1123": isHostnameRFC1123, // RFC 1123
@@ -2611,6 +2614,70 @@ func isUnixAddrResolvable(fl FieldLevel) bool {
26112614
return err == nil
26122615
}
26132616

2617+
// isUnixDomainSocketExists is the validation function for validating if the field's value is an existing Unix domain socket.
2618+
// It handles both filesystem-based sockets and Linux abstract sockets.
2619+
// It always returns false for Windows.
2620+
func isUnixDomainSocketExists(fl FieldLevel) bool {
2621+
if runtime.GOOS == "windows" {
2622+
return false
2623+
}
2624+
2625+
sockpath := fl.Field().String()
2626+
2627+
if sockpath == "" {
2628+
return false
2629+
}
2630+
2631+
// On Linux, check for abstract sockets (prefixed with @)
2632+
if runtime.GOOS == "linux" && strings.HasPrefix(sockpath, "@") {
2633+
return isAbstractSocketExists(sockpath)
2634+
}
2635+
2636+
// For filesystem-based sockets, check if the path exists and is a socket
2637+
stats, err := os.Stat(sockpath)
2638+
if err != nil {
2639+
return false
2640+
}
2641+
2642+
return stats.Mode().Type() == fs.ModeSocket
2643+
}
2644+
2645+
// isAbstractSocketExists checks if a Linux abstract socket exists by reading /proc/net/unix.
2646+
// Abstract sockets are identified by an @ prefix in human-readable form.
2647+
func isAbstractSocketExists(sockpath string) bool {
2648+
file, err := os.Open("/proc/net/unix")
2649+
if err != nil {
2650+
return false
2651+
}
2652+
defer func() {
2653+
_ = file.Close()
2654+
}()
2655+
2656+
scanner := bufio.NewScanner(file)
2657+
2658+
// Skip the header line
2659+
if !scanner.Scan() {
2660+
return false
2661+
}
2662+
2663+
// Abstract sockets in /proc/net/unix are represented with @ prefix
2664+
// The socket path is the last field in each line
2665+
for scanner.Scan() {
2666+
line := scanner.Text()
2667+
fields := strings.Fields(line)
2668+
2669+
// The path is the last field (8th field typically)
2670+
if len(fields) >= 8 {
2671+
path := fields[len(fields)-1]
2672+
if path == sockpath {
2673+
return true
2674+
}
2675+
}
2676+
}
2677+
2678+
return false
2679+
}
2680+
26142681
func isIP4Addr(fl FieldLevel) bool {
26152682
val := fl.Field().String()
26162683

doc.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,6 +1278,15 @@ This validates that a string value contains a valid Unix Address.
12781278
12791279
Usage: unix_addr
12801280
1281+
# Unix Domain Socket Exists
1282+
1283+
This validates that a Unix domain socket file exists at the specified path.
1284+
It checks both filesystem-based sockets and Linux abstract sockets (prefixed with @).
1285+
For filesystem sockets, it verifies the path exists and is a socket file.
1286+
For abstract sockets on Linux, it checks /proc/net/unix.
1287+
1288+
Usage: uds_exists
1289+
12811290
# Media Access Control Address MAC
12821291
12831292
This validates that a string value contains a valid MAC Address.

validator_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ import (
1212
"image"
1313
"image/jpeg"
1414
"image/png"
15+
"net"
1516
"os"
1617
"path/filepath"
1718
"reflect"
19+
"runtime"
1820
"strings"
1921
"testing"
2022
"time"
@@ -3016,6 +3018,98 @@ func TestUnixAddrValidation(t *testing.T) {
30163018
}
30173019
}
30183020

3021+
func TestUnixDomainSocketExistsValidation(t *testing.T) {
3022+
if runtime.GOOS == "windows" {
3023+
t.Skip("Unix domain sockets are not supported on Windows")
3024+
}
3025+
3026+
validate := New()
3027+
3028+
t.Run("empty", func(t *testing.T) {
3029+
errs := validate.Var("", "uds_exists")
3030+
NotEqual(t, errs, nil)
3031+
AssertError(t, errs, "", "", "", "", "uds_exists")
3032+
})
3033+
3034+
t.Run("non_existent", func(t *testing.T) {
3035+
errs := validate.Var("/tmp/nonexistent.sock", "uds_exists")
3036+
NotEqual(t, errs, nil)
3037+
AssertError(t, errs, "", "", "", "", "uds_exists")
3038+
})
3039+
3040+
t.Run("sock_file", func(t *testing.T) {
3041+
sockPath := "/tmp/test_validator.sock"
3042+
var lc net.ListenConfig
3043+
listener, err := lc.Listen(t.Context(), "unix", sockPath)
3044+
if err != nil {
3045+
t.Fatalf("Failed to create test socket: %v", err)
3046+
}
3047+
defer func() {
3048+
_ = os.Remove(sockPath)
3049+
_ = listener.Close()
3050+
}()
3051+
3052+
errs := validate.Var(sockPath, "uds_exists")
3053+
Equal(t, errs, nil)
3054+
})
3055+
3056+
t.Run("regular_file", func(t *testing.T) {
3057+
regularFile := "/tmp/test_validator_regular.txt"
3058+
if err := os.WriteFile(regularFile, []byte("test"), 0644); err != nil {
3059+
t.Fatalf("Failed to create regular file: %v", err)
3060+
}
3061+
defer func() {
3062+
_ = os.Remove(regularFile)
3063+
}()
3064+
3065+
errs := validate.Var(regularFile, "uds_exists")
3066+
NotEqual(t, errs, nil)
3067+
AssertError(t, errs, "", "", "", "", "uds_exists")
3068+
})
3069+
3070+
t.Run("directory", func(t *testing.T) {
3071+
dirPath := "/tmp/test_validator_dir"
3072+
if err := os.Mkdir(dirPath, 0755); err != nil && !os.IsExist(err) {
3073+
t.Fatalf("Failed to create directory: %v", err)
3074+
}
3075+
defer func() {
3076+
_ = os.RemoveAll(dirPath)
3077+
}()
3078+
3079+
errs := validate.Var(dirPath, "uds_exists")
3080+
NotEqual(t, errs, nil)
3081+
AssertError(t, errs, "", "", "", "", "uds_exists")
3082+
})
3083+
3084+
// only supported on linux
3085+
t.Run("abstract_sockets", func(t *testing.T) {
3086+
if runtime.GOOS != "linux" {
3087+
return
3088+
}
3089+
3090+
t.Run("non_existent", func(t *testing.T) {
3091+
errs := validate.Var("@nonexistent_abstract_socket", "uds_exists")
3092+
NotEqual(t, errs, nil)
3093+
AssertError(t, errs, "", "", "", "", "uds_exists")
3094+
})
3095+
3096+
t.Run("existing", func(t *testing.T) {
3097+
abstractSockName := "@test_abstract_socket_" + fmt.Sprintf("%d", time.Now().UnixNano())
3098+
var lc net.ListenConfig
3099+
abstractListener, err := lc.Listen(t.Context(), "unix", "\x00"+abstractSockName[1:])
3100+
if err != nil {
3101+
t.Fatalf("Failed to create abstract socket: %v", err)
3102+
}
3103+
defer func() {
3104+
_ = abstractListener.Close()
3105+
}()
3106+
3107+
errs := validate.Var(abstractSockName, "uds_exists")
3108+
Equal(t, errs, nil)
3109+
})
3110+
})
3111+
}
3112+
30193113
func TestSliceMapArrayChanFuncPtrInterfaceRequiredValidation(t *testing.T) {
30203114
validate := New()
30213115

0 commit comments

Comments
 (0)