use std::sync::Arc;
use axum::{
extract::State,
http::StatusCode,
response::{IntoResponse, Json},
};
use serde::{Deserialize, Serialize};
use crate::proxy::{AppState, ccr_get};
#[derive(Debug, Deserialize)]
pub struct RetrieveRequest {
pub hash:Option<String>,
pub query:Option<String>,
#[serde(default)]
pub offset:usize,
#[serde(default)]
pub limit:usize,
}
#[derive(Debug, Serialize)]
pub struct RetrieveResponse {
pub found:bool,
pub content:Option<String>,
pub source:String,
pub error:Option<String>,
pub truncated:bool,
}
pub async fn handle_retrieve(State(state):State<Arc<AppState>>, Json(req):Json<RetrieveRequest>) -> impl IntoResponse {
let hash = match &req.hash {
Some(h) => crate::marker::normalize_hash(h).to_string(),
None => {
return (
StatusCode::BAD_REQUEST,
Json(RetrieveResponse {
found:false,
content:None,
source:"none".into(),
error:Some("`hash` required".into()),
truncated:false,
}),
)
.into_response();
},
};
let mut content = {
let inline_hit = state.inline_ccr.lock().ok().and_then(|mut map| map.get(&hash).cloned());
if let Some(cached) = inline_hit {
state.inline_ccr_hits.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
state.ccr_hits.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
cached
} else {
state.inline_ccr_misses.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
match &state.ccr {
Some(ccr) => {
match ccr_get(ccr, &hash).await {
Some(c) => {
state.ccr_hits.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
c
},
None => {
state.ccr_misses.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return (
StatusCode::NOT_FOUND,
Json(RetrieveResponse {
found:false,
content:None,
source:"none".into(),
error:Some(format!("CCR entry not found: {}", hash)),
truncated:false,
}),
)
.into_response();
},
}
},
None => {
state.ccr_misses.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
return (
StatusCode::NOT_FOUND,
Json(RetrieveResponse {
found:false,
content:None,
source:"none".into(),
error:Some(format!("CCR entry not found: {}", hash)),
truncated:false,
}),
)
.into_response();
},
}
}
};
content = filter_content(&content, req.query.as_deref());
let (content, truncated) = match paginate(&content, req.offset, req.limit) {
Ok(paged) => paged,
Err(err) => {
return (
StatusCode::BAD_REQUEST,
Json(RetrieveResponse {
found:false,
content:Some(err),
source:"ccr".into(),
error:None,
truncated:false,
}),
)
.into_response();
},
};
Json(RetrieveResponse { found:true, content:Some(content), source:"ccr".into(), error:None, truncated })
.into_response()
}
fn filter_content(content:&str, query:Option<&str>) -> String {
match query {
Some(q) if !q.is_empty() => {
let q = crate::struct_extract::floor_boundary(q, 512);
let q_lower = q.to_ascii_lowercase();
let filtered:Vec<&str> = content
.lines()
.filter(|line| line.to_ascii_lowercase().contains(&q_lower))
.collect();
if filtered.is_empty() {
format!("[no lines matching {:?} in {} lines]", q, content.lines().count())
} else {
filtered.join("\n")
}
},
_ => content.to_string(),
}
}
fn paginate(content:&str, offset:usize, limit:usize) -> Result<(String, bool), String> {
let limit = if limit == 0 { 10_000 } else { limit.min(10_000) };
let lines:Vec<&str> = content.lines().collect();
let total = lines.len();
if total == 0 {
return Ok((String::new(), false));
}
if offset >= total {
return Err(format!("[offset {} out of range; document has {} lines]", offset, total));
}
let start = offset.min(total);
let end = (start + limit).min(total);
let truncated = start > 0 || end < total;
if !truncated {
return Ok((content.to_string(), false));
}
let mut windowed = lines[start..end].join("\n");
if end == total && content.ends_with('\n') {
windowed.push('\n');
}
windowed = format!("[lines {}-{}/{}]\n{}", start + 1, end, total, windowed);
Ok((windowed, truncated))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_handle_retrieve_normalizes_pipe_suffixed_and_whitespace_hash() {
let state = crate::proxy::tests::test_state_with_ccr();
{
let mut map = state.inline_ccr.lock().unwrap();
map.put("abc123".to_string(), "<the real content>".to_string());
}
let state = Arc::new(state);
async fn body_of(resp:axum::response::Response) -> serde_json::Value {
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap();
serde_json::from_slice(&bytes).unwrap()
}
tokio::runtime::Runtime::new().unwrap().block_on(async {
for hash_arg in ["abc123", "abc123|tool|1024", " abc123 "] {
let resp = handle_retrieve(
State(state.clone()),
Json(RetrieveRequest { hash:Some(hash_arg.to_string()), query:None, offset:0, limit:0 }),
)
.await
.into_response();
let body = body_of(resp).await;
assert_eq!(body["found"], true, "hash arg {hash_arg:?} must resolve: {body:?}");
assert_eq!(body["content"], "<the real content>");
}
});
}
#[test]
fn test_paginate_offset_zero_full_document_no_header() {
let content = "a\nb\nc";
let (result, truncated) = paginate(content, 0, 0).unwrap();
assert_eq!(result, "a\nb\nc");
assert!(!truncated);
}
#[test]
fn test_paginate_preserves_trailing_newline_on_full_document() {
let content = "a\nb\nc\n";
let (result, truncated) = paginate(content, 0, 0).unwrap();
assert_eq!(result, content, "full-document retrieval must return the exact original bytes");
assert!(!truncated);
}
#[test]
fn test_paginate_preserves_trailing_newline_on_partial_window_reaching_end() {
let content = "a\nb\nc\n";
let (result, truncated) = paginate(content, 1, 0).unwrap();
assert_eq!(result, "[lines 2-3/3]\nb\nc\n");
assert!(truncated);
}
#[test]
fn test_paginate_empty_content_returns_empty_ok_not_error() {
assert_eq!(paginate("", 0, 0), Ok((String::new(), false)));
}
#[test]
fn test_paginate_offset_at_or_past_total_is_error() {
let content = "a\nb\nc"; assert!(paginate(content, 3, 0).is_err());
assert!(paginate(content, 10, 0).is_err());
let err = paginate(content, 3, 0).unwrap_err();
assert!(err.contains("out of range"));
assert!(err.contains("3 lines"));
}
#[test]
fn test_paginate_limit_zero_clamps_to_10000() {
let content = (0..5).map(|i| i.to_string()).collect::<Vec<_>>().join("\n");
let (result, truncated) = paginate(&content, 0, 0).unwrap();
assert_eq!(result, content);
assert!(!truncated);
}
#[test]
fn test_paginate_window_header_format() {
let content = "a\nb\nc\nd\ne"; let (result, truncated) = paginate(content, 1, 2).unwrap();
assert_eq!(result, "[lines 2-3/5]\nb\nc");
assert!(truncated);
}
#[test]
fn test_paginate_limit_clamped_to_10000_max() {
let content = (0..5).map(|i| i.to_string()).collect::<Vec<_>>().join("\n");
let (result, truncated) = paginate(&content, 0, 999_999).unwrap();
assert_eq!(result, content);
assert!(!truncated);
}
#[test]
fn test_filter_content_no_query_returns_unchanged() {
assert_eq!(filter_content("a\nb\nc", None), "a\nb\nc");
assert_eq!(filter_content("a\nb\nc", Some("")), "a\nb\nc");
}
#[test]
fn test_filter_content_match() {
let content = "alpha\nbeta error\ngamma\n";
let result = filter_content(content, Some("error"));
assert_eq!(result, "beta error");
}
#[test]
fn test_filter_content_no_match_message() {
let content = "alpha\nbeta\n";
let result = filter_content(content, Some("nonexistent"));
assert!(result.contains("no lines matching"));
assert!(result.contains("2 lines"));
}
#[test]
fn test_filter_content_case_insensitive() {
let content = "ALPHA line\nbeta line\n";
let result = filter_content(content, Some("alpha"));
assert_eq!(result, "ALPHA line");
}
#[test]
fn test_filter_content_query_over_512_chars_is_truncated() {
let long_query = "x".repeat(600);
let content = "some line with lots of x's\n";
let result = filter_content(content, Some(&long_query));
assert!(result.contains("no lines matching"));
}
}