Skip to content

Commit f24a454

Browse files
committed
fix(tools): order rss scan budget before alias policy
1 parent 7cd8a31 commit f24a454

2 files changed

Lines changed: 109 additions & 0 deletions

File tree

‎rss/tools/search_files.rss‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,7 +754,19 @@ fn walk_search(state: map, path: string, depth: int) -> map {
754754
if map_bool(listed, "ok", false) == false {
755755
let error: map = types::map_map(listed, "error");
756756
let code: string = types::map_string(error, "code", "internal_error");
757+
let mut treat_truncated: bool = false;
757758
if code == "budget_exceeded" {
759+
treat_truncated = true;
760+
} else {
761+
if remaining <= 2 {
762+
if code == "path_denied" {
763+
if types::map_string(error, "message", "") == "regular files with multiple hard links are not permitted" {
764+
treat_truncated = true;
765+
}
766+
}
767+
}
768+
}
769+
if treat_truncated {
758770
state.truncated = true;
759771
state.stop = true;
760772
} else {

‎tests/rss_file_tool_tests.rs‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2159,6 +2159,103 @@ fn search_hardlink_in_child_discards_parent_matches_like_native() {
21592159
);
21602160
}
21612161

2162+
#[cfg(unix)]
2163+
#[test]
2164+
fn search_remaining_2_hardlink_only_truncates_like_native() {
2165+
let fixture = Fixture::new("search-rem2-hardlink");
2166+
let outside = fixture.parent.join("outside-shared-rem2");
2167+
fs::write(&outside, "needle shared\n").unwrap();
2168+
fs::hard_link(&outside, fixture.root.join("linked")).unwrap();
2169+
let mut config = fixture.config();
2170+
config.max_search_files = 2;
2171+
config.artifact_store.root = fixture.parent.join("artifacts-rem2-hardlink");
2172+
let arguments = json!({"pattern": "needle"});
2173+
let native = native_execute(
2174+
&fixture.tools_with_config(config.clone()),
2175+
NativeToolExecutor::SearchFiles,
2176+
&arguments,
2177+
);
2178+
let rss = run_rss_search(&fixture, &config, arguments);
2179+
assert_exact_envelope(&native, &rss.result);
2180+
assert!(native.ok, "native={native:?}");
2181+
assert!(native.truncated, "native must truncate: {native:?}");
2182+
assert_eq!(native.content, "");
2183+
assert_eq!(native.data["match_count"], json!(0));
2184+
assert_eq!(native.data["files_visited"], json!(0));
2185+
assert_eq!(native.data["dirs_visited"], json!(1));
2186+
assert_eq!(rss.result["ok"], json!(true), "rss={}", rss.result);
2187+
assert_eq!(rss.result["truncated"], json!(true), "rss={}", rss.result);
2188+
assert_eq!(rss.result["content"], json!(""));
2189+
assert_eq!(rss.result["data"]["match_count"], json!(0));
2190+
assert_eq!(rss.result["data"]["files_visited"], json!(0));
2191+
assert_eq!(rss.result["data"]["dirs_visited"], json!(1));
2192+
}
2193+
2194+
#[cfg(unix)]
2195+
#[test]
2196+
fn search_nested_remaining_2_hardlink_sibling_stops_later_siblings_like_native() {
2197+
let fixture = Fixture::new("search-nested-rem2-hardlink");
2198+
fs::create_dir_all(fixture.root.join("adir")).unwrap();
2199+
fs::create_dir_all(fixture.root.join("bdir")).unwrap();
2200+
fs::create_dir_all(fixture.root.join("zdir")).unwrap();
2201+
fs::write(fixture.root.join("adir/f0.txt"), "needle\n").unwrap();
2202+
fs::write(fixture.root.join("adir/f1.txt"), "needle\n").unwrap();
2203+
fs::write(fixture.root.join("adir/f2.txt"), "needle\n").unwrap();
2204+
let outside = fixture.parent.join("outside-shared-nested");
2205+
fs::write(&outside, "secret needle\n").unwrap();
2206+
fs::hard_link(&outside, fixture.root.join("bdir/linked")).unwrap();
2207+
fs::write(fixture.root.join("zdir/late.txt"), "needle late\n").unwrap();
2208+
let mut config = fixture.config();
2209+
config.max_search_files = 5;
2210+
config.artifact_store.root = fixture.parent.join("artifacts-nested-rem2-hardlink");
2211+
let arguments = json!({"pattern": "needle"});
2212+
let native = native_execute(
2213+
&fixture.tools_with_config(config.clone()),
2214+
NativeToolExecutor::SearchFiles,
2215+
&arguments,
2216+
);
2217+
let rss = run_rss_search(&fixture, &config, arguments);
2218+
assert_exact_envelope(&native, &rss.result);
2219+
assert!(native.ok, "native={native:?}");
2220+
assert!(native.truncated, "native must truncate: {native:?}");
2221+
assert!(
2222+
native.content.contains("adir/f0.txt")
2223+
&& native.content.contains("adir/f1.txt")
2224+
&& native.content.contains("adir/f2.txt"),
2225+
"prior matches must remain: native={native:?}"
2226+
);
2227+
assert!(
2228+
!native.content.contains("late.txt"),
2229+
"later sibling must not be traversed after remaining<=2 hardlink: native={native:?}"
2230+
);
2231+
assert!(
2232+
!native.content.contains("linked"),
2233+
"hardlink must not be searched after remaining<=2: native={native:?}"
2234+
);
2235+
assert_eq!(native.data["files_visited"], json!(3));
2236+
assert_eq!(native.data["dirs_visited"], json!(3));
2237+
assert_eq!(rss.result["ok"], json!(true), "rss={}", rss.result);
2238+
assert_eq!(rss.result["truncated"], json!(true), "rss={}", rss.result);
2239+
assert_eq!(rss.result["data"]["files_visited"], json!(3));
2240+
assert_eq!(rss.result["data"]["dirs_visited"], json!(3));
2241+
assert!(
2242+
rss.result["content"]
2243+
.as_str()
2244+
.is_some_and(|content| content.contains("adir/f0.txt")
2245+
&& content.contains("adir/f1.txt")
2246+
&& content.contains("adir/f2.txt")),
2247+
"prior matches must remain: rss={}",
2248+
rss.result
2249+
);
2250+
assert!(
2251+
!rss.result["content"]
2252+
.as_str()
2253+
.is_some_and(|content| content.contains("late.txt") || content.contains("linked")),
2254+
"later sibling and hardlink must not leak: rss={}",
2255+
rss.result
2256+
);
2257+
}
2258+
21622259
#[test]
21632260
fn search_directory_order_is_byte_lexicographic_including_multibyte() {
21642261
let fixture = Fixture::new("sort-multi");

0 commit comments

Comments
 (0)