dockerfile_parser_rs/
file.rs1use 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#[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 #[must_use]
53 pub const fn new(instructions: Vec<Instruction>) -> Self {
54 Self { instructions }
55 }
56
57 #[must_use]
59 pub const fn empty() -> Self {
60 Self::new(Vec::new())
61 }
62
63 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 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 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 #[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 #[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 #[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 if line.is_empty() {
163 instructions.push(Instruction::Empty {});
164 } 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}