Skip to main content

netget/client/dict/
wire.rs

1//! Bounded RFC 2229 framing and structured dictionary responses.
2use 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}
115// Only the first three 151 fields have quoting syntax. Explanatory text can
116// contain apostrophes or quotes and is not another protocol parameter.
117fn 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}