From e682e3e67fcc6100b80ee6b5ca2f515f6835402d Mon Sep 17 00:00:00 2001 From: OrtegaMatias <212189024+OrtegaMatias@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:56:06 -0400 Subject: [PATCH] Fix container cp rejecting paths that contain a colon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ContainerCopy.parsePathRef` split a ":" reference on every colon and rejected anything with more than one part, so `container cp` failed with "invalid path given" on a container path whose filename contains a colon — e.g. an ISO-8601 timestamp such as /var/log/app-2026-07-20T10:30:00.log (the file exists; only the parse fails). Split on the first colon only — a container id never contains a path separator, but the path may contain colons — and treat a reference whose pre-colon part contains '/' as a local path. This also lets local paths that contain a colon be copied. --- .../Container/ContainerCopy.swift | 20 ++++-- .../ContainerCopyPathRefTests.swift | 65 +++++++++++++++++++ 2 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 Tests/ContainerCommandsTests/ContainerCopyPathRefTests.swift diff --git a/Sources/ContainerCommands/Container/ContainerCopy.swift b/Sources/ContainerCommands/Container/ContainerCopy.swift index 83023316e..7138f16f3 100644 --- a/Sources/ContainerCommands/Container/ContainerCopy.swift +++ b/Sources/ContainerCommands/Container/ContainerCopy.swift @@ -24,21 +24,27 @@ import SystemPackage extension Application { public struct ContainerCopy: AsyncLoggableCommand { - enum PathRef { + enum PathRef: Equatable { case local(String) case container(id: String, path: String) } static func parsePathRef(_ ref: String) throws -> PathRef { - let parts = ref.components(separatedBy: ":") - switch parts.count { - case 1: + // A container reference is ":". The id never + // contains a path separator, but the path may itself contain ':' + // (e.g. an ISO-8601 timestamp in a filename), so split on the FIRST + // ':' only rather than on every colon. If the text before the first + // ':' contains '/', the whole ref is a local path that happens to + // contain a colon (e.g. "/home/2026-01-01T00:00:00.log"). + guard let colon = ref.firstIndex(of: ":"), !ref[.. Application.ContainerCopy.PathRef { + try Application.ContainerCopy.parsePathRef(ref) + } + + @Test("a path without a colon is a local path") + func localNoColon() throws { + #expect(try parse("/tmp/file.txt") == .local("/tmp/file.txt")) + } + + @Test("an id followed by an absolute path is a container reference") + func containerRef() throws { + #expect(try parse("box:/tmp/file.txt") == .container(id: "box", path: "/tmp/file.txt")) + } + + @Test("a container path containing colons is preserved") + func containerPathWithColons() throws { + // Regression: an ISO-8601 timestamp filename was rejected as an + // "invalid path" because the reference was split on every colon + // instead of only the first. + #expect( + try parse("box:/var/log/app-2026-07-20T10:30:00.log") + == .container(id: "box", path: "/var/log/app-2026-07-20T10:30:00.log") + ) + } + + @Test("a local path containing colons is a local path") + func localPathWithColons() throws { + #expect( + try parse("/home/user/2026-07-20T10:30:00.log") + == .local("/home/user/2026-07-20T10:30:00.log") + ) + } + + @Test("an empty container id is rejected") + func emptyId() { + #expect(throws: (any Error).self) { try parse(":/tmp/x") } + } + + @Test("a non-absolute container path is rejected") + func relativeContainerPath() { + #expect(throws: (any Error).self) { try parse("box:relative/path") } + } +}