-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathguest.py
More file actions
165 lines (159 loc) · 7.24 KB
/
Copy pathguest.py
File metadata and controls
165 lines (159 loc) · 7.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
"""Bounded remote file operations. Inputs arrive as JSON, never as shell fragments."""
import json
import os
import stat
import tempfile
import sys
import subprocess
MAX_BYTES = 1024 * 1024
def read(path):
with os.fdopen(os.open(path, os.O_RDONLY | os.O_NONBLOCK), "rb") as stream:
if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
raise ValueError("Not a regular file")
data = stream.read(MAX_BYTES + 1)
if len(data) > MAX_BYTES:
raise ValueError("File exceeds 1 MiB; use shell for a bounded slice")
return data.decode("utf-8")
def write(path, content):
path = os.path.realpath(path)
parent = os.path.dirname(path)
os.makedirs(parent, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix=".createos-", dir=parent)
try:
if os.path.exists(path):
os.fchmod(fd, stat.S_IMODE(os.stat(path).st_mode))
with os.fdopen(fd, "w", encoding="utf-8") as stream:
stream.write(content)
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
def patch(document):
lines = document.splitlines()
if not lines or lines[0] != "*** Begin Patch" or lines[-1] != "*** End Patch":
raise ValueError("Expected Begin/End Patch envelope")
changes = []
i = 1
while i < len(lines) - 1:
header = lines[i]
i += 1
if header.startswith("*** Add File: "):
path = header[14:]
if os.path.lexists(path):
raise ValueError("Add target already exists: " + path)
body = []
while i < len(lines) - 1 and not lines[i].startswith("*** "):
if not lines[i].startswith("+"):
raise ValueError("Add lines must start with +")
body.append(lines[i][1:])
i += 1
changes.append((path, None, "\n".join(body) + "\n"))
elif header.startswith("*** Delete File: "):
path = header[17:]
read(path)
changes.append((path, path, None))
elif header.startswith("*** Update File: "):
path = header[17:]
target = path
original = read(path)
content = original
if lines[i].startswith("*** Move to: "):
target = lines[i][13:]
if os.path.lexists(target):
raise ValueError("Move target already exists")
i += 1
while i < len(lines) - 1 and not lines[i].startswith("*** "):
if not lines[i].startswith("@@"):
raise ValueError("Expected patch hunk")
i += 1
old, new = [], []
while i < len(lines) - 1 and not lines[i].startswith(("@@", "*** ")):
line = lines[i]
if not line or line[0] not in " +-":
raise ValueError("Invalid hunk line")
if line[0] in " -": old.append(line[1:])
if line[0] in " +": new.append(line[1:])
i += 1
source = content.splitlines()
positions = [position for position in range(len(source) + 1) if old and source[position:position + len(old)] == old]
if len(positions) != 1:
raise ValueError("Patch context must match exactly once: " + path)
position = positions[0]
source[position:position + len(old)] = new
content = "\n".join(source) + ("\n" if content.endswith("\n") else "")
if i < len(lines) and lines[i] == "*** End of File": i += 1
changes.append((target, path if target != path else None, content))
else:
raise ValueError("Unknown patch header: " + header)
paths = [os.path.realpath(item[0]) for item in changes]
if len(paths) != len(set(paths)):
raise ValueError("A patch may target each file only once")
# Validate all hunks before the first mutation; each individual write is atomic.
for target, remove, content in changes:
if content is not None: write(target, content)
if remove is not None: os.unlink(remove)
return {"files": [item[0] for item in changes]}
def main(data):
os.chdir(data["cwd"])
op, args = data["op"], data["args"]
path = args.get("path", args.get("filePath", "."))
if op == "read":
if os.path.isdir(path): return "\n".join(sorted(os.listdir(path))[:2000])
lines = read(path).splitlines()
offset = max(0, int(args.get("offset", 1)) - 1)
limit = min(2000, max(1, int(args.get("limit", 2000))))
return "\n".join(f"{i + 1}: {line}" for i, line in enumerate(lines[offset:offset+limit], offset))
if op == "write":
write(path, args["content"])
return {"written": path}
if op == "edit":
content = read(path)
old, new = args["oldString"], args["newString"]
count = content.count(old)
if not old or count == 0 or (count != 1 and not args.get("replaceAll", False)):
raise ValueError("Edit must match exactly once unless replaceAll is true")
write(path, content.replace(old, new, -1 if args.get("replaceAll") else 1))
return {"edited": path, "matches": count}
if op == "patch": return patch(args.get("patchText", args.get("patch", "")))
if op in ("glob", "grep"):
limit = min(2000, max(1, int(args.get("limit", 100))))
pattern = args["pattern"]
command = ["rg", "--color=never"]
if op == "glob":
command += ["--files", "--glob", pattern]
if args.get("hidden", False): command += ["--hidden"]
command += ["--", path]
else:
command += ["--line-number", "--no-heading", "--max-columns", "2000"]
if args.get("literal"): command += ["--fixed-strings"]
if not args.get("caseSensitive", True): command += ["--ignore-case"]
if args.get("include"): command += ["--glob", args["include"]]
command += ["--", pattern, path]
matches = []
# Drain errors to disk while bounding returned output, avoiding pipe deadlocks.
with tempfile.TemporaryFile() as errors:
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=errors)
limited = False
try:
for line in process.stdout:
matches.append(line.decode("utf-8", errors="replace").rstrip("\n").removeprefix("./"))
if len(matches) >= limit:
limited = True
process.terminate()
break
status = process.wait()
finally:
if process.poll() is None:
process.kill()
process.wait()
process.stdout.close()
if not limited and status not in (0, 1):
errors.seek(0)
raise ValueError(errors.read(8192).decode("utf-8", errors="replace"))
return "\n".join(matches) if matches else "No matches found"
raise ValueError("Unsupported remote operation: " + op)
try:
print(json.dumps({"result": main(json.load(sys.stdin))}))
except Exception as error:
print(json.dumps({"error": str(error)}))
sys.exit(1)