1use uuid::Uuid;
10
11use crate::clixml::{PsObject, PsValue, parse_clixml, to_clixml};
12use crate::error::{PsrpError, Result};
13use crate::message::MessageType;
14use crate::pipeline::PipelineState;
15use crate::runspace::RunspacePool;
16use crate::transport::PsrpTransport;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct CommandType(u32);
22
23impl CommandType {
24 pub const ALIAS: Self = Self(0x0001);
25 pub const FUNCTION: Self = Self(0x0002);
26 pub const FILTER: Self = Self(0x0004);
27 pub const CMDLET: Self = Self(0x0008);
28 pub const EXTERNAL_SCRIPT: Self = Self(0x0010);
29 pub const APPLICATION: Self = Self(0x0020);
30 pub const SCRIPT: Self = Self(0x0040);
31 pub const WORKFLOW: Self = Self(0x0080);
32 pub const CONFIGURATION: Self = Self(0x0100);
33 pub const ALL: Self = Self(0x01FF);
34
35 #[must_use]
36 pub const fn empty() -> Self {
37 Self(0)
38 }
39 #[must_use]
40 pub const fn bits(self) -> u32 {
41 self.0
42 }
43 #[must_use]
44 pub const fn contains(self, other: Self) -> bool {
45 (self.0 & other.0) == other.0
46 }
47}
48
49impl std::ops::BitOr for CommandType {
50 type Output = Self;
51 fn bitor(self, rhs: Self) -> Self::Output {
52 Self(self.0 | rhs.0)
53 }
54}
55
56impl std::ops::BitAnd for CommandType {
57 type Output = Self;
58 fn bitand(self, rhs: Self) -> Self::Output {
59 Self(self.0 & rhs.0)
60 }
61}
62
63#[derive(Debug, Clone, Default, PartialEq)]
65pub struct CommandMetadata {
66 pub name: String,
67 pub namespace: Option<String>,
68 pub has_common_parameters: Option<bool>,
69 pub command_type: Option<i32>,
70 pub parameters: Vec<ParameterMetadata>,
71}
72
73#[derive(Debug, Clone, Default, PartialEq)]
75pub struct ParameterMetadata {
76 pub name: String,
77 pub parameter_type: Option<String>,
78 pub is_mandatory: Option<bool>,
79 pub position: Option<i32>,
80}
81
82impl CommandMetadata {
83 fn from_ps_object(value: &PsValue) -> Option<Self> {
84 let obj = value.properties()?;
85 Some(Self {
86 name: obj
87 .get("Name")
88 .and_then(PsValue::as_str)
89 .unwrap_or_default()
90 .to_string(),
91 namespace: obj
92 .get("Namespace")
93 .and_then(PsValue::as_str)
94 .map(str::to_string),
95 has_common_parameters: obj.get("HasCommonParameters").and_then(PsValue::as_bool),
96 command_type: obj.get("CommandType").and_then(PsValue::as_i32),
97 parameters: match obj.get("Parameters") {
98 Some(PsValue::List(list)) => list
99 .iter()
100 .filter_map(ParameterMetadata::from_ps_value)
101 .collect(),
102 _ => Vec::new(),
103 },
104 })
105 }
106}
107
108impl ParameterMetadata {
109 fn from_ps_value(value: &PsValue) -> Option<Self> {
110 let obj = value.properties()?;
111 Some(Self {
112 name: obj
113 .get("Name")
114 .and_then(PsValue::as_str)
115 .unwrap_or_default()
116 .to_string(),
117 parameter_type: obj
118 .get("ParameterType")
119 .and_then(PsValue::as_str)
120 .map(str::to_string),
121 is_mandatory: obj.get("IsMandatory").and_then(PsValue::as_bool),
122 position: obj.get("Position").and_then(PsValue::as_i32),
123 })
124 }
125}
126
127impl<T: PsrpTransport> RunspacePool<T> {
128 pub async fn get_command_metadata(
134 &mut self,
135 patterns: &[&str],
136 command_type: CommandType,
137 ) -> Result<Vec<CommandMetadata>> {
138 let pid = Uuid::new_v4();
139 let body = build_get_command_metadata_body(patterns, command_type);
140 self.send_pipeline_message(MessageType::GetCommandMetadata, pid, body)
141 .await?;
142
143 let mut out = Vec::new();
144 loop {
145 let msg = self.next_message().await?;
146 match msg.message_type {
147 MessageType::PipelineOutput => {
148 for v in parse_clixml(&msg.data)? {
149 if let Some(cm) = CommandMetadata::from_ps_object(&v) {
150 out.push(cm);
151 }
152 }
153 }
154 MessageType::PipelineState => {
155 if let Some(state) = state_from_xml(&msg.data) {
156 if state.is_terminal() {
157 if state == PipelineState::Failed {
158 return Err(PsrpError::PipelineFailed(
159 "GetCommandMetadata pipeline failed".into(),
160 ));
161 }
162 return Ok(out);
163 }
164 }
165 }
166 _ => continue,
167 }
168 }
169 }
170}
171
172fn state_from_xml(xml: &str) -> Option<PipelineState> {
173 parse_clixml(xml).ok().and_then(|values| {
174 values.into_iter().find_map(|v| match v {
175 PsValue::Object(obj) => obj
176 .get("PipelineState")
177 .and_then(PsValue::as_i32)
178 .map(pipeline_state_from_i32),
179 _ => None,
180 })
181 })
182}
183
184fn pipeline_state_from_i32(v: i32) -> PipelineState {
185 match v {
187 0 => PipelineState::NotStarted,
188 1 => PipelineState::Running,
189 2 => PipelineState::Stopping,
190 3 => PipelineState::Stopped,
191 4 => PipelineState::Completed,
192 5 => PipelineState::Failed,
193 6 => PipelineState::Disconnected,
194 _ => PipelineState::Unknown,
195 }
196}
197
198fn build_get_command_metadata_body(patterns: &[&str], command_type: CommandType) -> String {
199 let names = PsValue::List(
200 patterns
201 .iter()
202 .map(|p| PsValue::String((*p).to_string()))
203 .collect(),
204 );
205 let obj = PsObject::new()
206 .with("Name", names)
207 .with("CommandType", PsValue::I32(command_type.bits() as i32))
208 .with("Namespace", PsValue::List(Vec::new()))
209 .with("ArgumentList", PsValue::List(Vec::new()));
210 to_clixml(&PsValue::Object(obj))
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use crate::clixml::PsObject;
217
218 #[test]
219 fn command_type_constants() {
220 assert_eq!(CommandType::CMDLET.bits(), 0x0008);
221 assert_eq!(CommandType::ALL.bits(), 0x01FF);
222 let combo = CommandType::CMDLET | CommandType::FUNCTION;
223 assert!(combo.contains(CommandType::CMDLET));
224 assert!(combo.contains(CommandType::FUNCTION));
225 assert!(!combo.contains(CommandType::ALIAS));
226 }
227
228 #[test]
229 fn get_command_metadata_body_contains_name_and_type() {
230 let body = build_get_command_metadata_body(&["Get-*", "Set-*"], CommandType::CMDLET);
231 assert!(body.contains("<S>Get-*</S>"));
232 assert!(body.contains("<S>Set-*</S>"));
233 assert!(body.contains("<I32 N=\"CommandType\">8</I32>"));
235 }
236
237 #[test]
238 fn decode_command_metadata_object() {
239 let obj = PsObject::new()
240 .with("Name", PsValue::String("Get-Date".into()))
241 .with("HasCommonParameters", PsValue::Bool(true))
242 .with("CommandType", PsValue::I32(8))
243 .with(
244 "Parameters",
245 PsValue::List(vec![PsValue::Object(
246 PsObject::new()
247 .with("Name", PsValue::String("Format".into()))
248 .with("ParameterType", PsValue::String("System.String".into()))
249 .with("IsMandatory", PsValue::Bool(false))
250 .with("Position", PsValue::I32(0)),
251 )]),
252 );
253 let cm = CommandMetadata::from_ps_object(&PsValue::Object(obj)).unwrap();
254 assert_eq!(cm.name, "Get-Date");
255 assert_eq!(cm.has_common_parameters, Some(true));
256 assert_eq!(cm.command_type, Some(8));
257 assert_eq!(cm.parameters.len(), 1);
258 assert_eq!(cm.parameters[0].name, "Format");
259 assert_eq!(
260 cm.parameters[0].parameter_type.as_deref(),
261 Some("System.String")
262 );
263 }
264
265 #[test]
266 fn decode_rejects_non_object() {
267 assert!(CommandMetadata::from_ps_object(&PsValue::I32(1)).is_none());
268 }
269
270 #[test]
271 fn pipeline_state_shim_matches() {
272 assert_eq!(pipeline_state_from_i32(0), PipelineState::NotStarted);
273 assert_eq!(pipeline_state_from_i32(1), PipelineState::Running);
274 assert_eq!(pipeline_state_from_i32(2), PipelineState::Stopping);
275 assert_eq!(pipeline_state_from_i32(3), PipelineState::Stopped);
276 assert_eq!(pipeline_state_from_i32(4), PipelineState::Completed);
277 assert_eq!(pipeline_state_from_i32(5), PipelineState::Failed);
278 assert_eq!(pipeline_state_from_i32(6), PipelineState::Disconnected);
279 assert_eq!(pipeline_state_from_i32(99), PipelineState::Unknown);
280 }
281
282 #[test]
283 fn state_from_xml_missing_is_none() {
284 assert!(state_from_xml("<Obj RefId=\"0\"><MS/></Obj>").is_none());
285 }
286
287 #[test]
288 fn state_from_xml_ok() {
289 let xml = to_clixml(&PsValue::Object(
290 PsObject::new().with("PipelineState", PsValue::I32(4)),
291 ));
292 assert_eq!(state_from_xml(&xml), Some(PipelineState::Completed));
293 }
294
295 use crate::fragment::encode_message;
298 use crate::message::{Destination, PsrpMessage};
299 use crate::runspace::RunspacePoolState;
300 use crate::transport::mock::MockTransport;
301 use uuid::Uuid;
302
303 fn wire_msg(mt: MessageType, data: String) -> Vec<u8> {
304 PsrpMessage {
305 destination: Destination::Client,
306 message_type: mt,
307 rpid: Uuid::nil(),
308 pid: Uuid::nil(),
309 data,
310 }
311 .encode()
312 }
313
314 fn opened_state() -> Vec<u8> {
315 wire_msg(
316 MessageType::RunspacePoolState,
317 to_clixml(&PsValue::Object(PsObject::new().with(
318 "RunspaceState",
319 PsValue::I32(RunspacePoolState::Opened as i32),
320 ))),
321 )
322 }
323
324 fn pipeline_state(state: PipelineState) -> Vec<u8> {
325 wire_msg(
326 MessageType::PipelineState,
327 to_clixml(&PsValue::Object(
328 PsObject::new().with("PipelineState", PsValue::I32(state as i32)),
329 )),
330 )
331 }
332
333 #[tokio::test]
334 async fn get_command_metadata_returns_items() {
335 let t = MockTransport::new();
336 t.push_incoming(encode_message(1, &opened_state()));
337
338 let cmd = |name: &str| {
340 to_clixml(&PsValue::Object(
341 PsObject::new()
342 .with("Name", PsValue::String(name.into()))
343 .with("CommandType", PsValue::I32(8)),
344 ))
345 };
346 t.push_incoming(encode_message(
347 10,
348 &wire_msg(MessageType::PipelineOutput, cmd("Get-Date")),
349 ));
350 t.push_incoming(encode_message(
351 11,
352 &wire_msg(MessageType::PipelineOutput, cmd("Get-Process")),
353 ));
354 t.push_incoming(encode_message(
355 12,
356 &pipeline_state(PipelineState::Completed),
357 ));
358
359 let mut pool = crate::runspace::RunspacePool::open_with_transport(t.clone())
360 .await
361 .unwrap();
362 let cmds = pool
363 .get_command_metadata(&["Get-*"], CommandType::CMDLET)
364 .await
365 .unwrap();
366 assert_eq!(cmds.len(), 2);
367 assert_eq!(cmds[0].name, "Get-Date");
368 assert_eq!(cmds[1].name, "Get-Process");
369 let _ = pool.close().await;
370 }
371
372 #[tokio::test]
373 async fn get_command_metadata_failed_pipeline_errors() {
374 let t = MockTransport::new();
375 t.push_incoming(encode_message(1, &opened_state()));
376 t.push_incoming(encode_message(10, &pipeline_state(PipelineState::Failed)));
377 let mut pool = crate::runspace::RunspacePool::open_with_transport(t)
378 .await
379 .unwrap();
380 let err = pool
381 .get_command_metadata(&["Nothing"], CommandType::ALL)
382 .await
383 .unwrap_err();
384 assert!(matches!(err, crate::error::PsrpError::PipelineFailed(_)));
385 let _ = pool.close().await;
386 }
387
388 #[tokio::test]
389 async fn get_command_metadata_empty_result() {
390 let t = MockTransport::new();
391 t.push_incoming(encode_message(1, &opened_state()));
392 t.push_incoming(encode_message(
393 10,
394 &pipeline_state(PipelineState::Completed),
395 ));
396 let mut pool = crate::runspace::RunspacePool::open_with_transport(t)
397 .await
398 .unwrap();
399 let cmds = pool
400 .get_command_metadata(&["None-*"], CommandType::CMDLET)
401 .await
402 .unwrap();
403 assert!(cmds.is_empty());
404 let _ = pool.close().await;
405 }
406
407 #[test]
408 fn command_type_bit_and() {
409 let mask = CommandType::ALL & CommandType::CMDLET;
410 assert_eq!(mask.bits(), CommandType::CMDLET.bits());
411 let empty = CommandType::empty();
412 assert_eq!(empty.bits(), 0);
413 }
414
415 #[test]
416 fn command_type_bit_or() {
417 let combined = CommandType::CMDLET | CommandType::FUNCTION;
418 assert!(combined.contains(CommandType::CMDLET));
419 assert!(combined.contains(CommandType::FUNCTION));
420 assert!(!combined.contains(CommandType::ALIAS));
421 assert_eq!(
423 combined.bits(),
424 CommandType::CMDLET.bits() | CommandType::FUNCTION.bits()
425 );
426 let double = CommandType::CMDLET | CommandType::CMDLET;
428 assert!(double.contains(CommandType::CMDLET));
429 assert_eq!(double.bits(), CommandType::CMDLET.bits());
430 }
431}