115 lines
2.9 KiB
Go
115 lines
2.9 KiB
Go
package workspace
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
func SafePath(root string, name string) (string, error) {
|
|
root = strings.TrimSpace(root)
|
|
if root == "" {
|
|
return "", fmt.Errorf("workspace root must not be empty")
|
|
}
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return "", fmt.Errorf("workspace artifact name must not be empty")
|
|
}
|
|
if strings.Contains(name, `\`) {
|
|
return "", fmt.Errorf("workspace artifact name %q must use slash-separated relative paths", name)
|
|
}
|
|
if path.IsAbs(name) || filepath.IsAbs(name) {
|
|
return "", fmt.Errorf("workspace artifact name %q must be relative", name)
|
|
}
|
|
if name == "." || strings.Contains(name, "..") {
|
|
return "", fmt.Errorf("workspace artifact name %q must not contain ..", name)
|
|
}
|
|
cleaned := path.Clean(name)
|
|
if cleaned != name {
|
|
return "", fmt.Errorf("workspace artifact name %q must be clean", name)
|
|
}
|
|
|
|
absRoot, err := filepath.Abs(root)
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolve workspace root %q: %w", root, err)
|
|
}
|
|
target, err := filepath.Abs(filepath.Join(absRoot, filepath.FromSlash(cleaned)))
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolve workspace artifact %q: %w", name, err)
|
|
}
|
|
rel, err := filepath.Rel(absRoot, target)
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolve workspace artifact %q: %w", name, err)
|
|
}
|
|
if rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
|
return "", fmt.Errorf("workspace artifact name %q resolves outside workspace root", name)
|
|
}
|
|
return target, nil
|
|
}
|
|
|
|
func WriteJSON(root string, name string, payload any) error {
|
|
target, err := SafePath(root, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data, err := json.MarshalIndent(payload, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("marshal workspace artifact %q: %w", name, err)
|
|
}
|
|
data = append(data, '\n')
|
|
if err := writeFileAtomic(target, data, 0o644); err != nil {
|
|
return fmt.Errorf("write workspace artifact %q: %w", name, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func WriteBytes(root string, name string, data []byte) error {
|
|
target, err := SafePath(root, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := writeFileAtomic(target, data, 0o644); err != nil {
|
|
return fmt.Errorf("write workspace artifact %q: %w", name, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeFileAtomic(target string, data []byte, perm os.FileMode) error {
|
|
dir := filepath.Dir(target)
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
|
|
temp, err := os.CreateTemp(dir, "."+filepath.Base(target)+".tmp-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tempPath := temp.Name()
|
|
removeTemp := true
|
|
defer func() {
|
|
if removeTemp {
|
|
_ = os.Remove(tempPath)
|
|
}
|
|
}()
|
|
|
|
if _, err := temp.Write(data); err != nil {
|
|
_ = temp.Close()
|
|
return err
|
|
}
|
|
if err := temp.Chmod(perm); err != nil {
|
|
_ = temp.Close()
|
|
return err
|
|
}
|
|
if err := temp.Close(); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Rename(tempPath, target); err != nil {
|
|
return err
|
|
}
|
|
removeTemp = false
|
|
return nil
|
|
}
|