mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-02-13 01:12:01 +01:00
lots of changes. new wshrpc implementation. unify websocket, web, blockcontroller, domain sockets, and terminal inputs to all use the new rpc system. lots of moving files around to deal with circular dependencies use new wshrpc as a client in wsh cmd
793 lines
16 KiB
Go
793 lines
16 KiB
Go
// Copyright 2024, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package utilfn
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
mathrand "math/rand"
|
|
"mime"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"syscall"
|
|
"unicode/utf8"
|
|
|
|
"github.com/mitchellh/mapstructure"
|
|
)
|
|
|
|
var HexDigits = []byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}
|
|
|
|
func GetStrArr(v interface{}, field string) []string {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
m, ok := v.(map[string]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
fieldVal := m[field]
|
|
if fieldVal == nil {
|
|
return nil
|
|
}
|
|
iarr, ok := fieldVal.([]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
var sarr []string
|
|
for _, iv := range iarr {
|
|
if sv, ok := iv.(string); ok {
|
|
sarr = append(sarr, sv)
|
|
}
|
|
}
|
|
return sarr
|
|
}
|
|
|
|
func GetBool(v interface{}, field string) bool {
|
|
if v == nil {
|
|
return false
|
|
}
|
|
m, ok := v.(map[string]interface{})
|
|
if !ok {
|
|
return false
|
|
}
|
|
fieldVal := m[field]
|
|
if fieldVal == nil {
|
|
return false
|
|
}
|
|
bval, ok := fieldVal.(bool)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return bval
|
|
}
|
|
|
|
var needsQuoteRe = regexp.MustCompile(`[^\w@%:,./=+-]`)
|
|
|
|
// minimum maxlen=6
|
|
func ShellQuote(val string, forceQuote bool, maxLen int) string {
|
|
if maxLen < 6 {
|
|
maxLen = 6
|
|
}
|
|
rtn := val
|
|
if needsQuoteRe.MatchString(val) {
|
|
rtn = "'" + strings.ReplaceAll(val, "'", `'"'"'`) + "'"
|
|
}
|
|
if strings.HasPrefix(rtn, "\"") || strings.HasPrefix(rtn, "'") {
|
|
if len(rtn) > maxLen {
|
|
return rtn[0:maxLen-4] + "..." + rtn[0:1]
|
|
}
|
|
return rtn
|
|
}
|
|
if forceQuote {
|
|
if len(rtn) > maxLen-2 {
|
|
return "\"" + rtn[0:maxLen-5] + "...\""
|
|
}
|
|
return "\"" + rtn + "\""
|
|
} else {
|
|
if len(rtn) > maxLen {
|
|
return rtn[0:maxLen-3] + "..."
|
|
}
|
|
return rtn
|
|
}
|
|
}
|
|
|
|
func EllipsisStr(s string, maxLen int) string {
|
|
if maxLen < 4 {
|
|
maxLen = 4
|
|
}
|
|
if len(s) > maxLen {
|
|
return s[0:maxLen-3] + "..."
|
|
}
|
|
return s
|
|
}
|
|
|
|
func LongestPrefix(root string, strs []string) string {
|
|
if len(strs) == 0 {
|
|
return root
|
|
}
|
|
if len(strs) == 1 {
|
|
comp := strs[0]
|
|
if len(comp) >= len(root) && strings.HasPrefix(comp, root) {
|
|
if strings.HasSuffix(comp, "/") {
|
|
return strs[0]
|
|
}
|
|
return strs[0]
|
|
}
|
|
}
|
|
lcp := strs[0]
|
|
for i := 1; i < len(strs); i++ {
|
|
s := strs[i]
|
|
for j := 0; j < len(lcp); j++ {
|
|
if j >= len(s) || lcp[j] != s[j] {
|
|
lcp = lcp[0:j]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if len(lcp) < len(root) || !strings.HasPrefix(lcp, root) {
|
|
return root
|
|
}
|
|
return lcp
|
|
}
|
|
|
|
func ContainsStr(strs []string, test string) bool {
|
|
for _, s := range strs {
|
|
if s == test {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func IsPrefix(strs []string, test string) bool {
|
|
for _, s := range strs {
|
|
if len(s) > len(test) && strings.HasPrefix(s, test) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// sentinel value for StrWithPos.Pos to indicate no position
|
|
const NoStrPos = -1
|
|
|
|
type StrWithPos struct {
|
|
Str string `json:"str"`
|
|
Pos int `json:"pos"` // this is a 'rune' position (not a byte position)
|
|
}
|
|
|
|
func (sp StrWithPos) String() string {
|
|
return strWithCursor(sp.Str, sp.Pos)
|
|
}
|
|
|
|
func ParseToSP(s string) StrWithPos {
|
|
idx := strings.Index(s, "[*]")
|
|
if idx == -1 {
|
|
return StrWithPos{Str: s, Pos: NoStrPos}
|
|
}
|
|
return StrWithPos{Str: s[0:idx] + s[idx+3:], Pos: utf8.RuneCountInString(s[0:idx])}
|
|
}
|
|
|
|
func strWithCursor(str string, pos int) string {
|
|
if pos == NoStrPos {
|
|
return str
|
|
}
|
|
if pos < 0 {
|
|
// invalid position
|
|
return "[*]_" + str
|
|
}
|
|
if pos > len(str) {
|
|
// invalid position
|
|
return str + "_[*]"
|
|
}
|
|
if pos == len(str) {
|
|
return str + "[*]"
|
|
}
|
|
var rtn []rune
|
|
for _, ch := range str {
|
|
if len(rtn) == pos {
|
|
rtn = append(rtn, '[', '*', ']')
|
|
}
|
|
rtn = append(rtn, ch)
|
|
}
|
|
return string(rtn)
|
|
}
|
|
|
|
func (sp StrWithPos) Prepend(str string) StrWithPos {
|
|
return StrWithPos{Str: str + sp.Str, Pos: utf8.RuneCountInString(str) + sp.Pos}
|
|
}
|
|
|
|
func (sp StrWithPos) Append(str string) StrWithPos {
|
|
return StrWithPos{Str: sp.Str + str, Pos: sp.Pos}
|
|
}
|
|
|
|
// returns base64 hash of data
|
|
func Sha1Hash(data []byte) string {
|
|
hvalRaw := sha1.Sum(data)
|
|
hval := base64.StdEncoding.EncodeToString(hvalRaw[:])
|
|
return hval
|
|
}
|
|
|
|
func ChunkSlice[T any](s []T, chunkSize int) [][]T {
|
|
var rtn [][]T
|
|
for len(rtn) > 0 {
|
|
if len(s) <= chunkSize {
|
|
rtn = append(rtn, s)
|
|
break
|
|
}
|
|
rtn = append(rtn, s[:chunkSize])
|
|
s = s[chunkSize:]
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
var ErrOverflow = errors.New("integer overflow")
|
|
|
|
// Add two int values, returning an error if the result overflows.
|
|
func AddInt(left, right int) (int, error) {
|
|
if right > 0 {
|
|
if left > math.MaxInt-right {
|
|
return 0, ErrOverflow
|
|
}
|
|
} else {
|
|
if left < math.MinInt-right {
|
|
return 0, ErrOverflow
|
|
}
|
|
}
|
|
return left + right, nil
|
|
}
|
|
|
|
// Add a slice of ints, returning an error if the result overflows.
|
|
func AddIntSlice(vals ...int) (int, error) {
|
|
var rtn int
|
|
for _, v := range vals {
|
|
var err error
|
|
rtn, err = AddInt(rtn, v)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
return rtn, nil
|
|
}
|
|
|
|
func StrsEqual(s1arr []string, s2arr []string) bool {
|
|
if len(s1arr) != len(s2arr) {
|
|
return false
|
|
}
|
|
for i, s1 := range s1arr {
|
|
s2 := s2arr[i]
|
|
if s1 != s2 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func StrMapsEqual(m1 map[string]string, m2 map[string]string) bool {
|
|
if len(m1) != len(m2) {
|
|
return false
|
|
}
|
|
for key, val1 := range m1 {
|
|
val2, found := m2[key]
|
|
if !found || val1 != val2 {
|
|
return false
|
|
}
|
|
}
|
|
for key := range m2 {
|
|
_, found := m1[key]
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func ByteMapsEqual(m1 map[string][]byte, m2 map[string][]byte) bool {
|
|
if len(m1) != len(m2) {
|
|
return false
|
|
}
|
|
for key, val1 := range m1 {
|
|
val2, found := m2[key]
|
|
if !found || !bytes.Equal(val1, val2) {
|
|
return false
|
|
}
|
|
}
|
|
for key := range m2 {
|
|
_, found := m1[key]
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func GetOrderedStringerMapKeys[K interface {
|
|
comparable
|
|
fmt.Stringer
|
|
}, V any](m map[K]V) []K {
|
|
keyStrMap := make(map[K]string)
|
|
keys := make([]K, 0, len(m))
|
|
for key := range m {
|
|
keys = append(keys, key)
|
|
keyStrMap[key] = key.String()
|
|
}
|
|
sort.Slice(keys, func(i, j int) bool {
|
|
return keyStrMap[keys[i]] < keyStrMap[keys[j]]
|
|
})
|
|
return keys
|
|
}
|
|
|
|
func GetOrderedMapKeys[V any](m map[string]V) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for key := range m {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
const (
|
|
nullEncodeEscByte = '\\'
|
|
nullEncodeSepByte = '|'
|
|
nullEncodeEqByte = '='
|
|
nullEncodeZeroByteEsc = '0'
|
|
nullEncodeEscByteEsc = '\\'
|
|
nullEncodeSepByteEsc = 's'
|
|
nullEncodeEqByteEsc = 'e'
|
|
)
|
|
|
|
func EncodeStringMap(m map[string]string) []byte {
|
|
var buf bytes.Buffer
|
|
for idx, key := range GetOrderedMapKeys(m) {
|
|
val := m[key]
|
|
buf.Write(NullEncodeStr(key))
|
|
buf.WriteByte(nullEncodeEqByte)
|
|
buf.Write(NullEncodeStr(val))
|
|
if idx < len(m)-1 {
|
|
buf.WriteByte(nullEncodeSepByte)
|
|
}
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func DecodeStringMap(barr []byte) (map[string]string, error) {
|
|
if len(barr) == 0 {
|
|
return nil, nil
|
|
}
|
|
var rtn = make(map[string]string)
|
|
for _, b := range bytes.Split(barr, []byte{nullEncodeSepByte}) {
|
|
keyVal := bytes.SplitN(b, []byte{nullEncodeEqByte}, 2)
|
|
if len(keyVal) != 2 {
|
|
return nil, fmt.Errorf("invalid null encoding: %s", string(b))
|
|
}
|
|
key, err := NullDecodeStr(keyVal[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
val, err := NullDecodeStr(keyVal[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rtn[key] = val
|
|
}
|
|
return rtn, nil
|
|
}
|
|
|
|
func EncodeStringArray(arr []string) []byte {
|
|
var buf bytes.Buffer
|
|
for idx, s := range arr {
|
|
buf.Write(NullEncodeStr(s))
|
|
if idx < len(arr)-1 {
|
|
buf.WriteByte(nullEncodeSepByte)
|
|
}
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func DecodeStringArray(barr []byte) ([]string, error) {
|
|
if len(barr) == 0 {
|
|
return nil, nil
|
|
}
|
|
var rtn []string
|
|
for _, b := range bytes.Split(barr, []byte{nullEncodeSepByte}) {
|
|
s, err := NullDecodeStr(b)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rtn = append(rtn, s)
|
|
}
|
|
return rtn, nil
|
|
}
|
|
|
|
func EncodedStringArrayHasFirstVal(encoded []byte, firstKey string) bool {
|
|
firstKeyBytes := NullEncodeStr(firstKey)
|
|
if !bytes.HasPrefix(encoded, firstKeyBytes) {
|
|
return false
|
|
}
|
|
if len(encoded) == len(firstKeyBytes) || encoded[len(firstKeyBytes)] == nullEncodeSepByte {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// on encoding error returns ""
|
|
// this is used to perform logic on first value without decoding the entire array
|
|
func EncodedStringArrayGetFirstVal(encoded []byte) string {
|
|
sepIdx := bytes.IndexByte(encoded, nullEncodeSepByte)
|
|
if sepIdx == -1 {
|
|
str, _ := NullDecodeStr(encoded)
|
|
return str
|
|
}
|
|
str, _ := NullDecodeStr(encoded[0:sepIdx])
|
|
return str
|
|
}
|
|
|
|
// encodes a string, removing null/zero bytes (and separators '|')
|
|
// a zero byte is encoded as "\0", a '\' is encoded as "\\", sep is encoded as "\s"
|
|
// allows for easy double splitting (first on \x00, and next on "|")
|
|
func NullEncodeStr(s string) []byte {
|
|
strBytes := []byte(s)
|
|
if bytes.IndexByte(strBytes, 0) == -1 &&
|
|
bytes.IndexByte(strBytes, nullEncodeEscByte) == -1 &&
|
|
bytes.IndexByte(strBytes, nullEncodeSepByte) == -1 &&
|
|
bytes.IndexByte(strBytes, nullEncodeEqByte) == -1 {
|
|
return strBytes
|
|
}
|
|
var rtn []byte
|
|
for _, b := range strBytes {
|
|
if b == 0 {
|
|
rtn = append(rtn, nullEncodeEscByte, nullEncodeZeroByteEsc)
|
|
} else if b == nullEncodeEscByte {
|
|
rtn = append(rtn, nullEncodeEscByte, nullEncodeEscByteEsc)
|
|
} else if b == nullEncodeSepByte {
|
|
rtn = append(rtn, nullEncodeEscByte, nullEncodeSepByteEsc)
|
|
} else if b == nullEncodeEqByte {
|
|
rtn = append(rtn, nullEncodeEscByte, nullEncodeEqByteEsc)
|
|
} else {
|
|
rtn = append(rtn, b)
|
|
}
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
func NullDecodeStr(barr []byte) (string, error) {
|
|
if bytes.IndexByte(barr, nullEncodeEscByte) == -1 {
|
|
return string(barr), nil
|
|
}
|
|
var rtn []byte
|
|
for i := 0; i < len(barr); i++ {
|
|
curByte := barr[i]
|
|
if curByte == nullEncodeEscByte {
|
|
i++
|
|
nextByte := barr[i]
|
|
if nextByte == nullEncodeZeroByteEsc {
|
|
rtn = append(rtn, 0)
|
|
} else if nextByte == nullEncodeEscByteEsc {
|
|
rtn = append(rtn, nullEncodeEscByte)
|
|
} else if nextByte == nullEncodeSepByteEsc {
|
|
rtn = append(rtn, nullEncodeSepByte)
|
|
} else if nextByte == nullEncodeEqByteEsc {
|
|
rtn = append(rtn, nullEncodeEqByte)
|
|
} else {
|
|
// invalid encoding
|
|
return "", fmt.Errorf("invalid null encoding: %d", nextByte)
|
|
}
|
|
} else {
|
|
rtn = append(rtn, curByte)
|
|
}
|
|
}
|
|
return string(rtn), nil
|
|
}
|
|
|
|
func SortStringRunes(s string) string {
|
|
runes := []rune(s)
|
|
sort.Slice(runes, func(i, j int) bool {
|
|
return runes[i] < runes[j]
|
|
})
|
|
return string(runes)
|
|
}
|
|
|
|
// will overwrite m1 with m2's values
|
|
func CombineMaps[V any](m1 map[string]V, m2 map[string]V) {
|
|
for key, val := range m2 {
|
|
m1[key] = val
|
|
}
|
|
}
|
|
|
|
// returns hex escaped string (\xNN for each byte)
|
|
func ShellHexEscape(s string) string {
|
|
var rtn []byte
|
|
for _, ch := range []byte(s) {
|
|
rtn = append(rtn, []byte(fmt.Sprintf("\\x%02x", ch))...)
|
|
}
|
|
return string(rtn)
|
|
}
|
|
|
|
func GetMapKeys[K comparable, V any](m map[K]V) []K {
|
|
var rtn []K
|
|
for key := range m {
|
|
rtn = append(rtn, key)
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
// combines string arrays and removes duplicates (returns a new array)
|
|
func CombineStrArrays(sarr1 []string, sarr2 []string) []string {
|
|
var rtn []string
|
|
m := make(map[string]struct{})
|
|
for _, s := range sarr1 {
|
|
if _, found := m[s]; found {
|
|
continue
|
|
}
|
|
m[s] = struct{}{}
|
|
rtn = append(rtn, s)
|
|
}
|
|
for _, s := range sarr2 {
|
|
if _, found := m[s]; found {
|
|
continue
|
|
}
|
|
m[s] = struct{}{}
|
|
rtn = append(rtn, s)
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
func QuickJson(v interface{}) string {
|
|
barr, _ := json.Marshal(v)
|
|
return string(barr)
|
|
}
|
|
|
|
func QuickParseJson[T any](s string) T {
|
|
var v T
|
|
_ = json.Unmarshal([]byte(s), &v)
|
|
return v
|
|
}
|
|
|
|
func StrArrayToMap(sarr []string) map[string]bool {
|
|
m := make(map[string]bool)
|
|
for _, s := range sarr {
|
|
m[s] = true
|
|
}
|
|
return m
|
|
}
|
|
|
|
func AppendNonZeroRandomBytes(b []byte, randLen int) []byte {
|
|
if randLen <= 0 {
|
|
return b
|
|
}
|
|
numAdded := 0
|
|
for numAdded < randLen {
|
|
rn := mathrand.Intn(256)
|
|
if rn > 0 && rn < 256 { // exclude 0, also helps to suppress security warning to have a guard here
|
|
b = append(b, byte(rn))
|
|
numAdded++
|
|
}
|
|
}
|
|
return b
|
|
}
|
|
|
|
// returns (isEOF, error)
|
|
func CopyWithEndBytes(outputBuf *bytes.Buffer, reader io.Reader, endBytes []byte) (bool, error) {
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
n, err := reader.Read(buf)
|
|
if n > 0 {
|
|
outputBuf.Write(buf[:n])
|
|
obytes := outputBuf.Bytes()
|
|
if bytes.HasSuffix(obytes, endBytes) {
|
|
outputBuf.Truncate(len(obytes) - len(endBytes))
|
|
return (err == io.EOF), nil
|
|
}
|
|
}
|
|
if err == io.EOF {
|
|
return true, nil
|
|
}
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// does *not* close outputCh on EOF or error
|
|
func CopyToChannel(outputCh chan<- []byte, reader io.Reader) error {
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
n, err := reader.Read(buf)
|
|
if n > 0 {
|
|
// copy so client can use []byte without it being overwritten
|
|
bufCopy := make([]byte, n)
|
|
copy(bufCopy, buf[:n])
|
|
outputCh <- bufCopy
|
|
}
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// on error just returns ""
|
|
// does not return "application/octet-stream" as this is considered a detection failure
|
|
func DetectMimeType(path string) string {
|
|
ext := filepath.Ext(path)
|
|
if mimeType, ok := StaticMimeTypeMap[ext]; ok {
|
|
return mimeType
|
|
}
|
|
if mimeType := mime.TypeByExtension(ext); mimeType != "" {
|
|
return mimeType
|
|
}
|
|
stats, err := os.Stat(path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
if stats.IsDir() {
|
|
return "directory"
|
|
}
|
|
if stats.Mode()&os.ModeNamedPipe == os.ModeNamedPipe {
|
|
return "pipe"
|
|
}
|
|
charDevice := os.ModeDevice | os.ModeCharDevice
|
|
if stats.Mode()&charDevice == charDevice {
|
|
return "character-special"
|
|
}
|
|
if stats.Mode()&os.ModeDevice == os.ModeDevice {
|
|
return "block-special"
|
|
}
|
|
fd, err := os.Open(path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer fd.Close()
|
|
buf := make([]byte, 512)
|
|
// ignore the error (EOF / UnexpectedEOF is fine, just process how much we got back)
|
|
n, _ := io.ReadAtLeast(fd, buf, 512)
|
|
if n == 0 {
|
|
return ""
|
|
}
|
|
buf = buf[:n]
|
|
rtn := http.DetectContentType(buf)
|
|
if rtn == "application/octet-stream" {
|
|
return ""
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
func GetCmdExitCode(cmd *exec.Cmd, err error) int {
|
|
if cmd == nil || cmd.ProcessState == nil {
|
|
return GetExitCode(err)
|
|
}
|
|
status, ok := cmd.ProcessState.Sys().(syscall.WaitStatus)
|
|
if !ok {
|
|
return cmd.ProcessState.ExitCode()
|
|
}
|
|
signaled := status.Signaled()
|
|
if signaled {
|
|
signal := status.Signal()
|
|
return 128 + int(signal)
|
|
}
|
|
exitStatus := status.ExitStatus()
|
|
return exitStatus
|
|
}
|
|
|
|
func GetExitCode(err error) int {
|
|
if err == nil {
|
|
return 0
|
|
}
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
return exitErr.ExitCode()
|
|
} else {
|
|
return -1
|
|
}
|
|
}
|
|
|
|
func GetFirstLine(s string) string {
|
|
idx := strings.Index(s, "\n")
|
|
if idx == -1 {
|
|
return s
|
|
}
|
|
return s[0:idx]
|
|
}
|
|
|
|
func JsonMapToStruct(m map[string]any, v interface{}) error {
|
|
barr, err := json.Marshal(m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return json.Unmarshal(barr, v)
|
|
}
|
|
|
|
func StructToJsonMap(v interface{}) (map[string]any, error) {
|
|
barr, err := json.Marshal(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var m map[string]any
|
|
err = json.Unmarshal(barr, &m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func IndentString(indent string, str string) string {
|
|
splitArr := strings.Split(str, "\n")
|
|
var rtn strings.Builder
|
|
for _, line := range splitArr {
|
|
if line == "" {
|
|
rtn.WriteByte('\n')
|
|
continue
|
|
}
|
|
rtn.WriteString(indent)
|
|
rtn.WriteString(line)
|
|
rtn.WriteByte('\n')
|
|
}
|
|
return rtn.String()
|
|
}
|
|
|
|
func ReUnmarshal(out any, in any) error {
|
|
barr, err := json.Marshal(in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return json.Unmarshal(barr, out)
|
|
}
|
|
|
|
// does a mapstructure using "json" tags
|
|
func DoMapStucture(out any, input any) error {
|
|
dconfig := &mapstructure.DecoderConfig{
|
|
Result: out,
|
|
TagName: "json",
|
|
}
|
|
decoder, err := mapstructure.NewDecoder(dconfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return decoder.Decode(input)
|
|
}
|
|
|
|
func SliceIdx[T comparable](arr []T, elem T) int {
|
|
for idx, e := range arr {
|
|
if e == elem {
|
|
return idx
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func RemoveElemFromSlice[T comparable](arr []T, elem T) []T {
|
|
rtn := make([]T, 0, len(arr))
|
|
for _, e := range arr {
|
|
if e != elem {
|
|
rtn = append(rtn, e)
|
|
}
|
|
}
|
|
return rtn
|
|
}
|
|
|
|
func MoveSliceIdxToFront[T any](arr []T, idx int) []T {
|
|
// create and return a new slice with idx moved to the front
|
|
if idx == 0 || idx >= len(arr) {
|
|
// make a copy still
|
|
return append([]T(nil), arr...)
|
|
}
|
|
rtn := make([]T, 0, len(arr))
|
|
rtn = append(rtn, arr[idx])
|
|
rtn = append(rtn, arr[0:idx]...)
|
|
rtn = append(rtn, arr[idx+1:]...)
|
|
return rtn
|
|
}
|