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 }