composerize_np/
parser.rs

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    // Handle bash-style line continuation (backslash + newline)
7    let cleaned = input
8        .trim()
9        .replace("\\\n", " ")  // Bash-style: \ + newline
10        .replace("\\\r\n", " ") // Windows-style: \ + CRLF
11        .replace('\n', " ")
12        .split_whitespace()
13        .collect::<Vec<_>>()
14        .join(" ");
15
16    // Remove docker/podman run/create
17    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    // Improved tokenizer with escaping support
27    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                // Escaping - add next character as is
36                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            // Check for --flag=value format
74            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                // Boolean flag
84                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                // Flag with value via space
90                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                // Unknown flag without value - treat as boolean
96                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 it's a single character, check for value
105            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                // Multiple boolean flags (e.g., -it)
119                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            // This is image or command
128            positional.push(strip_quotes(token));
129            i += 1;
130            
131            // Everything else is command
132            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; // Ignore (e.g., --rm, --detached)
157            }
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            // Special handling for healthcheck test
187            if parts.len() > 0 && parts[parts.len() - 1] == "test" && parts.contains(&"healthcheck") {
188                // Convert to CMD-SHELL format
189                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            // MapArray is an array of objects (e.g., for --mount)
223            // Check mount type
224            if value.starts_with("type=tmpfs") {
225                // Convert mount format to docker run style for tmpfs
226                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                // Convert mount to short syntax for volumes
230                let volume_value = convert_mount_to_volume(value);
231                set_nested_array(service, &["volumes"], &volume_value);
232            } else {
233                // Regular mount (bind, volume) goes to volumes
234                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                // Named network
242                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                // network_mode
250                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            // Эти типы обрабатываются в оригинале, но пока упрощаем
261            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    // Converts --mount type=tmpfs,destination=/tmp,tmpfs-size=256m,tmpfs-mode=1777
376    // to format /tmp:rw,noexec,nosuid,size=256m
377    
378    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" => {}, // Ignore mode, compose doesn't support it
388                "type" => {}, // Skip type
389                _ => {}
390            }
391        }
392    }
393    
394    // Add standard security options
395    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    // Parse --mount format: type=bind,source=/path,target=/path,readonly
404    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    // Convert to short syntax: source:target[:ro]
421    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        // If parsing failed, return as is
429        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}