package workspace import ( "container/heap" "crypto/rand" "errors" "fmt" "io" "os" "path" "sort" "time" iop "iop/proto/gen/iop" ) const maxListEntries = 1024 const listBatchSize = 128 type listMaxHeap []string func (h listMaxHeap) Len() int { return len(h) } func (h listMaxHeap) Less(i, j int) bool { return h[i] > h[j] } func (h listMaxHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } func (h *listMaxHeap) Push(value any) { *h = append(*h, value.(string)) } func (h *listMaxHeap) Pop() any { old := *h last := old[len(old)-1] *h = old[:len(old)-1] return last } // Result is intentionally content-free on failure. The Node handler maps it to // the typed wire response without returning filesystem paths or OS errors. type Result struct { Status iop.WorkspaceStatus Code iop.WorkspaceErrorCode Content []byte Entries []string Stdout []byte Stderr []byte ExitCode int32 Truncated bool DurationMS int64 } func (r *Runtime) Read(requestID, relativePath string) (result Result) { startedAt := time.Now() var correlation string defer func() { result.DurationMS = time.Since(startedAt).Milliseconds() r.observeTool(correlation, iop.WorkspaceOperation_WORKSPACE_OPERATION_READ, result) }() return r.withRequest(requestID, func(req *Request) Result { correlation = req.correlation if _, result := r.allows(requestID, iop.WorkspaceOperation_WORKSPACE_OPERATION_READ); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS { return result } name, err := userPath(relativePath) if err != nil { return failureFor(err) } info, err := checkedExisting(req.entry, name, false, false) if err != nil || !info.Mode().IsRegular() { if err == nil { err = errUnsafePath } return failureFor(err) } file, err := req.entry.root.Open(name) if err != nil { return failureFor(err) } defer file.Close() opened, err := file.Stat() if err != nil || !opened.Mode().IsRegular() { return failureFor(errUnsafePath) } if device, _, ok := fileIdentity(opened); !ok || device != req.entry.device { return failureFor(errUnsafePath) } data, err := io.ReadAll(io.LimitReader(file, req.maxRead+1)) if err != nil { return failureFor(err) } if int64(len(data)) > req.maxRead { return Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS, Content: data[:req.maxRead], Truncated: true} } return Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS, Content: data} }) } func (r *Runtime) List(requestID, relativePath string) (result Result) { startedAt := time.Now() var correlation string defer func() { result.DurationMS = time.Since(startedAt).Milliseconds() r.observeTool(correlation, iop.WorkspaceOperation_WORKSPACE_OPERATION_LIST, result) }() return r.withRequest(requestID, func(req *Request) Result { correlation = req.correlation if _, result := r.allows(requestID, iop.WorkspaceOperation_WORKSPACE_OPERATION_LIST); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS { return result } name, err := userPath(relativePath) if err != nil { return failureFor(err) } if _, err := checkedExisting(req.entry, name, true, false); err != nil { return failureFor(err) } file, err := req.entry.root.Open(name) if err != nil { return failureFor(err) } defer file.Close() opened, err := file.Stat() if err != nil || !opened.IsDir() { return failureFor(errUnsafePath) } if device, _, ok := fileIdentity(opened); !ok || device != req.entry.device { return failureFor(errUnsafePath) } retained := make(listMaxHeap, 0, maxListEntries) heap.Init(&retained) truncated := false for { batch, readErr := file.ReadDir(listBatchSize) for _, item := range batch { candidate := item.Name() if name == "." && candidate == ".iop" { continue } if retained.Len() < maxListEntries { heap.Push(&retained, candidate) continue } truncated = true if candidate < retained[0] { retained[0] = candidate heap.Fix(&retained, 0) } } if errors.Is(readErr, io.EOF) { break } if readErr != nil { return failureFor(readErr) } } entries := []string(retained) sort.Strings(entries) result := Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS, Truncated: truncated} var bytes int64 for _, item := range entries { child := item if name != "." { child = path.Join(name, child) } info, err := checkedExisting(req.entry, child, false, false) if err != nil { return failureFor(err) } if !info.IsDir() && !info.Mode().IsRegular() { return failureFor(errUnsafePath) } encoded := item + "\t" + entryType(info) if bytes+int64(len(encoded)) > req.maxOutput { result.Truncated = true break } bytes += int64(len(encoded)) result.Entries = append(result.Entries, encoded) } return result }) } func (r *Runtime) Write(requestID, relativePath string, content []byte) (result Result) { startedAt := time.Now() var correlation string defer func() { result.DurationMS = time.Since(startedAt).Milliseconds() r.observeTool(correlation, iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE, result) }() return r.withRequest(requestID, func(req *Request) Result { correlation = req.correlation if _, result := r.allows(requestID, iop.WorkspaceOperation_WORKSPACE_OPERATION_WRITE); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS { return result } if int64(len(content)) > req.maxWrite { return failureFor(errInvalidPath) } name, err := userPath(relativePath) if err != nil || name == "." { if err == nil { err = errInvalidPath } return failureFor(err) } parent, base, err := openOrCreateParentNoFollow(req.entry, name) if err != nil { return failureFor(err) } defer parent.close() initialTarget, err := parent.targetIdentity(base) if err != nil || initialTarget.exists && (!initialTarget.mode.IsRegular() || initialTarget.device != req.entry.device) { return failureFor(errUnsafePath) } tmpBase, err := randomTempBase() if err != nil { return failureFor(err) } file, err := parent.createTemp(tmpBase) if err != nil { return failureFor(err) } ok := false defer func() { if !ok { _ = parent.remove(tmpBase) } }() if _, err := file.Write(content); err != nil { _ = file.Close() return failureFor(err) } if err := file.Sync(); err != nil { _ = file.Close() return failureFor(err) } if err := file.Close(); err != nil { return failureFor(err) } if req.entry.beforeRename != nil { if err := req.entry.beforeRename(); err != nil { return failureFor(err) } } if err := parent.revalidate(); err != nil { return failureFor(err) } currentTarget, err := parent.targetIdentity(base) if err != nil || currentTarget != initialTarget { return failureFor(errUnsafePath) } if err := parent.rename(tmpBase, base); err != nil { return failureFor(err) } ok = true // The atomic replacement is already committed. Directory sync is best // effort because reporting a post-effect failure would violate the // executor's failure-preserves-target contract. _ = parent.sync() return Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS} }) } func (r *Runtime) Delete(requestID, relativePath string) (result Result) { startedAt := time.Now() var correlation string defer func() { result.DurationMS = time.Since(startedAt).Milliseconds() r.observeTool(correlation, iop.WorkspaceOperation_WORKSPACE_OPERATION_DELETE, result) }() return r.withRequest(requestID, func(req *Request) Result { correlation = req.correlation if _, result := r.allows(requestID, iop.WorkspaceOperation_WORKSPACE_OPERATION_DELETE); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS { return result } name, err := userPath(relativePath) if err != nil || name == "." { if err == nil { err = errInvalidPath } return failureFor(err) } info, err := checkedExisting(req.entry, name, false, true) if err != nil { return failureFor(err) } if info.Mode()&os.ModeSymlink == 0 && !info.Mode().IsRegular() && !info.IsDir() { return failureFor(errUnsafePath) } if err := req.entry.root.Remove(name); err != nil { return failureFor(err) } return Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS} }) } func entryType(info os.FileInfo) string { switch { case info.IsDir(): return "dir" case info.Mode().IsRegular(): return "file" default: return "other" } } func randomTempBase() (string, error) { var token [12]byte if _, err := rand.Read(token[:]); err != nil { return "", err } return fmt.Sprintf(".iop-write-%x", token), nil } func failureFor(err error) Result { if errors.Is(err, errNotFound) || errors.Is(err, os.ErrNotExist) { return Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, Code: iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_FOUND} } if errors.Is(err, ErrClosed) { return Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_UNSUPPORTED, Code: iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_NOT_READY} } if errors.Is(err, ErrInvalidRequest) || errors.Is(err, ErrRequestConflict) || errors.Is(err, ErrUnknownWorkspace) || errors.Is(err, errInvalidPath) || errors.Is(err, errReservedPath) { return Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, Code: iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INVALID_REQUEST} } return Result{Status: iop.WorkspaceStatus_WORKSPACE_STATUS_ERROR, Code: iop.WorkspaceErrorCode_WORKSPACE_ERROR_CODE_INTERNAL} }