1use super::packstream::Value;
8
9pub const MAGIC: [u8; 4] = [0x60, 0x60, 0xB0, 0x17];
11
12pub const MAJOR: u8 = 5;
14pub const MIN_MINOR: u8 = 0;
16pub const MAX_MINOR: u8 = 8;
17
18pub const HELLO: u8 = 0x01;
20pub const GOODBYE: u8 = 0x02;
21pub const RESET: u8 = 0x0F;
22pub const RUN: u8 = 0x10;
23pub const BEGIN: u8 = 0x11;
24pub const COMMIT: u8 = 0x12;
25pub const ROLLBACK: u8 = 0x13;
26pub const DISCARD: u8 = 0x2F;
27pub const PULL: u8 = 0x3F;
28pub const TELEMETRY: u8 = 0x54;
29pub const ROUTE: u8 = 0x66;
30pub const LOGON: u8 = 0x6A;
31pub const LOGOFF: u8 = 0x6B;
32
33pub const SUCCESS: u8 = 0x70;
35pub const RECORD: u8 = 0x71;
36pub const IGNORED: u8 = 0x7E;
37pub const FAILURE: u8 = 0x7F;
38
39pub fn negotiate(proposals: &[u8; 16]) -> Option<(u8, u8)> {
47 for [_, range, minor, major] in proposals.as_chunks::<4>().0 {
48 if *major != MAJOR {
49 continue;
50 }
51 let lowest = minor.saturating_sub(*range);
54 let chosen = (*minor).min(MAX_MINOR);
55 if chosen >= lowest {
56 return Some((MAJOR, chosen));
57 }
58 }
59 None
60}
61
62#[derive(Debug, Clone)]
64pub enum Request {
65 Hello {
66 extra: Value,
67 },
68 Logon {
69 auth: Value,
70 },
71 Logoff,
72 Goodbye,
73 Reset,
74 Run {
75 query: String,
76 params: Value,
77 extra: Value,
78 },
79 Begin {
80 extra: Value,
81 },
82 Commit,
83 Rollback,
84 Pull {
85 n: i64,
86 qid: i64,
87 },
88 Discard {
89 n: i64,
90 qid: i64,
91 },
92 Route {
93 routing: Value,
94 extra: Value,
95 },
96 Telemetry,
97}
98
99impl Request {
100 pub fn name(&self) -> &'static str {
101 match self {
102 Request::Hello { .. } => "HELLO",
103 Request::Logon { .. } => "LOGON",
104 Request::Logoff => "LOGOFF",
105 Request::Goodbye => "GOODBYE",
106 Request::Reset => "RESET",
107 Request::Run { .. } => "RUN",
108 Request::Begin { .. } => "BEGIN",
109 Request::Commit => "COMMIT",
110 Request::Rollback => "ROLLBACK",
111 Request::Pull { .. } => "PULL",
112 Request::Discard { .. } => "DISCARD",
113 Request::Route { .. } => "ROUTE",
114 Request::Telemetry => "TELEMETRY",
115 }
116 }
117}
118
119fn map_field(fields: &[Value], i: usize) -> Result<Value, String> {
120 match fields.get(i) {
121 Some(v @ Value::Map(_)) => Ok(v.clone()),
122 _ => Err(format!("field {i} must be a map")),
123 }
124}
125
126pub fn parse_request(value: Value) -> Result<Request, String> {
129 let Value::Struct { tag, fields } = value else {
130 return Err("a Bolt message must be a structure".to_string());
131 };
132 let arity = match tag {
133 HELLO | LOGON | BEGIN | PULL | DISCARD | TELEMETRY => 1,
134 GOODBYE | RESET | LOGOFF | COMMIT | ROLLBACK => 0,
135 RUN | ROUTE => 3,
136 _ => return Err(format!("unknown message tag 0x{tag:02X}")),
137 };
138 if fields.len() != arity {
139 return Err(format!("message 0x{tag:02X} requires {arity} fields"));
140 }
141 Ok(match tag {
142 HELLO => Request::Hello {
143 extra: map_field(&fields, 0)?,
144 },
145 LOGON => Request::Logon {
146 auth: map_field(&fields, 0)?,
147 },
148 LOGOFF => Request::Logoff,
149 GOODBYE => Request::Goodbye,
150 RESET => Request::Reset,
151 RUN => {
152 let query = fields
153 .first()
154 .and_then(Value::as_str)
155 .ok_or("RUN needs a query string")?
156 .to_string();
157 Request::Run {
158 query,
159 params: map_field(&fields, 1)?,
160 extra: map_field(&fields, 2)?,
161 }
162 }
163 BEGIN => Request::Begin {
164 extra: map_field(&fields, 0)?,
165 },
166 COMMIT => Request::Commit,
167 ROLLBACK => Request::Rollback,
168 PULL | DISCARD => {
169 let extra = map_field(&fields, 0)?;
170 let n = extra
171 .get("n")
172 .and_then(Value::as_int)
173 .ok_or("'n' must be present and an integer")?;
174 let qid = match extra.get("qid") {
175 None => -1,
176 Some(value) => value
177 .as_int()
178 .filter(|n| *n >= -1)
179 .ok_or("'qid' must be an integer greater than or equal to -1")?,
180 };
181 if n == 0 || n < -1 {
182 return Err(format!("'n' must be positive or -1, got {n}"));
183 }
184 if tag == PULL {
185 Request::Pull { n, qid }
186 } else {
187 Request::Discard { n, qid }
188 }
189 }
190 ROUTE => {
191 match &fields[1] {
192 Value::List(bookmarks) if bookmarks.iter().all(|v| v.as_str().is_some()) => {}
193 _ => return Err("ROUTE bookmarks must be a list of strings".into()),
194 }
195 Request::Route {
196 routing: map_field(&fields, 0)?,
197 extra: map_field(&fields, 2)?,
198 }
199 }
200 TELEMETRY => {
201 if !fields[0].as_int().is_some_and(|n| (0..=3).contains(&n)) {
202 return Err("TELEMETRY requires an API integer from 0 to 3".into());
203 }
204 Request::Telemetry
205 }
206 other => return Err(format!("unknown message tag 0x{other:02X}")),
207 })
208}
209
210pub fn success(metadata: Vec<(&str, Value)>) -> Value {
211 Value::Struct {
212 tag: SUCCESS,
213 fields: vec![Value::map(metadata)],
214 }
215}
216
217pub fn record(values: Vec<Value>) -> Value {
218 Value::Struct {
219 tag: RECORD,
220 fields: vec![Value::List(values)],
221 }
222}
223
224pub fn ignored() -> Value {
225 Value::Struct {
226 tag: IGNORED,
227 fields: Vec::new(),
228 }
229}
230
231pub const GQL_STATUS: &str = "50N42";
236const GQL_DESCRIPTION: &str = "error: general processing exception - unexpected error";
237
238pub fn failure(minor: u8, code: &str, message: &str) -> Value {
242 let metadata = if minor >= 7 {
243 vec![
244 ("gql_status", Value::string(GQL_STATUS)),
245 (
246 "description",
247 Value::String(format!("{GQL_DESCRIPTION}. {message}")),
248 ),
249 ("message", Value::string(message)),
250 ("neo4j_code", Value::string(code)),
251 (
252 "diagnostic_record",
253 Value::map([
254 ("OPERATION", Value::string("")),
255 ("OPERATION_CODE", Value::string("0")),
256 ("CURRENT_SCHEMA", Value::string("/")),
257 ]),
258 ),
259 ]
260 } else {
261 vec![
262 ("code", Value::string(code)),
263 ("message", Value::string(message)),
264 ]
265 };
266 Value::Struct {
267 tag: FAILURE,
268 fields: vec![Value::map(metadata)],
269 }
270}