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> {
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 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 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 #[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 #[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 #[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 if line.is_empty() {
164 instructions.push(Instruction::Empty {});
165 } 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}