ioutilx: add native fallback for cygpath path conversion

Plain Windows ships no cygpath, so WindowsSubsystemPath failed there.
Add a native fallback for the absolute drive-letter case, and split out
WindowsSubsystemPathWithCygpath so a caller can pin the conversion to a
specific ssh toolchain's own cygpath.

Signed-off-by: mie313 <mie313.tanaka@gmail.com>
Co-authored-by: Jan Dubois <jan.dubois@suse.com>
This commit is contained in:
mie313andJan Dubois committed 2026-07-17 20:35:56 +01:00
1 parent 1ba38dc238
commit 54db504f87
2 files changed
+85 -5

No files matched your search

+41 -5
View File
@@ -50,13 +50,49 @@ func FromUTF16leToString(r io.Reader) (string, error) {
return string(out), nil
}
// WindowsSubsystemPath converts a Windows path to a Cygwin/MSYS form
// (C:\Users\jan -> /c/Users/jan) via cygpath on PATH. A caller holding a
// specific ssh toolchain should use WindowsSubsystemPathWithCygpath, which
// runs that toolchain's own cygpath and respects its fstab.
func WindowsSubsystemPath(ctx context.Context, orig string) (string, error) {
out, err := exec.CommandContext(ctx, "cygpath", filepath.ToSlash(orig)).CombinedOutput()
if err != nil {
logrus.WithError(err).Errorf("failed to convert path to mingw, maybe not using Git ssh?")
return "", err
return WindowsSubsystemPathWithCygpath(ctx, "cygpath", orig)
}
// WindowsSubsystemPathWithCygpath converts a Windows path with the given
// cygpathExe ("cygpath" for PATH, or an absolute path to bind the
// conversion to one Cygwin install). When cygpath is unavailable it falls
// back to a native conversion of the drive-letter case; Universal Naming Convention
// (UNC) and other non-drive-letter inputs return an error.
func WindowsSubsystemPathWithCygpath(ctx context.Context, cygpathExe, orig string) (string, error) {
out, err := exec.CommandContext(ctx, cygpathExe, filepath.ToSlash(orig)).CombinedOutput()
if err == nil {
return strings.TrimSpace(string(out)), nil
}
return strings.TrimSpace(string(out)), nil
logrus.WithError(err).Debugf("cygpath unavailable for %#q (output: %#q), attempting native conversion", orig, strings.TrimSpace(string(out)))
return windowsSubsystemPathWithoutCygpath(orig)
}
func windowsSubsystemPathWithoutCygpath(orig string) (string, error) {
// Only an absolute drive-letter path ("C:\foo") has a well-defined
// Cygwin form here. A drive-relative path ("C:foo") would become
// an unrelated absolute path, so reject it.
if !filepath.IsAbs(orig) {
return "", fmt.Errorf("cannot convert %#q to a Cygwin-style path: input is not an absolute drive-letter path", orig)
}
// UNC path ("\\server\share\foo") is rejected here.
if vol := filepath.VolumeName(orig); len(vol) == 2 && vol[1] == ':' {
// orig[2:] starts with a separator for an absolute drive path
// (C:\foo, C:/foo); strip it so the result stays canonical.
tail := strings.TrimPrefix(filepath.ToSlash(orig[2:]), "/")
converted := "/" + strings.ToLower(vol[:1]) + "/" + tail
logrus.Debugf("native cygpath fallback: %#q -> %#q", orig, converted)
return converted, nil
}
return "", fmt.Errorf("cannot convert %#q to a Cygwin-style path: cygpath is unavailable and input is not an absolute drive-letter path", orig)
}
func WindowsSubsystemPathForLinux(ctx context.Context, orig, distro string) (string, error) {
+44
View File
@@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0
package ioutilx
import (
"strings"
"testing"
"gotest.tools/v3/assert"
)
// TestWindowsSubsystemPathWithoutCygpath exercises the native
// drive-letter fallback path: when the cygpath binary is unreachable,
// absolute drive-letter inputs convert in-process (e.g., "C:\foo" -> "/c/foo")
// and inputs without a drive letter return an error.
func TestWindowsSubsystemPathWithoutCygpath(t *testing.T) {
t.Run("drive-letter backslash", func(t *testing.T) {
got, err := windowsSubsystemPathWithoutCygpath(`C:\Users\USER`)
assert.NilError(t, err)
assert.Equal(t, got, "/c/Users/USER")
})
t.Run("drive-letter forward-slash", func(t *testing.T) {
got, err := windowsSubsystemPathWithoutCygpath(`C:/Users/USER`)
assert.NilError(t, err)
assert.Equal(t, got, "/c/Users/USER")
})
t.Run("non-drive-letter rejected", func(t *testing.T) {
// UNC and POSIX inputs both lack a drive letter; on every
// platform the fallback must refuse rather than fabricate.
for _, input := range []string{`\\server\share\foo`, "/etc/hosts", "relative.txt"} {
_, err := windowsSubsystemPathWithoutCygpath(input)
assert.ErrorContains(t, err, "cannot convert", "input=%q", input)
}
})
t.Run("lowercases drive letter", func(t *testing.T) {
got, err := windowsSubsystemPathWithoutCygpath(`d:\path`)
assert.NilError(t, err)
assert.Assert(t, strings.HasPrefix(got, "/d/"), "got %q", got)
})
}