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    /// Creates 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    /// Creates an empty `Dockerfile` instance.
58    #[must_use]
59    pub const fn empty() -> Self {
60        Self::new(Vec::new())
61    }
62
63    /// Parses the content of the Dockerfile and returns 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    ///     dockerfile.to_json();
80    ///     Ok(())
81    /// }
82    /// ```
83    ///
84    /// ## Errors
85    ///
86    /// Returns an error if the file cannot be opened or if there is a syntax error in the Dockerfile.
87    pub fn from(path: PathBuf) -> ParseResult<Self> {
88        let file = File::open(path).map_err(|e| ParseError::FileError(e.to_string()))?;
89        let reader = BufReader::new(file);
90        let lines = process_dockerfile_content(reader.lines().map_while(Result::ok));
91
92        let instructions = parse(lines)?;
93        Ok(Self::new(instructions))
94    }
95
96    /// Dumps the instructions to a file.
97    ///
98    /// If the file does not exist, it will be created.
99    /// If the file exists, it will be overwritten.
100    ///
101    /// ## Errors
102    ///
103    /// Returns an error if the file cannot be created or written to.
104    pub fn dump(&self, path: PathBuf) -> ParseResult<()> {
105        let mut file = File::create(path).map_err(|e| ParseError::FileError(e.to_string()))?;
106        for instruction in &self.instructions {
107            writeln!(file, "{instruction}").map_err(|e| ParseError::FileError(e.to_string()))?;
108        }
109        Ok(())
110    }
111
112    /// Writes the Dockerfile to the standard output in JSON format.
113    ///
114    /// ## Errors
115    ///
116    /// Returns an error if the Dockerfile cannot be serialized to JSON.
117    pub fn to_json(&self) -> ParseResult<()> {
118        let json =
119            serde_json::to_string_pretty(self).map_err(|e| ParseError::FileError(e.to_string()))?;
120        println!("{json}");
121        Ok(())
122    }
123
124    /// Returns number of instructions in the Dockerfile.
125    #[must_use]
126    pub fn steps(&self) -> usize {
127        self.instructions
128            .iter()
129            .filter(|i| !matches!(i, Instruction::Empty {} | Instruction::Comment { .. }))
130            .count()
131    }
132
133    /// Returns number of layers in the Dockerfile.
134    #[must_use]
135    pub fn layers(&self) -> usize {
136        self.instructions
137            .iter()
138            .filter(|i| {
139                matches!(
140                    i,
141                    Instruction::Add { .. } | Instruction::Copy { .. } | Instruction::Run { .. }
142                )
143            })
144            .count()
145    }
146
147    /// Returns number of stages in the Dockerfile.
148    #[must_use]
149    pub fn stages(&self) -> usize {
150        self.instructions
151            .iter()
152            .filter(|i| matches!(i, Instruction::From { .. }))
153            .count()
154    }
155}
156
157fn parse(lines: Vec<String>) -> ParseResult<Vec<Instruction>> {
158    let mut instructions = Vec::new();
159
160    for line in lines {
161        // preserve empty lines
162        if line.is_empty() {
163            instructions.push(Instruction::Empty {});
164        // preserve comments
165        } else if line.starts_with(HASHTAG) {
166            instructions.push(Instruction::Comment(line.clone()));
167        } else {
168            let (instruction, arguments) = split_instruction_and_arguments(&line)?;
169            let instruction = match instruction.as_str() {
170                "ADD" => add::parse(&arguments),
171                "ARG" => Ok(arg::parse(&arguments)),
172                "CMD" => Ok(cmd::parse(&arguments)),
173                "COPY" => copy::parse(&arguments),
174                "ENTRYPOINT" => Ok(entrypoint::parse(&arguments)),
175                "ENV" => Ok(env::parse(&arguments)),
176                "EXPOSE" => Ok(expose::parse(arguments)),
177                "LABEL" => Ok(label::parse(&arguments)),
178                "FROM" => from::parse(&arguments),
179                "RUN" => run::parse(&arguments),
180                "SHELL" => shell::parse(&arguments),
181                "STOPSIGNAL" => stopsignal::parse(&arguments),
182                "USER" => user::parse(&arguments),
183                "VOLUME" => Ok(volume::parse(&arguments)),
184                "WORKDIR" => workdir::parse(&arguments),
185                _ => return Err(ParseError::UnknownInstruction(instruction)),
186            }?;
187            instructions.push(instruction);
188        }
189    }
190    Ok(instructions)
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    fn mock_dummy_dockerfile() -> Dockerfile {
198        let instructions = vec![
199            Instruction::From {
200                platform: None,
201                image: String::from("docker.io/library/fedora:latest"),
202                alias: Some(String::from("base")),
203            },
204            Instruction::Run {
205                mount: None,
206                network: None,
207                security: None,
208                command: vec![String::from("cat"), String::from("/etc/os-release")],
209                heredoc: None,
210            },
211            Instruction::From {
212                platform: None,
213                image: String::from("docker.io/library/ubuntu:latest"),
214                alias: Some(String::from("builder")),
215            },
216            Instruction::Copy {
217                from: Some(String::from("base")),
218                chown: None,
219                chmod: None,
220                link: None,
221                sources: vec![String::from("file.txt")],
222                destination: String::from("/tmp/file.txt"),
223            },
224            Instruction::Entrypoint(vec![String::from("/bin/bash")]),
225        ];
226
227        Dockerfile::new(instructions)
228    }
229
230    #[test]
231    fn test_dockerfile_from_str() {
232        let mut content = String::new();
233        content.push_str("FROM docker.io/library/fedora:latest\n");
234        content.push_str("RUN cat /etc/os-release\n");
235        content.push_str("FROM docker.io/library/ubuntu:latest\n");
236        content.push_str("COPY file.txt /tmp/file.txt\n");
237        content.push_str("ENTRYPOINT [\"/bin/bash\"]\n");
238
239        let dockerfile = Dockerfile::from_str(&content).unwrap();
240        assert_eq!(dockerfile.steps(), 5);
241        assert_eq!(dockerfile.layers(), 2);
242        assert_eq!(dockerfile.stages(), 2);
243    }
244
245    #[test]
246    fn test_dockerfile_steps() {
247        let dockerfile = mock_dummy_dockerfile();
248        assert_eq!(dockerfile.steps(), 5);
249    }
250
251    #[test]
252    fn test_dockerfile_layers() {
253        let dockerfile = mock_dummy_dockerfile();
254        assert_eq!(dockerfile.layers(), 2);
255    }
256
257    #[test]
258    fn test_dockerfile_stages() {
259        let dockerfile = mock_dummy_dockerfile();
260        assert_eq!(dockerfile.stages(), 2);
261    }
262}