dockerfile_parser_rs/
ast.rs

1// https://docs.docker.com/reference/dockerfile/#overview
2
3use std::collections::BTreeMap;
4use std::fmt;
5
6use strum_macros::Display;
7use strum_macros::EnumString;
8
9#[derive(Debug, Display, EnumString)]
10#[strum(serialize_all = "lowercase")]
11/// This enum represents available protocols for the EXPOSE instruction in a Dockerfile.
12pub enum Protocol {
13    Tcp,
14    Udp,
15}
16
17#[derive(Debug)]
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(BTreeMap<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(BTreeMap<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(BTreeMap<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_btree_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, "{}", line)
165            }
166            Instruction::Label(labels) => {
167                write!(f, "LABEL {}", helpers::format_btree_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                let prefix = if options_string.is_empty() {
182                    String::new()
183                } else {
184                    format!("{} ", options_string)
185                };
186                write!(f, "RUN {}{:?}", prefix, command)
187            }
188            Instruction::Shell(shell) => write!(f, "SHELL {:?}", shell),
189            Instruction::User { user, group } => {
190                if let Some(group) = group {
191                    write!(f, "USER {}:{}", user, group)
192                } else {
193                    write!(f, "USER {}", user)
194                }
195            }
196            Instruction::Volume { mounts } => write!(f, "VOLUME {:?}", mounts),
197            Instruction::Workdir { path } => write!(f, "WORKDIR {}", path),
198            //-------------//
199            //    Extra    //
200            //-------------//
201            Instruction::Comment(comment) => write!(f, "{}", comment),
202            Instruction::Empty => write!(f, ""),
203        }
204    }
205}
206
207mod helpers {
208    use super::*;
209
210    pub fn format_instruction_option(key: &str, value: &Option<String>) -> String {
211        value
212            .as_ref()
213            .map(|v| format!("--{}={}", key, v))
214            .unwrap_or_default()
215    }
216
217    pub fn format_options_string(options: Vec<String>) -> String {
218        options
219            .into_iter()
220            .filter(|s| !s.is_empty())
221            .collect::<Vec<String>>()
222            .join(" ")
223    }
224
225    pub fn format_btree_map(pairs: &BTreeMap<String, String>) -> String {
226        pairs
227            .iter()
228            .map(|(key, value)| format!("{}=\"{}\"", key, value))
229            .collect::<Vec<String>>()
230            .join(" ")
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_display_instruction_add() {
240        let instruction = Instruction::Add {
241            checksum: None,
242            chown: None,
243            chmod: None,
244            link: None,
245            sources: vec![String::from("source1"), String::from("source2")],
246            destination: String::from("/destination"),
247        };
248
249        let expected = "ADD source1 source2 /destination";
250        assert_eq!(format!("{}", instruction), expected);
251    }
252
253    #[test]
254    fn test_display_instruction_arg() {
255        let instruction = Instruction::Arg(BTreeMap::from([
256            (String::from("ARG2"), None),
257            (String::from("ARG1"), Some(String::from("value1"))),
258        ]));
259
260        // must be sorted
261        let expected = "ARG ARG1=value1 ARG2";
262        assert_eq!(format!("{}", instruction), expected);
263    }
264
265    #[test]
266    fn test_display_instruction_cmd() {
267        let instruction =
268            Instruction::Cmd(vec![String::from("echo"), String::from("Hello, World!")]);
269
270        let expected = "CMD [\"echo\", \"Hello, World!\"]";
271        assert_eq!(format!("{}", instruction), expected);
272    }
273
274    #[test]
275    fn test_display_instruction_copy() {
276        let instruction = Instruction::Copy {
277            from: Some(String::from("builder")),
278            chown: None,
279            chmod: None,
280            link: None,
281            sources: vec![String::from("source1"), String::from("source2")],
282            destination: String::from("/destination"),
283        };
284
285        let expected = "COPY --from=builder source1 source2 /destination";
286        assert_eq!(format!("{}", instruction), expected);
287    }
288
289    #[test]
290    fn test_display_instruction_entrypoint() {
291        let instruction = Instruction::Entrypoint(vec![String::from("entrypoint.sh")]);
292
293        let expected = "ENTRYPOINT [\"entrypoint.sh\"]";
294        assert_eq!(format!("{}", instruction), expected);
295    }
296
297    #[test]
298    fn test_display_instruction_env() {
299        let instruction = Instruction::Env(BTreeMap::from([
300            (String::from("ENV2"), String::from("value2")),
301            (String::from("ENV1"), String::from("value1")),
302        ]));
303
304        // must be sorted
305        let expected = "ENV ENV1=\"value1\" ENV2=\"value2\"";
306        assert_eq!(format!("{}", instruction), expected);
307    }
308
309    #[test]
310    fn test_display_instruction_expose() {
311        let instruction = Instruction::Expose {
312            port: String::from("8080"),
313            protocol: Some(Protocol::Udp),
314        };
315
316        let expected = "EXPOSE 8080/udp";
317        assert_eq!(format!("{}", instruction), expected);
318    }
319
320    #[test]
321    fn test_display_instruction_from() {
322        let instruction = Instruction::From {
323            platform: Some(String::from("linux/amd64")),
324            image: String::from("docker.io/library/fedora:latest"),
325            alias: Some(String::from("builder")),
326        };
327
328        let expected = "FROM --platform=linux/amd64 docker.io/library/fedora:latest AS builder";
329        assert_eq!(format!("{}", instruction), expected);
330    }
331
332    #[test]
333    fn test_display_instruction_label() {
334        let instruction = Instruction::Label(BTreeMap::from([
335            (String::from("version"), String::from("1.0")),
336            (String::from("maintainer"), String::from("John Doe")),
337        ]));
338
339        // must be sorted
340        let expected = "LABEL maintainer=\"John Doe\" version=\"1.0\"";
341        assert_eq!(format!("{}", instruction), expected);
342    }
343
344    #[test]
345    fn test_display_instruction_run() {
346        let instruction = Instruction::Run {
347            mount: None,
348            network: None,
349            security: None,
350            command: vec![String::from("echo"), String::from("Hello, World!")],
351        };
352
353        let expected = "RUN [\"echo\", \"Hello, World!\"]";
354        assert_eq!(format!("{}", instruction), expected);
355    }
356
357    #[test]
358    fn test_display_instruction_shell() {
359        let instruction = Instruction::Shell(vec![String::from("/bin/sh"), String::from("-c")]);
360
361        let expected = "SHELL [\"/bin/sh\", \"-c\"]";
362        assert_eq!(format!("{}", instruction), expected);
363    }
364
365    #[test]
366    fn test_display_instruction_user() {
367        let instruction = Instruction::User {
368            user: String::from("root"),
369            group: Some(String::from("root")),
370        };
371
372        let expected = "USER root:root";
373        assert_eq!(format!("{}", instruction), expected);
374    }
375
376    #[test]
377    fn test_display_instruction_volume() {
378        let instruction = Instruction::Volume {
379            mounts: vec![String::from("/data"), String::from("/var/log")],
380        };
381
382        let expected = "VOLUME [\"/data\", \"/var/log\"]";
383        assert_eq!(format!("{}", instruction), expected);
384    }
385
386    #[test]
387    fn test_display_instruction_workdir() {
388        let instruction = Instruction::Workdir {
389            path: String::from("/app"),
390        };
391
392        let expected = "WORKDIR /app";
393        assert_eq!(format!("{}", instruction), expected);
394    }
395
396    #[test]
397    fn test_display_instruction_comment() {
398        let instruction = Instruction::Comment(String::from("# This is a comment"));
399
400        let expected = "# This is a comment";
401        assert_eq!(format!("{}", instruction), expected);
402    }
403
404    #[test]
405    fn test_display_instruction_empty() {
406        let instruction = Instruction::Empty;
407
408        let expected = "";
409        assert_eq!(format!("{}", instruction), expected);
410    }
411}