1 ///! Rule group for tracking command collision issues.
2 use chrono::NaiveDateTime;
3 use std::convert::Into;
4 use std::io::Write;
5 
6 use crate::engine::{Rule, RuleGroup, Signal};
7 use crate::parser::{Packet, PacketChild};
8 use hcidoc_packets::hci::{ErrorCode, EventChild, OpCode};
9 
10 enum CollisionSignal {
11     RnrAndInquiry,
12     RnrAndConnection,
13     InquiryAndConnection,
14     InquirySelf,
15 }
16 
17 impl Into<&'static str> for CollisionSignal {
into(self) -> &'static str18     fn into(self) -> &'static str {
19         match self {
20             CollisionSignal::RnrAndInquiry => "RnR-Inquiry-Collision",
21             CollisionSignal::RnrAndConnection => "RnR-Connection-Collision",
22             CollisionSignal::InquiryAndConnection => "Inquiry-Connection-Collision",
23             CollisionSignal::InquirySelf => "Inquiry-Self-Collision",
24         }
25     }
26 }
27 
28 // What state are we in for the serializable state?
29 #[derive(Debug, PartialEq)]
30 enum CollisionState {
31     Nothing,
32     ConnectionActive,
33     RemoteNameReqActive,
34     InquiryActive,
35 }
36 
37 /// This rule keeps track of collisions that occur due to serializable commands
38 /// not being serialized correctly: Create Connection, Remote Name Request and Inquiry.
39 struct ConnectionSerializationRule {
40     /// What's the current state of the serializable commands?
41     state: CollisionState,
42 
43     /// When was the last state set? (Except Nothing)
44     state_set_at: Option<(usize, NaiveDateTime)>,
45 
46     /// Pre-defined signals discovered in the logs.
47     signals: Vec<Signal>,
48 
49     /// Interesting occurrences surfaced by this rule.
50     reportable: Vec<(NaiveDateTime, String)>,
51 }
52 
53 impl ConnectionSerializationRule {
new() -> Self54     pub fn new() -> Self {
55         ConnectionSerializationRule {
56             state: CollisionState::Nothing,
57             state_set_at: None,
58             signals: vec![],
59             reportable: vec![],
60         }
61     }
62 
63     // Determine signal to emit based on the currently active state.
get_signal_type(&self, opcode: &OpCode) -> Option<CollisionSignal>64     pub(crate) fn get_signal_type(&self, opcode: &OpCode) -> Option<CollisionSignal> {
65         match (opcode, &self.state) {
66             (&OpCode::CreateConnection, &CollisionState::InquiryActive) => {
67                 Some(CollisionSignal::InquiryAndConnection)
68             }
69 
70             (&OpCode::CreateConnection, &CollisionState::RemoteNameReqActive) => {
71                 Some(CollisionSignal::RnrAndConnection)
72             }
73 
74             (&OpCode::Inquiry, &CollisionState::InquiryActive) => {
75                 Some(CollisionSignal::InquirySelf)
76             }
77 
78             (&OpCode::Inquiry, &CollisionState::RemoteNameReqActive) => {
79                 Some(CollisionSignal::RnrAndInquiry)
80             }
81 
82             (&OpCode::Inquiry, &CollisionState::ConnectionActive) => {
83                 Some(CollisionSignal::InquiryAndConnection)
84             }
85 
86             (&OpCode::RemoteNameRequest, &CollisionState::InquiryActive) => {
87                 Some(CollisionSignal::RnrAndInquiry)
88             }
89 
90             (&OpCode::RemoteNameRequest, &CollisionState::ConnectionActive) => {
91                 Some(CollisionSignal::RnrAndConnection)
92             }
93 
94             (_, _) => None,
95         }
96     }
97 }
98 
99 impl Rule for ConnectionSerializationRule {
process(&mut self, packet: &Packet)100     fn process(&mut self, packet: &Packet) {
101         match &packet.inner {
102             PacketChild::HciEvent(ev) => match ev.specialize() {
103                 // Most of the serializable commands will get "Disallowed" on
104                 // command status except `Remote Name Req`.
105                 EventChild::CommandStatus(cs) => match cs.get_command_op_code() {
106                     OpCode::CreateConnection | OpCode::Inquiry | OpCode::RemoteNameRequest => {
107                         // Set the state to the new successful command if it was
108                         // previously not set.
109                         if cs.get_status() == ErrorCode::Success
110                             && self.state == CollisionState::Nothing
111                         {
112                             let new_state: Option<CollisionState> = match cs.get_command_op_code() {
113                                 OpCode::CreateConnection => Some(CollisionState::ConnectionActive),
114                                 OpCode::Inquiry => Some(CollisionState::InquiryActive),
115                                 OpCode::RemoteNameRequest => {
116                                     Some(CollisionState::RemoteNameReqActive)
117                                 }
118                                 _ => None,
119                             };
120 
121                             if let Some(new_state) = new_state {
122                                 self.state = new_state;
123                                 self.state_set_at = Some((packet.index, packet.ts.clone()));
124                             }
125                         }
126                         // We've hit a disallowed status. Check if we're
127                         // conflicting with something that should be serializable.
128                         else if cs.get_status() == ErrorCode::CommandDisallowed {
129                             self.reportable.push((
130                                 packet.ts,
131                                 format!("Command Status was 'Disallowed' on {:?}. Potential conflict with: {:?} at {:?}",
132                                     cs.get_command_op_code(), self.state, self.state_set_at)
133                             ));
134 
135                             let signal: Option<CollisionSignal> =
136                                 self.get_signal_type(&cs.get_command_op_code());
137 
138                             if let Some(signal) = signal {
139                                 self.signals.push(Signal {
140                                     index: packet.index,
141                                     ts: packet.ts.clone(),
142                                     tag: signal.into(),
143                                 });
144                             }
145                         }
146                     }
147 
148                     _ => (),
149                 },
150 
151                 // RnR will only tell you "Disallowed" on the event itself.
152                 // However, we should handle this in CommandStatus as well in case
153                 // this is a controller quirk.
154                 EventChild::RemoteNameRequestComplete(rnr_ev) => {
155                     // Everything except "Disallowed" resets the state.
156                     if rnr_ev.get_status() != ErrorCode::CommandDisallowed
157                         && self.state == CollisionState::RemoteNameReqActive
158                     {
159                         self.state = CollisionState::Nothing;
160                         self.state_set_at = None;
161                     } else if rnr_ev.get_status() == ErrorCode::CommandDisallowed {
162                         self.reportable.push((
163                                 packet.ts,
164                                 format!("Remote name req complete with disallowed. Potential conflict with: {:?} at {:?}",
165                                     self.state, self.state_set_at)
166 
167                         ));
168 
169                         // Insert signals based on current serializable state.
170                         let signal = self.get_signal_type(&OpCode::RemoteNameRequest);
171 
172                         if let Some(signal) = signal {
173                             self.signals.push(Signal {
174                                 index: packet.index,
175                                 ts: packet.ts.clone(),
176                                 tag: signal.into(),
177                             });
178                         }
179                     }
180                 }
181 
182                 EventChild::InquiryComplete(_) => {
183                     // Inquiry was observed to be disallowed only on |CommandStatus|
184                     // so always clear state here.
185                     if self.state == CollisionState::InquiryActive {
186                         self.state = CollisionState::Nothing;
187                         self.state_set_at = None;
188                     }
189                 }
190 
191                 EventChild::ConnectionComplete(_) => {
192                     // Connection was observed to be disallowed only on |CommandStatus|
193                     // so always clear state here.
194                     if self.state == CollisionState::ConnectionActive {
195                         self.state = CollisionState::Nothing;
196                         self.state_set_at = None;
197                     }
198                 }
199 
200                 _ => {}
201             },
202 
203             _ => {}
204         }
205     }
206 
report(&self, writer: &mut dyn Write)207     fn report(&self, writer: &mut dyn Write) {
208         if self.reportable.len() > 0 {
209             let _ = writeln!(writer, "ConnectionSerializationRule report:");
210             for (ts, message) in self.reportable.iter() {
211                 let _ = writeln!(writer, "[{:?}] {}", ts, message);
212             }
213         }
214     }
215 
report_signals(&self) -> &[Signal]216     fn report_signals(&self) -> &[Signal] {
217         self.signals.as_slice()
218     }
219 }
220 
221 /// Get a rule group with collision rules.
get_collisions_group() -> RuleGroup222 pub fn get_collisions_group() -> RuleGroup {
223     let mut group = RuleGroup::new();
224     group.add_rule(Box::new(ConnectionSerializationRule::new()));
225 
226     group
227 }
228