package execution import ( "archive/tar" "bytes" "context" "encoding/json" "fmt" "io" "os" "path/filepath" "strconv" "strings" "time" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" "github.com/docker/go-connections/nat" "github.com/docker/go-units" ) // DockerSandbox implements ExecutionSandbox using Docker containers type DockerSandbox struct { client *client.Client containerID string config *SandboxConfig info SandboxInfo workingDir string environment map[string]string tempDir string } // NewDockerSandbox creates a new Docker-based sandbox func NewDockerSandbox() *DockerSandbox { return &DockerSandbox{ environment: make(map[string]string), } } // Initialize sets up the Docker container sandbox func (d *DockerSandbox) Initialize(ctx context.Context, config *SandboxConfig) error { d.config = config // Initialize Docker client cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return NewSandboxErrorWithCause(ErrSandboxInitFailed, "failed to create Docker client", err) } d.client = cli // Create temporary directory for file operations tempDir, err := os.MkdirTemp("", "chorus-sandbox-*") if err != nil { return NewSandboxErrorWithCause(ErrSandboxInitFailed, "failed to create temp directory", err) } d.tempDir = tempDir // Pull image if needed if err := d.ensureImage(ctx); err != nil { return err } // Create and start container if err := d.createContainer(ctx); err != nil { return err } if err := d.startContainer(ctx); err != nil { return err } // Set up repository if configured if config.Repository.URL != "" || config.Repository.LocalPath != "" { if err := d.setupRepository(ctx); err != nil { return err } } // Configure environment and working directory d.workingDir = config.WorkingDir if d.workingDir == "" { d.workingDir = config.Repository.MountPoint if d.workingDir == "" { d.workingDir = "/workspace" } } // Merge environment variables for k, v := range config.Environment { d.environment[k] = v } d.info = SandboxInfo{ ID: d.containerID, Name: fmt.Sprintf("chorus-sandbox-%s", d.containerID[:12]), Type: "docker", Status: StatusRunning, CreatedAt: time.Now(), StartedAt: time.Now(), Runtime: "docker", Image: config.Image, Platform: config.Architecture, Config: *config, Labels: config.Labels, Annotations: config.Annotations, } return nil } // ExecuteCommand runs a command in the Docker container func (d *DockerSandbox) ExecuteCommand(ctx context.Context, cmd *Command) (*CommandResult, error) { if d.containerID == "" { return nil, NewSandboxError(ErrSandboxNotRunning, "container not initialized") } startTime := time.Now() // Build command arguments execCmd := []string{cmd.Executable} execCmd = append(execCmd, cmd.Args...) // Prepare environment env := d.buildEnvironment(cmd.Environment) // Set working directory workDir := cmd.WorkingDir if workDir == "" { workDir = d.workingDir } // Create execution configuration execConfig := container.ExecOptions{ User: cmd.User, Privileged: false, Tty: false, AttachStdin: cmd.Stdin != nil || cmd.StdinContent != "", AttachStdout: true, AttachStderr: true, Env: env, WorkingDir: workDir, Cmd: execCmd, } // Create exec instance exec, err := d.client.ContainerExecCreate(ctx, d.containerID, execConfig) if err != nil { return nil, NewSandboxErrorWithCause(ErrCommandExecutionFailed, "failed to create exec instance", err) } // Attach to execution attachOptions := container.ExecAttachOptions{} resp, err := d.client.ContainerExecAttach(ctx, exec.ID, attachOptions) if err != nil { return nil, NewSandboxErrorWithCause(ErrCommandExecutionFailed, "failed to attach to exec", err) } defer resp.Close() // Handle stdin if provided if cmd.Stdin != nil { go func() { defer resp.CloseWrite() io.Copy(resp.Conn, cmd.Stdin) }() } else if cmd.StdinContent != "" { go func() { defer resp.CloseWrite() io.WriteString(resp.Conn, cmd.StdinContent) }() } // Read stdout and stderr var stdout, stderr bytes.Buffer var combined bytes.Buffer // Use a multiplexed reader to separate stdout/stderr stdoutReader := io.TeeReader(resp.Reader, &combined) go func() { // Docker multiplexes stdout/stderr, we need to demultiplex d.demultiplexOutput(stdoutReader, &stdout, &stderr) }() // Wait for execution with timeout execCtx := ctx if cmd.Timeout > 0 { var cancel context.CancelFunc execCtx, cancel = context.WithTimeout(ctx, cmd.Timeout) defer cancel() } // Wait for completion for { inspect, err := d.client.ContainerExecInspect(execCtx, exec.ID) if err != nil { return nil, NewSandboxErrorWithCause(ErrCommandExecutionFailed, "failed to inspect exec", err) } if !inspect.Running { endTime := time.Now() // Get resource usage resourceUsage, _ := d.GetResourceUsage(ctx) result := &CommandResult{ ExitCode: inspect.ExitCode, Success: inspect.ExitCode == 0, Stdout: stdout.String(), Stderr: stderr.String(), Combined: combined.String(), StartTime: startTime, EndTime: endTime, Duration: endTime.Sub(startTime), ProcessID: inspect.Pid, } if resourceUsage != nil { result.ResourceUsage = *resourceUsage } if inspect.ExitCode != 0 { result.Error = fmt.Sprintf("command exited with code %d", inspect.ExitCode) } return result, nil } // Check for timeout select { case <-execCtx.Done(): return nil, NewSandboxError(ErrTimeoutExceeded, "command execution timed out") case <-time.After(100 * time.Millisecond): // Continue polling } } } // CopyFiles copies files between host and container func (d *DockerSandbox) CopyFiles(ctx context.Context, source, dest string) error { if d.containerID == "" { return NewSandboxError(ErrSandboxNotRunning, "container not initialized") } // Determine copy direction based on paths if strings.HasPrefix(source, "container:") { // Copy from container to host containerPath := strings.TrimPrefix(source, "container:") return d.copyFromContainer(ctx, containerPath, dest) } else if strings.HasPrefix(dest, "container:") { // Copy from host to container containerPath := strings.TrimPrefix(dest, "container:") return d.copyToContainer(ctx, source, containerPath) } return NewSandboxError(ErrFileOperationFailed, "invalid copy paths: use container: prefix for container paths") } // WriteFile writes content to a file in the container func (d *DockerSandbox) WriteFile(ctx context.Context, path string, content []byte, mode uint32) error { // Create a tar archive with the file buf := new(bytes.Buffer) tw := tar.NewWriter(buf) header := &tar.Header{ Name: filepath.Base(path), Mode: int64(mode), Size: int64(len(content)), } if err := tw.WriteHeader(header); err != nil { return NewSandboxErrorWithCause(ErrFileOperationFailed, "failed to create tar header", err) } if _, err := tw.Write(content); err != nil { return NewSandboxErrorWithCause(ErrFileOperationFailed, "failed to write file content", err) } tw.Close() // Copy to container containerDir := filepath.Dir(path) return d.client.CopyToContainer(ctx, d.containerID, containerDir, buf, container.CopyToContainerOptions{}) } // ReadFile reads content from a file in the container func (d *DockerSandbox) ReadFile(ctx context.Context, path string) ([]byte, error) { reader, _, err := d.client.CopyFromContainer(ctx, d.containerID, path) if err != nil { return nil, NewSandboxErrorWithCause(ErrFileOperationFailed, "failed to copy from container", err) } defer reader.Close() // Extract from tar archive tr := tar.NewReader(reader) header, err := tr.Next() if err != nil { return nil, NewSandboxErrorWithCause(ErrFileOperationFailed, "failed to read tar header", err) } content := make([]byte, header.Size) if _, err := io.ReadFull(tr, content); err != nil { return nil, NewSandboxErrorWithCause(ErrFileOperationFailed, "failed to read file content", err) } return content, nil } // ListFiles lists files in a directory within the container func (d *DockerSandbox) ListFiles(ctx context.Context, path string) ([]FileInfo, error) { // Use ls command to list files cmd := &Command{ Executable: "ls", Args: []string{"-la", "--time-style=+%Y-%m-%d %H:%M:%S", path}, } result, err := d.ExecuteCommand(ctx, cmd) if err != nil { return nil, err } if !result.Success { return nil, NewSandboxError(ErrFileOperationFailed, "ls command failed: "+result.Stderr) } // Parse ls output return d.parseLsOutput(result.Stdout, path) } // GetWorkingDirectory returns the current working directory func (d *DockerSandbox) GetWorkingDirectory() string { return d.workingDir } // SetWorkingDirectory changes the working directory func (d *DockerSandbox) SetWorkingDirectory(path string) error { d.workingDir = path return nil } // GetEnvironment returns environment variables func (d *DockerSandbox) GetEnvironment() map[string]string { env := make(map[string]string) for k, v := range d.environment { env[k] = v } return env } // SetEnvironment sets environment variables func (d *DockerSandbox) SetEnvironment(env map[string]string) error { for k, v := range env { d.environment[k] = v } return nil } // GetResourceUsage returns current resource usage func (d *DockerSandbox) GetResourceUsage(ctx context.Context) (*ResourceUsage, error) { if d.containerID == "" { return nil, NewSandboxError(ErrSandboxNotRunning, "container not initialized") } stats, err := d.client.ContainerStats(ctx, d.containerID, false) if err != nil { return nil, NewSandboxErrorWithCause(ErrSandboxInitFailed, "failed to get container stats", err) } defer stats.Body.Close() var dockerStats container.StatsResponse if err := json.NewDecoder(stats.Body).Decode(&dockerStats); err != nil { return nil, NewSandboxErrorWithCause(ErrSandboxInitFailed, "failed to decode stats", err) } // Calculate CPU usage percentage cpuDelta := float64(dockerStats.CPUStats.CPUUsage.TotalUsage - dockerStats.PreCPUStats.CPUUsage.TotalUsage) systemDelta := float64(dockerStats.CPUStats.SystemUsage - dockerStats.PreCPUStats.SystemUsage) cpuPercent := 0.0 if systemDelta > 0 { cpuPercent = (cpuDelta / systemDelta) * float64(len(dockerStats.CPUStats.CPUUsage.PercpuUsage)) * 100.0 } // Calculate memory usage memUsage := dockerStats.MemoryStats.Usage if dockerStats.MemoryStats.Stats != nil { if cache, ok := dockerStats.MemoryStats.Stats["cache"]; ok { memUsage -= cache } } memPercent := 0.0 if dockerStats.MemoryStats.Limit > 0 { memPercent = float64(memUsage) / float64(dockerStats.MemoryStats.Limit) * 100.0 } return &ResourceUsage{ Timestamp: time.Now(), CPUUsage: cpuPercent, CPUTime: time.Duration(dockerStats.CPUStats.CPUUsage.TotalUsage), MemoryUsage: int64(memUsage), MemoryPercent: memPercent, MemoryPeak: int64(dockerStats.MemoryStats.MaxUsage), NetworkIn: int64(dockerStats.Networks["eth0"].RxBytes), NetworkOut: int64(dockerStats.Networks["eth0"].TxBytes), ProcessCount: int(dockerStats.PidsStats.Current), }, nil } // Cleanup destroys the container and cleans up resources func (d *DockerSandbox) Cleanup() error { if d.client == nil { return nil } var lastErr error // Stop container if running if d.containerID != "" { timeout := int(30) // 30 seconds if err := d.client.ContainerStop(context.Background(), d.containerID, container.StopOptions{ Timeout: &timeout, }); err != nil { lastErr = err } // Remove container if err := d.client.ContainerRemove(context.Background(), d.containerID, container.RemoveOptions{ Force: true, }); err != nil { lastErr = err } } // Clean up temporary directory if d.tempDir != "" { if err := os.RemoveAll(d.tempDir); err != nil { lastErr = err } } // Close Docker client if err := d.client.Close(); err != nil { lastErr = err } if lastErr != nil { return NewSandboxErrorWithCause(ErrSandboxInitFailed, "cleanup failed", lastErr) } d.info.Status = StatusDestroyed return nil } // GetInfo returns sandbox information func (d *DockerSandbox) GetInfo() SandboxInfo { return d.info } // ensureImage pulls the Docker image if it doesn't exist func (d *DockerSandbox) ensureImage(ctx context.Context) error { // Check if image exists locally imageFilters := filters.NewArgs() imageFilters.Add("reference", d.config.Image) images, err := d.client.ImageList(ctx, image.ListOptions{ Filters: imageFilters, }) if err != nil { return NewSandboxErrorWithCause(ErrSandboxInitFailed, "failed to list images", err) } // Pull image if not found if len(images) == 0 { reader, err := d.client.ImagePull(ctx, d.config.Image, image.PullOptions{}) if err != nil { return NewSandboxErrorWithCause(ErrSandboxInitFailed, "failed to pull image", err) } defer reader.Close() // Wait for pull to complete if _, err := io.ReadAll(reader); err != nil { return NewSandboxErrorWithCause(ErrSandboxInitFailed, "failed to read pull response", err) } } return nil } // createContainer creates the Docker container func (d *DockerSandbox) createContainer(ctx context.Context) error { // Build container configuration containerConfig := d.buildContainerConfig() hostConfig := d.buildHostConfig() networkConfig := d.buildNetworkConfig() // Create container resp, err := d.client.ContainerCreate(ctx, containerConfig, hostConfig, networkConfig, nil, "") if err != nil { return NewSandboxErrorWithCause(ErrSandboxInitFailed, "failed to create container", err) } d.containerID = resp.ID d.info.Status = StatusCreating return nil } // startContainer starts the Docker container func (d *DockerSandbox) startContainer(ctx context.Context) error { if err := d.client.ContainerStart(ctx, d.containerID, container.StartOptions{}); err != nil { return NewSandboxErrorWithCause(ErrSandboxInitFailed, "failed to start container", err) } d.info.Status = StatusRunning return nil } // buildContainerConfig builds the container configuration func (d *DockerSandbox) buildContainerConfig() *container.Config { env := d.buildEnvironment(d.config.Environment) config := &container.Config{ Image: d.config.Image, Env: env, WorkingDir: d.config.WorkingDir, Cmd: []string{"tail", "-f", "/dev/null"}, // Keep container running AttachStdin: false, AttachStdout: false, AttachStderr: false, Tty: false, OpenStdin: false, StdinOnce: false, Labels: d.config.Labels, } // Configure user if specified if d.config.Security.RunAsUser != "" { config.User = d.config.Security.RunAsUser if d.config.Security.RunAsGroup != "" { config.User = d.config.Security.RunAsUser + ":" + d.config.Security.RunAsGroup } } // Add exposed ports if len(d.config.Network.PortMappings) > 0 { exposedPorts := make(nat.PortSet) for _, mapping := range d.config.Network.PortMappings { port := nat.Port(fmt.Sprintf("%d/%s", mapping.ContainerPort, mapping.Protocol)) exposedPorts[port] = struct{}{} } config.ExposedPorts = exposedPorts } return config } // buildHostConfig builds the host configuration func (d *DockerSandbox) buildHostConfig() *container.HostConfig { // Configure resource limits resources := container.Resources{} if d.config.Resources.MemoryLimit > 0 { resources.Memory = d.config.Resources.MemoryLimit } if d.config.Resources.MemoryRequest > 0 { resources.MemoryReservation = d.config.Resources.MemoryRequest } if d.config.Resources.CPULimit > 0 { resources.NanoCPUs = int64(d.config.Resources.CPULimit * 1e9) } if d.config.Resources.ProcessLimit > 0 { pidsLimit := int64(d.config.Resources.ProcessLimit) resources.PidsLimit = &pidsLimit } // Configure ulimits for file handles and other limits ulimits := []*units.Ulimit{} if d.config.Resources.FileLimit > 0 { ulimits = append(ulimits, &units.Ulimit{ Name: "nofile", Soft: int64(d.config.Resources.FileLimit), Hard: int64(d.config.Resources.FileLimit), }) } // Configure security options securityOpt := []string{} if d.config.Security.NoNewPrivileges { securityOpt = append(securityOpt, "no-new-privileges:true") } if d.config.Security.SELinuxContext != "" { securityOpt = append(securityOpt, "label="+d.config.Security.SELinuxContext) } if d.config.Security.AppArmorProfile != "" { securityOpt = append(securityOpt, "apparmor="+d.config.Security.AppArmorProfile) } if d.config.Security.SeccompProfile != "" { securityOpt = append(securityOpt, "seccomp="+d.config.Security.SeccompProfile) } // Configure capabilities - default to secure by dropping all, then adding only necessary ones capAdd := []string{} capDrop := []string{} if len(d.config.Security.DropCapabilities) > 0 { capDrop = append(capDrop, d.config.Security.DropCapabilities...) } else { // Default security: drop all capabilities capDrop = append(capDrop, "ALL") } if len(d.config.Security.AddCapabilities) > 0 { capAdd = append(capAdd, d.config.Security.AddCapabilities...) } else if d.config.Security.AllowNetworking { // Add minimal networking capabilities if networking is allowed capAdd = append(capAdd, "NET_BIND_SERVICE") } // Configure network settings networkMode := container.NetworkMode("bridge") if d.config.Security.IsolateNetwork || !d.config.Security.AllowNetworking { networkMode = container.NetworkMode("none") } hostConfig := &container.HostConfig{ RestartPolicy: container.RestartPolicy{Name: "no"}, NetworkMode: networkMode, IpcMode: container.IpcMode("private"), PidMode: container.PidMode("private"), Resources: resources, SecurityOpt: securityOpt, CapAdd: capAdd, CapDrop: capDrop, ReadonlyRootfs: d.config.Security.ReadOnlyRoot, UsernsMode: container.UsernsMode("host"), // Required for proper file permissions AutoRemove: false, // We handle cleanup manually Privileged: false, // Never run privileged PublishAllPorts: false, } // Configure user if specified if d.config.Security.RunAsUser != "" { // Note: User is set in container config, not host config } // Configure port mappings if len(d.config.Network.PortMappings) > 0 { portBindings := make(nat.PortMap) for _, mapping := range d.config.Network.PortMappings { containerPort := nat.Port(fmt.Sprintf("%d/%s", mapping.ContainerPort, mapping.Protocol)) hostBinding := nat.PortBinding{ HostPort: strconv.Itoa(mapping.HostPort), } portBindings[containerPort] = []nat.PortBinding{hostBinding} } hostConfig.PortBindings = portBindings } // Configure mounts mounts := []mount.Mount{} // Add repository mount if configured if d.config.Repository.LocalPath != "" { mountPoint := d.config.Repository.MountPoint if mountPoint == "" { mountPoint = "/workspace" } mounts = append(mounts, mount.Mount{ Type: mount.TypeBind, Source: d.config.Repository.LocalPath, Target: mountPoint, ReadOnly: d.config.Repository.ReadOnly, BindOptions: &mount.BindOptions{ Propagation: mount.PropagationRPrivate, }, }) } // Add tmpfs mounts for security for _, tmpfsPath := range d.config.Security.TmpfsPaths { mounts = append(mounts, mount.Mount{ Type: mount.TypeTmpfs, Target: tmpfsPath, TmpfsOptions: &mount.TmpfsOptions{ SizeBytes: 100 * 1024 * 1024, // 100MB default Mode: 0755, }, }) } // Add masked paths (bind mount /dev/null over them) for _, maskedPath := range d.config.Security.MaskedPaths { mounts = append(mounts, mount.Mount{ Type: mount.TypeBind, Source: "/dev/null", Target: maskedPath, ReadOnly: true, }) } // Add read-only bind mounts for _, readOnlyPath := range d.config.Security.ReadOnlyPaths { mounts = append(mounts, mount.Mount{ Type: mount.TypeBind, Source: readOnlyPath, Target: readOnlyPath, ReadOnly: true, BindOptions: &mount.BindOptions{ Propagation: mount.PropagationRPrivate, }, }) } hostConfig.Mounts = mounts return hostConfig } // buildNetworkConfig builds the network configuration func (d *DockerSandbox) buildNetworkConfig() *network.NetworkingConfig { return &network.NetworkingConfig{} } // setupRepository sets up the repository in the container func (d *DockerSandbox) setupRepository(ctx context.Context) error { if d.config.Repository.LocalPath == "" { return nil // No local repository to set up } // Repository is mounted via volume, configure Git if needed if d.config.Repository.GitConfig.UserName != "" { gitCmds := [][]string{ {"git", "config", "--global", "user.name", d.config.Repository.GitConfig.UserName}, {"git", "config", "--global", "user.email", d.config.Repository.GitConfig.UserEmail}, } for key, value := range d.config.Repository.GitConfig.ConfigValues { gitCmds = append(gitCmds, []string{"git", "config", "--global", key, value}) } for _, cmdArgs := range gitCmds { cmd := &Command{ Executable: cmdArgs[0], Args: cmdArgs[1:], } if _, err := d.ExecuteCommand(ctx, cmd); err != nil { return NewSandboxErrorWithCause(ErrSandboxInitFailed, "failed to configure git", err) } } } return nil } // buildEnvironment builds environment variable array func (d *DockerSandbox) buildEnvironment(additional map[string]string) []string { env := make([]string, 0) // Add default environment for k, v := range d.environment { env = append(env, fmt.Sprintf("%s=%s", k, v)) } // Add additional environment for k, v := range additional { env = append(env, fmt.Sprintf("%s=%s", k, v)) } return env } // copyToContainer copies files from host to container func (d *DockerSandbox) copyToContainer(ctx context.Context, sourcePath, destPath string) error { // Create tar archive buf := new(bytes.Buffer) tw := tar.NewWriter(buf) defer tw.Close() err := filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // Create tar header header, err := tar.FileInfoHeader(info, "") if err != nil { return err } // Update header name to be relative relPath, err := filepath.Rel(sourcePath, path) if err != nil { return err } header.Name = relPath if err := tw.WriteHeader(header); err != nil { return err } // Copy file content if info.Mode().IsRegular() { file, err := os.Open(path) if err != nil { return err } defer file.Close() if _, err := io.Copy(tw, file); err != nil { return err } } return nil }) if err != nil { return NewSandboxErrorWithCause(ErrFileOperationFailed, "failed to create tar archive", err) } // Copy to container return d.client.CopyToContainer(ctx, d.containerID, destPath, buf, container.CopyToContainerOptions{}) } // copyFromContainer copies files from container to host func (d *DockerSandbox) copyFromContainer(ctx context.Context, sourcePath, destPath string) error { reader, _, err := d.client.CopyFromContainer(ctx, d.containerID, sourcePath) if err != nil { return NewSandboxErrorWithCause(ErrFileOperationFailed, "failed to copy from container", err) } defer reader.Close() // Extract tar archive tr := tar.NewReader(reader) for { header, err := tr.Next() if err == io.EOF { break } if err != nil { return NewSandboxErrorWithCause(ErrFileOperationFailed, "failed to read tar header", err) } // Create destination path destFile := filepath.Join(destPath, header.Name) destDir := filepath.Dir(destFile) if err := os.MkdirAll(destDir, 0755); err != nil { return NewSandboxErrorWithCause(ErrFileOperationFailed, "failed to create directory", err) } // Create file file, err := os.Create(destFile) if err != nil { return NewSandboxErrorWithCause(ErrFileOperationFailed, "failed to create file", err) } defer file.Close() // Copy content if _, err := io.Copy(file, tr); err != nil { return NewSandboxErrorWithCause(ErrFileOperationFailed, "failed to copy file content", err) } // Set file mode if err := file.Chmod(os.FileMode(header.Mode)); err != nil { return NewSandboxErrorWithCause(ErrFileOperationFailed, "failed to set file mode", err) } } return nil } // demultiplexOutput separates Docker's multiplexed stdout/stderr func (d *DockerSandbox) demultiplexOutput(reader io.Reader, stdout, stderr io.Writer) error { buf := make([]byte, 8192) for { n, err := reader.Read(buf) if err != nil { if err == io.EOF { return nil } return err } if n < 8 { continue // Not enough data for Docker header } // Docker multiplexing format: [STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4, DATA...] streamType := buf[0] size := int(buf[4])<<24 + int(buf[5])<<16 + int(buf[6])<<8 + int(buf[7]) if n < 8+size { continue // Incomplete frame } data := buf[8 : 8+size] switch streamType { case 1: // stdout stdout.Write(data) case 2: // stderr stderr.Write(data) } // Continue with remaining data if n > 8+size { remaining := buf[8+size : n] reader = io.MultiReader(bytes.NewReader(remaining), reader) } } } // parseLsOutput parses ls command output into FileInfo structs func (d *DockerSandbox) parseLsOutput(output, basePath string) ([]FileInfo, error) { lines := strings.Split(strings.TrimSpace(output), "\n") files := make([]FileInfo, 0, len(lines)) for _, line := range lines { if strings.HasPrefix(line, "total ") || line == "" { continue } fields := strings.Fields(line) if len(fields) < 9 { continue } // Parse file info from ls output permissions := fields[0] owner := fields[2] group := fields[3] sizeStr := fields[4] name := strings.Join(fields[8:], " ") size, _ := strconv.ParseInt(sizeStr, 10, 64) isDir := strings.HasPrefix(permissions, "d") // Convert permissions to mode mode := d.parsePermissions(permissions) files = append(files, FileInfo{ Name: name, Path: filepath.Join(basePath, name), Size: size, Mode: mode, IsDir: isDir, Owner: owner, Group: group, Permissions: permissions, }) } return files, nil } // parsePermissions converts ls permission string to mode func (d *DockerSandbox) parsePermissions(perms string) uint32 { mode := uint32(0) if len(perms) < 10 { return mode } // File type switch perms[0] { case 'd': mode |= 0040000 // Directory case 'l': mode |= 0120000 // Symbolic link case 'p': mode |= 0010000 // Named pipe case 's': mode |= 0140000 // Socket case 'b': mode |= 0060000 // Block device case 'c': mode |= 0020000 // Character device default: mode |= 0100000 // Regular file } // Owner permissions if perms[1] == 'r' { mode |= 0400 } if perms[2] == 'w' { mode |= 0200 } if perms[3] == 'x' { mode |= 0100 } // Group permissions if perms[4] == 'r' { mode |= 0040 } if perms[5] == 'w' { mode |= 0020 } if perms[6] == 'x' { mode |= 0010 } // Other permissions if perms[7] == 'r' { mode |= 0004 } if perms[8] == 'w' { mode |= 0002 } if perms[9] == 'x' { mode |= 0001 } return mode }