misc.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package utils
  15. import (
  16. "encoding/binary"
  17. "fmt"
  18. "net"
  19. "net/url"
  20. "reflect"
  21. "regexp"
  22. "strconv"
  23. "strings"
  24. "time"
  25. "yunion.io/x/pkg/errors"
  26. )
  27. type selectFunc func(obj interface{}) (string, error)
  28. func ToDict(objs interface{}, ks selectFunc) (map[string]interface{}, error) {
  29. s := reflect.ValueOf(objs)
  30. if s.Kind() != reflect.Slice {
  31. return nil, fmt.Errorf("Not slice")
  32. }
  33. res := map[string]interface{}{}
  34. for i := 0; i < s.Len(); i++ {
  35. obj := s.Index(i).Interface()
  36. key, err := ks(obj)
  37. if err != nil {
  38. return nil, err
  39. }
  40. res[key] = obj
  41. }
  42. return res, nil
  43. }
  44. func GroupBy(items interface{}, ks selectFunc) (map[string][]interface{}, error) {
  45. s := reflect.ValueOf(items)
  46. if s.Kind() != reflect.Slice {
  47. return nil, fmt.Errorf("Not slice")
  48. }
  49. res := map[string][]interface{}{}
  50. for i := 0; i < s.Len(); i++ {
  51. obj := s.Index(i).Interface()
  52. key, err := ks(obj)
  53. if err != nil {
  54. return nil, err
  55. }
  56. values, ok := res[key]
  57. if !ok {
  58. values = []interface{}{}
  59. }
  60. values = append(values, obj)
  61. res[key] = values
  62. }
  63. return res, nil
  64. }
  65. func SelectDistinct(items []interface{}, ks selectFunc) ([]string, error) {
  66. keyMap := make(map[string]interface{})
  67. for _, item := range items {
  68. key, err := ks(item)
  69. if err != nil {
  70. return nil, err
  71. }
  72. keyMap[key] = item
  73. }
  74. keys := []string{}
  75. for key := range keyMap {
  76. keys = append(keys, key)
  77. }
  78. return keys, nil
  79. }
  80. func SubDict(dict map[string][]interface{}, keys ...string) (map[string][]interface{}, error) {
  81. if len(keys) == 0 {
  82. return dict, nil
  83. }
  84. res := make(map[string][]interface{})
  85. for _, key := range keys {
  86. if value, ok := dict[key]; ok {
  87. res[key] = value
  88. }
  89. }
  90. return res, nil
  91. }
  92. type StatItem2 interface {
  93. First() string
  94. Second() interface{}
  95. }
  96. type StatItem3 interface {
  97. First() string
  98. Second() string
  99. Third() interface{}
  100. }
  101. func ToStatDict2(items []StatItem2) (map[string]interface{}, error) {
  102. res := make(map[string]interface{})
  103. if len(items) == 0 {
  104. return res, nil
  105. }
  106. for _, item := range items {
  107. res[item.First()] = item.Second()
  108. }
  109. return res, nil
  110. }
  111. func ToStatDict3(items []StatItem3) (map[string]map[string]interface{}, error) {
  112. res := make(map[string]map[string]interface{})
  113. if len(items) == 0 {
  114. return res, nil
  115. }
  116. for _, item := range items {
  117. d1, ok := res[item.First()]
  118. if !ok {
  119. d1 = make(map[string]interface{})
  120. res[item.First()] = d1
  121. }
  122. d1[item.Second()] = item.Third()
  123. }
  124. return res, nil
  125. }
  126. func ConvertError(obj interface{}, toType string) error {
  127. return fmt.Errorf("Type cast error: %#v => %s", obj, toType)
  128. }
  129. func HasPrefix(s string, prefix string) bool {
  130. return len(s) >= len(prefix) && s[0:len(prefix)] == prefix
  131. }
  132. func HasSuffix(s string, suffix string) bool {
  133. return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
  134. }
  135. func IsMatch(s string, pattern string) bool {
  136. success, _ := regexp.MatchString(pattern, s)
  137. return success
  138. }
  139. func IsMatchIP4(s string) bool {
  140. return IsMatch(s, "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$")
  141. }
  142. func IsMatchIP6(s string) bool {
  143. return IsMatch(s, "^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}))|:)))(%.+)?i\\s*$")
  144. }
  145. func IsMatchCompactMacAddr(s string) bool {
  146. return IsMatch(s, "^[0-9a-fA-F]{12}$")
  147. }
  148. func IsMatchMacAddr(s string) bool {
  149. return IsMatch(s, "^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$")
  150. }
  151. func IsMatchSize(s string) bool {
  152. return IsMatch(s, "^\\d+[bBkKmMgG]?$")
  153. }
  154. func IsMatchUUID(s string) bool {
  155. return IsMatch(s, "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$")
  156. }
  157. func IsMatchInteger(s string) bool {
  158. return IsMatch(s, "^[0-9]+$")
  159. }
  160. func IsMatchFloat(s string) bool {
  161. return IsMatch(s, "^\\d+(\\.\\d*)?$")
  162. }
  163. func Max(a int64, b int64) int64 {
  164. if a > b {
  165. return a
  166. }
  167. return b
  168. }
  169. func Min(a int64, b int64) int64 {
  170. if a < b {
  171. return a
  172. }
  173. return b
  174. }
  175. type Pred_t func(interface{}) bool
  176. func Any(pred Pred_t, items ...interface{}) (bool, interface{}) {
  177. for _, it := range items {
  178. if pred(it) {
  179. return true, it
  180. }
  181. }
  182. return false, nil
  183. }
  184. func All(pred Pred_t, items ...interface{}) (bool, interface{}) {
  185. for _, it := range items {
  186. if !pred(it) {
  187. return false, it
  188. }
  189. }
  190. return true, nil
  191. }
  192. func IsLocalStorage(storageType string) bool {
  193. s := storageType
  194. return s == "local" || s == "baremetal" || s == "raw" ||
  195. s == "docker" || s == "volume" || s == "lvm"
  196. }
  197. func DistinctJoin(list []string, separator string) string {
  198. return strings.Join(Distinct(list), separator)
  199. }
  200. func Distinct(list []string) []string {
  201. ss := []string{}
  202. ssMap := make(map[string]int)
  203. for _, s := range list {
  204. if _, ok := ssMap[s]; !ok {
  205. ssMap[s] = 0
  206. ss = append(ss, s)
  207. }
  208. }
  209. return ss
  210. }
  211. func Ip2Int(ipString string) uint32 {
  212. ip := net.ParseIP(ipString)
  213. if ip == nil {
  214. return 0
  215. }
  216. if len(ip) == 16 {
  217. return binary.BigEndian.Uint32(ip[12:16])
  218. }
  219. return binary.BigEndian.Uint32(ip)
  220. }
  221. func IpRangeCount(ipStart, ipEnd string) int {
  222. return int(Ip2Int(ipEnd) - Ip2Int(ipStart) + 1)
  223. }
  224. var (
  225. PrivateIP1Start = Ip2Int("10.0.0.0")
  226. PrivateIP1End = Ip2Int("10.255.255.255")
  227. PrivateIP2Start = Ip2Int("172.16.0.0")
  228. PrivateIP2End = Ip2Int("172.31.255.255")
  229. PrivateIP3Start = Ip2Int("192.168.0.0")
  230. PrivateIP3End = Ip2Int("192.168.255.255")
  231. HostLocalIPStart = Ip2Int("127.0.0.0")
  232. HostLocalIPEnd = Ip2Int("127.255.255.255")
  233. LinkLocalIPStart = Ip2Int("169.254.0.0")
  234. LinkLocalIPEnd = Ip2Int("169.254.255.255")
  235. )
  236. func IsPrivate(ip uint32) bool {
  237. return (PrivateIP1Start <= ip && ip <= PrivateIP1End) ||
  238. (PrivateIP2Start <= ip && ip <= PrivateIP3End) ||
  239. (PrivateIP3Start <= ip && ip <= PrivateIP3End)
  240. }
  241. func IsHostLocal(ip uint32) bool {
  242. return HostLocalIPStart <= ip && ip <= HostLocalIPEnd
  243. }
  244. func IsLinkLocal(ip uint32) bool {
  245. return LinkLocalIPStart <= ip && ip <= LinkLocalIPEnd
  246. }
  247. func IsExitAddress(ip string) bool {
  248. ipUint32 := Ip2Int(ip)
  249. if ipUint32 == 0 {
  250. return false
  251. }
  252. return !(IsPrivate(ipUint32) || IsHostLocal(ipUint32) || IsLinkLocal(ipUint32))
  253. }
  254. func Truncate(s string, length int) string {
  255. if len(s) <= length {
  256. return s
  257. }
  258. return s[0:length] + "..."
  259. }
  260. // From html/template/content.go
  261. // Copyright 2011 The Go Authors. All rights reserved.
  262. // indirect returns the value, after dereferencing as many times
  263. // as necessary to reach the base type (or nil).
  264. func indirect(a interface{}) interface{} {
  265. if a == nil {
  266. return nil
  267. }
  268. if t := reflect.TypeOf(a); t.Kind() != reflect.Ptr {
  269. // Avoid creating a reflect.Value if it's not a pointer.
  270. return a
  271. }
  272. v := reflect.ValueOf(a)
  273. for v.Kind() == reflect.Ptr && !v.IsNil() {
  274. v = v.Elem()
  275. }
  276. return v.Interface()
  277. }
  278. // ToInt64E casts an interface to an int64 type.
  279. func ToInt64E(i interface{}) (int64, error) {
  280. i = indirect(i)
  281. switch s := i.(type) {
  282. case int:
  283. return int64(s), nil
  284. case int64:
  285. return s, nil
  286. case int32:
  287. return int64(s), nil
  288. case int16:
  289. return int64(s), nil
  290. case int8:
  291. return int64(s), nil
  292. case uint:
  293. return int64(s), nil
  294. case uint64:
  295. return int64(s), nil
  296. case uint32:
  297. return int64(s), nil
  298. case uint16:
  299. return int64(s), nil
  300. case uint8:
  301. return int64(s), nil
  302. case float64:
  303. return int64(s), nil
  304. case float32:
  305. return int64(s), nil
  306. case string:
  307. v, err := strconv.ParseInt(s, 0, 0)
  308. if err == nil {
  309. return v, nil
  310. }
  311. return 0, fmt.Errorf("unable to cast %#v of type %T to int64", i, i)
  312. case bool:
  313. if s {
  314. return 1, nil
  315. }
  316. return 0, nil
  317. case nil:
  318. return 0, nil
  319. default:
  320. return 0, fmt.Errorf("unable to cast %#v of type %T to int64", i, i)
  321. }
  322. }
  323. // ToInt64 casts an interface to an int64 type.
  324. func ToInt64(i interface{}) int64 {
  325. v, _ := ToInt64E(i)
  326. return v
  327. }
  328. // ToFloat64 casts an interface to a float64 type.
  329. func ToFloat64(i interface{}) float64 {
  330. v, _ := ToFloat64E(i)
  331. return v
  332. }
  333. // ToFloat64E casts an interface to a float64 type.
  334. func ToFloat64E(i interface{}) (float64, error) {
  335. i = indirect(i)
  336. switch s := i.(type) {
  337. case float64:
  338. return s, nil
  339. case float32:
  340. return float64(s), nil
  341. case int:
  342. return float64(s), nil
  343. case int64:
  344. return float64(s), nil
  345. case int32:
  346. return float64(s), nil
  347. case int16:
  348. return float64(s), nil
  349. case int8:
  350. return float64(s), nil
  351. case uint:
  352. return float64(s), nil
  353. case uint64:
  354. return float64(s), nil
  355. case uint32:
  356. return float64(s), nil
  357. case uint16:
  358. return float64(s), nil
  359. case uint8:
  360. return float64(s), nil
  361. case string:
  362. v, err := strconv.ParseFloat(s, 64)
  363. if err == nil {
  364. return v, nil
  365. }
  366. return 0, fmt.Errorf("unable to cast %#v of type %T to float64", i, i)
  367. case bool:
  368. if s {
  369. return 1, nil
  370. }
  371. return 0, nil
  372. default:
  373. return 0, fmt.Errorf("unable to cast %#v of type %T to float64", i, i)
  374. }
  375. }
  376. func ToDurationE(i interface{}) (d time.Duration, err error) {
  377. i = indirect(i)
  378. switch s := i.(type) {
  379. case time.Duration:
  380. return s, nil
  381. case int, int64, int32, int16, int8, uint, uint64, uint32, uint16, uint8:
  382. d = time.Duration(ToInt64(s))
  383. return
  384. case float32, float64:
  385. d = time.Duration(ToFloat64(s))
  386. return
  387. case string:
  388. if strings.ContainsAny(s, "nsuµmh") {
  389. d, err = time.ParseDuration(s)
  390. } else {
  391. d, err = time.ParseDuration(s + "ns")
  392. }
  393. return
  394. default:
  395. err = fmt.Errorf("unable to cast %#v of type %T to Duration", i, i)
  396. return
  397. }
  398. }
  399. func ToDuration(i interface{}) time.Duration {
  400. v, _ := ToDurationE(i)
  401. return v
  402. }
  403. // GetSize parse size string to int
  404. // defaultSize be used when sizeStr not end with defaultSize
  405. // sizeStr: 1024, 1M, 1m, 1K etc.
  406. // defaultSize: g, m, k, b etc.
  407. // base: base multiple unit, 1024
  408. func GetSize(sizeStr, defaultSize string, base int64) (size int64, err error) {
  409. if IsMatchInteger(sizeStr) {
  410. sizeStr += defaultSize
  411. }
  412. sizeNumStr := sizeStr[0 : len(sizeStr)-1]
  413. size, err = strconv.ParseInt(sizeNumStr, 10, 64)
  414. if err != nil {
  415. return
  416. }
  417. switch u := sizeStr[len(sizeStr)-1]; u {
  418. case 't', 'T':
  419. size = size * base * base * base * base
  420. case 'g', 'G':
  421. size = size * base * base * base
  422. case 'm', 'M':
  423. size = size * base * base
  424. case 'k', 'K':
  425. size = size * base
  426. case 'b', 'B':
  427. size = size
  428. default:
  429. err = fmt.Errorf("Incorrect unit %q", u)
  430. }
  431. return
  432. }
  433. func GetSizeBytes(sizeStr, defaultSize string) (int64, error) {
  434. return GetSize(sizeStr, defaultSize, 1024)
  435. }
  436. func GetBytes(sizeStr string) (int64, error) {
  437. if IsMatchInteger(sizeStr) {
  438. return 0, fmt.Errorf("Please append suffix unit like '[g, m, k, b]' to %q", sizeStr)
  439. }
  440. return GetSize(sizeStr, "", 1024)
  441. }
  442. func GetSizeGB(sizeStr, defaultSize string) (int64, error) {
  443. bytes, err := GetSizeBytes(sizeStr, defaultSize)
  444. if err != nil {
  445. return 0, err
  446. }
  447. return bytes / 1024 / 1024 / 1024, nil
  448. }
  449. func GetSizeMB(sizeStr, defaultSize string) (int64, error) {
  450. bytes, err := GetSizeBytes(sizeStr, defaultSize)
  451. if err != nil {
  452. return 0, err
  453. }
  454. return bytes / 1024 / 1024, nil
  455. }
  456. func GetSizeKB(sizeStr, defaultSize string) (int64, error) {
  457. bytes, err := GetSizeBytes(sizeStr, defaultSize)
  458. if err != nil {
  459. return 0, err
  460. }
  461. return bytes / 1024, nil
  462. }
  463. func transMysqlQuery(dburl string) (string, error) {
  464. queryPos := strings.IndexByte(dburl, '?')
  465. if queryPos == 0 {
  466. return "", fmt.Errorf("Missing database name")
  467. }
  468. var query url.Values
  469. if queryPos > 0 {
  470. queryStr := dburl[queryPos+1:]
  471. if len(queryStr) > 0 {
  472. var err error
  473. query, err = url.ParseQuery(queryStr)
  474. if err != nil {
  475. return "", errors.Wrap(err, "ParseQuery")
  476. }
  477. }
  478. dburl = dburl[:queryPos]
  479. } else {
  480. query = url.Values{}
  481. }
  482. query.Set("parseTime", "true")
  483. query.Set("charset", "utf8mb4")
  484. query.Set("interpolateParams", "true")
  485. return dburl + "?" + query.Encode(), nil
  486. }
  487. func TransSQLAchemyURL(pySQLSrc string) (dialect, ret string, err error) {
  488. if len(pySQLSrc) == 0 {
  489. err = fmt.Errorf("Empty input")
  490. return
  491. }
  492. dialect = "mysql"
  493. if !strings.Contains(pySQLSrc, `//`) {
  494. pySQLSrc, err = transMysqlQuery(pySQLSrc)
  495. if err != nil {
  496. return
  497. }
  498. return dialect, pySQLSrc, nil
  499. }
  500. lastAtIndex := strings.LastIndex(pySQLSrc, "@")
  501. if lastAtIndex == -1 {
  502. err = fmt.Errorf("Incorrect mysql connection url: %s", pySQLSrc)
  503. return
  504. }
  505. firstPart := pySQLSrc[:lastAtIndex]
  506. secondPart := pySQLSrc[lastAtIndex+1:]
  507. r := regexp.MustCompile(`[/:]+`)
  508. firstPartArr := r.Split(firstPart, -1)
  509. if len(firstPartArr) < 3 {
  510. err = fmt.Errorf("Incorrect mysql connection url: %s", pySQLSrc)
  511. return
  512. }
  513. user := firstPartArr[1]
  514. passwd := firstPartArr[2]
  515. var host, port, dburl string
  516. if strings.HasPrefix(secondPart, "[") {
  517. endBracket := strings.Index(secondPart, "]")
  518. if endBracket == -1 {
  519. err = fmt.Errorf("Incorrect IPv6 address format: %s", pySQLSrc)
  520. return
  521. }
  522. host = secondPart[:endBracket+1]
  523. remaining := secondPart[endBracket+1:]
  524. if !strings.HasPrefix(remaining, ":") {
  525. err = fmt.Errorf("Incorrect mysql connection url: %s", pySQLSrc)
  526. return
  527. }
  528. remaining = remaining[1:]
  529. slashIndex := strings.Index(remaining, "/")
  530. if slashIndex == -1 {
  531. err = fmt.Errorf("Incorrect mysql connection url: %s", pySQLSrc)
  532. return
  533. }
  534. port = remaining[:slashIndex]
  535. dburl = remaining[slashIndex+1:]
  536. } else {
  537. colonIndex := strings.Index(secondPart, ":")
  538. if colonIndex == -1 {
  539. err = fmt.Errorf("Incorrect mysql connection url: %s", pySQLSrc)
  540. return
  541. }
  542. host = secondPart[:colonIndex]
  543. remaining := secondPart[colonIndex+1:]
  544. slashIndex := strings.Index(remaining, "/")
  545. if slashIndex == -1 {
  546. err = fmt.Errorf("Incorrect mysql connection url: %s", pySQLSrc)
  547. return
  548. }
  549. port = remaining[:slashIndex]
  550. dburl = remaining[slashIndex+1:]
  551. }
  552. dburl, err = transMysqlQuery(dburl)
  553. if err != nil {
  554. return
  555. }
  556. ret = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", user, passwd, host, port, dburl)
  557. return
  558. }
  559. func ComposeURL(paths ...string) string {
  560. if len(paths) == 0 || len(paths[0]) == 0 {
  561. return ""
  562. }
  563. restURL := ComposeURL(paths[1:]...)
  564. if len(restURL) == 0 {
  565. return fmt.Sprintf("/%s", paths[0])
  566. }
  567. return fmt.Sprintf("/%s%s", paths[0], ComposeURL(paths[1:]...))
  568. }