use url::Url;
use crate::{ErrorCode, ErrorPayload, ProtocolVersionError};
pub const A2C_VERSION_QUERY_KEY: &str = "a2c_version";
pub const DEFAULT_HANDSHAKE_TRANSPORTS: [&str; 2] = ["polling", "websocket"];
pub fn build_handshake_url(
base_url: &str,
protocol_version: &str,
) -> Result<String, url::ParseError> {
let mut url = Url::parse(base_url)?;
let preserved: Vec<(String, String)> = url
.query_pairs()
.filter(|(k, _)| k != A2C_VERSION_QUERY_KEY)
.map(|(k, v)| (k.into_owned(), v.into_owned()))
.collect();
{
let mut qp = url.query_pairs_mut();
qp.clear();
for (k, v) in &preserved {
qp.append_pair(k, v);
}
qp.append_pair(A2C_VERSION_QUERY_KEY, protocol_version);
}
Ok(url.to_string())
}
pub fn extract_a2c_version(query_string: &str) -> Option<String> {
url::form_urlencoded::parse(query_string.as_bytes())
.find(|(k, _)| k == A2C_VERSION_QUERY_KEY)
.map(|(_, v)| v.into_owned())
.filter(|v| !v.is_empty())
}
pub fn enforce_polling_first(transports: &[String]) -> Vec<String> {
if transports.is_empty() {
return DEFAULT_HANDSHAKE_TRANSPORTS
.iter()
.map(|s| s.to_string())
.collect();
}
let mut out: Vec<String> = Vec::with_capacity(transports.len() + 1);
out.push("polling".to_string());
for t in transports {
if t != "polling" && !out.iter().any(|x| x == t) {
out.push(t.clone());
}
}
out
}
pub fn apply_polling_first_guard(transports: Option<&[String]>) -> Vec<String> {
match transports {
None => DEFAULT_HANDSHAKE_TRANSPORTS
.iter()
.map(|s| s.to_string())
.collect(),
Some(ts) => enforce_polling_first(ts),
}
}
pub fn extract_4008_payload(body: &str) -> Option<ErrorPayload> {
let payload: ErrorPayload = serde_json::from_str(body).ok()?;
if payload.code == i64::from(ErrorCode::ProtocolVersionMismatch.code()) {
Some(payload)
} else {
None
}
}
pub fn build_protocol_version_error(payload: &ErrorPayload) -> ProtocolVersionError {
ProtocolVersionError {
client_version: payload.client_version.clone(),
server_version: payload.server_version.clone(),
min_supported: payload.min_supported.clone(),
max_supported: payload.max_supported.clone(),
message: payload.message.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pairs(u: &str) -> std::collections::HashMap<String, String> {
Url::parse(u).unwrap().query_pairs().into_owned().collect()
}
#[test]
fn test_build_handshake_url_injects_and_preserves() {
let out = build_handshake_url("http://h:3000/", "0.2.0").unwrap();
assert_eq!(pairs(&out).get("a2c_version").unwrap(), "0.2.0");
let out = build_handshake_url("http://h:3000/smcp?foo=bar&x=1", "0.2.0").unwrap();
let p = pairs(&out);
assert_eq!(p.get("a2c_version").unwrap(), "0.2.0");
assert_eq!(p.get("foo").unwrap(), "bar");
assert_eq!(p.get("x").unwrap(), "1");
}
#[test]
fn test_build_handshake_url_drops_caller_supplied_version() {
let out = build_handshake_url("http://h/?a2c_version=9.9.9&keep=1", "0.2.0").unwrap();
assert_eq!(pairs(&out).get("a2c_version").unwrap(), "0.2.0");
assert_eq!(pairs(&out).get("keep").unwrap(), "1");
let count = Url::parse(&out)
.unwrap()
.query_pairs()
.filter(|(k, _)| k == "a2c_version")
.count();
assert_eq!(count, 1, "a2c_version 不可重复");
}
#[test]
fn test_build_handshake_url_invalid() {
assert!(build_handshake_url("not a url", "0.2.0").is_err());
}
#[test]
fn test_extract_a2c_version() {
assert_eq!(
extract_a2c_version("transport=polling&a2c_version=0.2.0&t=abc").as_deref(),
Some("0.2.0")
);
assert_eq!(
extract_a2c_version("a2c_version=0.2.1").as_deref(),
Some("0.2.1")
);
assert_eq!(extract_a2c_version("transport=polling&t=abc"), None);
assert_eq!(extract_a2c_version("a2c_version="), None);
assert_eq!(extract_a2c_version(""), None);
assert_eq!(
extract_a2c_version("a2c_version=0.2.0&a2c_version=9.9.9").as_deref(),
Some("0.2.0")
);
}
#[test]
fn test_enforce_polling_first() {
assert_eq!(
enforce_polling_first(&["websocket".to_string()]),
vec!["polling".to_string(), "websocket".to_string()]
);
assert_eq!(
enforce_polling_first(&["polling".to_string(), "websocket".to_string()]),
vec!["polling".to_string(), "websocket".to_string()]
);
assert_eq!(
enforce_polling_first(&[
"websocket".to_string(),
"polling".to_string(),
"websocket".to_string()
]),
vec!["polling".to_string(), "websocket".to_string()]
);
assert_eq!(
enforce_polling_first(&[]),
vec!["polling".to_string(), "websocket".to_string()]
);
}
#[test]
fn test_apply_polling_first_guard() {
assert_eq!(
apply_polling_first_guard(None),
vec!["polling".to_string(), "websocket".to_string()]
);
let ws_only = vec!["websocket".to_string()];
assert_eq!(
apply_polling_first_guard(Some(&ws_only)),
vec!["polling".to_string(), "websocket".to_string()]
);
}
#[test]
fn test_extract_4008_payload() {
let body = r#"{"code":4008,"message":"Protocol version mismatch","server_version":"0.2.0","client_version":"0.1.0","min_supported":"0.2.0","max_supported":"0.2.999"}"#;
let p = extract_4008_payload(body).expect("应识别 4008");
assert_eq!(p.code, 4008);
assert_eq!(p.server_version.as_deref(), Some("0.2.0"));
assert_eq!(p.client_version.as_deref(), Some("0.1.0"));
assert!(extract_4008_payload(r#"{"code":400,"message":"Missing a2c_version"}"#).is_none());
assert!(extract_4008_payload(r#"{"code":404,"message":"nope"}"#).is_none());
assert!(extract_4008_payload("not json").is_none());
}
#[test]
fn test_build_protocol_version_error() {
let body = r#"{"code":4008,"message":"Protocol version mismatch","server_version":"0.2.0","client_version":"0.1.0","min_supported":"0.2.0","max_supported":"0.2.999"}"#;
let payload = extract_4008_payload(body).unwrap();
let err = build_protocol_version_error(&payload);
assert_eq!(err.client_version.as_deref(), Some("0.1.0"));
assert_eq!(err.server_version.as_deref(), Some("0.2.0"));
assert_eq!(err.min_supported.as_deref(), Some("0.2.0"));
assert_eq!(err.max_supported.as_deref(), Some("0.2.999"));
let s = err.to_string();
assert!(s.contains("Protocol version mismatch"));
assert!(s.contains("client=0.1.0"));
assert!(s.contains("server=0.2.0"));
}
#[test]
fn test_version_mismatch_roundtrip() {
use crate::ProtocolVersion;
let client = ProtocolVersion::new(0, 1, 0);
let server = ProtocolVersion::new(0, 2, 0);
let payload = ErrorPayload::version_mismatch(&client, &server);
assert_eq!(payload.code, 4008);
assert_eq!(payload.server_version.as_deref(), Some("0.2.0"));
assert_eq!(payload.client_version.as_deref(), Some("0.1.0"));
assert_eq!(payload.min_supported.as_deref(), Some("0.2.0"));
assert_eq!(payload.max_supported.as_deref(), Some("0.2.999"));
let json = serde_json::to_value(&payload).unwrap();
assert_eq!(json["code"], 4008);
assert_eq!(json["server_version"], "0.2.0");
assert!(json.get("error").is_none());
let s = serde_json::to_string(&payload).unwrap();
let back = extract_4008_payload(&s).unwrap();
assert_eq!(back, payload);
}
}