1use crate::ComputeCliError;
4
5const MAX_SELECTOR_BYTES: usize = 64;
6const MAX_DEVICES: usize = 64;
7const MAX_PROFILE_BYTES: usize = 64 * 1024;
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum OutputMode {
12 Text,
14 Json,
16}
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum ProfileAction {
21 List,
23 Read,
25 Save,
27}
28
29#[derive(Clone, Debug, Eq, PartialEq)]
31pub enum ComputeCommand {
32 Help,
34 Devices(Selection),
36 Probe(Selection),
38 Profile(ProfileRequest),
40 Explain(ProfileRequest),
42 Recipe(RecipeRequest),
44}
45
46#[derive(Clone, Debug, Eq, PartialEq)]
48pub struct Selection {
49 pub selector: Option<String>,
51 pub output: OutputMode,
53 pub max_devices: usize,
55}
56
57#[derive(Clone, Debug, Eq, PartialEq)]
59pub struct ProfileRequest {
60 pub action: ProfileAction,
62 pub key: Option<String>,
64 pub adapter: String,
66 pub driver: String,
68 pub backend: String,
70 pub now_tick: u64,
72 pub output: OutputMode,
74 pub max_profile_bytes: usize,
76}
77
78#[derive(Clone, Debug, Eq, PartialEq)]
80pub struct RecipeRequest {
81 pub id: String,
83 pub output: OutputMode,
85}
86
87pub fn parse_compute_args(args: &[String]) -> Result<ComputeCommand, ComputeCliError> {
89 let args = strip_verb(args);
90 if args.is_empty() || args.iter().any(|arg| arg == "--help" || arg == "-h") {
91 return Ok(ComputeCommand::Help);
92 }
93 match args[0].as_str() {
94 "devices" => Ok(ComputeCommand::Devices(parse_selection(
95 "devices",
96 &args[1..],
97 )?)),
98 "probe" => Ok(ComputeCommand::Probe(parse_selection("probe", &args[1..])?)),
99 "profile" => Ok(ComputeCommand::Profile(parse_profile(&args[1..])?)),
100 "explain" => {
101 let mut request = parse_profile(&args[1..])?;
102 if request.action == ProfileAction::List {
103 request.action = ProfileAction::Read;
104 }
105 Ok(ComputeCommand::Explain(request))
106 }
107 "recipe" => Ok(ComputeCommand::Recipe(parse_recipe(&args[1..])?)),
108 other => Err(ComputeCliError::new(format!(
109 "unknown compute verb: {other}"
110 ))),
111 }
112}
113
114fn strip_verb(args: &[String]) -> &[String] {
115 if args.first().is_some_and(|arg| arg == "compute") {
116 &args[1..]
117 } else {
118 args
119 }
120}
121
122fn parse_selection(verb: &str, args: &[String]) -> Result<Selection, ComputeCliError> {
123 let mut output = OutputMode::Text;
124 let mut selector = None;
125 let mut max_devices = 16;
126 let mut i = 0;
127 while i < args.len() {
128 match args[i].as_str() {
129 "--json" => output = OutputMode::Json,
130 "--max-devices" => {
131 max_devices = bounded_usize(take_value(args, &mut i, "--max-devices")?)?;
132 if max_devices == 0 || max_devices > MAX_DEVICES {
133 return Err(ComputeCliError::new("max-devices is outside policy"));
134 }
135 }
136 value if value.starts_with('-') => {
137 return Err(ComputeCliError::new(format!(
138 "unknown {verb} option: {value}"
139 )));
140 }
141 value => {
142 checked_selector(value)?;
143 if selector.replace(value.to_owned()).is_some() {
144 return Err(ComputeCliError::new(format!(
145 "{verb} accepts at most one selector"
146 )));
147 }
148 }
149 }
150 i += 1;
151 }
152 Ok(Selection {
153 selector,
154 output,
155 max_devices,
156 })
157}
158
159fn parse_profile(args: &[String]) -> Result<ProfileRequest, ComputeCliError> {
160 let mut request = ProfileRequest {
161 action: ProfileAction::List,
162 key: None,
163 adapter: "modeled-adapter".to_owned(),
164 driver: "modeled-driver".to_owned(),
165 backend: "modeled".to_owned(),
166 now_tick: 0,
167 output: OutputMode::Text,
168 max_profile_bytes: 8192,
169 };
170 let mut i = 0;
171 while i < args.len() {
172 match args[i].as_str() {
173 "list" => request.action = ProfileAction::List,
174 "read" => request.action = ProfileAction::Read,
175 "save" => request.action = ProfileAction::Save,
176 "--json" => request.output = OutputMode::Json,
177 "--key" => request.key = Some(checked_value(take_value(args, &mut i, "--key")?)?),
178 "--adapter" => request.adapter = checked_value(take_value(args, &mut i, "--adapter")?)?,
179 "--driver" => request.driver = checked_value(take_value(args, &mut i, "--driver")?)?,
180 "--backend" => request.backend = checked_value(take_value(args, &mut i, "--backend")?)?,
181 "--now" => request.now_tick = bounded_u64(take_value(args, &mut i, "--now")?)?,
182 "--max-profile-bytes" => {
183 request.max_profile_bytes =
184 bounded_usize(take_value(args, &mut i, "--max-profile-bytes")?)?;
185 if request.max_profile_bytes == 0 || request.max_profile_bytes > MAX_PROFILE_BYTES {
186 return Err(ComputeCliError::new("max-profile-bytes is outside policy"));
187 }
188 }
189 value if value.starts_with('-') => {
190 return Err(ComputeCliError::new(format!(
191 "unknown profile option: {value}"
192 )));
193 }
194 value => {
195 return Err(ComputeCliError::new(format!(
196 "unknown profile action: {value}"
197 )));
198 }
199 }
200 i += 1;
201 }
202 Ok(request)
203}
204
205fn parse_recipe(args: &[String]) -> Result<RecipeRequest, ComputeCliError> {
206 let mut output = OutputMode::Text;
207 let mut id = "inspect-compute-device".to_owned();
208 for arg in args {
209 match arg.as_str() {
210 "--json" => output = OutputMode::Json,
211 value if value.starts_with('-') => {
212 return Err(ComputeCliError::new(format!(
213 "unknown recipe option: {value}"
214 )));
215 }
216 value => id = checked_value(value.to_owned())?,
217 }
218 }
219 if id != "inspect-compute-device" {
220 return Err(ComputeCliError::new(format!(
221 "unknown compute recipe: {id}"
222 )));
223 }
224 Ok(RecipeRequest { id, output })
225}
226
227fn take_value(args: &[String], i: &mut usize, flag: &str) -> Result<String, ComputeCliError> {
228 *i += 1;
229 args.get(*i)
230 .filter(|value| !value.starts_with('-'))
231 .cloned()
232 .ok_or_else(|| ComputeCliError::new(format!("{flag} requires a value")))
233}
234
235fn checked_value(value: String) -> Result<String, ComputeCliError> {
236 checked_selector(&value)?;
237 Ok(value)
238}
239
240fn checked_selector(value: &str) -> Result<(), ComputeCliError> {
241 if value.is_empty() || value.len() > MAX_SELECTOR_BYTES || value.contains("..") {
242 return Err(ComputeCliError::new("selector is outside policy"));
243 }
244 if !value
245 .chars()
246 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '/' | '.'))
247 {
248 return Err(ComputeCliError::new(
249 "selector contains unsupported characters",
250 ));
251 }
252 Ok(())
253}
254
255fn bounded_usize(value: String) -> Result<usize, ComputeCliError> {
256 value
257 .parse::<usize>()
258 .map_err(|_| ComputeCliError::new("numeric ceiling must be an unsigned integer"))
259}
260
261fn bounded_u64(value: String) -> Result<u64, ComputeCliError> {
262 value
263 .parse::<u64>()
264 .map_err(|_| ComputeCliError::new("tick must be an unsigned integer"))
265}