Skip to content

Commit f534c34

Browse files
authored
Merge pull request #33 from TheManticoreProject/improve-utils
[enhancement] Improve utils
2 parents 03c727c + bb51372 commit f534c34

File tree

10 files changed

+719
-30
lines changed

10 files changed

+719
-30
lines changed

utils/path.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package utils
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
)
8+
9+
// PathSafeString returns a string that is safe to use in file paths by replacing unsafe characters.
10+
//
11+
// Parameters:
12+
// - input: The input string to be sanitized.
13+
//
14+
// Returns:
15+
// - A string that is safe to use in file paths.
16+
func PathSafeString(input string) string {
17+
unsafeChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|", " ", "\n", "\r", "\t", "\v", "\f", "\b", " "}
18+
safeString := input
19+
for _, char := range unsafeChars {
20+
safeString = strings.ReplaceAll(safeString, char, "_")
21+
}
22+
return safeString
23+
}
24+
25+
// EnsureDirExists checks if a directory exists and creates it if it doesn't.
26+
// It uses os.MkdirAll, so it can create parent directories as needed.
27+
//
28+
// Parameters:
29+
// - dirPath: The path of the directory to ensure existence.
30+
//
31+
// Returns:
32+
// - An error if the directory could not be created or if there was an issue checking its existence.
33+
func EnsureDirExists(dirPath string) error {
34+
info, err := os.Stat(dirPath)
35+
if err == nil {
36+
if info.IsDir() {
37+
return nil
38+
}
39+
return fmt.Errorf("path exists and is not a directory: %s", dirPath)
40+
}
41+
if os.IsNotExist(err) {
42+
return os.MkdirAll(dirPath, 0755) // 0755 is a common permission for directories
43+
}
44+
return err
45+
}

utils/path_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package utils_test
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/TheManticoreProject/Manticore/utils"
9+
)
10+
11+
func TestPathSafeString(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
input string
15+
expected string
16+
}{
17+
{
18+
name: "empty string",
19+
input: "",
20+
expected: "",
21+
},
22+
{
23+
name: "no unsafe characters",
24+
input: "safe_filename_123",
25+
expected: "safe_filename_123",
26+
},
27+
{
28+
name: "common unsafe characters",
29+
input: "file/name\\with:bad*chars?\"<>|",
30+
expected: "file_name_with_bad_chars_____",
31+
},
32+
{
33+
name: "whitespace characters",
34+
input: "file name\nwith\rspaces\t",
35+
expected: "file_name_with_spaces_",
36+
},
37+
{
38+
name: "mixed safe and unsafe",
39+
input: "My Document (v1.0)/part_A.txt",
40+
expected: "My_Document_(v1.0)_part_A.txt",
41+
},
42+
{
43+
name: "all unsafe characters",
44+
input: "/:*?\"<>| \n\r\t",
45+
expected: "____________",
46+
},
47+
{
48+
name: "multiple occurrences of same unsafe char",
49+
input: "path//to\\file",
50+
expected: "path__to_file",
51+
},
52+
{
53+
name: "leading/trailing unsafe chars",
54+
input: " /file.txt ",
55+
expected: "__file.txt_",
56+
},
57+
}
58+
59+
for _, tt := range tests {
60+
t.Run(tt.name, func(t *testing.T) {
61+
got := utils.PathSafeString(tt.input)
62+
if got != tt.expected {
63+
t.Errorf("PathSafeString(%q) = %q, want %q", tt.input, got, tt.expected)
64+
}
65+
})
66+
}
67+
}
68+
69+
func TestEnsureDirExists(t *testing.T) {
70+
baseTempDir := filepath.Join(os.TempDir(), "test_ensure_dir_exists")
71+
// Ensure cleanup for all tests in this function
72+
defer func() {
73+
if err := os.RemoveAll(baseTempDir); err != nil {
74+
t.Logf("Failed to clean up base temp directory %q: %v", baseTempDir, err)
75+
}
76+
}()
77+
78+
t.Run("directory does not exist", func(t *testing.T) {
79+
dirPath := filepath.Join(baseTempDir, "new_dir")
80+
// Ensure it doesn't exist before the test
81+
_ = os.RemoveAll(dirPath)
82+
83+
err := utils.EnsureDirExists(dirPath)
84+
if err != nil {
85+
t.Fatalf("EnsureDirExists(%q) returned an error: %v", dirPath, err)
86+
}
87+
88+
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
89+
t.Errorf("EnsureDirExists(%q) failed to create directory", dirPath)
90+
}
91+
})
92+
93+
t.Run("directory already exists", func(t *testing.T) {
94+
dirPath := filepath.Join(baseTempDir, "existing_dir")
95+
err := os.MkdirAll(dirPath, 0755)
96+
if err != nil {
97+
t.Fatalf("Failed to create pre-existing directory: %v", err)
98+
}
99+
100+
err = utils.EnsureDirExists(dirPath)
101+
if err != nil {
102+
t.Errorf("EnsureDirExists(%q) returned an error for existing directory: %v", dirPath, err)
103+
}
104+
})
105+
106+
t.Run("nested directories", func(t *testing.T) {
107+
dirPath := filepath.Join(baseTempDir, "parent", "child", "grandchild")
108+
// Ensure parent doesn't exist before the test
109+
_ = os.RemoveAll(filepath.Join(baseTempDir, "parent"))
110+
111+
err := utils.EnsureDirExists(dirPath)
112+
if err != nil {
113+
t.Fatalf("EnsureDirExists(%q) returned an error for nested directories: %v", dirPath, err)
114+
}
115+
116+
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
117+
t.Errorf("EnsureDirExists(%q) failed to create nested directories", dirPath)
118+
}
119+
})
120+
121+
t.Run("path exists as file returns error", func(t *testing.T) {
122+
filePath := filepath.Join(baseTempDir, "somefile")
123+
// Ensure fresh base dir
124+
_ = os.RemoveAll(baseTempDir)
125+
if err := os.MkdirAll(baseTempDir, 0755); err != nil {
126+
t.Fatalf("failed to create base temp dir: %v", err)
127+
}
128+
// Create a file at the path
129+
f, err := os.Create(filePath)
130+
if err != nil {
131+
t.Fatalf("failed to create file for test: %v", err)
132+
}
133+
_ = f.Close()
134+
135+
err = utils.EnsureDirExists(filePath)
136+
if err == nil {
137+
t.Fatalf("EnsureDirExists(%q) did not return error when path is an existing file", filePath)
138+
}
139+
})
140+
}

utils/random.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package utils
2+
3+
import (
4+
crand "crypto/rand"
5+
"math/big"
6+
mrand "math/rand"
7+
)
8+
9+
// RandomString generates a random string of the specified length.
10+
//
11+
// Parameters:
12+
// - length: The length of the random string to generate.
13+
//
14+
// Returns:
15+
// - A string of the specified length.
16+
func RandomString(length int) string {
17+
if length <= 0 {
18+
return ""
19+
}
20+
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
21+
22+
b := make([]byte, length)
23+
rangeSize := big.NewInt(int64(len(charset)))
24+
for i := 0; i < length; i++ {
25+
n, err := crand.Int(crand.Reader, rangeSize)
26+
if err != nil {
27+
b[i] = charset[mrand.Intn(len(charset))]
28+
continue
29+
}
30+
b[i] = charset[n.Int64()]
31+
}
32+
33+
return string(b)
34+
}
35+
36+
// RandomInt generates a random integer between min (inclusive) and max (inclusive).
37+
//
38+
// Parameters:
39+
// - min: The minimum value (inclusive).
40+
// - max: The maximum value (inclusive).
41+
//
42+
// Returns:
43+
// - A random integer within the specified range.
44+
func RandomInt(min, max int) int {
45+
if min > max {
46+
min, max = max, min // Swap if min is greater than max
47+
}
48+
// Use int64 to avoid overflow when computing range size
49+
a := int64(min)
50+
b := int64(max)
51+
rangeSize := b - a + 1
52+
if rangeSize <= 0 {
53+
// Overflow or invalid range; return min as a safe fallback
54+
return min
55+
}
56+
r, err := crand.Int(crand.Reader, big.NewInt(rangeSize))
57+
if err != nil {
58+
// Fallback to math/rand if crypto/rand fails
59+
return int(a + (mrand.Int63n(rangeSize)))
60+
}
61+
return int(a + r.Int64())
62+
}
63+
64+
// RandomBytes generates a random byte slice of the specified length.
65+
//
66+
// Parameters:
67+
// - length: The length of the byte slice to generate.
68+
//
69+
// Returns:
70+
// - A byte slice filled with random data.
71+
func RandomBytes(length int) []byte {
72+
if length <= 0 {
73+
return []byte{}
74+
}
75+
b := make([]byte, length)
76+
if _, err := crand.Read(b); err != nil {
77+
// Fallback to non-crypto PRNG if crypto/rand fails
78+
for i := range b {
79+
b[i] = byte(mrand.Intn(256))
80+
}
81+
}
82+
return b
83+
}
84+
85+
// RandomBool generates a random boolean value.
86+
//
87+
// Returns:
88+
// - A random boolean (true or false).
89+
func RandomBool() bool {
90+
var one [1]byte
91+
if _, err := crand.Read(one[:]); err == nil {
92+
return (one[0] & 1) == 0 // 50% chance for true, 50% for false
93+
}
94+
return mrand.Intn(2) == 0
95+
}

0 commit comments

Comments
 (0)