dockerfile_parser_rs/
file.rs

1use std::fs::File;
2use std::io::Write;
3use std::path::PathBuf;
4
5use crate::ParseResult;
6use crate::ast::Instruction;
7use crate::error::ParseError;
8use crate::parser::instructions::add;
9use crate::parser::instructions::arg;
10use crate::parser::instructions::cmd;
11use crate::parser::instructions::copy;
12use crate::parser::instructions::entrypoint;
13use crate::parser::instructions::env;
14use crate::parser::instructions::expose;
15use crate::parser::instructions::from;
16use crate::parser::instructions::label;
17use crate::parser::instructions::run;
18use crate::parser::instructions::shell;
19use crate::parser::instructions::stopsignal;
20use crate::parser::instructions::user;
21use crate::parser::instructions::volume;
22use crate::parser::instructions::workdir;
23use crate::symbols::chars::HASHTAG;
24use crate::utils::read_lines;
25use crate::utils::split_instruction_and_arguments;
26
27/// This struct represents a Dockerfile instance.
28#[derive(Debug)]
29pub struct Dockerfile {
30    pub path: PathBuf,
31    pub instructions: Vec<Instruction>,
32}
33
34impl Dockerfile {
35    /// Creates a new `Dockerfile` instance for the given path and instructions.
36    ///
37    /// The actual file does not need to exist at this point.
38    pub fn new(path: PathBuf, instructions: Vec<Instruction>) -> Self {
39        Dockerfile { path, instructions }
40    }
41
42    /// Creates an empty `Dockerfile` instance for the given path.
43    ///
44    /// The actual file does not need to exist at this point.
45    pub fn empty(path: PathBuf) -> Self {
46        Dockerfile::new(path, Vec::new())
47    }
48
49    /// Parses the content of the Dockerfile and returns a populated `Dockerfile` instance.
50    ///
51    /// The file is read line by line, preserving empty lines and comments.
52    ///
53    /// # Example
54    ///
55    /// ```
56    /// use std::path::PathBuf;
57    ///
58    /// use dockerfile_parser_rs::Dockerfile;
59    /// use dockerfile_parser_rs::ParseResult;
60    ///
61    /// fn main() -> ParseResult<()> {
62    ///     let dockerfile = Dockerfile::from(PathBuf::from("./Dockerfile"))?;
63    ///     println!("{:#?}", dockerfile.instructions);
64    ///     Ok(())
65    /// }
66    /// ```
67    pub fn from(path: PathBuf) -> ParseResult<Self> {
68        let mut dockerfile = Dockerfile::empty(path);
69        dockerfile.instructions = dockerfile.parse()?;
70        Ok(dockerfile)
71    }
72
73    /// Parses the content of the Dockerfile and returns a vector of `Instruction` items.
74    ///
75    /// The file is read line by line, preserving empty lines and comments.
76    ///
77    /// **The attributes of the `Dockerfile` instance are not modified by this method.**
78    ///
79    /// # Example
80    ///
81    /// ```
82    /// use std::path::PathBuf;
83    ///
84    /// use dockerfile_parser_rs::Dockerfile;
85    /// use dockerfile_parser_rs::ParseResult;
86    ///
87    /// fn main() -> ParseResult<()> {
88    ///     let dockerfile = Dockerfile::empty(PathBuf::from("./Dockerfile"));
89    ///     let instructions = dockerfile.parse()?;
90    ///     println!("{:#?}", instructions);
91    ///     Ok(())
92    /// }
93    /// ```
94    pub fn parse(&self) -> ParseResult<Vec<Instruction>> {
95        let file = File::open(&self.path).map_err(|e| ParseError::FileError(e.to_string()))?;
96        let lines = read_lines(&file);
97
98        let mut instructions = Vec::new();
99
100        for line in lines {
101            // preserve empty lines
102            if line.is_empty() {
103                instructions.push(Instruction::EMPTY);
104            // preserve comments
105            } else if line.starts_with(HASHTAG) {
106                instructions.push(Instruction::COMMENT(line.to_owned()));
107            } else {
108                let (instruction, arguments) = split_instruction_and_arguments(&line)?;
109                let instruction = match instruction.as_str() {
110                    "ADD" => add::parse(arguments),
111                    "ARG" => arg::parse(arguments),
112                    "CMD" => cmd::parse(arguments),
113                    "COPY" => copy::parse(arguments),
114                    "ENTRYPOINT" => entrypoint::parse(arguments),
115                    "ENV" => env::parse(arguments),
116                    "EXPOSE" => expose::parse(arguments),
117                    "LABEL" => label::parse(arguments),
118                    "FROM" => from::parse(arguments),
119                    "RUN" => run::parse(arguments),
120                    "SHELL" => shell::parse(arguments),
121                    "STOPSIGNAL" => stopsignal::parse(arguments),
122                    "USER" => user::parse(arguments),
123                    "VOLUME" => volume::parse(arguments),
124                    "WORKDIR" => workdir::parse(arguments),
125                    _ => return Err(ParseError::UnknownInstruction(instruction)),
126                };
127                match instruction {
128                    Ok(instruction) => instructions.push(instruction),
129                    Err(e) => {
130                        return Err(ParseError::SyntaxError(format!("{line}: {e}")));
131                    }
132                }
133            }
134        }
135        Ok(instructions)
136    }
137
138    /// Dumps the current instructions into the Dockerfile.
139    ///
140    /// If the file does not exist, it will be created.
141    /// If the file exists, it will be overwritten.
142    pub fn dump(&self) -> std::io::Result<()> {
143        let mut file = File::create(&self.path)?;
144        for instruction in &self.instructions {
145            writeln!(file, "{instruction}")?;
146        }
147        Ok(())
148    }
149
150    /// Returns number of instructions in the Dockerfile.
151    pub fn steps(&self) -> usize {
152        self.instructions
153            .iter()
154            .filter(|i| !matches!(i, Instruction::EMPTY | Instruction::COMMENT { .. }))
155            .count()
156    }
157
158    /// Returns number of layers in the Dockerfile.
159    pub fn layers(&self) -> usize {
160        self.instructions
161            .iter()
162            .filter(|i| {
163                matches!(
164                    i,
165                    Instruction::ADD { .. } | Instruction::COPY { .. } | Instruction::RUN { .. }
166                )
167            })
168            .count()
169    }
170
171    /// Returns number of stages in the Dockerfile.
172    pub fn stages(&self) -> usize {
173        self.instructions
174            .iter()
175            .filter(|i| matches!(i, Instruction::FROM { .. }))
176            .count()
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    fn mock_dummy_dockerfile() -> Dockerfile {
185        let path = PathBuf::from("./Dockerfile");
186        let instructions = vec![
187            Instruction::FROM {
188                platform: None,
189                image: String::from("docker.io/library/fedora:latest"),
190                alias: Some(String::from("base")),
191            },
192            Instruction::RUN {
193                mount: None,
194                network: None,
195                security: None,
196                command: vec![
197                    String::from("echo"),
198                    String::from("hello"),
199                    String::from("world"),
200                ],
201                heredoc: None,
202            },
203            Instruction::FROM {
204                platform: None,
205                image: String::from("docker.io/library/ubuntu:latest"),
206                alias: Some(String::from("builder")),
207            },
208            Instruction::COPY {
209                from: Some(String::from("base")),
210                chown: None,
211                chmod: None,
212                link: None,
213                sources: vec![String::from("hello.txt")],
214                destination: String::from("/tmp/hello.txt"),
215            },
216            Instruction::ENTRYPOINT(vec![String::from("/bin/bash")]),
217        ];
218        Dockerfile::new(path, instructions)
219    }
220
221    #[test]
222    fn test_dockerfile_steps() {
223        let dockerfile = mock_dummy_dockerfile();
224        assert_eq!(dockerfile.steps(), 5);
225    }
226
227    #[test]
228    fn test_dockerfile_layers() {
229        let dockerfile = mock_dummy_dockerfile();
230        assert_eq!(dockerfile.layers(), 2);
231    }
232
233    #[test]
234    fn test_dockerfile_stages() {
235        let dockerfile = mock_dummy_dockerfile();
236        assert_eq!(dockerfile.stages(), 2);
237    }
238}