diff --git a/cli/command/stack/loader/loader.go b/cli/command/stack/loader/loader.go index b47909451294..4e7ea8f6682b 100644 --- a/cli/command/stack/loader/loader.go +++ b/cli/command/stack/loader/loader.go @@ -14,6 +14,7 @@ import ( "github.com/docker/cli/cli/compose/loader" "github.com/docker/cli/cli/compose/schema" composetypes "github.com/docker/cli/cli/compose/types" + "github.com/docker/docker/pkg/homedir" "github.com/pkg/errors" ) @@ -97,6 +98,7 @@ func getConfigDetails(composefiles []string, stdin io.Reader) (composetypes.Conf // Take the first file version (2 files can't have different version) details.Version = schema.Version(details.ConfigFiles[0].Config) details.Environment, err = buildEnvironment(os.Environ()) + details.HomeDir = homedir.Get() return details, err } diff --git a/cli/command/stack/loader/loader_test.go b/cli/command/stack/loader/loader_test.go index de524cc5272c..c5d30257e6b6 100644 --- a/cli/command/stack/loader/loader_test.go +++ b/cli/command/stack/loader/loader_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/docker/docker/pkg/homedir" "gotest.tools/assert" is "gotest.tools/assert/cmp" "gotest.tools/fs" @@ -24,6 +25,7 @@ services: details, err := getConfigDetails([]string{file.Path()}, nil) assert.NilError(t, err) assert.Check(t, is.Equal(filepath.Dir(file.Path()), details.WorkingDir)) + assert.Check(t, is.Equal(homedir.Get(), details.HomeDir)) assert.Assert(t, is.Len(details.ConfigFiles, 1)) assert.Check(t, is.Equal("3.0", details.ConfigFiles[0].Config["version"])) assert.Check(t, is.Len(details.Environment, len(os.Environ()))) @@ -41,6 +43,7 @@ services: cwd, err := os.Getwd() assert.NilError(t, err) assert.Check(t, is.Equal(cwd, details.WorkingDir)) + assert.Check(t, is.Equal(homedir.Get(), details.HomeDir)) assert.Assert(t, is.Len(details.ConfigFiles, 1)) assert.Check(t, is.Equal("3.0", details.ConfigFiles[0].Config["version"])) assert.Check(t, is.Len(details.Environment, len(os.Environ()))) diff --git a/cli/compose/loader/full-struct_test.go b/cli/compose/loader/full-struct_test.go index 427f2e8e06db..132697dfe6ac 100644 --- a/cli/compose/loader/full-struct_test.go +++ b/cli/compose/loader/full-struct_test.go @@ -5,6 +5,8 @@ import ( "path/filepath" "time" + "github.com/simonferquel/crosspath" + "github.com/docker/cli/cli/compose/types" ) @@ -28,6 +30,7 @@ func fullExampleConfig(workingDir, homeDir string) *types.Config { } func services(workingDir, homeDir string) []types.ServiceConfig { + configsPath, _ := crosspath.ParsePathWithDefaults(homeDir + "/configs") return []types.ServiceConfig{ { Name: "foo", @@ -367,7 +370,7 @@ func services(workingDir, homeDir string) []types.ServiceConfig { {Source: "/opt/data", Target: "/var/lib/mysql", Type: "bind"}, {Source: workingDir, Target: "/code", Type: "bind"}, {Source: filepath.Join(workingDir, "static"), Target: "/var/www/html", Type: "bind"}, - {Source: homeDir + "/configs", Target: "/etc/configs/", Type: "bind", ReadOnly: true}, + {Source: configsPath.String(), Target: "/etc/configs/", Type: "bind", ReadOnly: true}, {Source: "datavolume", Target: "/var/lib/mysql", Type: "volume"}, {Source: filepath.Join(workingDir, "opt"), Target: "/opt", Consistency: "cached", Type: "bind"}, {Target: "/opt", Type: "tmpfs", Tmpfs: &types.ServiceVolumeTmpfs{ diff --git a/cli/compose/loader/loader.go b/cli/compose/loader/loader.go index f5787126c952..2fd053db197f 100644 --- a/cli/compose/loader/loader.go +++ b/cli/compose/loader/loader.go @@ -2,7 +2,6 @@ package loader import ( "fmt" - "path" "path/filepath" "reflect" "sort" @@ -20,6 +19,7 @@ import ( shellwords "github.com/mattn/go-shellwords" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" + "github.com/simonferquel/crosspath" "github.com/sirupsen/logrus" yaml "gopkg.in/yaml.v2" ) @@ -137,7 +137,7 @@ func loadSections(config map[string]interface{}, configDetails types.ConfigDetai { key: "services", fnc: func(config map[string]interface{}) error { - cfg.Services, err = LoadServices(config, configDetails.WorkingDir, configDetails.LookupEnv) + cfg.Services, err = LoadServices(config, configDetails.WorkingDir, configDetails.HomeDir, configDetails.LookupEnv) return err }, }, @@ -377,11 +377,11 @@ func formatInvalidKeyError(keyPrefix string, key interface{}) error { // LoadServices produces a ServiceConfig map from a compose file Dict // the servicesDict is not validated if directly used. Use Load() to enable validation -func LoadServices(servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) ([]types.ServiceConfig, error) { +func LoadServices(servicesDict map[string]interface{}, workingDir, homeDir string, lookupEnv template.Mapping) ([]types.ServiceConfig, error) { var services []types.ServiceConfig for name, serviceDef := range servicesDict { - serviceConfig, err := LoadService(name, serviceDef.(map[string]interface{}), workingDir, lookupEnv) + serviceConfig, err := LoadService(name, serviceDef.(map[string]interface{}), workingDir, homeDir, lookupEnv) if err != nil { return nil, err } @@ -393,7 +393,7 @@ func LoadServices(servicesDict map[string]interface{}, workingDir string, lookup // LoadService produces a single ServiceConfig from a compose file Dict // the serviceDict is not validated if directly used. Use Load() to enable validation -func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) { +func LoadService(name string, serviceDict map[string]interface{}, workingDir, homeDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) { serviceConfig := &types.ServiceConfig{} if err := Transform(serviceDict, serviceConfig); err != nil { return nil, err @@ -404,7 +404,7 @@ func LoadService(name string, serviceDict map[string]interface{}, workingDir str return nil, err } - if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv); err != nil { + if err := resolveVolumePaths(serviceConfig.Volumes, workingDir, homeDir); err != nil { return nil, err } @@ -468,7 +468,7 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, l return nil } -func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) error { +func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir, homeDir string) error { for i, volume := range volumes { if volume.Type != "bind" { continue @@ -478,32 +478,43 @@ func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string, return errors.New(`invalid mount config for type "bind": field Source must not be empty`) } - filePath := expandUser(volume.Source, lookupEnv) - // Check for a Unix absolute path first, to handle a Windows client - // with a Unix daemon. This handles a Windows client connecting to a - // Unix daemon. Note that this is not required for Docker for Windows - // when specifying a local Windows path, because Docker for Windows - // translates the Windows path into a valid path within the VM. - if !path.IsAbs(filePath) { - filePath = absPath(workingDir, filePath) + filePath, err := transformFilePath(volume.Source, workingDir, homeDir) + if err != nil { + return err } + volume.Source = filePath volumes[i] = volume } return nil } -// TODO: make this more robust -func expandUser(path string, lookupEnv template.Mapping) string { - if strings.HasPrefix(path, "~") { - home, ok := lookupEnv("HOME") - if !ok { - logrus.Warn("cannot expand '~', because the environment lacks HOME") - return path +func transformFilePath(origin string, workingDir, homeDir string) (string, error) { + path, err := crosspath.ParsePathWithDefaults(origin) + if err != nil { + return "", err + } + switch path.Kind() { + case crosspath.Relative: + basePath, err := crosspath.ParsePathWithDefaults(workingDir) + if err != nil { + return "", err + } + path, err = basePath.Join(path) + if err != nil { + return "", err + } + case crosspath.HomeRooted: + basePath, err := crosspath.ParsePathWithDefaults(homeDir) + if err != nil { + return "", err + } + path, err = basePath.Join(path) + if err != nil { + return "", err } - return strings.Replace(path, "~", home, 1) } - return path + return path.String(), nil } func transformUlimits(data interface{}) (interface{}, error) { diff --git a/cli/compose/loader/loader_test.go b/cli/compose/loader/loader_test.go index a5933cc31c4b..78d6da9f214d 100644 --- a/cli/compose/loader/loader_test.go +++ b/cli/compose/loader/loader_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/docker/cli/cli/compose/types" + "github.com/docker/docker/pkg/homedir" "github.com/google/go-cmp/cmp/cmpopts" "github.com/sirupsen/logrus" "gotest.tools/assert" @@ -28,6 +29,7 @@ func buildConfigDetails(source map[string]interface{}, env map[string]string) ty {Filename: "filename.yml", Config: source}, }, Environment: env, + HomeDir: homedir.Get(), } } @@ -905,15 +907,14 @@ func TestFullExample(t *testing.T) { bytes, err := ioutil.ReadFile("full-example.yml") assert.NilError(t, err) - homeDir := "/home/foo" - env := map[string]string{"HOME": homeDir, "QUX": "qux_from_environment"} + env := map[string]string{"QUX": "qux_from_environment"} config, err := loadYAMLWithEnv(string(bytes), env) assert.NilError(t, err) workingDir, err := os.Getwd() assert.NilError(t, err) - expectedConfig := fullExampleConfig(workingDir, homeDir) + expectedConfig := fullExampleConfig(workingDir, homedir.Get()) assert.Check(t, is.DeepEqual(expectedConfig.Services, config.Services)) assert.Check(t, is.DeepEqual(expectedConfig.Networks, config.Networks)) @@ -1316,6 +1317,7 @@ func TestLoadSecretsWarnOnDeprecatedExternalNameVersion35(t *testing.T) { } details := types.ConfigDetails{ Version: "3.5", + HomeDir: homedir.Get(), } secrets, err := LoadSecrets(source, details) assert.NilError(t, err) @@ -1664,3 +1666,79 @@ secrets: } assert.DeepEqual(t, config, expected, cmpopts.EquateEmpty()) } + +func TestFilePathsCrossPlat(t *testing.T) { + cases := []struct { + path string + workingDir string + expected string + homeDir string + }{ + { + path: `/var/data`, + workingDir: `/working/dir`, + expected: `/var/data`, + }, + { + path: `/var/data`, + workingDir: `c:\working\dir`, + expected: `/var/data`, + }, + { + path: `relative/data`, + workingDir: `/working/dir`, + expected: `/working/dir/relative/data`, + }, + { + path: `relative/data`, + workingDir: `c:\working\dir`, + expected: `c:\working\dir\relative\data`, + }, + { + path: `c:\var\data`, + workingDir: `/working/dir`, + expected: `c:\var\data`, + }, + { + path: `c:\var\data`, + workingDir: `c:\working\dir`, + expected: `c:\var\data`, + }, + { + path: `relative\data`, + workingDir: `/working/dir`, + expected: `/working/dir/relative/data`, + }, + { + path: `relative\data`, + workingDir: `c:\working\dir`, + expected: `c:\working\dir\relative\data`, + }, + { + path: `~\homerooted\data`, + homeDir: `c:\users\user`, + expected: `c:\users\user\homerooted\data`, + }, + { + path: `~\homerooted\data`, + homeDir: `/home/user`, + expected: `/home/user/homerooted/data`, + }, + + { + path: `~/homerooted/data`, + homeDir: `c:\users\user`, + expected: `c:\users\user\homerooted\data`, + }, + { + path: `~/homerooted/data`, + homeDir: `/home/user`, + expected: `/home/user/homerooted/data`, + }, + } + for _, c := range cases { + result, err := transformFilePath(c.path, c.workingDir, c.homeDir) + assert.NilError(t, err) + assert.Equal(t, c.expected, result) + } +} diff --git a/cli/compose/types/types.go b/cli/compose/types/types.go index d77c1b63dc4e..91eb216f8dfd 100644 --- a/cli/compose/types/types.go +++ b/cli/compose/types/types.go @@ -58,6 +58,7 @@ type ConfigFile struct { type ConfigDetails struct { Version string WorkingDir string + HomeDir string ConfigFiles []ConfigFile Environment map[string]string } diff --git a/vendor.conf b/vendor.conf index 9f4fd0a497b7..0025689d8ec2 100755 --- a/vendor.conf +++ b/vendor.conf @@ -67,6 +67,7 @@ github.com/prometheus/common 7600349dcfe1abd18d72d3a17708 github.com/prometheus/procfs 7d6f385de8bea29190f15ba9931442a0eaef9af7 github.com/russross/blackfriday 1d6b8e9301e720b08a8938b8c25c018285885438 github.com/shurcooL/sanitized_anchor_name 10ef21a441db47d8b13ebcc5fd2310f636973c77 +github.com/simonferquel/crosspath a71ebd7c04a936888624ea43e6da893a7e765ef0 # v0.1.3 github.com/sirupsen/logrus 8bdbc7bcc01dcbb8ec23dc8a28e332258d25251f # v1.4.1 github.com/spf13/cobra ef82de70bb3f60c65fb8eebacbb2d122ef517385 # v0.0.3 github.com/spf13/pflag 4cb166e4f25ac4e8016a3595bbf7ea2e9aa85a2c https://github.com/thaJeztah/pflag.git # temporary fork with https://github.com/spf13/pflag/pull/170 applied, which isn't merged yet upstream diff --git a/vendor/github.com/simonferquel/crosspath/LICENSE b/vendor/github.com/simonferquel/crosspath/LICENSE new file mode 100644 index 000000000000..0e25c96e8815 --- /dev/null +++ b/vendor/github.com/simonferquel/crosspath/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2018 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/simonferquel/crosspath/path.go b/vendor/github.com/simonferquel/crosspath/path.go new file mode 100644 index 000000000000..c4af4e385d65 --- /dev/null +++ b/vendor/github.com/simonferquel/crosspath/path.go @@ -0,0 +1,58 @@ +package crosspath + +import ( + "fmt" + "runtime" +) + +// TargetOS qualifies a path with a target OS +type TargetOS string + +const ( + // Unix represents Unix-style file system + Unix = TargetOS("unix") + // Windows represents Windows-style file system + Windows = TargetOS("windows") +) + +// Kind qualifies the nature of a path (absolute, relative, home-rooted...) +type Kind string + +const ( + // Absolute is an absolute path + Absolute = Kind("absolute") + // Relative is a relative path + Relative = Kind("relative") + // HomeRooted is a path relative to user's home path + HomeRooted = Kind("home-rooted") + // AbsoluteFromCurrentDrive is a windows only kind of style "\some\path" + AbsoluteFromCurrentDrive = Kind("absolute-current-drive") + // RelativeFromDriveCurrentDir is a wondows only of style "c:some\path" + RelativeFromDriveCurrentDir = Kind("relative-drive-specified") + // WindowsDevice represents a path to a Windows device or virtual device such as pipe + WindowsDevice = Kind("windows-device") + // UNC is a UNC share file path + UNC = Kind("unc") +) + +// Path represents a file Path +type Path interface { + fmt.Stringer + TargetOS() TargetOS + Kind() Kind + Separator() rune + segments() []string + Convert(os TargetOS) (Path, error) + Normalize() Path + Join(paths ...Path) (Path, error) + hasWindowsSpecificNamespacePrefix() bool + Raw() string +} + +// RuntimeOS returns information about the running OS (in term of file paths semantic) +func RuntimeOS() TargetOS { + if runtime.GOOS == "windows" { + return Windows + } + return Unix +} diff --git a/vendor/github.com/simonferquel/crosspath/resolver.go b/vendor/github.com/simonferquel/crosspath/resolver.go new file mode 100644 index 000000000000..911f1316ba5c --- /dev/null +++ b/vendor/github.com/simonferquel/crosspath/resolver.go @@ -0,0 +1,122 @@ +package crosspath + +// Preference represents a Comparer result +type Preference int + +const ( + // PreferLeft indicates the comparer prefers the left value + PreferLeft = Preference(-1) + // PreferRight indicates the comparer prefers the right value + PreferRight = Preference(1) + // PreferNone indicates the comparer can't decide which one is better + PreferNone = Preference(0) +) + +// Comparer compares 2 paths and returns a value which indicates what Path it prefers +type Comparer func(lhs, rhs Path) Preference + +// PreferOS returns a comparer that prefers an os target +func PreferOS(os TargetOS) Comparer { + return func(lhs, rhs Path) Preference { + if lhs.TargetOS() == rhs.TargetOS() { + return PreferNone + } + if lhs.TargetOS() == os { + return PreferLeft + } + if rhs.TargetOS() == os { + return PreferRight + } + return PreferNone + } +} + +// PreferGreaterSegmentsLength returns a comparer that prefers path with more directory delimiters +func PreferGreaterSegmentsLength() Comparer { + return func(lhs, rhs Path) Preference { + l := len(lhs.segments()) + r := len(rhs.segments()) + if l == r { + return PreferNone + } + if l > r { + return PreferLeft + } + return PreferRight + } +} + +// PreferKinds returns a comparer that prefers path kinds in the specified order +func PreferKinds(kindsPreferenceOrder ...Kind) Comparer { + return func(lhs, rhs Path) Preference { + l := lhs.Kind() + r := rhs.Kind() + if l == r { + return PreferNone + } + for _, k := range kindsPreferenceOrder { + if l == k { + return PreferLeft + } + if r == k { + return PreferRight + } + } + return PreferNone + } +} + +// PreferWithWindowsSpecificNamespacePrefix prefers paths with a win32 FileSystem or Device prefix +func PreferWithWindowsSpecificNamespacePrefix() Comparer { + return func(lhs, rhs Path) Preference { + if lhs.hasWindowsSpecificNamespacePrefix() { + return PreferLeft + } + if rhs.hasWindowsSpecificNamespacePrefix() { + return PreferRight + } + return PreferNone + } +} + +// PreferChain chain comparers to define the best Path candidate +func PreferChain(comparers ...Comparer) Comparer { + return func(lhs, rhs Path) Preference { + for _, c := range comparers { + result := c(lhs, rhs) + if result != PreferNone { + return result + } + } + return PreferNone + } +} + +// ParsePathWithPreference tries to parse the path both for windows and unix target OS, and returns the best match +// depending on the given comparer result +func ParsePathWithPreference(path string, comparer Comparer) (Path, error) { + unixPath, err := NewUnixPath(path) + if err != nil { + // not valid for unix. so return a windows path + return NewWindowsPath(path, true) + } + winPath, err := NewWindowsPath(path, true) + if err != nil { + return unixPath, nil + } + if comparer(unixPath, winPath) == PreferRight { + return winPath, nil + } + return unixPath, nil +} + +// ParsePathWithDefaults parse a path with default heuristics to define if a given path targets Windows or Linux +func ParsePathWithDefaults(path string) (Path, error) { + p := PreferChain( + PreferWithWindowsSpecificNamespacePrefix(), + PreferKinds(Absolute, HomeRooted, AbsoluteFromCurrentDrive, RelativeFromDriveCurrentDir, WindowsDevice, UNC, Relative), + PreferGreaterSegmentsLength(), + PreferOS(Unix), + ) + return ParsePathWithPreference(path, p) +} diff --git a/vendor/github.com/simonferquel/crosspath/unixpath.go b/vendor/github.com/simonferquel/crosspath/unixpath.go new file mode 100644 index 000000000000..d4151e32f33c --- /dev/null +++ b/vendor/github.com/simonferquel/crosspath/unixpath.go @@ -0,0 +1,124 @@ +package crosspath + +import ( + "errors" + "strings" +) + +var _ Path = &unixPath{} + +// NewUnixPath parse a string and make it a unix-style path +func NewUnixPath(path string) (Path, error) { + if len(path) == 0 { + return nil, errors.New("path is empty") + } + return &unixPath{tokens: strings.Split(path, "/")}, nil +} + +type unixPath struct { + tokens []string +} + +func (p *unixPath) Raw() string { + return strings.Join(p.tokens, "/") +} + +func (p *unixPath) String() string { + return p.Normalize().Raw() +} + +func (p *unixPath) TargetOS() TargetOS { + return Unix +} + +func (p *unixPath) Kind() Kind { + switch p.tokens[0] { + case "": + return Absolute + case "~": + return HomeRooted + default: + return Relative + } +} + +func (p *unixPath) Separator() rune { + return '/' +} + +func (p *unixPath) segments() []string { + // clone + result := make([]string, len(p.tokens)) + copy(result, p.tokens) + return result +} + +func (p *unixPath) Normalize() Path { + var result []string + if p.Kind() == Absolute { + result = []string{""} + } + for _, s := range p.tokens { + switch s { + case "": + continue + case ".": + continue + case "..": + if p.Kind() == Absolute && len(result) <= 1 { + continue + } + if len(result) == 0 || + result[len(result)-1] == ".." || + (len(result) == 1 && result[0] == "~") { + result = append(result, "..") + } else { + result = result[:len(result)-1] + } + default: + result = append(result, s) + } + } + if len(result) == 0 { + result = []string{"."} + } + return &unixPath{tokens: result} +} + +func (p *unixPath) Join(paths ...Path) (Path, error) { + if len(paths) == 0 { + return p, nil + } + head := paths[0] + tail := paths[1:] + if head.Kind() != Relative && head.Kind() != HomeRooted { + return nil, errors.New("can only join relative or home rooted paths") + } + var err error + if head, err = head.Convert(Unix); err != nil { + return nil, err + } + + segs := head.segments() + if head.Kind() == HomeRooted { + segs = segs[1:] + } + current := &unixPath{tokens: append(p.tokens, segs...)} + return current.Join(tail...) +} + +func (p *unixPath) Convert(os TargetOS) (Path, error) { + if os == Unix { + return p, nil + } + switch p.Kind() { + case Relative, HomeRooted: + return NewWindowsPath(strings.Join(p.tokens, `\`), false) + default: + return nil, errors.New("only relative and home rooted paths can be converted") + } +} + +func (p *unixPath) hasWindowsSpecificNamespacePrefix() bool { + return false +} diff --git a/vendor/github.com/simonferquel/crosspath/windowspath.go b/vendor/github.com/simonferquel/crosspath/windowspath.go new file mode 100644 index 000000000000..aa06b6a5ae5a --- /dev/null +++ b/vendor/github.com/simonferquel/crosspath/windowspath.go @@ -0,0 +1,229 @@ +package crosspath + +import ( + "errors" + "fmt" + "strings" +) + +const ( + windowsForbiddenRunes = "<>:\"/\\|?*\r\n\t" + windowsFirstTokenForbiddenRunes = "<>\"/\\|?*\r\n\t" + win32FileSystemNamespacePrefix = `\\?\` + win32DeviceNamespacePrefix = `\\.\` + uncPrefix = `\\` +) + +func tokenizeWindowsPath(path string, isUnc bool) ([]string, error) { + tokens := strings.Split(path, `\`) + for ix, token := range tokens { + forbidden := windowsForbiddenRunes + if ix == 0 && !isUnc { + forbidden = windowsFirstTokenForbiddenRunes + } + if strings.ContainsAny(token, forbidden) { + return nil, fmt.Errorf("invalid charcter in token %q", token) + } + if strings.HasSuffix(token, " ") { + return nil, fmt.Errorf("token %q should not end with a space", token) + } + if token != "." && token != ".." && strings.HasSuffix(token, ".") { + return nil, fmt.Errorf("token %q should not end with a dot", token) + } + } + if !isUnc && len(tokens) > 0 { + // on non-unc path, ':' can only occur at the second character position + idx := strings.IndexRune(tokens[0], ':') + if idx != -1 && idx != 1 { + return nil, fmt.Errorf("token %q should not contain ':' at this position", tokens[0]) + } + } + return tokens, nil +} + +// NewWindowsPath parses a Windows file path +func NewWindowsPath(path string, convertSlashes bool) (Path, error) { + if convertSlashes { + path = strings.Replace(path, "/", `\`, -1) + } + namespacePrefix := "" + prefix := "" + isUnc := false + if strings.HasPrefix(path, win32FileSystemNamespacePrefix) { + path = strings.TrimPrefix(path, win32FileSystemNamespacePrefix) + namespacePrefix = win32FileSystemNamespacePrefix + if strings.HasPrefix(path, `UNC\`) { + isUnc = true + prefix = `UNC\` + path = strings.TrimPrefix(path, `UNC\`) + } + } else if strings.HasPrefix(path, win32DeviceNamespacePrefix) { + path = strings.TrimPrefix(path, win32DeviceNamespacePrefix) + namespacePrefix = win32DeviceNamespacePrefix + } else if strings.HasPrefix(path, uncPrefix) { + isUnc = true + path = strings.TrimPrefix(path, uncPrefix) + prefix = uncPrefix + } + tokens, err := tokenizeWindowsPath(path, isUnc) + if err != nil { + return nil, err + } + // validation rules + + if len(tokens) == 0 { + return nil, errors.New("unsupported empty path") + } + + // if unc, first token cannot be empty, . or .. + if isUnc && + (tokens[0] == "" || tokens[0] == "." || tokens[0] == "..") { + return nil, errors.New("invalid unc path") + } + + // device namespace prefix forbid unc paths + if isUnc && namespacePrefix == win32DeviceNamespacePrefix { + return nil, errors.New("cannot express UNC paths after a windows device namespace prefix") + } + return &windowsPath{ + namespacePrefix: namespacePrefix, + prefix: prefix, + unc: isUnc, + tokens: tokens, + }, nil +} + +type windowsPath struct { + namespacePrefix string + prefix string + unc bool + tokens []string +} + +func (p *windowsPath) Raw() string { + return p.namespacePrefix + p.prefix + strings.Join(p.tokens, `\`) +} + +func (p *windowsPath) String() string { + return p.Normalize().Raw() +} + +func (p *windowsPath) TargetOS() TargetOS { + return Windows +} + +func isTokenWindowsDriveRoot(token string) bool { + return len(token) == 2 && token[1] == ':' +} +func (p *windowsPath) Kind() Kind { + if p.namespacePrefix == win32DeviceNamespacePrefix { + return WindowsDevice + } + if p.unc { + return UNC + } + switch { + case p.unc: + return UNC + case p.tokens[0] == `~`: + return HomeRooted + case isTokenWindowsDriveRoot(p.tokens[0]): + return Absolute + case (len(p.tokens[0]) > 2 && p.tokens[0][1] == ':'): + return RelativeFromDriveCurrentDir + case p.tokens[0] == "": + return AbsoluteFromCurrentDrive + default: + return Relative + } +} + +func (p *windowsPath) Separator() rune { + return '\\' +} + +func (p *windowsPath) segments() []string { + // clone + result := make([]string, len(p.tokens)) + copy(result, p.tokens) + return result +} + +func (p *windowsPath) Normalize() Path { + if p.namespacePrefix == win32FileSystemNamespacePrefix { + // using this namespace bypasses all path resolution mechanism + // and allows underlying file system drivers to interpret paths names like "." or ".." themselves + // so, do nothing + return p + } + kind := p.Kind() + var result []string + if kind == AbsoluteFromCurrentDrive { + result = []string{""} + } + for _, token := range p.tokens { + switch token { + case "": + continue + case ".": + continue + case "..": + if (kind == Absolute || kind == UNC || kind == AbsoluteFromCurrentDrive) && len(result) <= 1 { + continue + } + if len(result) == 0 || + result[len(result)-1] == ".." || + (len(result) == 1 && result[0] == "~") { + result = append(result, "..") + } else { + result = result[:len(result)-1] + } + default: + result = append(result, token) + } + } + if len(result) == 0 { + result = []string{"."} + } + if len(result) == 1 && (kind == Absolute || kind == AbsoluteFromCurrentDrive) { + result = append(result, "") + } + return &windowsPath{namespacePrefix: p.namespacePrefix, prefix: p.prefix, tokens: result, unc: p.unc} +} + +func (p *windowsPath) Join(paths ...Path) (Path, error) { + if len(paths) == 0 { + return p, nil + } + head := paths[0] + tail := paths[1:] + if head.Kind() != Relative && head.Kind() != HomeRooted { + return nil, errors.New("can only join relative paths") + } + var err error + if head, err = head.Convert(Unix); err != nil { + return nil, err + } + segs := head.segments() + if head.Kind() == HomeRooted { + segs = segs[1:] + } + current := &windowsPath{tokens: append(p.tokens, segs...), namespacePrefix: p.namespacePrefix, prefix: p.prefix, unc: p.unc} + return current.Join(tail...) +} + +func (p *windowsPath) Convert(os TargetOS) (Path, error) { + if os == Windows { + return p, nil + } + switch p.Kind() { + case Relative, HomeRooted: + return NewUnixPath(strings.Join(p.tokens, `/`)) + default: + return nil, errors.New("only relative and home rooted paths can be converted") + } +} + +func (p *windowsPath) hasWindowsSpecificNamespacePrefix() bool { + return p.namespacePrefix != "" +}