348 lines
9.4 KiB
Go
348 lines
9.4 KiB
Go
|
package url
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"regexp"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
const regexPartProtocol = "^((?:lbry://|https://)?)"
|
||
|
const regexPartHost = "((?:open.lbry.com/|lbry.tv/|lbry.lat/|lbry.fr/|lbry.in/)?)"
|
||
|
const regexPartStreamOrChannelName = "([^:$#/]*)"
|
||
|
const regexPartModifierSeparator = "([:$#]?)([^/]*)"
|
||
|
const regexQueryStringBreaker = "^([\\S]+)([?][\\S]*)"
|
||
|
const urlComponentsSize = 9
|
||
|
|
||
|
const ChannelNameMinLength = 1
|
||
|
const ClaimIdMaxLength = 40
|
||
|
const ProtoDefault = "lbry://"
|
||
|
const RegexClaimId = "(?i)^[0-9a-f]+$"
|
||
|
const RegexInvalidUri = "(?i)[ =&#:$@%?;/\\\\\\\\\\\"<>%\\\\{\\\\}|^~\\\\[\\\\]`\\u0000-\\u0008\\u000b-\\u000c\\u000e-\\u001F\\uD800-\\uDFFF\\uFFFE-\\uFFFF]"
|
||
|
|
||
|
type LbryUri struct {
|
||
|
Path string
|
||
|
IsChannel bool
|
||
|
StreamName string
|
||
|
StreamClaimId string
|
||
|
ChannelName string
|
||
|
ChannelClaimId string
|
||
|
PrimaryClaimSequence int
|
||
|
SecondaryClaimSequence int
|
||
|
PrimaryBidPosition int
|
||
|
SecondaryBidPosition int
|
||
|
ClaimName string
|
||
|
ClaimId string
|
||
|
ContentName string
|
||
|
QueryString string
|
||
|
}
|
||
|
|
||
|
type UriModifier struct {
|
||
|
ClaimId string
|
||
|
ClaimSequence int
|
||
|
BidPosition int
|
||
|
}
|
||
|
|
||
|
func (uri LbryUri) IsChannelUrl() bool {
|
||
|
return (!isEmpty(uri.ChannelName) && isEmpty(uri.StreamName)) || (!isEmpty(uri.ClaimName) && strings.HasPrefix(uri.ClaimName, "@"))
|
||
|
}
|
||
|
|
||
|
func (uri LbryUri) IsNameValid(name string) bool {
|
||
|
return !regexp.MustCompile(RegexInvalidUri).MatchString(name)
|
||
|
}
|
||
|
|
||
|
func (uri LbryUri) String() string {
|
||
|
return uri.Build(true, ProtoDefault, false)
|
||
|
}
|
||
|
|
||
|
func (uri LbryUri) VanityString() string {
|
||
|
return uri.Build(true, ProtoDefault, true)
|
||
|
}
|
||
|
|
||
|
func (uri LbryUri) TvString() string {
|
||
|
return uri.Build(true, "https://lbry.tv/", false)
|
||
|
}
|
||
|
|
||
|
func (uri LbryUri) Build(includeProto bool, protocol string, vanity bool) string {
|
||
|
formattedChannelName := ""
|
||
|
if !isEmpty(uri.ChannelName) {
|
||
|
formattedChannelName = uri.ChannelName
|
||
|
if !strings.HasPrefix(formattedChannelName, "@") {
|
||
|
formattedChannelName = fmt.Sprintf("@%s", formattedChannelName)
|
||
|
}
|
||
|
}
|
||
|
primaryClaimName := uri.ClaimName
|
||
|
if isEmpty(primaryClaimName) {
|
||
|
primaryClaimName = uri.ContentName
|
||
|
}
|
||
|
if isEmpty(primaryClaimName) {
|
||
|
primaryClaimName = formattedChannelName
|
||
|
}
|
||
|
if isEmpty(primaryClaimName) {
|
||
|
primaryClaimName = uri.StreamName
|
||
|
}
|
||
|
|
||
|
primaryClaimId := uri.ClaimId
|
||
|
if isEmpty(primaryClaimId) {
|
||
|
if !isEmpty(formattedChannelName) {
|
||
|
primaryClaimId = uri.ChannelClaimId
|
||
|
} else {
|
||
|
primaryClaimId = uri.StreamClaimId
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var sb strings.Builder
|
||
|
if includeProto {
|
||
|
sb.WriteString(protocol)
|
||
|
}
|
||
|
sb.WriteString(primaryClaimName)
|
||
|
if vanity {
|
||
|
return sb.String()
|
||
|
}
|
||
|
|
||
|
secondaryClaimName := ""
|
||
|
if isEmpty(uri.ClaimName) && !isEmpty(uri.ContentName) {
|
||
|
secondaryClaimName = uri.ContentName
|
||
|
}
|
||
|
if isEmpty(secondaryClaimName) {
|
||
|
if !isEmpty(formattedChannelName) {
|
||
|
secondaryClaimName = uri.StreamName
|
||
|
}
|
||
|
}
|
||
|
secondaryClaimId := ""
|
||
|
if !isEmpty(secondaryClaimName) {
|
||
|
secondaryClaimId = uri.StreamClaimId
|
||
|
}
|
||
|
|
||
|
if !isEmpty(primaryClaimId) {
|
||
|
sb.WriteString("#")
|
||
|
sb.WriteString(primaryClaimId)
|
||
|
} else if uri.PrimaryClaimSequence > 0 {
|
||
|
sb.WriteString(":")
|
||
|
sb.WriteString(strconv.Itoa(uri.PrimaryClaimSequence))
|
||
|
} else if uri.PrimaryBidPosition > 0 {
|
||
|
sb.WriteString("$")
|
||
|
sb.WriteString(strconv.Itoa(uri.PrimaryBidPosition))
|
||
|
}
|
||
|
|
||
|
if !isEmpty(secondaryClaimName) {
|
||
|
sb.WriteString("/")
|
||
|
sb.WriteString(secondaryClaimName)
|
||
|
}
|
||
|
|
||
|
if !isEmpty(secondaryClaimId) {
|
||
|
sb.WriteString("#")
|
||
|
sb.WriteString(secondaryClaimId)
|
||
|
} else if uri.SecondaryClaimSequence > 0 {
|
||
|
sb.WriteString(":")
|
||
|
sb.WriteString(strconv.Itoa(uri.SecondaryClaimSequence))
|
||
|
} else if uri.SecondaryBidPosition > 0 {
|
||
|
sb.WriteString("$")
|
||
|
sb.WriteString(strconv.Itoa(uri.SecondaryBidPosition))
|
||
|
}
|
||
|
|
||
|
return sb.String()
|
||
|
}
|
||
|
|
||
|
func Parse(url string, requireProto bool) (*LbryUri, error) {
|
||
|
if isEmpty(url) {
|
||
|
return nil, errors.New("invalid url parameter")
|
||
|
}
|
||
|
|
||
|
reComponents := regexp.MustCompile(
|
||
|
fmt.Sprintf("(?i)%s%s%s%s(/?)%s%s",
|
||
|
regexPartProtocol,
|
||
|
regexPartHost,
|
||
|
regexPartStreamOrChannelName,
|
||
|
regexPartModifierSeparator,
|
||
|
regexPartStreamOrChannelName,
|
||
|
regexPartModifierSeparator))
|
||
|
reSeparateQueryString := regexp.MustCompile(regexQueryStringBreaker)
|
||
|
|
||
|
cleanUrl := url
|
||
|
queryString := ""
|
||
|
|
||
|
qsMatches := reSeparateQueryString.FindStringSubmatch(url)
|
||
|
if len(qsMatches) == 3 {
|
||
|
cleanUrl = qsMatches[1]
|
||
|
queryString = qsMatches[2][1:]
|
||
|
}
|
||
|
|
||
|
var components []string
|
||
|
componentMatches := reComponents.FindStringSubmatch(cleanUrl)
|
||
|
for _, component := range componentMatches[1:] {
|
||
|
components = append(components, component)
|
||
|
}
|
||
|
if len(components) != urlComponentsSize {
|
||
|
return nil, errors.New("regular expression error occurred while trying to Parse the value")
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* components[0] = proto
|
||
|
* components[1] = host
|
||
|
* components[2] = streamName or channelName
|
||
|
* components[3] = primaryModSeparator
|
||
|
* components[4] = primaryModValue
|
||
|
* components[5] = path separator
|
||
|
* components[6] = possibleStreamName
|
||
|
* components[7] = secondaryModSeparator
|
||
|
* components[8] = secondaryModValue
|
||
|
*/
|
||
|
if requireProto && isEmpty(components[0]) {
|
||
|
return nil, errors.New("url must include a protocol prefix (lbry://)")
|
||
|
}
|
||
|
if isEmpty(components[2]) {
|
||
|
return nil, errors.New("url does not include a name")
|
||
|
}
|
||
|
for _, component := range components[2:] {
|
||
|
if strings.Index(component, " ") > -1 {
|
||
|
return nil, errors.New("url cannot include a space")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
streamOrChannelName := components[2]
|
||
|
primaryModSeparator := components[3]
|
||
|
primaryModValue := components[4]
|
||
|
possibleStreamName := components[6]
|
||
|
secondaryModSeparator := components[7]
|
||
|
secondaryModValue := components[8]
|
||
|
primaryClaimId := ""
|
||
|
primaryClaimSequence := -1
|
||
|
primaryBidPosition := -1
|
||
|
secondaryClaimSequence := -1
|
||
|
secondaryBidPosition := -1
|
||
|
|
||
|
includesChannel := strings.HasPrefix(streamOrChannelName, "@")
|
||
|
isChannel := includesChannel && isEmpty(possibleStreamName)
|
||
|
channelName := ""
|
||
|
if includesChannel && len(streamOrChannelName) > 1 {
|
||
|
channelName = streamOrChannelName[1:]
|
||
|
}
|
||
|
|
||
|
// Convert the mod separators when parsing with protocol https://lbry.tv/ or similar
|
||
|
// [https://] uses ':', [lbry://] expects #
|
||
|
if !isEmpty(components[1]) {
|
||
|
if primaryModSeparator == ":" {
|
||
|
primaryModSeparator = "#"
|
||
|
}
|
||
|
if secondaryModSeparator == ":" {
|
||
|
secondaryModSeparator = "#"
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if includesChannel {
|
||
|
if isEmpty(channelName) {
|
||
|
// I wonder if this check is really necessary, considering the subsequent min length check
|
||
|
return nil, errors.New("no channel name after @")
|
||
|
}
|
||
|
if len(channelName) < ChannelNameMinLength {
|
||
|
return nil, errors.New(fmt.Sprintf("Channel names must be at least %d character long.", ChannelNameMinLength))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var err error
|
||
|
var primaryMod *UriModifier
|
||
|
var secondaryMod *UriModifier
|
||
|
if !isEmpty(primaryModSeparator) && !isEmpty(primaryModValue) {
|
||
|
primaryMod, err = parseModifier(primaryModSeparator, primaryModValue)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
primaryClaimId = primaryMod.ClaimId
|
||
|
primaryClaimSequence = primaryMod.ClaimSequence
|
||
|
primaryBidPosition = primaryMod.BidPosition
|
||
|
}
|
||
|
if !isEmpty(secondaryModSeparator) && !isEmpty(secondaryModValue) {
|
||
|
secondaryMod, err = parseModifier(secondaryModSeparator, secondaryModValue)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
secondaryClaimSequence = secondaryMod.ClaimSequence
|
||
|
secondaryBidPosition = secondaryMod.BidPosition
|
||
|
}
|
||
|
|
||
|
streamName := streamOrChannelName
|
||
|
if includesChannel {
|
||
|
streamName = possibleStreamName
|
||
|
}
|
||
|
|
||
|
streamClaimId := ""
|
||
|
if includesChannel && secondaryMod != nil {
|
||
|
streamClaimId = secondaryMod.ClaimId
|
||
|
} else if primaryMod != nil {
|
||
|
streamClaimId = primaryMod.ClaimId
|
||
|
}
|
||
|
channelClaimId := ""
|
||
|
if includesChannel && primaryMod != nil {
|
||
|
channelClaimId = primaryMod.ClaimId
|
||
|
}
|
||
|
|
||
|
return &LbryUri{
|
||
|
Path: strings.Join(components[2:], ""),
|
||
|
IsChannel: isChannel,
|
||
|
StreamName: streamName,
|
||
|
StreamClaimId: streamClaimId,
|
||
|
ChannelName: channelName,
|
||
|
ChannelClaimId: channelClaimId,
|
||
|
PrimaryClaimSequence: primaryClaimSequence,
|
||
|
SecondaryClaimSequence: secondaryClaimSequence,
|
||
|
PrimaryBidPosition: primaryBidPosition,
|
||
|
SecondaryBidPosition: secondaryBidPosition,
|
||
|
ClaimName: streamOrChannelName,
|
||
|
ClaimId: primaryClaimId,
|
||
|
ContentName: streamName,
|
||
|
QueryString: queryString,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func parseModifier(modSeparator string, modValue string) (*UriModifier, error) {
|
||
|
claimId := ""
|
||
|
claimSequence := 0
|
||
|
bidPosition := 0
|
||
|
|
||
|
if !isEmpty(modSeparator) {
|
||
|
if isEmpty(modValue) {
|
||
|
return nil, errors.New(fmt.Sprintf("No modifier provided after separator %s", modSeparator))
|
||
|
}
|
||
|
|
||
|
if modSeparator == "#" {
|
||
|
claimId = modValue
|
||
|
} else if modSeparator == ":" {
|
||
|
claimSequence = parseInt(modValue, -1)
|
||
|
} else if modSeparator == "$" {
|
||
|
bidPosition = parseInt(modValue, -1)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if !isEmpty(claimId) && (len(claimId) > ClaimIdMaxLength || !regexp.MustCompile(RegexClaimId).MatchString(claimId)) {
|
||
|
return nil, errors.New(fmt.Sprintf("Invalid claim ID %s", claimId))
|
||
|
}
|
||
|
if claimSequence == -1 {
|
||
|
return nil, errors.New("claim sequence must be a number")
|
||
|
}
|
||
|
if bidPosition == -1 {
|
||
|
return nil, errors.New("bid position must be a number")
|
||
|
}
|
||
|
|
||
|
return &UriModifier{
|
||
|
ClaimId: claimId,
|
||
|
ClaimSequence: claimSequence,
|
||
|
BidPosition: bidPosition,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func parseInt(value string, defaultValue int) int {
|
||
|
v, err := strconv.ParseInt(value, 10, 32)
|
||
|
if err != nil {
|
||
|
return defaultValue
|
||
|
}
|
||
|
return int(v)
|
||
|
}
|
||
|
|
||
|
func isEmpty(s string) bool {
|
||
|
return len(strings.TrimSpace(s)) == 0
|
||
|
}
|