1use anyhow::{bail, ensure, Context, Result};
3use serde::{Deserialize, Serialize};
4use std::{
5 collections::BTreeMap,
6 io::{Read, Write},
7};
8
9pub const MAX_BODY_BYTES: usize = 256 * 1024;
10pub const MAX_LINE_BYTES: usize = 16 * 1024;
11pub const MAX_POINTS: usize = 256;
12pub const MAX_LINES: usize = 1024;
13pub const MAX_TAGS: usize = 64;
14pub const MAX_FIELDS: usize = 64;
15pub const MAX_NAME_BYTES: usize = 1024;
16pub const MIN_TIMESTAMP_NS: i64 = i64::MIN + 2;
17pub const MAX_TIMESTAMP_NS: i64 = i64::MAX - 1;
18pub const DEFAULT_LLM_FALLBACK: bool = false;
19
20#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
21pub enum Precision {
22 #[default]
23 #[serde(rename = "ns")]
24 Nanoseconds,
25 #[serde(rename = "us")]
26 Microseconds,
27 #[serde(rename = "ms")]
28 Milliseconds,
29 #[serde(rename = "s")]
30 Seconds,
31}
32impl Precision {
33 pub fn parse(s: &str) -> Result<Self> {
34 Ok(match s {
35 "ns" => Self::Nanoseconds,
36 "us" => Self::Microseconds,
37 "ms" => Self::Milliseconds,
38 "s" => Self::Seconds,
39 _ => bail!("precision must be ns, us, ms or s"),
40 })
41 }
42 pub fn as_str(self) -> &'static str {
43 match self {
44 Self::Nanoseconds => "ns",
45 Self::Microseconds => "us",
46 Self::Milliseconds => "ms",
47 Self::Seconds => "s",
48 }
49 }
50 pub fn to_nanoseconds(self, timestamp: i64) -> Result<i64> {
51 let factor = match self {
52 Self::Nanoseconds => 1,
53 Self::Microseconds => 1000,
54 Self::Milliseconds => 1_000_000,
55 Self::Seconds => 1_000_000_000,
56 };
57 let ns = timestamp
58 .checked_mul(factor)
59 .context("timestamp precision overflow")?;
60 ensure!(
61 (MIN_TIMESTAMP_NS..=MAX_TIMESTAMP_NS).contains(&ns),
62 "timestamp outside InfluxDB v2 range"
63 );
64 Ok(ns)
65 }
66}
67#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
68#[serde(tag = "type", content = "value", rename_all = "snake_case")]
69pub enum FieldValue {
70 Float(f64),
71 Integer(i64),
72 Unsigned(u64),
73 Boolean(bool),
74 String(String),
75}
76#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
77#[serde(deny_unknown_fields)]
78pub struct Point {
79 pub measurement: String,
80 #[serde(default)]
81 pub tags: BTreeMap<String, String>,
82 pub fields: BTreeMap<String, FieldValue>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub timestamp: Option<i64>,
85}
86#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
87#[serde(deny_unknown_fields)]
88pub struct WriteBatch {
89 pub org: String,
90 pub bucket: String,
91 #[serde(default)]
92 pub precision: Precision,
93 pub points: Vec<Point>,
94 #[serde(default)]
95 pub gzip: bool,
96}
97#[derive(Clone, Debug, PartialEq, Serialize)]
98pub struct ObservedPoint {
99 pub line: usize,
100 #[serde(flatten)]
101 pub point: Point,
102 pub timestamp_ns: i64,
103}
104#[derive(Clone, Debug, PartialEq, Serialize)]
105pub struct LineError {
106 pub line: usize,
107 pub message: String,
108}
109#[derive(Clone, Debug, PartialEq, Serialize)]
110pub struct ParsedBatch {
111 pub points: Vec<ObservedPoint>,
112 pub errors: Vec<LineError>,
113}
114
115pub fn validate_token(value: &str) -> Result<()> {
116 ensure!(
117 !value.is_empty()
118 && value.len() <= MAX_NAME_BYTES
119 && value.bytes().all(|b| b.is_ascii_graphic()),
120 "token must be nonempty printable ASCII, <=1024 bytes"
121 );
122 Ok(())
123}
124pub fn validate_target(value: &str) -> Result<()> {
125 ensure!(
126 !value.is_empty()
127 && value.len() <= MAX_NAME_BYTES
128 && !crate::utils::sanitize::has_controls(&value),
129 "org/bucket must be nonempty, <=1024 bytes, without controls"
130 );
131 Ok(())
132}
133fn name(value: &str, namespace: bool) -> Result<()> {
134 ensure!(
135 !value.is_empty()
136 && value.len() <= MAX_NAME_BYTES
137 && !value.bytes().any(|b| b < 32 || b == 127)
138 && !value.ends_with('\\'),
139 "empty, oversized, control or trailing-backslash name"
140 );
141 if namespace {
142 ensure!(
143 !value.starts_with('_'),
144 "reserved InfluxDB underscore namespace"
145 );
146 }
147 Ok(())
148}
149fn validate_point(p: &Point, precision: Precision) -> Result<()> {
150 name(&p.measurement, true)?;
151 ensure!(
152 !p.measurement.starts_with('#'),
153 "measurement cannot begin comment marker"
154 );
155 ensure!(p.tags.len() <= MAX_TAGS, "tag count limit");
156 ensure!(
157 !p.fields.is_empty() && p.fields.len() <= MAX_FIELDS,
158 "point requires 1..64 fields"
159 );
160 for (key, value) in &p.tags {
161 name(key, true)?;
162 ensure!(key != "time", "reserved time tag key");
163 name(value, false)?;
164 }
165 for (key, value) in &p.fields {
166 name(key, true)?;
167 ensure!(key != "time", "reserved time field key");
168 match value {
169 FieldValue::Float(n) => ensure!(n.is_finite(), "nonfinite float field"),
170 FieldValue::String(s) => ensure!(
171 s.len() <= MAX_LINE_BYTES && !s.contains(['\r', '\n', '\0']),
172 "string field byte/newline/control limit"
173 ),
174 _ => {}
175 }
176 }
177 if let Some(ts) = p.timestamp {
178 precision.to_nanoseconds(ts)?;
179 }
180 Ok(())
181}
182fn escaped(value: &str, special: &[u8]) -> String {
183 let mut out = String::new();
184 for c in value.chars() {
185 if c.is_ascii() && special.contains(&(c as u8)) {
186 out.push('\\');
187 }
188 out.push(c);
189 }
190 out
191}
192fn encode_point(p: &Point, precision: Precision) -> Result<String> {
193 validate_point(p, precision)?;
194 let mut out = escaped(&p.measurement, b", ");
195 for (key, value) in &p.tags {
196 out.push(',');
197 out.push_str(&escaped(key, b",= "));
198 out.push('=');
199 out.push_str(&escaped(value, b",= "));
200 }
201 out.push(' ');
202 for (index, (key, value)) in p.fields.iter().enumerate() {
203 if index > 0 {
204 out.push(',');
205 }
206 out.push_str(&escaped(key, b",= "));
207 out.push('=');
208 match value {
209 FieldValue::Float(n) => out.push_str(&n.to_string()),
210 FieldValue::Integer(n) => {
211 out.push_str(&n.to_string());
212 out.push('i');
213 }
214 FieldValue::Unsigned(n) => {
215 out.push_str(&n.to_string());
216 out.push('u');
217 }
218 FieldValue::Boolean(b) => out.push_str(if *b { "true" } else { "false" }),
219 FieldValue::String(s) => {
220 out.push('"');
221 out.push_str(&escaped(s, b"\\\""));
222 out.push('"');
223 }
224 }
225 ensure!(out.len() <= MAX_LINE_BYTES, "encoded line byte limit");
226 }
227 if let Some(ts) = p.timestamp {
228 out.push(' ');
229 out.push_str(&ts.to_string());
230 }
231 ensure!(out.len() <= MAX_LINE_BYTES, "encoded line byte limit");
232 Ok(out)
233}
234pub fn encode_batch(batch: &WriteBatch) -> Result<Vec<u8>> {
235 validate_target(&batch.org)?;
236 validate_target(&batch.bucket)?;
237 ensure!(
238 !batch.points.is_empty() && batch.points.len() <= MAX_POINTS,
239 "batch requires 1..256 points"
240 );
241 let mut out = Vec::new();
242 for p in &batch.points {
243 let line = encode_point(p, batch.precision)?;
244 ensure!(
245 out.len() + line.len() < MAX_BODY_BYTES,
246 "encoded body byte limit"
247 );
248 out.extend_from_slice(line.as_bytes());
249 out.push(b'\n');
250 }
251 if batch.gzip {
252 let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
253 e.write_all(&out)?;
254 out = e.finish()?;
255 ensure!(out.len() <= MAX_BODY_BYTES, "compressed body byte limit");
256 }
257 Ok(out)
258}
259pub fn decode_body(bytes: &[u8], encoding: &str) -> Result<Vec<u8>> {
260 ensure!(bytes.len() <= MAX_BODY_BYTES, "body byte limit");
261 match encoding {
262 "" | "identity" => Ok(bytes.to_vec()),
263 "gzip" => {
264 let mut d = flate2::bufread::MultiGzDecoder::new(bytes);
265 let mut out = Vec::new();
266 d.by_ref()
267 .take((MAX_BODY_BYTES + 1) as u64)
268 .read_to_end(&mut out)
269 .context("malformed gzip body")?;
270 ensure!(
271 out.len() <= MAX_BODY_BYTES && d.get_ref().is_empty(),
272 "decompressed body byte limit or trailing bytes"
273 );
274 Ok(out)
275 }
276 _ => bail!("unsupported content encoding"),
277 }
278}
279fn token(bytes: &[u8], at: &mut usize, delimiters: &[u8], escapes: &[u8]) -> Result<String> {
280 let mut out = Vec::new();
281 while *at < bytes.len() {
282 let b = bytes[*at];
283 if b == b'\\'
284 && bytes
285 .get(*at + 1)
286 .is_some_and(|next| escapes.contains(next))
287 {
288 out.push(bytes[*at + 1]);
289 *at += 2;
290 } else if delimiters.contains(&b) {
291 break;
292 } else {
293 out.push(b);
294 *at += 1;
295 }
296 }
297 Ok(String::from_utf8(out)?)
298}
299fn expect(bytes: &[u8], at: &mut usize, b: u8) -> Result<()> {
300 ensure!(bytes.get(*at) == Some(&b), "expected '{}'", b as char);
301 *at += 1;
302 Ok(())
303}
304fn field(bytes: &[u8], at: &mut usize) -> Result<FieldValue> {
305 if bytes.get(*at) == Some(&b'"') {
306 *at += 1;
307 let mut string = Vec::new();
308 while *at < bytes.len() && bytes[*at] != b'"' {
309 let b = bytes[*at];
310 let replacement = if b == b'\\' {
311 bytes.get(*at + 1).and_then(|c| match c {
312 b'\\' => Some(b'\\'),
313 b'"' => Some(b'"'),
314 b'n' => Some(b'\n'),
315 b'r' => Some(b'\r'),
316 b't' => Some(b'\t'),
317 _ => None,
318 })
319 } else {
320 None
321 };
322 if let Some(c) = replacement {
323 string.push(c);
324 *at += 2;
325 } else {
326 string.push(b);
327 *at += 1;
328 }
329 }
330 let s = String::from_utf8(string)?;
331 expect(bytes, at, b'"')?;
332 ensure!(
333 *at == bytes.len() || matches!(bytes[*at], b',' | b' '),
334 "unexpected characters after string field"
335 );
336 return Ok(FieldValue::String(s));
337 }
338 let s = token(bytes, at, b", ", b"")?;
339 ensure!(!s.is_empty(), "empty field value");
340 Ok(match s.as_str() {
341 "t" | "T" | "true" | "True" | "TRUE" => FieldValue::Boolean(true),
342 "f" | "F" | "false" | "False" | "FALSE" => FieldValue::Boolean(false),
343 _ if s.ends_with('i') => {
344 FieldValue::Integer(s[..s.len() - 1].parse().context("invalid int64 field")?)
345 }
346 _ if s.ends_with('u') => {
347 FieldValue::Unsigned(s[..s.len() - 1].parse().context("invalid uint64 field")?)
348 }
349 _ => {
350 let n: f64 = s.parse().context("invalid float field")?;
351 ensure!(n.is_finite(), "nonfinite float field");
352 FieldValue::Float(n)
353 }
354 })
355}
356fn parse_point(line: &str, precision: Precision) -> Result<Point> {
357 ensure!(line.len() <= MAX_LINE_BYTES, "line byte limit");
358 let bytes = line.as_bytes();
359 let mut at = 0;
360 let measurement = token(bytes, &mut at, b", ", b", ")?;
361 let mut tags = BTreeMap::new();
362 while bytes.get(at) == Some(&b',') {
363 at += 1;
364 ensure!(tags.len() < MAX_TAGS, "tag count limit");
365 let key = token(bytes, &mut at, b"=, ", b",= ")?;
366 expect(bytes, &mut at, b'=')?;
367 let value = token(bytes, &mut at, b",= ", b",= ")?;
368 ensure!(tags.insert(key, value).is_none(), "duplicate tag key");
369 }
370 expect(bytes, &mut at, b' ')?;
371 let mut fields = BTreeMap::new();
372 loop {
373 ensure!(fields.len() < MAX_FIELDS, "field count limit");
374 let key = token(bytes, &mut at, b"=, ", b",= ")?;
375 expect(bytes, &mut at, b'=')?;
376 let value = field(bytes, &mut at)?;
377 ensure!(fields.insert(key, value).is_none(), "duplicate field key");
378 if bytes.get(at) != Some(&b',') {
379 break;
380 }
381 at += 1;
382 }
383 let timestamp = if at == bytes.len() {
384 None
385 } else {
386 expect(bytes, &mut at, b' ')?;
387 let ts = &line[at..];
388 ensure!(
389 !ts.is_empty()
390 && ts
391 .strip_prefix('-')
392 .unwrap_or(ts)
393 .bytes()
394 .all(|b| b.is_ascii_digit()),
395 "invalid timestamp syntax"
396 );
397 Some(ts.parse().context("timestamp int64 overflow")?)
398 };
399 let point = Point {
400 measurement,
401 tags,
402 fields,
403 timestamp,
404 };
405 validate_point(&point, precision)?;
406 Ok(point)
407}
408pub fn parse_batch(bytes: &[u8], precision: Precision, received_ns: i64) -> Result<ParsedBatch> {
409 ensure!(bytes.len() <= MAX_BODY_BYTES, "body byte limit");
410 ensure!(
411 (MIN_TIMESTAMP_NS..=MAX_TIMESTAMP_NS).contains(&received_ns),
412 "receiver timestamp outside range"
413 );
414 let body = std::str::from_utf8(bytes).context("line protocol must be UTF-8")?;
415 let mut out = ParsedBatch {
416 points: Vec::new(),
417 errors: Vec::new(),
418 };
419 for (index, line) in body.lines().enumerate() {
420 ensure!(index < MAX_LINES, "line count limit");
421 if line.is_empty() || line.starts_with('#') {
422 continue;
423 }
424 ensure!(
425 out.points.len() + out.errors.len() < MAX_POINTS,
426 "point count limit"
427 );
428 match parse_point(line, precision) {
429 Ok(point) => {
430 let timestamp_ns = point
431 .timestamp
432 .map(|ts| precision.to_nanoseconds(ts))
433 .transpose()?
434 .unwrap_or(received_ns);
435 out.points.push(ObservedPoint {
436 line: index + 1,
437 point,
438 timestamp_ns,
439 });
440 }
441 Err(error) => out.errors.push(LineError {
442 line: index + 1,
443 message: error.to_string(),
444 }),
445 }
446 }
447 Ok(out)
448}