secop_core/
proto.rs

1// -----------------------------------------------------------------------------
2// Rust SECoP playground
3//
4// This program is free software; you can redistribute it and/or modify it under
5// the terms of the GNU General Public License as published by the Free Software
6// Foundation; either version 2 of the License, or (at your option) any later
7// version.
8//
9// This program is distributed in the hope that it will be useful, but WITHOUT
10// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11// FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
12// details.
13//
14// You should have received a copy of the GNU General Public License along with
15// this program; if not, write to the Free Software Foundation, Inc.,
16// 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17//
18// Module authors:
19//   Georg Brandl <g.brandl@fz-juelich.de>
20//
21// -----------------------------------------------------------------------------
22//
23//! This module contains the definition of a protocol message, along with tools
24//! to parse and string-format it.
25
26use std::fmt;
27use regex::Regex;
28use serde_json::Value;
29use lazy_static::lazy_static;
30
31use crate::errors::Error;
32
33
34lazy_static! {
35    static ref MSG_RE: Regex = Regex::new(r#"(?x)
36    ^
37    (?P<type>[*?\w]+)                 # message type (verb)
38    (?: \s
39      (?P<spec>[\w:<>]+)              # spec (object)
40      (?: \s
41        (?P<json>.*)                  # data (json)
42      )?
43    )?
44    $
45    "#).expect("valid regex");
46}
47
48pub const IDENT_REPLY: &str = "SINE2020&ISSE,SECoP,V2019-09-16,v1.0";
49
50/// Enum that represents any message that can be sent over the network in the
51/// protocol, and some others that are only used internally.
52#[derive(Debug, Clone)]
53pub enum Msg {
54    /// identify request
55    Idn,
56    /// identify reply
57    IdnReply { encoded: String },
58    /// description request
59    Describe,
60    /// description reply
61    Describing { id: String, structure: Value },
62    /// event enable request
63    Activate { module: String },
64    /// event enable reply
65    Active { module: String },
66    /// event disable request
67    Deactivate { module: String },
68    /// event disable reply
69    Inactive { module: String },
70    /// command execution request
71    Do { module: String, command: String, arg: Value },
72    /// command result
73    Done { module: String, command: String, data: Value },
74    /// change request
75    Change { module: String, param: String, value: Value },
76    /// change result
77    Changed { module: String, param: String, data: Value },
78    /// read request
79    Read { module: String, param: String },
80    /// heartbeat request
81    Ping { token: String },
82    /// heartbeat reply
83    Pong { token: String, data: Value },
84    /// error reply
85    ErrMsg { class: String, report: Value },
86    /// update event
87    Update { module: String, param: String, data: Value },
88
89    /// not a protocol message, but a collection of initial updates
90    InitUpdates { module: String, updates: Vec<Msg> },
91    /// not a protocol message, indicates the connection is done
92    Quit,
93}
94
95/// An incoming message that carries around the originating line from the
96/// client.  We need this line for the error message if something goes wrong.
97#[derive(Clone)]
98pub struct IncomingMsg(pub String, pub Msg);
99
100use self::Msg::*;
101
102mod wire {
103    pub const IDN: &str = "*IDN?";
104    pub const DESCRIBE: &str = "describe";
105    pub const DESCRIBING: &str = "describing";
106    pub const ACTIVATE: &str = "activate";
107    pub const ACTIVE: &str = "active";
108    pub const DEACTIVATE: &str = "deactivate";
109    pub const INACTIVE: &str = "inactive";
110    pub const PING: &str = "ping";
111    pub const PONG: &str = "pong";
112    pub const ERROR: &str = "error";
113    pub const DO: &str = "do";
114    pub const DONE: &str = "done";
115    pub const CHANGE: &str = "change";
116    pub const CHANGED: &str = "changed";
117    pub const READ: &str = "read";
118    pub const UPDATE: &str = "update";
119}
120
121impl Msg {
122    /// Parse a string slice containing a message.
123    ///
124    /// This matches a regular expression, and then creates a `Msg` if successful.
125    pub fn parse(msg: String) -> Result<IncomingMsg, Msg> {
126        match Self::parse_inner(&msg) {
127            Ok(v) => Ok(IncomingMsg(msg, v)),
128            Err(e) => Err(e.into_msg(msg)),
129        }
130    }
131
132    fn parse_inner(msg: &str) -> Result<Msg, Error> {
133        if let Some(captures) = MSG_RE.captures(msg) {
134            let action = captures.get(1).expect("is required").as_str();
135
136            let specifier = captures.get(2).map(|m| m.as_str()).unwrap_or("");
137            let mut spec_split = specifier.splitn(2, ':').map(Into::into);
138            let module = spec_split.next().expect("cannot be absent");
139            let mut param = || spec_split.next().ok_or(Error::protocol("missing parameter"));
140
141            let data = if let Some(jsonstr) = captures.get(3) {
142                serde_json::from_str(jsonstr.as_str())
143                    .map_err(|_| Error::protocol("invalid JSON"))?
144            } else {
145                Value::Null
146            };
147
148            let parsed = match action {
149                wire::READ =>       Read { module, param: param()? },
150                wire::CHANGE =>     Change { module, param: param()?, value: data },
151                wire::DO =>         Do { module, command: param()?, arg: data },
152                wire::DESCRIBE =>   Describe,
153                wire::ACTIVATE =>   Activate { module },
154                wire::DEACTIVATE => Deactivate { module },
155                wire::PING =>       Ping { token: specifier.into() },
156                wire::IDN =>        Idn,
157                wire::UPDATE =>     Update { module, param: param()?, data },
158                wire::CHANGED =>    Changed { module, param: param()?, data },
159                wire::DONE =>       Done { module, command: param()?, data },
160                wire::DESCRIBING => Describing { id: specifier.into(), structure: data },
161                wire::ACTIVE =>     Active { module },
162                wire::INACTIVE =>   Inactive { module },
163                wire::PONG =>       Pong { token: specifier.into(), data },
164                wire::ERROR =>      ErrMsg { class: specifier.into(), report: data },
165                _ => return Err(Error::protocol("no such message type"))
166            };
167
168            Ok(parsed)
169        } else if msg == IDENT_REPLY {
170            // identify reply has a special format (to be compatible with SCPI)
171            Ok(IdnReply { encoded: IDENT_REPLY.into() })
172        } else {
173            Err(Error::protocol("invalid message format"))
174        }
175    }
176}
177
178/// "Serialize" a `Msg` back to a String.
179///
180/// Not all messages are actually used for stringification, but this is also
181/// nice for debugging purposes.
182impl fmt::Display for Msg {
183    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
184        match self {
185            Update { module, param, data } =>
186                write!(f, "{} {}:{} {}", wire::UPDATE, module, param, data),
187            Changed { module, param, data } =>
188                write!(f, "{} {}:{} {}", wire::CHANGED, module, param, data),
189            Done { module, command, data } =>
190                write!(f, "{} {}:{} {}", wire::DONE, module, command, data),
191            Describing { id, structure } =>
192                write!(f, "{} {} {}", wire::DESCRIBING, id, structure),
193            Active { module } =>
194                if module.is_empty() { f.write_str(wire::ACTIVE) }
195                else { write!(f, "{} {}", wire::ACTIVE, module) },
196            Inactive { module } =>
197                if module.is_empty() { f.write_str(wire::INACTIVE) }
198                else { write!(f, "{} {}", wire::INACTIVE, module) },
199            Pong { token, data } =>
200                write!(f, "{} {} {}", wire::PONG, token, data),
201            Idn => f.write_str(wire::IDN),
202            IdnReply { encoded } => f.write_str(&encoded),
203            Read { module, param } =>
204                write!(f, "{} {}:{}", wire::READ, module, param),
205            Change { module, param, value } =>
206                write!(f, "{} {}:{} {}", wire::CHANGE, module, param, value),
207            Do { module, command, arg } =>
208                write!(f, "{} {}:{} {}", wire::DO, module, command, arg),
209            Describe => f.write_str(wire::DESCRIBE),
210            Activate { module } =>
211                if module.is_empty() { f.write_str(wire::ACTIVATE) }
212                else { write!(f, "{} {}", wire::ACTIVATE, module) },
213            Deactivate { module } =>
214                if module.is_empty() { f.write_str(wire::DEACTIVATE) }
215                else { write!(f, "{} {}", wire::DEACTIVATE, module) },
216            Ping { token } =>
217                if token.is_empty() { f.write_str(wire::PING) }
218                else { write!(f, "{} {}", wire::PING, token) },
219            ErrMsg { class, report } =>
220                write!(f, "{} {} {}", wire::ERROR, class, report),
221            InitUpdates { .. } => write!(f, "<updates>"),
222            Quit => write!(f, "<eof>"),
223        }
224    }
225}
226
227impl IncomingMsg {
228    pub fn bare(msg: Msg) -> Self {
229        IncomingMsg(String::new(), msg)
230    }
231}
232
233impl fmt::Display for IncomingMsg {
234    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
235        write!(f, "{}", self.0)
236    }
237}