yao/sandbox/helpers.go

314 lines
6.2 KiB
Go

package sandbox
import (
"archive/tar"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
// mapToSlice converts map to []string for environment variables
func mapToSlice(m map[string]string) []string {
if m == nil {
return nil
}
result := make([]string, 0, len(m))
for k, v := range m {
result = append(result, k+"="+v)
}
return result
}
// parseMemory converts string like "2g" to bytes
func parseMemory(s string) int64 {
if s == "" {
return 0
}
s = strings.ToLower(strings.TrimSpace(s))
if len(s) < 2 {
v, _ := strconv.ParseInt(s, 10, 64)
return v
}
unit := s[len(s)-1]
numStr := s[:len(s)-1]
num, err := strconv.ParseFloat(numStr, 64)
if err != nil {
return 0
}
switch unit {
case 'k':
return int64(num * 1024)
case 'm':
return int64(num * 1024 * 1024)
case 'g':
return int64(num * 1024 * 1024 * 1024)
case 't':
return int64(num * 1024 * 1024 * 1024 * 1024)
default:
// Assume bytes if no unit
v, _ := strconv.ParseInt(s, 10, 64)
return v
}
}
// parseLS parses ls -la --time-style=+%s output to []FileInfo
func parseLS(output string) []FileInfo {
lines := strings.Split(strings.TrimSpace(output), "\n")
var result []FileInfo
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "total") {
continue
}
// Parse ls -la output: drwxr-xr-x 2 user group 4096 1234567890 filename
fields := strings.Fields(line)
if len(fields) < 7 {
continue
}
// Parse mode
modeStr := fields[0]
if len(modeStr) == 0 {
continue
}
mode := parseLSMode(modeStr)
// Parse size
size, _ := strconv.ParseInt(fields[4], 10, 64)
// Parse timestamp (Unix epoch)
timestamp, _ := strconv.ParseInt(fields[5], 10, 64)
modTime := time.Unix(timestamp, 0)
// Get filename (may contain spaces)
name := strings.Join(fields[6:], " ")
// Skip . and ..
if name == "." || name == ".." {
continue
}
result = append(result, FileInfo{
Name: name,
Size: size,
Mode: mode,
ModTime: modTime,
IsDir: modeStr[0] == 'd',
})
}
return result
}
// parseLSMode parses ls mode string to os.FileMode
func parseLSMode(s string) os.FileMode {
if len(s) < 10 {
return 0
}
var mode os.FileMode
// File type
switch s[0] {
case 'd':
mode |= os.ModeDir
case 'l':
mode |= os.ModeSymlink
case 'c':
mode |= os.ModeCharDevice
case 'b':
mode |= os.ModeDevice
case 'p':
mode |= os.ModeNamedPipe
case 's':
mode |= os.ModeSocket
}
// Permissions
perms := s[1:10]
permBits := []os.FileMode{
0400, 0200, 0100, // owner
0040, 0020, 0010, // group
0004, 0002, 0001, // other
}
for i, b := range perms {
if b != '-' && i < len(permBits) {
mode |= permBits[i]
}
}
return mode
}
// parseStat parses stat --format=%n|%s|%f|%Y|%F output to *FileInfo
func parseStat(output string) *FileInfo {
output = strings.TrimSpace(output)
parts := strings.Split(output, "|")
if len(parts) < 5 {
return nil
}
name := parts[0]
size, _ := strconv.ParseInt(parts[1], 10, 64)
modeHex, _ := strconv.ParseUint(parts[2], 16, 32)
timestamp, _ := strconv.ParseInt(parts[3], 10, 64)
fileType := parts[4]
return &FileInfo{
Name: filepath.Base(name),
Path: name,
Size: size,
Mode: os.FileMode(modeHex),
ModTime: time.Unix(timestamp, 0),
IsDir: strings.Contains(fileType, "directory"),
}
}
// createTarFromPath creates a tar archive from a host path
func createTarFromPath(hostPath string) (io.ReadCloser, error) {
// Validate path exists before starting goroutine
info, err := os.Stat(hostPath)
if err != nil {
return nil, fmt.Errorf("failed to stat path: %w", err)
}
pr, pw := io.Pipe()
go func() {
tw := tar.NewWriter(pw)
var finalErr error
defer func() {
tw.Close()
if finalErr != nil {
pw.CloseWithError(finalErr)
} else {
pw.Close()
}
}()
baseDir := filepath.Dir(hostPath)
walkFn := func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
// Get relative path
relPath, err := filepath.Rel(baseDir, path)
if err != nil {
return err
}
// Create header
header, err := tar.FileInfoHeader(fi, "")
if err != nil {
return err
}
header.Name = relPath
// Handle symlinks
if fi.Mode()&os.ModeSymlink != 0 {
link, err := os.Readlink(path)
if err != nil {
return err
}
header.Linkname = link
}
if err := tw.WriteHeader(header); err != nil {
return err
}
// Write file content
if fi.Mode().IsRegular() {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(tw, f); err != nil {
return err
}
}
return nil
}
if info.IsDir() {
finalErr = filepath.Walk(hostPath, walkFn)
} else {
finalErr = walkFn(hostPath, info, nil)
}
}()
return pr, nil
}
// extractTarToPath extracts a tar archive to a host path
func extractTarToPath(reader io.Reader, hostPath string) error {
tr := tar.NewReader(reader)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("tar read error: %w", err)
}
target := filepath.Join(hostPath, header.Name)
// Security check: prevent path traversal
if !strings.HasPrefix(filepath.Clean(target), filepath.Clean(hostPath)) {
return fmt.Errorf("invalid tar path: %s", header.Name)
}
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
return err
}
case tar.TypeReg:
// Ensure parent directory exists
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
return err
}
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return err
}
if _, err := io.Copy(f, tr); err != nil {
f.Close()
return err
}
f.Close()
case tar.TypeSymlink:
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
return err
}
os.Remove(target) // Remove existing symlink if any
if err := os.Symlink(header.Linkname, target); err != nil {
return err
}
}
}
return nil
}
// containerName generates a container name from userID and chatID
func containerName(userID, chatID string) string {
return fmt.Sprintf("yao-sandbox-%s-%s", userID, chatID)
}