fix: bound encoded common text results including escape and record overhead
This commit is contained in:
@@ -265,7 +265,9 @@ pub(crate) fn window(
|
|||||||
};
|
};
|
||||||
let offset = integer("offset", 0)?;
|
let offset = integer("offset", 0)?;
|
||||||
let limit = integer("limit", MAX_LINES)?;
|
let limit = integer("limit", MAX_LINES)?;
|
||||||
if !(1..=MAX_LINES).contains(&limit) || offset > i64::MAX as usize - MAX_LINES {
|
if !(1..=MAX_LINES).contains(&limit)
|
||||||
|
|| (offset as u64) > 9_007_199_254_740_991u64 - MAX_LINES as u64
|
||||||
|
{
|
||||||
return Err(failure(
|
return Err(failure(
|
||||||
ProtocolErrorCode::InvalidArguments,
|
ProtocolErrorCode::InvalidArguments,
|
||||||
"text range exceeds bounds",
|
"text range exceeds bounds",
|
||||||
@@ -365,7 +367,19 @@ pub(crate) fn page_result(
|
|||||||
fs_operation::text::render_numbered(text, offset, offset + count, false, MAX_BYTES);
|
fs_operation::text::render_numbered(text, offset, offset + count, false, MAX_BYTES);
|
||||||
fields.insert("content".into(), Value::String(rendered.body));
|
fields.insert("content".into(), Value::String(rendered.body));
|
||||||
}
|
}
|
||||||
Ok(Value::Record(fields))
|
let value = Value::Record(fields);
|
||||||
|
let json = crate::wip::wip_to_json(&value)
|
||||||
|
.map_err(|_| failure(ProtocolErrorCode::Internal, "text result unavailable"))?;
|
||||||
|
// The model receives pretty JSON, so budget escapes and record/indentation overhead too.
|
||||||
|
let encoded = serde_json::to_vec_pretty(&json)
|
||||||
|
.map_err(|_| failure(ProtocolErrorCode::Internal, "text result unavailable"))?;
|
||||||
|
if encoded.len() > MAX_BYTES {
|
||||||
|
return Err(failure(
|
||||||
|
ProtocolErrorCode::ResourceLimitExceeded,
|
||||||
|
"serialized text window exceeds output byte bound",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(value)
|
||||||
}
|
}
|
||||||
fn parameter(n: &str, r: bool, t: TypeExpr) -> ParameterDeclaration {
|
fn parameter(n: &str, r: bool, t: TypeExpr) -> ParameterDeclaration {
|
||||||
ParameterDeclaration {
|
ParameterDeclaration {
|
||||||
@@ -459,3 +473,82 @@ fn grep_parameters() -> Vec<ParameterDeclaration> {
|
|||||||
}
|
}
|
||||||
p
|
p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
fn args(value: serde_json::Value) -> BTreeMap<String, Value> {
|
||||||
|
let Value::Record(args) = crate::wip::json_to_wip(&value).unwrap() else {
|
||||||
|
panic!("record")
|
||||||
|
};
|
||||||
|
args
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn text_window_preserves_unicode_crlf_offsets_and_literal_search_continuation() {
|
||||||
|
let input = "first\r\n雪.*\r\nnot a match\n雪.*\n";
|
||||||
|
let arguments = args(serde_json::json!({"offset":1,"limit":2,"pattern":".*"}));
|
||||||
|
let whole = text_result("search_text", &arguments, input)
|
||||||
|
.unwrap_or_else(|_| panic!("text result refused"));
|
||||||
|
let partial = page_result("search_text", &arguments, "雪.*\r\nnot a match\n", 1, true)
|
||||||
|
.unwrap_or_else(|_| panic!("page refused"));
|
||||||
|
assert_eq!(whole, partial);
|
||||||
|
let result = crate::wip::wip_to_json(&whole).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
serde_json::json!({"matches":[{"line":2,"text":"雪.*"}],"next_offset":3,"truncated":true})
|
||||||
|
);
|
||||||
|
let read = text_result(
|
||||||
|
"read_lines",
|
||||||
|
&args(serde_json::json!({"offset":1,"limit":1})),
|
||||||
|
input,
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|_| panic!("read refused"));
|
||||||
|
assert_eq!(
|
||||||
|
crate::wip::wip_to_json(&read).unwrap(),
|
||||||
|
serde_json::json!({"content":" 2\t雪.*\n","next_offset":2,"truncated":true})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn text_window_refuses_invalid_ranges_patterns_source_and_encoded_output_bounds() {
|
||||||
|
for value in [
|
||||||
|
serde_json::json!({"limit":0}),
|
||||||
|
serde_json::json!({"limit":2001}),
|
||||||
|
serde_json::json!({"offset":-1}),
|
||||||
|
serde_json::json!({"offset":9_007_199_254_740_991i64}),
|
||||||
|
serde_json::json!({"file_path":"elsewhere"}),
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
matches!(text_result("read_lines",&args(value),"x"),Err(WipOperationError::Protocol(e)) if e.code==ProtocolErrorCode::InvalidArguments)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for pattern in [String::new(), "x".repeat(4097)] {
|
||||||
|
assert!(
|
||||||
|
matches!(text_result("search_text",&args(serde_json::json!({"pattern":pattern})),"x"),Err(WipOperationError::Protocol(e)) if e.code==ProtocolErrorCode::InvalidArguments)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (operation, arguments, source) in [
|
||||||
|
(
|
||||||
|
"read_lines",
|
||||||
|
serde_json::json!({}),
|
||||||
|
"雪".repeat(MAX_BYTES / 3 + 1),
|
||||||
|
),
|
||||||
|
("read_lines", serde_json::json!({}), "\u{1}".repeat(16000)),
|
||||||
|
(
|
||||||
|
"search_text",
|
||||||
|
serde_json::json!({"pattern":"x"}),
|
||||||
|
"x\n".repeat(MAX_LINES),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"search_text",
|
||||||
|
serde_json::json!({"pattern":"\""}),
|
||||||
|
format!("{}\n", "\"".repeat(40000)),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
matches!(text_result(operation,&args(arguments),&source),Err(WipOperationError::Protocol(e)) if e.code==ProtocolErrorCode::ResourceLimitExceeded),
|
||||||
|
"{operation}: {} source bytes",
|
||||||
|
source.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user