Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions loading/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"os"
"path/filepath"
"runtime"
"time"
)

Expand Down Expand Up @@ -76,19 +77,50 @@ func (fo fileOptions) ReadFileFunc() func(string) ([]byte, error) {
// absolute path that lexically escapes root yields a "../" prefix here and is then rejected by
// os.Root.
func rootRelative(root, name string) (string, error) {
osName := filepath.FromSlash(name)
osName := toOSPath(name)
if !filepath.IsAbs(osName) {
return name, nil
}

absRoot, err := filepath.Abs(filepath.FromSlash(root))
absRoot, err := filepath.Abs(toOSPath(root))
if err != nil {
return "", err
}

return filepath.Rel(absRoot, osName)
}

// toOSPath converts a slash-separated path to an OS-native path.
//
// On Windows it additionally normalizes the "/C:/dir" form — a leading separator before a drive
// letter — that file URIs and URL-style path normalization (as performed by
// github.com/go-openapi/spec) produce. Without this, filepath.IsAbs does not recognize such a
// path as absolute and os.Root rejects an otherwise in-root target. This mirrors the file://
// drive-letter handling in LoadStrategy, which the os.Root loader bypasses.
func toOSPath(p string) string {
p = filepath.FromSlash(p)
if runtime.GOOS == "windows" {
p = stripLeadingDriveSlash(p)
}

return p
}

// stripLeadingDriveSlash removes a leading separator that precedes a Windows drive letter,
// turning "\C:\dir" (from a "/C:/dir" URL path) into "C:\dir". Any other path is returned
// unchanged. It is pure (no OS dependency) so that its logic can be tested on any platform.
func stripLeadingDriveSlash(p string) string {
if len(p) >= 3 && (p[0] == '/' || p[0] == '\\') && p[2] == ':' && isASCIILetter(p[1]) {
return p[1:]
}

return p
}

func isASCIILetter(b byte) bool {
return ('a' <= b && b <= 'z') || ('A' <= b && b <= 'Z')
}

// WithTimeout sets a timeout for the remote file loader.
//
// The default timeout is 30s.
Expand Down
17 changes: 17 additions & 0 deletions loading/withroot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,20 @@ func TestRootRelative(t *testing.T) {
"expected a traversal path, got %q", got)
})
}

func TestStripLeadingDriveSlash(t *testing.T) {
// A Windows file-URL path ("/C:/dir/file", produced e.g. by go-openapi/spec normalization) must
// have its leading separator stripped so it is recognized as an absolute path; anything else is
// left untouched. The helper is pure, so its logic is exercised on every platform.
for in, want := range map[string]string{
`/C:/dir/file`: `C:/dir/file`, // forward-slash URL form
`\C:\dir\file`: `C:\dir\file`, // back-slash form
`/c:/x`: `c:/x`, // lowercase drive letter
`/usr/local`: `/usr/local`, // no drive letter: unchanged
`relative/x`: `relative/x`, // relative: unchanged
`/C`: `/C`, // too short: unchanged
``: ``, // empty: unchanged
} {
assert.EqualT(t, want, stripLeadingDriveSlash(in), "input %q", in)
}
}
14 changes: 14 additions & 0 deletions loading/withroot_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
package loading

import (
"path/filepath"
"testing"

"github.com/go-openapi/testify/v2/assert"
"github.com/go-openapi/testify/v2/require"
)

Expand All @@ -18,3 +20,15 @@ func TestRootRelativeVolumeMismatch(t *testing.T) {
_, err := rootRelative(`C:\root`, `D:\secret.txt`)
require.Error(t, err)
}

func TestRootRelativeWindowsURLPath(t *testing.T) {
// go-openapi/spec normalizes Windows paths to a file-URL form with a leading slash before the
// drive letter, e.g. "/D:/dir/child.json". WithRoot must rebase such an in-root path onto the
// root rather than reject it for not looking absolute.
root := t.TempDir()
urlForm := "/" + filepath.ToSlash(filepath.Join(root, "child.json")) // "/D:/.../child.json"

rel, err := rootRelative(root, urlForm)
require.NoError(t, err)
assert.EqualT(t, "child.json", rel)
}
Loading