From d048ea5201a9e5799a7828b6d7ae93e28e6c92a0 Mon Sep 17 00:00:00 2001 From: Kathryn Baldauf Date: Tue, 5 Aug 2025 16:33:07 -0700 Subject: [PATCH] Native builder: remove option token in favor of string literals (#450) Remove the option token from the dockerfile tokenizer for the native builder. This cleans up some of the logic around handling options depending on if they're instruction options or user provided options to a command. Signed-off-by: Kathryn Baldauf --- .../Docker/DockerfileParser.swift | 82 ++++++++++++++----- .../Docker/DockerfileTokenizer.swift | 24 ------ .../ContainerBuildParser/Types.swift | 13 --- .../TokenizerTests.swift | 64 ++++++++------- 4 files changed, 95 insertions(+), 88 deletions(-) diff --git a/Sources/NativeBuilder/ContainerBuildParser/Docker/DockerfileParser.swift b/Sources/NativeBuilder/ContainerBuildParser/Docker/DockerfileParser.swift index 96a9de7a..0dcd4b6c 100644 --- a/Sources/NativeBuilder/ContainerBuildParser/Docker/DockerfileParser.swift +++ b/Sources/NativeBuilder/ContainerBuildParser/Docker/DockerfileParser.swift @@ -74,6 +74,50 @@ public struct DockerfileParser: BuildParser { } } + private struct InstructionOpt { + let key: String + let value: String + } + + private func parseInstructionOpts(start: Int, tokens: [Token]) throws -> (Int, [InstructionOpt]) { + var done = false + var opts: [InstructionOpt] = [] + var index = start + + while index < tokens.endIndex, !done { + guard case .stringLiteral(let raw) = tokens[index] else { + done = true + break + } + + guard raw.hasPrefix("--") else { + done = true + break + } + + let components = raw.split(separator: "=", maxSplits: 1) + if components.count == 2 { + opts.append(InstructionOpt(key: String(components[0]), value: String(components[1]))) + } else { + // instruction options should ALWAYS have a value, so if we're at + // the end or the next value is a string list, that's invalid input + index += 1 + guard index < tokens.endIndex else { + throw ParseError.invalidOption(raw) + } + + guard case .stringLiteral(let value) = tokens[index] else { + throw ParseError.invalidOption(raw) + } + + opts.append(InstructionOpt(key: raw, value: value)) + } + index += 1 + } + + return (index, opts) + } + internal func tokensToFromInstruction(tokens: [Token]) throws -> FromInstruction { var index = tokens.startIndex index += 1 // skip the instruction @@ -82,16 +126,18 @@ public struct DockerfileParser: BuildParser { var platform: String? var imageName: String? - // Step 1: parse options - while index < tokens.endIndex { - guard case .option(let option) = tokens[index] else { - break - } - guard FromOptions(rawValue: option.key) == .platform else { + // Step 1: Parse instruction options + let (newIndex, instructionOpts) = try parseInstructionOpts(start: index, tokens: tokens) + index = newIndex + + for option in instructionOpts { + guard FromOptions(rawValue: String(option.key)) == .platform else { throw ParseError.unexpectedValue } + if platform != nil { + throw ParseError.duplicateOptionSet(FromOptions.platform.rawValue) + } platform = option.value - index += 1 } // Step 2: Parse image name @@ -137,12 +183,11 @@ public struct DockerfileParser: BuildParser { var rawMounts = [String]() var network: String? = nil - // Step 1: parse options - while index < tokens.endIndex { - guard case .option(let option) = tokens[index] else { - break - } + // Step 1: Parse instruction options + let (lastOption, instructionOpts) = try parseInstructionOpts(start: index, tokens: tokens) + index = lastOption + for option in instructionOpts { guard let runOpt = RunOptions(rawValue: option.key) else { throw ParseError.unexpectedValue } @@ -155,7 +200,6 @@ public struct DockerfileParser: BuildParser { default: throw ParseError.unexpectedValue } - index += 1 } // Step 2: parse run command @@ -185,8 +229,6 @@ public struct DockerfileParser: BuildParser { break } else if case .stringLiteral(let value) = tokens[index] { command.append(value) - } else if case .option(let option) = tokens[index] { - command.append(option.raw) } else { break } @@ -206,12 +248,11 @@ public struct DockerfileParser: BuildParser { var chown: String? = nil var link: String? = nil - // Step 1: parse options - while index < tokens.endIndex { - guard case .option(let option) = tokens[index] else { - break - } + // Step 1: Parse instruction options + let (newIndex, instructionOpts) = try parseInstructionOpts(start: index, tokens: tokens) + index = newIndex + for option in instructionOpts { guard let copyOpt = CopyOptions(rawValue: option.key) else { throw ParseError.unexpectedValue } @@ -240,7 +281,6 @@ public struct DockerfileParser: BuildParser { default: throw ParseError.unexpectedValue } - index += 1 } // Step 2: Get all source paths and destination path diff --git a/Sources/NativeBuilder/ContainerBuildParser/Docker/DockerfileTokenizer.swift b/Sources/NativeBuilder/ContainerBuildParser/Docker/DockerfileTokenizer.swift index a3c13947..a50e161f 100644 --- a/Sources/NativeBuilder/ContainerBuildParser/Docker/DockerfileTokenizer.swift +++ b/Sources/NativeBuilder/ContainerBuildParser/Docker/DockerfileTokenizer.swift @@ -54,8 +54,6 @@ struct DockerfileTokenizer { results.append(listToken) } else if char == "#" { parseComment() - } else if char == "-" { - results.append(parseOption()) } else { let start = position parseWord() @@ -120,26 +118,4 @@ struct DockerfileTokenizer { position = input.index(after: position) } } - - mutating private func parseOption() -> Token { - let wordStart = position - parseWord() - - let rawWord = input[wordStart..