1use crate::mappings::{get_mappings, strip_quotes, parse_key_value_list, is_boolean_flag, ArgType};
2use indexmap::IndexMap;
3use serde_yaml::Value;
4
5pub fn parse_docker_command(input: &str) -> Result<(String, Vec<String>, IndexMap<String, Vec<String>>), String> {
6 let cleaned = input
8 .trim()
9 .replace("\\\n", " ") .replace("\\\r\n", " ") .replace('\n', " ")
12 .split_whitespace()
13 .collect::<Vec<_>>()
14 .join(" ");
15
16 let re = regex::Regex::new(r"^(?:docker|podman)\s+(?:run|create|container\s+run|service\s+create)\s+")
18 .map_err(|e| format!("Regex error: {}", e))?;
19
20 let cleaned = re.replace(&cleaned, "").to_string();
21
22 let mut args: IndexMap<String, Vec<String>> = IndexMap::new();
23 let mut positional = Vec::new();
24 let mut tokens: Vec<String> = Vec::new();
25
26 let mut current = String::new();
28 let mut in_quotes = false;
29 let mut quote_char = ' ';
30 let mut chars = cleaned.chars().peekable();
31
32 while let Some(ch) = chars.next() {
33 match ch {
34 '\\' => {
35 if let Some(next_ch) = chars.next() {
37 current.push(next_ch);
38 }
39 }
40 '"' | '\'' if !in_quotes => {
41 in_quotes = true;
42 quote_char = ch;
43 current.push(ch);
44 }
45 c if c == quote_char && in_quotes => {
46 in_quotes = false;
47 current.push(ch);
48 }
49 c if c.is_whitespace() && !in_quotes => {
50 if !current.is_empty() {
51 tokens.push(current.clone());
52 current.clear();
53 }
54 }
55 _ => {
56 current.push(ch);
57 }
58 }
59 }
60
61 if !current.is_empty() {
62 tokens.push(current);
63 }
64
65 let mut i = 0;
66
67 while i < tokens.len() {
68 let token = &tokens[i];
69
70 if token.starts_with("--") {
71 let flag_part = token.trim_start_matches("--");
72
73 if flag_part.contains('=') {
75 let parts: Vec<&str> = flag_part.splitn(2, '=').collect();
76 if parts.len() == 2 {
77 args.entry(parts[0].to_string())
78 .or_insert_with(Vec::new)
79 .push(strip_quotes(parts[1]));
80 }
81 i += 1;
82 } else if is_boolean_flag(flag_part) {
83 args.entry(flag_part.to_string())
85 .or_insert_with(Vec::new)
86 .push("true".to_string());
87 i += 1;
88 } else if i + 1 < tokens.len() && !tokens[i + 1].starts_with('-') {
89 args.entry(flag_part.to_string())
91 .or_insert_with(Vec::new)
92 .push(strip_quotes(&tokens[i + 1]));
93 i += 2;
94 } else {
95 args.entry(flag_part.to_string())
97 .or_insert_with(Vec::new)
98 .push("true".to_string());
99 i += 1;
100 }
101 } else if token.starts_with('-') && token.len() > 1 && !token.chars().nth(1).unwrap().is_numeric() {
102 let flags = token.trim_start_matches('-');
103
104 if flags.len() == 1 {
106 if i + 1 < tokens.len() && !tokens[i + 1].starts_with('-') {
107 args.entry(flags.to_string())
108 .or_insert_with(Vec::new)
109 .push(strip_quotes(&tokens[i + 1]));
110 i += 2;
111 } else {
112 args.entry(flags.to_string())
113 .or_insert_with(Vec::new)
114 .push("true".to_string());
115 i += 1;
116 }
117 } else {
118 for flag_char in flags.chars() {
120 args.entry(flag_char.to_string())
121 .or_insert_with(Vec::new)
122 .push("true".to_string());
123 }
124 i += 1;
125 }
126 } else {
127 positional.push(strip_quotes(token));
129 i += 1;
130
131 while i < tokens.len() {
133 positional.push(strip_quotes(&tokens[i]));
134 i += 1;
135 }
136 break;
137 }
138 }
139
140 let image = positional.first().ok_or("No image specified")?.clone();
141 let command = positional.into_iter().skip(1).collect();
142
143 Ok((image, command, args))
144}
145
146pub fn build_compose_value(
147 args: &IndexMap<String, Vec<String>>,
148 network: &str,
149) -> Result<Value, String> {
150 let mappings = get_mappings();
151 let mut service = serde_yaml::Mapping::new();
152
153 for (key, values) in args {
154 if let Some(mapping) = mappings.get(key) {
155 if mapping.path.is_empty() {
156 continue; }
158
159 for value in values {
160 let path = mapping.path.replace("¤network¤", network);
161 apply_mapping(&mut service, &path, value, &mapping.arg_type)?;
162 }
163 }
164 }
165
166 Ok(Value::Mapping(service))
167}
168
169fn apply_mapping(
170 service: &mut serde_yaml::Mapping,
171 path: &str,
172 value: &str,
173 arg_type: &ArgType,
174) -> Result<(), String> {
175 let parts: Vec<&str> = path.split('/').collect();
176
177 match arg_type {
178 ArgType::Array => {
179 set_nested_array(service, &parts, value);
180 }
181 ArgType::Switch => {
182 let bool_val = value == "true";
183 set_nested_value(service, &parts, Value::Bool(bool_val));
184 }
185 ArgType::Value => {
186 if parts.len() > 0 && parts[parts.len() - 1] == "test" && parts.contains(&"healthcheck") {
188 let test_array = vec![
190 Value::String("CMD-SHELL".to_string()),
191 Value::String(value.to_string())
192 ];
193 set_nested_value(service, &parts, Value::Sequence(test_array));
194 } else {
195 set_nested_value(service, &parts, Value::String(value.to_string()));
196 }
197 }
198 ArgType::IntValue => {
199 let int_val = value.parse::<i64>()
200 .map_err(|_| format!("Invalid integer: {}", value))?;
201 set_nested_value(service, &parts, Value::Number(int_val.into()));
202 }
203 ArgType::FloatValue => {
204 let float_val = value.parse::<f64>()
205 .map_err(|_| format!("Invalid float: {}", value))?;
206 set_nested_value(service, &parts, Value::Number(serde_yaml::Number::from(float_val)));
207 }
208 ArgType::Envs => {
209 let env_value = if value.contains('=') {
210 let parts: Vec<&str> = value.splitn(2, '=').collect();
211 format!("{}={}", parts[0], strip_quotes(parts[1]))
212 } else {
213 value.to_string()
214 };
215 set_nested_array(service, &parts, &env_value);
216 }
217 ArgType::Map => {
218 let map = parse_key_value_list(value, ',', '=');
219 set_nested_map(service, &parts, map);
220 }
221 ArgType::MapArray => {
222 if value.starts_with("type=tmpfs") {
225 let tmpfs_value = convert_mount_to_tmpfs(value);
227 set_nested_array(service, &["tmpfs"], &tmpfs_value);
228 } else if value.starts_with("type=bind") || value.starts_with("type=volume") {
229 let volume_value = convert_mount_to_volume(value);
231 set_nested_array(service, &["volumes"], &volume_value);
232 } else {
233 set_nested_array(service, &parts, value);
235 }
236 }
237 ArgType::Networks => {
238 if value.matches(|c| c == ':').count() == 0
239 && !["host", "bridge", "none"].contains(&value)
240 && !value.starts_with("container:") {
241 let mut network_map = IndexMap::new();
243 network_map.insert(
244 Value::String(value.to_string()),
245 Value::Mapping(serde_yaml::Mapping::new())
246 );
247 set_nested_value(service, &["networks"], Value::Mapping(network_map.into_iter().collect()));
248 } else {
249 set_nested_value(service, &["network_mode"], Value::String(value.to_string()));
251 }
252 }
253 ArgType::Ulimits => {
254 parse_ulimit(service, &parts, value)?;
255 }
256 ArgType::Gpus => {
257 parse_gpus(service, value)?;
258 }
259 ArgType::DeviceBlockIOConfigRate | ArgType::DeviceBlockIOConfigWeight => {
260 set_nested_value(service, &parts, Value::String(value.to_string()));
262 }
263 }
264
265 Ok(())
266}
267
268fn set_nested_value(map: &mut serde_yaml::Mapping, path: &[&str], value: Value) {
269 if path.is_empty() {
270 return;
271 }
272
273 if path.len() == 1 {
274 map.insert(Value::String(path[0].to_string()), value);
275 return;
276 }
277
278 let key = Value::String(path[0].to_string());
279 let nested = map.entry(key.clone())
280 .or_insert_with(|| Value::Mapping(serde_yaml::Mapping::new()));
281
282 if let Value::Mapping(nested_map) = nested {
283 set_nested_value(nested_map, &path[1..], value);
284 }
285}
286
287fn set_nested_array(map: &mut serde_yaml::Mapping, path: &[&str], value: &str) {
288 if path.is_empty() {
289 return;
290 }
291
292 if path.len() == 1 {
293 let key = Value::String(path[0].to_string());
294 let arr = map.entry(key.clone())
295 .or_insert_with(|| Value::Sequence(Vec::new()));
296
297 if let Value::Sequence(seq) = arr {
298 seq.push(Value::String(value.to_string()));
299 }
300 return;
301 }
302
303 let key = Value::String(path[0].to_string());
304 let nested = map.entry(key.clone())
305 .or_insert_with(|| Value::Mapping(serde_yaml::Mapping::new()));
306
307 if let Value::Mapping(nested_map) = nested {
308 set_nested_array(nested_map, &path[1..], value);
309 }
310}
311
312fn set_nested_map(map: &mut serde_yaml::Mapping, path: &[&str], value: IndexMap<String, Value>) {
313 if path.is_empty() {
314 return;
315 }
316
317 if path.len() == 1 {
318 let key = Value::String(path[0].to_string());
319 let existing = map.entry(key.clone())
320 .or_insert_with(|| Value::Mapping(serde_yaml::Mapping::new()));
321
322 if let Value::Mapping(existing_map) = existing {
323 for (k, v) in value {
324 existing_map.insert(Value::String(k), v);
325 }
326 }
327 return;
328 }
329
330 let key = Value::String(path[0].to_string());
331 let nested = map.entry(key.clone())
332 .or_insert_with(|| Value::Mapping(serde_yaml::Mapping::new()));
333
334 if let Value::Mapping(nested_map) = nested {
335 set_nested_map(nested_map, &path[1..], value);
336 }
337}
338
339fn parse_ulimit(map: &mut serde_yaml::Mapping, path: &[&str], value: &str) -> Result<(), String> {
340 let parts: Vec<&str> = value.splitn(2, '=').collect();
341 if parts.len() != 2 {
342 return Err(format!("Invalid ulimit format: {}", value));
343 }
344
345 let limit_name = parts[0];
346 let limit_value = parts[1];
347
348 let full_path = format!("{}/{}", path.join("/"), limit_name);
349 let full_parts: Vec<&str> = full_path.split('/').collect();
350
351 if limit_value.contains(':') {
352 let limits: Vec<&str> = limit_value.split(':').collect();
353 if limits.len() == 2 {
354 let soft = limits[0].parse::<i64>()
355 .map_err(|_| format!("Invalid soft limit: {}", limits[0]))?;
356 let hard = limits[1].parse::<i64>()
357 .map_err(|_| format!("Invalid hard limit: {}", limits[1]))?;
358
359 let mut limit_map = IndexMap::new();
360 limit_map.insert("soft".to_string(), Value::Number(soft.into()));
361 limit_map.insert("hard".to_string(), Value::Number(hard.into()));
362
363 set_nested_map(map, &full_parts, limit_map);
364 }
365 } else {
366 let limit = limit_value.parse::<i64>()
367 .map_err(|_| format!("Invalid limit: {}", limit_value))?;
368 set_nested_value(map, &full_parts, Value::Number(limit.into()));
369 }
370
371 Ok(())
372}
373
374fn convert_mount_to_tmpfs(mount_str: &str) -> String {
375 let mut destination = String::new();
379 let mut options = Vec::new();
380
381 for part in mount_str.split(',') {
382 let kv: Vec<&str> = part.splitn(2, '=').collect();
383 if kv.len() == 2 {
384 match kv[0] {
385 "destination" | "target" | "dst" => destination = kv[1].to_string(),
386 "tmpfs-size" => options.push(format!("size={}", kv[1])),
387 "tmpfs-mode" => {}, "type" => {}, _ => {}
390 }
391 }
392 }
393
394 options.insert(0, "rw".to_string());
396 options.insert(1, "noexec".to_string());
397 options.insert(2, "nosuid".to_string());
398
399 format!("{}:{}", destination, options.join(","))
400}
401
402fn convert_mount_to_volume(mount_str: &str) -> String {
403 let mut source = String::new();
405 let mut target = String::new();
406 let mut readonly = false;
407
408 for part in mount_str.split(',') {
409 if let Some(value) = part.strip_prefix("source=") {
410 source = value.to_string();
411 } else if let Some(value) = part.strip_prefix("target=") {
412 target = value.to_string();
413 } else if let Some(value) = part.strip_prefix("destination=") {
414 target = value.to_string();
415 } else if part == "readonly" || part == "ro" {
416 readonly = true;
417 }
418 }
419
420 if !source.is_empty() && !target.is_empty() {
422 if readonly {
423 format!("{}:{}:ro", source, target)
424 } else {
425 format!("{}:{}", source, target)
426 }
427 } else {
428 mount_str.to_string()
430 }
431}
432
433fn parse_gpus(map: &mut serde_yaml::Mapping, value: &str) -> Result<(), String> {
434 let count_value = if value == "all" {
435 Value::String("all".to_string())
436 } else {
437 Value::Number(value.parse::<i64>()
438 .map_err(|_| format!("Invalid GPU count: {}", value))?.into())
439 };
440
441 let mut device = IndexMap::new();
442 device.insert("driver".to_string(), Value::String("nvidia".to_string()));
443 device.insert("count".to_string(), count_value);
444 device.insert("capabilities".to_string(), Value::Sequence(vec![Value::String("gpu".to_string())]));
445
446 let mut devices = IndexMap::new();
447 devices.insert("devices".to_string(), Value::Sequence(vec![
448 Value::Mapping(device.into_iter().map(|(k, v)| (Value::String(k), v)).collect())
449 ]));
450
451 let mut reservations = IndexMap::new();
452 reservations.insert("reservations".to_string(), Value::Mapping(
453 devices.into_iter().map(|(k, v)| (Value::String(k), v)).collect()
454 ));
455
456 let mut resources = IndexMap::new();
457 resources.insert("resources".to_string(), Value::Mapping(
458 reservations.into_iter().map(|(k, v)| (Value::String(k), v)).collect()
459 ));
460
461 set_nested_map(map, &["deploy"], resources);
462
463 Ok(())
464}