From 541264c9c53356d0c214d3859c72e85cfd8bbef9 Mon Sep 17 00:00:00 2001 From: arraykeys Date: Thu, 21 Dec 2023 14:31:09 +0800 Subject: [PATCH] misc --- .github/workflows/test.yml | 6 +- go.mod | 2 +- util/file/file.go | 10 + util/file/file_test.go | 4 + util/hash/hash.go | 216 ++++++++++++++++++++++ util/hash/hash_test.go | 323 +++++++++++++++++++++++++++++++++ util/net/common.go | 42 +++++ util/net/common_test.go | 32 ++++ util/strings/benchmark_test.go | 4 +- util/strings/strings.go | 22 ++- util/strings/strings_test.go | 9 +- util/value/value.go | 57 ++++++ util/value/value_test.go | 40 ++++ 13 files changed, 749 insertions(+), 18 deletions(-) create mode 100644 util/hash/hash.go create mode 100644 util/hash/hash_test.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab7f8035c..420303231 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: testing: strategy: matrix: - go-version: [ 1.13.x,1.14.x,1.15.x,1.16.x,1.17.x,1.18.x,1.19.x,1.20.x,1.21.x ] + go-version: [ 1.16.x,1.17.x,1.18.x,1.19.x,1.20.x,1.21.x ] platform: [ubuntu-latest] runs-on: ${{ matrix.platform }} services: @@ -89,10 +89,10 @@ jobs: sleep 1 done - - name: Set up Go 1.18 + - name: Set up Go 1.20 uses: actions/setup-go@v3 with: - go-version: 1.18.x + go-version: 1.20.x id: go - name: Checkout code diff --git a/go.mod b/go.mod index ba316d331..8a1ce6c0a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/snail007/gmc -go 1.13 +go 1.16 require ( github.com/dsnet/compress v0.0.1 diff --git a/util/file/file.go b/util/file/file.go index 1e61baaf9..a24175ca8 100644 --- a/util/file/file.go +++ b/util/file/file.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "strings" + "time" ) // Exists checks whether a file or directory exists. @@ -81,6 +82,15 @@ func FileSize(file string) (int64, error) { return f.Size(), nil } +// FileMTime get file modified time +func FileMTime(file string) (time.Time, error) { + f, e := os.Stat(file) + if e != nil { + return time.Time{}, e + } + return f.ModTime(), nil +} + // HomeDir returns the home directory. // // Return "" if the home directory is empty. diff --git a/util/file/file_test.go b/util/file/file_test.go index ee2f7daf6..41c9a7239 100644 --- a/util/file/file_test.go +++ b/util/file/file_test.go @@ -2,9 +2,11 @@ package gfile import ( "github.com/magiconair/properties/assert" + gvalue "github.com/snail007/gmc/util/value" assert2 "github.com/stretchr/testify/assert" "os" "testing" + "time" ) func TestAbs(t *testing.T) { @@ -44,7 +46,9 @@ func TestFileName(t *testing.T) { func TestFileSize(t *testing.T) { f := "foo.txt" defer os.Remove(f) + _t := time.Now() assert2.Nil(t, WriteString(f, "a", false)) + assert.Equal(t, _t.Unix(), gvalue.Must(FileMTime(f)).Time().Unix()) size, err := FileSize(f) assert2.Nil(t, err) assert.Equal(t, size, int64(1)) diff --git a/util/hash/hash.go b/util/hash/hash.go new file mode 100644 index 000000000..e64e4f73f --- /dev/null +++ b/util/hash/hash.go @@ -0,0 +1,216 @@ +package ghash + +import ( + "bufio" + "bytes" + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "fmt" + gsync "github.com/snail007/gmc/util/sync" + "golang.org/x/crypto/blake2s" + "hash" + "hash/crc32" + "io" + "os" + "strings" + "sync" + "time" +) + +const bufferSize = 65536 + +func MD5(str string) string { + return Md5Bytes([]byte(str)) +} + +func Md5Bytes(d []byte) string { + return fmt.Sprintf("%x", md5.Sum(d)) +} + +func MD5File(filename string) (string, error) { + return sumFile(md5.New(), filename) +} + +func MD5Reader(reader io.ReadCloser) (string, error) { + return sumReader(md5.New(), reader) +} + +func SHA256(str string) string { + s, _ := sumReader(sha256.New(), newStringCloser(str)) + return s +} + +func SHA256Bytes(b []byte) string { + s, _ := sumReader(sha256.New(), newReaderCloser(bytes.NewReader(b))) + return s +} + +func SHA256File(filename string) (string, error) { + return sumFile(sha256.New(), filename) +} + +func SHA256Reader(reader io.ReadCloser) (string, error) { + return sumReader(sha256.New(), reader) +} + +func SHA1(str string) string { + s, _ := sumReader(sha1.New(), newStringCloser(str)) + return s +} + +func SHA1Bytes(b []byte) string { + s, _ := sumReader(sha1.New(), newReaderCloser(bytes.NewReader(b))) + return s +} + +func SHA1sumFile(filename string) (string, error) { + return sumFile(sha1.New(), filename) +} + +func SHA1Reader(reader io.ReadCloser) (string, error) { + return sumReader(sha1.New(), reader) +} + +func Blake2s256(str string) string { + hash, _ := blake2s.New256([]byte{}) + s, _ := sumReader(hash, newStringCloser(str)) + return s +} + +func Blake2s256Bytes(b []byte) string { + hash, _ := blake2s.New256([]byte{}) + s, _ := sumReader(hash, newReaderCloser(bytes.NewReader(b))) + return s +} + +func Blake2s256File(filename string) (string, error) { + hash, _ := blake2s.New256([]byte{}) + return sumFile(hash, filename) +} + +func Blake2s256Reader(reader io.ReadCloser) (string, error) { + hash, _ := blake2s.New256([]byte{}) + return sumReader(hash, reader) +} + +func CRC32(str string) string { + s, _ := CRC32Reader(newStringCloser(str)) + return s +} + +func CRC32Bytes(b []byte) string { + s, _ := CRC32Reader(newReaderCloser(bytes.NewReader(b))) + return s +} + +func CRC32File(filename string) (string, error) { + if info, err := os.Stat(filename); err != nil || info.IsDir() { + return "", err + } + + file, err := os.Open(filename) + if err != nil { + return "", err + } + defer func() { _ = file.Close() }() + + return CRC32Reader(bufio.NewReader(file)) +} + +func CRC32Reader(reader io.Reader) (string, error) { + table := crc32.MakeTable(crc32.IEEE) + checkSum := crc32.Checksum([]byte(""), table) + buf := make([]byte, bufferSize) + for { + switch n, err := reader.Read(buf); err { + case nil: + checkSum = crc32.Update(checkSum, table, buf[:n]) + case io.EOF: + return fmt.Sprintf("%08x", checkSum), nil + default: + return "", err + } + } +} + +// sumReader calculates the hash based on a provided hash provider +func sumReader(hashAlgorithm hash.Hash, reader io.ReadCloser) (string, error) { + buf := make([]byte, bufferSize) + t := time.AfterFunc(time.Second*30, func() { + reader.Close() + }) + defer t.Stop() + for { + t.Reset(time.Second * 30) + switch n, err := reader.Read(buf); err { + case nil: + hashAlgorithm.Write(buf[:n]) + case io.EOF: + return fmt.Sprintf("%x", hashAlgorithm.Sum(nil)), nil + default: + return "", err + } + } +} + +type readerCloser struct { + io.Reader +} + +func newReaderCloser(r io.Reader) *readerCloser { + return &readerCloser{Reader: r} +} + +func (s *readerCloser) Close() error { + return nil +} + +func newStringCloser(str string) io.ReadCloser { + return newReaderCloser(strings.NewReader(str)) +} + +// sumFile calculates the hash based on a provided hash provider +func sumFile(hashAlgorithm hash.Hash, filename string) (string, error) { + if info, err := os.Stat(filename); err != nil || info.IsDir() { + return "", err + } + var file *os.File + var err error + g := sync.WaitGroup{} + g.Add(1) + go func() { + defer g.Done() + file, err = os.Open(filename) + }() + select { + case <-gsync.Wait(&g): + case <-time.After(time.Second * 3): + return "", fmt.Errorf("open file timeout, %s", filename) + } + if err != nil { + return "", err + } + defer func() { _ = file.Close() }() + + return sumReader(hashAlgorithm, newBufReaderCloser(file)) +} + +type bufReaderCloser struct { + *bufio.Reader + f *os.File +} + +func (s *bufReaderCloser) Close() error { + if s.f != nil { + return s.f.Close() + } + return nil +} + +func newBufReaderCloser(f *os.File) *bufReaderCloser { + return &bufReaderCloser{ + f: f, + Reader: bufio.NewReaderSize(f, 1024*8), + } +} diff --git a/util/hash/hash_test.go b/util/hash/hash_test.go new file mode 100644 index 000000000..f524e3e56 --- /dev/null +++ b/util/hash/hash_test.go @@ -0,0 +1,323 @@ +package ghash + +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "fmt" + "io" + "os" + "strings" + "testing" + "time" +) + +func createTempFile(content string) (string, func(), error) { + file, err := os.Create("testfile") + if err != nil { + return "", nil, err + } + _, err = file.WriteString(content) + if err != nil { + return "", nil, err + } + return file.Name(), func() { os.Remove(file.Name()) }, nil +} + +func TestMD5(t *testing.T) { + input := "hello world" + expectedHash := fmt.Sprintf("%x", md5.Sum([]byte(input))) + result := MD5(input) + if result != expectedHash { + t.Errorf("MD5 hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestMd5Bytes(t *testing.T) { + input := []byte("hello world") + expectedHash := fmt.Sprintf("%x", md5.Sum(input)) + result := Md5Bytes(input) + if result != expectedHash { + t.Errorf("Md5Bytes hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestMD5File(t *testing.T) { + content := "test file content" + filePath, cleanup, err := createTempFile(content) + if err != nil { + t.Fatalf("Error creating temp file: %v", err) + } + defer cleanup() + + expectedHash := fmt.Sprintf("%x", md5.Sum([]byte(content))) + result, err := MD5File(filePath) + if err != nil { + t.Fatalf("Error calculating MD5 for file: %v", err) + } + + if result != expectedHash { + t.Errorf("MD5File hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestSHA256(t *testing.T) { + input := "hello world" + expectedHash := fmt.Sprintf("%x", sha256.Sum256([]byte(input))) + result := SHA256(input) + if result != expectedHash { + t.Errorf("SHA256 hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestSHA256File(t *testing.T) { + content := "test file content" + filePath, cleanup, err := createTempFile(content) + if err != nil { + t.Fatalf("Error creating temp file: %v", err) + } + defer cleanup() + + expectedHash := fmt.Sprintf("%x", sha256.Sum256([]byte(content))) + result, err := SHA256File(filePath) + if err != nil { + t.Fatalf("Error calculating SHA256 for file: %v", err) + } + + if result != expectedHash { + t.Errorf("SHA256File hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestSHA1(t *testing.T) { + input := "hello world" + expectedHash := fmt.Sprintf("%x", sha1.Sum([]byte(input))) + result := SHA1(input) + if result != expectedHash { + t.Errorf("SHA1 hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestSHA1File(t *testing.T) { + content := "test file content" + filePath, cleanup, err := createTempFile(content) + if err != nil { + t.Fatalf("Error creating temp file: %v", err) + } + defer cleanup() + + expectedHash := fmt.Sprintf("%x", sha1.Sum([]byte(content))) + result, err := SHA1sumFile(filePath) + if err != nil { + t.Fatalf("Error calculating SHA1 for file: %v", err) + } + + if result != expectedHash { + t.Errorf("SHA1sumFile hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestBlake2s256(t *testing.T) { + input := "hello world" + expectedHash := Blake2s256(input) + result := Blake2s256(input) + if result != expectedHash { + t.Errorf("Blake2s256 hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestBlake2s256File(t *testing.T) { + content := "test file content" + filePath, cleanup, err := createTempFile(content) + if err != nil { + t.Fatalf("Error creating temp file: %v", err) + } + defer cleanup() + + expectedHash := Blake2s256(content) + result, err := Blake2s256File(filePath) + if err != nil { + t.Fatalf("Error calculating Blake2s256 for file: %v", err) + } + + if result != expectedHash { + t.Errorf("Blake2s256File hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestCRC32(t *testing.T) { + input := "hello world" + expectedHash := CRC32(input) + result := CRC32(input) + if result != expectedHash { + t.Errorf("CRC32 hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestCRC32File(t *testing.T) { + content := "test file content" + filePath, cleanup, err := createTempFile(content) + if err != nil { + t.Fatalf("Error creating temp file: %v", err) + } + defer cleanup() + + expectedHash := CRC32(content) + result, err := CRC32File(filePath) + if err != nil { + t.Fatalf("Error calculating CRC32 for file: %v", err) + } + + if result != expectedHash { + t.Errorf("CRC32File hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestSHA256Bytes(t *testing.T) { + input := []byte("hello world") + expectedHash := fmt.Sprintf("%x", sha256.Sum256(input)) + result := SHA256Bytes(input) + if result != expectedHash { + t.Errorf("SHA256Bytes hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestSHA1Bytes(t *testing.T) { + input := []byte("hello world") + expectedHash := fmt.Sprintf("%x", sha1.Sum(input)) + result := SHA1Bytes(input) + if result != expectedHash { + t.Errorf("SHA1Bytes hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestBlake2s256Bytes(t *testing.T) { + input := []byte("hello world") + expectedHash := Blake2s256Bytes(input) + result := Blake2s256Bytes(input) + if result != expectedHash { + t.Errorf("Blake2s256Bytes hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestCRC32Bytes(t *testing.T) { + input := []byte("hello world") + expectedHash := CRC32Bytes(input) + result := CRC32Bytes(input) + if result != expectedHash { + t.Errorf("CRC32Bytes hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestMD5Reader(t *testing.T) { + input := "hello world" + reader := strings.NewReader(input) + expectedHash := fmt.Sprintf("%x", md5.Sum([]byte(input))) + result, err := MD5Reader(newReaderCloser(reader)) + if err != nil { + t.Fatalf("Error calculating MD5 for reader: %v", err) + } + + if result != expectedHash { + t.Errorf("MD5Reader hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestSHA256Reader(t *testing.T) { + input := "hello world" + reader := strings.NewReader(input) + expectedHash := SHA256(input) + result, err := SHA256Reader(newReaderCloser(reader)) + if err != nil { + t.Fatalf("Error calculating SHA256 for reader: %v", err) + } + + if result != expectedHash { + t.Errorf("SHA256Reader hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestSHA1Reader(t *testing.T) { + input := "hello world" + reader := strings.NewReader(input) + expectedHash := SHA1(input) + result, err := SHA1Reader(newReaderCloser(reader)) + if err != nil { + t.Fatalf("Error calculating SHA1 for reader: %v", err) + } + + if result != expectedHash { + t.Errorf("SHA1Reader hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestBlake2s256Reader(t *testing.T) { + input := "hello world" + reader := strings.NewReader(input) + expectedHash := Blake2s256(input) + result, err := Blake2s256Reader(newReaderCloser(reader)) + if err != nil { + t.Fatalf("Error calculating Blake2s256 for reader: %v", err) + } + + if result != expectedHash { + t.Errorf("Blake2s256Reader hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestCRC32Reader(t *testing.T) { + input := "hello world" + reader := strings.NewReader(input) + expectedHash := CRC32(input) + result, err := CRC32Reader(newReaderCloser(reader)) + if err != nil { + t.Fatalf("Error calculating CRC32 for reader: %v", err) + } + + if result != expectedHash { + t.Errorf("CRC32Reader hash mismatch. Expected: %s, Got: %s", expectedHash, result) + } +} + +func TestMd5Bytes_CloseError(t *testing.T) { + t.Parallel() + // Simulate a reader with an error when closing + reader := &mockReadCloser{Reader: strings.NewReader("hello world"), + Error: fmt.Errorf("error1")} + _, err := MD5Reader(reader) + if err == nil || err.Error() != "error1" { + t.Errorf("Expected close error, but got: %v", err) + } + + reader = &mockReadCloser{Reader: strings.NewReader("hello world"), + sleep: time.Second * 33, + Error: fmt.Errorf("error2")} + _, err = MD5Reader(reader) + if err == nil || err.Error() != "error2" { + t.Errorf("Expected close error, but got: %v", err) + } +} + +func TestMD5File_ErrorOpeningFile(t *testing.T) { + _, err := MD5File("nonexistentfile.txt") + if err == nil { + t.Error("Expected error opening file, but got nil") + } +} + +type mockReadCloser struct { + io.Reader + Error error + sleep time.Duration +} + +func (m *mockReadCloser) Read(p []byte) (n int, err error) { + if m.sleep > 0 { + time.Sleep(m.sleep) + } + return 0, m.Error +} + +func (m *mockReadCloser) Close() error { + return nil +} diff --git a/util/net/common.go b/util/net/common.go index df1045c4a..7744b86b4 100644 --- a/util/net/common.go +++ b/util/net/common.go @@ -242,3 +242,45 @@ func NewTCPAddr(address string) *Addr { err: err, } } + +func LocalOutgoingIP() (string, error) { + addr, err := net.ResolveUDPAddr("udp", "1.2.3.4:1") + if err != nil { + return "", err + } + + conn, err := net.DialUDP("udp", nil, addr) + if err != nil { + return "", err + } + + defer conn.Close() + + host, _, err := net.SplitHostPort(conn.LocalAddr().String()) + if err != nil { + return "", err + } + + return host, nil +} + +func IsPrivateIP(ip string) bool { + parsedIP := net.ParseIP(ip) + if parsedIP == nil { + return false + } + if ip4 := parsedIP.To4(); ip4 != nil { + // Following RFC 1918, Section 3. Private Address Space which says: + // The Internet Assigned Numbers Authority (IANA) has reserved the + // following three blocks of the IP address space for private internets: + // 10.0.0.0 - 10.255.255.255 (10/8 prefix) + // 172.16.0.0 - 172.31.255.255 (172.16/12 prefix) + // 192.168.0.0 - 192.168.255.255 (192.168/16 prefix) + return ip4[0] == 10 || + (ip4[0] == 172 && ip4[1]&0xf0 == 16) || + (ip4[0] == 192 && ip4[1] == 168) + } + // Following RFC 4193, Section 8. IANA Considerations which says: + // The IANA has assigned the FC00::/7 prefix to "Unique Local Unicast". + return len(ip) == 16 && ip[0]&0xfe == 0xfc +} diff --git a/util/net/common_test.go b/util/net/common_test.go index 6a0d6ff21..e0118b078 100644 --- a/util/net/common_test.go +++ b/util/net/common_test.go @@ -134,3 +134,35 @@ func TestListenEventError(t *testing.T) { out, _, _ := gtest.NewProcess(t).Wait() assert.True(t, strings.Contains(out, "listen_error")) } + +func TestLocalOutgoingIP(t *testing.T) { + ip, err := LocalOutgoingIP() + assert.Nil(t, err) + assert.NotNil(t, net.ParseIP(ip)) +} + +func TestIsPrivateIP(t *testing.T) { + testCases := []struct { + ip string + expected bool + }{ + {"192.168.1.1", true}, // IPv4 private address + {"2001:0db8::1", false}, // IPv6 public address + {"8.8.8.8", false}, // Public IPv4 address + {"2001:4860:4860::8888", false}, // Public IPv6 address + {"invalidIP", false}, // Invalid IP address + {"10.0.0.1", true}, // Additional IPv4 private address + {"172.16.0.1", true}, // Additional IPv4 private address + {"192.168.2.1", true}, // Additional IPv4 private address + {"fc00::1", true}, // Additional IPv6 private address + {"fe80::1", true}, // Additional IPv6 private address + {"2001:0db8::2", false}, // Additional public IPv6 address + } + + for _, testCase := range testCases { + result := IsPrivateIP(testCase.ip) + if result != testCase.expected { + t.Errorf("For IP %s, expected %t, but got %t", testCase.ip, testCase.expected, result) + } + } +} diff --git a/util/strings/benchmark_test.go b/util/strings/benchmark_test.go index 50865888e..750788d38 100644 --- a/util/strings/benchmark_test.go +++ b/util/strings/benchmark_test.go @@ -12,7 +12,7 @@ func Benchmark_NormalBytes2String(b *testing.B) { func Benchmark_StringRef(b *testing.B) { x := []byte("Hello GMC! Hello GMC! Hello GMC!") for i := 0; i < b.N; i++ { - _ = StringRef(x) + _ = BytesToString(x) } } @@ -26,6 +26,6 @@ func Benchmark_NormalString2Bytes(b *testing.B) { func Benchmark_BytesRef(b *testing.B) { x := "Hello GMC! Hello GMC! Hello GMC!" for i := 0; i < b.N; i++ { - _ = BytesRef(x) + _ = StringToBytes(x) } } diff --git a/util/strings/strings.go b/util/strings/strings.go index 78055e532..49da28bce 100644 --- a/util/strings/strings.go +++ b/util/strings/strings.go @@ -1,22 +1,15 @@ package gstrings import ( - "reflect" "strings" "unsafe" ) -func BytesRef(s string) []byte { - sh := (*reflect.StringHeader)(unsafe.Pointer(&s)) - bh := reflect.SliceHeader{ - Data: sh.Data, - Len: sh.Len, - Cap: sh.Len, - } - return *(*[]byte)(unsafe.Pointer(&bh)) +func StringToBytes(s string) []byte { + return *(*[]byte)(unsafe.Pointer(&s)) } -func StringRef(b []byte) string { +func BytesToString(b []byte) string { return *(*string)(unsafe.Pointer(&b)) } @@ -29,6 +22,15 @@ func HasPrefixAny(str string, prefix ...string) bool { return false } +func HasSuffixAny(str string, suffix ...string) bool { + for _, v := range suffix { + if strings.HasSuffix(str, v) { + return true + } + } + return false +} + func HasHTTPPrefix(str string) bool { return HasPrefixAny(strings.ToLower(str), "http://", "https://") } diff --git a/util/strings/strings_test.go b/util/strings/strings_test.go index cf04cd970..3d587874d 100644 --- a/util/strings/strings_test.go +++ b/util/strings/strings_test.go @@ -7,13 +7,13 @@ import ( func TestBytesRef(t *testing.T) { a := "abc" - b := BytesRef(a) + b := StringToBytes(a) assert.Equal(t, []byte("abc"), b) } func TestStringRef(t *testing.T) { a := []byte("abc") - b := StringRef(a) + b := BytesToString(a) assert.Equal(t, "abc", b) } @@ -26,3 +26,8 @@ func TestHasHTTPPrefix(t *testing.T) { assert.True(t, HasHTTPPrefix("http://")) assert.False(t, HasHTTPPrefix("ftp://")) } + +func TestHasSuffix(t *testing.T) { + assert.True(t, HasSuffixAny("a.txt", ".log", ".txt")) + assert.False(t, HasSuffixAny("a.log", ".log1", ".txt1")) +} diff --git a/util/value/value.go b/util/value/value.go index 74648dbbc..0f139f42e 100644 --- a/util/value/value.go +++ b/util/value/value.go @@ -890,3 +890,60 @@ func walkSlice(val interface{}, f func(v interface{})) { } } } + +// GetValueAt sliceOrArray is slice or array, idx is the element to get, if idx invalid or sliceOrArray is not slice or array, the +// default value will be used +func GetValueAt(sliceOrArray interface{}, idx int, defaultValue interface{}) *Value { + value := reflect.ValueOf(sliceOrArray) + + if value.Kind() != reflect.Slice && value.Kind() != reflect.Array { + return New(defaultValue) + } + + if idx < 0 || idx >= value.Len() { + return New(defaultValue) + } + + return New(value.Index(idx).Interface()) +} + +// Keys input _map is a map, returns its keys as slice, AnyValue.xxxSlice to get the typed slice. +func Keys(_map interface{}) *AnyValue { + value := reflect.ValueOf(_map) + + if value.Kind() != reflect.Map { + return NewAny(nil) + } + + keys := value.MapKeys() + result := make([]interface{}, len(keys)) + + for i, key := range keys { + result[i] = key.Interface() + } + + return NewAny(result) +} + +// Contains check the input slice if contains the value +func Contains(sliceInterface interface{}, value interface{}) bool { + return IndexOf(sliceInterface, value) >= 0 +} + +// IndexOf returns the index of value in slice, if not found -1 returned +func IndexOf(sliceInterface interface{}, value interface{}) int { + sliceValue := reflect.ValueOf(sliceInterface) + + if sliceValue.Kind() != reflect.Slice { + return -1 + } + + for i := 0; i < sliceValue.Len(); i++ { + element := sliceValue.Index(i).Interface() + if reflect.DeepEqual(element, value) { + return i + } + } + + return -1 +} diff --git a/util/value/value_test.go b/util/value/value_test.go index 0cadd3a31..3a04ac6de 100644 --- a/util/value/value_test.go +++ b/util/value/value_test.go @@ -384,3 +384,43 @@ func TestParseByteSize(t *testing.T) { func TestParseNumber(t *testing.T) { assert.Equal(t, int64(123000), ParseNumber("123,000")) } + +func TestGetValueAt(t *testing.T) { + var a interface{} + a = 123 + assert.Equal(t, 456, GetValueAt(a, 1, 456).Int()) + a = []int{1, 2, 3} + assert.Equal(t, 2, GetValueAt(a, 1, 3).Int()) + assert.Equal(t, 3, GetValueAt(a, 100, 3).Int()) + assert.Equal(t, 3, GetValueAt(a, -1, 3).Int()) + + a = []int{1, 2, 3} + assert.Equal(t, 2, GetValueAt(a, 1, 3).Val()) +} + +func TestKeys(t *testing.T) { + assert.Equal(t, nil, Keys(nil).Val()) + assert.Equal(t, []string(nil), Keys(nil).StringSlice()) + var a interface{} + a = 123 + assert.Equal(t, nil, Keys(a).Val()) + assert.Equal(t, []string(nil), Keys(a).StringSlice()) + a = map[string]string{"a": "1"} + assert.Equal(t, []string{"a"}, Keys(a).StringSlice()) + a = map[string]string{} + assert.Equal(t, []string(nil), Keys(a).StringSlice()) +} + +func TestContains(t *testing.T) { + assert.Equal(t, false, Contains(nil, 1)) + var a interface{} + a = 123 + assert.Equal(t, false, Contains(a, 1)) + a = []int{1, 2, 3} + assert.Equal(t, true, Contains(a, 1)) + assert.Equal(t, 2, IndexOf(a, 3)) + a = []interface{}{1, 2, 3, nil} + assert.Equal(t, 3, IndexOf(a, nil)) + assert.Equal(t, -1, IndexOf(a, 0)) + +}