Skip to content

Commit 0d07f04

Browse files
committed
fix(core): error handl, RAW_CAP, recursion guard, atomic writes
1 parent 41ebf0b commit 0d07f04

4 files changed

Lines changed: 45 additions & 41 deletions

File tree

src/core/stream.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ pub fn run_streaming(
152152
match stdout_mode {
153153
FilterMode::Passthrough => {
154154
for line in BufReader::new(stdout).lines().map_while(Result::ok) {
155-
if raw_stdout.len() < RAW_CAP {
155+
if raw_stdout.len() + line.len() + 1 <= RAW_CAP {
156156
raw_stdout.push_str(&line);
157157
raw_stdout.push('\n');
158158
}
@@ -166,7 +166,7 @@ pub fn run_streaming(
166166
}
167167
FilterMode::Streaming(mut filter) => {
168168
for line in BufReader::new(stdout).lines().map_while(Result::ok) {
169-
if raw_stdout.len() < RAW_CAP {
169+
if raw_stdout.len() + line.len() + 1 <= RAW_CAP {
170170
raw_stdout.push_str(&line);
171171
raw_stdout.push('\n');
172172
}
@@ -189,22 +189,21 @@ pub fn run_streaming(
189189
}
190190
FilterMode::Buffered(filter_fn) => {
191191
for line in BufReader::new(stdout).lines().map_while(Result::ok) {
192-
if raw_stdout.len() < RAW_CAP {
192+
if raw_stdout.len() + line.len() + 1 <= RAW_CAP {
193193
raw_stdout.push_str(&line);
194194
raw_stdout.push('\n');
195195
}
196196
}
197-
let result = filter_fn(&raw_stdout);
198-
filtered = result.clone();
199-
match write!(out, "{}", result) {
197+
filtered = filter_fn(&raw_stdout);
198+
match write!(out, "{}", filtered) {
200199
Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {}
201200
Err(e) => return Err(e.into()),
202201
Ok(_) => {}
203202
}
204203
}
205204
FilterMode::CaptureOnly => {
206205
for line in BufReader::new(stdout).lines().map_while(Result::ok) {
207-
if raw_stdout.len() < RAW_CAP {
206+
if raw_stdout.len() + line.len() + 1 <= RAW_CAP {
208207
raw_stdout.push_str(&line);
209208
raw_stdout.push('\n');
210209
}
@@ -214,7 +213,10 @@ pub fn run_streaming(
214213
}
215214
}
216215

217-
let raw_stderr = stderr_thread.join().unwrap_or_else(|_| String::new());
216+
let raw_stderr = stderr_thread.join().unwrap_or_else(|e| {
217+
eprintln!("[rtk] warning: stderr reader thread panicked: {:?}", e);
218+
String::new()
219+
});
218220
if let Some(t) = stdin_thread {
219221
t.join().ok();
220222
}

src/discover/registry.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -485,24 +485,28 @@ fn rewrite_line_range(cmd: &str) -> Option<String> {
485485
/// but don't change which command runs. Strip before routing, re-prepend after.
486486
const SHELL_PREFIX_BUILTINS: &[&str] = &["noglob", "command", "builtin", "exec", "nocorrect"];
487487

488-
/// Rewrite a single (non-compound) command segment.
489-
/// Returns `Some(rewritten)` if matched (including already-RTK pass-through).
490-
/// Returns `None` if no match (caller uses original segment).
488+
const MAX_PREFIX_DEPTH: usize = 10;
489+
491490
fn rewrite_segment(seg: &str, excluded: &[String]) -> Option<String> {
491+
rewrite_segment_inner(seg, excluded, 0)
492+
}
493+
494+
fn rewrite_segment_inner(seg: &str, excluded: &[String], depth: usize) -> Option<String> {
492495
let trimmed = seg.trim();
493496
if trimmed.is_empty() {
494497
return None;
495498
}
496499

497-
// Peel shell prefix builtins (noglob, command, builtin, exec, nocorrect)
498-
// before routing, re-prepend after.
500+
if depth >= MAX_PREFIX_DEPTH {
501+
return None;
502+
}
503+
499504
for &prefix in SHELL_PREFIX_BUILTINS {
500505
if let Some(rest) = strip_word_prefix(trimmed, prefix) {
501506
if rest.is_empty() {
502-
return None; // bare "noglob" etc. — nothing to rewrite
507+
return None;
503508
}
504-
// Recursively rewrite the inner command
505-
return match rewrite_segment(rest, excluded) {
509+
return match rewrite_segment_inner(rest, excluded, depth + 1) {
506510
Some(rewritten) => Some(format!("{} {}", prefix, rewritten)),
507511
None => None,
508512
};

src/hooks/init.rs

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -302,15 +302,15 @@ fn write_if_changed(path: &Path, content: &str, name: &str, verbose: u8) -> Resu
302302
}
303303
Ok(false)
304304
} else {
305-
fs::write(path, content)
305+
atomic_write(path, content)
306306
.with_context(|| format!("Failed to write {}: {}", name, path.display()))?;
307307
if verbose > 0 {
308308
eprintln!("Updated {}: {}", name, path.display());
309309
}
310310
Ok(true)
311311
}
312312
} else {
313-
fs::write(path, content)
313+
atomic_write(path, content)
314314
.with_context(|| format!("Failed to write {}: {}", name, path.display()))?;
315315
if verbose > 0 {
316316
eprintln!("Created {}: {}", name, path.display());
@@ -682,8 +682,7 @@ fn patch_settings_json_command(
682682
}
683683
}
684684

685-
// Deep-merge hook
686-
insert_hook_entry(&mut root, hook_command);
685+
insert_hook_entry(&mut root, hook_command)?;
687686

688687
// Backup original
689688
if settings_path.exists() {
@@ -748,38 +747,35 @@ fn clean_double_blanks(content: &str) -> String {
748747

749748
/// Deep-merge RTK hook entry into settings.json
750749
/// Creates hooks.PreToolUse structure if missing, preserves existing hooks
751-
fn insert_hook_entry(root: &mut serde_json::Value, hook_command: &str) {
752-
// Ensure root is an object
750+
fn insert_hook_entry(root: &mut serde_json::Value, hook_command: &str) -> Result<()> {
753751
let root_obj = match root.as_object_mut() {
754752
Some(obj) => obj,
755753
None => {
756754
*root = serde_json::json!({});
757-
root.as_object_mut()
758-
.expect("Just created object, must succeed")
755+
root.as_object_mut().expect("just-created json object")
759756
}
760757
};
761758

762-
// Use entry() API for idiomatic insertion
763759
let hooks = root_obj
764760
.entry("hooks")
765761
.or_insert_with(|| serde_json::json!({}))
766762
.as_object_mut()
767-
.expect("hooks must be an object");
763+
.context("hooks value is not an object")?;
768764

769765
let pre_tool_use = hooks
770766
.entry(PRE_TOOL_USE_KEY)
771767
.or_insert_with(|| serde_json::json!([]))
772768
.as_array_mut()
773-
.expect("PreToolUse must be an array");
769+
.context("PreToolUse value is not an array")?;
774770

775-
// Append RTK hook entry
776771
pre_tool_use.push(serde_json::json!({
777772
"matcher": "Bash",
778773
"hooks": [{
779774
"type": "command",
780775
"command": hook_command
781776
}]
782777
}));
778+
Ok(())
783779
}
784780

785781
/// Check if RTK hook is already present in settings.json
@@ -1622,8 +1618,7 @@ fn patch_cursor_hooks_json(path: &Path, verbose: u8) -> Result<bool> {
16221618
return Ok(false);
16231619
}
16241620

1625-
// Insert the RTK preToolUse entry
1626-
insert_cursor_hook_entry(&mut root);
1621+
insert_cursor_hook_entry(&mut root)?;
16271622

16281623
// Backup if exists
16291624
if path.exists() {
@@ -1664,35 +1659,34 @@ fn cursor_hook_already_present(root: &serde_json::Value) -> bool {
16641659
}
16651660

16661661
/// Insert RTK preToolUse entry into Cursor hooks.json
1667-
fn insert_cursor_hook_entry(root: &mut serde_json::Value) {
1662+
fn insert_cursor_hook_entry(root: &mut serde_json::Value) -> Result<()> {
16681663
let root_obj = match root.as_object_mut() {
16691664
Some(obj) => obj,
16701665
None => {
16711666
*root = serde_json::json!({ "version": 1 });
1672-
root.as_object_mut()
1673-
.expect("Just created object, must succeed")
1667+
root.as_object_mut().expect("just-created json object")
16741668
}
16751669
};
16761670

1677-
// Ensure version key
16781671
root_obj.entry("version").or_insert(serde_json::json!(1));
16791672

16801673
let hooks = root_obj
16811674
.entry("hooks")
16821675
.or_insert_with(|| serde_json::json!({}))
16831676
.as_object_mut()
1684-
.expect("hooks must be an object");
1677+
.context("hooks value is not an object")?;
16851678

16861679
let pre_tool_use = hooks
16871680
.entry("preToolUse")
16881681
.or_insert_with(|| serde_json::json!([]))
16891682
.as_array_mut()
1690-
.expect("preToolUse must be an array");
1683+
.context("preToolUse value is not an array")?;
16911684

16921685
pre_tool_use.push(serde_json::json!({
16931686
"command": CURSOR_HOOK_COMMAND,
16941687
"matcher": "Shell"
16951688
}));
1689+
Ok(())
16961690
}
16971691

16981692
/// Remove Cursor RTK artifacts: hook script + hooks.json entry
@@ -2745,7 +2739,7 @@ More notes
27452739
let mut json_content = serde_json::json!({});
27462740
let hook_command = "/Users/test/.claude/hooks/rtk-rewrite.sh";
27472741

2748-
insert_hook_entry(&mut json_content, hook_command);
2742+
insert_hook_entry(&mut json_content, hook_command).unwrap();
27492743

27502744
// Should create full structure
27512745
assert!(json_content.get("hooks").is_some());
@@ -2777,7 +2771,7 @@ More notes
27772771
});
27782772

27792773
let hook_command = "/Users/test/.claude/hooks/rtk-rewrite.sh";
2780-
insert_hook_entry(&mut json_content, hook_command);
2774+
insert_hook_entry(&mut json_content, hook_command).unwrap();
27812775

27822776
let pre_tool_use = json_content["hooks"]["PreToolUse"].as_array().unwrap();
27832777
assert_eq!(pre_tool_use.len(), 2); // Should have both hooks
@@ -2800,7 +2794,7 @@ More notes
28002794
});
28012795

28022796
let hook_command = "/Users/test/.claude/hooks/rtk-rewrite.sh";
2803-
insert_hook_entry(&mut json_content, hook_command);
2797+
insert_hook_entry(&mut json_content, hook_command).unwrap();
28042798

28052799
// Should preserve all other keys
28062800
assert_eq!(json_content["env"]["PATH"], "/custom/path");
@@ -3003,7 +2997,7 @@ More notes
30032997
#[test]
30042998
fn test_insert_cursor_hook_entry_empty() {
30052999
let mut json_content = serde_json::json!({ "version": 1 });
3006-
insert_cursor_hook_entry(&mut json_content);
3000+
insert_cursor_hook_entry(&mut json_content).unwrap();
30073001

30083002
let hooks = json_content["hooks"]["preToolUse"].as_array().unwrap();
30093003
assert_eq!(hooks.len(), 1);
@@ -3027,7 +3021,7 @@ More notes
30273021
}
30283022
});
30293023

3030-
insert_cursor_hook_entry(&mut json_content);
3024+
insert_cursor_hook_entry(&mut json_content).unwrap();
30313025

30323026
let pre_tool_use = json_content["hooks"]["preToolUse"].as_array().unwrap();
30333027
assert_eq!(pre_tool_use.len(), 2);

src/hooks/permissions.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ fn load_permission_rules() -> (Vec<String>, Vec<String>, Vec<String>) {
9898
continue;
9999
};
100100
let Ok(json) = serde_json::from_str::<Value>(&content) else {
101+
eprintln!(
102+
"[rtk] warning: failed to parse permissions from {}",
103+
path.display()
104+
);
101105
continue;
102106
};
103107
let Some(permissions) = json.get("permissions") else {

0 commit comments

Comments
 (0)