Skip to main content

dockerfile_parser_rs/
file.rs

1use std::fs::File;
2use std::io::BufRead;
3use std::io::BufReader;
4use std::io::Write;
5use std::path::PathBuf;
6use std::str::FromStr;
7
8use serde::Deserialize;
9use serde::Serialize;
10
11use crate::ParseResult;
12use crate::ast::Instruction;
13use crate::error::ParseError;
14use crate::parser::instructions::add;
15use crate::parser::instructions::arg;
16use crate::parser::instructions::cmd;
17use crate::parser::instructions::copy;
18use crate::parser::instructions::entrypoint;
19use crate::parser::instructions::env;
20use crate::parser::instructions::expose;
21use crate::parser::instructions::from;
22use crate::parser::instructions::label;
23use crate::parser::instructions::run;
24use crate::parser::instructions::shell;
25use crate::parser::instructions::stopsignal;
26use crate::parser::instructions::user;
27use crate::parser::instructions::volume;
28use crate::parser::instructions::workdir;
29use crate::symbols::chars::HASHTAG;
30use crate::utils::process_dockerfile_content;
31use crate::utils::split_instruction_and_arguments;
32
33/// This struct represents a Dockerfile instance.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct Dockerfile {
36    pub instructions: Vec<Instruction>,
37}
38
39impl FromStr for Dockerfile {
40    type Err = ParseError;
41
42    fn from_str(s: &str) -> Result<Self, Self::Err> {
43        let lines = process_dockerfile_content(s.lines().map(String::from));
44
45        let instructions = parse(lines)?;
46        Ok(Self::new(instructions))
47    }
48}
49
50impl Dockerfile {
51    /// Create a new `Dockerfile` instance for the given instructions.
52    #[must_use]
53    pub const fn new(instructions: Vec<Instruction>) -> Self {
54        Self { instructions }
55    }
56
57    /// Create an empty `Dockerfile` instance.
58    #[must_use]
59    pub const fn empty() -> Self {
60        Self::new(Vec::new())
61    }
62
63    /// Parse the content of the Dockerfile and return a populated `Dockerfile` instance.
64    ///
65    /// The file is read line by line, preserving empty lines and comments.
66    ///
67    /// ## Example
68    ///
69    /// ```no_run
70    /// use std::path::PathBuf;
71    ///
72    /// use dockerfile_parser_rs::Dockerfile;
73    /// use dockerfile_parser_rs::ParseResult;
74    ///
75    /// fn main() -> ParseResult<()> {
76    ///     let path = PathBuf::from("./Dockerfile");
77    ///
78    ///     let dockerfile = Dockerfile::from(path)?;
79    ///     let dockerfile_json = dockerfile.to_json()?;
80    ///
81    ///     println!("{dockerfile_json}");
82    ///     Ok(())
83    /// }
84    /// ```
85    ///
86    /// ## Errors
87    ///
88    /// Return an error if the file cannot be opened or if there is a syntax error in the Dockerfile.
89    pub fn from(path: PathBuf) -> ParseResult<Self> {
90        let file = File::open(path).map_err(|e| ParseError::FileError(e.to_string()))?;
91        let reader = BufReader::new(file);
92        let lines = process_dockerfile_content(reader.lines().map_while(Result::ok));
93
94        let instructions = parse(lines)?;
95        Ok(Self::new(instructions))
96    }
97
98    /// Dump the instructions to a file.
99    ///
100    /// If the file does not exist, it will be created.
101    /// If the file exists, it will be overwritten.
102    ///
103    /// ## Errors
104    ///
105    /// Return an error if the file cannot be created or written to.
106    pub fn dump(&self, path: PathBuf) -> ParseResult<()> {
107        let mut file = File::create(path).map_err(|e| ParseError::FileError(e.to_string()))?;
108        for instruction in &self.instructions {
109            writeln!(file, "{instruction}").map_err(|e| ParseError::FileError(e.to_string()))?;
110        }
111        Ok(())
112    }
113
114    /// Serialize the Dockerfile in JSON format.
115    ///
116    /// ## Errors
117    ///
118    /// Return an error if the Dockerfile cannot be serialized to JSON.
119    pub fn to_json(&self) -> ParseResult<String> {
120        let json = serde_json::to_string_pretty(self)
121            .map_err(|e| ParseError::InternalError(e.to_string()))?;
122        Ok(json)
123    }
124
125    /// Return the number of instructions in the Dockerfile.
126    #[must_use]
127    pub fn steps(&self) -> usize {
128        self.instructions
129            .iter()
130            .filter(|i| !matches!(i, Instruction::Empty {} | Instruction::Comment { .. }))
131            .count()
132    }
133
134    /// Return the number of layers in the Dockerfile.
135    #[must_use]
136    pub fn layers(&self) -> usize {
137        self.instructions
138            .iter()
139            .filter(|i| {
140                matches!(
141                    i,
142                    Instruction::Add { .. } | Instruction::Copy { .. } | Instruction::Run { .. }
143                )
144            })
145            .count()
146    }
147
148    /// Return the number of stages in the Dockerfile.
149    #[must_use]
150    pub fn stages(&self) -> usize {
151        self.instructions
152            .iter()
153            .filter(|i| matches!(i, Instruction::From { .. }))
154            .count()
155    }
156}
157
158fn parse(lines: Vec<String>) -> ParseResult<Vec<Instruction>> {
159    let mut instructions = Vec::new();
160
161    for line in lines {
162        // preserve empty lines
163        if line.is_empty() {
164            instructions.push(Instruction::Empty {});
165        // preserve comments
166        } else if line.starts_with(HASHTAG) {
167            instructions.push(Instruction::Comment(line.clone()));
168        } else {
169            let (instruction, arguments) = split_instruction_and_arguments(&line)?;
170            let instruction = match instruction.as_str() {
171                "ADD" => add::parse(&arguments),
172                "ARG" => Ok(arg::parse(&arguments)),
173                "CMD" => Ok(cmd::parse(&arguments)),
174                "COPY" => copy::parse(&arguments),
175                "ENTRYPOINT" => Ok(entrypoint::parse(&arguments)),
176                "ENV" => Ok(env::parse(&arguments)),
177                "EXPOSE" => Ok(expose::parse(arguments)),
178                "LABEL" => Ok(label::parse(&arguments)),
179                "FROM" => from::parse(&arguments),
180                "RUN" => run::parse(&arguments),
181                "SHELL" => shell::parse(&arguments),
182                "STOPSIGNAL" => stopsignal::parse(&arguments),
183                "USER" => user::parse(&arguments),
184                "VOLUME" => Ok(volume::parse(&arguments)),
185                "WORKDIR" => workdir::parse(&arguments),
186                _ => return Err(ParseError::UnknownInstruction(instruction)),
187            }?;
188            instructions.push(instruction);
189        }
190    }
191    Ok(instructions)
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    fn mock_dummy_dockerfile() -> 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
228        Dockerfile::new(instructions)
229    }
230
231    #[test]
232    fn test_dockerfile_from_str() {
233        let mut content = String::new();
234        content.push_str("FROM docker.io/library/fedora:latest\n");
235        content.push_str("RUN cat /etc/os-release\n");
236        content.push_str("FROM docker.io/library/ubuntu:latest\n");
237        content.push_str("COPY file.txt /tmp/file.txt\n");
238        content.push_str("ENTRYPOINT [\"/bin/bash\"]\n");
239
240        let dockerfile = Dockerfile::from_str(&content).unwrap();
241        assert_eq!(dockerfile.steps(), 5);
242        assert_eq!(dockerfile.layers(), 2);
243        assert_eq!(dockerfile.stages(), 2);
244    }
245
246    #[test]
247    fn test_dockerfile_steps() {
248        let dockerfile = mock_dummy_dockerfile();
249        assert_eq!(dockerfile.steps(), 5);
250    }
251
252    #[test]
253    fn test_dockerfile_layers() {
254        let dockerfile = mock_dummy_dockerfile();
255        assert_eq!(dockerfile.layers(), 2);
256    }
257
258    #[test]
259    fn test_dockerfile_stages() {
260        let dockerfile = mock_dummy_dockerfile();
261        assert_eq!(dockerfile.stages(), 2);
262    }
263}