1use crate::server::bolt::{
3 messages as m,
4 packstream::{self, Value},
5};
6use anyhow::{bail, ensure, Context, Result};
7use serde_json::{json, Value as Json};
8pub const MAX_DEPTH: usize = 32;
9pub const MAX_NODES: usize = 65536;
10pub const MAX_RETAINED_BYTES: usize = 8 * 1024 * 1024;
11pub const MAX_PAGE_BYTES: usize = 4 * 1024 * 1024;
12pub const MAX_PAGE_RECORDS: usize = 500;
13pub const MAX_FIELDS: usize = 256;
14pub const MAX_TEXT: usize = 65536;
15pub const MAX_PASSWORD: usize = 16384;
16pub const MAX_EVENT_DEPTH: usize = MAX_DEPTH + 8;
17pub fn within_budget(value: &Json) -> bool {
18 crate::utils::json_budget::within_budget(value, MAX_RETAINED_BYTES, MAX_NODES, MAX_DEPTH)
19}
20pub enum Action {
21 Login {
22 username: Option<String>,
23 password: Option<String>,
24 },
25 Logoff,
26 Run {
27 query: String,
28 parameters: Value,
29 extra: Value,
30 },
31 Pull(usize),
32 Discard,
33 Begin(Value),
34 Commit,
35 Rollback,
36 Reset,
37 Disconnect,
38}
39fn text<'a>(value: &'a Json, key: &str, max: usize, required: bool) -> Result<Option<&'a str>> {
40 match value.get(key) {
41 None if !required => Ok(None),
42 Some(Json::String(s)) if !s.is_empty() && s.len() <= max => Ok(Some(s)),
43 _ => bail!("Bolt {key} must be a nonempty bounded string"),
44 }
45}
46fn extra(action: &Json) -> Result<Value> {
47 let mut extra = Vec::new();
48 if let Some(db) = text(action, "database", 256, false)? {
49 extra.push(("db".into(), Value::string(db)));
50 }
51 if let Some(mode) = action.get("mode") {
52 let mode = match mode.as_str() {
53 Some("read") => "r",
54 Some("write") => "w",
55 _ => bail!("Bolt mode must be read or write"),
56 };
57 extra.push(("mode".into(), Value::string(mode)));
58 }
59 if let Some(ms) = action.get("transaction_timeout_ms") {
60 let ms = ms
61 .as_i64()
62 .filter(|n| (1..=30000).contains(n))
63 .context("Bolt transaction_timeout_ms must be1..30000")?;
64 extra.push(("tx_timeout".into(), Value::Int(ms)));
65 }
66 if let Some(bookmarks) = action.get("bookmarks") {
67 let bookmarks = bookmarks
68 .as_array()
69 .filter(|v| v.len() <= 16)
70 .context("Bolt bookmarks must be a bounded list")?;
71 let values = bookmarks
72 .iter()
73 .map(|v| {
74 v.as_str()
75 .filter(|s| !s.is_empty() && s.len() <= 4096)
76 .map(Value::string)
77 .context("Bolt bookmark must be bounded string")
78 })
79 .collect::<Result<Vec<_>>>()?;
80 extra.push(("bookmarks".into(), Value::List(values)));
81 }
82 Ok(Value::Map(extra))
83}
84fn parameter(value: &Json, depth: usize) -> Result<Value> {
85 ensure!(depth <= MAX_DEPTH + 1, "Bolt parameter wire depth limit");
86 if value.is_array() || value.is_object() {
87 ensure!(depth <= MAX_DEPTH, "Bolt parameter wire depth limit");
88 }
89 Ok(match value {
90 Json::Null => Value::Null,
91 Json::Bool(v) => Value::Bool(*v),
92 Json::Number(v) => {
93 if let Some(v) = v.as_i64() {
94 Value::Int(v)
95 } else if v.is_u64() {
96 bail!("Bolt parameters require signed64-bit integers")
97 } else {
98 Value::Float(
99 v.as_f64()
100 .filter(|v| v.is_finite())
101 .context("Bolt finite number required")?,
102 )
103 }
104 }
105 Json::String(v) => {
106 ensure!(v.len() <= MAX_TEXT, "Bolt parameter string limit");
107 Value::string(v)
108 }
109 Json::Array(v) => {
110 ensure!(v.len() <= 10000, "Bolt parameter list limit");
111 Value::List(
112 v.iter()
113 .map(|v| parameter(v, depth + 1))
114 .collect::<Result<_>>()?,
115 )
116 }
117 Json::Object(v) => {
118 ensure!(v.len() <= 256, "Bolt parameter map limit");
119 Value::Map(
120 v.iter()
121 .map(|(k, v)| {
122 ensure!(k.len() <= 4096, "Bolt parameter key limit");
123 Ok((k.clone(), parameter(v, depth + 1)?))
124 })
125 .collect::<Result<_>>()?,
126 )
127 }
128 })
129}
130pub fn action(value: &Json) -> Result<Action> {
131 ensure!(
132 within_budget(value),
133 "Bolt action depth/node/retained-content limit"
134 );
135 Ok(
136 match value
137 .get("type")
138 .and_then(Json::as_str)
139 .context("Bolt action type required")?
140 {
141 "bolt_login" => {
142 let username = text(value, "username", 256, false)?.map(str::to_owned);
143 let password = text(value, "password", MAX_PASSWORD, false)?.map(str::to_owned);
144 ensure!(
145 username.is_some() == password.is_some(),
146 "Bolt basic login requires both username and password; omit both for none"
147 );
148 Action::Login { username, password }
149 }
150 "bolt_logoff" => Action::Logoff,
151 "bolt_run" => {
152 let query = text(value, "query", MAX_TEXT, true)?.unwrap().to_owned();
153 let empty = json!({});
154 let parameters = value.get("parameters").unwrap_or(&empty);
155 ensure!(parameters.is_object(), "Bolt parameters must be an object");
156 Action::Run {
157 query,
158 parameters: parameter(parameters, 2)?,
159 extra: extra(value)?,
160 }
161 }
162 "bolt_pull" => Action::Pull(match value.get("n") {
163 None => 100,
164 Some(n) => n
165 .as_u64()
166 .filter(|n| (1..=MAX_PAGE_RECORDS as u64).contains(n))
167 .context("Bolt pull n must be1..500")? as usize,
168 }),
169 "bolt_discard" => Action::Discard,
170 "bolt_begin" => Action::Begin(extra(value)?),
171 "bolt_commit" => Action::Commit,
172 "bolt_rollback" => Action::Rollback,
173 "bolt_reset" => Action::Reset,
174 "disconnect" => Action::Disconnect,
175 _ => bail!("unknown selected Bolt action"),
176 },
177 )
178}
179pub fn message(tag: u8, fields: Vec<Value>) -> Result<Vec<u8>> {
180 let value = Value::Struct { tag, fields };
181 if !wire_budget(&value) {
182 drop_wire_iteratively(value);
183 bail!("Bolt encoded message depth/node/retained-content limit");
184 }
185 let bytes = packstream::to_bytes(&value);
186 ensure!(
187 bytes.len() <= packstream::MAX_MESSAGE_BYTES,
188 "Bolt message byte limit"
189 );
190 Ok(packstream::chunk(&bytes))
191}
192pub fn auth(username: Option<&str>, password: Option<&str>) -> Value {
193 match (username, password) {
194 (Some(u), Some(p)) => Value::map([
195 ("scheme", Value::string("basic")),
196 ("principal", Value::string(u)),
197 ("credentials", Value::string(p)),
198 ]),
199 _ => Value::map([("scheme", Value::string("none"))]),
200 }
201}
202pub fn hello(minor: u8, username: Option<&str>, password: Option<&str>) -> Result<Vec<u8>> {
203 let mut entries = vec![("user_agent".into(), Value::string("NetGet/0.1"))];
204 if minor >= 3 {
205 entries.push((
206 "bolt_agent".into(),
207 Value::map([
208 ("product", Value::string("NetGet/0.1")),
209 ("language", Value::string("Rust")),
210 ]),
211 ));
212 }
213 if minor == 0 {
214 let Value::Map(auth) = auth(username, password) else {
215 unreachable!()
216 };
217 entries.extend(auth);
218 }
219 message(m::HELLO, vec![Value::Map(entries)])
220}
221pub const PROPOSALS: [u8; 16] = [0, 2, 8, 5, 0, 4, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0];
222pub fn selected_version(bytes: [u8; 4]) -> Result<u8> {
223 ensure!(
224 bytes[0] == 0 && bytes[1] == 0 && bytes[3] == 5 && bytes[2] <= 8 && bytes[2] != 5,
225 "Bolt unsupported negotiated version"
226 );
227 Ok(bytes[2])
228}
229fn drop_wire_iteratively(value: Value) {
230 let mut pending = vec![value];
231 while let Some(value) = pending.pop() {
232 match value {
233 Value::List(values) | Value::Struct { fields: values, .. } => pending.extend(values),
234 Value::Map(values) => pending.extend(values.into_iter().map(|(_, v)| v)),
235 _ => {}
236 }
237 }
238}
239fn wire_budget(value: &Value) -> bool {
240 let mut pending = vec![(value, 1usize)];
241 let mut nodes = 0usize;
242 let mut retained = 0usize;
243 while let Some((v, depth)) = pending.pop() {
244 nodes += 1;
245 retained = retained.saturating_add(std::mem::size_of::<Value>());
246 if nodes > MAX_NODES || retained > MAX_RETAINED_BYTES {
247 return false;
248 }
249 match v {
250 Value::String(s) => retained = retained.saturating_add(s.len()),
251 Value::Bytes(s) => retained = retained.saturating_add(s.len()),
252 Value::Map(v) => {
253 if depth > MAX_DEPTH || v.len() > MAX_NODES.saturating_sub(nodes + pending.len()) {
254 return false;
255 }
256 for (k, v) in v {
257 retained = retained.saturating_add(k.len()).saturating_add(64);
258 pending.push((v, depth + 1));
259 }
260 }
261 Value::List(v) | Value::Struct { fields: v, .. } => {
262 if depth > MAX_DEPTH || v.len() > MAX_NODES.saturating_sub(nodes + pending.len()) {
263 return false;
264 }
265 pending.extend(v.iter().map(|v| (v, depth + 1)));
266 }
267 _ => {}
268 }
269 if retained > MAX_RETAINED_BYTES {
270 return false;
271 }
272 }
273 true
274}
275fn integer(v: &Value) -> Result<i64> {
276 v.as_int().context("Bolt value requires integer")
277}
278fn string(v: &Value) -> Result<&str> {
279 v.as_str()
280 .filter(|s| s.len() <= MAX_TEXT)
281 .context("Bolt value requires bounded string")
282}
283fn map(v: &Value) -> Result<&[(String, Value)]> {
284 match v {
285 Value::Map(v) if v.len() <= 10000 => Ok(v),
286 _ => bail!("Bolt value requires bounded map"),
287 }
288}
289fn list(v: &Value) -> Result<&[Value]> {
290 match v {
291 Value::List(v) if v.len() <= 10000 => Ok(v),
292 _ => bail!("Bolt value requires bounded list"),
293 }
294}
295fn arity(fields: &[Value], n: usize) -> Result<()> {
296 ensure!(fields.len() == n, "Bolt value structure arity");
297 Ok(())
298}
299fn float(v: f64) -> Json {
300 serde_json::Number::from_f64(v).map(Json::Number).unwrap_or_else(||json!({"$float":if v.is_nan(){"NaN"}else if v.is_sign_positive(){"Infinity"}else{"-Infinity"}}))
301}
302fn nanos(v: &Value) -> Result<i64> {
303 let n = integer(v)?;
304 ensure!(
305 (0..1_000_000_000).contains(&n),
306 "Bolt nanosecond fraction range"
307 );
308 Ok(n)
309}
310fn day_nanos(v: &Value) -> Result<i64> {
311 let n = integer(v)?;
312 ensure!(
313 (0..86_400_000_000_000).contains(&n),
314 "Bolt time nanosecond range"
315 );
316 Ok(n)
317}
318fn offset(v: &Value) -> Result<i64> {
319 let n = integer(v)?;
320 ensure!(
321 (-64_800..=64_800).contains(&n),
322 "Bolt timezone offset range"
323 );
324 Ok(n)
325}
326fn graph(v: &Value, tag: u8) -> bool {
327 matches!(v,Value::Struct{tag:t,..} if *t==tag)
328}
329fn value(v: &Value, depth: usize, redactor: Option<&Redactor>) -> Result<Json> {
330 ensure!(depth <= MAX_DEPTH + 1, "Bolt value conversion depth limit");
331 Ok(match v {
332 Value::Null => Json::Null,
333 Value::Bool(v) => json!(v),
334 Value::Int(v) => json!(v),
335 Value::Float(v) => float(*v),
336 Value::String(v) => {
337 ensure!(v.len() <= MAX_TEXT, "Bolt result string limit");
338 json!(shown(v, redactor))
339 }
340 Value::Bytes(v) => json!({"$bytes":{"length":v.len(),"content_omitted":true}}),
341 Value::List(v) => {
342 ensure!(v.len() <= 10000, "Bolt result list limit");
343 Json::Array(
344 v.iter()
345 .map(|v| value(v, depth + 1, redactor))
346 .collect::<Result<_>>()?,
347 )
348 }
349 Value::Map(v) => {
350 ensure!(v.len() <= 10000, "Bolt result map limit");
351 Json::Object(
352 v.iter()
353 .map(|(k, v)| {
354 ensure!(k.len() <= 4096, "Bolt result key limit");
355 Ok((shown(k, redactor), value(v, depth + 1, redactor)?))
356 })
357 .collect::<Result<_>>()?,
358 )
359 }
360 Value::Struct { tag, fields: f } => match tag {
361 0x4e => {
362 arity(f, 4)?;
363 let labels = list(&f[1])?
364 .iter()
365 .map(|v| string(v).map(|s| shown(s, redactor)))
366 .collect::<Result<Vec<_>>>()?;
367 map(&f[2])?;
368 json!({"$node":{"id":integer(&f[0])?,"labels":labels,"properties":value(&f[2],depth+1,redactor)?,"element_id":shown_string(&f[3],redactor)?}})
369 }
370 0x52 => {
371 arity(f, 8)?;
372 map(&f[4])?;
373 json!({"$relationship":{"id":integer(&f[0])?,"start":integer(&f[1])?,"end":integer(&f[2])?,"relationship_type":shown_string(&f[3],redactor)?,"properties":value(&f[4],depth+1,redactor)?,"element_id":shown_string(&f[5],redactor)?,"start_element_id":shown_string(&f[6],redactor)?,"end_element_id":shown_string(&f[7],redactor)?}})
374 }
375 0x72 => {
376 arity(f, 4)?;
377 map(&f[2])?;
378 json!({"$unbound_relationship":{"id":integer(&f[0])?,"relationship_type":shown_string(&f[1],redactor)?,"properties":value(&f[2],depth+1,redactor)?,"element_id":shown_string(&f[3],redactor)?}})
379 }
380 0x50 => {
381 arity(f, 3)?;
382 let nodes = list(&f[0])?;
383 let rels = list(&f[1])?;
384 let indices = list(&f[2])?;
385 ensure!(
386 !nodes.is_empty()
387 && nodes.iter().all(|v| graph(v, 0x4e))
388 && rels.iter().all(|v| graph(v, 0x72))
389 && indices.len() % 2 == 0,
390 "Bolt path table schema"
391 );
392 for pair in indices.chunks_exact(2) {
393 let rel = integer(&pair[0])?;
394 let node = integer(&pair[1])?;
395 ensure!(
396 rel.checked_abs()
397 .is_some_and(|r| r > 0 && (r as u64) <= rels.len() as u64)
398 && node >= 0
399 && (node as u64) < nodes.len() as u64,
400 "Bolt path table index"
401 );
402 }
403 json!({"$path":{"nodes":value(&f[0],depth+1,redactor)?,"relationships":value(&f[1],depth+1,redactor)?,"indices":value(&f[2],depth+1,redactor)?}})
404 }
405 0x44 => {
406 arity(f, 1)?;
407 json!({"$date":{"days_since_epoch":integer(&f[0])?}})
408 }
409 0x54 => {
410 arity(f, 2)?;
411 json!({"$time":{"nanoseconds_since_midnight":day_nanos(&f[0])?,"offset_seconds":offset(&f[1])?}})
412 }
413 0x74 => {
414 arity(f, 1)?;
415 json!({"$local_time":{"nanoseconds_since_midnight":day_nanos(&f[0])?}})
416 }
417 0x49 => {
418 arity(f, 3)?;
419 json!({"$datetime":{"epoch_seconds":integer(&f[0])?,"nanoseconds":nanos(&f[1])?,"offset_seconds":offset(&f[2])?}})
420 }
421 0x69 => {
422 arity(f, 3)?;
423 json!({"$datetime_zone":{"epoch_seconds":integer(&f[0])?,"nanoseconds":nanos(&f[1])?,"zone_id":shown_string(&f[2],redactor)?}})
424 }
425 0x64 => {
426 arity(f, 2)?;
427 json!({"$local_datetime":{"local_epoch_seconds":integer(&f[0])?,"nanoseconds":nanos(&f[1])?}})
428 }
429 0x45 => {
430 arity(f, 4)?;
431 json!({"$duration":{"months":integer(&f[0])?,"days":integer(&f[1])?,"seconds":integer(&f[2])?,"nanoseconds":nanos(&f[3])?}})
432 }
433 0x58 | 0x59 => {
434 arity(f, if *tag == 0x58 { 3 } else { 4 })?;
435 let mut coordinates = Vec::new();
436 for v in &f[1..] {
437 let Value::Float(v) = v else {
438 bail!("Bolt point coordinate requires float")
439 };
440 ensure!(v.is_finite(), "Bolt point coordinate must be finite");
441 coordinates.push(float(*v));
442 }
443 json!({"$point":{"srid":integer(&f[0])?,"coordinates":coordinates}})
444 }
445 _ => bail!("unsupported Bolt value structure"),
446 },
447 })
448}
449#[derive(Debug)]
450pub enum Reply {
451 Success(Json),
452 Record(Vec<Json>),
453 Ignored,
454 Failure(Json),
455}
456pub fn reply(bytes: &[u8], minor: u8) -> Result<Reply> {
457 reply_inner(bytes, minor, None)
458}
459pub fn reply_private(bytes: &[u8], minor: u8, secret: Option<&str>) -> Result<Reply> {
460 let redactor = secret.filter(|v| !v.is_empty()).map(Redactor::new);
461 reply_inner(bytes, minor, redactor.as_ref())
462}
463fn reply_inner(bytes: &[u8], minor: u8, redactor: Option<&Redactor>) -> Result<Reply> {
464 ensure!(
465 bytes.len() <= packstream::MAX_MESSAGE_BYTES,
466 "Bolt inbound byte limit"
467 );
468 let raw = packstream::decode(bytes)
469 .map_err(|_| anyhow::anyhow!("Bolt PackStream schema/depth refusal"))?;
470 ensure!(
471 wire_budget(&raw),
472 "Bolt inbound node/retained-content limit"
473 );
474 let Value::Struct { tag, fields } = raw else {
475 bail!("Bolt response must be structure")
476 };
477 match tag {
478 m::SUCCESS => {
479 arity(&fields, 1)?;
480 map(&fields[0])?;
481 let metadata = value(&fields[0], 2, None)?;
482 for name in [
483 "server",
484 "protocol_version",
485 "connection_id",
486 "bookmark",
487 "db",
488 "advertised_address",
489 ] {
490 if let Some(v) = metadata.get(name) {
491 ensure!(v.is_string(), "Bolt success string schema");
492 }
493 }
494 if let Some(v) = metadata.get("protocol_version") {
495 ensure!(
496 v.as_str() == Some(format!("5.{minor}").as_str()),
497 "Bolt HELLO protocol_version mismatch"
498 );
499 }
500 for name in ["has_more", "credentials_expired"] {
501 if let Some(v) = metadata.get(name) {
502 ensure!(v.is_boolean(), "Bolt success boolean schema");
503 }
504 }
505 for name in ["qid", "t_first", "t_last"] {
506 if let Some(v) = metadata.get(name) {
507 ensure!(
508 v.as_i64().is_some_and(|v| v >= 0),
509 "Bolt success integer schema"
510 );
511 }
512 }
513 if let Some(v) = metadata.get("fields") {
514 ensure!(
515 v.as_array().is_some_and(|v| v.len() <= MAX_FIELDS
516 && v.iter()
517 .all(|v| v.as_str().is_some_and(|s| s.len() <= 4096))),
518 "Bolt success fields schema"
519 );
520 }
521 if let Some(v) = metadata.get("type") {
522 ensure!(
523 matches!(v.as_str(), Some("r" | "w" | "rw" | "s")),
524 "Bolt query type schema"
525 );
526 }
527 for name in ["hints", "stats", "plan", "profile"] {
528 if let Some(v) = metadata.get(name) {
529 ensure!(v.is_object(), "Bolt success map schema");
530 }
531 }
532 if let Some(stats) = metadata.get("stats").and_then(Json::as_object) {
533 for (key, v) in stats {
534 if matches!(key.as_str(), "contains-updates" | "contains-system-updates") {
535 ensure!(v.is_boolean(), "Bolt update flag schema");
536 } else if matches!(
537 key.as_str(),
538 "nodes-created"
539 | "nodes-deleted"
540 | "relationships-created"
541 | "relationships-deleted"
542 | "properties-set"
543 | "labels-added"
544 | "labels-removed"
545 | "indexes-added"
546 | "indexes-removed"
547 | "constraints-added"
548 | "constraints-removed"
549 | "system-updates"
550 ) {
551 ensure!(
552 v.as_i64().is_some_and(|n| n >= 0),
553 "Bolt update counter schema"
554 );
555 }
556 }
557 }
558 for name in ["statuses", "notifications"] {
559 if let Some(v) = metadata.get(name) {
560 ensure!(
561 v.as_array().is_some_and(|v| v.iter().all(Json::is_object)),
562 "Bolt success notification schema"
563 );
564 }
565 }
566 Ok(Reply::Success(if redactor.is_some() {
567 metadata_value(&fields[0], 2, redactor, MetaKind::Success)?
568 } else {
569 metadata
570 }))
571 }
572 m::RECORD => {
573 arity(&fields, 1)?;
574 let records = list(&fields[0])?;
575 ensure!(records.len() <= MAX_FIELDS, "Bolt record width limit");
576 Ok(Reply::Record(
577 records
578 .iter()
579 .map(|v| value(v, 3, redactor))
580 .collect::<Result<_>>()?,
581 ))
582 }
583 m::IGNORED => {
584 arity(&fields, 0)?;
585 Ok(Reply::Ignored)
586 }
587 m::FAILURE => {
588 arity(&fields, 1)?;
589 map(&fields[0])?;
590 let failure = value(&fields[0], 2, None)?;
591 validate_failure(&failure, minor, 1)?;
592 Ok(Reply::Failure(if redactor.is_some() {
593 metadata_value(&fields[0], 2, redactor, MetaKind::Failure)?
594 } else {
595 failure
596 }))
597 }
598 _ => bail!("unsupported Bolt response tag"),
599 }
600}
601fn validate_failure(value: &Json, minor: u8, depth: usize) -> Result<()> {
602 ensure!(depth <= MAX_DEPTH, "Bolt failure cause depth limit");
603 ensure!(
604 value
605 .get("message")
606 .and_then(Json::as_str)
607 .is_some_and(|v| v.len() <= MAX_TEXT),
608 "Bolt failure message schema"
609 );
610 if depth == 1 {
611 text(
612 value,
613 if minor >= 7 { "neo4j_code" } else { "code" },
614 4096,
615 true,
616 )?;
617 }
618 if minor >= 7 {
619 let gql = text(value, "gql_status", 5, true)?.unwrap();
620 ensure!(
621 gql.len() == 5
622 && gql
623 .bytes()
624 .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()),
625 "Bolt GQL status schema"
626 );
627 ensure!(
628 value
629 .get("description")
630 .and_then(Json::as_str)
631 .is_some_and(|v| v.len() <= MAX_TEXT),
632 "Bolt failure description schema"
633 );
634 if let Some(v) = value.get("diagnostic_record") {
635 ensure!(v.is_object(), "Bolt diagnostic_record must be map");
636 }
637 if let Some(cause) = value.get("cause") {
638 ensure!(cause.is_object(), "Bolt cause must be map");
639 validate_failure(cause, minor, depth + 1)?;
640 }
641 }
642 Ok(())
643}
644struct Redactor {
647 forms: [String; 3],
648}
649impl Redactor {
650 fn new(secret: &str) -> Self {
651 let encoded = serde_json::to_string(secret).unwrap();
652 let debug = format!("{secret:?}");
653 Self {
654 forms: [
655 secret.into(),
656 encoded[1..encoded.len() - 1].into(),
657 debug[1..debug.len() - 1].into(),
658 ],
659 }
660 }
661 fn text(&self, text: &str) -> String {
662 if self.forms.iter().any(|s| text.contains(s)) {
663 crate::utils::redact::REDACTED.into()
664 } else {
665 text.into()
666 }
667 }
668}
669fn shown(text: &str, redactor: Option<&Redactor>) -> String {
670 redactor.map_or_else(|| text.into(), |r| r.text(text))
671}
672fn shown_string(v: &Value, redactor: Option<&Redactor>) -> Result<String> {
673 Ok(shown(string(v)?, redactor))
674}
675pub fn redact_text(text: &str, secret: Option<&str>) -> String {
676 secret
677 .filter(|s| !s.is_empty())
678 .map_or_else(|| text.into(), |s| Redactor::new(s).text(text))
679}
680#[derive(Clone, Copy)]
681enum MetaKind {
682 Success,
683 Failure,
684 Stats,
685 Diagnostic,
686}
687fn metadata_value(
688 v: &Value,
689 depth: usize,
690 redactor: Option<&Redactor>,
691 kind: MetaKind,
692) -> Result<Json> {
693 ensure!(depth <= MAX_DEPTH, "Bolt metadata conversion depth limit");
694 let mut result = serde_json::Map::new();
695 for (key, v) in map(v)? {
696 let fixed = match kind {
697 MetaKind::Success => matches!(
698 key.as_str(),
699 "server"
700 | "protocol_version"
701 | "connection_id"
702 | "bookmark"
703 | "db"
704 | "advertised_address"
705 | "has_more"
706 | "credentials_expired"
707 | "qid"
708 | "t_first"
709 | "t_last"
710 | "fields"
711 | "type"
712 | "hints"
713 | "stats"
714 | "plan"
715 | "profile"
716 | "notifications"
717 | "statuses"
718 ),
719 MetaKind::Failure => matches!(
720 key.as_str(),
721 "code"
722 | "message"
723 | "neo4j_code"
724 | "gql_status"
725 | "description"
726 | "diagnostic_record"
727 | "cause"
728 ),
729 MetaKind::Diagnostic => matches!(
730 key.as_str(),
731 "OPERATION"
732 | "OPERATION_CODE"
733 | "CURRENT_SCHEMA"
734 | "_classification"
735 | "_severity"
736 | "_position"
737 ),
738 MetaKind::Stats => matches!(
739 key.as_str(),
740 "nodes-created"
741 | "nodes-deleted"
742 | "relationships-created"
743 | "relationships-deleted"
744 | "properties-set"
745 | "labels-added"
746 | "labels-removed"
747 | "indexes-added"
748 | "indexes-removed"
749 | "constraints-added"
750 | "constraints-removed"
751 | "system-updates"
752 | "contains-updates"
753 | "contains-system-updates"
754 ),
755 };
756 let child = match (kind, key.as_str()) {
757 (MetaKind::Failure, "diagnostic_record") => {
758 metadata_value(v, depth + 1, redactor, MetaKind::Diagnostic)?
759 }
760 (MetaKind::Failure, "cause") => {
761 metadata_value(v, depth + 1, redactor, MetaKind::Failure)?
762 }
763 (MetaKind::Success, "stats") => {
764 metadata_value(v, depth + 1, redactor, MetaKind::Stats)?
765 }
766 _ => value(v, depth + 1, redactor)?,
767 };
768 result.insert(
769 if fixed {
770 key.clone()
771 } else {
772 shown(key, redactor)
773 },
774 child,
775 );
776 }
777 Ok(Json::Object(result))
778}
779
780pub fn retained_size(value: &Json) -> Option<usize> {
782 let mut pending = vec![(value, 0usize)];
783 let mut nodes = 0usize;
784 let mut bytes = 0usize;
785 while let Some((value, depth)) = pending.pop() {
786 nodes += 1;
787 bytes = bytes.saturating_add(std::mem::size_of::<Json>());
788 if nodes > MAX_NODES || depth > MAX_EVENT_DEPTH || bytes > MAX_RETAINED_BYTES {
789 return None;
790 }
791 match value {
792 Json::String(v) => bytes = bytes.saturating_add(v.len()),
793 Json::Array(v) => {
794 if v.len() > MAX_NODES.saturating_sub(nodes + pending.len()) {
795 return None;
796 }
797 pending.extend(v.iter().map(|v| (v, depth + 1)));
798 }
799 Json::Object(v) => {
800 if v.len() > MAX_NODES.saturating_sub(nodes + pending.len()) {
801 return None;
802 }
803 for (k, v) in v {
804 bytes = bytes.saturating_add(k.len()).saturating_add(64);
805 pending.push((v, depth + 1));
806 }
807 }
808 _ => {}
809 }
810 if bytes > MAX_RETAINED_BYTES {
811 return None;
812 }
813 }
814 Some(bytes)
815}