dockerfile_parser_rs/
file.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct Dockerfile {
31 pub path: PathBuf,
32 pub instructions: Vec<Instruction>,
33}
34
35impl Dockerfile {
36 #[must_use]
40 pub const fn new(path: PathBuf, instructions: Vec<Instruction>) -> Self {
41 Self { path, instructions }
42 }
43
44 #[must_use]
48 pub const fn empty(path: PathBuf) -> Self {
49 Self::new(path, Vec::new())
50 }
51
52 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 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 if line.is_empty() {
114 instructions.push(Instruction::Empty);
115 } 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 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 #[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 #[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 #[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}