1use std::fmt;
18use std::str;
19
20#[derive(Clone, PartialEq, Debug)]
21pub struct Image {
22 registry: String,
23 namespace: String,
24 repo: String,
25 tag: String,
26}
27
28#[derive(Clone, PartialEq, Debug)]
29pub enum ResolvedParent {
30 Image(Image),
31 Stage(String),
32}
33
34#[derive(Clone, PartialEq, Debug)]
35pub struct UnresolvedParent(String);
36
37#[derive(Clone, PartialEq, Debug)]
38pub struct From<P> {
39 pub parent: P,
40 pub alias: Option<String>,
41}
42
43#[derive(Clone, PartialEq, Debug)]
44pub struct Copy(pub String);
45
46#[derive(Clone, PartialEq, Debug)]
47pub struct Run(pub String);
48
49#[derive(Clone, PartialEq, Debug)]
50pub struct Env(pub String);
51
52#[derive(Clone, PartialEq, Debug)]
53pub struct Arg(pub String);
54
55#[derive(Clone, PartialEq, Debug)]
56pub struct Workdir(pub String);
57
58#[derive(Clone, PartialEq, Debug)]
59pub enum Instruction<P> {
60 From(From<P>),
61 Run(Run),
62 Cmd(String),
63 Label(String, String),
64 Env(Env),
67 Copy(Copy),
69 Entrypoint(String),
70 Workdir(Workdir),
73 Arg(Arg),
74 }
79
80#[derive(Clone, PartialEq, Debug)]
81pub struct Dockerfile<P>(pub Vec<Instruction<P>>);
82
83pub type ResolvedDockerfile = Dockerfile<ResolvedParent>;
84
85impl Image {
86 pub fn from_repo_tag(repo: String, tag: String) -> Image {
87 Image {
88 registry: String::new(),
89 namespace: String::new(),
90 repo,
91 tag,
92 }
93 }
94}
95
96impl fmt::Display for Image {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 if !self.registry.is_empty() {
99 write!(f, "{}/", self.registry)?;
100 }
101 if !self.namespace.is_empty() {
102 write!(f, "{}/", self.namespace)?;
103 }
104 write!(f, "{}", self.repo)?;
105 if self.tag.is_empty() {
106 write!(f, ":latest") } else {
108 write!(f, ":{}", self.tag)
109 }
110 }
111}
112
113impl str::FromStr for Image {
114 type Err = String;
115
116 fn from_str(s: &str) -> Result<Self, Self::Err> {
117 match parser::image(s) {
118 Result::Ok(("", o)) => Ok(o),
119 Result::Ok((rem, _)) => Err(format!("Unexpected {}", rem)),
120 Result::Err(e) => Result::Err(format!("{}", e)),
121 }
122 }
123}
124
125impl<P> fmt::Display for From<P>
126where
127 P: fmt::Display,
128{
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 match &self.alias {
131 Some(a) => write!(f, "{} AS {}", self.parent, a),
132 None => write!(f, "{}", self.parent),
133 }
134 }
135}
136
137impl fmt::Display for ResolvedParent {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 match &self {
140 ResolvedParent::Image(i) => write!(f, "{}", i),
141 ResolvedParent::Stage(s) => write!(f, "{}", s),
142 }
143 }
144}
145
146impl fmt::Display for UnresolvedParent {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 write!(f, "{}", self.0)
149 }
150}
151
152impl fmt::Display for Run {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 write!(f, "{}", self.0)
155 }
156}
157
158impl fmt::Display for Copy {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 write!(f, "{}", self.0)
161 }
162}
163
164impl fmt::Display for Env {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 write!(f, "{}", self.0)
167 }
168}
169
170impl fmt::Display for Arg {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 write!(f, "{}", self.0)
173 }
174}
175
176impl fmt::Display for Workdir {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 write!(f, "{}", self.0)
179 }
180}
181
182impl str::FromStr for Dockerfile<UnresolvedParent> {
183 type Err = String;
184
185 fn from_str(s: &str) -> Result<Self, Self::Err> {
186 match parser::dockerfile(s) {
187 Result::Ok((_, o)) => Ok(o),
188 Result::Err(e) => Result::Err(format!("{}", e)),
189 }
190 }
191}
192
193impl<T> fmt::Display for Dockerfile<T>
194where
195 T: fmt::Display,
196{
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 for i in self.0.iter() {
199 match i {
200 Instruction::Arg(s) => writeln!(f, "ARG {}", s),
201 Instruction::Copy(s) => writeln!(f, "COPY {}", s),
202 Instruction::From(image) => writeln!(f, "\nFROM {}", image),
203 Instruction::Run(s) => writeln!(f, "RUN {}", s),
204 Instruction::Env(s) => writeln!(f, "ENV {}", s),
205 Instruction::Workdir(s) => writeln!(f, "WORKDIR {}", s),
206 Instruction::Entrypoint(s) => writeln!(f, "ENTRYPOINT {}", s),
207 Instruction::Cmd(s) => writeln!(f, "CMD {}", s),
208 Instruction::Label(k, v) => writeln!(f, "LABEL {:?}={:?}", k, v),
209 }?;
210 }
211 Ok(())
212 }
213}
214
215pub mod parser {
216 use crate::logic::parser::{IResult, Span};
217
218 use super::*;
219
220 use nom::{
221 branch::alt,
222 bytes::complete::{is_not, tag_no_case},
223 character::complete::{alpha1, alphanumeric1, char, line_ending, one_of, space0, space1},
224 combinator::{eof, map, opt, peek, recognize, value},
225 multi::{many0, many1, separated_list1},
226 sequence::{delimited, pair, preceded, terminated, tuple},
227 };
228 use nom_supreme::tag::complete::tag;
229
230 pub fn alias_identifier(i: &str) -> IResult<&str, &str> {
232 recognize(pair(
233 alpha1,
234 many0(alt((alphanumeric1, tag("_"), tag("-")))),
235 ))(i)
236 }
237
238 pub fn repo_identifier(i: &str) -> IResult<&str, &str> {
240 recognize(pair(
241 alt((alphanumeric1, tag("_"))),
242 many0(alt((alphanumeric1, tag("_"), tag("-"), tag("/")))),
243 ))(i)
244 }
245
246 pub fn tag_identifier(i: &str) -> IResult<&str, &str> {
248 recognize(many0(alt((alphanumeric1, tag("_"), tag("-"), tag(".")))))(i)
249 }
250
251 fn line_continuation(i: &str) -> IResult<&str, ()> {
252 value(
253 (), tuple((char('\\'), space0, line_ending)),
255 )(i)
256 }
257
258 fn empty_line(i: &str) -> IResult<&str, ()> {
259 value(
260 (), alt((preceded(space0, line_ending), preceded(space1, peek(eof)))),
262 )(i)
263 }
264
265 fn empty_line_for_span(i: Span) -> IResult<Span, ()> {
266 value(
267 (), alt((preceded(space0, line_ending), preceded(space1, peek(eof)))),
269 )(i)
270 }
271
272 pub fn ignored_line(i: &str) -> IResult<&str, ()> {
273 value(
274 (), alt((comment_line, empty_line)),
276 )(i)
277 }
278
279 pub fn ignored_line_for_span(i: Span) -> IResult<Span, ()> {
280 value(
281 (), alt((comment_line_for_span, empty_line_for_span)),
283 )(i)
284 }
285
286 fn continuation_with_comments(i: &str) -> IResult<&str, ()> {
288 value(
289 (), terminated(line_continuation, many0(comment_line)),
291 )(i)
292 }
293
294 fn space(i: &str) -> IResult<&str, ()> {
295 value(
296 (), one_of(" \t"),
298 )(i)
299 }
300
301 fn optional_space(i: &str) -> IResult<&str, ()> {
302 value(
303 (), many0(alt((space, continuation_with_comments))),
305 )(i)
306 }
307
308 pub fn mandatory_space(i: &str) -> IResult<&str, ()> {
309 value(
310 (), delimited(many0(continuation_with_comments), space, optional_space),
312 )(i)
313 }
314
315 fn comment(i: &str) -> IResult<&str, &str> {
316 preceded(char('#'), is_not("\n\r"))(i)
317 }
318
319 fn comment_for_span(i: Span) -> IResult<Span, Span> {
320 preceded(char('#'), is_not("\n\r"))(i)
321 }
322
323 fn comment_line(i: &str) -> IResult<&str, ()> {
324 value(
325 (), delimited(space0, comment, alt((line_ending, peek(eof)))),
327 )(i)
328 }
329
330 fn comment_line_for_span(i: Span) -> IResult<Span, ()> {
331 value(
332 (), delimited(space0, comment_for_span, alt((line_ending, peek(eof)))),
334 )(i)
335 }
336
337 fn parent(i: &str) -> IResult<&str, UnresolvedParent> {
340 map(recognize(image), |s| UnresolvedParent(s.into()))(i)
341 }
342
343 fn from_content(i: &str) -> IResult<&str, From<UnresolvedParent>> {
344 map(
345 pair(
346 parent,
347 opt(map(
348 preceded(
349 delimited(optional_space, tag("AS"), space),
350 alias_identifier,
351 ),
352 String::from,
353 )),
354 ),
355 |(parent, alias)| From { parent, alias },
356 )(i)
357 }
358
359 pub fn multiline_string(i: &str) -> IResult<&str, String> {
360 let one_line = map(
361 many1(alt((
362 is_not("\\\n\r"),
363 recognize(tuple((char('\\'), many0(space), is_not(" \n\r")))),
364 ))),
365 |s| s.join(""),
366 );
367 let body = map(separated_list1(many1(line_continuation), one_line), |s| {
368 s.join("")
369 });
370 preceded(many0(line_continuation), body)(i)
371 }
372
373 pub fn from_instr(i: &str) -> IResult<&str, From<UnresolvedParent>> {
374 preceded(pair(tag_no_case("FROM"), mandatory_space), from_content)(i)
375 }
376
377 pub fn env_instr(i: &str) -> IResult<&str, Env> {
378 let body = map(multiline_string, Env);
379 preceded(pair(tag_no_case("ENV"), mandatory_space), body)(i)
380 }
381
382 pub fn copy_instr(i: &str) -> IResult<&str, Copy> {
383 let body = map(multiline_string, Copy);
384 preceded(pair(tag_no_case("COPY"), mandatory_space), body)(i)
385 }
386
387 pub fn arg_instr(i: &str) -> IResult<&str, Arg> {
388 let body = map(multiline_string, Arg);
389 preceded(pair(tag_no_case("ARG"), mandatory_space), body)(i)
390 }
391
392 pub fn run_instr(i: &str) -> IResult<&str, Run> {
393 let body = map(multiline_string, Run);
394 preceded(pair(tag_no_case("RUN"), mandatory_space), body)(i)
395 }
396
397 pub fn workdir_instr(i: &str) -> IResult<&str, Workdir> {
398 let body = map(multiline_string, Workdir);
399 preceded(pair(tag_no_case("WORKDIR"), mandatory_space), body)(i)
400 }
401
402 fn docker_instruction(i: &str) -> IResult<&str, Instruction<UnresolvedParent>> {
403 alt((
404 map(
405 terminated(from_instr, alt((line_ending, peek(eof)))),
406 Instruction::From,
407 ),
408 map(
409 terminated(copy_instr, alt((line_ending, peek(eof)))),
410 Instruction::Copy,
411 ),
412 map(
413 terminated(arg_instr, alt((line_ending, peek(eof)))),
414 Instruction::Arg,
415 ),
416 map(
417 terminated(run_instr, alt((line_ending, peek(eof)))),
418 Instruction::Run,
419 ),
420 map(
421 terminated(env_instr, alt((line_ending, peek(eof)))),
422 Instruction::Env,
423 ),
424 map(
425 terminated(workdir_instr, alt((line_ending, peek(eof)))),
426 Instruction::Workdir,
427 ),
428 ))(i)
429 }
430
431 pub fn dockerfile(i: &str) -> IResult<&str, Dockerfile<UnresolvedParent>> {
432 map(
433 terminated(
434 many0(preceded(many0(ignored_line), docker_instruction)),
435 terminated(many0(ignored_line), eof),
436 ),
437 Dockerfile,
438 )(i)
439 }
440
441 pub fn host_identifier(i: &str) -> IResult<&str, &str> {
442 recognize(delimited(
443 many0(alt((alphanumeric1, tag("_"), tag("-")))),
444 tag("."), many0(alt((alphanumeric1, tag("_"), tag("-"), tag(".")))),
446 ))(i)
447 }
448
449 pub fn image(i: &str) -> IResult<&str, Image> {
451 map(
452 pair(
453 opt(terminated(host_identifier, tag("/"))),
454 pair(
455 opt(recognize(many1(terminated(
456 many0(alt((alphanumeric1, tag("_"), tag("-")))),
457 tag("/"),
458 )))),
459 pair(repo_identifier, opt(preceded(tag(":"), tag_identifier))),
460 ),
461 ),
462 |(registry, (namespace, (repo, tag)))| Image {
463 registry: registry.unwrap_or("").into(),
464 namespace: match namespace {
465 Some(s) => {
466 let mut n = s.to_string();
467 n.pop();
468 n
469 }
470 None => String::new(),
471 },
472 repo: repo.into(),
473 tag: tag.unwrap_or("").into(),
474 },
475 )(i)
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 fn from_ubuntu_latest() -> From<UnresolvedParent> {
484 From {
485 parent: UnresolvedParent("ubuntu".into()),
486 alias: None,
487 }
488 }
489
490 fn from_ubuntu_20_04() -> From<UnresolvedParent> {
491 From {
492 parent: UnresolvedParent("ubuntu:20.04".into()),
493 alias: None,
494 }
495 }
496
497 #[test]
498 fn only_simple_from() {
499 let f = Instruction::From(from_ubuntu_latest());
500 let e = Dockerfile(vec![f]);
501 assert_eq!(Ok(e), "FROM ubuntu\n".parse());
502 }
503
504 #[test]
505 fn only_from_with_tag() {
506 let f = Instruction::From(from_ubuntu_20_04());
507 let e = Dockerfile(vec![f]);
508 assert_eq!(Ok(e), "FROM ubuntu:20.04\n".parse());
509 }
510
511 #[test]
512 fn two_instructions() {
513 let f = Instruction::From(from_ubuntu_latest());
514 let r = Instruction::Run(Run("ls".into()));
515 let e = Dockerfile(vec![f, r]);
516 assert_eq!(Ok(e), "FROM ubuntu\nRUN ls\n".parse());
517 }
518
519 #[test]
520 fn no_newline() {
521 let f = Instruction::From(from_ubuntu_latest());
522 let r = Instruction::Run(Run("ls".into()));
523 let e = Dockerfile(vec![f, r]);
524 assert_eq!(Ok(e), "FROM ubuntu\nRUN ls".parse());
525 }
526
527 #[test]
528 fn empty_line() {
529 let f = Instruction::From(from_ubuntu_latest());
530 let r = Instruction::Run(Run("ls".into()));
531 let e = Dockerfile(vec![f, r]);
532 assert_eq!(Ok(e), "\nFROM ubuntu\n \nRUN ls\n \n".parse());
533 }
534
535 #[test]
536 fn comment() {
537 let f = Instruction::From(from_ubuntu_latest());
538 let e = Dockerfile(vec![f]);
539 assert_eq!(Ok(e), "# hello world\nFROM ubuntu".parse());
540 }
541
542 #[test]
543 fn from_line_continuation_with_comment() {
544 let f = Instruction::From(from_ubuntu_latest());
545 let e = Dockerfile(vec![f]);
546 assert_eq!(Ok(e), "FROM\\\n# hello world\n ubuntu".parse());
547 }
548
549 #[test]
550 fn from_line_continuation() {
551 let f = Instruction::From(from_ubuntu_latest());
552 let e = Dockerfile(vec![f]);
553 assert_eq!(Ok(e), "FROM\\ \n ubuntu".parse());
554 }
555
556 #[test]
557 fn run_line_continuation_beginning() {
558 let f = Instruction::From(from_ubuntu_latest());
559 let r = Instruction::Run(Run("ls -la".into()));
560 let e = Dockerfile(vec![f, r]);
561 assert_eq!(Ok(e), "FROM ubuntu\nRUN\\\n ls -la".parse());
562 }
563
564 #[test]
565 fn run_line_continuation_middle() {
566 let f = Instruction::From(from_ubuntu_latest());
567 let r = Instruction::Run(Run("ls -la".into()));
568 let e = Dockerfile(vec![f, r]);
569 assert_eq!(Ok(e), "FROM ubuntu\nRUN ls \\\n-la".parse());
570 }
571
572 #[test]
573 fn parse_simple_image() {
574 let i = Image::from_repo_tag("ubuntu".into(), "latest".into());
575 assert_eq!(Ok(i), "ubuntu:latest".parse());
576 }
577
578 #[test]
579 fn parse_complex_image() {
580 let i = Image {
581 registry: "registry.access.redhat.com".into(),
582 namespace: "rhel7".into(),
583 repo: "rhel".into(),
584 tag: "7.3-53".into(),
585 };
586 assert_eq!(
587 ("", i),
588 parser::image("registry.access.redhat.com/rhel7/rhel:7.3-53").unwrap()
589 );
590 }
591
592 #[test]
593 fn parse_long_namespace() {
594 let i = Image {
595 registry: "".into(),
596 namespace: "a/b/c".into(),
597 repo: "r".into(),
598 tag: "".into(),
599 };
600 assert_eq!(("", i), parser::image("a/b/c/r").unwrap());
601 }
602}