sync15/clients_engine/
record.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use crate::error::error;
6use serde_derive::*;
7
8use super::Command;
9
10/// The serialized form of a client record.
11#[derive(Clone, Debug, Eq, Deserialize, Hash, PartialEq, Serialize)]
12#[serde(rename_all = "camelCase")]
13pub struct ClientRecord {
14    #[serde(rename = "id")]
15    pub id: String,
16
17    pub name: String,
18
19    #[serde(rename = "type")]
20    pub typ: crate::DeviceType,
21
22    #[serde(default, skip_serializing_if = "Vec::is_empty")]
23    pub commands: Vec<CommandRecord>,
24
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub fxa_device_id: Option<String>,
27
28    // `version`, `protocols`, `formfactor`, `os`, `appPackage`, `application`,
29    // and `device` are unused and optional in all implementations (Desktop,
30    // iOS, and Fennec), but we round-trip them.
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub version: Option<String>,
33
34    #[serde(default, skip_serializing_if = "Vec::is_empty")]
35    pub protocols: Vec<String>,
36
37    #[serde(
38        default,
39        rename = "formfactor",
40        skip_serializing_if = "Option::is_none"
41    )]
42    pub form_factor: Option<String>,
43
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub os: Option<String>,
46
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub app_package: Option<String>,
49
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub application: Option<String>,
52
53    /// The model of the device, like "iPhone" or "iPod touch" on iOS. Note
54    /// that this is _not_ the client ID (`id`) or the FxA device ID
55    /// (`fxa_device_id`).
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub device: Option<String>,
58}
59
60impl From<&ClientRecord> for crate::RemoteClient {
61    fn from(record: &ClientRecord) -> crate::RemoteClient {
62        crate::RemoteClient {
63            fxa_device_id: record.fxa_device_id.clone(),
64            device_name: record.name.clone(),
65            device_type: record.typ,
66        }
67    }
68}
69
70/// The serialized form of a client command.
71#[derive(Clone, Debug, Eq, Deserialize, Hash, PartialEq, Serialize)]
72#[serde(rename_all = "camelCase")]
73pub struct CommandRecord {
74    /// The command name. This is a string, not an enum, because we want to
75    /// round-trip commands that we don't support yet.
76    #[serde(rename = "command")]
77    pub name: String,
78
79    /// Extra, command-specific arguments. Note that we must send an empty
80    /// array if the command expects no arguments.
81    #[serde(default)]
82    pub args: Vec<Option<String>>,
83
84    /// Some commands, like repair, send a "flow ID" that other cliennts can
85    /// record in their telemetry. We don't currently send commands with
86    /// flow IDs, but we round-trip them.
87    #[serde(default, rename = "flowID", skip_serializing_if = "Option::is_none")]
88    pub flow_id: Option<String>,
89}
90
91impl CommandRecord {
92    // In an ideal future we'd treat non-string args as "soft errors" rather than a hard
93    // serde failure, but there's no evidence we actually see these. There *is* evidence of
94    // seeing nulls instead of strings though (presumably due to old sendTab commands), so we
95    // do handle that.
96    fn get_single_string_arg(&self) -> Option<String> {
97        let cmd_name = &self.name;
98        if self.args.len() == 1 {
99            match &self.args[0] {
100                Some(name) => Some(name.into()),
101                None => {
102                    error!("Incoming '{cmd_name}' command has null argument");
103                    None
104                }
105            }
106        } else {
107            error!(
108                "Incoming '{cmd_name}' command has wrong number of arguments ({})",
109                self.args.len()
110            );
111            None
112        }
113    }
114
115    /// Converts a serialized command into one that we can apply. Returns `None`
116    /// if we don't support the command.
117    pub fn as_command(&self) -> Option<Command> {
118        match self.name.as_str() {
119            "wipeEngine" => self.get_single_string_arg().map(Command::Wipe),
120            "resetEngine" => self.get_single_string_arg().map(Command::Reset),
121            "resetAll" => {
122                if self.args.is_empty() {
123                    Some(Command::ResetAll)
124                } else {
125                    error!("Invalid arguments for 'resetAll' command");
126                    None
127                }
128            }
129            // Note callers are expected to log on an unknown command.
130            _ => None,
131        }
132    }
133}
134
135impl From<Command> for CommandRecord {
136    fn from(command: Command) -> CommandRecord {
137        match command {
138            Command::Wipe(engine) => CommandRecord {
139                name: "wipeEngine".into(),
140                args: vec![Some(engine)],
141                flow_id: None,
142            },
143            Command::Reset(engine) => CommandRecord {
144                name: "resetEngine".into(),
145                args: vec![Some(engine)],
146                flow_id: None,
147            },
148            Command::ResetAll => CommandRecord {
149                name: "resetAll".into(),
150                args: Vec::new(),
151                flow_id: None,
152            },
153        }
154    }
155}
156
157#[cfg(test)]
158mod test {
159    use super::*;
160
161    #[test]
162    fn test_valid_commands() {
163        let ser = serde_json::json!({"command": "wipeEngine", "args": ["foo"]});
164        let record: CommandRecord = serde_json::from_value(ser).unwrap();
165        assert_eq!(record.as_command(), Some(Command::Wipe("foo".to_string())));
166
167        let ser = serde_json::json!({"command": "resetEngine", "args": ["foo"]});
168        let record: CommandRecord = serde_json::from_value(ser).unwrap();
169        assert_eq!(record.as_command(), Some(Command::Reset("foo".to_string())));
170
171        let ser = serde_json::json!({"command": "resetAll"});
172        let record: CommandRecord = serde_json::from_value(ser).unwrap();
173        assert_eq!(record.as_command(), Some(Command::ResetAll));
174    }
175
176    #[test]
177    fn test_unknown_command() {
178        let ser = serde_json::json!({"command": "unknown", "args": ["foo", "bar"]});
179        let record: CommandRecord = serde_json::from_value(ser).unwrap();
180        assert_eq!(record.as_command(), None);
181    }
182
183    #[test]
184    fn test_bad_args() {
185        let ser = serde_json::json!({"command": "wipeEngine", "args": ["foo", "bar"]});
186        let record: CommandRecord = serde_json::from_value(ser).unwrap();
187        assert_eq!(record.as_command(), None);
188
189        let ser = serde_json::json!({"command": "wipeEngine"});
190        let record: CommandRecord = serde_json::from_value(ser).unwrap();
191        assert_eq!(record.as_command(), None);
192
193        let ser = serde_json::json!({"command": "resetAll", "args": ["foo"]});
194        let record: CommandRecord = serde_json::from_value(ser).unwrap();
195        assert_eq!(record.as_command(), None);
196    }
197
198    #[test]
199    fn test_null_args() {
200        let ser = serde_json::json!({"command": "unknown", "args": ["foo", null]});
201        let record: CommandRecord = serde_json::from_value(ser).unwrap();
202        assert_eq!(record.as_command(), None);
203    }
204}