Merge nucleic/zesty-ancient-koala-i9if into dev
This commit is contained in:
+34
-2
@@ -398,6 +398,30 @@ fn read_back(record: &brush_core::gate::RedirectRecord) -> Option<(u64, Vec<u8>,
|
|||||||
Some((total, buf, total > n as u64))
|
Some((total, buf, total > n as u64))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve a redirection target against the directory its command ran in, so the
|
||||||
|
/// reported path is meaningful to a host that never saw that cwd. Purely lexical
|
||||||
|
/// (`.`/`..` collapsed, no symlink resolution, no filesystem access): the target of a
|
||||||
|
/// truncating write may not exist yet, and observation must never stat its way into a
|
||||||
|
/// slow or hanging path.
|
||||||
|
fn absolute_path(path: &Path, cwd: &Path) -> PathBuf {
|
||||||
|
let joined = if path.is_absolute() {
|
||||||
|
path.to_path_buf()
|
||||||
|
} else {
|
||||||
|
cwd.join(path)
|
||||||
|
};
|
||||||
|
let mut out = PathBuf::new();
|
||||||
|
for component in joined.components() {
|
||||||
|
match component {
|
||||||
|
std::path::Component::CurDir => {}
|
||||||
|
std::path::Component::ParentDir => {
|
||||||
|
out.pop();
|
||||||
|
}
|
||||||
|
other => out.push(other.as_os_str()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
/// Build a preview string (redacted, base64) from captured bytes. Binary data is
|
/// Build a preview string (redacted, base64) from captured bytes. Binary data is
|
||||||
/// previewed as a hex head rather than mangled UTF-8.
|
/// previewed as a hex head rather than mangled UTF-8.
|
||||||
fn preview_b64(data: &[u8]) -> String {
|
fn preview_b64(data: &[u8]) -> String {
|
||||||
@@ -502,6 +526,14 @@ impl brush_core::gate::Gate for RecordingGate {
|
|||||||
|
|
||||||
// Data-flow read-back for this command's redirects (docs/NASH.md §5.2).
|
// Data-flow read-back for this command's redirects (docs/NASH.md §5.2).
|
||||||
for record in &pending.redirects {
|
for record in &pending.redirects {
|
||||||
|
// The host resolves a redirect target against the session's worktree to decide
|
||||||
|
// which file was edited (and which lock that is), so report the path absolutely:
|
||||||
|
// `> out.txt` is only meaningful next to the directory the command ran in, and the
|
||||||
|
// redirect event — unlike `exec` — carries no cwd of its own.
|
||||||
|
let target = record
|
||||||
|
.path
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| absolute_path(p, &pending.cwd).to_string_lossy().into_owned());
|
||||||
let Some((total, data, truncated)) = read_back(record) else {
|
let Some((total, data, truncated)) = read_back(record) else {
|
||||||
// Special file / unreadable — emit metadata only.
|
// Special file / unreadable — emit metadata only.
|
||||||
obs.send(Event::Redirect {
|
obs.send(Event::Redirect {
|
||||||
@@ -510,7 +542,7 @@ impl brush_core::gate::Gate for RecordingGate {
|
|||||||
cmd_seq: exec_seq,
|
cmd_seq: exec_seq,
|
||||||
op: record.op.clone(),
|
op: record.op.clone(),
|
||||||
fd: record.fd,
|
fd: record.fd,
|
||||||
target: record.path.as_ref().map(|p| p.to_string_lossy().into_owned()),
|
target,
|
||||||
bytes: 0,
|
bytes: 0,
|
||||||
truncated: false,
|
truncated: false,
|
||||||
hash: String::new(),
|
hash: String::new(),
|
||||||
@@ -524,7 +556,7 @@ impl brush_core::gate::Gate for RecordingGate {
|
|||||||
cmd_seq: exec_seq,
|
cmd_seq: exec_seq,
|
||||||
op: record.op.clone(),
|
op: record.op.clone(),
|
||||||
fd: record.fd,
|
fd: record.fd,
|
||||||
target: record.path.as_ref().map(|p| p.to_string_lossy().into_owned()),
|
target,
|
||||||
bytes: total,
|
bytes: total,
|
||||||
truncated,
|
truncated,
|
||||||
hash: fnv1a_hex(&data),
|
hash: fnv1a_hex(&data),
|
||||||
|
|||||||
@@ -384,6 +384,12 @@ fn redirect_readback_captures_each_operator() {
|
|||||||
let trunc = redirs.iter().find(|e| e["op"] == ">").expect("truncate redirect");
|
let trunc = redirs.iter().find(|e| e["op"] == ">").expect("truncate redirect");
|
||||||
assert_eq!(decode_preview(trunc), "first\n");
|
assert_eq!(decode_preview(trunc), "first\n");
|
||||||
assert_eq!(trunc["bytes"], 6);
|
assert_eq!(trunc["bytes"], 6);
|
||||||
|
// The target is reported ABSOLUTE (docs/NASH.md §5.2). A redirect event carries no cwd of its
|
||||||
|
// own, and the host maps the path to a worktree file — and therefore to a file lock — so a bare
|
||||||
|
// `d.txt` would be unresolvable there. `run_nash` runs in a fresh temp workspace.
|
||||||
|
let target = trunc["target"].as_str().expect("redirect target");
|
||||||
|
assert!(target.starts_with('/'), "target not absolute: {target}");
|
||||||
|
assert!(target.ends_with("/d.txt"), "target lost its file name: {target}");
|
||||||
|
|
||||||
let append = redirs.iter().find(|e| e["op"] == ">>").expect("append redirect");
|
let append = redirs.iter().find(|e| e["op"] == ">>").expect("append redirect");
|
||||||
// Append captures ONLY the added bytes, not the whole file.
|
// Append captures ONLY the added bytes, not the whole file.
|
||||||
|
|||||||
Reference in New Issue
Block a user