dockerfile_parser_rs/
file.rs1use 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#[derive(Debug)]
29pub struct Dockerfile {
30 pub path: PathBuf,
31 pub instructions: Vec<Instruction>,
32}
33
34impl Dockerfile {
35 pub fn new(path: PathBuf, instructions: Vec<Instruction>) -> Self {
39 Dockerfile { path, instructions }
40 }
41
42 pub fn empty(path: PathBuf) -> Self {
46 Dockerfile::new(path, Vec::new())
47 }
48
49 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 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 if line.is_empty() {
103 instructions.push(Instruction::EMPTY);
104 } 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 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 pub fn steps(&self) -> usize {
152 self.instructions
153 .iter()
154 .filter(|i| !matches!(i, Instruction::EMPTY | Instruction::COMMENT { .. }))
155 .count()
156 }
157
158 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 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}