package main import ( "fmt" "path/filepath" "strings" ) // Spec is a parsed source/dest location: [user@]host:path, or a bare local // path (Host == ""). type Spec struct { Raw string User string Host string Path string } func (s Spec) IsLocal() bool { return s.Host == "" } func (s Spec) String() string { if s.IsLocal() { return s.Path } if s.User != "" { return fmt.Sprintf("%s@%s:%s", s.User, s.Host, s.Path) } return fmt.Sprintf("%s:%s", s.Host, s.Path) } // parseSpec parses "[user@]host:path" or a local "path". A leading "/", // "./" or "../", or the absence of any colon, is treated as a local path so // that ordinary absolute/relative paths are never mistaken for a host spec. func parseSpec(raw string) (Spec, error) { if raw == "" { return Spec{}, fmt.Errorf("empty location") } if strings.HasPrefix(raw, "/") || strings.HasPrefix(raw, "./") || strings.HasPrefix(raw, "../") || !strings.Contains(raw, ":") { return Spec{Raw: raw, Path: raw}, nil } idx := strings.Index(raw, ":") hostpart := raw[:idx] path := raw[idx+1:] if hostpart == "" || path == "" || strings.ContainsAny(hostpart, "/ ") { return Spec{}, fmt.Errorf("cannot parse location %q (expected [user@]host:path or a local path)", raw) } user := "" host := hostpart if at := strings.Index(hostpart, "@"); at >= 0 { user = hostpart[:at] host = hostpart[at+1:] } if host == "" { return Spec{}, fmt.Errorf("cannot parse location %q: empty host", raw) } return Spec{Raw: raw, User: user, Host: host, Path: path}, nil } // checkNotSame does a best-effort local check that source and dest don't // refer to the exact same path, to avoid an obviously destructive mistake. // It cannot resolve whether two different remote hostnames are actually the // same machine. func checkNotSame(src, dst Spec) error { if src.IsLocal() != dst.IsLocal() { return nil } if !src.IsLocal() && !strings.EqualFold(src.Host, dst.Host) { return nil } if !src.IsLocal() && src.User != dst.User { return nil } if filepath.Clean(src.Path) == filepath.Clean(dst.Path) { return fmt.Errorf("source and destination resolve to the same path (%s)", src.Path) } return nil }