返回 DeepSeek-Reasonix
read_snapshot.go
根目录 / internal / tool / builtin / read_snapshot.go
1 package builtin
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "os"
9 "strings"
10
11 "reasonix/internal/fileops"
12 "reasonix/internal/tool"
13 )
14
15 // Full/range reads may capture a bounded source for version-safe paging. A
16 // preview never scans a large file merely to establish a whole-file identity.
17
18 func (r readFile) ResolveReadPath(args json.RawMessage) (string, error) {
19 // Path identity must remain available even when another argument is
20 // invalid, so a failed continuation still belongs to its bounded task.
21 var p struct {
22 Path string `json:"path"`
23 }
24 if err := json.Unmarshal(args, &p); err != nil {
25 return "", err
26 }
27 if strings.TrimSpace(p.Path) == "" {
28 return "", fmt.Errorf("path is required")
29 }
30 return resolveReadablePath(r.workDir, p.Path, r.paths).Path, nil
31 }
32
33 func (r readFile) ExecuteRead(ctx context.Context, args json.RawMessage) (string, tool.ReadResultEnvelope, error) {
34 p, err := parseReadFileParams(args)
35 if err != nil {
36 return "", tool.ReadResultEnvelope{}, err
37 }
38 rp := resolveReadablePath(r.workDir, p.Path, r.paths)
39 if confineRead(r.forbidRoots, rp.Path) {
40 return "", tool.ReadResultEnvelope{}, &os.PathError{Op: "open", Path: rp.DisplayPath, Err: os.ErrNotExist}
41 }
42 source := tool.ReadResultSource{CanonicalPath: rp.Path}
43 r.captured = &source
44 var output string
45 store := fileops.FromContext(ctx)
46 if content, ok := r.overlayText(ctx, rp); ok {
47 source.Kind = tool.ReadSourceOverlay
48 version := fileops.OverlayVersion(content)
49 source.Identity = string(version)
50 // Both output and identity derive from this exact buffer instance.
51 output, err = r.scan(readContextReader{ctx, strings.NewReader(content)}, p.Offset, p.Limit)
52 if err == nil {
53 store.ObservePresent(overlayObservationTarget(r.overlay, rp.Path), version)
54 }
55 } else {
56 source.Kind = tool.ReadSourceDisk
57 f, openErr := os.Open(rp.Path)
58 if openErr != nil {
59 if os.IsNotExist(openErr) {
60 store.ObserveAbsent(fileops.DiskTarget(rp.Path, nil))
61 return "", tool.ReadResultEnvelope{}, &tool.OperationError{Diagnostic: tool.OperationDiagnostic{Code: tool.FSNotFound, Path: rp.DisplayPath, Recovery: "the file is absent; create it only if the task requires a new file"}, Cause: &os.PathError{Op: "read", Path: rp.DisplayPath, Err: os.ErrNotExist}}
62 }
63 return "", tool.ReadResultEnvelope{}, fmt.Errorf("read %s: %s", rp.DisplayPath, rp.ErrorText(openErr))
64 }
65 defer f.Close()
66 before, statErr := f.Stat()
67 if statErr != nil {
68 return "", tool.ReadResultEnvelope{}, fmt.Errorf("stat %s: %s", rp.DisplayPath, rp.ErrorText(statErr))
69 }
70 if before.IsDir() {
71 return "", tool.ReadResultEnvelope{}, fmt.Errorf("%s is a directory, not a file — use the ls tool to list it, or read a specific file inside it", rp.DisplayPath)
72 }
73 target, version := fileops.DiskHandleSnapshot(rp.Path, f, before)
74 source.Identity = string(version)
75 // The window and both metadata samples come from the same handle. Reading a
76 // small window therefore never scans the rest of a large file to mint a
77 // version, while a concurrent replacement cannot authorize the pathname.
78 output, err = r.scanEncoded(readContextReader{ctx, f}, p.Offset, p.Limit)
79 if err == nil {
80 handleAfter, handleErr := f.Stat()
81 pathAfter, pathErr := os.Stat(rp.Path)
82 handleTarget, handleVersion := fileops.DiskHandleSnapshot(rp.Path, f, handleAfter)
83 pathTarget, pathVersion := fileops.DiskSnapshot(rp.Path, pathAfter)
84 if handleErr == nil && pathErr == nil && handleTarget == target && handleVersion == version && pathTarget == target && pathVersion == version {
85 store.ObservePresent(target, version)
86 } else {
87 err = &tool.OperationError{Diagnostic: tool.OperationDiagnostic{Code: tool.FSStaleVersion, Path: rp.DisplayPath, Recovery: "the file changed while it was being read; read it again"}, Cause: ErrFileChanged}
88 }
89 }
90 }
91 source.Snapshot = tool.SourceSnapshot(source.Kind, source.CanonicalPath, source.Identity)
92 env, _ := r.ReadEnvelope(ctx, args, output)
93 return output, env, err
94 }
95
96 type readContextReader struct {
97 ctx context.Context
98 reader io.Reader
99 }
100
101 func (r readContextReader) Read(p []byte) (int, error) {
102 if err := r.ctx.Err(); err != nil {
103 return 0, err
104 }
105 return r.reader.Read(p)
106 }
107
107 lines GO