netget/server/inetd/
wire.rs1use anyhow::{bail, ensure, Result};
4use serde_json::{json, Value};
5use std::time::Duration;
6
7pub const READ_CHUNK: usize = 8192;
9pub const MAX_QUOTE_CHARS: usize = 512;
11pub const MAX_DAYTIME_CHARS: usize = 256;
13pub const MAX_CHARGEN_UDP: usize = 512;
15pub const MAX_CHARGEN_LINE: usize = 512;
17pub const SECONDS_1900_TO_1970: u64 = 2_208_988_800;
19pub const IO_TIMEOUT: Duration = Duration::from_secs(30);
21pub const MAX_STREAM_REPLY: usize = 64 * 1024;
23
24pub fn default_charset() -> String {
26 (0x20u8..=0x7e).map(char::from).collect()
27}
28
29pub fn chargen_line(charset: &[u8], width: usize, n: usize) -> Vec<u8> {
31 let mut line = Vec::with_capacity(width + 2);
32 for i in 0..width {
33 line.push(charset[(n + i) % charset.len()]);
34 }
35 line.extend_from_slice(b"\r\n");
36 line
37}
38
39pub fn conforms_to_chargen(text: &str, charset: &str) -> bool {
41 let set = charset.as_bytes();
42 let lines: Vec<&str> = text.split("\r\n").filter(|l| !l.is_empty()).collect();
43 if lines.len() < 2 {
44 return !lines.is_empty() && lines[0].bytes().all(|b| set.contains(&b));
45 }
46 let start = |line: &str| {
47 line.bytes()
48 .next()
49 .and_then(|b| set.iter().position(|c| *c == b))
50 };
51 lines
52 .windows(2)
53 .all(|pair| match (start(pair[0]), start(pair[1])) {
54 (Some(a), Some(b)) => b == (a + 1) % set.len(),
55 _ => false,
56 })
57 && lines.iter().all(|line| {
58 let first = start(line).unwrap_or(0);
59 line.bytes()
60 .enumerate()
61 .all(|(i, b)| set[(first + i) % set.len()] == b)
62 })
63}
64
65pub fn charset(value: Option<&Value>) -> Result<String> {
67 match value {
68 None | Some(Value::Null) => Ok(default_charset()),
69 Some(Value::String(s)) => {
70 ensure!(
71 !s.is_empty() && s.len() <= 95,
72 "charset must hold 1 to 95 characters"
73 );
74 ensure!(
75 s.bytes().all(|b| (0x20..=0x7e).contains(&b)),
76 "charset must be printable ASCII"
77 );
78 Ok(s.clone())
79 }
80 _ => bail!("charset must be a string"),
81 }
82}
83
84pub fn daytime_now() -> String {
86 chrono::Utc::now()
87 .format("%A, %B %-d, %Y %H:%M:%S-UTC")
88 .to_string()
89}
90
91pub fn time_bytes(unix_seconds: i64) -> [u8; 4] {
93 let seconds = (unix_seconds as i128 + SECONDS_1900_TO_1970 as i128).rem_euclid(1 << 32) as u32;
94 seconds.to_be_bytes()
95}
96
97pub fn requested_time(answer: &Value) -> Result<i64> {
99 if let Some(seconds) = answer.get("unix_seconds").filter(|v| !v.is_null()) {
100 return seconds
101 .as_i64()
102 .ok_or_else(|| anyhow::anyhow!("unix_seconds must be an integer"));
103 }
104 if let Some(iso) = answer.get("iso8601").filter(|v| !v.is_null()) {
105 let iso = iso
106 .as_str()
107 .ok_or_else(|| anyhow::anyhow!("iso8601 must be a string"))?;
108 return Ok(chrono::DateTime::parse_from_rfc3339(iso)
109 .map_err(|e| anyhow::anyhow!("iso8601 is not RFC 3339: {e}"))?
110 .timestamp());
111 }
112 Ok(chrono::Utc::now().timestamp())
113}
114
115pub fn describe_time(bytes: [u8; 4]) -> Value {
117 let since_1900 = u32::from_be_bytes(bytes) as i64;
118 let unix = since_1900 - SECONDS_1900_TO_1970 as i64;
119 let iso = chrono::DateTime::from_timestamp(unix, 0).map(|t| t.to_rfc3339());
120 json!({"seconds_since_1900": since_1900, "unix_seconds": unix, "iso8601": iso})
121}
122
123pub fn encode(bytes: &[u8]) -> (String, &'static str) {
125 match std::str::from_utf8(bytes) {
126 Ok(text)
127 if !crate::utils::sanitize::has_controls(&text.replace(['\n', '\r', '\t'], "")) =>
129 {
130 (text.to_string(), "utf8")
131 }
132 _ => (hex::encode(bytes), "hex"),
133 }
134}
135
136pub fn decode(data: &str, encoding: Option<&str>) -> Result<Vec<u8>> {
138 match encoding.unwrap_or("utf8") {
139 "utf8" => Ok(data.as_bytes().to_vec()),
140 "hex" => hex::decode(data).map_err(|e| anyhow::anyhow!("data is not valid hex: {e}")),
141 other => bail!("encoding must be utf8 or hex, not {other}"),
142 }
143}
144
145pub fn line(text: &str, limit: usize) -> String {
147 crate::utils::sanitize::strip_controls(text)
148 .chars()
149 .take(limit)
150 .collect()
151}