dockerfile_parser_rs/
file.rs

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