1use crate::error::Error;
2use std::io::Read;
3use std::time::Duration;
4
5pub fn encode_cmd(cmd: &str) -> String {
11 let mut out = String::with_capacity(cmd.len());
12 for byte in cmd.bytes() {
13 match byte {
14 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
15 out.push(byte as char);
16 }
17 b => {
18 const HEX: &[u8; 16] = b"0123456789ABCDEF";
20 out.push('%');
21 out.push(HEX[(b >> 4) as usize] as char);
22 out.push(HEX[(b & 0xf) as usize] as char);
23 }
24 }
25 }
26 out
27}
28
29pub fn lookup(cmd: &str) -> Result<String, Error> {
34 if cmd.is_empty() {
35 return Err(Error::Help("command name must not be empty".to_owned()));
36 }
37
38 let encoded = encode_cmd(cmd);
39 let url = format!("https://cheat.sh/{encoded}?T");
40
41 let config = ureq::Agent::config_builder()
42 .timeout_global(Some(Duration::from_secs(10)))
43 .build();
44 let agent: ureq::Agent = config.into();
45
46 match agent.get(&url).call() {
47 Ok(mut response) => {
48 const MAX_BYTES: u64 = 65_536;
50 let mut buf = String::new();
51 response
52 .body_mut()
53 .as_reader()
54 .take(MAX_BYTES)
55 .read_to_string(&mut buf)
56 .map_err(|e| Error::Help(format!("read response: {e}")))?;
57 Ok(buf)
58 }
59 Err(ureq::Error::StatusCode(404)) => {
60 Err(Error::Help(format!("no help available for '{cmd}'")))
61 }
62 Err(e) => Err(Error::Help(format!("network error: {e}"))),
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn test_encode_cmd_spaces() {
72 assert_eq!(encode_cmd("git commit"), "git%20commit");
73 }
74
75 #[test]
76 fn test_encode_cmd_simple() {
77 assert_eq!(encode_cmd("find"), "find");
78 }
79
80 #[test]
81 fn test_encode_cmd_special_chars() {
82 assert_eq!(encode_cmd("foo&bar"), "foo%26bar");
84 assert_eq!(encode_cmd("foo%bar"), "foo%25bar");
85 }
86
87 #[test]
88 fn test_encode_cmd_hash_and_query() {
89 assert_eq!(encode_cmd("foo#bar"), "foo%23bar");
90 assert_eq!(encode_cmd("foo?bar"), "foo%3Fbar");
91 }
92
93 #[test]
94 fn test_encode_cmd_slash() {
95 assert_eq!(encode_cmd("a/b"), "a%2Fb");
96 }
97
98 #[test]
99 fn test_encode_cmd_unreserved_passthrough() {
100 assert_eq!(encode_cmd("abc-XYZ_0.9~"), "abc-XYZ_0.9~");
102 }
103
104 #[test]
105 fn test_encode_cmd_null_byte() {
106 assert_eq!(encode_cmd("foo\0bar"), "foo%00bar");
107 }
108
109 #[test]
110 fn test_lookup_empty_returns_error() {
111 let result = lookup("");
112 assert!(result.is_err());
113 let msg = result.unwrap_err().to_string();
114 assert!(msg.contains("empty"), "expected 'empty' in: {msg}");
115 }
116
117 #[test]
118 #[ignore = "requires network"]
119 fn test_lookup_known_command() {
120 let result = lookup("echo");
121 assert!(result.is_ok(), "lookup failed: {:?}", result.err());
122 let content = result.unwrap();
123 assert!(!content.is_empty());
124 }
125
126 #[test]
127 #[ignore = "requires network"]
128 fn test_lookup_nonexistent_command() {
129 let result = lookup("__nonexistent_oo_test_xyz__");
133 match result {
134 Ok(content) => assert!(!content.is_empty(), "expected non-empty response"),
135 Err(e) => assert!(
136 e.to_string().contains("no help") || e.to_string().contains("network"),
137 "unexpected error variant: {e}"
138 ),
139 }
140 }
141}