diff --git a/pkg/filesystem/local_directory_windows.go b/pkg/filesystem/local_directory_windows.go index 62a463ec..bee777ac 100644 --- a/pkg/filesystem/local_directory_windows.go +++ b/pkg/filesystem/local_directory_windows.go @@ -109,11 +109,7 @@ func newLocalDirectory(absPath string, openReparsePoint bool) (DirectoryCloser, } func NewLocalDirectory(path string) (DirectoryCloser, error) { - absPath, err := filepath.Abs(path) - if err != nil { - return nil, err - } - absPath = "\\??\\" + absPath + absPath := "\\??\\" + path return newLocalDirectory(absPath, true) } diff --git a/pkg/filesystem/path/BUILD.bazel b/pkg/filesystem/path/BUILD.bazel index b311c86c..f16dc714 100644 --- a/pkg/filesystem/path/BUILD.bazel +++ b/pkg/filesystem/path/BUILD.bazel @@ -21,6 +21,7 @@ go_library( "virtual_root_scope_walker_factory.go", "void_component_walker.go", "void_scope_walker.go", + "windows_parser.go", ], importpath = "github.com/buildbarn/bb-storage/pkg/filesystem/path", visibility = ["//visibility:public"], diff --git a/pkg/filesystem/path/absolute_scope_walker.go b/pkg/filesystem/path/absolute_scope_walker.go index ef88131f..00bada9d 100644 --- a/pkg/filesystem/path/absolute_scope_walker.go +++ b/pkg/filesystem/path/absolute_scope_walker.go @@ -24,3 +24,7 @@ func (pw *absoluteScopeWalker) OnRelative() (ComponentWalker, error) { func (pw *absoluteScopeWalker) OnAbsolute() (ComponentWalker, error) { return pw.componentWalker, nil } + +func (pw *absoluteScopeWalker) OnDriveLetter(drive rune) (ComponentWalker, error) { + return pw.componentWalker, nil +} diff --git a/pkg/filesystem/path/builder.go b/pkg/filesystem/path/builder.go index f5276689..9c9ba458 100644 --- a/pkg/filesystem/path/builder.go +++ b/pkg/filesystem/path/builder.go @@ -20,6 +20,7 @@ import ( // system. type Builder struct { absolute bool + driveletter rune components []string firstReversibleIndex int suffix string @@ -41,8 +42,18 @@ var EmptyBuilder = Builder{ // Builder that use this path as their starting point can be created by // calling RootBuilder.Join(). var RootBuilder = Builder{ - absolute: true, - suffix: "/", + absolute: true, + driveletter: rune(0), + suffix: "/", +} + +// NewDriveLetterBuilder returns a builder rooted at a Windows drive. +func NewDriveLetterBuilder(drive rune) Builder { + return Builder{ + absolute: false, + driveletter: drive, + suffix: "/", + } } // GetUNIXString returns a string representation of the path for use on @@ -53,6 +64,12 @@ func (b *Builder) GetUNIXString() string { if b.absolute { prefix = "/" } + if b.driveletter != rune(0) { + // Drive letter is not meaningful in the REAPI. We just drop it. + // Symlinks may point to the same drive but not other drives, + // so it can be implicit. + prefix = "/" + } var out strings.Builder for _, component := range b.components { out.WriteString(prefix) @@ -62,15 +79,57 @@ func (b *Builder) GetUNIXString() string { // Emit trailing slash in case the path refers to a directory, // or a dot or slash if the path is empty. - out.WriteString(b.suffix) + suffix := b.suffix + // The suffix may have been constructed by platform-specific code that + // uses backslashes. To construct a UNIX path we must use a forward slash. + // We can construct UNIX paths from Windows-native paths, where the On* + // scope functions set the suffix to backslash. + if suffix == "\\" { + suffix = "/" + } + out.WriteString(suffix) + return out.String() +} + +// GetWindowsString returns a string representation of the path for use on +// UNIX-like operating systems. We do not create a windows-style path. +func (b *Builder) GetWindowsString() string { + // Emit pathname components. + prefix := "" + if b.absolute { + prefix = "\\" + } + + var out strings.Builder + if b.driveletter != rune(0) { + out.WriteString(string(b.driveletter) + ":") + prefix = "\\" + } + + for _, component := range b.components { + out.WriteString(prefix) + out.WriteString(component) + prefix = "\\" + } + + // Emit trailing slash in case the path refers to a directory, + // or a dot or slash if the path is empty. + suffix := b.suffix + // The suffix may have been constructed by platform-independent code that + // uses forward slashes. To construct a Windows path we must use a + // backslash. + if suffix == "/" { + suffix = "\\" + } + out.WriteString(suffix) return out.String() } func (b *Builder) addTrailingSlash() { if len(b.components) == 0 { // An empty path. Ensure we either emit a "/" or ".", - // depending on whether the path is absolute. - if b.absolute { + // depending on whether the path is absolute/driveletter. + if b.absolute || b.driveletter != rune(0) { b.suffix = "/" } else { b.suffix = "." @@ -106,6 +165,8 @@ func (b *Builder) getComponentWalker(base ComponentWalker) ComponentWalker { func (b *Builder) ParseScope(scopeWalker ScopeWalker) (next ComponentWalker, remainder RelativeParser, err error) { if b.absolute { next, err = scopeWalker.OnAbsolute() + } else if b.driveletter != rune(0) { + next, err = scopeWalker.OnDriveLetter(b.driveletter) } else { next, err = scopeWalker.OnRelative() } @@ -154,6 +215,19 @@ func (w *buildingScopeWalker) OnAbsolute() (ComponentWalker, error) { return w.b.getComponentWalker(componentWalker), nil } +func (w *buildingScopeWalker) OnDriveLetter(drive rune) (ComponentWalker, error) { + componentWalker, err := w.base.OnDriveLetter(drive) + if err != nil { + return nil, err + } + *w.b = Builder{ + driveletter: drive, + components: w.b.components[:0], + suffix: "\\", + } + return w.b.getComponentWalker(componentWalker), nil +} + func (w *buildingScopeWalker) OnRelative() (ComponentWalker, error) { componentWalker, err := w.base.OnRelative() if err != nil { diff --git a/pkg/filesystem/path/builder_test.go b/pkg/filesystem/path/builder_test.go index 0a0d91c3..447c6a69 100644 --- a/pkg/filesystem/path/builder_test.go +++ b/pkg/filesystem/path/builder_test.go @@ -1,6 +1,7 @@ package path_test import ( + "strings" "testing" "github.com/buildbarn/bb-storage/internal/mock" @@ -31,6 +32,7 @@ func TestBuilder(t *testing.T) { "/hello/../world/foo", } { t.Run(p, func(t *testing.T) { + // Unix Parser builder1, scopewalker1 := path.EmptyBuilder.Join(path.VoidScopeWalker) require.NoError(t, path.Resolve(path.MustNewUNIXParser(p), scopewalker1)) require.Equal(t, p, builder1.GetUNIXString()) @@ -38,6 +40,68 @@ func TestBuilder(t *testing.T) { builder2, scopewalker2 := path.EmptyBuilder.Join(path.VoidScopeWalker) require.NoError(t, path.Resolve(builder1, scopewalker2)) require.Equal(t, p, builder2.GetUNIXString()) + + // Windows Parser, compare Windows and UNIX string identity. + windowsStyle := strings.Replace(p, "/", "\\", -1) + builder3, scopewalker3 := path.EmptyBuilder.Join(path.VoidScopeWalker) + require.NoError(t, path.Resolve(path.MustNewWindowsParser(p), scopewalker3)) + require.Equal(t, windowsStyle, builder3.GetWindowsString()) + require.Equal(t, p, builder3.GetUNIXString()) + + builder4, scopewalker4 := path.EmptyBuilder.Join(path.VoidScopeWalker) + require.NoError(t, path.Resolve(builder3, scopewalker4)) + require.Equal(t, windowsStyle, builder4.GetWindowsString()) + require.Equal(t, p, builder4.GetUNIXString()) + }) + } + + for _, p := range []string{ + "/C/", + "/C/hello/", + "/C/hello/..", + "/C/hello/../world", + "/C/hello/../world/", + "/C/hello/../world/foo", + "\\C\\", + "\\C\\hello\\", + "\\C\\hello\\..", + "\\C\\hello\\..\\world", + "\\C\\hello\\..\\world\\", + "\\C\\hello\\..\\world\\foo", + "C:\\", + "C:\\hello\\", + "C:\\hello\\..", + "C:\\hello\\..\\world", + "C:\\hello\\..\\world\\", + "C:\\hello\\..\\world\\foo", + } { + t.Run(p, func(t *testing.T) { + // Drive letter paths. Should be converted to the UNIX-like slash-based path. + // And the UNIX string should drop the drive letter. + noDriveletter := strings.Replace(p[2:], "\\", "/", -1) + windowsStyle := strings.Replace(noDriveletter, "/", "\\", -1) + windowsDriveletter := "C:" + windowsStyle + + builder1, scopewalker1 := path.EmptyBuilder.Join(path.VoidScopeWalker) + require.NoError(t, path.Resolve(path.MustNewWindowsParser(p), scopewalker1)) + require.Equal(t, windowsDriveletter, builder1.GetWindowsString()) + require.Equal(t, noDriveletter, builder1.GetUNIXString()) + + builder2, scopewalker2 := path.EmptyBuilder.Join(path.VoidScopeWalker) + require.NoError(t, path.Resolve(builder1, scopewalker2)) + require.Equal(t, windowsDriveletter, builder2.GetWindowsString()) + require.Equal(t, noDriveletter, builder2.GetUNIXString()) + }) + } + for _, p := range []string{} { + t.Run(p, func(t *testing.T) { + builder1, scopewalker1 := path.EmptyBuilder.Join(path.VoidScopeWalker) + require.NoError(t, path.Resolve(path.MustNewWindowsParser(p), scopewalker1)) + require.Equal(t, p, builder1.GetWindowsString()) + + builder2, scopewalker2 := path.EmptyBuilder.Join(path.VoidScopeWalker) + require.NoError(t, path.Resolve(builder1, scopewalker2)) + require.Equal(t, p, builder2.GetWindowsString()) }) } }) diff --git a/pkg/filesystem/path/component_walker.go b/pkg/filesystem/path/component_walker.go index 33ba623c..1024a68c 100644 --- a/pkg/filesystem/path/component_walker.go +++ b/pkg/filesystem/path/component_walker.go @@ -56,9 +56,10 @@ type ComponentWalker interface { // If the pathname component refers to a symbolic link, this // function will return a GotSymlink containing a ScopeWalker, which // can be used to perform expansion of the symbolic link. The - // Resolve() function will call into OnAbsolute() or OnRelative() to - // signal whether resolution should continue at the root directory - // or at the directory that contained the symbolic link. + // Resolve() function will call into OnAbsolute(), OnRealtive() or + // OnDriveLetter() to signal whether resolution should continue at + // the root directory or at the directory that contained the + // symbolic link. OnDirectory(name Component) (GotDirectoryOrSymlink, error) // OnTerminal is called for the potentially last pathname diff --git a/pkg/filesystem/path/local_windows.go b/pkg/filesystem/path/local_windows.go index f2f71fa6..1ccc6056 100644 --- a/pkg/filesystem/path/local_windows.go +++ b/pkg/filesystem/path/local_windows.go @@ -9,13 +9,11 @@ import ( // NewLocalParser creates a pathname parser for paths that are native to // the locally running operating system. func NewLocalParser(path string) (Parser, error) { - // TODO: Implement an actual Windows pathname parser. - return NewUNIXParser(filepath.ToSlash(path)) + return NewWindowsParser(filepath.ToSlash(path)) } // GetLocalString converts a path to a string representation that is // supported by the locally running operating system. func GetLocalString(s Stringer) (string, error) { - // TODO: Implement an actual Windows pathname formatter. - return filepath.FromSlash(s.GetUNIXString()), nil + return s.GetWindowsString(), nil } diff --git a/pkg/filesystem/path/loop_detecting_scope_walker.go b/pkg/filesystem/path/loop_detecting_scope_walker.go index 861e6d78..bed8d041 100644 --- a/pkg/filesystem/path/loop_detecting_scope_walker.go +++ b/pkg/filesystem/path/loop_detecting_scope_walker.go @@ -34,6 +34,17 @@ func (w *loopDetectingScopeWalker) OnAbsolute() (ComponentWalker, error) { }, nil } +func (w *loopDetectingScopeWalker) OnDriveLetter(drive rune) (ComponentWalker, error) { + componentWalker, err := w.base.OnDriveLetter(drive) + if err != nil { + return nil, err + } + return &loopDetectingComponentWalker{ + base: componentWalker, + symlinksLeft: w.symlinksLeft, + }, nil +} + func (w *loopDetectingScopeWalker) OnRelative() (ComponentWalker, error) { componentWalker, err := w.base.OnRelative() if err != nil { diff --git a/pkg/filesystem/path/relative_scope_walker.go b/pkg/filesystem/path/relative_scope_walker.go index 36295cbf..c551edab 100644 --- a/pkg/filesystem/path/relative_scope_walker.go +++ b/pkg/filesystem/path/relative_scope_walker.go @@ -21,6 +21,10 @@ func (pw *relativeScopeWalker) OnAbsolute() (ComponentWalker, error) { return nil, status.Error(codes.InvalidArgument, "Path is absolute, while a relative path was expected") } +func (pw *relativeScopeWalker) OnDriveLetter(drive rune) (ComponentWalker, error) { + return nil, status.Error(codes.InvalidArgument, "Path is absolute with drive letter, while a relative path was expected") +} + func (pw *relativeScopeWalker) OnRelative() (ComponentWalker, error) { return pw.componentWalker, nil } diff --git a/pkg/filesystem/path/scope_walker.go b/pkg/filesystem/path/scope_walker.go index 58eb441f..00687cad 100644 --- a/pkg/filesystem/path/scope_walker.go +++ b/pkg/filesystem/path/scope_walker.go @@ -7,7 +7,9 @@ type ScopeWalker interface { // One of these functions is called right before processing the // first component in the path (if any). Based on the // characteristics of the path. Absolute paths are handled through - // OnAbsolute(), and relative paths require OnRelative(). + // OnAbsolute(), and relative paths require OnRelative(), on Windows + // absolute paths can also start with a drive letter, which is handled + // through OnDriveLetter(). // // These functions can be used by the implementation to determine // whether path resolution needs to be relative to the current @@ -25,4 +27,5 @@ type ScopeWalker interface { // system. OnAbsolute() (ComponentWalker, error) OnRelative() (ComponentWalker, error) + OnDriveLetter(drive rune) (ComponentWalker, error) } diff --git a/pkg/filesystem/path/stringer.go b/pkg/filesystem/path/stringer.go index eafaa1ca..0e0b0428 100644 --- a/pkg/filesystem/path/stringer.go +++ b/pkg/filesystem/path/stringer.go @@ -4,4 +4,5 @@ package path // converted to string representations. type Stringer interface { GetUNIXString() string + GetWindowsString() string } diff --git a/pkg/filesystem/path/trace.go b/pkg/filesystem/path/trace.go index 3fe609df..a0dcb7c2 100644 --- a/pkg/filesystem/path/trace.go +++ b/pkg/filesystem/path/trace.go @@ -44,3 +44,13 @@ func (t *Trace) GetUNIXString() string { t.writeToStringBuilder(&sb) return sb.String() } + +// GetWindowsString returns a string representation of the path for use on Windows. +func (t *Trace) GetWindowsString() string { + if t == nil { + return "." + } + var sb strings.Builder + t.writeToStringBuilder(&sb) + return sb.String() +} diff --git a/pkg/filesystem/path/unix_parser.go b/pkg/filesystem/path/unix_parser.go index 4634eb31..7c207b28 100644 --- a/pkg/filesystem/path/unix_parser.go +++ b/pkg/filesystem/path/unix_parser.go @@ -49,7 +49,7 @@ func (p unixParser) ParseScope(scopeWalker ScopeWalker) (next ComponentWalker, r return nil, nil, err } - return next, unixRelativeParser{stripOneOrMoreSlashes(p.path)}, nil + return next, relativeParser{stripOneOrMoreSlashes(p.path), '/'}, nil } next, err = scopeWalker.OnRelative() @@ -57,17 +57,18 @@ func (p unixParser) ParseScope(scopeWalker ScopeWalker) (next ComponentWalker, r return nil, nil, err } - return next, unixRelativeParser{p.path}, nil + return next, relativeParser{p.path, '/'}, nil } -type unixRelativeParser struct { - path string +type relativeParser struct { + path string + pathSeparator byte } -func (rp unixRelativeParser) ParseFirstComponent(componentWalker ComponentWalker, mustBeDirectory bool) (next GotDirectoryOrSymlink, remainder RelativeParser, err error) { +func (rp relativeParser) ParseFirstComponent(componentWalker ComponentWalker, mustBeDirectory bool) (next GotDirectoryOrSymlink, remainder RelativeParser, err error) { var name string terminal := false - if slash := strings.IndexByte(rp.path, '/'); slash == -1 { + if slash := strings.IndexByte(rp.path, rp.pathSeparator); slash == -1 { // Path no longer contains a slash. Consume it entirely. terminal = true name = rp.path @@ -75,7 +76,7 @@ func (rp unixRelativeParser) ParseFirstComponent(componentWalker ComponentWalker } else { name = rp.path[:slash] rp.path = stripOneOrMoreSlashes(rp.path[slash:]) - remainder = unixRelativeParser{rp.path} + remainder = relativeParser{rp.path, rp.pathSeparator} } switch name { diff --git a/pkg/filesystem/path/virtual_root_scope_walker_factory.go b/pkg/filesystem/path/virtual_root_scope_walker_factory.go index 732a2174..80ee1fc1 100644 --- a/pkg/filesystem/path/virtual_root_scope_walker_factory.go +++ b/pkg/filesystem/path/virtual_root_scope_walker_factory.go @@ -207,6 +207,10 @@ func (w *virtualRootScopeWalker) OnAbsolute() (ComponentWalker, error) { return w.getComponentWalker(w.rootNode) } +func (w *virtualRootScopeWalker) OnDriveLetter(drive rune) (ComponentWalker, error) { + return w.getComponentWalker(w.rootNode) +} + func (w *virtualRootScopeWalker) OnRelative() (ComponentWalker, error) { // Attempted to resolve a relative path. There is no need to // rewrite any paths. Do wrap the ComponentWalker to ensure diff --git a/pkg/filesystem/path/void_scope_walker.go b/pkg/filesystem/path/void_scope_walker.go index 9216fce1..9650f5c5 100644 --- a/pkg/filesystem/path/void_scope_walker.go +++ b/pkg/filesystem/path/void_scope_walker.go @@ -6,6 +6,10 @@ func (w voidScopeWalker) OnAbsolute() (ComponentWalker, error) { return VoidComponentWalker, nil } +func (w voidScopeWalker) OnDriveLetter(drive rune) (ComponentWalker, error) { + return VoidComponentWalker, nil +} + func (w voidScopeWalker) OnRelative() (ComponentWalker, error) { return VoidComponentWalker, nil } diff --git a/pkg/filesystem/path/windows_parser.go b/pkg/filesystem/path/windows_parser.go new file mode 100644 index 00000000..c2f39dd1 --- /dev/null +++ b/pkg/filesystem/path/windows_parser.go @@ -0,0 +1,83 @@ +package path + +import ( + "strings" + "unicode" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type windowsParser struct { + absolute bool + driveletter rune + path string +} + +// NewWindowsParser creates a Parser for Windows paths that can be used in Resolve. +func NewWindowsParser(path string) (Parser, error) { + // Windows paths are generally passed to system calls that + // accept C strings. There is no way these can accept null + // bytes. + driveletter := rune(0) + absolute := false + + if strings.ContainsRune(path, '\x00') { + return nil, status.Error(codes.InvalidArgument, "Path contains a null byte") + } + + path = strings.Replace(path, "/", "\\", -1) + // C:\ + if len(path) >= 3 && unicode.IsLetter(rune(path[0])) && path[1] == ':' && path[2] == '\\' { + driveletter = rune(path[0]) + path = path[3:] + } + // \C\ + if len(path) >= 1 && path[0] == '\\' { + if len(path) >= 2 && unicode.IsLetter(rune(path[1])) && path[0] == '\\' && path[2] == '\\' { + driveletter = rune(path[1]) + path = path[3:] + } else { + absolute = true + } + } + + return &windowsParser{absolute, driveletter, path}, nil +} + +// MustNewWindowsParser is identical to NewWindowsParser, except that it panics +// upon failure. +func MustNewWindowsParser(path string) Parser { + parser, err := NewWindowsParser(path) + if err != nil { + panic(err) + } + return parser +} + +func (p windowsParser) ParseScope(scopeWalker ScopeWalker) (next ComponentWalker, remainder RelativeParser, err error) { + if p.absolute { + next, err = scopeWalker.OnAbsolute() + if err != nil { + return nil, nil, err + } + + return next, relativeParser{stripOneOrMoreSlashes(p.path), '\\'}, nil + } + + if p.driveletter != rune(0) { + next, err = scopeWalker.OnDriveLetter(p.driveletter) + if err != nil { + return nil, nil, err + } + + return next, relativeParser{p.path, '\\'}, nil + } + + next, err = scopeWalker.OnRelative() + if err != nil { + return nil, nil, err + } + + return next, relativeParser{p.path, '\\'}, nil +}