1use async_trait::async_trait;
12use serde_json::Value;
13use sha2::{Digest, Sha256, Sha512};
14
15use crate::tool::{Capability, Tool, ToolDefinition};
16use crate::tool_error::ToolError;
17
18pub struct HashTool {
31 definition: ToolDefinition,
32}
33
34impl HashTool {
35 pub fn new() -> Self {
37 Self {
38 definition: ToolDefinition::new(
39 "hash",
40 "Compute cryptographic hash (SHA-256 or SHA-512) of text input.",
41 r#"{
42 "type": "object",
43 "properties": {
44 "text": {
45 "type": "string",
46 "description": "Text to hash"
47 },
48 "algorithm": {
49 "type": "string",
50 "enum": ["sha256", "sha512"],
51 "default": "sha256",
52 "description": "Hash algorithm to use"
53 }
54 },
55 "required": ["text"]
56 }"#,
57 ),
58 }
59 }
60}
61
62impl Default for HashTool {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68#[async_trait]
69impl Tool for HashTool {
70 fn definition(&self) -> &ToolDefinition {
71 &self.definition
72 }
73
74 fn capabilities(&self) -> Vec<Capability> {
75 vec![Capability::PureComputation, Capability::Cryptography]
76 }
77
78 fn validate(&self, args: &Value) -> Result<(), ToolError> {
79 let text = args
80 .get("text")
81 .and_then(|t| t.as_str())
82 .ok_or_else(|| ToolError::invalid_args("hash", "Missing required field 'text'"))?;
83
84 if text.len() > 1_000_000 {
86 return Err(ToolError::invalid_args(
87 "hash",
88 "Input text too large (max 1MB)",
89 ));
90 }
91
92 if let Some(algo) = args.get("algorithm").and_then(|a| a.as_str()) {
94 if algo != "sha256" && algo != "sha512" {
95 return Err(ToolError::invalid_args(
96 "hash",
97 format!("Invalid algorithm '{}'. Must be 'sha256' or 'sha512'", algo),
98 ));
99 }
100 }
101
102 Ok(())
103 }
104
105 async fn execute(&self, args: Value) -> Result<Value, ToolError> {
106 let text = args["text"]
107 .as_str()
108 .ok_or_else(|| ToolError::invalid_args("hash", "Missing 'text' field"))?;
109
110 let algorithm = args
111 .get("algorithm")
112 .and_then(|a| a.as_str())
113 .unwrap_or("sha256");
114
115 let hash_hex = match algorithm {
116 "sha512" => {
117 let mut hasher = Sha512::new();
118 hasher.update(text.as_bytes());
119 hex::encode(hasher.finalize())
120 }
121 _ => {
122 let mut hasher = Sha256::new();
123 hasher.update(text.as_bytes());
124 hex::encode(hasher.finalize())
125 }
126 };
127
128 Ok(serde_json::json!({
129 "hash": hash_hex,
130 "algorithm": algorithm,
131 "input_length": text.len()
132 }))
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[tokio::test]
141 async fn test_sha256_hash() {
142 let tool = HashTool::new();
143 let result = tool
144 .execute(serde_json::json!({"text": "hello", "algorithm": "sha256"}))
145 .await
146 .unwrap();
147
148 assert_eq!(
150 result["hash"],
151 "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
152 );
153 assert_eq!(result["algorithm"], "sha256");
154 }
155
156 #[tokio::test]
157 async fn test_sha512_hash() {
158 let tool = HashTool::new();
159 let result = tool
160 .execute(serde_json::json!({"text": "hello", "algorithm": "sha512"}))
161 .await
162 .unwrap();
163
164 assert_eq!(result["algorithm"], "sha512");
165 assert_eq!(result["hash"].as_str().unwrap().len(), 128);
167 }
168
169 #[tokio::test]
170 async fn test_default_algorithm() {
171 let tool = HashTool::new();
172 let result = tool
173 .execute(serde_json::json!({"text": "test"}))
174 .await
175 .unwrap();
176
177 assert_eq!(result["algorithm"], "sha256");
179 }
180
181 #[tokio::test]
182 async fn test_invalid_algorithm() {
183 let tool = HashTool::new();
184 let result = tool.validate(&serde_json::json!({"text": "hello", "algorithm": "md5"}));
185
186 assert!(matches!(result, Err(ToolError::InvalidArguments { .. })));
187 }
188
189 #[tokio::test]
190 async fn test_missing_text() {
191 let tool = HashTool::new();
192 let result = tool.validate(&serde_json::json!({}));
193
194 assert!(matches!(result, Err(ToolError::InvalidArguments { .. })));
195 }
196
197 #[tokio::test]
198 async fn test_empty_string() {
199 let tool = HashTool::new();
200 let result = tool.execute(serde_json::json!({"text": ""})).await.unwrap();
201
202 assert_eq!(
204 result["hash"],
205 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
206 );
207 }
208}