Skip to main content

submilli_engine/stdlib/code/
mod.rs

1//! Contained workspace navigation and anchor-based editing, gated by existing fs grants.
2mod budget;
3pub mod declaration;
4mod install;
5mod myers;
6mod patch;
7#[cfg(test)]
8mod tests;
9mod text;
10mod update;
11mod walk;
12
13use crate::runtime::StoreData;
14use crate::runtime::filesystem::PreparedFileUpdate;
15use crate::runtime::fuel;
16use crate::stdlib::shared::{check_security, check_security_call, require_writable};
17use budget::{Budget, OutputBudget};
18use serde_json::{Value, json};
19
20use wasmtime::{Caller, Result, bail};
21
22pub const MODULE_NAME: &str = "submilli:code";
23pub use declaration::package_declaration;
24pub use install::install;
25use walk::{glob, search, tree};
26
27async fn read(
28    caller: &mut Caller<'_, StoreData>,
29    budget: &mut Budget,
30    path: &str,
31    start: usize,
32    limit: usize,
33) -> Result<Value> {
34    let source = read_file(caller, budget, path, "read").await?;
35    budget.charge(
36        caller,
37        source
38            .bytes()
39            .filter(|&b| b == b'\n')
40            .count()
41            .saturating_add(1)
42            .saturating_mul(32),
43    )?;
44    let lines = text::lines(source.strip_prefix('\u{feff}').unwrap_or(&source));
45    let mut output = OutputBudget::new();
46    let mut values = Vec::new();
47    for (index, line) in lines.iter().enumerate().skip(start).take(limit) {
48        if !output.reserve(caller, budget, OutputBudget::line_bytes(line))? {
49            break;
50        }
51        values.push(json!({"line":index+1,"text":text::line_text(line)}));
52    }
53    Ok(
54        json!({"path":path,"truncated":start.saturating_add(values.len()) < lines.len(),"lines":values}),
55    )
56}
57
58async fn edit(
59    caller: &mut Caller<'_, StoreData>,
60    budget: &mut Budget,
61    path: &str,
62    old: &str,
63    new: &str,
64    replace_all: bool,
65    near_line: usize,
66) -> Result<PreparedFileUpdate> {
67    let original = read_file(caller, budget, path, "edit").await?;
68    budget.charge(caller, original.len().saturating_mul(64))?;
69    fuel::charge(&mut *caller, fuel::SCAN, original.len() as u64)?;
70    let change = prepare_edit(caller, budget, &original, old, new, replace_all, near_line)?;
71    prepare_change(caller, budget, path, "edit", &original, change).await
72}
73
74async fn insert_at(
75    caller: &mut Caller<'_, StoreData>,
76    budget: &mut Budget,
77    path: &str,
78    line: usize,
79    text: &str,
80) -> Result<PreparedFileUpdate> {
81    let original = read_file(caller, budget, path, "insertAt").await?;
82    budget.charge(caller, original.len().saturating_mul(64))?;
83    fuel::charge(&mut *caller, fuel::SCAN, original.len() as u64)?;
84    let change = text::insert(&original, line, text)?;
85    prepare_change(caller, budget, path, "insertAt", &original, change).await
86}
87
88async fn diff_files(
89    caller: &mut Caller<'_, StoreData>,
90    budget: &mut Budget,
91    first: &str,
92    second: &str,
93) -> Result<Vec<u16>> {
94    let first = read_file(caller, budget, first, "diffFiles").await?;
95    let second = read_file(caller, budget, second, "diffFiles").await?;
96    budget.charge(caller, (first.len() + second.len()).saturating_mul(2))?;
97    diff_text(
98        caller,
99        budget,
100        &first.encode_utf16().collect::<Vec<_>>(),
101        &second.encode_utf16().collect::<Vec<_>>(),
102    )
103}
104
105async fn apply_patch(
106    caller: &mut Caller<'_, StoreData>,
107    budget: &mut Budget,
108    path: &str,
109    patch: &str,
110) -> Result<PreparedFileUpdate> {
111    let original = read_file(caller, budget, path, "applyPatch").await?;
112    budget.charge(caller, original.len().saturating_mul(64))?;
113    fuel::charge(&mut *caller, fuel::SCAN, original.len() as u64)?;
114    budget.charge(caller, patch.len().saturating_mul(32))?;
115    fuel::charge(
116        &mut *caller,
117        fuel::SCAN,
118        (original.len() as u64).saturating_mul(patch.lines().count().max(1) as u64),
119    )?;
120    fuel::charge(&mut *caller, fuel::PARSE, patch.len() as u64)?;
121    let change = patch::apply(&original, patch)?;
122    prepare_change(caller, budget, path, "applyPatch", &original, change).await
123}
124
125async fn prepare_change(
126    caller: &mut Caller<'_, StoreData>,
127    budget: &mut Budget,
128    path: &str,
129    _op: &str,
130    original: &str,
131    change: text::Edit,
132) -> Result<PreparedFileUpdate> {
133    budget.check_size(change.text.len())?;
134    let success = change.diagnostics.is_empty();
135    let changed = success && change.text != original;
136    let patch = if changed {
137        String::from_utf16(&diff_text(
138            caller,
139            budget,
140            &original.encode_utf16().collect::<Vec<_>>(),
141            &change.text.encode_utf16().collect::<Vec<_>>(),
142        )?)?
143    } else {
144        String::new()
145    };
146    let ticket = check_security_call(
147        &mut *caller,
148        "fs.write",
149        json!({"path":path, "length": change.text.len(), "diff": patch}),
150    )?;
151    // Build the native result before commit; history captures it before guest conversion.
152    let result =
153        json!({"success":success,"changed":changed,"diff":patch,"diagnostics":change.diagnostics});
154    let output = update::encode(caller, budget, &result)?;
155    if changed {
156        let placement = caller
157            .data()
158            .filesystem
159            .placement(path)
160            .await
161            .map_err(|e| backend_error(_op, path, e))?;
162        require_writable(&*caller, ticket, &placement, "fs.write", path)?;
163    }
164    Ok(PreparedFileUpdate {
165        contents: changed.then_some(change.text.into_bytes()),
166        output,
167    })
168}
169fn prepare_edit(
170    caller: &mut Caller<'_, StoreData>,
171    budget: &mut Budget,
172    original: &str,
173    old: &str,
174    new: &str,
175    replace_all: bool,
176    near_line: usize,
177) -> Result<text::Edit> {
178    // Naive substring search: every position against the whole needle.
179    fuel::charge(
180        &mut *caller,
181        fuel::SCAN,
182        (original.len() as u64).saturating_mul(old.len().max(1) as u64),
183    )?;
184    if old.is_empty() {
185        bail!("code.edit: oldString must not be empty; use insertAt");
186    }
187    let matches = original.match_indices(old).count();
188    let selected = if replace_all {
189        matches
190    } else {
191        usize::from(matches > 0)
192    };
193    budget.charge(
194        caller,
195        text::MAX_DIAGNOSTICS
196            .min(original.len().saturating_add(1))
197            .saturating_mul(1024),
198    )?;
199    let length = original
200        .len()
201        .saturating_sub(selected.saturating_mul(old.len()))
202        .saturating_add(selected.saturating_mul(new.len()));
203    budget.check_size(length)?;
204    budget.charge(caller, length.saturating_mul(2))?;
205    let maximum = if matches == 0 {
206        budget.max_bytes.min(
207            original
208                .len()
209                .saturating_mul(16)
210                .saturating_add(new.len().saturating_mul(4)),
211        )
212    } else {
213        budget.max_bytes
214    };
215    text::replace(original, old, new, replace_all, near_line, maximum)
216}
217
218fn normalize(cwd: &str, path: &str) -> Result<String> {
219    crate::runtime::fs::guest_normalize(cwd, path)
220        .map_err(|e| wasmtime::Error::msg(format!("code: {e}")))
221}
222fn gate(caller: &mut Caller<'_, StoreData>, capability: &str, path: &str) -> Result<()> {
223    check_security(caller, capability, json!({"path":path,"recursive":true}))
224}
225fn gate_call(
226    caller: &mut Caller<'_, StoreData>,
227    capability: &str,
228    path: &str,
229) -> Result<Option<crate::runtime::CallTicket>> {
230    check_security_call(caller, capability, json!({"path":path,"recursive":true}))
231}
232async fn read_file(
233    caller: &mut Caller<'_, StoreData>,
234    budget: &mut Budget,
235    path: &str,
236    op: &str,
237) -> Result<String> {
238    read_contents(caller, budget, path, op, false).await
239}
240async fn read_contents(
241    caller: &mut Caller<'_, StoreData>,
242    budget: &mut Budget,
243    path: &str,
244    op: &str,
245    skip_binary: bool,
246) -> Result<String> {
247    let ticket = gate_call(caller, "fs.read", path)?;
248    let filesystem = caller.data().filesystem.clone();
249    let work = begin_work(caller)?;
250    let result = filesystem
251        .read_regular(
252            Some(work.clone()),
253            path,
254            budget.max_bytes as u64,
255            &caller.data().tenant_limits,
256        )
257        .await;
258    work.settle(&mut *caller)?;
259    let contents = result.map_err(|e| backend_error(op, path, e))?;
260    let bytes = contents.bytes;
261    budget.charge(caller, bytes.len().saturating_mul(8).saturating_add(1024))?;
262    fuel::charge(&mut *caller, fuel::SCAN, bytes.len() as u64)?;
263    crate::runtime::call_log::record_payload(
264        &*caller,
265        ticket,
266        crate::runtime::call_log::Side::Response,
267        || crate::runtime::call_log::Payload::meta(Value::Null).with_body(&bytes),
268    );
269    if skip_binary && bytes.contains(&0) {
270        return Ok(String::new());
271    }
272    String::from_utf8(bytes)
273        .map_err(|_| wasmtime::Error::msg(format!("code.{op}: {path} is not valid UTF-8")))
274}
275fn diff_text(
276    caller: &mut Caller<'_, StoreData>,
277    budget: &mut Budget,
278    a: &[u16],
279    b: &[u16],
280) -> Result<Vec<u16>> {
281    budget.charge(caller, (a.len() + b.len()).saturating_mul(16))?;
282    let na = text::line_count(a);
283    let nb = text::line_count(b);
284    budget.charge(caller, (na + nb).saturating_mul(1024))?;
285    text::check_diff_size(na, nb)?;
286    fuel::charge(&mut *caller, fuel::SCAN, (a.len() + b.len()) as u64)?;
287    let result = text::diff_charged(a, b, |steps| fuel::charge(&mut *caller, fuel::PARSE, steps))?;
288    budget.check_size(result.len().saturating_mul(2))?;
289    Ok(result)
290}
291
292fn backend_error(
293    op: &str,
294    path: &str,
295    error: crate::runtime::filesystem::FilesystemError,
296) -> wasmtime::Error {
297    crate::stdlib::fs::backend_error(&format!("code.{op}"), path, error)
298}
299fn begin_work(caller: &mut Caller<'_, StoreData>) -> Result<std::sync::Arc<fuel::WorkBudget>> {
300    Ok(std::sync::Arc::new(fuel::WorkBudget::new(
301        fuel::remaining_fuel(caller)?,
302    )))
303}
304async fn metadata(
305    caller: &mut Caller<'_, StoreData>,
306    path: &str,
307) -> std::result::Result<cap_std::fs::Metadata, crate::runtime::filesystem::FilesystemError> {
308    caller
309        .data()
310        .filesystem
311        .metadata(&"code-observation".into(), None, path, false)
312        .await
313}