dockerfile_parser_rs/
ast.rs

1// https://docs.docker.com/reference/dockerfile/#overview
2
3use std::collections::HashMap;
4use std::fmt;
5
6use strum_macros::EnumString;
7
8#[derive(Debug, EnumString)]
9#[strum(serialize_all = "lowercase")]
10/// This enum represents available protocols for the EXPOSE instruction in a Dockerfile.
11pub enum Protocol {
12    Tcp,
13    Udp,
14}
15
16#[derive(Debug, EnumString)]
17#[strum(serialize_all = "UPPERCASE")]
18/// This enum represents available instructions in a Dockerfile.
19pub enum Instruction {
20    Add {
21        checksum: Option<String>,
22        chown: Option<String>,
23        chmod: Option<String>,
24        link: Option<String>,
25        sources: Vec<String>,
26        destination: String,
27    },
28    Arg(HashMap<String, Option<String>>),
29    Cmd(Vec<String>),
30    Copy {
31        from: Option<String>,
32        chown: Option<String>,
33        chmod: Option<String>,
34        link: Option<String>,
35        sources: Vec<String>,
36        destination: String,
37    },
38    Entrypoint(Vec<String>),
39    Env(HashMap<String, String>),
40    Expose {
41        port: String,
42        protocol: Option<Protocol>,
43    },
44    From {
45        platform: Option<String>,
46        image: String,
47        alias: Option<String>,
48    },
49    Label(HashMap<String, String>),
50    Run {
51        mount: Option<String>,
52        network: Option<String>,
53        security: Option<String>,
54        command: Vec<String>,
55    },
56    Shell(Vec<String>),
57    User {
58        user: String,
59        group: Option<String>,
60    },
61    Volume {
62        mounts: Vec<String>,
63    },
64    Workdir {
65        path: String,
66    },
67    //-------------//
68    //    Extra    //
69    //-------------//
70    Comment(String),
71    Empty,
72}
73
74impl fmt::Display for Instruction {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        match self {
77            Instruction::Add {
78                checksum,
79                chown,
80                chmod,
81                link,
82                sources,
83                destination,
84            } => {
85                let options = vec![
86                    helpers::format_instruction_option("checksum", checksum),
87                    helpers::format_instruction_option("chown", chown),
88                    helpers::format_instruction_option("chmod", chmod),
89                    helpers::format_instruction_option("link", link),
90                ];
91                let options_string = helpers::format_options_string(options);
92                let prefix = if options_string.is_empty() {
93                    String::new()
94                } else {
95                    format!("{} ", options_string)
96                };
97                write!(f, "ADD {}{} {}", prefix, sources.join(" "), destination)
98            }
99            Instruction::Arg(args) => {
100                let arg_string = args
101                    .iter()
102                    .map(|(key, value)| {
103                        if let Some(default) = value {
104                            format!("{}={}", key, default)
105                        } else {
106                            key.to_owned()
107                        }
108                    })
109                    .collect::<Vec<String>>()
110                    .join(" ");
111                write!(f, "ARG {}", arg_string)
112            }
113            Instruction::Cmd(cmd) => write!(f, "CMD {:?}", cmd),
114            Instruction::Copy {
115                from,
116                chown,
117                chmod,
118                link,
119                sources,
120                destination,
121            } => {
122                let options = vec![
123                    helpers::format_instruction_option("from", from),
124                    helpers::format_instruction_option("chown", chown),
125                    helpers::format_instruction_option("chmod", chmod),
126                    helpers::format_instruction_option("link", link),
127                ];
128                let options_string = helpers::format_options_string(options);
129                let prefix = if options_string.is_empty() {
130                    String::new()
131                } else {
132                    format!("{} ", options_string)
133                };
134                write!(f, "COPY {}{} {}", prefix, sources.join(" "), destination)
135            }
136            Instruction::Entrypoint(entrypoint) => write!(f, "ENTRYPOINT {:?}", entrypoint),
137            Instruction::Env(env) => {
138                write!(f, "ENV {}", helpers::format_hash_map(env))
139            }
140            Instruction::Expose { port, protocol } => {
141                if let Some(protocol) = protocol {
142                    write!(f, "EXPOSE {}/{:?}", port, protocol)
143                } else {
144                    write!(f, "EXPOSE {}", port)
145                }
146            }
147            Instruction::From {
148                platform,
149                image,
150                alias,
151            } => {
152                let options = vec![helpers::format_instruction_option("platform", platform)];
153                let options_string = helpers::format_options_string(options);
154                let prefix = if options_string.is_empty() {
155                    String::new()
156                } else {
157                    format!("{} ", options_string)
158                };
159                let mut line = format!("FROM {}{}", prefix, image);
160
161                if let Some(alias) = alias {
162                    line.push_str(&format!(" AS {}", alias));
163                }
164                write!(f, "FROM {}", line)
165            }
166            Instruction::Label(labels) => {
167                write!(f, "LABEL {}", helpers::format_hash_map(labels))
168            }
169            Instruction::Run {
170                mount,
171                network,
172                security,
173                command,
174            } => {
175                let options = vec![
176                    helpers::format_instruction_option("mount", mount),
177                    helpers::format_instruction_option("network", network),
178                    helpers::format_instruction_option("security", security),
179                ];
180                let options_string = helpers::format_options_string(options);
181                write!(f, "RUN {}{:?}", options_string, command)
182            }
183            Instruction::Shell(shell) => write!(f, "SHELL {:?}", shell),
184            Instruction::User { user, group } => {
185                if let Some(group) = group {
186                    write!(f, "USER {}:{}", user, group)
187                } else {
188                    write!(f, "USER {}", user)
189                }
190            }
191            Instruction::Volume { mounts } => write!(f, "VOLUME {:?}", mounts),
192            Instruction::Workdir { path } => write!(f, "WORKDIR {}", path),
193            //-------------//
194            //    Extra    //
195            //-------------//
196            Instruction::Comment(comment) => write!(f, "{}", comment),
197            Instruction::Empty => write!(f, ""),
198        }
199    }
200}
201
202mod helpers {
203    use super::*;
204
205    pub fn format_instruction_option(key: &str, value: &Option<String>) -> String {
206        value
207            .as_ref()
208            .map(|v| format!("--{}={}", key, v))
209            .unwrap_or_default()
210    }
211
212    pub fn format_options_string(options: Vec<String>) -> String {
213        options
214            .into_iter()
215            .filter(|s| !s.is_empty())
216            .collect::<Vec<String>>()
217            .join(" ")
218    }
219
220    pub fn format_hash_map(pairs: &HashMap<String, String>) -> String {
221        pairs
222            .iter()
223            .map(|(key, value)| format!("{}=\"{}\"", key, value))
224            .collect::<Vec<String>>()
225            .join(" ")
226    }
227}