package loader import ( "strings" "unicode" "unicode/utf8" "github.com/docker/cli/cli/compose/types" "github.com/docker/docker/api/types/mount" "github.com/pkg/errors" ) const endOfSpec = rune(0) // ParseVolume parses a volume spec without any knowledge of the target platform func ParseVolume(spec string) (types.ServiceVolumeConfig, error) { volume := types.ServiceVolumeConfig{} switch len(spec) { case 0: return volume, errors.New("invalid empty volume spec") case 1, 2: volume.Target = spec volume.Type = string(mount.TypeVolume) return volume, nil } buffer := []rune{} for _, char := range spec + string(endOfSpec) { switch { case isWindowsDrive(buffer, char): buffer = append(buffer, char) case char == ':' || char == endOfSpec: if err := populateFieldFromBuffer(char, buffer, &volume); err != nil { populateType(&volume) return volume, errors.Wrapf(err, "invalid spec: %s", spec) } buffer = []rune{} default: buffer = append(buffer, char) } } populateType(&volume) return volume, nil } func isWindowsDrive(buffer []rune, char rune) bool { return char == ':' && len(buffer) == 1 && unicode.IsLetter(buffer[0]) } func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolumeConfig) error { strBuffer := string(buffer) switch { case len(buffer) == 0: return errors.New("empty section between colons") // Anonymous volume case volume.Source == "" && char == endOfSpec: volume.Target = strBuffer return nil case volume.Source == "": volume.Source = strBuffer return nil case volume.Target == "": volume.Target = strBuffer return nil case char == ':': return errors.New("too many colons") } for _, option := range strings.Split(strBuffer, ",") { switch option { case "ro": volume.ReadOnly = true case "rw": volume.ReadOnly = false case "nocopy": volume.Volume = &types.ServiceVolumeVolume{NoCopy: true} default: if isBindOption(option) { volume.Bind = &types.ServiceVolumeBind{Propagation: option} } // ignore unknown options } } return nil } func isBindOption(option string) bool { for _, propagation := range mount.Propagations { if mount.Propagation(option) == propagation { return true } } return false } func populateType(volume *types.ServiceVolumeConfig) { switch { // Anonymous volume case volume.Source == "": volume.Type = string(mount.TypeVolume) case isFilePath(volume.Source): volume.Type = string(mount.TypeBind) default: volume.Type = string(mount.TypeVolume) } } func isFilePath(source string) bool { switch source[0] { case '.', '/', '~': return true } // windows named pipes if strings.HasPrefix(source, `\\`) { return true } first, nextIndex := utf8.DecodeRuneInString(source) return isWindowsDrive([]rune{first}, rune(source[nextIndex])) }