1use std::collections::{BTreeMap, HashMap, HashSet};
9use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
10use std::sync::atomic::{AtomicUsize, Ordering};
11use std::sync::{Arc, Mutex, OnceLock};
12use std::time::{Duration, Instant, UNIX_EPOCH};
13
14use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
15use axum::extract::{ConnectInfo, State};
16use axum::http::header::{HOST, ORIGIN};
17use axum::http::{HeaderMap, StatusCode};
18use axum::response::{IntoResponse, Response};
19use base64::Engine;
20use pv::{Listener, Pv, PvManager, PvState, Scheme, Severity, Value};
21use serde_json::{Map, Value as Json, json};
22use tokio::sync::Notify;
23
24pub struct Config {
25 pub write: bool,
27 pub write_token: String,
29 pub allow_origins: Vec<String>,
32 pub hosts: Vec<String>,
36 pub throttle: Duration,
39 pub max_per_ip: usize,
41 pub ping: Duration,
44}
45
46pub struct Hub {
48 manager: PvManager,
49 config: Config,
50 started: String,
51 clients: Mutex<HashMap<IpAddr, usize>>,
53 subscriptions: Arc<AtomicUsize>,
55}
56
57impl Hub {
58 pub fn new(manager: PvManager, config: Config) -> Self {
59 let started = UNIX_EPOCH
60 .elapsed()
61 .unwrap_or_default()
62 .as_secs()
63 .to_string();
64 Self {
65 manager,
66 config,
67 started,
68 clients: Mutex::new(HashMap::new()),
69 subscriptions: Arc::new(AtomicUsize::new(0)),
70 }
71 }
72}
73
74struct ClientSlot(Arc<Hub>, IpAddr);
76
77impl ClientSlot {
78 fn take(hub: &Arc<Hub>, ip: IpAddr) -> Result<Self, String> {
80 let ip = client_key(ip);
81 let mut clients = hub.clients.lock().unwrap();
82 if clients.values().sum::<usize>() >= MAX_CLIENTS {
83 return Err(format!("{MAX_CLIENTS} clients already connected"));
84 }
85 let n = clients.entry(ip).or_default();
86 if *n >= hub.config.max_per_ip {
87 return Err(format!(
88 "{} clients already connected from {ip}",
89 hub.config.max_per_ip
90 ));
91 }
92 *n += 1;
93 Ok(Self(hub.clone(), ip))
94 }
95}
96
97impl Drop for ClientSlot {
98 fn drop(&mut self) {
99 let mut clients = self.0.clients.lock().unwrap();
100 if let Some(n) = clients.get_mut(&self.1) {
101 *n -= 1;
102 if *n == 0 {
103 clients.remove(&self.1);
104 }
105 }
106 }
107}
108
109fn client_key(ip: IpAddr) -> IpAddr {
113 match ip.to_canonical() {
114 IpAddr::V6(v6) => {
115 let net = u128::from(v6) & !((1u128 << 64) - 1);
116 IpAddr::V6(Ipv6Addr::from(net))
117 }
118 v4 => v4,
119 }
120}
121
122const MAX_MESSAGE: usize = 1 << 20;
124const MAX_PVS: usize = 10_000;
126const MAX_CLIENTS: usize = 100;
128const MAX_TOTAL_PVS: usize = 50_000;
130const MAX_NAME: usize = 256;
132const SEND_TIMEOUT: Duration = Duration::from_secs(10);
135const SUBSCRIBE_BURST: f64 = MAX_PVS as f64;
137const SUBSCRIBE_RATE: f64 = 1000.0;
139
140pub async fn socket(
141 State(hub): State<Arc<Hub>>,
142 ConnectInfo(peer): ConnectInfo<SocketAddr>,
143 headers: HeaderMap,
144 ws: WebSocketUpgrade,
145) -> Response {
146 if !browser_allowed(&headers, &hub.config) {
151 let origin = headers.get(ORIGIN).and_then(|o| o.to_str().ok());
152 let host = headers.get(HOST).and_then(|h| h.to_str().ok());
153 log::warn!(
154 "refused a PVWS client {peer} from origin {origin:?} to host {host:?} \
155 (see --allow-origin and --host)"
156 );
157 return StatusCode::FORBIDDEN.into_response();
158 }
159 let slot = match ClientSlot::take(&hub, peer.ip()) {
160 Ok(slot) => slot,
161 Err(why) => {
162 log::warn!("refused PVWS client {peer}: {why}");
163 return StatusCode::SERVICE_UNAVAILABLE.into_response();
164 }
165 };
166 ws.max_message_size(MAX_MESSAGE)
167 .on_upgrade(move |socket| async move {
168 serve(&hub, socket, peer).await;
169 drop(slot);
170 })
171}
172
173fn browser_allowed(headers: &HeaderMap, config: &Config) -> bool {
177 let Some(origin) = headers.get(ORIGIN) else {
178 return true;
179 };
180 let Ok(origin) = origin.to_str() else {
181 return false;
182 };
183 let Some(host) = headers.get(HOST).and_then(|h| h.to_str().ok()) else {
184 return false;
185 };
186 let host = host.to_ascii_lowercase();
187 let name = host_name(&host);
189 if !host_allowed(name.strip_suffix('.').unwrap_or(name), config) {
190 return false;
191 }
192 let origin = normalise_origin(origin);
193 if config
194 .allow_origins
195 .iter()
196 .any(|a| normalise_origin(a) == origin)
197 {
198 return true;
199 }
200 let (scheme, origin_host) = origin.split_once("://").unwrap_or_default();
202 origin_host == strip_default_port(scheme, &host)
203}
204
205fn host_allowed(host: &str, config: &Config) -> bool {
207 if host.parse::<Ipv4Addr>().is_ok()
208 || host
209 .strip_prefix('[')
210 .and_then(|h| h.strip_suffix(']'))
211 .is_some_and(|h| h.parse::<Ipv6Addr>().is_ok())
212 || host == "localhost"
213 {
214 return true;
215 }
216 machine_names().iter().any(|name| name == host)
217 || config.hosts.iter().any(|h| h.eq_ignore_ascii_case(host))
218 || config.allow_origins.iter().any(|a| {
219 let origin = normalise_origin(a);
220 let host_port = origin.split_once("://").map_or(&*origin, |(_, h)| h);
221 host_name(host_port) == host
222 })
223}
224
225fn host_name(host_port: &str) -> &str {
227 match host_port.find(']') {
228 Some(end) if host_port.starts_with('[') => &host_port[..=end],
229 _ => host_port.split(':').next().unwrap_or_default(),
230 }
231}
232
233fn normalise_origin(origin: &str) -> String {
235 let origin = origin.trim_end_matches('/').to_ascii_lowercase();
236 match origin.split_once("://") {
237 Some((scheme, host)) => format!("{scheme}://{}", strip_default_port(scheme, host)),
238 None => origin,
239 }
240}
241
242fn strip_default_port<'a>(scheme: &str, host: &'a str) -> &'a str {
243 let port = match scheme {
244 "http" | "ws" => ":80",
245 "https" | "wss" => ":443",
246 _ => return host,
247 };
248 host.strip_suffix(port).unwrap_or(host)
249}
250
251fn machine_names() -> &'static [String] {
254 static NAMES: OnceLock<Vec<String>> = OnceLock::new();
255 NAMES.get_or_init(|| {
256 let read = |f: &str| std::fs::read_to_string(f).unwrap_or_default();
257 let hostnames = [read("/proc/sys/kernel/hostname"), read("/etc/hostname")];
258 names_of(&hostnames, &resolv_domains(&read("/etc/resolv.conf")))
259 })
260}
261
262fn names_of(hostnames: &[String], domains: &[String]) -> Vec<String> {
265 let mut names = Vec::new();
266 for name in hostnames {
267 let name = name.trim().trim_end_matches('.').to_ascii_lowercase();
268 if name.is_empty() {
269 continue;
270 }
271 let short = name.split('.').next().unwrap_or_default().to_string();
272 names.extend(domains.iter().map(|d| format!("{short}.{d}")));
273 names.push(short);
274 names.push(name);
275 }
276 names.sort();
277 names.dedup();
278 names
279}
280
281fn resolv_domains(conf: &str) -> Vec<String> {
283 conf.lines()
284 .filter_map(|line| {
285 let mut words = line.split_whitespace();
286 matches!(words.next(), Some("domain" | "search")).then_some(words)
287 })
288 .flatten()
289 .map(|d| d.trim_end_matches('.').to_ascii_lowercase())
290 .filter(|d| !d.is_empty())
291 .collect()
292}
293
294pub async fn info(State(hub): State<Arc<Hub>>) -> axum::Json<Json> {
295 axum::Json(json!({
296 "server": format!("bobview-server {}", env!("CARGO_PKG_VERSION")),
297 "start_time": hub.started,
298 "write": hub.config.write,
299 }))
300}
301
302struct Sub {
303 pv: Pv,
304 sent: Option<PvState>,
306 _listener: Listener,
308 _slot: SubSlot,
310}
311
312#[derive(Default)]
314struct Changed {
315 names: Mutex<HashSet<String>>,
316 wake: Notify,
317}
318
319impl Changed {
320 fn mark(&self, name: &str) {
321 let mut names = self.names.lock().unwrap();
322 if !names.contains(name) {
323 names.insert(name.to_string());
324 }
325 drop(names);
326 self.wake.notify_one();
327 }
328
329 fn take(&self) -> HashSet<String> {
330 std::mem::take(&mut *self.names.lock().unwrap())
331 }
332}
333
334#[derive(Default)]
336struct Subs {
337 map: BTreeMap<String, Sub>,
338 changed: Arc<Changed>,
339}
340
341impl Subs {
342 fn add(&mut self, name: &str, pv: Pv, slot: SubSlot) {
345 let changed = Arc::downgrade(&self.changed);
346 let key = name.to_string();
347 let listener: Listener = Arc::new(move |_: &PvState| {
348 if let Some(changed) = changed.upgrade() {
349 changed.mark(&key);
350 }
351 });
352 pv.listen(&listener);
354 let sub = Sub {
355 pv,
356 sent: None,
357 _listener: listener,
358 _slot: slot,
359 };
360 self.map.insert(name.to_string(), sub);
361 }
362}
363
364struct Pace {
367 tokens: f64,
368 at: Instant,
369}
370
371impl Pace {
372 fn new() -> Self {
373 Self {
374 tokens: SUBSCRIBE_BURST,
375 at: Instant::now(),
376 }
377 }
378
379 fn spend(&mut self, n: usize, now: Instant) -> Duration {
381 let earned = now.saturating_duration_since(self.at).as_secs_f64() * SUBSCRIBE_RATE;
382 self.tokens = (self.tokens + earned).min(SUBSCRIBE_BURST) - n as f64;
383 self.at = now;
384 if self.tokens >= 0.0 {
385 Duration::ZERO
386 } else {
387 Duration::from_secs_f64(-self.tokens / SUBSCRIBE_RATE)
388 }
389 }
390}
391
392struct SubSlot(Arc<AtomicUsize>);
393
394impl SubSlot {
395 fn take(total: &Arc<AtomicUsize>) -> Option<Self> {
396 let n = total.fetch_add(1, Ordering::SeqCst);
397 let slot = Self(total.clone());
398 (n < MAX_TOTAL_PVS).then_some(slot)
399 }
400}
401
402impl Drop for SubSlot {
403 fn drop(&mut self) {
404 self.0.fetch_sub(1, Ordering::SeqCst);
405 }
406}
407
408fn valid_name(name: &str) -> bool {
411 !name.is_empty() && name.len() <= MAX_NAME && name.bytes().all(|b| b.is_ascii_graphic())
412}
413
414fn served_scheme(name: &str) -> bool {
419 !matches!(
420 pv::PvName::parse(name).scheme,
421 Scheme::Loc | Scheme::Sim | Scheme::Formula
422 )
423}
424
425fn token_ok(given: &str, token: &str) -> bool {
428 let (a, b) = (given.as_bytes(), token.as_bytes());
429 a.len() == b.len() && a.iter().zip(b).fold(0, |d, (x, y)| d | (x ^ y)) == 0
430}
431
432async fn serve(hub: &Hub, mut ws: WebSocket, peer: SocketAddr) {
433 let mut subs = Subs::default();
434 let changed = subs.changed.clone();
435 let mut pace = Pace::new();
436 let mut last_round = Instant::now();
437 let mut ping = tokio::time::interval(hub.config.ping);
440 let mut heard = Instant::now();
441 loop {
442 tokio::select! {
443 msg = ws.recv() => {
444 heard = Instant::now();
445 match msg {
446 Some(Ok(Message::Text(text))) => {
447 let wait = pace.spend(new_names(&subs, &text), Instant::now());
450 if !wait.is_zero() {
451 log::debug!("pacing PVWS client {peer}'s subscriptions: {wait:?}");
452 tokio::time::sleep(wait).await;
453 }
454 let replies = handle(hub, &mut subs, &text, peer);
455 if !send_all(&mut ws, replies).await
457 || !send_updates(hub, &mut ws, &mut subs).await
458 {
459 break;
460 }
461 }
462 Some(Ok(Message::Close(_))) | Some(Err(_)) | None => break,
463 Some(Ok(_)) => {}
464 }
465 }
466 _ = ping.tick() => {
467 if heard.elapsed() > hub.config.ping * 2 {
468 log::info!("dropping PVWS client {peer}: no answer to pings");
469 break;
470 }
471 if !send(&mut ws, Message::Ping(Default::default())).await {
472 break;
473 }
474 }
475 () = changed.wake.notified() => {
476 tokio::time::sleep_until((last_round + hub.config.throttle).into()).await;
477 last_round = Instant::now();
478 if !send_updates(hub, &mut ws, &mut subs).await {
479 break;
480 }
481 }
482 }
483 }
484 log::debug!("PVWS client {peer} left with {} PVs", subs.map.len());
485}
486
487fn new_names(subs: &Subs, text: &str) -> usize {
489 let Ok(Json::Object(msg)) = serde_json::from_str::<Json>(text) else {
490 return 0;
491 };
492 if !matches!(
493 msg.get("type").and_then(Json::as_str),
494 Some("subscribe" | "monitor")
495 ) {
496 return 0;
497 }
498 let new: HashSet<&str> = msg
499 .get("pvs")
500 .and_then(Json::as_array)
501 .into_iter()
502 .flatten()
503 .filter_map(Json::as_str)
504 .filter(|name| valid_name(name) && served_scheme(name) && !subs.map.contains_key(*name))
505 .collect();
506 new.len().min(MAX_PVS.saturating_sub(subs.map.len()))
508}
509
510async fn send(ws: &mut WebSocket, msg: Message) -> bool {
512 matches!(
513 tokio::time::timeout(SEND_TIMEOUT, ws.send(msg)).await,
514 Ok(Ok(()))
515 )
516}
517
518fn handle(hub: &Hub, subs: &mut Subs, text: &str, peer: SocketAddr) -> Vec<String> {
520 let Ok(Json::Object(msg)) = serde_json::from_str::<Json>(text) else {
521 return vec![error(format!("cannot parse {text:.100}"))];
522 };
523 let names = || {
524 msg.get("pvs")
525 .and_then(Json::as_array)
526 .into_iter()
527 .flatten()
528 .filter_map(Json::as_str)
529 };
530 match msg.get("type").and_then(Json::as_str) {
531 Some("subscribe" | "monitor") => {
532 let mut refused = 0;
533 let mut refused_total = 0;
534 let mut invalid = 0;
535 let mut local = 0;
536 for name in names() {
537 if let Some(sub) = subs.map.get_mut(name) {
539 sub.sent = None;
540 subs.changed.mark(name);
541 } else if !valid_name(name) {
542 invalid += 1;
543 } else if !served_scheme(name) {
544 local += 1;
545 } else if subs.map.len() >= MAX_PVS {
546 refused += 1;
547 } else if let Some(slot) = SubSlot::take(&hub.subscriptions) {
548 subs.add(name, hub.manager.subscribe(name), slot);
549 } else {
550 refused_total += 1;
551 }
552 }
553 if invalid > 0 {
554 log::warn!("PVWS client {peer} asked for {invalid} invalid PV names");
555 return vec![error(format!(
556 "Cannot subscribe to {invalid} PVs: names are at most {MAX_NAME} \
557 printable characters with no spaces"
558 ))];
559 }
560 if local > 0 {
561 log::warn!("PVWS client {peer} asked for {local} loc://, sim:// or formula PVs");
562 return vec![error(format!(
563 "Cannot subscribe to {local} PVs: loc:// and sim:// PVs and formulas \
564 are made by the page, not this server"
565 ))];
566 }
567 if refused > 0 {
568 return vec![error(format!(
569 "Cannot subscribe to {refused} more PVs: at most {MAX_PVS} per client"
570 ))];
571 }
572 if refused_total > 0 {
573 return vec![error(format!(
574 "Cannot subscribe to {refused_total} more PVs: the server is at its \
575 limit of {MAX_TOTAL_PVS} PVs for all clients"
576 ))];
577 }
578 vec![]
579 }
580 Some("clear") => {
581 for name in names() {
582 subs.map.remove(name);
583 }
584 vec![]
585 }
586 Some("list") => {
587 vec![json!({"type": "list", "pvs": subs.map.keys().collect::<Vec<_>>()}).to_string()]
588 }
589 Some("write") => {
590 let name = msg.get("pv").and_then(Json::as_str).unwrap_or_default();
591 let Some(sub) = subs.map.get(name) else {
592 return vec![error(format!("Cannot write unknown PV {name}"))];
593 };
594 if !hub.config.write {
595 return vec![write_error(
596 name,
597 "writes are disabled on this server (--write)",
598 )];
599 }
600 let given = msg.get("token").and_then(Json::as_str).unwrap_or_default();
601 if !token_ok(given, &hub.config.write_token) {
602 log::warn!("refused a write to {name} from {peer}: bad token");
603 return vec![write_error(name, "bad or missing token")];
604 }
605 let Some(value) = msg.get("value").and_then(write_text) else {
606 return vec![write_error(name, "no value")];
607 };
608 if sub.pv.dropped() {
611 return vec![write_error(name, "not connected")];
612 }
613 log::info!("write {name} = {value} from {peer}");
614 if msg.get("long_string").and_then(Json::as_bool) == Some(true) {
617 sub.pv.write_long_string(value);
618 } else {
619 sub.pv.write(value);
620 }
621 vec![]
622 }
623 Some("echo") => vec![text.to_string()],
624 Some("ping") => vec![],
625 other => vec![error(format!("unknown message type {other:?}"))],
626 }
627}
628
629fn error(message: String) -> String {
630 json!({"type": "error", "message": message}).to_string()
631}
632
633fn write_error(name: &str, why: &str) -> String {
636 json!({
637 "type": "error",
638 "pv": name,
639 "message": format!("Cannot write {name}: {why}"),
640 "write_error": why,
641 })
642 .to_string()
643}
644
645fn write_text(value: &Json) -> Option<String> {
648 match value {
649 Json::String(s) => Some(s.clone()),
650 Json::Number(n) => Some(match n.as_f64() {
651 Some(x) if x.fract() == 0.0 && x.abs() < 1e15 => (x as i64).to_string(),
652 _ => n.to_string(),
653 }),
654 Json::Bool(b) => Some(u8::from(*b).to_string()),
655 _ => None,
656 }
657}
658
659async fn send_all(ws: &mut WebSocket, messages: Vec<String>) -> bool {
660 for msg in messages {
661 if !send(ws, Message::Text(msg.into())).await {
662 return false;
663 }
664 }
665 true
666}
667
668async fn send_updates(hub: &Hub, ws: &mut WebSocket, subs: &mut Subs) -> bool {
670 send_all(ws, updates(hub, subs)).await
671}
672
673fn updates(hub: &Hub, subs: &mut Subs) -> Vec<String> {
675 let mut messages = Vec::new();
676 for name in subs.changed.take() {
677 let Some(sub) = subs.map.get_mut(&name) else {
678 continue;
679 };
680 let state = sub.pv.state();
681 if let Some(msg) = update(&name, &state, sub.sent.as_ref(), hub.config.write) {
682 messages.push(msg);
683 }
684 sub.sent = Some(state);
685 }
686 messages
687}
688
689fn update(name: &str, s: &PvState, last: Option<&PvState>, write: bool) -> Option<String> {
692 let mut m = Map::new();
693 let readonly = !(write && s.writable);
694 let live = |s: &PvState| s.connected && s.value != Value::None;
697 let was_connected = last.is_some_and(live);
698 if last.is_none_or(|l| readonly != !(write && l.writable)) {
699 m.insert("readonly".into(), json!(readonly));
700 }
701 if !live(s) {
702 if was_connected {
703 m.insert("severity".into(), json!("UNDEFINED"));
705 }
706 } else {
707 let first = !was_connected;
708 let changed = |f: &dyn Fn(&PvState) -> bool| first || last.is_some_and(f);
709 if first {
710 m.insert("vtype".into(), json!(vtype(&s.value)));
711 }
712 if changed(&|l| l.units != s.units) && !matches!(s.value, Value::Enum(_)) {
713 m.insert("units".into(), json!(s.units));
714 }
715 if let Some(p) = s
716 .precision
717 .filter(|_| changed(&|l| l.precision != s.precision))
718 {
719 m.insert("precision".into(), json!(p));
720 }
721 if let Some((lo, hi)) = s
722 .display_range
723 .filter(|_| changed(&|l| l.display_range != s.display_range))
724 {
725 m.insert("min".into(), number(lo));
726 m.insert("max".into(), number(hi));
727 }
728 let limits = [
731 (s.warning_range, "warn_low", "warn_high"),
732 (s.alarm_range, "alarm_low", "alarm_high"),
733 (s.control_range, "ctrl_low", "ctrl_high"),
734 ];
735 let last_limits = last.map(|l| [l.warning_range, l.alarm_range, l.control_range]);
736 for (i, (range, lo_key, hi_key)) in limits.into_iter().enumerate() {
737 let Some((lo, hi)) = range else { continue };
738 if first || last_limits.is_some_and(|l| !same_range(l[i], range)) {
739 m.insert(lo_key.into(), number(lo));
740 m.insert(hi_key.into(), number(hi));
741 }
742 }
743 if matches!(s.value, Value::Enum(_)) && changed(&|l| l.labels != s.labels) {
744 m.insert("labels".into(), json!(s.labels));
745 }
746 if changed(&|l| l.severity != s.severity) {
747 m.insert("severity".into(), json!(severity(s.severity)));
748 }
749 if changed(&|l| !same_value(&l.value, &s.value)) {
750 encode_value(&mut m, s);
751 }
752 if let Some(t) = s
753 .timestamp
754 .filter(|_| changed(&|l| l.timestamp != s.timestamp))
755 {
756 let t = t.duration_since(UNIX_EPOCH).unwrap_or_default();
757 m.insert("seconds".into(), json!(t.as_secs()));
758 m.insert("nanos".into(), json!(t.subsec_nanos()));
759 }
760 }
761 if m.is_empty() {
762 return None;
763 }
764 let mut msg = Map::new();
765 msg.insert("type".into(), json!("update"));
766 msg.insert("pv".into(), json!(name));
767 msg.extend(m);
768 Some(Json::Object(msg).to_string())
769}
770
771fn same_range(a: Option<(f64, f64)>, b: Option<(f64, f64)>) -> bool {
774 let same = |x: f64, y: f64| x == y || (x.is_nan() && y.is_nan());
775 match (a, b) {
776 (Some(a), Some(b)) => same(a.0, b.0) && same(a.1, b.1),
777 (None, None) => true,
778 _ => false,
779 }
780}
781
782fn same_value(a: &Value, b: &Value) -> bool {
785 match (a, b) {
786 (Value::Double(x), Value::Double(y)) => x == y || (x.is_nan() && y.is_nan()),
787 (Value::DoubleArray(x), Value::DoubleArray(y)) => Arc::ptr_eq(x, y) || x == y,
788 (Value::LongArray(x), Value::LongArray(y)) => Arc::ptr_eq(x, y) || x == y,
789 (Value::StrArray(x), Value::StrArray(y)) => Arc::ptr_eq(x, y) || x == y,
790 (Value::Image(x), Value::Image(y)) => Arc::ptr_eq(x, y) || x == y,
791 _ => a == b,
792 }
793}
794
795fn vtype(value: &Value) -> &'static str {
796 match value {
797 Value::Double(_) | Value::None => "VDouble",
798 Value::Long(_) => "VLong",
799 Value::Str(_) => "VString",
800 Value::Enum(_) => "VEnum",
801 Value::DoubleArray(_) => "VDoubleArray",
802 Value::LongArray(a) if fits_i32(a) => "VIntArray",
803 Value::LongArray(_) => "VDoubleArray",
804 Value::StrArray(_) => "VStringArray",
805 Value::Image(_) => "VImage",
806 }
807}
808
809fn fits_i32(a: &[i64]) -> bool {
810 a.iter().all(|&v| i32::try_from(v).is_ok())
811}
812
813fn severity(s: Severity) -> &'static str {
814 match s {
815 Severity::NoAlarm => "NONE",
816 Severity::Minor => "MINOR",
817 Severity::Major => "MAJOR",
818 Severity::Invalid => "INVALID",
819 }
820}
821
822fn number(x: f64) -> Json {
824 if x.is_finite() {
825 json!(x)
826 } else {
827 json!("NaN")
828 }
829}
830
831fn encode_value(m: &mut Map<String, Json>, s: &PvState) {
832 let b64 = |bytes: Vec<u8>| json!(base64::engine::general_purpose::STANDARD.encode(bytes));
833 match &s.value {
834 Value::None => {}
835 Value::Double(v) => {
836 m.insert("value".into(), number(*v));
837 }
838 Value::Long(v) => {
839 m.insert("value".into(), json!(v));
840 }
841 Value::Str(t) => {
842 m.insert("text".into(), json!(t));
843 }
844 Value::Enum(i) => {
845 m.insert("value".into(), json!(i));
846 let label = s
847 .labels
848 .get(*i as usize)
849 .cloned()
850 .unwrap_or_else(|| i.to_string());
851 m.insert("text".into(), json!(label));
852 }
853 Value::DoubleArray(a) => {
854 m.insert(
855 "b64dbl".into(),
856 b64(a.iter().flat_map(|v| v.to_le_bytes()).collect()),
857 );
858 }
859 Value::LongArray(a) if fits_i32(a) => {
860 let bytes = a.iter().flat_map(|&v| (v as i32).to_le_bytes()).collect();
861 m.insert("b64int".into(), b64(bytes));
862 }
863 Value::LongArray(a) => {
864 let bytes = a.iter().flat_map(|&v| (v as f64).to_le_bytes()).collect();
865 m.insert("b64dbl".into(), b64(bytes));
866 }
867 Value::StrArray(a) => {
868 m.insert("text".into(), json!(a.join(", ")));
872 m.insert("strings".into(), json!(a.as_ref()));
873 }
874 Value::Image(img) => {
875 m.insert(
877 "text".into(),
878 json!(format!("image {}x{}", img.width, img.height)),
879 );
880 }
881 }
882}
883
884#[cfg(test)]
885mod tests {
886 use super::*;
887
888 fn parse(msg: Option<String>) -> Map<String, Json> {
889 match serde_json::from_str(&msg.expect("an update")).unwrap() {
890 Json::Object(m) => m,
891 _ => panic!("not an object"),
892 }
893 }
894
895 fn double(v: f64) -> PvState {
896 PvState {
897 connected: true,
898 value: Value::Double(v),
899 units: "mA".into(),
900 precision: Some(3),
901 writable: true,
902 ..Default::default()
903 }
904 }
905
906 #[test]
907 fn first_update_has_metadata_then_only_changes() {
908 let a = double(1.5);
909 let m = parse(update("X", &a, None, true));
910 assert_eq!(m["vtype"], "VDouble");
911 assert_eq!(m["units"], "mA");
912 assert_eq!(m["severity"], "NONE");
913 assert_eq!(m["value"], 1.5);
914 assert_eq!(m["readonly"], false);
915
916 let b = double(2.5);
917 let m = parse(update("X", &b, Some(&a), true));
918 let mut keys: Vec<_> = m.keys().collect();
919 keys.sort();
920 assert_eq!(keys, ["pv", "type", "value"]);
921 assert!(update("X", &b, Some(&b), true).is_none());
922 }
923
924 #[test]
925 fn unreachable_pv_has_no_value() {
926 let m = parse(update("X", &PvState::default(), None, true));
927 assert_eq!(m["readonly"], true);
928 assert!(!m.contains_key("value") && !m.contains_key("text"));
929 let a = double(1.0);
931 let gone = PvState {
932 writable: true,
933 ..Default::default()
934 };
935 let m = parse(update("X", &gone, Some(&a), true));
936 assert_eq!(m["severity"], "UNDEFINED");
937 assert!(update("X", &gone, Some(&gone), true).is_none());
938 }
939
940 #[test]
941 fn metadata_comes_with_the_first_value() {
942 let empty = PvState {
944 connected: true,
945 writable: true,
946 ..Default::default()
947 };
948 let m = parse(update("X", &empty, None, true));
949 assert!(!m.contains_key("vtype") && !m.contains_key("severity"));
950 assert!(update("X", &empty, Some(&empty), true).is_none());
951
952 let m = parse(update("X", &double(1.0), Some(&empty), true));
953 assert_eq!(m["vtype"], "VDouble");
954 assert_eq!(m["units"], "mA");
955 assert_eq!(m["value"], 1.0);
956 }
957
958 #[test]
959 fn no_change_no_update() {
960 let states = [
961 PvState::default(),
962 PvState {
963 connected: true,
964 ..Default::default()
965 },
966 double(1.0),
967 double(f64::NAN),
968 PvState {
969 connected: true,
970 value: Value::DoubleArray(vec![f64::NAN, 1.0].into()),
971 ..Default::default()
972 },
973 ];
974 for s in &states {
975 for write in [false, true] {
976 assert!(update("X", s, Some(s), write).is_none(), "{s:?}");
977 }
978 }
979 }
980
981 #[test]
982 fn limits_and_string_arrays() {
983 let mut a = PvState {
984 alarm_range: Some((f64::NAN, f64::NAN)),
985 warning_range: Some((1.0, 9.0)),
986 control_range: Some((0.0, 10.0)),
987 ..double(5.0)
988 };
989 let m = parse(update("X", &a, None, false));
990 assert_eq!(m["warn_low"], 1.0);
991 assert_eq!(m["warn_high"], 9.0);
992 assert_eq!(m["alarm_low"], "NaN");
993 assert_eq!(m["ctrl_high"], 10.0);
994 assert!(update("X", &a.clone(), Some(&a), false).is_none());
996 let before = a.clone();
997 a.control_range = Some((0.0, 20.0));
998 let m = parse(update("X", &a, Some(&before), false));
999 assert_eq!(m["ctrl_high"], 20.0);
1000 assert!(!m.contains_key("warn_low"));
1001
1002 let s = PvState {
1003 connected: true,
1004 value: Value::StrArray(vec!["a, b".into(), "c".into()].into()),
1005 ..Default::default()
1006 };
1007 let m = parse(update("X", &s, None, false));
1008 assert_eq!(m["vtype"], "VStringArray");
1009 assert_eq!(m["strings"], json!(["a, b", "c"]));
1010 assert_eq!(m["text"], "a, b, c");
1011 assert!(!m.contains_key("value"), "PVWS sends no value for these");
1012 }
1013
1014 #[test]
1015 fn writes_off_means_readonly() {
1016 let m = parse(update("X", &double(1.0), None, false));
1017 assert_eq!(m["readonly"], true);
1018 }
1019
1020 #[test]
1021 fn enums_and_arrays() {
1022 let e = PvState {
1023 connected: true,
1024 value: Value::Enum(1),
1025 labels: vec!["Off".into(), "On".into()],
1026 ..Default::default()
1027 };
1028 let m = parse(update("X", &e, None, false));
1029 assert_eq!(m["vtype"], "VEnum");
1030 assert_eq!(m["value"], 1);
1031 assert_eq!(m["text"], "On");
1032 assert_eq!(m["labels"], json!(["Off", "On"]));
1033
1034 let a = PvState {
1035 connected: true,
1036 value: Value::LongArray(vec![1, -2].into()),
1037 ..Default::default()
1038 };
1039 let m = parse(update("X", &a, None, false));
1040 let bytes = base64::engine::general_purpose::STANDARD
1041 .decode(m["b64int"].as_str().unwrap())
1042 .unwrap();
1043 assert_eq!(bytes, [1, 0, 0, 0, 0xfe, 0xff, 0xff, 0xff]);
1044 }
1045
1046 fn config(allow_origins: &[&str], hosts: &[&str]) -> Config {
1047 Config {
1048 write: true,
1049 write_token: "secret".into(),
1050 allow_origins: allow_origins.iter().map(|s| s.to_string()).collect(),
1051 hosts: hosts.iter().map(|s| s.to_string()).collect(),
1052 throttle: Duration::ZERO,
1053 max_per_ip: 10,
1054 ping: Duration::from_secs(30),
1055 }
1056 }
1057
1058 fn headers(host: &str, origin: Option<&str>) -> HeaderMap {
1059 let mut h = HeaderMap::new();
1060 h.insert(HOST, host.parse().unwrap());
1061 if let Some(o) = origin {
1062 h.insert(ORIGIN, o.parse().unwrap());
1063 }
1064 h
1065 }
1066
1067 #[test]
1068 fn origins() {
1069 let none = config(&[], &["server"]);
1070 let allowed = config(&["https://Other.example.com:443/"], &["server"]);
1071 let ok = |host, origin, config: &Config| browser_allowed(&headers(host, origin), config);
1072 assert!(ok("server:8080", None, &none));
1073 assert!(ok("server:8080", Some("http://server:8080"), &none));
1074 assert!(ok("Server:8080", Some("http://server:8080"), &none));
1075 assert!(!ok("server:8080", Some("http://evil:8080"), &none));
1076 assert!(!ok("server:8080", Some("http://server"), &none));
1077 assert!(ok("server", Some("http://server"), &none));
1078 assert!(ok("server:80", Some("http://server"), &none));
1079 assert!(!ok("server:8080", Some("https://other.example.com"), &none));
1080 assert!(ok(
1081 "server:8080",
1082 Some("https://other.example.com"),
1083 &allowed
1084 ));
1085 assert!(!ok(
1087 "evil.example:8080",
1088 Some("http://evil.example:8080"),
1089 &none
1090 ));
1091 assert!(ok("evil.example:8080", None, &none));
1093 assert!(ok("127.0.0.1:8080", Some("http://127.0.0.1:8080"), &none));
1094 assert!(ok("[::1]:8080", Some("http://[::1]:8080"), &none));
1095 assert!(ok("localhost:8080", Some("http://localhost:8080"), &none));
1096 assert!(ok("localhost.:8080", Some("http://localhost.:8080"), &none));
1098 assert!(ok("server.", Some("http://server."), &none));
1099 assert!(!ok(
1100 "evil.example.:8080",
1101 Some("http://evil.example.:8080"),
1102 &none
1103 ));
1104 assert!(!ok("server..", Some("http://server.."), &none));
1105 assert!(!ok("[evil]:8080", Some("http://[evil]:8080"), &none));
1106 assert!(ok(
1108 "other.example.com",
1109 Some("http://other.example.com"),
1110 &allowed
1111 ));
1112 }
1113
1114 #[test]
1115 fn machine_names_are_hosts() {
1116 let config = config(&[], &[]);
1117 for name in machine_names() {
1118 assert!(host_allowed(name, &config), "{name}");
1119 let short = name.split('.').next().unwrap();
1120 assert!(!host_allowed(&format!("{short}.evil.com"), &config));
1122 assert!(!host_allowed(&format!("{short}x"), &config));
1123 }
1124 }
1125
1126 #[test]
1127 fn only_exact_machine_names() {
1128 let names = names_of(
1129 &["WS01.site.org\n".into(), "ws01".into(), String::new()],
1130 &resolv_domains("# a comment\ndomain site.org\nsearch Other.org. site.org\n"),
1131 );
1132 assert_eq!(
1133 names,
1134 ["ws01", "ws01.other.org", "ws01.site.org"].map(String::from)
1135 );
1136 let names = names_of(&["ws01".into()], &[]);
1137 assert_eq!(names, ["ws01"]);
1138 assert_eq!(
1139 resolv_domains("nameserver 10.0.0.1\n"),
1140 Vec::<String>::new()
1141 );
1142 }
1143
1144 #[test]
1145 fn clients_are_counted_by_v4_address_or_v6_network() {
1146 let ip = |s: &str| client_key(s.parse().unwrap());
1147 assert_eq!(ip("192.0.2.1"), ip("::ffff:192.0.2.1"));
1148 assert_ne!(ip("192.0.2.1"), ip("192.0.2.2"));
1149 assert_eq!(ip("2001:db8:1:2::1"), ip("2001:db8:1:2:ffff::9"));
1150 assert_eq!(ip("2001:db8:1:2::1"), ip("2001:db8:1:2::"));
1151 assert_ne!(ip("2001:db8:1:2::1"), ip("2001:db8:1:3::1"));
1152 }
1153
1154 #[test]
1155 fn one_v6_network_shares_a_cap() {
1156 let hub = Arc::new(Hub::new(PvManager::new(), config(&[], &[])));
1157 let slots: Vec<_> = (0..10u16)
1158 .map(|n| {
1159 let ip = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 1, 0, 0, 0, n));
1160 ClientSlot::take(&hub, ip).expect("under the cap")
1161 })
1162 .collect();
1163 let next = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 1, 9, 9, 9, 9));
1164 assert!(ClientSlot::take(&hub, next).is_err());
1165 let other = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 2, 0, 0, 0, 1));
1166 assert!(ClientSlot::take(&hub, other).is_ok());
1167 drop(slots);
1168 }
1169
1170 #[test]
1171 fn subscriptions_are_paced() {
1172 let t0 = Instant::now();
1173 let mut pace = Pace::new();
1174 assert_eq!(pace.spend(MAX_PVS, t0), Duration::ZERO);
1175 let wait = pace.spend(500, t0);
1177 assert!((wait.as_secs_f64() - 0.5).abs() < 1e-9, "{wait:?}");
1178 let later = t0 + Duration::from_secs(20);
1180 assert_eq!(pace.spend(MAX_PVS, later), Duration::ZERO);
1181 assert!(pace.spend(1, later) > Duration::ZERO);
1182 }
1183
1184 #[test]
1185 fn pacing_counts_only_pvs_that_would_be_subscribed() {
1186 let mut subs = Subs::default();
1187 let total = Arc::new(AtomicUsize::new(0));
1188 let pvs = PvManager::new();
1189 subs.add(
1190 "HAVE",
1191 pvs.subscribe("loc://pace_have"),
1192 SubSlot::take(&total).unwrap(),
1193 );
1194 let msg = |names: Vec<&str>| json!({"type": "subscribe", "pvs": names}).to_string();
1195 let mut names = vec!["DUP:X"; 30_000];
1196 names.extend([
1197 "HAVE",
1198 "bad name",
1199 "loc://x",
1200 "sim://sine",
1201 "NEW",
1202 "pva://NEW",
1203 ]);
1204 assert_eq!(new_names(&subs, &msg(names)), 3);
1205 let many: Vec<String> = (0..MAX_PVS + 5).map(|i| format!("PV{i}")).collect();
1207 let many = msg(many.iter().map(String::as_str).collect());
1208 assert_eq!(new_names(&subs, &many), MAX_PVS - 1);
1209 assert_eq!(new_names(&subs, r#"{"type": "clear", "pvs": ["A"]}"#), 0);
1210 }
1211
1212 #[test]
1213 fn local_and_simulated_pvs_are_refused() {
1214 let hub = Hub::new(PvManager::new(), config(&[], &[]));
1215 let peer: SocketAddr = "192.0.2.1:5000".parse().unwrap();
1216 let mut subs = Subs::default();
1217 for name in ["loc://x(1)", "sim://sine", "loc://y", "=pv('loc://x')"] {
1218 let msg = json!({"type": "subscribe", "pvs": [name]}).to_string();
1219 let replies = handle(&hub, &mut subs, &msg, peer);
1220 assert!(replies[0].contains("loc:// and sim://"), "{replies:?}");
1221 }
1222 assert!(subs.map.is_empty());
1223 assert_eq!(hub.subscriptions.load(Ordering::SeqCst), 0);
1224 assert!(served_scheme("ca://X") && served_scheme("pva://X") && served_scheme("X"));
1225 }
1226
1227 #[test]
1228 fn only_changed_pvs_are_sent() {
1229 let hub = Hub::new(PvManager::new(), config(&[], &[]));
1230 let mut subs = Subs::default();
1231 let total = Arc::new(AtomicUsize::new(0));
1232 let a = hub.manager.subscribe("loc://changed_a(1)");
1233 let b = hub.manager.subscribe("loc://changed_b(1)");
1234 subs.add("a", a.clone(), SubSlot::take(&total).unwrap());
1235 subs.add("b", b.clone(), SubSlot::take(&total).unwrap());
1236 assert_eq!(updates(&hub, &mut subs).len(), 2);
1238 assert!(updates(&hub, &mut subs).is_empty());
1239 a.write("2");
1240 let sent = updates(&hub, &mut subs);
1241 assert_eq!(sent.len(), 1);
1242 assert!(sent[0].contains("\"pv\":\"a\""), "{sent:?}");
1243 subs.map.remove("b");
1245 b.write("3");
1246 assert!(subs.changed.take().is_empty());
1247 assert_eq!(total.load(Ordering::SeqCst), 1);
1248 }
1249
1250 #[test]
1251 fn normalised_origins() {
1252 assert_eq!(
1253 normalise_origin("HTTPS://Host.Example:443/"),
1254 "https://host.example"
1255 );
1256 assert_eq!(normalise_origin("http://host:80"), "http://host");
1257 assert_eq!(normalise_origin("http://host:8080"), "http://host:8080");
1258 assert_eq!(normalise_origin("https://host:80"), "https://host:80");
1259 }
1260
1261 fn start(config: Config) -> (Arc<Hub>, SocketAddr, PvManager) {
1263 let manager = PvManager::new();
1264 let hub = Arc::new(Hub::new(manager.clone(), config));
1265 let app = axum::Router::new()
1266 .route("/pvws/pv", axum::routing::get(socket))
1267 .with_state(hub.clone())
1268 .into_make_service_with_connect_info::<SocketAddr>();
1269 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1270 listener.set_nonblocking(true).unwrap();
1271 let addr = listener.local_addr().unwrap();
1272 manager.runtime().spawn(async move {
1273 let listener = tokio::net::TcpListener::from_std(listener).unwrap();
1274 axum::serve(listener, app).await.unwrap();
1275 });
1276 (hub, addr, manager)
1277 }
1278
1279 async fn upgrade(addr: SocketAddr, origin: Option<&str>) -> (tokio::net::TcpStream, String) {
1281 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
1282 let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
1283 let origin = origin.map_or(String::new(), |o| format!("Origin: {o}\r\n"));
1284 let request = format!(
1285 "GET /pvws/pv HTTP/1.1\r\nHost: {addr}\r\n{origin}Connection: Upgrade\r\n\
1286 Upgrade: websocket\r\nSec-WebSocket-Version: 13\r\n\
1287 Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n"
1288 );
1289 stream.write_all(request.as_bytes()).await.unwrap();
1290 let mut status = String::new();
1291 BufReader::new(&mut stream)
1292 .read_line(&mut status)
1293 .await
1294 .unwrap();
1295 (stream, status)
1296 }
1297
1298 #[test]
1299 fn origin_is_checked_without_writes() {
1300 let mut read_only = config(&[], &[]);
1301 read_only.write = false;
1302 let (_hub, addr, manager) = start(read_only);
1303 manager.runtime().block_on(async {
1304 let (_s, status) = upgrade(addr, Some("http://evil.example")).await;
1305 assert!(status.contains("403"), "{status}");
1306 let (_s, status) = upgrade(addr, Some(&format!("http://{addr}"))).await;
1307 assert!(status.contains("101"), "{status}");
1308 });
1309 }
1310
1311 #[test]
1312 fn silent_clients_are_dropped() {
1313 let mut quick = config(&[], &[]);
1314 quick.ping = Duration::from_millis(50);
1315 let (hub, addr, manager) = start(quick);
1316 manager.runtime().block_on(async {
1317 let (stream, status) = upgrade(addr, None).await;
1319 assert!(status.contains("101"), "{status}");
1320 assert_eq!(hub.clients.lock().unwrap().len(), 1);
1321 let mut waited = 0;
1322 while !hub.clients.lock().unwrap().is_empty() {
1323 assert!(waited < 100, "the silent client kept its slot");
1324 tokio::time::sleep(Duration::from_millis(50)).await;
1325 waited += 1;
1326 }
1327 drop(stream);
1328 });
1329 }
1330
1331 #[test]
1332 fn pv_names() {
1333 assert!(valid_name("SR-DI-DCCT-01:SIGNAL"));
1334 assert!(valid_name("pva://a.b[1]{x}"));
1335 assert!(!valid_name(""));
1336 assert!(!valid_name("has space"));
1337 assert!(!valid_name("new\nline"));
1338 assert!(!valid_name("caf\u{e9}"));
1339 assert!(valid_name(&"x".repeat(MAX_NAME)));
1340 assert!(!valid_name(&"x".repeat(MAX_NAME + 1)));
1341 }
1342
1343 #[test]
1344 fn write_tokens() {
1345 assert!(token_ok("secret", "secret"));
1346 assert!(!token_ok("", "secret"));
1347 assert!(!token_ok("secreT", "secret"));
1348 assert!(!token_ok("secret1", "secret"));
1349 }
1350
1351 #[test]
1352 fn client_and_subscription_caps() {
1353 let hub = Arc::new(Hub::new(PvManager::new(), config(&[], &[])));
1354 let ip = |n: usize| IpAddr::from([10, 0, (n / 250) as u8, (n % 250) as u8]);
1355 let slots: Vec<_> = (0..MAX_CLIENTS)
1356 .map(|n| ClientSlot::take(&hub, ip(n)).expect("under the cap"))
1357 .collect();
1358 assert!(ClientSlot::take(&hub, ip(MAX_CLIENTS)).is_err());
1359 drop(slots);
1360 assert!(hub.clients.lock().unwrap().is_empty());
1361 assert!(ClientSlot::take(&hub, ip(0)).is_ok());
1362
1363 let mine: Vec<_> = (0..10)
1365 .map(|_| ClientSlot::take(&hub, ip(1)).expect("under the per-address cap"))
1366 .collect();
1367 assert!(ClientSlot::take(&hub, ip(1)).is_err());
1368 assert!(ClientSlot::take(&hub, ip(2)).is_ok());
1369 drop(mine);
1370 assert!(ClientSlot::take(&hub, ip(1)).is_ok());
1371
1372 let total = Arc::new(AtomicUsize::new(MAX_TOTAL_PVS - 1));
1373 let last = SubSlot::take(&total).expect("one left");
1374 assert!(SubSlot::take(&total).is_none());
1375 drop(last);
1376 assert_eq!(total.load(Ordering::SeqCst), MAX_TOTAL_PVS - 1);
1377 }
1378
1379 #[test]
1380 fn writes_need_the_token() {
1381 let hub = Hub::new(PvManager::new(), config(&[], &[]));
1382 let peer: SocketAddr = "192.0.2.1:5000".parse().unwrap();
1383 let mut subs = Subs::default();
1384 let sub = |subs: &mut Subs, name: &str| {
1385 let msg = json!({"type": "subscribe", "pvs": [name]}).to_string();
1386 handle(&hub, subs, &msg, peer)
1387 };
1388 assert!(sub(&mut subs, "TEST:WRITE").is_empty());
1389 assert_eq!(hub.subscriptions.load(Ordering::SeqCst), 1);
1390 let write = |subs: &mut Subs, token: Option<&str>| {
1391 let mut msg = json!({"type": "write", "pv": "TEST:WRITE", "value": 2});
1392 if let Some(t) = token {
1393 msg["token"] = json!(t);
1394 }
1395 handle(&hub, subs, &msg.to_string(), peer)
1396 };
1397 assert!(write(&mut subs, None)[0].contains("token"));
1398 assert!(write(&mut subs, Some("wrong"))[0].contains("token"));
1399 assert!(write(&mut subs, Some("secret")).is_empty());
1402 let pv = &subs.map["TEST:WRITE"].pv;
1404 pv.set_connected_for_test(true);
1405 pv.set_connected_for_test(false);
1406 let refused = write(&mut subs, Some("secret"));
1407 let m: Map<String, Json> = serde_json::from_str(&refused[0]).unwrap();
1408 assert_eq!(m["pv"], "TEST:WRITE");
1409 assert_eq!(m["write_error"], "not connected");
1410 assert!(sub(&mut subs, "bad name")[0].contains("Cannot subscribe"));
1412 assert_eq!(subs.map.len(), 1);
1413 subs.map.clear();
1414 assert_eq!(hub.subscriptions.load(Ordering::SeqCst), 0);
1415 }
1416
1417 #[test]
1418 fn long_string_writes_are_marked() {
1419 let hub = Hub::new(PvManager::new(), config(&[], &[]));
1420 let peer: SocketAddr = "192.0.2.1:5000".parse().unwrap();
1421 let mut subs = Subs::default();
1422 let name = "loc://srv_long_string<VLongArray>(0,0)";
1425 let slot = SubSlot::take(&hub.subscriptions).unwrap();
1426 subs.add(name, hub.manager.subscribe(name), slot);
1427 let write = |subs: &mut Subs, value: &str, long_string: bool| {
1428 let mut msg = json!({"type": "write", "pv": name, "value": value, "token": "secret"});
1429 if long_string {
1430 msg["long_string"] = json!(true);
1431 }
1432 assert!(handle(&hub, subs, &msg.to_string(), peer).is_empty());
1433 subs.map[name].pv.state().value
1434 };
1435 assert_eq!(
1436 write(&mut subs, "ab", true),
1437 Value::LongArray(Arc::from(vec![97, 98, 0]))
1438 );
1439 assert_eq!(
1440 write(&mut subs, "[1, 2]", false),
1441 Value::LongArray(Arc::from(vec![1, 2]))
1442 );
1443 }
1444
1445 #[test]
1446 fn written_numbers() {
1447 assert_eq!(write_text(&json!(5.0)).unwrap(), "5");
1448 assert_eq!(write_text(&json!(2.5)).unwrap(), "2.5");
1449 assert_eq!(write_text(&json!("abc")).unwrap(), "abc");
1450 }
1451}