blob: 5f24404874b6d730ffab7e79a61a0cf595056da6 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package naming
import (
"strings"
)
// SplitAddressName takes an object name and returns the server address and
// the name relative to the server.
// The name parameter may be a rooted name or a relative name; an empty string
// address is returned for the latter case.
// The returned address may be in endpoint format or host:port format.
func SplitAddressName(name string) (string, string) {
name = Clean(name)
if !Rooted(name) {
return "", name
}
name = name[1:] // trim the beginning "/"
if len(name) == 0 {
return "", ""
}
// Could have used regular expressions, but that makes this function
// 10x slower as per the benchmark.
if strings.HasPrefix(name, "@") { // <endpoint>/<suffix>
addr, suffix := splitIntoTwo(name, "@@/")
if len(suffix) > 0 { // The trailing "@@" was stripped, restore it
addr = addr + "@@"
}
return addr, suffix
}
if strings.HasPrefix(name, "(") { // (blessing)@host:[port]/suffix
_, tmp := splitIntoTwo(name, ")@")
_, suffix := splitIntoTwo(tmp, "/")
return strings.TrimSuffix(name, "/"+suffix), suffix
}
// host:[port]/suffix
return splitIntoTwo(name, "/")
}
// JoinAddressName takes an address and a relative name and returns a rooted
// or relative name. If a valid address is supplied then the returned name
// will always be a rooted name (i.e. starting with /), otherwise it may
// be relative. Address should not start with a / and if it does,
// that prefix will be stripped.
func JoinAddressName(address, name string) string {
address = strings.TrimLeft(address, "/")
if len(address) == 0 {
return Clean(name)
}
if len(name) == 0 {
return Clean("/" + address)
}
return Clean("/" + address + "/" + name)
}
// Rooted returns true for any name that is considered to be rooted.
// A rooted name is one that starts with a single / followed by
// a non /. / on its own is considered rooted.
func Rooted(name string) bool {
return strings.HasPrefix(name, "/")
}
// Join takes a variable number of name fragments and concatenates them
// together using '/'. The returned name is cleaned of multiple adjacent
// '/'s.
func Join(elems ...string) string {
for len(elems) > 0 && elems[0] == "" {
elems = elems[1:]
}
return Clean(strings.Join(elems, "/"))
}
// TrimSuffix removes the suffix (and any connecting '/') from
// the name.
func TrimSuffix(name, suffix string) string {
name = Clean(name)
suffix = Clean(suffix)
// Easy cases first.
if name == suffix {
return ""
}
if len(suffix) >= len(name) {
return name
}
// A suffix starting with a slash cannot be a partial match.
if strings.HasPrefix(suffix, "/") {
return name
}
// At this point suffix is guaranteed not to start with a '/' and
// suffix is shorter than name.
if strings.HasSuffix(name, suffix) {
prefix := strings.TrimSuffix(name, suffix)
if strings.HasSuffix(prefix, "/") {
if len(prefix) == 1 {
return name
}
return strings.TrimSuffix(prefix, "/")
}
}
return name
}
// IsReserved returns true if name is a reserved name.
func IsReserved(name string) bool {
return strings.HasPrefix(name, ReservedNamePrefix)
}
// StripReserved returns the name stripped of the reserved prefix.
func StripReserved(name string) string {
if IsReserved(name) {
return name[len(ReservedNamePrefix):]
}
return name
}
// Clean reduces multiple adjacent slashes to a single slash.
// It also removes any trailing slash.
func Clean(name string) string {
// Eradicate duplicate slashes and trailing slashes. We
// could use path.Clean but it has other side effects.
for strings.Contains(name, "//") {
name = strings.Replace(name, "//", "/", -1)
}
if name == "/" {
return name
}
return strings.TrimSuffix(name, "/")
}
func splitIntoTwo(str, separator string) (string, string) {
elems := strings.SplitN(str, separator, 2)
if len(elems) == 1 {
return elems[0], ""
}
return elems[0], elems[1]
}
// Escape encodes a string replacing the characters in <special> and % with a %<hex> escape.
// For efficiency we assume that special contains only single byte characters. If we need
// to escape runes, we'll have to rethink.
func Escape(s, special string) string {
special += "%"
if !strings.ContainsAny(s, special) {
return s
}
// Enough room to escape every character.
t := make([]byte, len(s)*3)
j := 0
for i := 0; i < len(s); i++ {
c := s[i]
if strings.IndexByte(special, c) < 0 {
t[j] = c
j++
continue
}
t[j] = '%'
t[j+1] = "0123456789ABCDEF"[c>>4]
t[j+2] = "0123456789ABCDEF"[c&15]
j += 3
}
return string(t[:j])
}
// EncodeAsNameElement makes a string representable as a name element by escaping slashes.
func EncodeAsNameElement(s string) string {
return Escape(s, "/")
}
func isHex(c byte) bool {
switch {
case '0' <= c && c <= '9':
return true
case 'a' <= c && c <= 'f':
return true
case 'A' <= c && c <= 'F':
return true
}
return false
}
func unHex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}
// Unescape decodes %<hex> encodings in a string into the relevant character. It returns false on error.
func Unescape(s string) (string, bool) {
if !strings.Contains(s, "%") {
return s, true
}
t := make([]byte, len(s))
j := 0
for i := 0; i < len(s); {
if s[i] != '%' {
t[j] = s[i]
j++
i++
continue
}
if len(s) <= i+2 || !isHex(s[i+1]) || !isHex(s[i+2]) {
return s, false
}
t[j] = unHex(s[i+1])<<4 | unHex(s[i+2])
i += 3
j++
}
return string(t[:j]), true
}
// DecodeFromNameElement decodes an encoded name element. If s is correctly encoded return
// the decoded string and true. Otherwise return the original string and false.
//
// Note that this is more than the inverse of EncodeAsNameElement since it cn handle more hex encodings than
// / and %. This is intentional since we'll most likely want to add other letters to the set to be encoded.
func DecodeFromNameElement(s string) (string, bool) {
return Unescape(s)
}