use super::packstream::Value;
pub const MAGIC: [u8; 4] = [0x60, 0x60, 0xB0, 0x17];
pub const MAJOR: u8 = 5;
pub const MIN_MINOR: u8 = 0;
pub const MAX_MINOR: u8 = 8;
pub const HELLO: u8 = 0x01;
pub const GOODBYE: u8 = 0x02;
pub const RESET: u8 = 0x0F;
pub const RUN: u8 = 0x10;
pub const BEGIN: u8 = 0x11;
pub const COMMIT: u8 = 0x12;
pub const ROLLBACK: u8 = 0x13;
pub const DISCARD: u8 = 0x2F;
pub const PULL: u8 = 0x3F;
pub const TELEMETRY: u8 = 0x54;
pub const ROUTE: u8 = 0x66;
pub const LOGON: u8 = 0x6A;
pub const LOGOFF: u8 = 0x6B;
pub const SUCCESS: u8 = 0x70;
pub const RECORD: u8 = 0x71;
pub const IGNORED: u8 = 0x7E;
pub const FAILURE: u8 = 0x7F;
pub fn negotiate(proposals: &[u8; 16]) -> Option<(u8, u8)> {
for [_, range, minor, major] in proposals.as_chunks::<4>().0 {
if *major != MAJOR {
continue;
}
let lowest = minor.saturating_sub(*range);
let chosen = (*minor).min(MAX_MINOR);
if chosen >= lowest {
return Some((MAJOR, chosen));
}
}
None
}
#[derive(Debug, Clone)]
pub enum Request {
Hello {
extra: Value,
},
Logon {
auth: Value,
},
Logoff,
Goodbye,
Reset,
Run {
query: String,
params: Value,
extra: Value,
},
Begin {
extra: Value,
},
Commit,
Rollback,
Pull {
n: i64,
qid: i64,
},
Discard {
n: i64,
qid: i64,
},
Route {
routing: Value,
extra: Value,
},
Telemetry,
}
impl Request {
pub fn name(&self) -> &'static str {
match self {
Request::Hello { .. } => "HELLO",
Request::Logon { .. } => "LOGON",
Request::Logoff => "LOGOFF",
Request::Goodbye => "GOODBYE",
Request::Reset => "RESET",
Request::Run { .. } => "RUN",
Request::Begin { .. } => "BEGIN",
Request::Commit => "COMMIT",
Request::Rollback => "ROLLBACK",
Request::Pull { .. } => "PULL",
Request::Discard { .. } => "DISCARD",
Request::Route { .. } => "ROUTE",
Request::Telemetry => "TELEMETRY",
}
}
}
fn map_field(fields: &[Value], i: usize) -> Result<Value, String> {
match fields.get(i) {
Some(v @ Value::Map(_)) => Ok(v.clone()),
_ => Err(format!("field {i} must be a map")),
}
}
pub fn parse_request(value: Value) -> Result<Request, String> {
let Value::Struct { tag, fields } = value else {
return Err("a Bolt message must be a structure".to_string());
};
let arity = match tag {
HELLO | LOGON | BEGIN | PULL | DISCARD | TELEMETRY => 1,
GOODBYE | RESET | LOGOFF | COMMIT | ROLLBACK => 0,
RUN | ROUTE => 3,
_ => return Err(format!("unknown message tag 0x{tag:02X}")),
};
if fields.len() != arity {
return Err(format!("message 0x{tag:02X} requires {arity} fields"));
}
Ok(match tag {
HELLO => Request::Hello {
extra: map_field(&fields, 0)?,
},
LOGON => Request::Logon {
auth: map_field(&fields, 0)?,
},
LOGOFF => Request::Logoff,
GOODBYE => Request::Goodbye,
RESET => Request::Reset,
RUN => {
let query = fields
.first()
.and_then(Value::as_str)
.ok_or("RUN needs a query string")?
.to_string();
Request::Run {
query,
params: map_field(&fields, 1)?,
extra: map_field(&fields, 2)?,
}
}
BEGIN => Request::Begin {
extra: map_field(&fields, 0)?,
},
COMMIT => Request::Commit,
ROLLBACK => Request::Rollback,
PULL | DISCARD => {
let extra = map_field(&fields, 0)?;
let n = extra
.get("n")
.and_then(Value::as_int)
.ok_or("'n' must be present and an integer")?;
let qid = match extra.get("qid") {
None => -1,
Some(value) => value
.as_int()
.filter(|n| *n >= -1)
.ok_or("'qid' must be an integer greater than or equal to -1")?,
};
if n == 0 || n < -1 {
return Err(format!("'n' must be positive or -1, got {n}"));
}
if tag == PULL {
Request::Pull { n, qid }
} else {
Request::Discard { n, qid }
}
}
ROUTE => {
match &fields[1] {
Value::List(bookmarks) if bookmarks.iter().all(|v| v.as_str().is_some()) => {}
_ => return Err("ROUTE bookmarks must be a list of strings".into()),
}
Request::Route {
routing: map_field(&fields, 0)?,
extra: map_field(&fields, 2)?,
}
}
TELEMETRY => {
if !fields[0].as_int().is_some_and(|n| (0..=3).contains(&n)) {
return Err("TELEMETRY requires an API integer from 0 to 3".into());
}
Request::Telemetry
}
other => return Err(format!("unknown message tag 0x{other:02X}")),
})
}
pub fn success(metadata: Vec<(&str, Value)>) -> Value {
Value::Struct {
tag: SUCCESS,
fields: vec![Value::map(metadata)],
}
}
pub fn record(values: Vec<Value>) -> Value {
Value::Struct {
tag: RECORD,
fields: vec![Value::List(values)],
}
}
pub fn ignored() -> Value {
Value::Struct {
tag: IGNORED,
fields: Vec::new(),
}
}
pub const GQL_STATUS: &str = "50N42";
const GQL_DESCRIPTION: &str = "error: general processing exception - unexpected error";
pub fn failure(minor: u8, code: &str, message: &str) -> Value {
let metadata = if minor >= 7 {
vec![
("gql_status", Value::string(GQL_STATUS)),
(
"description",
Value::String(format!("{GQL_DESCRIPTION}. {message}")),
),
("message", Value::string(message)),
("neo4j_code", Value::string(code)),
(
"diagnostic_record",
Value::map([
("OPERATION", Value::string("")),
("OPERATION_CODE", Value::string("0")),
("CURRENT_SCHEMA", Value::string("/")),
]),
),
]
} else {
vec![
("code", Value::string(code)),
("message", Value::string(message)),
]
};
Value::Struct {
tag: FAILURE,
fields: vec![Value::map(metadata)],
}
}