1use crate::server::dict::wire;
3use anyhow::{bail, ensure, Context, Result};
4use serde_json::{json, Value};
5use std::time::Duration;
6use tokio::io::{AsyncBufRead, AsyncBufReadExt};
7pub const DEADLINE: Duration = Duration::from_secs(30);
8pub const MAX_RESPONSE_BYTES: usize = 1024 * 1024;
9pub const MAX_ENTRIES: usize = 4096;
10pub const MAX_RESPONSE_LINE: usize = 65536;
11#[derive(Debug)]
12pub struct Request {
13 pub operation: String,
14 pub bytes: Vec<u8>,
15}
16impl Request {
17 pub fn from_action(v: &Value) -> Result<Self> {
18 let operation = v["operation"].as_str().context("Missing operation")?;
19 let text = |key: &str, default: Option<&str>| -> Result<String> {
20 let s = v
21 .get(key)
22 .map(|v| v.as_str().context("Expected string"))
23 .unwrap_or_else(|| default.context("Missing parameter"))?;
24 ensure!(
25 !crate::utils::sanitize::has_controls(&s),
26 "DICT parameters cannot contain control characters"
27 );
28 Ok(wire::quoted(s))
29 };
30 let database = || text("database", Some("*"));
31 let line = match operation {
32 "define" => format!("DEFINE {} {}", database()?, text("word", None)?),
33 "match" => format!(
34 "MATCH {} {} {}",
35 database()?,
36 text("strategy", Some("."))?,
37 text("word", None)?
38 ),
39 "databases" => "SHOW DB".into(),
40 "strategies" => "SHOW STRAT".into(),
41 "info" => format!("SHOW INFO {}", text("database", None)?),
42 "server" => "SHOW SERVER".into(),
43 "status" => "STATUS".into(),
44 "help" => "HELP".into(),
45 "client" => format!("CLIENT {}", text("name", None)?),
46 "quit" => "QUIT".into(),
47 _ => bail!("Unsupported DICT operation"),
48 };
49 ensure!(
50 line.len() + 2 <= wire::MAX_LINE_BYTES,
51 "DICT command exceeds client 1024-byte bound"
52 );
53 Ok(Self {
54 operation: operation.into(),
55 bytes: format!("{line}\r\n").into_bytes(),
56 })
57 }
58}
59pub async fn line<R: AsyncBufRead + Unpin>(reader: &mut R, budget: &mut usize) -> Result<String> {
60 let mut result = Vec::new();
61 loop {
62 let chunk = reader.fill_buf().await?;
63 ensure!(!chunk.is_empty(), "Truncated DICT response");
64 let n = chunk
65 .iter()
66 .position(|b| *b == b'\n')
67 .map(|n| n + 1)
68 .unwrap_or(chunk.len());
69 ensure!(
70 result.len() + n <= MAX_RESPONSE_LINE,
71 "DICT response line too large"
72 );
73 ensure!(
74 *budget + n <= MAX_RESPONSE_BYTES,
75 "DICT response exceeds limit"
76 );
77 result.extend_from_slice(&chunk[..n]);
78 reader.consume(n);
79 *budget += n;
80 if result.ends_with(b"\n") {
81 ensure!(result.ends_with(b"\r\n"), "DICT requires CRLF");
82 result.truncate(result.len() - 2);
83 return Ok(String::from_utf8(result)?);
84 }
85 }
86}
87fn status(line: &str) -> Result<(u16, &str)> {
88 let bytes = line.as_bytes();
89 ensure!(
90 bytes.len() >= 3
91 && bytes[..3].iter().all(u8::is_ascii_digit)
92 && (bytes.len() == 3 || bytes[3] == b' '),
93 "Malformed DICT status"
94 );
95 Ok((line[..3].parse()?, line.get(4..).unwrap_or("")))
96}
97async fn block<R: AsyncBufRead + Unpin>(reader: &mut R, budget: &mut usize) -> Result<Vec<String>> {
98 let mut lines = Vec::new();
99 loop {
100 let line = line(reader, budget).await?;
101 if line == "." {
102 return Ok(lines);
103 }
104 ensure!(
105 lines.len() < MAX_ENTRIES,
106 "DICT text/list has too many lines"
107 );
108 lines.push(
109 line.strip_prefix("..")
110 .map(|s| format!(".{s}"))
111 .unwrap_or(line),
112 );
113 }
114}
115fn definition_fields(text: &str) -> Result<Vec<String>> {
118 let mut quote = None;
119 let mut escaped = false;
120 let mut in_token = false;
121 let mut fields = 0;
122 let mut end = text.len();
123 for (i, c) in text.char_indices() {
124 if escaped {
125 escaped = false;
126 continue;
127 }
128 if c == '\\' {
129 escaped = true;
130 in_token = true;
131 continue;
132 }
133 if let Some(q) = quote {
134 if c == q {
135 quote = None;
136 }
137 continue;
138 }
139 if c == '\"' || c == '\'' {
140 quote = Some(c);
141 in_token = true;
142 continue;
143 }
144 if c == ' ' || c == '\t' {
145 if in_token {
146 fields += 1;
147 in_token = false;
148 if fields == 3 {
149 end = i;
150 break;
151 }
152 }
153 } else {
154 in_token = true;
155 }
156 }
157 wire::split_args(&text[..end]).map_err(|_| anyhow::anyhow!("Invalid definition fields"))
158}
159async fn finish<R: AsyncBufRead + Unpin>(reader: &mut R, budget: &mut usize) -> Result<()> {
160 let s = line(reader, budget).await?;
161 ensure!(status(&s)?.0 == 250, "DICT response missing final 250");
162 Ok(())
163}
164pub async fn greeting<R: AsyncBufRead + Unpin>(reader: &mut R) -> Result<Value> {
165 tokio::time::timeout(DEADLINE, async {
166 let mut budget = 0;
167 let s = line(reader, &mut budget).await?;
168 let (code, message) = status(&s)?;
169 ensure!(
170 code == 220,
171 "DICT server refused connection: {code} {message}"
172 );
173 Ok(json!({"greeting":message}))
174 })
175 .await
176 .context("DICT greeting deadline exceeded")?
177}
178pub async fn response<R: AsyncBufRead + Unpin>(reader: &mut R, request: &Request) -> Result<Value> {
179 tokio::time::timeout(DEADLINE, read_response(reader, request))
180 .await
181 .context("DICT response deadline exceeded")?
182}
183async fn read_response<R: AsyncBufRead + Unpin>(
184 reader: &mut R,
185 request: &Request,
186) -> Result<Value> {
187 let mut budget = 0;
188 let first = line(reader, &mut budget).await?;
189 let (code, message) = status(&first)?;
190 if (400..600).contains(&code) {
191 return Ok(json!({"code":code,"error":message}));
192 }
193 match request.operation.as_str() {
194 "define" => {
195 ensure!(code == 150, "Expected DICT definition header");
196 let count = message
197 .split_whitespace()
198 .next()
199 .context("Missing definition count")?
200 .parse::<usize>()?;
201 ensure!(count <= MAX_ENTRIES, "Too many definitions");
202 let mut entries = Vec::new();
203 for _ in 0..count {
204 let header = line(reader, &mut budget).await?;
205 let (code, text) = status(&header)?;
206 ensure!(code == 151, "Expected definition preface");
207 let fields = definition_fields(text)?;
208 ensure!(fields.len() >= 3, "Invalid definition preface");
209 let text = block(reader, &mut budget).await?.join("\n");
210 entries.push(json!({"word":fields[0],"database":fields[1],"database_description":fields[2],"text":text}));
211 }
212 finish(reader, &mut budget).await?;
213 Ok(json!({"code":150,"definitions":entries}))
214 }
215 "match" | "databases" | "strategies" => {
216 let expected = match request.operation.as_str() {
217 "match" => 152,
218 "databases" => 110,
219 _ => 111,
220 };
221 ensure!(code == expected, "Unexpected DICT listing status");
222 let count = message
223 .split_whitespace()
224 .next()
225 .context("Missing list count")?
226 .parse::<usize>()?;
227 ensure!(count <= MAX_ENTRIES, "Too many list entries");
228 let lines = block(reader, &mut budget).await?;
229 ensure!(lines.len() == count, "DICT list count mismatch");
230 let mut entries = Vec::new();
231 for line in lines {
232 let fields = wire::split_args(&line)
233 .map_err(|_| anyhow::anyhow!("Invalid listing fields"))?;
234 ensure!(fields.len() == 2, "Invalid listing row");
235 entries.push(if request.operation == "match" {
236 json!({"database":fields[0],"word":fields[1]})
237 } else {
238 json!({"name":fields[0],"description":fields[1]})
239 });
240 }
241 finish(reader, &mut budget).await?;
242 Ok(json!({"code":code,"entries":entries}))
243 }
244 "info" | "server" | "help" => {
245 ensure!(
246 code == match request.operation.as_str() {
247 "info" => 112,
248 "help" => 113,
249 _ => 114,
250 },
251 "Unexpected DICT text status"
252 );
253 let text = block(reader, &mut budget).await?.join("\n");
254 finish(reader, &mut budget).await?;
255 Ok(json!({"code":code,"text":text}))
256 }
257 "client" => {
258 ensure!(code == 250, "Unexpected CLIENT reply");
259 Ok(json!({"code":code,"message":message}))
260 }
261 "status" => {
262 ensure!(code == 210, "Unexpected STATUS reply");
263 Ok(json!({"code":code,"message":message}))
264 }
265 "quit" => {
266 ensure!(code == 221, "Unexpected QUIT reply");
267 Ok(json!({"code":code,"message":message}))
268 }
269 _ => bail!("Unsupported DICT response"),
270 }
271}