diff --git a/io.go b/io.go index 7db1e66..ac770f8 100644 --- a/io.go +++ b/io.go @@ -17,7 +17,7 @@ func loadURL(u *url.URL) (map[string]interface{}, error) { return nil, fmt.Errorf("unsupported %q URL scheme", u.Scheme) } - return loadFile(filepath.FromSlash(u.Path)) + return loadFile(urlPathToOSPath(u.Path)) } func loadFile(pth string) (map[string]interface{}, error) { diff --git a/main.go b/main.go index 33c8844..775dcb6 100644 --- a/main.go +++ b/main.go @@ -148,7 +148,7 @@ func processFile(pth string, encode func(interface{}) error, debug *debugFlags) err = ExpandRefs(&tmp, &url.URL{ //Scheme: "file", - Path: filepath.ToSlash(pth), + Path: osPathToURLPath(pth), }, trace) if err != nil { return err diff --git a/refs.go b/refs.go index 4c221bd..89604ab 100644 --- a/refs.go +++ b/refs.go @@ -118,7 +118,7 @@ func (l *loc) Index(i int) loc { func (l *loc) Rel(basePath string) loc { // FIXME do not use FS dependent paths - rel, err := filepath.Rel(filepath.FromSlash(basePath), filepath.FromSlash(l.Path)) + rel, err := filepath.Rel(urlPathToOSPath(basePath), urlPathToOSPath(l.Path)) if err != nil { return *l } @@ -240,7 +240,7 @@ func (resolver *refResolver) resolve(link string, relativeTo *loc) (*node, error rdoc, loaded := resolver.docs[targetLoc.Path] if !loaded { //log.Println("Loading", &targetLoc) - doc, err := loadFile(filepath.FromSlash(targetLoc.Path)) + doc, err := loadFile(urlPathToOSPath(targetLoc.Path)) if err != nil { return nil, fmt.Errorf("can't load %q: %v", targetLoc.Path, err) } @@ -629,7 +629,7 @@ func ExpandRefs(rdoc *interface{}, docURL *url.URL, trace func(string)) error { path := path.Clean(docURL.Path) resolver := refResolver{ - basePath: filepath.ToSlash(cwd), + basePath: osPathToURLPath(cwd), rootPath: path, docs: map[string]*interface{}{ path: rdoc, diff --git a/resolvepath.go b/resolvepath.go index 5a2dfa3..a2467d7 100644 --- a/resolvepath.go +++ b/resolvepath.go @@ -4,7 +4,50 @@ package main -import "strings" +import ( + "path/filepath" + "runtime" + "strings" +) + +// osPathToURLPath converts a native absolute filesystem path (as returned by +// filepath.Abs) into the "/"-rooted path convention used internally to +// identify documents, which resolvePath always produces and expects as input. +// +// On POSIX this is a no-op beyond slash conversion, since native absolute +// paths already start with "/". On Windows, native absolute paths start with +// a drive letter (e.g. "C:\foo\bar") with no leading separator, so one is +// added, matching the "file:///C:/foo/bar" URI convention. +func osPathToURLPath(pth string) string { + pth = filepath.ToSlash(pth) + if pth == "" || pth[0] != '/' { + pth = "/" + pth + } + return pth +} + +// isWindowsDriveAbs reports whether pth is an internal "/"-rooted path +// wrapping a Windows drive-absolute path, e.g. "/C:/foo/bar". +func isWindowsDriveAbs(pth string) bool { + return len(pth) >= 3 && + pth[0] == '/' && + (('a' <= pth[1] && pth[1] <= 'z') || ('A' <= pth[1] && pth[1] <= 'Z')) && + pth[2] == ':' && + (len(pth) == 3 || pth[3] == '/') +} + +// urlPathToOSPath is the reverse of osPathToURLPath: it turns an internal +// "/"-rooted document path back into a native OS path suitable for os.Open. +// +// On Windows, a path like "/C:/foo/bar" must have its leading "/" stripped +// before the drive letter, or filepath.FromSlash would produce the invalid +// "\C:\foo\bar". +func urlPathToOSPath(pth string) string { + if runtime.GOOS == "windows" && isWindowsDriveAbs(pth) { + pth = pth[1:] + } + return filepath.FromSlash(pth) +} // resolvePath applies special path segments from refs and applies // them to base, per RFC 3986.